從 微信 JS-SDK 認識 JSBridge

熊的貓發表於2023-02-13

前言

前段時間由於要實現 H5 移動端拉取微信卡包並同步卡包資料的功能,於是在專案中引入了 微信 JS-SDK(jweixin) 相關包實現功能,但也由此讓我對其產生了好奇心,於是打算好好了解下相關的內容,透過查閱相關資料發現這其實屬於 JSBridge 的一種實現方式。

因此,只要瞭解 JSBridge 就能明白 微信 JS-SDK 是怎麼一回事。

898C8D35.jpg

為什麼需要 JSBridge?

相信大多數人都有相同的經歷,第一次瞭解到關於 JSBridge 都是從 微信 JS-SDK(WeiXinJSBridge) 開始,當然如果你從事的是 Hybrid 應用React-Native 開發的話相信你自然(應該、會)很瞭解。

其實 JSBridge 早就出現並被實際應用了,如早前桌面應用的訊息推送等,而在移動端盛行的時代已經越來越需要 JSBridge,因為我們期望移動端(Hybrid 應用React-Native)能做更多的事情,其中包括使用 客戶端原生功能 提供更好的 互動 和 服務 等。

然而 JavaScript 並不能直接呼叫和它不同語言(如 Java、C/C++ 等)提供的功能特性,因此需要一箇中間層去實現 JavaScript其他語言 間的一個相互協作,這裡透過一個 Node 架構來進行說明。

Node 架構

5

核心內容如下:

  • 頂層 Node Api

    • 提供 http 模組、流模組、fs檔案模組等等,可以透過 JavaScript 直接呼叫
  • 中間層 Node Bindings

    • 主要是使 JavaScript 和 C/C++ 進行通訊,原因是 JavaScript 無法直接呼叫 C/C++ 的庫(libuv),需要一箇中間的橋樑,node 中提供了很多 binding,這些稱為 Node bindings
  • 底層 V8 + libuv

    • v8 負責解釋、執行頂層的 JavaScript 程式碼
    • libuv 負責提供 I/O 相關的操作,其主要語言是 C/C++ 語言,其目的就是實現一個 跨平臺(如 Windows、Linux 等)的非同步 I/O 庫,它直接與作業系統進行互動

這裡不難發現 Node Bindings 就有點類似 JSBridge 的功能,所以 JSBridge 本身是一個很簡單的東西,其更多的是 一種形式、一種思想

為什麼叫 JSBridge?

Stack Overflow 聯合創始人 Jeff Atwood 在 2007 年的部落格《The Principle of Least Power》中認為 “任何可以使用 JavaScript 來編寫的應用,並最終也會由 JavaScript 編寫”,後來 JavaScript 的發展確實非常驚人,現在我們可以基於 JavaScript 來做各種事情,比如 網頁、APP、小程式、後端等,並且各種相關的生態越來越豐富。

作為 Web 技術邏輯核心的 JavaScript 自然而然就需要承擔與 其他技術 進行『橋接』的職責,而且任何一個 移動作業系統 中都會包含 執行 JavaScript 的容器環境,例如 WebViewJSCore 等,這就意味著 執行 JavaScript 不用像執行其他語言時需要額外新增相應的執行環境。

JSBridge 應用在國內真正流行起來則是因為 微信 的出現,當時微信的一個主要功能就是可以在網頁中透過 JSBridge 來實現 內容分享

JSBridge 能做什麼?

舉個最常見的前端和後端的例子,後端只提供了一個查詢介面,但是沒有提供更新介面,那麼對於前端來講就是再想實現更新介面,也是沒有任何法子的!

同樣的,JSBridge 能做什麼得看原生端給 JavaScript 提供呼叫 Native 什麼功能的介面,比如透過 微信 JS-SDK 網頁開發者可藉助微信使用 拍照、選圖、語音、位置 等手機系統的能力,同時可以直接使用 微信分享、掃一掃、卡券、支付 等微信特有的能力。

JSBridge 作為 JavaScriptNative 之間的一個 橋樑,表面上看是允許 JavaScript 呼叫 Native 的功能,但其核心是建立 Native非 Native 間訊息 雙向通訊 通道。

1

雙向通訊的通道

  • JavaScript 向 Native 傳送訊息:

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

    • 回溯呼叫結果
    • 訊息推送
    • 通知 JavaScript 當前 Native 的狀態等

JSBridge 是如何實現的?

