react-native webview怎么加载前端打包出来的SPA静态文件

react-native webview怎么加载前端打包出来的SPA静态文件

前言

这是一次比较有意思的记录,关于这个react-native加载打包前端的出来的静态文件,网上找了很多文章,没有一个说清楚的,说的都是有点模糊,当我亲自去尝试,踩了比较多坑,先说说,为什么会有这种想法,为什么不用链接的方式引入呢,因为如果以链接的方式引入的时候,加载过程比较慢,而且会产生白屏,再者就是直接崩溃,网咯不行的时候,这时候就会给用户带来非常不好的体验。把前端的文件放到apk里面,直接加载本地,好处,前端文件都在本地,当没网的时候可以渲染骨架,也可以渲染出页面只是显示暂无数据,就不会有H5都是白屏的情况,再者加载本地的前端资源,比加载服务器资源的要快,虽然造成了APK包会偏大,但是用户的体验会好很多,这就是以空间换时间,从而达成目的。

而能够达到以上的需求,如果是我们传统的开发比如react,vue,进行开发,最后进行打包成apk,借助Hbuilderx工具,我有一篇文章就是介绍这个web打包成app的,除了这种方式,我们通常也会选择跨端的方式进行多端开发,如uniapp,react-native。 而unaipp通常也是借助Hbuilderx进行打包成apk,当然也可以打包成本地离线资源再借助android studio进行打包,如果这些方案不想要,你可以直接再android studio新建一个安卓项目,进行加载本地打包。而今天的主角,我用的是react-native进行加载本地资源打包,其实,底层还是安卓的打包,只是别人封装好了插件, 我直接使用。

抛出问题

项目中会遇到我们需要嵌套前端页面进入react-native工程,react-native的webview引入时会遇到一个问题,按照我们平时的引入方法发现,如果是单页面的引入不会有问题,但是如果是一个带有js,css,img这样的静态前端打包出来的静态资源引入时,我们会发现不是和我们想象的一样,他会加载不进来,显示的仅仅是html这个文件,那如何才能把他的静态引入正确导入我们的包里且正确显示在app呢?今天就带着大家解决这个困惑,轻松搞定静态引入h5到我们的webview里.

前端能否加载出来SPA呢?

官网给出的回复是这样的。

翻译一下

注意:这目前可能不起作用,如#428和#518中所述。可能的解决方法包括将所有资源与webpack或类似工具捆绑在一起,或运行本地Web服务器。 显示非工作方法有时,您会将HTML文件与应用程序捆绑在一起,并希望将HTML资源加载到您的WebView中。

没有说不行,可能不起作用,(下面会为你讲解),作者将会深入给你讲解。

作者的版本:

"react-native": "0.75.4",

"react-native-webview": "^13.12.3",

react-native-webview

为了达成以上需求,我们使用react-native-webview。在较新版本的React Native中不再默认包含webview组件,我们需要自己安装。

1. 安装

npm install react-native-webview

react-native link react-native-webview


我这里就不详细说面这个webview了,rn的webview安装配置api请参考(github.com/react-nativ…)

2. 使用

react-native-webview的使用非常简洁,就像使用任何一个react组件一样。

import React from 'react'
import { WebView } from 'react-native-webview'

const App = () => {
  return (
    <WebView
      source={{
        uri: 'https://www.juejin.cn'
      }}
      style={{
        flex: 1
      }}
    />
  )
}
export default App


3.加载前端SPA文件

1.rn工程根目录新建bundle文件夹

在RN项目根目录中创建public文件,public文件下创建Static.bundle文件夹

2.这个bundle文件夹里放置前端的所有静态文件

3.android配置

打开根目录/android/app/build.gradle,添加以下内容,这里只是把public下的所有的文件拷贝到安卓的src/main/assets目录下不包括public文件夹(注意)

