import { forwardRef, useRef, useContext, useMemo, useState } from 'react' import { warn, isFunction } from '@mpxjs/utils' import Portal from './mpx-portal/index' import { usePreventRemove, PreventRemoveEvent } from '@react-navigation/native' import { getCustomEvent } from './getInnerListeners' import { promisify, redirectTo, navigateTo, navigateBack, reLaunch, switchTab } from '@mpxjs/api-proxy' import { WebView } from 'react-native-webview' import useNodesRef, { HandlerRef } from './useNodesRef' import { getCurrentPage, useNavigation } from './utils' import { WebViewHttpErrorEvent, WebViewEvent, WebViewMessageEvent, WebViewNavigation, WebViewProgressEvent } from 'react-native-webview/lib/WebViewTypes' import { RouteContext } from './context' import { StyleSheet, View, Text } from 'react-native' type OnMessageCallbackEvent = { detail: { data: any[] } } type CommonCallbackEvent = { detail: { src?: string } } interface WebViewProps { src?: string bindmessage?: (event: OnMessageCallbackEvent) => void bindload?: (event: CommonCallbackEvent) => void binderror?: (event: CommonCallbackEvent) => void [x: string]: any } type Listener = (type: string, callback: (e: Event) => void) => () => void interface PayloadData { [x: string]: any } type MessageData = { payload?: PayloadData, args?: Array, type?: string, callbackId?: number } type LanguageCode = 'zh-CN' | 'en-US'; // 支持的语言代码 interface ErrorText { text: string; button: string; } type ErrorTextMap = Record const styles = StyleSheet.create({ loadErrorContext: { display: 'flex', alignItems: 'center' }, loadErrorText: { fontSize: 12, color: '#666666', paddingTop: '40%', paddingBottom: 20, paddingLeft: '10%', paddingRight: '10%', textAlign: 'center' }, loadErrorButton: { color: '#666666', textAlign: 'center', padding: 10, borderColor: '#666666', borderStyle: 'solid', borderWidth: StyleSheet.hairlineWidth, borderRadius: 10 } }) const _WebView = forwardRef, WebViewProps>((props, ref): JSX.Element | null => { const { src, bindmessage, bindload, binderror } = props const mpx = global.__mpx const errorText: ErrorTextMap = { 'zh-CN': { text: '网络不可用,请检查网络设置', button: '重新加载' }, 'en-US': { text: 'The network is not available. Please check the network settings', button: 'Reload' } } const currentErrorText = errorText[(mpx.i18n?.locale as LanguageCode) || 'zh-CN'] if (props.style) { warn('The web-view component does not support the style prop.') } const { pageId } = useContext(RouteContext) || {} const [pageLoadErr, setPageLoadErr] = useState(false) const currentPage = useMemo(() => getCurrentPage(pageId), [pageId]) const webViewRef = useRef(null) const fristLoaded = useRef(false) const isLoadError = useRef(false) const isNavigateBack = useRef(false) const statusCode = useRef('') const defaultWebViewStyle = { position: 'absolute' as const, left: 0, right: 0, top: 0, bottom: 0 } const navigation = useNavigation() const [isIntercept, setIsIntercept] = useState(false) usePreventRemove(isIntercept, (event: PreventRemoveEvent) => { const { data } = event if (isNavigateBack.current) { navigation?.dispatch(data.action) } else { webViewRef.current?.goBack() } isNavigateBack.current = false }) useNodesRef(props, ref, webViewRef, { style: defaultWebViewStyle }) const getHostFromUrl = function (url: string): string { if (!url) return '' // 匹配协议://主机名(:端口) 的模式 const regex = /^(?:https?|ftp):\/\/([^/?:#]+)(?::(\d+))?/i const match = url.match(regex) return match ? match[1] : '' } const hostValidate = (url: string) => { const host = url && getHostFromUrl(url) const hostWhitelists = mpx.config.rnConfig?.webviewConfig?.hostWhitelists || [] if (hostWhitelists.length) { return hostWhitelists.some((item: string) => { return host.endsWith(item) }) } else { return true } } if (!src) { return null } if (!hostValidate(src)) { console.error('访问页面域名不符合domainWhiteLists白名单配置,请确认是否正确配置该域名白名单') return null } const _reload = function () { if (__mpx_mode__ !== 'ios') { fristLoaded.current = false // 安卓需要重新设置 } setPageLoadErr(false) } const injectedJavaScript = ` if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) { var _documentTitle = document.title; window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'setTitle', payload: { _documentTitle: _documentTitle } })) Object.defineProperty(document, 'title', { set (val) { _documentTitle = val window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'setTitle', payload: { _documentTitle: _documentTitle } })) }, get () { return _documentTitle } }); } true; ` const sendMessage = function (params: string) { return ` window.mpxWebviewMessageCallback && window.mpxWebviewMessageCallback(${params}) true; ` } const _changeUrl = function (navState: WebViewNavigation) { if (navState.navigationType) { // navigationType这个事件在页面开始加载时和页面加载完成时都会被触发所以判断这个避免其他无效触发执行该逻辑 currentPage.__webViewUrl = navState.url setIsIntercept(navState.canGoBack) } } const _onLoadProgress = function (event: WebViewProgressEvent) { if (__mpx_mode__ !== 'ios') { setIsIntercept(event.nativeEvent.canGoBack) } } const _message = function (res: WebViewMessageEvent) { if (!hostValidate(res.nativeEvent?.url)) { return } let data: MessageData = {} let asyncCallback const navObj = promisify({ redirectTo, navigateTo, navigateBack, reLaunch, switchTab }) try { const nativeEventData = res.nativeEvent?.data if (typeof nativeEventData === 'string') { data = JSON.parse(nativeEventData) } } catch (e) {} const args = data.args const postData: PayloadData = data.payload || {} const params = Array.isArray(args) ? args : [postData] const type = data.type switch (type) { case 'setTitle': { // case下不允许直接声明,包个块解决该问题 const title = postData._documentTitle?.trim() if (title !== undefined) { navigation && navigation.setPageConfig({ navigationBarTitleText: title }) } } break case 'postMessage': bindmessage && bindmessage(getCustomEvent('message', {}, { // RN组件销毁顺序与小程序不一致,所以改成和支付宝消息一致 detail: { data: params[0]?.data } })) asyncCallback = Promise.resolve({ errMsg: 'invokeWebappApi:ok' }) break case 'navigateTo': asyncCallback = navObj.navigateTo(...params) break case 'navigateBack': isNavigateBack.current = true asyncCallback = navObj.navigateBack(...params) break case 'redirectTo': asyncCallback = navObj.redirectTo(...params) break case 'switchTab': asyncCallback = navObj.switchTab(...params) break case 'reLaunch': asyncCallback = navObj.reLaunch(...params) break default: if (type) { const implement = mpx.config.rnConfig.webviewConfig && mpx.config.rnConfig.webviewConfig.apiImplementations && mpx.config.rnConfig.webviewConfig.apiImplementations[type] if (isFunction(implement)) { asyncCallback = Promise.resolve(implement(...params)) } else { /* eslint-disable prefer-promise-reject-errors */ asyncCallback = Promise.reject({ errMsg: `未在apiImplementations中配置${type}方法` }) } } break } asyncCallback && asyncCallback.then((res: any) => { if (webViewRef.current?.postMessage) { const result = JSON.stringify({ type, callbackId: data.callbackId, result: res }) webViewRef.current.injectJavaScript(sendMessage(result)) } }).catch((error: any) => { if (webViewRef.current?.postMessage) { const result = JSON.stringify({ type, callbackId: data.callbackId, error }) webViewRef.current.injectJavaScript(sendMessage(result)) } }) } const onLoadEndHandle = function (res: WebViewEvent) { fristLoaded.current = true const src = res.nativeEvent?.url if (isLoadError.current) { isLoadError.current = false isNavigateBack.current = false const result = { type: 'error', timeStamp: res.timeStamp, detail: { src, statusCode: statusCode.current } } binderror && binderror(result) } else { const result = { type: 'load', timeStamp: res.timeStamp, detail: { src } } bindload?.(result) } } const onLoadEnd = function (res: WebViewEvent) { if (__mpx_mode__ !== 'ios') { res.persist() setTimeout(() => { onLoadEndHandle(res) }, 0) } else { onLoadEndHandle(res) } } const onHttpError = function (res: WebViewHttpErrorEvent) { isLoadError.current = true statusCode.current = res.nativeEvent?.statusCode } const onError = function () { statusCode.current = '' isLoadError.current = true if (!fristLoaded.current) { setPageLoadErr(true) } } return ( {pageLoadErr ? ( {currentErrorText.text} {currentErrorText.button} ) : ( )} ) }) _WebView.displayName = 'MpxWebview' export default _WebView