# 导语
在 React Native 项目中加载 Web 页面时,如果直接使用网络 URL,不仅需要等待资源加载,还可能面临网络不稳定导致的页面空白问题。将 Web 资源打包到 App 本地,可以实现秒开体验,大幅提升用户体验。
本文总结了在 React Native 中加载本地 Web 静态资源的完整方案,包含打包配置、RN 集成、RN 与 Web 通信等核心内容。
# 一、Next.js 打包配置
# 1.1 打包命令
首先需要修改 package.json 中的打包命令:
"build:app": "cross-env APP_MODE=1 next build && sh ./scripts/buildAfter.sh"
@前端进阶之旅: 代码已经复制到剪贴板
# 1.2 构建后处理脚本
创建 scripts/buildAfter.sh 脚本,处理构建产物:
cd dist
# 重命名,避免在安卓端加载不出来
mv ./_next ./next
# 替换代码中的 _next 为 next
grep -rli '_next' * | xargs -I@ sed -i '' 's/_next/next/g' @
@前端进阶之旅: 代码已经复制到剪贴板
为什么要重命名?
在安卓端,assets 目录下的文件以下划线开头可能会导致加载失败。将 _next 重命名为 next 可以避免这个问题。
# 二、Next.js 端接收数据
# 2.1 获取注入参数
通过 window.injectParams 获取 React Native WebView 注入的参数:
// 获取 react-native webview 注入的 window.injectParams 参数
export const getInjectParams = () => {
const query = typeof window !== 'undefined' ? window?.injectParams || {} : {}
return query as InjectParams
}
@前端进阶之旅: 代码已经复制到剪贴板
# 2.2 监听 RN 消息
实现 RN 与 Web 双向通信,接收来自 RN 的消息:
useEffect(() => {
if (process.env.APP_MODE !== '1') return
const messageHandler = (e: any) => {
const data = e?.data ? JSON.parse(e?.data) : undefined
const type = data?.type // 消息类型
const payload = data?.payload // 消息内容
// 处理行情同步
if (type === 'syncQuote') {
ws.syncUpdateRNKlineData(payload)
}
// 处理品种切换
if (type === 'changeSymbol' && payload?.symbol) {
stores.global.setSymbolInfo(payload)
stores.ws.lastbar = {} // 重置上一根 K 线
mitt.emit('symbol_change')
const symbolName = payload?.symbol
setTimeout(() => {
if (ws.tvWidget) {
ws.tvWidget.onChartReady(() => {
ws.tvWidget.activeChart().resetData()
ws.tvWidget.activeChart().setSymbol(symbolName, {
dataReady: () => {
console.log('切换品种成功')
}
})
})
}
}, 100)
}
}
// iOS 使用 window 监听,Android 使用 document 监听
if (isAndroid) {
document.addEventListener('message', messageHandler)
} else {
window.addEventListener('message', messageHandler)
}
return () => {
if (isAndroid) {
document.removeEventListener('message', messageHandler)
} else {
window.removeEventListener('message', messageHandler)
}
}
}, [])
@前端进阶之旅: 代码已经复制到剪贴板
# 三、集成到 React Native
# 3.1 Android 配置
修改 android/app/build.gradle:
android {
sourceSets {
main {
// 把项目根目录 public 下的所有文件拷贝到安卓的 src/main/assets 目录下
assets.srcDirs = ['src/main/assets', '../../app/public']
}
}
}
@前端进阶之旅: 代码已经复制到剪贴板
# 3.2 iOS 配置
将打包后的 bundle 文件添加到 Xcode 项目中:



# 3.3 WebView 组件实现
核心代码实现 RN 加载本地 Web 资源:
import WebView from 'react-native-webview'
function Tradingview() {
const webviewRefs = useRef<any>(null)
const { symbol, dataSourceCode, dataSourceSymbol, accountGroupId } = useParams()
const { theme, locale } = useTheme()
// 本地 bundle 路径
const sourceUri = Platform.OS === 'ios'
? 'Tradingview.bundle/index.html'
: 'file:///android_asset/Tradingview.bundle/index.html'
// 注入 JavaScript 参数
const injectedJavaScript = `
window.injectParams = {
'symbolName': '${symbol}',
'dataSourceCode': '${dataSourceCode}',
'dataSourceSymbol': '${dataSourceSymbol}',
'accountGroupId': '${accountGroupId}',
'locale': '${locale}',
'colorType': '${theme.direction + 1}',
'token': '${token}',
'baseUrl': '${baseUrl}',
'wsUrl': '${wsUrl}',
'symbolInfo': ${JSON.stringify(symbolInfo)},
'debug': ${__DEV__},
'watermarkLogoUrl': '${watermarkLogoUrl}',
};
true;
`
// 切换品种
const switchSymbol = useCallback(() => {
const message = JSON.stringify({
type: 'changeSymbol',
payload: getSymbolInfo()
})
webviewRefs?.current?.postMessage?.(message)
}, [symbol])
// 同步行情数据
useEffect(() => {
if (currentQuote) {
const message = JSON.stringify({
type: 'syncQuote',
payload: currentQuote
})
webviewRefs?.current?.postMessage?.(message)
}
}, [currentQuote])
return (
<WebView
source={{ uri: sourceUri }}
injectedJavaScript={injectedJavaScript}
javaScriptEnabled={true}
allowFileAccess={true}
allowFileAccessFromFileURLs={true}
originWhitelist={['*']}
scalesPageToFit={false}
scrollEnabled={false}
domStorageEnabled={true}
mixedContentMode="always"
onMessage={(event) => {
// iOS 必须加上 onMessage 否则加载不出本地资源
console.log('event.nativeEvent.data', event.nativeEvent.data)
}}
/>
)
}
@前端进阶之旅: 代码已经复制到剪贴板
# 3.4 应用前后台处理
处理 App 进入前台/后台时的逻辑:
import useAppState from '@/hooks/useAppState'
const checkTradingviewReload = async () => {
// 缓存时间大于 5 分钟,reload 整个实例
const updateTime = await STORAGE_GET_TRADINGVIEW_RELOAD_TIME()
if ((updateTime && Date.now() - updateTime > 5 * 60 * 1000) || !updateTime) {
STORAGE_SET_TRADINGVIEW_RELOAD_TIME(Date.now())
return true
}
return false
}
useAppState(
async () => {
// 应用回到前台
const shouldReload = await checkTradingviewReload()
if (shouldReload && webviewRefs?.current) {
// 刷新整个 WebView,避免长时间不进入导致页面空白
webviewRefs?.current?.reload?.()
} else {
// 不刷新页面,只切换品种
switchSymbol()
}
},
() => {
// 应用进入后台
STORAGE_SET_TRADINGVIEW_RELOAD_TIME(Date.now())
// 清空缓存,否则绘制有问题
ws.quotes = new Map()
}
)
@前端进阶之旅: 代码已经复制到剪贴板
# 四、关键配置说明
# 4.1 WebView 重要属性
| 属性 | 说明 |
|---|---|
source.uri |
本地资源路径(iOS/Android 不同) |
injectedJavaScript |
注入 JS 参数到 Web 页面 |
javaScriptEnabled |
启用 JavaScript |
allowFileAccess |
允许通过 file:// 形式加载资源 |
scalesPageToFit |
禁止页面缩放 |
domStorageEnabled |
启用 DOM 存储 |
mixedContentMode |
允许加载非 HTTPS 内容 |