【前端基礎】Web與Native交互之The JSBridge FAQ

今天咱們來簡單聊一下JSBridgejavascript

爲何要聊JSBridge?

不爲何前端

好吧,JSBridge雖然也算比較古老了,但關於JSBridge的原理也是一個目前做爲一名前端開發人員須要瞭解掌握的知識。java

現現在,在作移動端H5開發時,少不了與Native之間進行交互,這裏的Native,包括了傳統意義上的App和現現在各類各樣的坑王小程序們。 一般,各大公司都會封裝一套本身的js sdk,用於提供給Web開發人員,來實現與Native之間的交互,他們可能有本身的名字,但他們統稱爲JSBridgegit

網上有不少介紹關於JSBridge原理的文章,充斥着大量OC 或Java代碼,對於沒有作過移動端Native開發但想了解JSBridge原理的同窗來講都不是很友好。 因此,這篇文章主要就來給沒有Native開發經驗的同窗們介紹一下H5頁面在WebView中是如何經過JSBridge與Native進行交互的。同時,爲了不見到沒必要要的Native代碼,這裏不過多介紹經過JavaScriptCore API來實現交互的方式了,只介紹最初經典的方案。github

什麼是JSBridge?

簡單的來講,JSBridge是一種H5頁面與Native之間異步雙向的通訊的方式,它與咱們平常接觸到的最多見的HTTP這種通訊方式,本質上沒什麼區別。web

JSBridge存在的意義是啥?

爲了生活變的更美好json

爲了體驗更好小程序

爲了讓用戶分不清他用的究竟是Web App仍是Native App數組

爲了效率app

搞開發的嘛,確定都是爲了提升工做效率,同時不下降太多用戶體驗的狀況下,複用,複用這個複用那個,統一這個統一那個的。一款App產品要作好幾個端,重複的頁面扔給H5算了!啥!須要一些奇特的功能?作個bridge接口吧!

JSBridge的通訊過程是什麼樣的?

image

簡單來講,這個通訊過程與在餐廳吃飯時廚師上菜的流程相似,廚師通常不會親自爲你上菜(土豪私廚請忽略),廚師們每作好一道菜後就會按一下取餐鈴,服務員聽到鈴聲後就會過來取餐併爲客人上菜。

一般狀況下,按一下鈴就會有服務員過來把菜取走,有些時候,廚師們可能同時作好了N道菜,按了N次鈴,這時,服務員也會很敬業的將這些菜所有打包取走並處理。

那麼它是如何實現的?

接下來該進入正題了,咱們來看一下JSBridge的基本原理

  • 先從web 向 native發起通訊開始:

前面咱們瞭解到JSBridge的本質就是一種通訊方式,因此,這裏就比較容易理解了,H5調用Native的本質就是請求攔截

當H5的世界想與外面交流時,他只須要(也只能)發送一個請求,好比發送一個簡單的GET請求便可。

咱們會想到三種發請求的方式:

  1. 使用帶有src屬性標籤發送請求,如iframe...
const iframe = document.createElement('iframe')
    iframe.src = "xxx"
複製代碼

這種方式也是各大Hybrid框架經常使用的方式,重複大量發送也不須要擔憂消息的丟失問題

  1. 使用location.href發送請求
location.href = "xxx"
複製代碼

這種方式比較適用於一些一次性調用的場景,例如H5中某個操做須要跳轉至App的某一個頁面,經過這種方式重複發送大量請求會形成請求消息的丟失,只接受最後一次。

  1. 使用Ajax的方式來發送請求
const url = 'xxx'
    fetch(url, { ... })
        .then()
        .catch()
複製代碼

這種方式寫起來比較麻煩,但性能上略好於前兩種

著名的Cordova.js使用的方式:

execIframe = document.createElement('iframe')
execIframe.style.display = 'none'
execIframe.src = 'gap://ready'
document.body.appendChild(execIframe)
複製代碼

iOS中的一個叫WebViewJavascriptBridge的庫一樣使用的是相似的方式:

const messagingIframe = document.createElement('iframe')
messagingIframe.src = 'https://__wvjb_queue_message__'
body.appendChild(messagingIframe)
複製代碼

