摘要:前言實(shí)現(xiàn)階段之端的實(shí)現(xiàn),重點(diǎn)描述這個(gè)項(xiàng)目的端都有些什么內(nèi)容,是如何實(shí)現(xiàn)的。這個(gè)項(xiàng)目中,基于進(jìn)行單元測(cè)試,而且并不是測(cè)試驅(qū)動(dòng),而是在確定好內(nèi)容后,對(duì)核心部分的代碼都進(jìn)行單測(cè)。
前言
API實(shí)現(xiàn)階段之JS端的實(shí)現(xiàn),重點(diǎn)描述這個(gè)項(xiàng)目的JS端都有些什么內(nèi)容,是如何實(shí)現(xiàn)的。
不同于一般混合框架的只包含JSBridge部分的前端實(shí)現(xiàn),本框架的前端實(shí)現(xiàn)包括JSBridge部分、多平臺(tái)支持,統(tǒng)一預(yù)處理等等。
項(xiàng)目的結(jié)構(gòu)在最初的版本中,其實(shí)整個(gè)前端庫(kù)就只有一個(gè)文件,里面只規(guī)定著如何實(shí)現(xiàn)JSBridge和原生交互部分。但是到最新的版本中,由于功能逐步增加,單一文件難以滿足要求和維護(hù),因此重構(gòu)成了一整個(gè)項(xiàng)目。
整個(gè)項(xiàng)目基于ES6、Airbnb代碼規(guī)范,使用gulp + rollup構(gòu)建,部分重要代碼進(jìn)行了Karma + Mocha單元測(cè)試
整體目錄結(jié)構(gòu)如下:
quickhybrid |- dist // 發(fā)布目錄 | |- quick.js | |- quick.h5.js |- build // 構(gòu)建項(xiàng)目的相關(guān)代碼 | |- gulpfile.js | |- rollupbuild.js |- src // 核心源碼 | |- api // 各個(gè)環(huán)境下的api實(shí)現(xiàn) | | |- h5 // h5下的api | | |- native // quick下的api | |- core // 核心控制 | | |- ... // 將核心代碼切割為多個(gè)文件 | |- inner // 內(nèi)部用到的代碼 | |- util // 用到的工具類 |- test // 單元測(cè)試相關(guān) | |- unit | | |- karma.xxx.config.js | |- xxx.spec.js | |- ...代碼架構(gòu)
項(xiàng)目代中將核心代碼和API實(shí)現(xiàn)代碼分開,核心代碼相當(dāng)于一個(gè)處理引擎,而各個(gè)環(huán)境下的不同API實(shí)現(xiàn)可以多帶帶掛載(這里是為了方便其它地方組合不同環(huán)境下的API所以才分開的,實(shí)際上可以將native和核心代碼打包到一起)
quick.js quick.h5.js quick.native.js
這里需要注意,quick.xx環(huán)境.js中的代碼是基于quick.js核心代碼的(譬如里面需要用到一些特點(diǎn)的快速調(diào)用底層的方法)
而其中最核心的quick.js代碼架構(gòu)如下
index |- os // 系統(tǒng)判斷相關(guān) |- promise // promise支持,這里并沒(méi)有重新定義,而是判斷環(huán)境中是否已經(jīng)支持來(lái)決定是否支持 |- error // 統(tǒng)一錯(cuò)誤處理 |- proxy // API的代理對(duì)象,內(nèi)部對(duì)進(jìn)行統(tǒng)一預(yù)處理,如默認(rèn)參數(shù),promise支持等 |- jsbridge // 與native環(huán)境下原生交互的橋梁 |- callinner // API的默認(rèn)實(shí)現(xiàn),如果是標(biāo)準(zhǔn)的API,可以不傳入runcode,內(nèi)部默認(rèn)采用這個(gè)實(shí)現(xiàn) |- defineapi // API的定義,API多平臺(tái)支撐的關(guān)鍵,也約定著該如何拓展 |- callnative // 定義一個(gè)調(diào)用通用native環(huán)境API的方法,拓展組件API(自定義)時(shí)需要這個(gè)方法調(diào)用 |- init // 里面定義config,ready,error的使用 |- innerUtil // 給核心文件綁定一些內(nèi)部工具類,供不同API實(shí)現(xiàn)中使用
可以看到,核心代碼已經(jīng)被切割成很小的單元了,雖然說(shuō)最終打包起來(lái)總共代碼也沒(méi)有多少,但是為了維護(hù)性,簡(jiǎn)潔性,這種拆分還是很有必要的
統(tǒng)一的預(yù)處理在上一篇API多平臺(tái)的支撐中有提到如何基于Object.defineProperty實(shí)現(xiàn)一個(gè)支持多平臺(tái)調(diào)用的API,實(shí)現(xiàn)起來(lái)的API大致是這樣子的
Object.defineProperty(apiParent, apiName, { configurable: true, enumerable: true, get: function proxyGetter() { // 確保get得到的函數(shù)一定是能執(zhí)行的 const nameSpaceApi = proxysApis[finalNameSpace]; // 得到當(dāng)前是哪一個(gè)環(huán)境,獲得對(duì)應(yīng)環(huán)境下的代理對(duì)象 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,也就是說(shuō)直接執(zhí)行runCode(...)中的代碼
僅僅這樣是不夠的,我們需要對(duì)調(diào)用方法的輸入等做統(tǒng)一預(yù)處理,因此在這里,我們基于實(shí)際的情況,在此基礎(chǔ)上進(jìn)一步完善,加上統(tǒng)一預(yù)處理機(jī)制,也就是
const newProxy = new Proxy(api, apiRuncode); Object.defineProperty(apiParent, apiName, { ... get: function proxyGetter() { ... return newProxy.walk(); } });
我們將新的運(yùn)行代碼變?yōu)橐粋€(gè)代理對(duì)象Proxy,代理api.runCode,然后在get時(shí)返回代理過(guò)后的實(shí)際方法(.walk()方法代表代理對(duì)象內(nèi)部會(huì)進(jìn)行一次統(tǒng)一的預(yù)處理)
代理對(duì)象的代碼如下
function Proxy(api, callback) { this.api = api; this.callback = callback; } Proxy.prototype.walk = function walk() { // 實(shí)時(shí)獲取promise const Promise = hybridJs.getPromise(); // 返回一個(gè)閉包函數(shù) return (...rest) = >{ let args = rest; args[0] = args[0] || {}; // 默認(rèn)參數(shù)的處理 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內(nèi)部,方便直接使用一些api關(guān)鍵參數(shù) 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); }; };
從源碼中可以看到,這個(gè)代理對(duì)象統(tǒng)一預(yù)處理了兩件事情:
1.對(duì)于合法的輸入?yún)?shù),進(jìn)行默認(rèn)參數(shù)的匹配
2.如果環(huán)境中支持Promise,那么返回Promise對(duì)象并且參數(shù)的最后加上resolve,reject
而且,后續(xù)如果有新的統(tǒng)一預(yù)處理(調(diào)用API前的預(yù)處理),只需在這個(gè)代理對(duì)象的這個(gè)方法中增加即可
JSBridge解析規(guī)則前面的文章中有提到JSBridge的實(shí)現(xiàn),但那時(shí)其實(shí)更多的是關(guān)注原理層面,那么實(shí)際上,定義的交互解析規(guī)則是什么樣的呢?如下
// 以u(píng)i.toast實(shí)際調(diào)用的示例 // `${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無(wú)效, 對(duì)應(yīng)scheme: ${uri}`); }
原生容器中接收到對(duì)于的uri后反解析即可知道調(diào)用了些什么,上述中:
QuickHybridJSBridge是本框架交互的scheme標(biāo)識(shí)
module和method分別代表API的模塊名和方法名
params是對(duì)于方法傳遞的額外參數(shù),原生容器會(huì)解析成JSONObject
callbackId是本次API調(diào)用在H5端的回調(diào)id,原生容器執(zhí)行完后,通知H5時(shí)會(huì)傳遞回調(diào)id,然后H5端找到對(duì)應(yīng)的回調(diào)函數(shù)并執(zhí)行
為什么要用uri的方式,因?yàn)檫@種方式可以兼容以前的scheme方式,如果方案切換,變動(dòng)代價(jià)下(本身就是這樣升級(jí)上來(lái)的,所以沒(méi)有替換的必要)
UA約定混合開發(fā)容器中,需要有一個(gè)UA標(biāo)識(shí)位來(lái)判斷當(dāng)前系統(tǒng)。
這里Android和iOS原生容器統(tǒng)一在webview中加上如下UA標(biāo)識(shí)(也就是說(shuō),如果容器UA中有這個(gè)標(biāo)識(shí)位,就代表是quick環(huán)境-這也是os判斷的實(shí)現(xiàn)原理)
String ua = webview.getSettings().getUserAgentString(); ua += " QuickHybridJs/" + getVersion(); // 設(shè)置瀏覽器UA,JS端通過(guò)UA判斷是否屬于quick環(huán)境 webview.getSettings().setUserAgentString(ua);
// 獲取默認(rèn)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中添加關(guān)鍵性的標(biāo)識(shí)位。
API內(nèi)部做了些什么API內(nèi)部只做與本身功能邏輯相關(guān)的操作,這里有幾個(gè)示例
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]; // 實(shí)際的toast實(shí)現(xiàn) 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環(huán)境下的實(shí)現(xiàn),其中,在quick環(huán)境下唯一做的就是兼容了一個(gè)字符串形式的調(diào)用,在h5環(huán)境下則是完全的實(shí)現(xiàn)了h5下對(duì)應(yīng)的功能(promise也需自行兼容)
為什么h5中更復(fù)雜?因?yàn)閝uick環(huán)境中,只需要拼湊成一個(gè)JSBridge命令發(fā)送給原生即可,具體功能由原生實(shí)現(xiàn),而h5的實(shí)現(xiàn)是需要自己完全實(shí)現(xiàn)的。
另外,其實(shí)在quick環(huán)境中,上述還不是最少的代碼(上述加了一個(gè)兼容調(diào)用功能,所以多了幾行),最少代碼如下
quick.extendModule("ui", [{ namespace: "confirm", os: ["quick"], defaultParams: { title: "", message: "", buttonLabels: ["取消", "確定"], }, }, ...]);
可以看到,只要是符合標(biāo)準(zhǔn)的API定義,在quick環(huán)境下的實(shí)現(xiàn)只需要定義些默認(rèn)參數(shù)就可以了,其它的框架自動(dòng)幫助實(shí)現(xiàn)了(同樣promise的實(shí)現(xiàn)也在內(nèi)部默認(rèn)處理掉了)
這樣以來(lái),就算是標(biāo)準(zhǔn)quick環(huán)境下的API數(shù)量多,實(shí)際上增加的代碼也并不多。
關(guān)于代碼規(guī)范與單元測(cè)試項(xiàng)目中采用的Airbnb代碼規(guī)范并不是100%契合原版,而是基于項(xiàng)目的情況定制了下,但是總體上95%以上是符合的
還有一塊就是單元測(cè)試,這是很容易忽視的一塊,但是也挺難做好的。這個(gè)項(xiàng)目中,基于Karma + Mocha進(jìn)行單元測(cè)試,而且并不是測(cè)試驅(qū)動(dòng),而是在確定好內(nèi)容后,對(duì)核心部分的代碼都進(jìn)行單測(cè)。
內(nèi)部對(duì)于API的調(diào)用基本都是靠JS來(lái)模擬,對(duì)于一些特殊的方法,還需Object.defineProperty(window.navigator, name, prop)來(lái)改變window本身的屬性來(lái)模擬。
本項(xiàng)目中的核心代碼已經(jīng)達(dá)到了100%的代碼覆蓋率。
具體的代碼這里不贅述,可以參考源碼
返回根目錄【quickhybrid】如何實(shí)現(xiàn)一個(gè)Hybrid框架
源碼github上這個(gè)框架的實(shí)現(xiàn)
quickhybrid/quickhybrid
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/90463.html
閱讀 1013·2021-09-26 09:55
閱讀 3279·2021-09-22 15:36
閱讀 3044·2021-09-04 16:48
閱讀 3228·2021-09-01 11:41
閱讀 2649·2019-08-30 13:49
閱讀 1543·2019-08-29 18:46
閱讀 3598·2019-08-29 17:28
閱讀 3509·2019-08-29 14:11