android {
  …
  sourceSets {
    main {
      assets.srcDirs = ['src/main/assets','../../public']
    }
  }


4.RN页面加载

import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { WebView } from 'react-native-webview';
import { useWindowDimensions } from 'react-native';

const DetailsScreen = ({ route, navigation }: any) => {
  const { itemId, message } = route.params || {};
  const { width, height } = useWindowDimensions();

  const handleConsoleMessage = (event: any) => {
    console.log(event.nativeEvent.data); // 这里会打印你在 HTML 文件中 console.log 的内容
  }

  return (
    <View style={styles.container}>
      <Button title="Go back" onPress={() => navigation.goBack()} />
      <WebView
        originWhitelist={['*']}
        source={{ uri: 'file:///android_asset/Static.bundle/index.html' }} //这里只考虑安卓
        javaScriptEnabled={true}
        onMessage={handleConsoleMessage}
        domStorageEnabled={true}
        useWebKit={true}
        scalesPageToFit={false}
        allowFileAccess={true}
        style={{ width, height }}
        onError={(e) => console.log('Error loading HTML:', e.nativeEvent)}
        startInLoadingState={true}
        renderLoading={() => <View><Text>加载中...</Text></View>}
        onLoadStart={(e) => console.log('Loading URL:', e.nativeEvent.url)}
        onLoadEnd={() => console.log('HTML loaded')}
        allowsInlineMediaPlayback={true} // 允许嵌入的多媒体播放
        mixedContentMode="always" // 允许加载非 HTTPS 内容
        allowingReadAccessToURL="*"
        decelerationRate={'normal'}
        onHttpError={(syntheticEvent) => {
          const { nativeEvent } = syntheticEvent;
          console.log('HTTP Error: ', nativeEvent);
        }}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'red',
  },
  mainContent: {
    flexDirection: 'row',
    flex: 1,
    padding: 10,
  },
  leftPanel: {
    width: '45%',
    backgroundColor: '#123c76',
    padding: 15,
    borderRadius: 8,
    marginRight: 10,
  },
  attendanceBox: {
    alignItems: 'center',
    marginBottom: 20,
  },
  attendancelobo: {
    alignItems: 'center',
    marginBottom: 20,
  },
  attendanceText: {
    color: '#ffffff',
    fontSize: 24,
    fontWeight: 'bold',
  },
});

export default DetailsScreen;


5.主要容易出现的问题

1.加载的路径不对会出现这种情况

2.安卓内置的webview太低,无法解析js新的语法(会白屏)