注意到Cordova的src與下面的WebViewJavascriptBridge中的src不一樣的地方在於,Cordova使用了自定義URL Scheme的方式,嗯,這種方式用來喚起本地安裝的App更常見些。

  • 須要帶點參數

多數狀況下,咱們更但願給Native帶一些參數過去,因此接下來,咱們來看參數傳遞的問題

可能你會想到,在發送請求的時候直接把參數放在請求地址後面,經過query的形式拼接起來就能夠了,好比像這樣:

execIframe.src = 'gap://ready?p1=v1&p2=v2&p3=v3…'
複製代碼

但這種方式存在着一些問題

  • 首先,它的長度是有限的,雖然限制很長,但這終歸會是一個隱患

  • 其次,若是是本身公司的App臨時作一個jsbridge的接口,確實能夠這樣寫,可是若是做爲一個通用的工具來封裝,這樣去實現的話就與業務耦合的太緊密了。

在典型的JSBridge實現方案中,關於參數的處理是這樣實現的:

  • 首先,任什麼時候候,H5中JS須要調用Native時,發送請求的url是固定不變的,好比gap://ready

  • 其次,在window上定義一個全局的數組變量,名叫messageQueue,初始化時爲空,當H5須要給Native發送消息時,先建立一個對象,並把全部相關的參數放在這個對象中,而後將這個對象插入messageQueue數組隊尾,用代碼來解釋就是這樣:

const messageQueue = []
window.messageQueue = messageQueue

messageQueue.push(JSON.stringify({
	message: 'xxx',
	params: 'xxx'
}))
// 發一個請求,按一下鈴,戳一下Native~~
execIframe.src = 'gap://ready'
複製代碼
  • 當native收到gap://ready的請求後,就知道H5有新消息,就會執行一段神奇的代碼,進入到WebView中,並將定義在window上的全局變量messageQueue數組中所有數據打包取走,並將messageQueue清空,取走後逐條解析執行。咱們用eval函數來充當這段神奇的代碼來解釋這裏的邏輯:
// 當Native攔截到'gap://ready'請求後執行的magic code
const messageQueue = eval('window.messageQueue')
const messages = JSON.parse(messageQueue)
for (const message in messages) {
     doSomeThingWithMessage(message)
     …
}
eval('window.messageQueue = []')
複製代碼

這樣,菜就被服務員端走了, 消息就被Native取走了

  • 接下來看看Native如何將處理結果告訴H5

若是餐廳的一個負責任的廚師須要讓他的客戶快要吃完一道前菜時告訴他,以便他去及時準備主菜,只須要在上菜時放上本身的名片,讓客戶快吃完的時候把廚師的名片交給服務員就能夠了。

一樣,若是H5須要Native執行完某一條指令時通知到H5,那麼H5只須要在window上準備一個回調函數,在裏面作該作的事,並將這個回調函數的名字在上一步建立消息對象時,放進這個對象中:

messageQueue.push(JSON.stringify({
	message: 'xxx',
	params: 'xxx',
	callBackName: 'xxx',
}))
複製代碼

這樣,在Native執行完你須要的指令後會再次執行那段神奇的代碼進入WebView的世界,執行定義在window上名爲callbackName的方法,並把native執行的結果傳給這個方法。就像這樣:

const messageQueue = eval('window.messageQueue')
const messages = JSON.parse(messageQueue)
for (const message in messages) {
     const result = doSomeThingWithMessage(message)
     eval(`window[${message.callbackName}](${result})`)
     …
}
eval('window.messageQueue = []')
複製代碼

同時,這也就揭露了Native是如何給H5發送消息的,直接執行window上定義好的一個方法便可。

固然,爲了代碼更規範,保證H5不胡亂的建立callBackName,Native並非直接執行window上的callbackName方法,而是會調用一個大概叫handleMessageFromNative的方法,這個方法是H5這邊提早準備並定義在window上的方法,在這個方法中對消息的處理進行了收口,在裏面調用window上的callbackName方法,執行完成後,將callbackName方法從window上刪除掉 ,整個流程的代碼大概是這樣的:

// H5
function handleMessageFromNative (message) {
	if (typeof message.callbackName === 'function') {
		window[callbackName](message.result)
		delete window[callbackName]
	}
}

window.handleMessageFromNative = handleMessageFromNative

複製代碼
// Native
const messageQueue = eval('window.messageQueue')
const messages = JSON.parse(messageQueue)

for (const message in messages) {
     const result = doSomeThingWithMessage(message)
     
     const messageFromNative = JSON.stringify({
        result,
        callbackName: message.callbackName
     })
     
     eval(`window.handleMessageFromNative(${messageFromNative})`)
     …
}
eval('window.messageQueue = []')
複製代碼

這裏關於callbackName的生成也有一點規則,感興趣能夠去擼一下相關源碼。大概和jsonp的規則相似。

接下來,爲了方便他人使用,將以上的流程整理封裝完善一下,H5和Native同時暴露兩個接口,便成了以下的樣子:

// H5與Native同時增長以下兩個接口供對方使用:
// ≈ function addEventListener(eventName, callback)
function registerHandler (handlerName, block) {
    window.handlers[handlerName] = block
    …
}

// Web或Native調用對方接口的方式
// ≈ dispatchEvent(eventName, data, callback)
function callHandler (handlerName, message, callback) {
    window.handlers[handlerName](message)
    …
}
複製代碼

這樣就能夠很方便的使用了,例如要實現一個掃描二維碼的功能:

// Native
// 註冊了一個掃描二維碼的方法
registerHanlder('scanQRCode', () => {
    // ...
    Camera.open().scanQRCode()
    // ...
})
複製代碼
// H5
// 調用掃描二維碼的方法
callHanlder('scanQRCode', { type: 'qrcode' }, result => {
    console.log('掃碼結果:', result)
})
複製代碼

喜歡的話能夠用Promise封裝一下:

// 爲H5封裝好的bridge-sdk.js,在H5中使用
/** * 掃描二維碼並返回結果 * ... * @memberOf Camera * @async * @returns {Promise} 能夠在then中接受掃碼結果`result`,參數爲 { code: 'xxxxxx' } * ... */
export async function scanQRCode () {
    return new Promise((resolve, reject) => {
        callHanlder('scanQRCode', { type: 'qrcode' }, result => {
            console.log('掃碼結果:', result)
            resolve(result)
        })
    })
}
複製代碼

好了,以上就是經典的JSBridge的實現方案,看起來很是的簡單,且沒有兼容性問題。

既然Native有神奇的代碼,有沒有更完全些的辦法呢?

有!!!Native中有另外一個神奇的API,咱們暫且稱它爲defineFunc函數吧,它能夠直接將Native的代碼注入到H5的載體WebView中,並掛在WebView的window上。

// define 翻譯過來大概就是下面的這個意思
function defineFunc (funcName, func) {
    const window = webView.window ... // 經過一些Native的API拿到WebView的window
    window[funcName] = func // 這裏的func 是Native的func,執行的是純Native的代碼
}

// Native
defineFunc('callSomeNativeFunction', () => {
    // 這些是由Native的代碼翻譯成javascript的僞代碼
    const file = io.readFile('/path/to/file')
    ...
    // 作一些H5作不到的事情
    file.write('/path/to/file', 'content')
    ...
})

複製代碼

這就是利用如iOS中JavaScriptCore的API來實現交互的原理,安卓也有相似的方式,對系統版本有些許的要求,能夠忽略不計。這裏就不討論了。

什麼??Native能夠隨意到WebView中執行代碼?這個bug是否是Native亂搞搞出來的?細思極恐啊!

H5:天吶,咱們原來活在一個虛擬的世界裏!!!在鄙視鏈的最低端!!

是的!讓我想起了《黑客帝國》,啥?沒看過?暴露年齡了?

咱們生活的世界究竟是真實的嗎?

關於咱們

快狗打車前端團隊專一前端技術分享,按期推送高質量文章,歡迎關注點贊。

公衆號二維碼
相關文章
相關標籤/搜索