1. 程式人生 > >移動混合開發中的 JSBridge

移動混合開發中的 JSBridge

來源:https://mp.weixin.qq.com/s/I812Cr1_tLGrvIRb9jsg-A

【導讀】關於 JSBridge,絕大多數同學最早遇到的是微信的 WeiXinJSBridge(現在被封裝成 JSSDK),各種 Web 頁面可以通過 Bridge 呼叫微信提供的一些原生功能,為使用者提供相關的功能。其實,JSBridge 很早就出現在軟體開發中,在一些桌面軟體中很早就運用了這樣的形式,多用在通知、產品詳情、廣告等模組中,然後這些模組中,使用的是 Web UI,而相關按鈕點選後,呼叫的是 Native 功能。現在移動端盛行,不管是 Hybrid 應用,還是 React-Native 都離不開 JSBridge,當然也包括在國內舉足輕重的微信小程式。那麼,JSBridge 到底是什麼?它的出現是為了什麼?它究竟是怎麼實現的?在這篇文章中,會在移動混合開發的範疇內,將給大家帶來 JSBridge 的深入剖析。

1 前言

有些童鞋聽到 JSBridge 這個名詞,就是覺得非常高上大,有了它 Web 和 Native 可以進行互動,就像『進化藥水』,讓 Web 搖身一變,成為移動戰場的『上將一名』。其實並非如此,JSBridge 其實真是一個很簡單的東西,更多的是一種形式、一種思想。

2 JSBridge 的起源

為什麼是 JSBridge ?而不是 PythonBridge 或是 RubyBridge ?

當然不是因為 JavaScript 語言高人一等(雖然斯坦福大學已經把演算法導論的語言從 Java 改成 JavaScript,小得意一下,嘻嘻),主要的原因還是因為 JavaScript 主要載體 Web 是當前世界上的 最易編寫

 、 最易維護 、最易部署 的 UI 構建方式。工程師可以用很簡單的 HTML 標籤和 CSS 樣式快速的構建出一個頁面,並且在服務端部署後,使用者不需要主動更新,就能看到最新的 UI 展現。

因此,開發維護成本 和 更新成本 較低的 Web 技術成為混合開發中幾乎不二的選擇,而作為 Web 技術邏輯核心的 JavaScript 也理所應當肩負起與其他技術『橋接』的職責,並且作為移動不可缺少的一部分,任何一個移動作業系統中都包含可執行 JavaScript 的容器,例如 WebView 和 JSCore。所以,執行 JavaScript 不用像執行其他語言時,要額外新增執行環境。因此,基於上面種種原因,JSBridge 應運而生。

PhoneGap(Codova 的前身)作為 Hybrid 鼻祖框架,應該是最先被開發者廣泛認知的 JSBridge 的應用場景;而對於 JSBridge 的應用在國內真正興盛起來,則是因為殺手級應用微信的出現,主要用途是在網頁中通過 JSBridge 設定分享內容。

移動端混合開發中的 JSBridge,主要被應用在兩種形式的技術方案上:

  • 基於 Web 的 Hybrid 解決方案:例如微信瀏覽器、各公司的 Hybrid 方案

  • 非基於 Web UI 但業務邏輯基於 JavaScript 的解決方案:例如 React-Native

【注】:微信小程式基於 Web UI,但是為了追求執行效率,對 UI 展現邏輯和業務邏輯的 JavaScript 進行了隔離。因此小程式的技術方案介於上面描述的兩種方式之間。

3 JSBridge 的用途

JSBridge 簡單來講,主要是 給 JavaScript 提供呼叫 Native 功能的介面,讓混合開發中的『前端部分』可以方便地使用地址位置、攝像頭甚至支付等 Native 功能。

既然是『簡單來講』,那麼 JSBridge 的用途肯定不只『呼叫 Native 功能』這麼簡單寬泛。實際上,JSBridge 就像其名稱中的『Bridge』的意義一樣,是 Native 和非 Native 之間的橋樑,它的核心是 構建 Native 和非 Native 間訊息通訊的通道,而且是 雙向通訊的通道

所謂 雙向通訊的通道:

  • JS 向 Native 傳送訊息 : 呼叫相關功能、通知 Native 當前 JS 的相關狀態等。

  • Native 向 JS 傳送訊息 : 回溯呼叫結果、訊息推送、通知 JS 當前 Native 的狀態等。

這裡有些同學有疑問了:訊息都是單向的,那麼呼叫 Native 功能時 Callback 怎麼實現的?
對於這個問題,在下一節裡會給出解釋。

4 JSBridge 的實現原理

JavaScript 是執行在一個單獨的 JS Context 中(例如,WebView 的 Webkit 引擎、JSCore)。由於這些 Context 與原生執行環境的天然隔離,我們可以將這種情況與 RPC(Remote Procedure Call,遠端過程呼叫)通訊進行類比,將 Native 與 JavaScript 的每次互相呼叫看做一次 RPC 呼叫。如此一來我們可以按照通常的 RPC 方式來進行設計和實現。

在 JSBridge 的設計中,可以把前端看做 RPC 的客戶端,把 Native 端看做 RPC 的伺服器端,從而 JSBridge 要實現的主要邏輯就出現了:通訊呼叫(Native 與 JS 通訊) 和 控制代碼解析呼叫。(如果你是個前端,而且並不熟悉 RPC 的話,你也可以把這個流程類比成 JSONP 的流程)