JavaScript 的執行需要 JS 引擎的支援,包括 Chrome V8Firefox SpiderMonkeySafari JavaScriptCore 等,總之 JavaScript 執行環境 是和 原生執行環境 是天然隔離的,因此,在 JSBridge 的設計中我們可以把它 類比JSONP 的流程:

  • 客戶端透過 JavaScript 定義一個回撥函式,如: function callback(res) {...},並把這個回撥函式的名稱以引數的形式傳送給服務端
  • 服務端獲取到 callback 並攜帶對應的返回資料,以 JS 指令碼形式返回給客戶端
  • 客戶端接收並執行對應的 JS 指令碼即可

JSBridge 實現 JavaScript 呼叫的方式有兩種,如下:

  • JavaScript 呼叫 Native
  • Native 呼叫 JavaScript

在開始分析具體內容之前,還是有必要了解一下前置知識 WebView

WebView 是什麼?

WebView 是 原生系統 用於 移動端 APP 嵌入 Web 的技術,方式是內建了一款高效能 webkit 核心瀏覽器,一般會在 SDK 中封裝為一個 WebView 元件。

WebView 具有一般 View 的屬性和設定外,還對 url 進行請求、頁面載入、渲染、頁面互動進行增強處理,提供更強大的功能。

WebView 的優勢 在於當需要 更新頁面佈局業務邏輯發生變更 時,能夠更便捷的提供 APP 更新:

  • 對於 WebView 而言只需要修改前端部分的 Html、Css、JavaScript 等,通知使用者端進行重新整理即可
  • 對於 Native 而言需要修改前端內容後,再進行打包升級,重新發布,通知使用者下載更新,安裝後才可以使用最新的內容

微信小程式中的 WebView

小程式的主要開發語言是 JavaScript ,其中 邏輯層渲染層 是分開的,分別執行在不同的執行緒中,而其中的渲染層就是執行在 WebView 上:

執行環境邏輯層渲染層
iOSJavaScriptCoreWKWebView
安卓V8chromium 定製核心
小程式開發者工具NWJSChrome WebView

898E2DC5.jpg

在開發過程中遇到的一個 坑點 就是

  • 在真機中,需要實現同一域名下不同子路徑的應用實現資料互動(純前端操作,不涉及介面),由於同域名且是基於同一個頁面進行跳轉的(當然只是看起來是),而且這個資料是 臨時資料,因此覺得使用 sessionStorage 實現資料互動是很合適的
  • 實際上從 A 應用 跳轉到 B 應用 中卻無法獲取對應的資料,而這是因為 sessionStorage 是基於當前視窗的會話級的資料儲存,移動端瀏覽器 或 微信內建瀏覽器 中在跳轉新頁面時,可能開啟的是一個新的 WebView,這就相當於我們在瀏覽器中的一個新視窗中進行儲存,因此是沒辦法讀取在之前的視窗中儲存的資料

JavaScript 呼叫 Native — 實現方案一

透過 JavaScript 呼叫 Native 的方式,又會分為:

  • 注入 API
  • 劫持 URL Scheme
  • 彈窗攔截

【 注入 API 】

核心原理

  • 透過 WebView 提供的介面,向 JavaScript 的上下文(window)中注入 物件 或者 方法
  • 允許 JavaScript 進行呼叫時,直接執行相應的 Native 程式碼邏輯,實現 JavaScript 呼叫 Native

這裡不透過 iOSUIWebViewWKWebView 注入方式來介紹了,感興趣可以自行查詢資料,我們們這裡直接透過 微信 JS-SDK 來看看。

898E8B37.gif

當透過 <script src="https://res2.wx.qq.com/open/js/jweixin-1.4.0.js"></script> 的方式引入 JS-SDK 之後,就可以在頁面中使用和 微信相關的 API,例如:

// 微信授權
window.wx.config(wechatConfig)

// 授權回撥
window.wx.ready(function () {...})

// 異常處理
window.wx.error(function (err) {...})

// 拉起微信卡包
window.wx.invoke('chooseInvoice', invokeConf, function (res) {...})

