在做头像挂件需求的时候遇到了这个问题,在查阅了MDN和RN文档后找到了一个(可能)比较好的解决办法。
为什么要嵌套网页
对于RN内嵌webview,大家肯定不陌生,使用这种方式主要是因为有些效果只能依靠网页来实现,没法指望Native的组件。比如比较复杂的渐变/图表等
这里的红框部分就是用了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={{...}}
/>
如何通信
上述的例子比较简单,因为内嵌页只负责纯静态展示。那么如果和内嵌页有交互,消息该如何传输?
举个例子,下图,点击设置同款头像,需要让页面跳转,这就涉及到一个子页面向宿主发送消息的问题。
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
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!