00
其实我这篇文章的目的并不完全是要解决这个问题。而是想通过这个问题来简单讲讲React Native
组件和Android
原生控件的一个关系,以及如何通过看源码来排查解决React Native
中Android
机型遇到的问题的思路,当然iOS的思路也是大同小异的。ps:总结在最后。
需求背景:有一个文章详情是以html富文本的方式存在后台数据库的,现在需要
React Native
用WebView
来展示。而且在这个详情页头部是有除WebView
以外的组件。这个时候富文本里有一个<a>
标签跳转链接,需要另外打开一个页面来承载这个链接。
01
知道了这个需要,我们第一反应肯定是先去看文档,http://reactnative.cn/
中文网里WebView
章节里有这么一个方法onShouldStartLoadWithRequest
(允许为WebView
发起的请求运行一个自定义的处理函数。返回true或false表示是否要继续执行响应的请求。),但是....重点在但是,这个方法只有iOS
有。
作为一个Android
开发人员我就有点不理解了,Android WebView
明明有类似的方法回调shouldOverrideUrlLoading
,不是号称React Native
调用的就是原生的控件吗,为什么不提供呢?
02
接下来,我就去翻看了源码(以0.48版本为例)。node_modules/react-native/android/com/facebook/react/react-native/0.48.3/react-native-0.48.3-source.jar
这个包里,有个类ReactWebViewManager.java
,这就是facebook
开发人员封装的给RN
用的WebView
了。找到WebView
的shouldOverrideUrlLoading
方法。源码如下
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("http://") || url.startsWith("https://") ||
url.startsWith("file://") || url.equals("about:blank")) {
return false;
} else {
try {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
view.getContext().startActivity(intent);
} catch (ActivityNotFoundException e) {
FLog.w(ReactConstants.TAG, "activity not found to handle uri scheme for: " + url, e);
}
return true;
}
}
从源码来看,确实没有抛出事件给RN,个人分析原因,应该是因为Android和RN之间没有一个比较好的同步通信机制,至少官方文档里提到的通信方式都是异步的。所以这个地方暂时没有封装出去给RN来决定。
03
看到这里,其实已经发现了问题原因所在了。
接下来就是考虑怎么解决了。
这里我提供两个思路吧。
思路1:从Android这边入手。【强烈推荐】
既然官方提供的
WebView
没有提供方法,那我们完全可以自己封装一个WebView给RN用撒,RN那边设置一个参数是否需要shouldOverrideUrlLoading={true},Android这边接收这个参数,如果判断为true就在Android的shouldOverrideUrlLoading回调里将事件dispatchEvent分发给RN,RN那边写个回调就好啦,其实我觉得官方也可以这么来写。 后续我再写一篇文章,详细讲述这个编码过程。
思路2:从RN JS那边入手。
利用RN WebView的
injectedJavaScript
属性,给WebView注入一段js代码,拦截所有<a>标签的跳转,并将事件和即将跳转的url通过postMessage的方式回调给RN,这样就可以啦,以下是代码片段。这个方案其实不是一个保险的解决方案,因为看Android
源码可以看到,injectedJavaScript
是在onPageFinish里回调的,而这个回调在Android本身是有适配问题的,有时候是不会回调的,比如网页里某个css、js文件没下载下来,会一直卡住,以至于不会回调结束。所以还是推荐第一种方案。
renden的定义,【这里其实还实现WebView
的高度自适应】
render() {
const _w = this.props.width || Dimensions.get('window').width;
const _h = this.props.autoHeight ? this.state.webViewHeight : this.props.defaultHeight;
return <WebView
injectedJavaScript={'(' + String(injectedScript) + ')();'}
scrollEnabled={this.props.scrollEnabled || false}
onMessage={this._onMessage}
javaScriptEnabled={true}
automaticallyAdjustContentInsets={true}
renderLoading={this._loadingView}
{...this.props}
style={[{width: _w}, this.props.style, {height: _h}]}
/>
}
注入的js
const injectedScript = function () {
function awaitPostMessage() {
var isReactNativePostMessageReady = !!window.originalPostMessage;
var queue = [];
var currentPostMessageFn = function store(message) {
if (queue.length > 100) queue.shift();
queue.push(message);
};
if (!isReactNativePostMessageReady) {
var originalPostMessage = window.postMessage;
Object.defineProperty(window, 'postMessage', {
configurable: true,
enumerable: true,
get: function () {
return currentPostMessageFn;
},
set: function (fn) {
currentPostMessageFn = fn;
isReactNativePostMessageReady = true;
setTimeout(sendQueue, 0);
}
});
window.postMessage.toString = function () {
return String(originalPostMessage);
};
}
function sendQueue() {
while (queue.length > 0) window.postMessage(queue.shift());
}
}
awaitPostMessage(); // Call this only once in your Web Code.
//至此,是为了保证一定会调成功postMessage
var originalPostMessage = window.postMessage;
var patchedPostMessage = function (message, targetOrigin, transfer) {
originalPostMessage(message, targetOrigin, transfer);
};
patchedPostMessage.toString = function () {
return String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage');
};
window.postMessage = patchedPostMessage;
let height;
if (document.documentElement.clientHeight > document.body.clientHeight) {
height = document.documentElement.clientHeight
} else {
height = document.body.clientHeight
}
window.postMessage("height=" + height); //这里是把网页内容高度传给rn,以实现自适应高度
//以下就是找到所有a标签,并将url传给RN处理
var aNodes = document.getElementsByTagName('a');
for (var i = 0; i < aNodes.length; i++) {
aNodes[i].onclick = function (e) {
e.preventDefault();//这句话是阻止a标签跳转
window.postMessage("url=" + e.target.href)
}
}
};
onMessage的处理
_onMessage(e) {
let data = e.nativeEvent.data;
if (data.slice(0, 7) == 'height=') {
let height = data.substring(7, data.length)
this.setState({
webViewHeight: parseInt(height)
});
} else if (data.slice(0, 4) == 'url=') {
let url = data.substring(4, data.length)
//处理拦截的a标签事件
...
}
}
04
最后,做个首尾呼应。我们来简单总结下React Native
组件和Android
原生控件的一个关系。
通过上面这个案例分析,我们可以清晰的看到RN
是做了一个 用js
来调用原生控件的一个伟大事情,并在js
端以组件的方式来使用,但是这个原生控件是经过了一定封装的,并不是将所有原生控件的属性方法都暴露给js
端。这里就会有很大的坑,因为Android
的适配很多时候是一个经验工作,再加上国内很多手机厂商都有自己的修改过的ROM
,这就导致facebook
的开发人员在封装控件的时候可能并不能完全考虑该控件的适配问题以及使用场景,就会出现纯js
不能直接解决的问题。具体例子我就不再列举了,同理于iOS
。
所以,React Native
固然好,但是也有一定的局限,他的发展之所以到现在还在0.48版本,也是有一定道理的。
当然,RN
的好处也很多的,提高了业务的编码效率,让更多的web
前端开发也能写App
等等,最最重要的我觉得还是可以做到跨平台以及热更新。
05
至此!
感谢阅读!