【quickhybrid】API多平臺支撐的實現

撒網要見魚發表於2017-12-25

前言

在框架規劃時,就有提到過這個框架的一些常用功能需要支援H5環境下的呼叫,也就是需要實現API的多平臺支撐

為什麼要多平臺支撐?核心仍然是複用程式碼,比如在微信下,在釘釘下,在quick容器下,
如果沒有多平臺支撐,那麼quick.ui.alert只能用於quick容器下,釘釘和微信下就得分別用其它程式碼實現,
程式碼複用率低,如果實現了多平臺支撐。那麼三個平臺中同一個功能的程式碼則是一樣的。

什麼樣的多平臺支撐

當然了,本框架實現的多平臺支撐和一般框架的有點區別。

__一般的框架中支援多個平臺更多的是一個polyfill__,譬如

// 假設以前不支援h5
const oldToast = quick.ui.toast;

quick.ui.toast = function(...) {
    if (os.h5) {
        // 做一些h5中做的
        ...
    } else {
        oldToast(...);
    }
};

這就是墊片實現,如果是新的環境,用新的實現,否則用老的實現

而__本框架中的多平臺實現是直接內建到了框架核心中__,也就是說框架本身就支援多平臺API的設定

quick.extendModule(`ui`, [{
    namespace: `toast`,
    os: [`h5`],
    defaultParams: {
        message: ``,
    },
    runCode(...rest) {
        // 定義h5環境中的做法
        ...
    },
}, ...];

quick.extendModule(`ui`, [{
    namespace: `toast`,
    os: [`quick`],
    defaultParams: {
        message: ``,
    },
    runCode(...rest) {
        // 定義quick環境中的做法
        ...
    },
}, ...];

在框架內部定義API時,不再是直接的quick.ui.alert = xxx,而是通過特定的API單獨給某個環境下定義實現

而且,框架中的定義,每一個API都是有quickh5環境下的實現的。

多平臺支撐的核心

從上述的介紹中也可以看到,多平臺支撐主要是前端的實現,與原生API,原生API在這裡面只能算一個環境下的實現

核心就是基於:__Object.defineProperty,重寫set和get__

Object.defineProperty(apiParent, apiName, {
    configurable: true,
    enumerable: true,
    get: function proxyGetter() {
        // 需要根據不同的環境,返回對應下的內容
        ...
    },
    set: function proxySetter() {
        // 可以提示禁止修改API
    },
});

本框架中的多平臺實現程式碼可以參考原始碼,這裡不贅述,下文中會介紹如何簡單的實現一個多平臺支撐API

實現一個多平臺支撐API

我們先預設最終的結果:

quick.os.quick = true;
quick.ui.alert(`hello`); // quick-hello

quick.os.quick = false;
quick.ui.alert(`hello`); // h5-hello

quick.ui.alert = 11; // 提示:不允許修改quick API

那麼要達到上述的要求,應該如何做呢?

寫一個雛形

最簡單的,先假設這些實現都已經存在,然後直接基於defineProperty返回

function alertH5(message) {
    alert(`h5-` + message);
}
function alertQuick(message) {
    alert(`quick-` + message);
}
const quick = {};

quick.ui = {};
quick.os = {
    quick: false,
};

Object.defineProperty(quick.ui, `alert`, {
    configurable: true,
    enumerable: true,
    get: function proxyGetter() {
        // 需要根據不同的環境,返回對應下的內容
        if (quick.os.quick) {
            return alertQuick;
        } else {
            return alertH5;
        }
    },
    set: function proxySetter() {
        // 可以提示禁止修改API
        alert(`不允許修改quick API`);
    },
});

那麼,它的呼叫結果是

quick.os.quick = true;
quick.ui.alert(`hello`); // quick-hello

quick.os.quick = false;
quick.ui.alert(`hello`); // h5-hello

quick.ui.alert = 11; // 提示:不允許修改quick API

雖然效果和預設的一樣,但是很明顯還需優化完善

增加擴充API的方法

擴充方式的定義如下

const quick = {};

quick.os = {
    quick: false,
};
/**
 * 存放所有的代理 api物件
 * 每一個名稱空間下的每一個os都可以執行
 * proxyapi[namespace][os]
 */
const proxysApis = {};
// 支援的所有環境
const supportOsArray = [`quick`, `h5`];

function getCurrProxyApiOs(currOs) {
    for (let i = 0, len = supportOsArray.length; i < len; i += 1) {
        if (currOs[supportOsArray[i]]) {
            return supportOsArray[i];
        }
    }

    // 預設是h5
    return `h5`;
}

// 如獲取quick.ui.alert
function getModuleApiParentByNameSpace(module, namespace) {
    let apiParent = module;
    // 只取名稱空間的父級,如果僅僅是xxx,是沒有父級的
    const parentNamespaceArray = /[.]/.test(namespace) ? namespace.replace(/[.][^.]+$/, ``).split(`.`) : [];

    parentNamespaceArray.forEach((item) = >{
        apiParent[item] = apiParent[item] || {};
        apiParent = apiParent[item];
    });

    return apiParent;
}

function proxyApiNamespace(apiParent, apiName, finalNameSpace) {
    // 代理API,將apiParent裡的apiName代理到Proxy執行
    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`);
        },
    });
}

function extendApi(moduleName, apiParam) {
    if (!apiParam || !apiParam.namespace) {
        return;
    }

    if (!quick[moduleName]) {
        quick[moduleName] = {};
    }

    const api = apiParam;
    const modlue = quick[moduleName];
    const apiNamespace = api.namespace;
    const apiParent = getModuleApiParentByNameSpace(modlue, apiNamespace);
    // 最終的名稱空間是包含模組的
    const finalNameSpace = moduleName + `.` + apiNamespace;
    // 如果僅僅是xxx,直接取xxx,如果aa.bb,取bb
    const apiName = /[.]/.test(apiNamespace) ? api.namespace.match(/[.][^.]+$/)[0].substr(1) : apiNamespace;

    // 這裡防止觸發代理,就不用apiParent[apiName]了,而是用proxysApis[finalNameSpace]
    if (!proxysApis[finalNameSpace]) {
        // 如果還沒有代理這個API的名稱空間,代理之,只需要設定一次代理即可
        proxyApiNamespace(apiParent, apiName, finalNameSpace);
    }

    // 一個新的API代理,會替換以前API名稱空間中對應的內容
    const apiRuncode = api.runCode;
    const oldProxyNamespace = proxysApis[finalNameSpace] || {};

    proxysApis[finalNameSpace] = {};

    supportOsArray.forEach((osTmp) = >{
        if (api.os && api.os.indexOf(osTmp) !== -1) {
            // 如果存在這個os,並且合法,重新定義
            proxysApis[finalNameSpace][osTmp] = apiRuncode;
        } else if (oldProxyNamespace[osTmp]) {
            // 否則仍然使用老版本的代理
            proxysApis[finalNameSpace][osTmp] = oldProxyNamespace[osTmp];
        }
    });
}

function extendModule(moduleName, apis) {
    if (!apis || !Array.isArray(apis)) {
        return;
    }
    if (!quick[moduleName]) {
        quick[moduleName] = [];
    }
    for (let i = 0, len = apis.length; i < len; i += 1) {
        extendApi(moduleName, apis[i]);
    }

}

quick.extendModule = extendModule;

上述程式碼中增加了些複雜度,有一個統一管理所有代理呼叫的池,然後每次會更新對於環境下的代理

基於上述的方式可以如下擴充對於環境下的API

quick.extendModule(`ui`, [{
    namespace: `alert`,
    os: [`h5`],
    defaultParams: {
        message: ``,
    },
    runCode(message) {
        alert(`h5-` + message);
    },
}]);

quick.extendModule(`ui`, [{
    namespace: `alert`,
    os: [`quick`],
    defaultParams: {
        message: ``,
    },
    runCode(message) {
        alert(`quick-` + message);
    },
}]);

最終的呼叫如下(結果和預期一致)

quick.os.quick = true;
quick.ui.alert(`hello`); // quick-hello
quick.os.quick = false;
quick.ui.alert(`hello`); // h5-hello
quick.ui.alert = 11; // 提示:不允許修改quick API

雖然就一兩個API來說,這類擴充方式看起來很複雜,但是當API一多,特別是還需批量預處理時(如預設引數,Promise支援等),它的優勢就出來了

多平臺支撐在quick中的應用

quick hybrid框架中,預設支援quickh5有種環境,核心程式碼就是上述列舉的(當然,內部增加了一些代理,預設引數處理等,會稍微複雜一點)。

基於這個核心,然後可以將框架的定義和API定義分開打包

quick.js
quick.h5.js

這樣,最終看起來h5下的API定義就是一個擴充包,是沒有它也不會影響quick環境下的使用,而且,如果增加一個新的環境(比如dd),
只需要再新增另一個環境的擴充包而已,各種寫法都是一樣的,這樣便於了統一維護

返回根目錄

原始碼

github上這個框架的實現

quickhybrid/quickhybrid

相關文章