向在此次肺炎疫情中逝世的同胞默哀 |
本文首發於政採雲前端團隊部落格:小白必看,JSBridge 初探
JSBridge 的起源
近些年,移動端普及化越來越高,開發過程中選用 Native 還是 H5 一直是熱門話題。Native 和 H5 都有著各自的優缺點,為了滿足業務的需要,公司實際專案的開發過程中往往會融合兩者進行 Hybrid 開發。Native 和 H5 分處兩地,看起來無法聯絡,那麼如何才能讓雙方協同實現功能呢?
這時我們想到了 Cordova ,Cordova 提供了一組與裝置相關的 API ,是早期 JS 呼叫原生程式碼來實現原生功能的常用方案。不過 JSBridge 真正在國內廣泛應用是由於移動網際網路的盛行。
JSBridge 是一種 JS 實現的 Bridge,連線著橋兩端的 Native 和 H5。它在 APP 內方便地讓 Native 呼叫 JS,JS 呼叫 Native ,是雙向通訊的通道。JSBridge 主要提供了 JS 呼叫 Native 程式碼的能力,實現原生功能如檢視本地相簿、開啟攝像頭、指紋支付等。
H5 與 Native 對比
name | H5 | Native |
---|---|---|
穩定性 | 呼叫系統瀏覽器核心,穩定性較差 | 使用原生核心,更加穩定 |
靈活性 | 版本迭代快,上線靈活 | 迭代慢,需要應用商店稽核,上線速度受限制 |
受網速 影響 | 較大 | 較小 |
流暢度 | 有時載入慢,給使用者“卡頓”的感覺 | 載入速度快,更加流暢 |
使用者體驗 | 功能受瀏覽器限制,體驗有時較差 | 原生系統 api 豐富,能實現的功能較多,體驗較好 |
可移植性 | 相容跨平臺跨系統,如 PC 與 移動端,iOS 與 Android | 可移植性較低,對於 iOS 和 Android 需要維護兩套程式碼 |
JSBridge 的雙向通訊原理
-
JS 呼叫 Native
JS 呼叫 Native 的實現方式較多,主要有攔截 URL Scheme
、重寫 prompt 、注入 API 等方法。
攔截 URL Scheme
Android 和 iOS 都可以通過攔截 URL Scheme 並解析 Scheme 來決定是否進行對應的 Native 程式碼邏輯處理。
Android 的話,Webview
提供了 shouldOverrideUrlLoading
方法來提供給 Native 攔截 H5 傳送的 URL Scheme
請求。程式碼如下:
public class CustomWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
......
// 場景一: 攔截請求、接收 scheme
if (url.equals("xxx")) {
// handle
...
// callback
view.loadUrl("javascript:setAllContent(" + json + ");")
return true;
}
return super.shouldOverrideUrlLoading(url);
}
}
複製程式碼
iOS 的 WKWebview
可以根據攔截到的 URL Scheme
和對應的引數執行相關的操作。程式碼如下:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
if ([navigationAction.request.URL.absoluteString hasPrefix:@"xxx"]) {
[[UIApplication sharedApplication] openURL:navigationAction.request.URL];
}
decisionHandler(WKNavigationActionPolicyAllow);
}
複製程式碼
這種方法的優點是不存在漏洞問題、使用靈活,可以實現 H5 和 Native 頁面的無縫切換。例如在某一頁面需要快速上線的情況下,先開發出 H5 頁面。某一連結填寫的是 H5 連結,在對應的 Native 頁面開發完成前先跳轉至 H5 頁面,待 Native 頁面開發完後再進行攔截,跳轉至 Native 頁面,此時 H5 的連結無需進行修改。但是使用 iframe.src 來傳送 URL Scheme
需要對 URL 的長度作控制,使用複雜,速度較慢。
重寫 prompt 等原生 JS 方法
Android 4.2 之前注入物件的介面是 addJavascriptInterface ,但是由於安全原因慢慢不被使用。一般會通過修改瀏覽器的部分 Window 物件的方法來完成操作。主要是攔截 alert、confirm、prompt、console.log 四個方法,分別被 Webview
的 onJsAlert、onJsConfirm、onConsoleMessage、onJsPrompt 監聽。其中 onJsPrompt 監聽的程式碼如下:
public boolean onJsPrompt(WebView view, String origin, String message, String defaultValue, final JsPromptResult result) {
String handledRet = parentEngine.bridge.promptOnJsPrompt(origin, message, defaultValue);
xxx;
return true;
}
複製程式碼
iOS 由於安全機制,WKWebView
對 alert、confirm、prompt 等方法做了攔截,如果通過此方式進行 Native 與 JS 互動,需要實現 WKWebView
的三個 WKUIDelegate
代理方法。程式碼示例如下:
-(void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:([UIAlertAction actionWithTitle:@"確認" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}])];
[self presentViewController:alertController animated:YES completion:nil];
}
複製程式碼
使用該方式時,可以與 Android 和 iOS 約定好使用傳參的格式,這樣 H5 可以無需識別客戶端,傳入不同引數直接呼叫 Native 即可。剩下的交給客戶端自己去攔截相同的方法,識別相同的引數,進行自己的處理邏輯即可實現多端表現一致。如:
alert("確定xxx?", "取消", "確定", callback());
複製程式碼
另外,如果能與 Native 確定好方法名、傳參等呼叫的協議規範,這樣其它格式的 prompt 等方法是不會被識別的,能起到隔離的作用。
注入 API
基於 Webview
提供的能力,我們可以向 Window 上注入物件或方法。JS 通過這個物件或方法進行呼叫時,執行對應的邏輯操作,可以直接呼叫 Native 的方法。使用該方式時,JS 需要等到 Native 執行完對應的邏輯後才能進行回撥裡面的操作。
Android 的 Webview
提供了 addJavascriptInterface 方法,支援 Android 4.2 及以上系統。
gpcWebView.addJavascriptInterface(new JavaScriptInterface(), 'nativeApiBridge');
public class JavaScriptInterface {
Context mContext;
JavaScriptInterface(Context c) {
mContext = c;
}
public void share(String webMessage){
// Native 邏輯
}
}
複製程式碼
JS 呼叫示例:
window.NativeApi.share(xxx);
複製程式碼
iOS 的 UIWebview
提供了 JavaScriptScore 方法,支援 iOS 7.0 及以上系統。WKWebview
提供了 window.webkit.messageHandlers 方法,支援 iOS 8.0 及以上系統。UIWebview
在幾年前常用,目前已不常見。以下為建立 WKWebViewConfiguration
和 建立 WKWebView 示例:
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
WKPreferences *preferences = [WKPreferences new];
preferences.javaScriptCanOpenWindowsAutomatically = YES;
preferences.minimumFontSize = 40.0;
configuration.preferences = preferences;
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"share"];
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"pickImage"];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"share"];
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"pickImage"];
}
複製程式碼
JS 呼叫示例:
window.webkit.messageHandlers.share.postMessage(xxx);
複製程式碼
-
Native 呼叫 JS
Native 呼叫 JS 比較簡單,只要 H5 將 JS 方法暴露在 Window 上給 Native 呼叫即可。
Android 中主要有兩種方式實現。在 4.4 以前,通過 loadUrl 方法,執行一段 JS 程式碼來實現。在 4.4 以後,可以使用 evaluateJavascript 方法實現。loadUrl 方法使用起來方便簡潔,但是效率低無法獲得返回結果且呼叫的時候會重新整理 WebView。evaluateJavascript 方法效率高獲取返回值方便,呼叫時候不重新整理WebView,但是隻支援 Android 4.4+。相關程式碼如下:
webView.loadUrl("javascript:" + javaScriptString);
webView.evaluateJavascript(javaScriptString, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value){
xxx
}
});
複製程式碼
iOS 在 WKWebview
中可以通過 evaluateJavaScript:javaScriptString 來實現,支援 iOS 8.0 及以上系統。
// swift
func evaluateJavaScript(_ javaScriptString: String,
completionHandler: ((Any?, Error?) -> Void)? = nil)
// javaScriptString 需要呼叫的 JS 程式碼
// completionHandler 執行後的回撥
複製程式碼
// objective-c
[jsContext evaluateJavaScript:@"ZcyJsBridge(ev, data)"]
複製程式碼
JSBridge 的使用
-
如何引用
-
由 H5 引用
在我司移動端初期版本時採用的是該方式,採用本地引入 npm 包的方式進行呼叫。這種方式可以確定 JSBridge 是存在的,可直接呼叫 Native 方法。但是如果後期 Bridge 的實現方式改變,雙方需要做更多的相容,維護成本高
-
由 Native 注入
這是當前我司移動端選用的方式。在考慮到後期業務需要的情況下,進行了重新設計,選用 Native 注入的方式來引用 JSBridge。這樣有利於保持 API 與 Native 的一致性,但是缺點是在 Native 注入的方法和時機都受限,JS 呼叫 Native 之前需要先判斷 JSBridge 是否注入成功
-
-
使用規範
H5 呼叫 Native 方法的虛擬碼例項,如:
params = {
api_version: "xxx", // API 版本
title: "xxx", // 標題
filename: "xxx", // 檔名稱
image: "xxx", // 圖片連結
url: "xxx", // 網址連結
success: function (res) {
xxx; // 呼叫成功後執行
},
fail: function (err) {
if (err.code == '-2') {
fail && fail(err); // 呼叫了當前客戶端中不存在的 API 版本
} else {
const msg = err.msg; //異常資訊
Toast.fail(msg);
}
}
};
window.NativeApi.share(params);
複製程式碼
以下簡要列出通用方法的抽象,目前基本遵循以下規範進行雙端通訊。
window.NativeApi.xxx({
api_version:'',
name: "xxx",
path: "xxx",
id: "xxx",
success: function (res) {
console.log(res);
},
fail: function (err) {
console.log(err);
}
});
複製程式碼
由於初期版本選擇了由 H5 本地引用 JSBridge,後期採用 Native 注入的方式。現有的 H5 需要對各種情況做相容,邏輯抽象如下:
reqNativeBridge(vm, fn) {
if (!isApp()) {
// 如果不在 APP 內進行呼叫
vm.$dialog.alert({
message: "此功能需要訪問 APP 才能使用",
});
} else {
if (!window.NativeApi) {
// 針對初期版本
vm.$dialog.alert({
message: "請更新到最新 APP 使用該功能",
});
} else {
// 此處只針對“呼叫了當前客戶端中不存在的 API 版本”的報錯進行處理
// 其餘種類的錯誤資訊交由具體的業務去處理
fn && fn((err) => {
vm.$dialog.alert({
message: "請更新到最新 APP 使用該功能",
});
});
}
}
}
複製程式碼
總結
上述內容簡要介紹了 JSBridge 的部分原理,希望對從未了解過 JSBridge 的同學能有所幫助。如果需要更深入的瞭解 JSBridge 的原理和實現,如 JSBridge 介面呼叫的封裝實現,JS 呼叫 Native 時的回撥的唯一性等。大家可以去查閱更多資料,參考更詳細的相關文件或他人的整理成文的沉澱。
推薦閱讀
招賢納士
政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 50 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的“老”兵,也有浙大、中科大、杭電等校的應屆新人。團隊在日常的業務對接之外,還在物料體系、工程平臺、搭建平臺、效能體驗、雲端應用、資料分析及視覺化等方向進行技術探索和實戰,推動並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。
如果你想改變一直被事折騰,希望開始能折騰事;如果你想改變一直被告誡需要多些想法,卻無從破局;如果你想改變你有能力去做成那個結果,卻不需要你;如果你想改變你想做成的事需要一個團隊去支撐,但沒你帶人的位置;如果你想改變既定的節奏,將會是“ 5 年工作時間 3 年工作經驗”;如果你想改變本來悟性不錯,但總是有那一層窗戶紙的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望參與到隨著業務騰飛的過程,親手推動一個有著深入的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我覺得我們該聊聊。任何時間,等著你寫點什麼,發給 ZooTeam@cai-inc.com