在移動端H5開發中,咱們常會調用一些(Android/iOS)端上的功能,這些功能有的是用H5沒法實現的,有的純粹是懶得再用H5開發一遍。JSBridge的目標便是在H5中以某種方式喚起這些端上的方法。android
話很少說,先上代碼,這段代碼實現的功能便是在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
在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列表。
在大型平臺中,每每會有多個客戶端,端與端之間是獨立開發的,每一個客戶端各有一套獨立的接口,這些接口可能名稱不同、入參不同,也可能回調參數不同。
若是一個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 |
本着最大化兼容的原則構造一下統一接口,獲得的結果應該爲
名稱 | 參數 | 回調參數 |
---|---|---|
chooseImage | { type, suffix } | dataUrl |
window.URL.createObjectURL(blob)
將blob對象轉化爲dataUrl字符串。在處理其餘方法也應根據習慣來作兼容。講到這裏,有必要提一下這種結構所使用的模式,見名知意,JSBridge與橋接模式脫不了干係。
橋接模式用於將抽象與實現解耦,使得兩者能夠獨立變化,屬於結構型模式。
這種模式涉及到一個做爲橋接的接口,使得功能實現能夠獨立於接口實現,這兩種類型的類可被結構化改變而互不影響。
在這裏,抽象指的是JSBridge對接口的抽象,而實現則指的是客戶端上對功能的實現。H5只須要了解如何調用JSBridge的方法便可,無需關心客戶端上是如何實現這些功能的,也無需關心這些功能的實現是否有所變化。只要JSBridge能夠維持住調用方式(名稱、傳入參數、回調參數等)不發生變化,H5和客戶端兩端代碼就能夠獨立於彼此隨便修改,這是一種十分鬆散的結構。
下圖簡單演示了JSBridge的中間做用。