【quickhybrid】JS端的項目實現

前言

API實現階段之JS端的實現,重點描述這個項目的JS端都有些什麼內容,是如何實現的。前端

不一樣於通常混合框架的只包含JSBridge部分的前端實現,本框架的前端實現包括JSBridge部分、多平臺支持,統一預處理等等。ios

項目的結構

在最初的版本中,其實整個前端庫就只有一個文件,裏面只規定着如何實現JSBridge和原生交互部分。可是到最新的版本中,因爲功能逐步增長,單一文件難以知足要求和維護,所以重構成了一整個項目。git

整個項目基於ES6Airbnb代碼規範,使用gulp + rollup構建,部分重要代碼進行了Karma + Mocha單元測試github

總體目錄結構以下:web

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和核心代碼打包到一塊兒)gulp

quick.js
quick.h5.js
quick.native.js
複製代碼

這裏須要注意,quick.xx環境.js中的代碼是基於quick.js核心代碼的(譬如裏面須要用到一些特色的快速調用底層的方法)api

而其中最核心的quick.js代碼架構以下promise

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對象而且參數的最後加上resolvereject

並且,後續若是有新的統一預處理(調用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標識

  • modulemethod分別表明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上這個框架的實現

quickhybrid/quickhybrid

相關文章
相關標籤/搜索