前言
API實現階段之JS端的實現,重點描述這個專案的JS端都有些什麼內容,是如何實現的。
不同於一般混合框架的只包含JSBridge部分的前端實現,本框架的前端實現包括JSBridge部分、多平臺支援,統一預處理等等。
專案的結構
在最初的版本中,其實整個前端庫就只有一個檔案,裡面只規定著如何實現JSBridge和原生互動部分。但是到最新的版本中,由於功能逐步增加,單一檔案難以滿足要求和維護,因此重構成了一整個專案。
整個專案基於ES6
、Airbnb程式碼規範
,使用gulp + rollup
構建,部分重要程式碼進行了Karma + Mocha
單元測試
整體目錄結構如下:
quickhybrid
|- dist // 釋出目錄
| |- quick.js
| |- quick.h5.js
|- build // 構建專案的相關程式碼
| |- gulpfile.js
| |- rollupbuild.js
|- src // 核心原始碼
| |- api // 各個環境下的api實現
| | |- h5 // h5下的api
| | |- native // quick下的api
| |- core // 核心控制
| | |- ... // 將核心程式碼切割為多個檔案
| |- inner // 內部用到的程式碼
| |- util // 用到的工具類
|- test // 單元測試相關
| |- unit
| | |- karma.xxx.config.js
| |- xxx.spec.js
| |- ...
複製程式碼
程式碼架構
專案代中將核心程式碼和API實現程式碼分開,核心程式碼相當於一個處理引擎,而各個環境下的不同API實現可以單獨掛載(這裡是為了方便其它地方組合不同環境下的API所以才分開的,實際上可以將native和核心程式碼打包到一起)
quick.js
quick.h5.js
quick.native.js
複製程式碼
這裡需要注意,quick.xx環境.js
中的程式碼是基於quick.js
核心程式碼的(譬如裡面需要用到一些特點的快速呼叫底層的方法)
而其中最核心的quick.js
程式碼架構如下
index
|- os // 系統判斷相關
|- promise // promise支援,這裡並沒有重新定義,而是判斷環境中是否已經支援來決定是否支援
|- error // 統一錯誤處理
|- proxy // API的代理物件,內部對進行統一預處理,如預設引數,promise支援等
|- jsbridge // 與native環境下原生互動的橋樑
|- callinner // API的預設實現,如果是標準的API,可以不傳入runcode,內部預設採用這個實現
|- defineapi // API的定義,API多平臺支撐的關鍵,也約定著該如何擴充
|- callnative // 定義一個呼叫通用native環境API的方法,擴充元件API(自定義)時需要這個方法呼叫
|- init // 裡面定義config,ready,error的使用
|- innerUtil // 給核心檔案繫結一些內部工具類,供不同API實現中使用
複製程式碼
可以看到,核心程式碼已經被切割成很小的單元了,雖然說最終打包起來總共程式碼也沒有多少,但是為了維護性,簡潔性,這種拆分還是很有必要的
統一的預處理
在上一篇API多平臺的支撐
中有提到如何基於Object.defineProperty
實現一個支援多平臺呼叫的API,實現起來的API大致是這樣子的
Object.defineProperty(apiParent, apiName, {
configurable: true,
enumerable: true,
get: function proxyGetter() {
// 確保get得到的函式一定是能執行的
const nameSpaceApi = proxysApis[finalNameSpace];
// 得到當前是哪一個環境,獲得對應環境下的代理物件
return nameSpaceApi[getCurrProxyApiOs(quick.os)] || nameSpaceApi.h5;
},
set: function proxySetter() {
alert('不允許修改quick API');
},
});
...
quick.extendModule('ui', [{
namespace: 'alert',
os: ['h5'],
defaultParams: {
message: '',
},
runCode(message) {
alert('h5-' + message);
},
}]);
複製程式碼
其中nameSpaceApi.h5
的值是api.runCode
,也就是說直接執行runCode(...)
中的程式碼
僅僅這樣是不夠的,我們需要對呼叫方法的輸入等做統一預處理,因此在這裡,我們基於實際的情況,在此基礎上進一步完善,加上統一預處理
機制,也就是
const newProxy = new Proxy(api, apiRuncode);
Object.defineProperty(apiParent, apiName, {
...
get: function proxyGetter() {
...
return newProxy.walk();
}
});
複製程式碼
我們將新的執行程式碼變為一個代理物件Proxy
,代理api.runCode,然後在get時返回代理過後的實際方法(.walk()
方法代表代理物件內部會進行一次統一的預處理)
代理物件的程式碼如下
function Proxy(api, callback) {
this.api = api;
this.callback = callback;
}
Proxy.prototype.walk = function walk() {
// 實時獲取promise
const Promise = hybridJs.getPromise();
// 返回一個閉包函式
return (...rest) = >{
let args = rest;
args[0] = args[0] || {};
// 預設引數的處理
if (this.api.defaultParams && (args[0] instanceof Object)) {
Object.keys(this.api.defaultParams).forEach((item) = >{
if (args[0][item] === undefined) {
args[0][item] = this.api.defaultParams[item];
}
});
}
// 決定是否使用Promise
let finallyCallback;
if (this.callback) {
// 將this指標修正為proxy內部,方便直接使用一些api關鍵引數
finallyCallback = this.callback;
}
if (Promise) {
return finallyCallback && new Promise((resolve, reject) = >{
// 擴充 args
args = args.concat([resolve, reject]);
finallyCallback.apply(this, args);
});
}
return finallyCallback && finallyCallback.apply(this, args);
};
};
複製程式碼
從原始碼中可以看到,這個代理物件統一預處理了兩件事情:
-
1.對於合法的輸入引數,進行預設引數的匹配
-
2.如果環境中支援Promise,那麼返回Promise物件並且引數的最後加上
resolve
,reject
而且,後續如果有新的統一預處理(呼叫API前的預處理),只需在這個代理物件的這個方法中增加即可
JSBridge解析規則
前面的文章中有提到JSBridge的實現,但那時其實更多的是關注原理層面,那麼實際上,定義的互動解析規則是什麼樣的呢?如下
// 以ui.toast實際呼叫的示例
// `${CUSTOM_PROTOCOL_SCHEME}://${module}:${callbackId}/${method}?${params}`
const uri = 'QuickHybridJSBridge://ui:9527/toast?{"message":"hello"}';
if (os.quick) {
// 依賴於os判斷
if (os.ios) {
// ios採用
window.webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage(uri);
} else {
window.top.prompt(uri, '');
}
} else {
// 瀏覽器
warn(`瀏覽器中jsbridge無效, 對應scheme: ${uri}`);
}
複製程式碼
原生容器中接收到對於的uri後反解析即可知道呼叫了些什麼,上述中:
-
QuickHybridJSBridge
是本框架互動的scheme標識 -
module
和method
分別代表API的模組名和方法名 -
params
是對於方法傳遞的額外引數,原生容器會解析成JSONObject -
callbackId
是本次API呼叫在H5端的回撥id,原生容器執行完後,通知H5時會傳遞迴調id,然後H5端找到對應的回撥函式並執行
為什麼要用uri的方式,因為這種方式可以相容以前的scheme方式,如果方案切換,變動代價下(本身就是這樣升級上來的,所以沒有替換的必要)
UA約定
混合開發容器中,需要有一個UA標識位來判斷當前系統。
這裡Android和iOS原生容器統一在webview中加上如下UA標識(也就是說,如果容器UA中有這個標識位,就代表是quick環境-這也是os判斷的實現原理)
String ua = webview.getSettings().getUserAgentString();
ua += " QuickHybridJs/" + getVersion();
// 設定瀏覽器UA,JS端通過UA判斷是否屬於quick環境
webview.getSettings().setUserAgentString(ua);
複製程式碼
// 獲取預設UA
NSString *defaultUA = [[UIWebView new] stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
NSString *version = [[NSBundle mainBundle].infoDictionary objectForKey:@"CFBundleShortVersionString"];
NSString *customerUA = [defaultUA stringByAppendingString:[NSString stringWithFormat:@" QuickHybridJs/%@", version]];
[[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent":customerUA}];
複製程式碼
如上述程式碼中分別在Android和iOS容器的UA中新增關鍵性的標識位。
API內部做了些什麼
API內部只做與本身功能邏輯相關的操作,這裡有幾個示例
quick.extendModule('ui', [{
namespace: 'toast',
os: ['h5'],
defaultParams: {
message: '',
},
runCode(...rest) {
// 相容字串形式
const args = innerUtil.compatibleStringParamsToObject.call(this, rest, 'message', );
const options = args[0];
const resolve = args[1];
// 實際的toast實現
toast(options);
options.success && options.success();
resolve && resolve();
},
}, ...]);
複製程式碼
quick.extendModule('ui', [{
namespace: 'toast',
os: ['quick'],
defaultParams: {
message: '',
},
runCode(...rest) {
// 相容字串形式
const args = innerUtil.compatibleStringParamsToObject.call(this, rest, 'message');
quick.callInner.apply(this, args);
},
}, ...]);
複製程式碼
以上是toast功能在h5和quick環境下的實現,其中,在quick環境下唯一做的就是相容了一個字串形式的呼叫,在h5環境下則是完全的實現了h5下對應的功能(promise也需自行相容)
為什麼h5中更復雜?因為quick環境中,只需要拼湊成一個JSBridge命令傳送給原生即可,具體功能由原生實現,而h5的實現是需要自己完全實現的。
另外,其實在quick環境中,上述還不是最少的程式碼(上述加了一個相容呼叫功能,所以多了幾行),最少程式碼如下
quick.extendModule('ui', [{
namespace: 'confirm',
os: ['quick'],
defaultParams: {
title: '',
message: '',
buttonLabels: ['取消', '確定'],
},
}, ...]);
複製程式碼
可以看到,只要是符合標準的API定義,在quick環境下的實現只需要定義些預設引數就可以了,其它的框架自動幫助實現了(同樣promise的實現也在內部預設處理掉了)
這樣以來,就算是標準quick環境下的API數量多,實際上增加的程式碼也並不多。
關於程式碼規範與單元測試
專案中採用的Airbnb程式碼規範
並不是100%
契合原版,而是基於專案的情況定製了下,但是總體上95%
以上是符合的
還有一塊就是單元測試,這是很容易忽視的一塊,但是也挺難做好的。這個專案中,基於Karma + Mocha
進行單元測試,而且並不是測試驅動,而是在確定好內容後,對核心部分的程式碼都進行單測。
內部對於API的呼叫基本都是靠JS來模擬,對於一些特殊的方法,還需Object.defineProperty(window.navigator, name, prop)
來改變window本身的屬性來模擬。
本專案中的核心程式碼已經達到了100%
的程式碼覆蓋率。
具體的程式碼這裡不贅述,可以參考原始碼
返回根目錄
原始碼
github
上這個框架的實現