跨端技能必備之JSBridge

JSBridge & Fusion

JSBridge

在移動端H5開發中,咱們常會調用一些(Android/iOS)端上的功能,這些功能有的是用H5沒法實現的,有的純粹是懶得再用H5開發一遍。JSBridge的目標便是在H5中以某種方式喚起這些端上的方法。android

H5與Native通訊

話很少說,先上代碼,這段代碼實現的功能便是在H5中喚起Native方法:web

// bridgeCall.js
function schemeJump (url) { // 經過iframe子窗口執行URL
  let iframe = document.createElement('iframe')
  iframe.src = url
  iframe.style.display = 'none'
  document.documentElement.appendChild(iframe)
  setTimeout(() => {
    document.documentElement.removeChild(iframe)
  }, 0)
}

export default function bridgeCall (type, module, method, args) {
  let url = `fusion://${type}?`
  if (module) {
    url += `module=${module}&`
  }
  url += `method=${method}&`
  let param = args.map((arg) => {
    return encodeURIComponent(JSON.stringify(arg))
  })
  url += `arguments=%5B${param}%5D&`
  url += `origin=${window.location.hostname}`
  schemeJump(url)
}
複製代碼

在解釋這段代碼以前,咱們先來了解一些背景知識。ajax

WebView

在Android中,有個名爲android.webkit.WebView的組件,它繼承自android.widget.AbsoluteLayout,容許開發者在裏面加載和展現一些H5頁面。假如將WebView鋪滿整個頁面,全部的視圖表現都由H5來實現,那麼Android端所須要提供的就僅僅是一個WeView容器。與此同時,一份代碼徹底能夠跨瀏覽器和Android雙端運行,大大下降了開發成本。從iOS 8.0起,iOS也實現了WKWebView,它與其餘WebView的特性和用法類似。自此,基於WebView的移動端H5開發也就成爲了跨多端(瀏覽器、Android、iOS等)的最佳實踐。瀏覽器

WebView做爲承載H5頁面的容器,有一個特性是很是重要,即 它能夠捕捉到全部在容器中發起的網絡請求。其實想要 JS喚起Native 的方法,只要創建起 JS與Native通訊 的橋樑便可,而這一點正好被WebView的這一特性所實現。bash

傳遞消息

咱們能夠經過 發起網絡請求來向Native端傳遞消息,如在上述代碼中經過子窗口iframe.src來發起請求。固然,使用location.href也能夠發起請求,不過因爲location.href做爲當前頁面的地址,因此並不推薦使用。網絡

function schemeJump (url) { // 經過iframe子窗口發起網絡請求
  let iframe = document.createElement('iframe')
  iframe.src = url
  iframe.style.display = 'none' // 不顯示iframe
  document.documentElement.appendChild(iframe)
  setTimeout(() => {
    document.documentElement.removeChild(iframe)
  }, 0)
}
複製代碼

在與Native通訊的時候,能夠將一些參數傳入,在當前場景中,有通訊類型、模塊名、方法名和入參等。閉包

/** * @param type 通訊類型 * @param module 模塊名 * @param method 方法名 * @param args 入參 */
function bridgeCall (type, module, method, args) {
  let url = `fusion://${type}?` // 約定的協議
  if (module) {
    url += `module=${module}&`
  }
  url += `method=${method}&`
  let param = args.map((arg) => {
    return encodeURIComponent(JSON.stringify(arg))
  })
  url += `arguments=%5B${param}%5D&`
  url += `origin=${window.location.hostname}`
  schemeJump(url)
}
複製代碼

Native端在捕捉到這種協議頭的請求時,會進行解析,僞代碼以下:app

IF url 匹配 "fusion://"
  DO 解析參數 type,module,method,args
  IF type === "invokeNative"
    DO 執行模塊方法 FUNCS[module][method](args)
  END IF
END IF
複製代碼

咱們也可使用ajax來發送網絡請求,只要Native端與H5同步一套解析規則便可。不過在實際開發中,因爲ajax用於和服務端進行交互,因此最好仍是使用iframe子窗口來發送請求。學習

執行回調

在喚起Native方法後,每每還須要執行一些回調,因爲客戶端沒法直接執行JS代碼,但能夠獲取WebView中的 全局變量,所以能夠將回調方法掛載在全局變量上,以後客戶端調用全局變量上的回調方法就能夠了。ui

咱們能夠在全局設置一個單例的管理者,用其管理全部Native調用後的回調,以便於處理同時調用多個Native方法的複雜狀況。

// manager.js
function isFunction (func) {
  return Object.prototype.toString.call(callback) === '[object Function]'
}