通過以上的分析,可以清楚地知曉 JSBridge 主要的功能和職責,接下來就以 Hybrid 方案 為案例從這幾點來剖析 JSBridge 的實現原理。

4.1 JSBridge 的通訊原理

Hybrid 方案是基於 WebView 的,JavaScript 執行在 WebView 的 Webkit 引擎中。因此,Hybrid 方案中 JSBridge 的通訊原理會具有一些 Web 特性。

4.1.1 JavaScript 呼叫 Native

JavaScript 呼叫 Native 的方式,主要有兩種:注入 API 和 攔截 URL SCHEME

4.1.1.1 注入API

注入 API 方式的主要原理是,通過 WebView 提供的介面,向 JavaScript 的 Context(window)中注入物件或者方法,讓 JavaScript 呼叫時,直接執行相應的 Native 程式碼邏輯,達到 JavaScript 呼叫 Native 的目的。

對於 iOS 的 UIWebView,例項如下:

1
2
3
4
5
JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

context[@"postBridgeMessage"] = ^(NSArray<NSArray *> *calls) {
   // Native 邏輯
};

前端呼叫方式:

1
window.postBridgeMessage(message);

對於 iOS 的 WKWebView 可以用以下方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface WKWebVIewVC ()<WKScriptMessageHandler>

@implementation WKWebVIewVC

- (void)viewDidLoad {
   [super viewDidLoad];

   WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
   configuration.userContentController = [[WKUserContentController alloc] init];
   WKUserContentController *userCC = configuration.userContentController;
   // 注入物件,前端呼叫其方法時,Native 可以捕獲到
   [userCC addScriptMessageHandler:self name:@"nativeBridge"];

   WKWebView wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];

   // TODO 顯示 WebView
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
   if ([message.name isEqualToString:@"nativeBridge"]) {
       NSLog(@"前端傳遞的資料 %@: ",message.body);
       // Native 邏輯
   }
}

前端呼叫方式:

1
window.webkit.messageHandlers.nativeBridge.postMessage(message);

對於 Android 可以採用下面的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class JavaScriptInterfaceDemoActivity extends Activity {
private WebView Wv;

   @Override
   public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       Wv = (WebView)findViewById(R.id.webView);    
       final JavaScriptInterface myJavaScriptInterface = new JavaScriptInterface(this);    

       Wv.getSettings().setJavaScriptEnabled(true);
       Wv.addJavascriptInterface(myJavaScriptInterface, "nativeBridge");

       // TODO 顯示 WebView

   }

   public class JavaScriptInterface {
        Context mContext;

        JavaScriptInterface(Context c) {
            mContext = c;
        }

        public void postMessage(String webMessage){    
            // Native 邏輯
        }
    }
}

前端呼叫方式:

1
window.nativeBridge.postMessage(message);

在 4.2 之前,Android 注入 JavaScript 物件的介面是 addJavascriptInterface,但是這個介面有漏洞,可以被不法分子利用,危害使用者的安全,因此在 4.2 中引入新的介面 @JavascriptInterface(上面程式碼中使用的)來替代這個介面,解決安全問題。所以 Android 注入對物件的方式是 有相容性問題的。(4.2 之前很多方案都採用攔截 prompt 的方式來實現,因為篇幅有限,這裡就不展開了。)

4.1.1.2 攔截 URL SCHEME

先解釋一下 URL SCHEME:URL SCHEME是一種類似於url的連結,是為了方便app直接互相呼叫設計的,形式和普通的 url 近似,主要區別是 protocol 和 host 一般是自定義的,例如: qunarhy://hy/url?url=http://ymfe.tech,protocol 是 qunarhy,host 則是 hy。

攔截 URL SCHEME 的主要流程是:Web 端通過某種方式(例如 iframe.src)傳送 URL Scheme 請求,之後 Native 攔截到請求並根據 URL SCHEME(包括所帶的引數)進行相關操作。

在時間過程中,這種方式有一定的 缺陷

  • 使用 iframe.src 傳送 URL SCHEME 會有 url 長度的隱患。

  • 建立請求,需要一定的耗時,比注入 API 的方式呼叫同樣的功能,耗時會較長。

但是之前為什麼很多方案使用這種方式呢?因為它 支援 iOS6。而現在的大環境下,iOS6 佔比很小,基本上可以忽略,所以並不推薦為了 iOS6 使用這種 並不優雅 的方式。

【注】:有些方案為了規避 url 長度隱患的缺陷,在 iOS 上採用了使用 Ajax 傳送同域請求的方式,並將引數放到 head 或 body 裡。這樣,雖然規避了 url 長度的隱患,但是 WKWebView 並不支援這樣的方式。

【注2】:為什麼選擇 iframe.src 不選擇 locaiton.href ?因為如果通過 location.href 連續呼叫 Native,很容易丟失一些呼叫。

4.1.2 Native 呼叫 JavaScript

相比於 JavaScript 呼叫 Native, Native 呼叫 JavaScript 較為簡單,畢竟不管是 iOS 的 UIWebView 還是 WKWebView,還是 Android 的 WebView 元件,都以子元件的形式存在於 View/Activity 中,直接呼叫相應的 API 即可。

Native 呼叫 JavaScript,其實就是執行拼接 JavaScript 字串,從外部呼叫 JavaScript 中的方法,因此 JavaScript 的方法必須在全域性的