如果透過其內部編譯打包後的程式碼(簡化版)來看的話,其實不難發現:

  • 其中的 this(即引數 e)此時就是指向全域性的 window 物件
  • 在程式碼中使用的 window.wx 實際上是 e.jWeixin 也是其中定義的 N 物件
  • 而在 N 物件中定義的各種方法實際上又是透過 e.WeixinJSBridge 上的方法來實際執行的
  • e.WeixinJSBridge 就是由 微信內建瀏覽器 向 window 物件中注入 WeiXinJsBridge 介面實現的

    !(function (e, n) {
    'function' == typeof define && (define.amd || define.cmd)
      ? define(function () {
          return n(e)
        })
      : n(e, !0)
    })(this, function (e, n) {
     ...
     function i(n, i, t) {
      e.WeixinJSBridge
        ? WeixinJSBridge.invoke(n, o(i), function (e) {
            c(n, e, t)
          })
        : u(n, t)
     }
     
     if (!e.jWeixin) {
      
      var N = {
          config(){
            i(...)
          },
          ready(){},
          error(){},
          ...
      }
      
      return (
        S.addEventListener(
          'error',callback1,
          !0
        ),
        S.addEventListener(
          'load',callback2,
          !0
        ),
        n && (e.wx = e.jWeixin = N),
        N
      )
    }
    })

【 劫持 URL Scheme 】

URL Scheme 是什麼?

URL Scheme 是一種特殊的 URL,一般用於在 Web 端喚醒 App(或是跳轉到 App 的某個頁面),它能方便的實現 App 間互相呼叫(例如 QQ 和 微信 相互分享訊息)。

URL Scheme 的形式和 普通 URL(如:https://www.baidu.com)相似,主要區別是 protocolhost 一般是對應 APP 自定義的。

通常當 App 被安裝後會在系統上註冊一個 自定義的 URL Scheme,比如 weixin:// 這種,所以我們在手機瀏覽器裡面訪問這個 scheme 地址,系統就會喚起對應的 App

例如,當在瀏覽器中訪問 weixin:// 時,瀏覽器就會詢問你是否需要開啟對應的 APP:

3

劫持原理

Web 端透過某種方式(如 iframe.src)傳送 URL Scheme 請求,之後 Native 攔截到請求並根據 URL Scheme攜帶的引數 進行對應操作。

例如,對於谷歌瀏覽器可以透過 chrome://version/、chrome://chrome-urls/、chrome://settings/ 定位到不同的頁面內容,假設 跳轉到谷歌的設定頁並期望當前搜尋引擎改為百度,可以這樣設計 chrome://settings/engine?changeTo=baidu&callbak=callback_id:

  • 谷歌客戶端可以攔截這個請求,去解析對應引數 changeTo 來修改預設引擎
  • 然後透過 WebView 上面的 callbacks 物件來根據 callback_id 進行回撥

以上只是一個假設哈,並不是說真的可以這樣去針對谷歌瀏覽器進行修改,當然它要是真的支援也不是不可以。

是不是感覺確實和 JSONP 的流程很相似呀 ~ ~

【 彈窗攔截 】

彈窗攔截核心:利用彈窗會觸發 WebView 相應事件來實現的

一般是在透過攔截 Prompt、Confirm、Alert 等方法,然後解析它們傳遞過來的訊息,但這種方法存在的缺陷就是 iOS 中的 UIWebView 不支援,而且 iOS 中的 WKWebView 又有更好的 scriptMessageHandler,因此很難統一。

Native 呼叫 JavaScript — 實現方案二

Native 呼叫 JavaScript 的方式本質就是 執行拼接 JavaScript 字串,這就好比我們透過 eval() 函式來執行 JavaScript 字串形式的程式碼一樣,不同的系統也有相應的方法執行 JavaScript 指令碼。

Android

Android 中需要根據版本來區分:

  • 安卓 4.4 之前的版本使用 loadUrl()

    • loadUrl() 不能獲取 JavaScript 執行後的結果,這種方式更像在 <a href="javascript:void(0)">href 屬性中編寫的 JavaScript 程式碼

      webView.loadUrl("javascript:foo()")
  • 安卓 4.4 以上版本使用 evaluateJavascript()

    webView.evaluateJavascript("javascript:foo()", null);

    IOS

    IOS 中需要根據不同的 WebView 進行區分:

  • UIWebView 中通常使用 stringByEvaluatingJavaScriptFromString

    results = [self.webView stringByEvaluatingJavaScriptFromString:"foo()"];
  • WKWebView 中通常使用 evaluateJavaScript

    [self.webView evaluateJavaScript:@"document.body.offsetHeight;" completionHandler:^(id _Nullable response, NSError * _Nullable error) {
        // 獲取返回值
    }];

    最後

    以上透過 微信 JS-SDKJSBridge 的一個簡單介紹,大家現在應該不至於認為 JSBridge 是一個高大上、深不可測的東西了,畢竟其核心思想是清晰明瞭的,而且本質上還是需要強依賴於原生端的具體實現。

希望本文對你有所幫助!!!

本文參與了SegmentFault 思否寫作挑戰賽,歡迎正在閱讀的你也加入。

相關文章