作為后端云服務提供商,我們在底層通過 REST API 與 WebSocket 提供數據、文件存儲、短信、推送、實時消息等服務。還為各個目標平臺編寫了 SDK 來封裝這些 API,在 SDK 中實現客戶端狀態的持久化,為用戶提供更加符合直覺的抽象。一個有趣的現象是越來越多的平臺使用的都是 JavaScript:
為什么 SDK 要跨平臺?降低成本是為重要的一大原因。對于用戶,提供跨平臺的 SDK 可以降低學習與切換成本。并且,隨著同構應用以及服務端渲染的流行,對于采用這種方案的用戶,跨平臺 SDK 可以方便地作為「平臺無關」代碼進行共享。而對于公司而言,如果能夠在多個平臺中共享這部分代碼,將會減少 SDK 的開發與維護成本。
基于以上前提,我們的目標具體表現為:
接下來,我們分 API、編譯打包、小程序、測試四個部分詳細了解 SDK 在跨平臺實踐中遇到的常見問題及解決方案。
這些平臺都會使用內置或者外部的 JavaScript Engine 來執行 JavaScript 代碼。所有屬于 ECMAScript 標準的 API 都是所有平臺都支持的,比如 Math、Array、TypedArray、Promise、正則表達式。這倒不是指它們使用的是同一個 JavaScript Engine(事實上存在 V8、SpiderMonkey、JSC、Chakra 等各種實現),得益于 TC39 的存在以及 Babel 的出色表現,我們幾乎不需要擔心我們的 JavaScript 代碼在不同平臺上的一致性問題。這也意味著,如果一個第三方庫只使用了 ECMAScript 的 API,那么它一定是跨平臺的,我們可以放心使用,一個典型的例子就 lodash。
ECMAScript 的 API 是語言層面上的,除此之外,各個平臺還會根據自己需要解決的問題提供平臺特有的 API。比如,其中有委員會(W3C)來制定標準的平臺——Web 平臺——提供了下面這些 API。
其中 DOM API 在其他平臺上都沒有,而網絡請求 API 在 Node.js 平臺上則是完全不同的設計。對于 LeanCloud SDK,我們關心的是實現以下這些功能以及實現所需要用到的 API:
從上表中可以看到平臺在設計這些基礎能力 API 時,分為三大流派:
API 的本質是對實現的抽象,SDK 就像一個由 API 調用構成的金字塔,越往上抽象越貼近用戶。要跨平臺,用戶就需要將不同的底層 API 抽象成一個。這里有兩種思路,假設我們有兩個平臺的 API A 與 B:
具體到我們的實現:
要想達成只使用一套 codebase 的目標,除了統一的 API 在各平臺上的不同實現,還需要在不同的平臺上運行對應的代碼。我們先來看看有哪些工具能完成這個任務,這里以 WebSocket 為例。
開始,我們的 SDK 是沒有編譯打包環節的,在運行時進行平臺檢測來執行不同的代碼。
// src/websocket.js let WebSocket; if (!utils.isNode) {
WebSocket = window.WebSocket;
} else {
WebSocket = require('ws').WebScoket;
}
為了解決這個問題,我們引入了 webpack 來實現「條件編譯」:
// src/websocket.js let WebSocket; if (process.env.PLATFORM === 'Browser') {
WebSocket = window.WebSocket;
} else {
WebSocket = require('ws').WebScoket;
}
// webpack/browser.js module.exports = { // ... plugins: [ new webpack.EnvironmentPlugin(["PLATFORM"])
]
};
// package.json:
{
"scripts": {
"build:browser": "PLATFORM=Browser webpack --config webpack/browser.js" } }
webpack 后:
var WebSocket; if ('Browser' === 'Browser') {
WebSocket = window.WebSocket;
} else {
WebSocket = require('ws').WebScoket;
}
uglify 后:
var WebSocket;WebSocket=window.WebSocket;
可見:https://github.com/defunctzombie/package-browser-field-spec
// package.json:
{
"browser": {
"ws": "./src/websocket.js" } }
// src/websocket-browser.js module.exports = window.WebSocket;
// src/websocket.js const WebSocket = require('ws');
除了對內告訴 bundler 要如何打包模塊,browser field 也用來對外申明瀏覽器版本的入口:
// package.json:
{
"main": "./dist/node/index.js",
"browser": {
"./dist/node/index.js": "./dist/av.js",
"ws": "./src/websocket-browser.js" } }
作為事實標準,browser 字段得到了市面上幾乎所有 bundler 的支持(包括 React Native 內置的 Packager、cocos creator 使用的 browserify,以及 webpack 與 rollup),npm 上眾多跨平臺的 package 也都是采用了這種申明方式。
同樣的,我們還有一些 React Native 特有的代碼需要在打包時替換。webpack 使用了一種更通用的方式支持了這個特性。
// package.json:
{
"main": "./dist/node/index.js",
"browser": {
"./dist/node/index.js": "./dist/av.js",
"ws": "./src/websocket.js" },
"react-native": {
"./dist/node/index.js": "./dist/av-rn.js",
"./src/utils/localstorage.js": "./src/utils/localstorage-rn.js" } }
// webpack/react-native.js module.exports = { // ... resolve: {
aliasFields: ['react-native', 'browser']
}
};
剛才說到,市面上幾乎所有的 bundler 都支持這個標準,bundler 會按照我們的配置正確的使用對應的模塊,所以為目標平臺編譯出一個文件并不是必須的。事實上這樣做是有缺點的
與此同時,預編譯的版本也不會自動得到依賴模塊的新 bug,并且考慮到很多 bundler 在具體的實現上總有各種各樣的問題,所以我們目前依然在每次一發布時都提供了各個平臺的預編譯版本。
至此,我們幾乎完成了前面所設定的目標:
直到出現了一位新玩家。
先來看下小程序的架構。
在部分說到,由于 Web API 抽象層級高、后臺硬、現有輪子多,各個平臺都傾向于實現 Web API。SDK 大部分時候都是直接調用的 Web API。另一方面,我們也使用了 superagent/axios 等第三方庫提供更加易用的 API,并不希望去修改這些第三方庫。
很自然地,為了適配小程序,便捷的方案是用小程序的 API 來 polyfill Web API。很快我們就遇到了兩個問題:
小程序的 JavaScript 代碼在真機上是運行在 JSC / JSCore 上的,但是在開發者工具中,這部分代碼是直接運行在瀏覽器環境中的,是能夠使用包括 window、document、XMLHttpRequest 在內的所有 Web API 的。為了保證 IDE 與真機運行環境的一致性,IDE 在編譯階段會在每個文件的 CommonJS wapper 中申明這些變量:
define("app", function(require, module, exports, window,document,frames,self,location,navigator,localStorage,history,Caches,screen,alert,confirm,prompt,XMLHttpRequest,WebSocket,webkit,WeixinJSCore,WeixinJSBridge,Reporter){ 'use strict'; // SDK code new XMLHttpRequest(); // throw new window.XMLHttpRequest(); // throw });
這意味著即使能夠為 global object 增加 Web API,也無法在其他文件中訪問到。
define("app", function(require, module, exports, window,XMLHttpRequest/* ... */){ 'use strict'; // polyfill code window = window || {};
window.XMLHttpRequest = require('./xmlhttprequest.js'); try {
XMLHttpRequest = XMLHttpRequest || require('./xmlhttprequest.js');
} catch (e) {} // SDK code new XMLHttpRequest(); new window.XMLHttpRequest();
});
還是以 HTTP 請求為例,小程序的 wx.request API 在開發者工具中是用瀏覽器中的 XMLHttpRequest 實現的。因此小程序的 API 缺少了很多實現 Web API 需要的特性:
abort;
HEADERS_RECEIVED 與 LOADING 等中間狀態。
一方面,我們只能在微信小程序中禁用掉 SDK 的一些功能,比如文件上傳進度功能。另一方面盡可能去 mock 一些特性或數據來保證現有的基于 Web API 的代碼邏輯不會拋異常,比如 getResponseHeade('content-type') 始終返回'application/json',其他 key 始終返回 ‘’。
這些 polyfill 開源在 GitHub - leancloud/weapp-polyfill: Polyfills for w3c API on top of Weapp API 。目前我們 polyfill 了以下 API,如果有在小程序中使這些 API 的需求,這個庫應該能節省你一些時間。
測試是保證 SDK 質量的重要手段,我們使用了 Mocha 作為測試框架,Sinon.js 作為 spy 與 mock 工具,它們都同時支持瀏覽器與 Node.js。再加上 SDK 提供的 API 是平臺無關的,使得我們能夠使用一份測試代碼分別在瀏覽器與 Node.js 中運行測試。
對于跨平臺 SDK,測試流程的自動化是必不可少的。我們使用 travis-ci 來運行 Node.js 的測試,使用 Saucelabs(Selenium)來運行瀏覽器測試,保證每次提交在我們支持的所有 Node.js 版本與我們支持的所有瀏覽器中都能通過測試。
遺憾的是,對于其他平臺,由于工具的缺失,目前并沒有良好的測試方案,我們現在也只是在發布之前手動進行冒煙測試。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。