溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

通過Proxy如何實現(xiàn)JSBridge模塊化封裝

發(fā)布時間:2020-10-26 15:28:21 來源:億速云 閱讀:195 作者:Leah 欄目:開發(fā)技術(shù)

今天就跟大家聊聊有關(guān)通過Proxy如何實現(xiàn)JSBridge模塊化封裝,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

由于是Hybrid APP的性質(zhì),所以web與Native的通信是無法避免的;而為什么我要封裝jsBridge,主要在于下面兩點:

公司APP的JSBridge提供了數(shù)據(jù)的序列化和全局函數(shù)的注入,而我們這次由于包大小考慮,這一塊需要H5自己來實現(xiàn);

原生提供的接口協(xié)議太多,記住麻煩;

回調(diào)的寫法不太人性化,期望Promise;

由于本次項目只涉及到Andriod,所以沒有關(guān)于ios的處理,但我自認為他們只是協(xié)議的不同,Web的處理可以相同。

原理淺談

通過Proxy如何實現(xiàn)JSBridge模塊化封裝

看上圖的通信實現(xiàn)(圖片來源于文章開頭的文章),簡單說一下通信過程;

Webview加載時會將原生提供的JSBridge方法注入到window對象上,比如:window.JSBridge.getDeviceInfo就是原生提供的可以讀取一些設(shè)備標(biāo)識信息的接口;

H5通過window調(diào)用原生接口,基本都需要傳參,比如這次處理成功或則處理失敗的結(jié)果回調(diào)的,還有一些參數(shù)設(shè)置,拿上面給的方法來舉例:

window.JSBridge.getDeviceInfo({
 token: '*&^%$$#*',
 onOk(data) {
  save(data);
 },
 onError(error) {
  console.log(error.message);
 }
});

原生響應(yīng)H5的調(diào)用成功或失敗后,就執(zhí)行H5傳遞過來的回調(diào)函數(shù);

過程結(jié)束;

看上面的通信過程,貌似很簡單。但這里面存在一些協(xié)議的問題:

首先H5與原生端的通信消息,是只支持字符串的,如果要傳JS對象,那就先序列化;

序列化帶來的后果又是,對象中的函數(shù)就無法傳遞;

而就算函數(shù)傳過去了,也是存在問題的,由于安全的限制,webview和js的執(zhí)行沒有在一個容器中,回調(diào)這種局部函數(shù)是找不到的,所以是需要將回調(diào)函數(shù)注冊到全局;

所以下面就來解決這些問題

一步一步的具體實現(xiàn)

接口協(xié)議封裝

什么意思喃?看下面的圖:

通過Proxy如何實現(xiàn)JSBridge模塊化封裝

由于APP端協(xié)議及分包問題, 存在多個Bridge, 比如MBDevice、MBControl、MBFinance,上面列出來的只是一小部分,對于web來說記憶這些接口是一件很費事的事;還有就是以前我調(diào)APP的JSBridge, 總有下面這樣的代碼:

window.JSBridge && window.JSBridge.getDeviceInfo && window.JSBridge.getDeviceInfo({ ... })

至于上面,所以加了一層封裝,實現(xiàn)的核心就是Proxy和Map,具體實現(xiàn)看下面的偽代碼:

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);

基于上面的封裝,調(diào)用時,代碼就是下面這樣

sdk.setMaxTime({
   maxTime: 10,
  }).then(() => {
   console.log('設(shè)置成功');
  }, () => {
   window.alert('調(diào)用失敗');
  });

序列化與回調(diào)注冊

上面已經(jīng)列了為什么需要回調(diào)函數(shù)全局注冊和序列化,這里主要說一下實現(xiàn)原理,總得來說分兩步;

回調(diào)函數(shù)剝離,全局注冊;

參數(shù)序列化;

回調(diào)函數(shù)剝離和參數(shù)序列化

其實很好實現(xiàn),直接展開運算符搞定:

const { onOk, onError, ...others } = params; // 回調(diào)函數(shù)剝離
const str = JSON.stringify(others); // 參數(shù)序列化

函數(shù)全局注冊

看了很多文章的一些實現(xiàn),思路基本一致,比如下面這樣

window.bridgeCallbacks = {};
const callBacks = window.bridgeCallbacks;
const { onOk, onError, ...others } = params; // 回調(diào)函數(shù)剝離

const callbackId = generateId(); // 產(chǎn)生一個唯一的隨機數(shù)Id

callBacks[`success_${callbackId}`] = onOk;
callBacks[`onError${callbackId}`] = onError;

others.success = `window.bridgeCallbacks.success_${callbackId}`
// ....
// 調(diào)用jdk代碼