export default {
  GLOBAL_INSTANCE: {
    callbacks: [],
    callbackId: 0
  },
  callbackJs (callbackId, args) {
    let callback = this.globalInstance.callbacks[callbackId]
    if (callback && isFunction(callback)) {
      callback()
    }
  }
}
複製代碼

接着對Native方法喚起進一步封裝,順便將其回調記錄下來,這裏用到了閉包(裝飾器)模式。

// jsBridge.js
import manager from './manager.js'
import bridgeCall from './bridgeCall.js'

window.manager = manager // 掛載到全局,以便端上調用
function isFunction (func) {
  return Object.prototype.toString.call(callback) === '[object Function]'
}

export default class JSBridge {
    constructor () {}
    invokeNative (module, method) {
        return function (...args) { // 持有形參變量module, method
            for (var i = 0; i < args.length; i++) {
                if (isFunction(args[i])) {
                    args = args.slice(0, +i + 1)
                    manager.GLOBAL_INSTANCE.callbacks.push(args[i]) // 記錄回調方法
                    args[i] = manager.GLOBAL_INSTANCE.callbackId++ // 將回調索引傳到端上
                    break
                }
            }
            bridgeCall('invokeNative', module, method, args)
        }
    }
}
複製代碼

調用示例以下:

import JSBridge from './jsBridge.js'
let bridge = new JSBridge()
let requestLogin = bridge.invokeNative('COMMON_MODULE', 'requestLogin')
requestLogin({ saveSession: true }, function (res) {
    // do sth
})
複製代碼

在喚起Native方法的時候,咱們須要知道客戶端到底支持哪些方法,端上應該給出一個API列表。

Fusion

在大型平臺中,每每會有多個客戶端,端與端之間是獨立開發的,每一個客戶端各有一套獨立的接口,這些接口可能名稱不同、入參不同,也可能回調參數不同。

若是一個H5頁面在不一樣的端內喚起Native方法時須要書寫不一樣的代碼,那就毫無複用性可言。此外,學習各個端實現同一功能的不一樣API對開發者來講也是一種很大的負擔和浪費。

在這種狀況下,平臺每每會推出一個組件用來 整合各個環境的bridge,容許開發者以一種統一的形式來調用不一樣端的實現相同功能的方法,這個組件通常被稱爲 Fusion(聚合物)

假若有一個功能爲選擇本地圖片的方法:

模塊(非必需) 名稱 參數 回調參數
環境A common_module photograph opts: { type } Blob
環境B local_module chooseImage opts: { suffix } dataUrl
環境C other_module openAlbum opts: {} dataUrl
  • 在環境A中,photograph的傳入屬性type值爲1時表示開啓攝像頭,值爲2時表示打開相冊,爲 必填項;回調參數爲Blob對象格式的拍攝或被選圖片。
  • 在環境B中,chooseImage的功能爲打開相冊,入參屬性suffix用於限制可選圖片的後綴,非必填,不配置時默認容許選擇全部後綴的圖片;回調參數爲dataUrl字符串格式的被選圖片。
  • 在環境C中,openAlbum的功能爲打開相冊,無入參,回調參數爲Blob對象格式的被選圖片。

本着最大化兼容的原則構造一下統一接口,獲得的結果應該爲

名稱 參數 回調參數
chooseImage { type, suffix } dataUrl
  • 在被調用時,方法內部首先將進行環境的判斷,根據環境喚起不一樣端上的Native方法。
  • 因爲屬性type只在環境A中起做用,所以做爲統一接口的參數時它是非必填的。當判斷是環境A時,傳入一個默認值。
  • 因爲dataUrl格式是經常使用到的,因此這裏使用window.URL.createObjectURL(blob)將blob對象轉化爲dataUrl字符串。在處理其餘方法也應根據習慣來作兼容。

橋接模式

講到這裏,有必要提一下這種結構所使用的模式,見名知意,JSBridge與橋接模式脫不了干係。

橋接模式用於將抽象與實現解耦,使得兩者能夠獨立變化,屬於結構型模式。

這種模式涉及到一個做爲橋接的接口,使得功能實現能夠獨立於接口實現,這兩種類型的類可被結構化改變而互不影響。

在這裏,抽象指的是JSBridge對接口的抽象,而實現則指的是客戶端上對功能的實現。H5只須要了解如何調用JSBridge的方法便可,無需關心客戶端上是如何實現這些功能的,也無需關心這些功能的實現是否有所變化。只要JSBridge能夠維持住調用方式(名稱、傳入參數、回調參數等)不發生變化,H5和客戶端兩端代碼就能夠獨立於彼此隨便修改,這是一種十分鬆散的結構。

下圖簡單演示了JSBridge的中間做用。

相關文章
相關標籤/搜索