如果浏览器能正常打开,而安卓不能打开,报错 Uncaught SyntaxError: Unexpected token (

查看目前安卓使用的webview偏低

三种解决方案,一种是升级安卓的内置的webview,一种是使用第三方的webview SDK进行加载如腾讯的TBS X5内核webview加载页面。还有一种方案就是前端进行打包ES降级别,到降ES5

3.前端的打包(会白屏)(大部分人都卡在了这里)

一个是一定是hash路由。 本地请求跨域。这个需要服务进行运行。

4.怎么判断前端的打包是否能够加载出来

本地无法加载出来

需要开启服务进行解析,这种采用webview直接加载是加载不出来的。

前端打出来的是一个静态资源包,本地路径直接访问就能加载出来,就说明直接加载就可以

这是我成功加载出来,因为设备的安卓webview太低,我用手机的。

5.开启一个本地服务也能解决

对于,无法解决打包问题的前端同学,我这边还有一个解决方案。 看这里,利用react-native-static-server,开启一个服务,官方推荐的一种方法。然后webview的地址为这个服务的地址就行了。

github.com/react-nativ…

参考:

github.com/birdofpreyr…

6.其它问题

如果是其它问题,只能通过调试看看浏览器报错。

3. 调试

最后来讲讲如何调试webview,毕竟前端程序员要是没有console.log基本和瞎了差不多

使用chrome调试android的webview

  1. 运行安卓模拟器,并进入webview页面
  2. 在chrome地址栏中输入并访问chrome://inspect/#devices
  3. 在页面中点击inspect按钮,即可打开控制台。

4. React Native与WebView通信

只显示内容显然不够用,我们经常有在RN与WebView间通信的需求。

先给我们的index.html添加一个节点

<body>
  ……
  <p id="run-first">empty</p>
</body>


3.1 React Native => WebView

1. injectedJavaScript

injectedJavaScript可以在webview加载完页面后执行js,注意传入的是字符串。在iOS上还必须加上onMessage属性,否则不能触发。重新build之后,就可以看到效果了。类似的,还有一个injectedJavaScriptBeforeContentLoaded属性,不同点是这个属性的方法会在页面加载前触发。

import { SafeAreaView, Platform } from 'react-native'
import WebView from 'react-native-webview'

const App = () => {
  const runFirst = `const el = document.getElementById('run-first');
  el.innerText = 'run first!'`

  return (
    <SafeAreaView
      style={{
        flex: 1,
        backgroundColor: '#fff',
      }}
    >
      <WebView
        source={{
          uri: `${
            Platform.OS === 'android' ? 'file:///android_asset/' : ''
          }Static.bundle/index_.html`,
        }}
        originWhitelist={['*']}
        style={{
          flex: 1,
        }}
        injectedJavaScript={runFirst}
        onMessage={() => {}}
      />
    </SafeAreaView>
  )
}

export default App


2. postMessage

postMessage可以让我们手动触发发送数据到webview,注意发送的数据必须为字符串类型,在webview中通过data字段接收。

在rn组件中创建refWebView绑定到WebView上,并通过调用postMessage向webview发送数据

import { SafeAreaView, Platform, View, Pressable, Text } from 'react-native'
import WebView from 'react-native-webview'
import { useRef } from 'react'

const App = () => {
  const runFirst = `const el = document.getElementById('run-first');
  el.innerText = 'run first!'`

  const refWebView = useRef<WebView | null>(null)

  const onPress = () => {
    refWebView.current.postMessage('message from react native')
  }

  return (
    <SafeAreaView
      style={{
        flex: 1,
        backgroundColor: '#fff',
      }}
    >
      <View>
        <Pressable onPress={onPress}>
          <Text>Post Message</Text>
        </Pressable>
      </View>
      <WebView
        ref={refWebView}
        source={{
          uri: `${
            Platform.OS === 'android' ? 'file:///android_asset/' : ''
          }Static.bundle/index_.html`,
        }}
        originWhitelist={['*']}
        style={{
          flex: 1,
        }}
        injectedJavaScript={runFirst}
        onMessage={() => {}}
      />
    </SafeAreaView>
  )
}



3.2 WebView => React Native

从WebView发送数据到RN,使用window.ReactNativeWebView.postMessage。同样的,只能发送字符串类型,rn组件中使用onMessage来接收。

index.html中新增一个send message按钮,绑定sendMessage方法。

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <p id="run-first">empty</p>
  <p>Hello,React Native WebView</p>
  <button onclick="sendMessage()">send Message</button>
</body>
<style>
  body {
    height: 100%;
    width: 100%;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
  }
</style>
<script>
  const sendMessage = () => {
    window.ReactNativeWebView.postMessage('message from webview')
  }
  document.addEventListener('message', (msg) => {
    const el = document.getElementById('run-first')
    el.innerText = msg.data
  })
  window.addEventListener('message', (msg) => {
    const el = document.getElementById('run-first')
    el.innerText = msg.data
  })
</script>
</html>


在rn组件中,使用onMessage监听,并通过设置状态直接反映到屏幕上。

import { SafeAreaView, Platform, View, Pressable, Text } from 'react-native'
import WebView from 'react-native-webview'
import { useRef, useState } from 'react'

const App = () => {
  const runFirst = `const el = document.getElementById('run-first');
  el.innerText = 'run first!'`

  const refWebView = useRef<WebView | null>(null)

  const onPress = () => {
    refWebView.current.postMessage('message from react native')
  }

  const [msgFromWV, setMsgFromWV] = useState('')

  return (
    <SafeAreaView
      style={{
        flex: 1,
        backgroundColor: '#fff',
      }}
    >
      <View>
        <Pressable onPress={onPress}>
          <Text>Post Message</Text>
        </Pressable>
        <Text>{msgFromWV}</Text>
      </View>
      <WebView
        ref={refWebView}
        source={{
          uri: `${
            Platform.OS === 'android' ? 'file:///android_asset/' : ''
          }Static.bundle/index_.html`,
        }}
        originWhitelist={['*']}
        style={{
          flex: 1,
        }}
        injectedJavaScript={runFirst}
        onMessage={event => {
          setMsgFromWV(event.nativeEvent.data)
        }}
      />
    </SafeAreaView>
  )
}

export default App


最后总结

遇到问题一定不要抓瞎,一定要找到问题所在,如果是常见问题报错,可以直接搜出来,如果还是解决不了,一定要会调试,根据调试出来的信息进行解决,看看官网,看看有没有人跟你遇到一样的问题。如果都没有,就针对你的报错问题,进行排除解决,因为如果是跨端开发,你要排除是安卓问题,还是后台问题,再看看是否是前端的问题,再去解决对应的前端问题,前端搞跨端一定要懂调试,调试,调试,不能抓瞎,不管搞什么,都应该懂调试!!!


作者:大前端helloworld
链接:https://juejin.cn/post/7436272415435915316

原文链接:,转发请注明来源!