關於JSBridge的一些基礎知識,在網絡上有不少文章能夠參考:java
最近公司在作一個項目,經過把咱們本身的Webview植入第三方APP,而後咱們的業務所有經過H5實現。至於爲何不直接用第三方APP WebView,主要是身處金融行業,須要作一些風控相關功能。 ios
因爲是Hybrid APP的性質,因此web與Native的通訊是沒法避免的;而爲何我要封裝JSBridge,主要在於下面兩點:git
因爲本次項目只涉及到Andriod,因此沒有關於ios的處理,但我自認爲他們只是協議的不一樣,Web的處理能夠相同。github
看上圖的通訊實現(圖片來源於文章開頭的文章),簡單說一下通訊過程;web
H5經過window調用原生接口,基本都須要傳參,好比此次處理成功或則處理失敗的結果回調的,還有一些參數設置,拿上面給的方法來舉例:編程
window.JSBridge.getDeviceInfo({ token: '*&^%$$#*', onOk(data) { save(data); }, onError(error) { console.log(error.message); } });
看上面的通訊過程,貌似很簡單。但這裏面存在一些協議的問題:segmentfault
回調函數註冊到全局
;因此下面就來解決這些問題數組
什麼意思喃?看下面的圖: promise
因爲APP端協議及分包問題, 存在多個Bridge, 好比MBDevice、MBControl、MBFinance,上面列出來的只是一小部分,對於web來講記憶這些接口是一件很費事的事;還有就是之前我調APP的JSBridge, 總有下面這樣的代碼:安全
window.JSBridge && window.JSBridge.getDeviceInfo && window.JSBridge.getDeviceInfo({ ... })
至於上面,因此加了一層封裝,實現的核心就是Proxy和Map,具體實現看下面的僞代碼:
const MBSDK = { }; // sdk 提供的方法白名單 const whiteList = new Map([ ['setMaxTime', 'MBVideo'], ['getDeviceInfo', 'MBDevice.getInfo'], ['close', 'MBControl'], ['getFinaceInfo', 'MBFinance.getInfo'], ]); const handler = { get(target, key) { if (!whiteList.has(key)) { throw new Error('方法不存在'); } const parentKey = whiteList.get(key); function callback() { return [...parentKey.split('.'), key]; } return new Proxy(callback, applyHandler); // funcHandler後面再展開 }, }; export default new Proxy(MBSDK, handler);
基於上面的封裝,調用時,代碼就是下面這樣
sdk.setMaxTime({ maxTime: 10, }).then(() => { console.log('設置成功'); }, () => { window.alert('調用失敗'); });
上面已經列了爲何須要回調函數全局註冊和序列化,這裏主要說一下實現原理,總得來講分兩步;
其實很好實現,直接展開運算符搞定:
const { onOk, onError, ...others } = params; // 回調函數剝離 const str = JSON.stringify(others); // 參數序列化
看了不少文章的一些實現,思路基本一致,好比下面這樣
window.bridgeCallbacks = {}; const callBacks = window.bridgeCallbacks; const { onOk, onError, ...others } = params; // 回調函數剝離 const callbackId = generateId(); // 產生一個惟一的隨機數Id callBacks[`success_${callbackId}`] = onOk; callBacks[`onError${callbackId}`] = onError; others.success = `window.bridgeCallbacks.success_${callbackId}` // .... // 調用jdk代碼
這是一種很容易想到的問題,但卻存在一些問題,好比:
基於以上考慮,我換了一個方案,採用回調隊列,由於APP端說過,回調是按順序的,不會插隊;
class CallHeap { constructor() { this.okQueue = []; this.errorQueue = []; } success = (args) => { // 成對彈出回調:成功時,不止要處理成功的回調,失敗的也要同時彈出, const target = this.okQueue.shift(); this.errorQueue.shift(); target && target(args); } error = (args) => { const target = this.errorQueue.shift(); this.okQueue.shift(); target && target(args); } addQueue(onOk = Null, onError = Null) { this.okQueue.push(onOk); this.errorQueue.push(onError); } } window.bridgeCallbacks = {}; const callBacks = window.bridgeCallbacks; function applyhandler() { const { onOk, onError, ...others } = params; // 回調函數剝離 if (onOk || onError) { const callKey = transferKey || key; // transferKey || key後面會提到 // 若是全局未註冊,則先註冊對應的調用域 if (!callbacks[callKey]) { callbacks[callKey] = new CallHeap(); } // 添加回調 callbacks[callKey].addQueue(onOk, onError); others.success = `callBacks.${callKey}.success`; others.error = `callBacks.${callKey}.error`; } // 調用jdk代碼 }
基於以上的實現,就能夠保證發起多個Native請求,並保證有序回調;若是成功,成功回調被響應時,響應的失敗回調也會被彈出,由於回調函數式存在數組中的,因此執行完後,引用就不會再存在。
看了上面的代碼實現,但核心好像尚未說起,那就是調用參數的攔截。前面咱們用Proxy的get優雅的實現了SDK方法的攔截,這裏會接着採用Proxy的apply方法來攔截方法調用的傳參,直接看代碼吧:
// 結合最上面接口協議封裝的代碼一塊兒看 const applyHandler = { apply(target, object, args) { // transferKey 用於getFinaceInfo與getDeviceInfo這種數據命名重複的 const [parentKey, key, transferKey] = target(); console.log('res', parentKey, key); const func = (SDK[parentKey] || {})[key]; const { onOk, onError, ...params } = args[0] || {}; if (onOk || onError) { const callKey = transferKey || key; if (!callbacks[callKey]) { callbacks[callKey] = new CallHeap(); } callbacks[callKey].addQueue(onOk, onError); others.success = `callBacks.${callKey}.success`; others.error = `callBacks.${callKey}.error`; } return func && (window[parentKey][key])(JSON.stringify(params));; } };
前面吹過的牛逼還有兩個沒實現,好比:
首先來複習一下,怎麼封裝一個支持Promise的setTimeout函數:
function promiseTimeOut(time) { return new Promise((resolve, reject) => { setTimeout(resolve, time); }); } promiseTimeOut(1000).then(() => { console.log('time is ready'); })
若是對上面這個封裝不陌生,那基於回調函數的Promise化就變得簡單了
talk is cheap, show me your code
完整實現:
const MBSDK = { }; // sdk 提供的方法白名單 const whiteList = new Map([ ['setMaxTime', 'MBVideo'], ['getDeviceInfo', 'MBDevice.getInfo'], ['close', 'MBControl'], ['getFinaceInfo', 'MBFinance.getInfo'], ]); const applyHandler = { apply(target, object, args) { // transferKey 用於getFinaceInfo與getDeviceInfo這種數據命名重複的 const [parentKey, key, transferKey] = target(); // FYX 編程 const func = (window[parentKey] || {})[key]; // 設置一個默認的超時參數,支持配置 const { timeout = 5000, ...params } = args[0] || {}; return new Promise((resolve, reject) => { const callKey = transferKey || key; if (!callbacks[callKey]) { callbacks[callKey] = new CallHeap(); } const timeoutId = setTimeout(() => { // 超時,主動發起錯誤回調 window.callBacks[callKey].error({ message: '請求超時' }); }, timeout); callbacks[callKey].addQueue((data) => { clearTimeout(timeoutId); resolve(data); }, (data) => { clearTimeout(timeoutId); reject(data); }); params.success = `callBacks.${callKey}.success`; params.error = `callBacks.${callKey}.error`; func && (window[parentKey][key])(JSON.stringify(params)); }).catch((error) => { console.log('error:', error.message); }); } }; const handler = { get(target, key) { if (!whiteList.has(key)) { throw new Error('方法不存在'); } const parentKey = whiteList.get(key); function callback() { return [...parentKey.split('.'), key]; } return new Proxy(callback, applyHandler); // funcHandler後面再展開 }, }; export default new Proxy(MBSDK, handler);
而調用時,基本上,就能夠這樣玩了:
sdk.setMaxTime({ maxTime: 10, }).then(() => { console.log('設置成功'); }, () => { window.alert('調用失敗'); });
- func.call(null, JSON.stringify(params)) // 之前的 + func && (window[parentKey][key])(JSON.stringify(params)); // 如今的
開始函數的調用是採用func.call來實現的,當時我本地mock過,沒有問題。但在webview中就彈出了下面這樣一個錯誤:
java bridge method can't be invoked on a non-injected object
通過各類goggle,百度,查到的都是一條關於Andriod的注入漏洞。而至於我這裏經過JS的方式把bridge指向的函數地址,賦值給一個變量名,而後再經過變量名來調用就會報上面這個錯誤,我我的的猜想有兩個:一是協議這樣規定的;二是this指向問題。
若是有知道爲何的大佬,還請不吝賜教,謝謝。
經過這一次的封裝,本身對Proxy的應用更加熟練了,將所學的知識運用到工做中,不得不說是一件很是愉快的事情。這也是本身第二篇關於ES6深刻理解的文章;
第一篇: 從新認識ES6中的Set
原文見:issue , 若有不嚴謹之處,還請及時指正。