React Native Android從源碼看WebView 沒有OverrideUrl解決辦法,以及高度自適應

react native.jpg

00

其實我這篇文章的目的并不完全是要解決這個問題。而是想通過這個問題來簡單講講React Native組件和Android原生控件的一個關(guān)系,以及如何通過看源碼來排查解決React NativeAndroid機型遇到的問題的思路,當然iOS的思路也是大同小異的。ps:總結(jié)在最后。

需求背景:有一個文章詳情是以html富文本的方式存在后臺數(shù)據(jù)庫的,現(xiàn)在需要React NativeWebView來展示。而且在這個詳情頁頭部是有除WebView以外的組件。這個時候富文本里有一個<a>標簽跳轉(zhuǎn)鏈接,需要另外打開一個頁面來承載這個鏈接。

01

知道了這個需要,我們第一反應肯定是先去看文檔,http://reactnative.cn/中文網(wǎng)里WebView章節(jié)里有這么一個方法onShouldStartLoadWithRequest(允許為WebView發(fā)起的請求運行一個自定義的處理函數(shù)。返回true或false表示是否要繼續(xù)執(zhí)行響應的請求。),但是....重點在但是,這個方法只有iOS

作為一個Android開發(fā)人員我就有點不理解了,Android WebView明明有類似的方法回調(diào)shouldOverrideUrlLoading,不是號稱React Native調(diào)用的就是原生的控件嗎,為什么不提供呢?

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開發(fā)人員封裝的給RN用的WebView了。找到WebViewshouldOverrideUrlLoading方法。源碼如下

@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

看到這里,其實已經(jīng)發(fā)現(xiàn)了問題原因所在了。
接下來就是考慮怎么解決了。
這里我提供兩個思路吧。
思路1:從Android這邊入手。【強烈推薦】

既然官方提供的WebView沒有提供方法,那我們完全可以自己封裝一個WebView給RN用撒,RN那邊設(shè)置一個參數(shù)是否需要shouldOverrideUrlLoading={true},Android這邊接收這個參數(shù),如果判斷為true就在Android的shouldOverrideUrlLoading回調(diào)里將事件dispatchEvent分發(fā)給RN,RN那邊寫個回調(diào)就好啦,其實我覺得官方也可以這么來寫。 后續(xù)我再寫一篇文章,詳細講述這個編碼過程。

思路2:從RN JS那邊入手。

利用RN WebView的injectedJavaScript屬性,給WebView注入一段js代碼,攔截所有<a>標簽的跳轉(zhuǎn),并將事件和即將跳轉(zhuǎn)的url通過postMessage的方式回調(diào)給RN,這樣就可以啦,以下是代碼片段。這個方案其實不是一個保險的解決方案,因為看Android源碼可以看到,injectedJavaScript是在onPageFinish里回調(diào)的,而這個回調(diào)在Android本身是有適配問題的,有時候是不會回調(diào)的,比如網(wǎng)頁里某個css、js文件沒下載下來,會一直卡住,以至于不會回調(diào)結(jié)束。所以還是推薦第一種方案。

renden的定義,【這里其實還實現(xiàn)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.
//至此,是為了保證一定會調(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)頁內(nèi)容高度傳給rn,以實現(xiàn)自適應高度

//以下就是找到所有a標簽,并將url傳給RN處理
var aNodes = document.getElementsByTagName('a');
for (var i = 0; i < aNodes.length; i++) {
    aNodes[i].onclick = function (e) {
        e.preventDefault();//這句話是阻止a標簽跳轉(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標簽事件
            ...
    }
}

04

最后,做個首尾呼應。我們來簡單總結(jié)下React Native組件和Android原生控件的一個關(guān)系。

通過上面這個案例分析,我們可以清晰的看到RN是做了一個 用js來調(diào)用原生控件的一個偉大事情,并在js端以組件的方式來使用,但是這個原生控件是經(jīng)過了一定封裝的,并不是將所有原生控件的屬性方法都暴露給js端。這里就會有很大的坑,因為Android的適配很多時候是一個經(jīng)驗工作,再加上國內(nèi)很多手機廠商都有自己的修改過的ROM,這就導致facebook的開發(fā)人員在封裝控件的時候可能并不能完全考慮該控件的適配問題以及使用場景,就會出現(xiàn)純js不能直接解決的問題。具體例子我就不再列舉了,同理于iOS

所以,React Native固然好,但是也有一定的局限,他的發(fā)展之所以到現(xiàn)在還在0.48版本,也是有一定道理的。

當然,RN的好處也很多的,提高了業(yè)務的編碼效率,讓更多的web前端開發(fā)也能寫App等等,最最重要的我覺得還是可以做到跨平臺以及熱更新。

05

至此!
感謝閱讀!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容