摘要:前言實現階段之端的實現,重點描述這個項目的端都有些什么內容,是如何實現的。這個項目中,基于進行單元測試,而且并不是測試驅動,而是在確定好內容后,對核心部分的代碼都進行單測。
前言
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%的代碼覆蓋率。
具體的代碼這里不贅述,可以參考源碼
返回根目錄【quickhybrid】如何實現一個Hybrid框架
源碼github上這個框架的實現
quickhybrid/quickhybrid
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/90463.html
閱讀 947·2021-09-26 09:55
閱讀 3192·2021-09-22 15:36
閱讀 2982·2021-09-04 16:48
閱讀 3142·2021-09-01 11:41
閱讀 2591·2019-08-30 13:49
閱讀 1491·2019-08-29 18:46
閱讀 3546·2019-08-29 17:28
閱讀 3425·2019-08-29 14:11