由于Webview內(nèi)嵌H5的性能/功能各種受限,于是有了各種的混合開(kāi)發(fā)解決方案,例如Hybrid、RN、WEEX、Flutter、小程序、快應(yīng)用等等。
React Native 至今沒(méi)有推出1.0版本,由于各種可能的坑,一些hold不住的團(tuán)隊(duì)可能會(huì)放棄。
Flutter 是否可替代RN,真正實(shí)現(xiàn)兩端統(tǒng)一,拭目以待,他從頭到尾重寫(xiě)一套跨平臺(tái)的UI框架,包括UI控件、渲染邏輯甚至開(kāi)發(fā)語(yǔ)言。我本人之后會(huì)關(guān)注學(xué)習(xí)一下。
小程序 不用說(shuō)太多了,大家都很熟悉了;微信、支付寶、百度都在用。除了第一次需要花點(diǎn)時(shí)間下載,體驗(yàn)上可以說(shuō)是很不錯(cuò)了,但是封閉性是他很大的一個(gè)缺點(diǎn)。
快應(yīng)用 目標(biāo)是很好的,統(tǒng)一API,但是還是要看各廠家的執(zhí)行力度。
現(xiàn)在來(lái)總結(jié)一下我們團(tuán)隊(duì)目前使用的Hybrid方案。算是回顧一下,鞏固基礎(chǔ),好記性不如爛筆頭。
一、Hybrid簡(jiǎn)介
Hybrid可以說(shuō)是上面提到的幾種里最古老,最成熟的解決方案了。
缺點(diǎn)是明顯的:H5有的缺點(diǎn)他幾乎都有,比如性能差、JS執(zhí)行效率低等等。
但是優(yōu)點(diǎn)也很顯著:隨時(shí)發(fā)版,不受應(yīng)用市場(chǎng)審核限制(當(dāng)然這個(gè)前提是Hybrid對(duì)應(yīng)Native的功能都已準(zhǔn)備就緒);擁有幾乎和Native一樣的能力,eg:拍照、存儲(chǔ)、加日歷等等...
基本原理
Hybrid利用JSBridge進(jìn)行通信的基本原理網(wǎng)上一搜一大把,簡(jiǎn)單記錄一下。
Native => JS
兩端都有現(xiàn)成方法。誰(shuí)讓都在別人的地盤(pán)下面玩呢,Native當(dāng)然有辦法來(lái)執(zhí)行JS方法。
iOS
// Swift
webview.stringByEvaluatingJavaScriptFromString("Math.random()")
// OC
[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];
Android
mWebView.evaluateJavascript("javascript: 方法名('參數(shù),需要轉(zhuǎn)為字符串')", new ValueCallback() {
@Override
public void onReceiveValue(String value) {
//這里的value即為對(duì)應(yīng)JS方法的返回值
}
});
JS => Native
對(duì)于Webview中發(fā)起的網(wǎng)絡(luò)請(qǐng)求,Native都有能力去捕獲/截取/干預(yù)。所以JSBridge的核心就是設(shè)計(jì)一套u(yù)rl方案,讓Native可以識(shí)別,從而做出響應(yīng),執(zhí)行對(duì)應(yīng)的操作就完事。
例如,正常的網(wǎng)絡(luò)請(qǐng)求可能是: https://img.alicdn.com/tps/TB17ghmIFXXXXXAXFXXXXXXXXXX.png
我們可以自定義協(xié)議,改成jsbridge://methodName?param1=value1¶m2=value2。
Native攔截jsbridge開(kāi)頭的網(wǎng)絡(luò)請(qǐng)求,做出對(duì)應(yīng)的動(dòng)作。
最常見(jiàn)的做法就是創(chuàng)建一個(gè)隱藏的iframe來(lái)實(shí)現(xiàn)通信。
二、現(xiàn)成的解決方案
iOS WebViewJavascriptBridge
Android JsBridge
基本原理都相同,項(xiàng)目的設(shè)計(jì)就決定了一個(gè)它的可擴(kuò)展性&可維護(hù)性。良好的可擴(kuò)展性&可維護(hù)性對(duì)于JSBridge尤為重要,他是后面一切業(yè)務(wù)的基石。
基礎(chǔ)庫(kù)簡(jiǎn)析
(下面都以Android為例)
1、 初始化
類似寫(xiě)普通H5頁(yè)面需要監(jiān)聽(tīng)DOMContentLoaded或者onLoad來(lái)決定開(kāi)始執(zhí)行腳本一樣,JSBridge需要一個(gè)契機(jī)去告訴JS,我準(zhǔn)備好了,你可以來(lái)調(diào)用我的方法了。
[前端] 執(zhí)行監(jiān)聽(tīng) && 檢測(cè)
if (window.WebViewJavascriptBridge) {
//do your work here
} else {
document.addEventListener(
'WebViewJavascriptBridgeReady'
, function() {
//do your work here
},
false
);
}
[Native (埋在端里的JS)] dispatchEvent觸發(fā)
var WebViewJavascriptBridge = window.WebViewJavascriptBridge = {
init: init,
send: send,
registerHandler: registerHandler,
callHandler: callHandler,
_fetchQueue: _fetchQueue,
_handleMessageFromNative: _handleMessageFromNative
};
var readyEvent = doc.createEvent('Events');
readyEvent.initEvent('WebViewJavascriptBridgeReady');
readyEvent.bridge = WebViewJavascriptBridge;
doc.dispatchEvent(readyEvent);
2、JS調(diào)Native方法
先上代碼,下面是埋在端內(nèi)的,JSBridge.callHandler,用來(lái)實(shí)現(xiàn)JS調(diào)用Native。
// 調(diào)用線程
function callHandler(handlerName, data, responseCallback) {
_doSend({
handlerName: handlerName,
data: data
}, responseCallback);
}
//sendMessage add message, 觸發(fā)native處理 sendMessage
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message.callbackId = callbackId;
}
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
jsbridge.callHandler是JS調(diào)Native方法的核心。
handlerName是前端與Native協(xié)商好的方法名稱
data 參數(shù)
responseCallback 回調(diào)
回調(diào)函數(shù)綁在了一個(gè)內(nèi)部對(duì)象中var responseCallbacks = {},發(fā)送給Native的消息message中只包含了這個(gè)回調(diào)函數(shù)對(duì)應(yīng)的id,端上處理完成之后觸發(fā)&銷毀。
這個(gè)方法并不直接把消息全部推送走,而是存在一個(gè)隊(duì)列中sendMessageQueue。同時(shí)通知Native,有新數(shù)據(jù)(message)需要處理。即上面代碼的最后一行,他利用iframe的src通知端上的信息如下:
var CUSTOM_PROTOCOL_SCHEME = 'sn'
var QUEUE_HAS_MESSAGE = '__sn__queue_message__'
上面提到的,JS只是通知了端上有新消息,Native調(diào)用獲取時(shí)機(jī)暫時(shí)不考慮,就假設(shè)他收到一條就處理一次,極端高頻情況下,兩三條處理一次。Native通過(guò)_fetchQueue統(tǒng)一處理存儲(chǔ)在sendMessageQueue中的數(shù)據(jù):
// 提供給native調(diào)用,該函數(shù)作用:獲取sendMessageQueue返回給native,由于android不能直接獲取返回的內(nèi)容,所以使用url shouldOverrideUrlLoading 的方式返回內(nèi)容
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
//android can't read directly the return data, so we can reload iframe src to communicate with java
if (messageQueueString !== '[]') {
bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
}
這些基本就是JS主動(dòng)調(diào)用Native的流程,關(guān)于回調(diào)方法,下面統(tǒng)一說(shuō)。
3、Native調(diào)JS方法
雖說(shuō)Native可以隨意執(zhí)行JS,但是總是需要知道哪些JS方法是可執(zhí)行的吧。registerHandler就是用來(lái)執(zhí)行注冊(cè)。
registerHandler在Native端定義(是JSBridge對(duì)象的一個(gè)方法),由前端來(lái)注冊(cè)。
// 注冊(cè)線程 往數(shù)組里面添加值
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
Native主動(dòng)調(diào)用。
Native主動(dòng)調(diào)用分兩種情況,1是Native主動(dòng)觸發(fā)前端事件,例如通知前端頁(yè)面可視狀態(tài)變化。2是前端調(diào)用Native的回調(diào)。JSBridge是天生異步的,所以回調(diào)和主動(dòng)調(diào)用歸結(jié)到一類里面了。
如果是前端主動(dòng)調(diào)用的方法,有responseId,即有回調(diào),直接調(diào)用執(zhí)行即可。
否則就去注冊(cè)的messageHandlers中尋找方法,調(diào)用。
//提供給native使用,
function _dispatchMessageFromNative(messageJSON) {
setTimeout(function() {
var message = JSON.parse(messageJSON);
var responseCallback;
//java call finished, now need to call js callback function
// 前端主動(dòng)調(diào)用的Callback
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
// Native主動(dòng)調(diào)用
//直接發(fā)送
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({
responseId: callbackResponseId,
responseData: responseData
});
};
}
var handler = WebViewJavascriptBridge._messageHandler;
if (message.handlerName) {
handler = messageHandlers[message.handlerName];
}
//查找指定handler
try {
handler(message.data, responseCallback);
} catch (exception) {
if (typeof console != 'undefined') {
console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
}
}
}
});
}
代碼分析基本就到這里,盜一張圖(地址放在最后了),把流程都畫(huà)了出來(lái),個(gè)人感覺(jué)沒(méi)啥問(wèn)題