這是一種很容易想到的問題,但卻存在一些問題,比如:

bridgeCallbacks全局會注冊很多屬性,因為Native調(diào)用并沒有清理,而onOk這種很多時候是一個閉包,由于有引用,最后導(dǎo)致的問題就是內(nèi)存泄露;

就算處理了第一步的問題,webview無響應(yīng)怎么辦,那回調(diào)就會被一直掛起,確少超時響應(yīng)邏輯

callbackId的唯一性不好保證;

基于以上考慮,我換了一個方案,采用回調(diào)隊列,因為APP端說過,回調(diào)是按順序的,不會插隊;

class CallHeap {
 constructor() {
  this.okQueue = [];
  this.errorQueue = [];
 }
 success = (args) => {
  // 成對彈出回調(diào):成功時,不止要處理成功的回調(diào),失敗的也要同時彈出,
  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; // 回調(diào)函數(shù)剝離
 if (onOk || onError) {
   const callKey = transferKey || key; // transferKey || key后面會提到
   // 如果全局未注冊,則先注冊對應(yīng)的調(diào)用域
   if (!callbacks[callKey]) {
    callbacks[callKey] = new CallHeap();
   }
   // 添加回調(diào)
   callbacks[callKey].addQueue(onOk, onError);

   others.success = `callBacks.${callKey}.success`;
   others.error = `callBacks.${callKey}.error`;
  }
  // 調(diào)用jdk代碼
}

基于以上的實現(xiàn),就可以保證發(fā)起多個Native請求,并保證有序回調(diào);如果成功,成功回調(diào)被響應(yīng)時,響應(yīng)的失敗回調(diào)也會被彈出,因為回調(diào)函數(shù)式存在數(shù)組中的,所以執(zhí)行完后,引用就不會再存在。

完整實現(xiàn)

看了上面的代碼實現(xiàn),但核心好像還沒有提及,那就是調(diào)用參數(shù)的攔截。前面我們用Proxy的get優(yōu)雅的實現(xiàn)了SDK方法的攔截,這里會接著采用Proxy的apply方法來攔截方法調(diào)用的傳參,直接看代碼吧:

// 結(jié)合最上面接口協(xié)議封裝的代碼一起看
const applyHandler = {
 apply(target, object, args) {
  // transferKey 用于getFinaceInfo與getDeviceInfo這種數(shù)據(jù)命名重復(fù)的
  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 封裝

前面吹過的牛逼還有兩個沒實現(xiàn),比如:

promise支持

超時調(diào)用

首先來復(fù)習(xí)一下,怎么封裝一個支持Promise的setTimeout函數(shù):

function promiseTimeOut(time) {
 return new Promise((resolve, reject) => {
  setTimeout(resolve, time);
 });
}

promiseTimeOut(1000).then(() => {
 console.log('time is ready');
})

如果對上面這個封裝不陌生,那基于回調(diào)函數(shù)的Promise化就變得簡單了

talk is cheap, show me your code

完整實現(xiàn):

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這種數(shù)據(jù)命名重復(fù)的
  const [parentKey, key, transferKey] = target();
  // FYX 編程
  const func = (window[parentKey] || {})[key];
  // 設(shè)置一個默認的超時參數(shù),支持配置
  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(() => {
    // 超時,主動發(fā)起錯誤回調(diào)
    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);

而調(diào)用時,基本上,就可以這樣玩了:

sdk.setMaxTime({
   maxTime: 10,
  }).then(() => {
   console.log('設(shè)置成功');
  }, () => {
   window.alert('調(diào)用失敗');
  });

解惑

- func.call(null, JSON.stringify(params)) // 以前的

+ func && (window[parentKey][key])(JSON.stringify(params)); // 現(xiàn)在的

開始函數(shù)的調(diào)用是采用func.call來實現(xiàn)的,當(dāng)時我本地mock過,沒有問題。但在webview中就彈出了下面這樣一個錯誤:

java bridge method can't be invoked on a non-injected object

經(jīng)過各種goggle,百度,查到的都是一條關(guān)于Andriod的注入漏洞。而至于我這里通過JS的方式把bridge指向的函數(shù)地址,賦值給一個變量名,然后再通過變量名來調(diào)用就會報上面這個錯誤,我個人的猜測有兩個:一是協(xié)議這樣規(guī)定的;二是this指向問題。

看完上述內(nèi)容,你們對通過Proxy如何實現(xiàn)JSBridge模塊化封裝有進一步的了解嗎?如果還想了解更多知識或者相關(guān)內(nèi)容,請關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI