由于Webview內嵌H5的性能/功能各種受限拖叙,于是有了各種的混合開發(fā)解決方案,例如Hybrid赂乐、RN薯鳍、WEEX、Flutter挨措、小程序挖滤、快應用等等崩溪。
React Native 至今沒有推出1.0版本,由于各種可能的坑斩松,一些hold不住的團隊可能會放棄廓鞠。
Flutter 是否可替代RN站绪,真正實現兩端統一济舆,拭目以待浓恶,他從頭到尾重寫一套跨平臺的UI框架,包括UI控件岭参、渲染邏輯甚至開發(fā)語言。我本人之后會關注學習一下尝艘。
小程序 不用說太多了演侯,大家都很熟悉了;微信背亥、支付寶秒际、百度都在用。除了第一次需要花點時間下載狡汉,體驗上可以說是很不錯了娄徊,但是封閉性是他很大的一個缺點。
快應用 目標是很好的盾戴,統一API寄锐,但是還是要看各廠家的執(zhí)行力度。
現在來總結一下我們團隊目前使用的Hybrid方案尖啡。算是回顧一下橄仆,鞏固基礎,好記性不如爛筆頭衅斩。
一盆顾、Hybrid簡介
Hybrid可以說是上面提到的幾種里最古老,最成熟的解決方案了畏梆。
缺點是明顯的:H5有的缺點他幾乎都有您宪,比如性能差、JS執(zhí)行效率低等等奠涌。
但是優(yōu)點也很顯著:隨時發(fā)版宪巨,不受應用市場審核限制(當然這個前提是Hybrid對應Native的功能都已準備就緒);擁有幾乎和Native一樣的能力铣猩,eg:拍照揖铜、存儲、加日歷等等...
基本原理
Hybrid利用JSBridge進行通信的基本原理網上一搜一大把达皿,簡單記錄一下天吓。
Native => JS
兩端都有現成方法贿肩。誰讓都在別人的地盤下面玩呢,Native當然有辦法來執(zhí)行JS方法龄寞。
iOS
// Swift
webview.stringByEvaluatingJavaScriptFromString("Math.random()")
// OC
[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];
Android
mWebView.evaluateJavascript("javascript: 方法名('參數,需要轉為字符串')", new ValueCallback() {
@Override
public void onReceiveValue(String value) {
//這里的value即為對應JS方法的返回值
}
});
JS => Native
對于Webview中發(fā)起的網絡請求汰规,Native都有能力去捕獲/截取/干預。所以JSBridge的核心就是設計一套url方案物邑,讓Native可以識別溜哮,從而做出響應,執(zhí)行對應的操作就完事色解。
例如茂嗓,正常的網絡請求可能是: https://img.alicdn.com/tps/TB17ghmIFXXXXXAXFXXXXXXXXXX.png
我們可以自定義協議,改成jsbridge://methodName?param1=value1¶m2=value2
科阎。
Native攔截jsbridge
開頭的網絡請求述吸,做出對應的動作。
最常見的做法就是創(chuàng)建一個隱藏的iframe來實現通信锣笨。
二蝌矛、現成的解決方案
iOS WebViewJavascriptBridge
Android JsBridge
基本原理都相同,項目的設計就決定了一個它的可擴展性&可維護性错英。良好的可擴展性&可維護性對于JSBridge尤為重要入撒,他是后面一切業(yè)務的基石。
基礎庫簡析
(下面都以Android為例)
1椭岩、 初始化
類似寫普通H5頁面需要監(jiān)聽DOMContentLoaded
或者onLoad
來決定開始執(zhí)行腳本一樣茅逮,JSBridge需要一個契機去告訴JS,我準備好了簿煌,你可以來調用我的方法了氮唯。
[前端] 執(zhí)行監(jiān)聽 && 檢測
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調Native方法
先上代碼姨伟,下面是埋在端內的惩琉,JSBridge.callHandler,用來實現JS調用Native夺荒。
// 調用線程
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調Native方法的核心瞒渠。
handlerName
是前端與Native協商好的方法名稱
data
參數
responseCallback
回調
回調函數綁在了一個內部對象中var responseCallbacks = {}
,發(fā)送給Native的消息message中只包含了這個回調函數對應的id技扼,端上處理完成之后觸發(fā)&銷毀伍玖。
這個方法并不直接把消息全部推送走,而是存在一個隊列中sendMessageQueue
剿吻。同時通知Native窍箍,有新數據(message)需要處理。即上面代碼的最后一行,他利用iframe的src通知端上的信息如下:
var CUSTOM_PROTOCOL_SCHEME = 'sn'
var QUEUE_HAS_MESSAGE = '__sn__queue_message__'
上面提到的椰棘,JS只是通知了端上有新消息纺棺,Native調用獲取時機暫時不考慮,就假設他收到一條就處理一次邪狞,極端高頻情況下祷蝌,兩三條處理一次。Native通過_fetchQueue
統一處理存儲在sendMessageQueue
中的數據:
// 提供給native調用,該函數作用:獲取sendMessageQueue返回給native,由于android不能直接獲取返回的內容,所以使用url shouldOverrideUrlLoading 的方式返回內容
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主動調用Native的流程帆卓,關于回調方法巨朦,下面統一說。
3剑令、Native調JS方法
雖說Native可以隨意執(zhí)行JS糊啡,但是總是需要知道哪些JS方法是可執(zhí)行的吧。registerHandler
就是用來執(zhí)行注冊吁津。
registerHandler
在Native端定義(是JSBridge對象的一個方法)悔橄,由前端來注冊。
// 注冊線程 往數組里面添加值
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
Native主動調用腺毫。
Native主動調用分兩種情況,1是Native主動觸發(fā)前端事件挣柬,例如通知前端頁面可視狀態(tài)變化潮酒。2是前端調用Native的回調。JSBridge是天生異步的邪蛔,所以回調和主動調用歸結到一類里面了急黎。
如果是前端主動調用的方法,有responseId侧到,即有回調勃教,直接調用執(zhí)行即可。
否則就去注冊的messageHandlers中尋找方法匠抗,調用故源。
//提供給native使用,
function _dispatchMessageFromNative(messageJSON) {
setTimeout(function() {
var message = JSON.parse(messageJSON);
var responseCallback;
//java call finished, now need to call js callback function
// 前端主動調用的Callback
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
// Native主動調用
//直接發(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);
}
}
}
});
}
代碼分析基本就到這里,盜一張圖(地址放在最后了)汞贸,把流程都畫了出來绳军,個人感覺沒啥問題
三、業(yè)務封裝
直接使用前面的庫可以完成功能矢腻,但是不夠優(yōu)雅门驾,代碼不經過良好的設計可能會變得牽一發(fā)動全身,可維護性差多柑。下面說說我們的設計奶是,可能不是最好的,但是是很符合我們業(yè)務場景的。
- 事件基礎類 EventClass
處理事件廣播聂沙、訂閱秆麸。 - 連接基礎類 ConnectClass
/**
* 創(chuàng)建和獲取 jsbridge 基礎類
* @class ConnectClass
* @extends EventsClass
*/
class ConnectClass extends EventsClass {
/**
* 獲取jsbridge實例,注入到sncClass上的bridge屬性 `this.bridge`
*/
connect() {
// 事件廣播逐纬,通知開始建立連接蛔屹,統計使用
// 建立JSBridge
// 建立JSBridge.then 1.注冊Native主動調用的事件兔毒,對應上面的bridge.registerHandler芍殖;2.廣播 建立完成龟梦,統計使用
}
// ... 其他的一些方法
// eg: 分平臺初始化JSBridge,處理差異性
// eg: bridge.registerHandler 回調的封裝一層的統一處理函數
}
關于注冊Native主動調用的事件(和下面會提到的JS主動調用事件),實現插件化秧秉,并同一封裝呛踊。好處是可以明確代碼執(zhí)行步驟、方便業(yè)務同學調試(這不是我的鍋聘鳞,我已經執(zhí)行調用了...)脱惰、方便性能統計采盒。
- 業(yè)務類
class SncClass extends ConnectClass {
constructor(option){
// 監(jiān)聽connect烦租,監(jiān)聽首屏數據
// 建立連接 this.connect
// 掛載必備API
}
// 初始化者蠕,根據參數決定掛載哪些api
init(apis){
this.mountApi(apis);
}
/**
* 掛載 api
* @param {Object} apis api 對象集合
*/
mountApi(apis) {
// 1. 錯誤處理
// 2. 檢測是否已經jsb建立連接 已連接則 直接執(zhí)行真正掛載函數 return
// 3. bridge 未初始化時,定義方法預聲明蠢棱。執(zhí)行的方法將會被儲存在緩存隊列里在 bridge 初始化后調用
// 4. 監(jiān)聽連接事件,執(zhí)行真正掛載 loadMethods
}
}
/**
* 加載 API 到實例屬性甩栈,標志著 api 的真正掛載
*/
loadMethods(apis) {
// 1. 防止重復掛載 api泻仙,
// 2. 給插件初始化方法注入ctx,讓插件得以調用庫內真正的初始化函數量没,即封裝一層的上面提到的 callHandler
}
// ... 其他實例方法玉转,比如 extend,得以在業(yè)務中和Native互相約定新的非通用JSB殴蹄,方便擴展
- 初始化
導出單例appSNC究抓,擁有的方法都在appApis中定義,如果有新的業(yè)務需求直接擴展此文件夾中內容即可袭灯。
import * as apis from '../appApis'; // 方法集合
import SNC from './sdk'; // 上面的 SncClass
const option = {} ; // 一些配置
const appSNC = new SNC(option);
export default appSNC.init(apis);
以上就是我們正在使用的方案刺下,總結一下,不斷積累稽荧。