在做头像挂件需求的时候遇到了这个问题,在查阅了MDN和RN文档后找到了一个(可能)比较好的解决办法。

为什么要嵌套网页

对于RN内嵌webview,大家肯定不陌生,使用这种方式主要是因为有些效果只能依靠网页来实现,没法指望Native的组件。比如比较复杂的渐变/图表等

img

这里的红框部分就是用了webview,采用三层layer做渐变。

对于RN,如何嵌套页面,我们可以直接使用Webview组件来实现

<WebView
  style={{...}}
  androidjavaScriptEnabled={true} 
  javaScriptEnabled={true} 
  scrollEnabled={false}
  source={{ uri: urlSource }}
  scalesPageToFit={false}
/>

考虑到RN业务往往还有降级页的需求,那么当降级页的情况下,webview页相应退化成了iframe

<iframe 
  scrolling="no"
  frameborder={0}
  src={urlSource}
  height={this.state.height}
  style={{...}}
/>

如何通信

上述的例子比较简单,因为内嵌页只负责纯静态展示。那么如果和内嵌页有交互,消息该如何传输?

举个例子,下图,点击设置同款头像,需要让页面跳转,这就涉及到一个子页面向宿主发送消息的问题。

img

RN内嵌webview

对于RN内嵌webview,官方提供了相关的props来进行页面交互

onMessage:接受一个回调函数,当子页面调用 window.postMessage时响应该消息,消息必须为字符串类型,所以选择JSON.stringify来序列化。

示例代码如下:

// 宿主方的回调 
onWebViewMessage = (e) => {
    const { data } = e.nativeEvent
    try {
        const { type, payload } = JSON.parse(data) || {}
    switch (type) {
      case 'XX':     
        doSth1(payload)
        break
      case 'YY':
        doSth2(payload)
        break
      default:
        break
    }
   } catch (e) {
   }
}

// 子页面的消息发送
postMessage = (obj = {type: 'XX', payload: 'foo'}) => {
    const json = JSON.stringify(obj)
    window.postMessage(json)
}

目前没啥问题,下面来解决降级页内嵌iframe的问题

降级页内嵌iframe

对于降级页的情况,发送消息的逻辑其实是换汤不换药,也是利用 postMessage,不过有一些区别

  • iframe发送消息用的是 parent.postMessage,而不是window.postMessage

  • parent.postMessage需要多一个参数target origin,由于需要跨域,所以origin是 ‘*’

  • 需要在组件生命周期钩子上手动添加与卸载message的监听

  • 收到的event类型和RN不同,RN获取data的方式是 e.nativeEvent.data,降级页则是e.data

  // 宿主方的监听
  componentDidMount() {
    if (!isRN) {
      window.addEventListener('message', this.onWebViewMessage)
    }
  }

  componentWillUnmount() {
    if (!isRN) {
      window.removeEventListener('message', this.onWebViewMessage)
    }
  }
  onWebViewMessage = (e) => {
    // 降级页和RN的e不一样
    const { data } = isRN ? e.nativeEvent : e
    // console.error(data)
    try {
      const { height, type } = JSON.parse(data) || {}
      switch (type) {
        case 'setHeight':
          this.setState({ height: height || 100 })
          break
        case 'openDecoration':
          via.app.openSchema({
            reactId: this.props.context.reactId,
            schema: genDecSchema(this.props.context.decId),
          })
          break
        case 'openStar':
          via.app.openSchema({
            reactId: this.props.context.reactId,
            schema: genStarSchema(this.props.context.uid),
          })
          break
        default:
          break
      }
    } catch (e) {
    }
  }

  // 子页面的消息发送
  postMessage = (obj = {type: 'XX', payload: 'foo'}) => {
    const json = JSON.stringify(obj)
    parent.postMessage(json, '*')
  }

同时处理两种case

但是紧接着就会出现这样的问题:子页面本身对自己所处在什么环境是不知道的。那么该如何选择postMessage的方式呢?

利用webview注入JS

这里笔者想了一个笨办法,利用绑定在全局的变量标示一下宿主环境即可。很幸运,RN提供了这样的方案:injectedJavaScript

Set this to provide JavaScript that will be injected into the web page when the view loads.

这个props接受一串JS代码的字符串。子页面加载时将其执行。

那么我们只设一个全局变量标示宿主是RN即可。这里用了parentIsRN

  <WebView
    style={{ ... }}
    androidjavaScriptEnabled={true} 
    javaScriptEnabled={true} 
    scrollEnabled={false}
    injectedJavaScript="window.parentIsRN = true;"
    source={{ uri: urlSource }}
    onMessage={this.onWebViewMessage}
  />

剩下的事情就简单了,子页面根据有没有这个全局变量即可判断所处环境

postMessage = (obj = {type: 'XX', payload: 'foo'}) => {
  const json = JSON.stringify(obj)
  if (window.parentIsRN) {
    window.postMessage(json)
  } else {
    parent.postMessage(json, '*')
  }
}

通过url传参

大多数情况下,我们往往需要对降级页通过url传入一些参数。

// 宿主页编码参数
const queryUrl = Object.keys(queryObj).map(key => { 
  const value = queryObj[key]
  return `${key}=${value}`
}).join('&')

// 内嵌页解码参数
function getQueryVariable(variable) {
  var query = window.location.search.substring(1);
  var vars = query.split('&');
  const queryObj = {}
  for (var i = 0; i < vars.length; i++) {
      var pair = vars[i].split('=');
      queryObj[pair[0]] = decodeURIComponent(pair[1])
  }
  return queryObj
}

既然如此,那我们直接把 parentIsRN 作为url参数传过去就行了。

当然有一种情况不适用 ,就是如果url本来没有参数的话,那就比较麻烦了,还要加上解析url参数的逻辑。

参考资料

RN官方文档:https://reactnative.dev/docs/webview#__docusaurus

MDN:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage

掘金:https://juejin.im/post/590c3983ac502e006531df11



经验分享      RN

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!