
00
其實(shí)我這篇文章的目的并不完全是要解決這個(gè)問(wèn)題。而是想通過(guò)這個(gè)問(wèn)題來(lái)簡(jiǎn)單講講React Native組件和Android原生控件的一個(gè)關(guān)系,以及如何通過(guò)看源碼來(lái)排查解決React Native中Android機(jī)型遇到的問(wèn)題的思路,當(dāng)然iOS的思路也是大同小異的。ps:總結(jié)在最后。
需求背景:有一個(gè)文章詳情是以html富文本的方式存在后臺(tái)數(shù)據(jù)庫(kù)的,現(xiàn)在需要
React Native用WebView來(lái)展示。而且在這個(gè)詳情頁(yè)頭部是有除WebView以外的組件。這個(gè)時(shí)候富文本里有一個(gè)<a>標(biāo)簽跳轉(zhuǎn)鏈接,需要另外打開(kāi)一個(gè)頁(yè)面來(lái)承載這個(gè)鏈接。
01
知道了這個(gè)需要,我們第一反應(yīng)肯定是先去看文檔,http://reactnative.cn/中文網(wǎng)里WebView章節(jié)里有這么一個(gè)方法onShouldStartLoadWithRequest(允許為WebView發(fā)起的請(qǐng)求運(yùn)行一個(gè)自定義的處理函數(shù)。返回true或false表示是否要繼續(xù)執(zhí)行響應(yīng)的請(qǐng)求。),但是....重點(diǎn)在但是,這個(gè)方法只有iOS有。
作為一個(gè)Android開(kāi)發(fā)人員我就有點(diǎn)不理解了,Android WebView明明有類似的方法回調(diào)shouldOverrideUrlLoading,不是號(hào)稱React Native調(diào)用的就是原生的控件嗎,為什么不提供呢?
02
接下來(lái),我就去翻看了源碼(以0.48版本為例)。node_modules/react-native/android/com/facebook/react/react-native/0.48.3/react-native-0.48.3-source.jar這個(gè)包里,有個(gè)類ReactWebViewManager.java,這就是facebook開(kāi)發(fā)人員封裝的給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;
}
}
從源碼來(lái)看,確實(shí)沒(méi)有拋出事件給RN,個(gè)人分析原因,應(yīng)該是因?yàn)锳ndroid和RN之間沒(méi)有一個(gè)比較好的同步通信機(jī)制,至少官方文檔里提到的通信方式都是異步的。所以這個(gè)地方暫時(shí)沒(méi)有封裝出去給RN來(lái)決定。
03
看到這里,其實(shí)已經(jīng)發(fā)現(xiàn)了問(wèn)題原因所在了。
接下來(lái)就是考慮怎么解決了。
這里我提供兩個(gè)思路吧。
思路1:從Android這邊入手。【強(qiáng)烈推薦】
既然官方提供的
WebView沒(méi)有提供方法,那我們完全可以自己封裝一個(gè)WebView給RN用撒,RN那邊設(shè)置一個(gè)參數(shù)是否需要shouldOverrideUrlLoading={true},Android這邊接收這個(gè)參數(shù),如果判斷為true就在Android的shouldOverrideUrlLoading回調(diào)里將事件dispatchEvent分發(fā)給RN,RN那邊寫個(gè)回調(diào)就好啦,其實(shí)我覺(jué)得官方也可以這么來(lái)寫。 后續(xù)我再寫一篇文章,詳細(xì)講述這個(gè)編碼過(guò)程。
思路2:從RN JS那邊入手。
利用RN WebView的
injectedJavaScript屬性,給WebView注入一段js代碼,攔截所有<a>標(biāo)簽的跳轉(zhuǎn),并將事件和即將跳轉(zhuǎn)的url通過(guò)postMessage的方式回調(diào)給RN,這樣就可以啦,以下是代碼片段。這個(gè)方案其實(shí)不是一個(gè)保險(xiǎn)的解決方案,因?yàn)榭?code>Android源碼可以看到,injectedJavaScript是在onPageFinish里回調(diào)的,而這個(gè)回調(diào)在Android本身是有適配問(wèn)題的,有時(shí)候是不會(huì)回調(diào)的,比如網(wǎng)頁(yè)里某個(gè)css、js文件沒(méi)下載下來(lái),會(huì)一直卡住,以至于不會(huì)回調(diào)結(jié)束。所以還是推薦第一種方案。
renden的定義,【這里其實(shí)還實(shí)現(xiàn)WebView的高度自適應(yīng)】
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.
//至此,是為了保證一定會(huì)調(diào)成功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); //這里是把網(wǎng)頁(yè)內(nèi)容高度傳給rn,以實(shí)現(xiàn)自適應(yīng)高度
//以下就是找到所有a標(biāo)簽,并將url傳給RN處理
var aNodes = document.getElementsByTagName('a');
for (var i = 0; i < aNodes.length; i++) {
aNodes[i].onclick = function (e) {
e.preventDefault();//這句話是阻止a標(biāo)簽跳轉(zhuǎn)
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標(biāo)簽事件
...
}
}
04
最后,做個(gè)首尾呼應(yīng)。我們來(lái)簡(jiǎn)單總結(jié)下React Native組件和Android原生控件的一個(gè)關(guān)系。
通過(guò)上面這個(gè)案例分析,我們可以清晰的看到RN是做了一個(gè) 用js來(lái)調(diào)用原生控件的一個(gè)偉大事情,并在js端以組件的方式來(lái)使用,但是這個(gè)原生控件是經(jīng)過(guò)了一定封裝的,并不是將所有原生控件的屬性方法都暴露給js端。這里就會(huì)有很大的坑,因?yàn)?code>Android的適配很多時(shí)候是一個(gè)經(jīng)驗(yàn)工作,再加上國(guó)內(nèi)很多手機(jī)廠商都有自己的修改過(guò)的ROM,這就導(dǎo)致facebook的開(kāi)發(fā)人員在封裝控件的時(shí)候可能并不能完全考慮該控件的適配問(wèn)題以及使用場(chǎng)景,就會(huì)出現(xiàn)純js不能直接解決的問(wèn)題。具體例子我就不再列舉了,同理于iOS。
所以,React Native固然好,但是也有一定的局限,他的發(fā)展之所以到現(xiàn)在還在0.48版本,也是有一定道理的。
當(dāng)然,RN的好處也很多的,提高了業(yè)務(wù)的編碼效率,讓更多的web前端開(kāi)發(fā)也能寫App等等,最最重要的我覺(jué)得還是可以做到跨平臺(tái)以及熱更新。
05
至此!
感謝閱讀!