三、業(yè)務(wù)封裝
直接使用前面的庫(kù)可以完成功能,但是不夠優(yōu)雅,代碼不經(jīng)過(guò)良好的設(shè)計(jì)可能會(huì)變得牽一發(fā)動(dòng)全身,可維護(hù)性差。下面說(shuō)說(shuō)我們的設(shè)計(jì),可能不是最好的,但是是很符合我們業(yè)務(wù)場(chǎng)景的。
- 事件基礎(chǔ)類 EventClass
處理事件廣播、訂閱。 - 連接基礎(chǔ)類 ConnectClass
/**
* 創(chuàng)建和獲取 jsbridge 基礎(chǔ)類
* @class ConnectClass
* @extends EventsClass
*/
class ConnectClass extends EventsClass {
/**
* 獲取jsbridge實(shí)例,注入到sncClass上的bridge屬性 `this.bridge`
*/
connect() {
// 事件廣播,通知開(kāi)始建立連接,統(tǒng)計(jì)使用
// 建立JSBridge
// 建立JSBridge.then 1.注冊(cè)Native主動(dòng)調(diào)用的事件,對(duì)應(yīng)上面的bridge.registerHandler;2.廣播 建立完成,統(tǒng)計(jì)使用
}
// ... 其他的一些方法
// eg: 分平臺(tái)初始化JSBridge,處理差異性
// eg: bridge.registerHandler 回調(diào)的封裝一層的統(tǒng)一處理函數(shù)
}
關(guān)于注冊(cè)Native主動(dòng)調(diào)用的事件(和下面會(huì)提到的JS主動(dòng)調(diào)用事件),實(shí)現(xiàn)插件化,并同一封裝。好處是可以明確代碼執(zhí)行步驟、方便業(yè)務(wù)同學(xué)調(diào)試(這不是我的鍋,我已經(jīng)執(zhí)行調(diào)用了...)、方便性能統(tǒng)計(jì)。
- 業(yè)務(wù)類
class SncClass extends ConnectClass {
constructor(option){
// 監(jiān)聽(tīng)connect,監(jiān)聽(tīng)首屏數(shù)據(jù)
// 建立連接 this.connect
// 掛載必備API
}
// 初始化,根據(jù)參數(shù)決定掛載哪些api
init(apis){
this.mountApi(apis);
}
/**
* 掛載 api
* @param {Object} apis api 對(duì)象集合
*/
mountApi(apis) {
// 1. 錯(cuò)誤處理
// 2. 檢測(cè)是否已經(jīng)jsb建立連接 已連接則 直接執(zhí)行真正掛載函數(shù) return
// 3. bridge 未初始化時(shí),定義方法預(yù)聲明。執(zhí)行的方法將會(huì)被儲(chǔ)存在緩存隊(duì)列里在 bridge 初始化后調(diào)用
// 4. 監(jiān)聽(tīng)連接事件,執(zhí)行真正掛載 loadMethods
}
}
/**
* 加載 API 到實(shí)例屬性,標(biāo)志著 api 的真正掛載
*/
loadMethods(apis) {
// 1. 防止重復(fù)掛載 api,
// 2. 給插件初始化方法注入ctx,讓插件得以調(diào)用庫(kù)內(nèi)真正的初始化函數(shù),即封裝一層的上面提到的 callHandler
}
// ... 其他實(shí)例方法,比如 extend,得以在業(yè)務(wù)中和Native互相約定新的非通用JSB,方便擴(kuò)展
- 初始化
導(dǎo)出單例appSNC,擁有的方法都在appApis中定義,如果有新的業(yè)務(wù)需求直接擴(kuò)展此文件夾中內(nèi)容即可。
import * as apis from '../appApis'; // 方法集合
import SNC from './sdk'; // 上面的 SncClass
const option = {} ; // 一些配置
const appSNC = new SNC(option);
export default appSNC.init(apis);
以上就是我們正在使用的方案,總結(jié)一下,不斷積累。