SDK 跨平台支持常见问题及解决方案实践

2017-01-04 10:32:45来源:作者:极客头条人点击

第七城市

作者简介:李叶,毕业于华中科技大学,现为 LeanCloud JavaScript SDK 负责人。在前端工程化、前端性能方面有丰富的经验,关注 React 及相关技术。

责任编辑:唐小引,技术之路,共同进步。欢迎技术投稿、约稿,给文章纠错,请发送邮件至tangxy@csdn.net。

导语:本文主要介绍 SDK 在跨平台支持过程中开发者们经常遇见的问题,以及解决这些问题时用到的工具并总结的一些最佳实践。希望可以为那些对跨平台开发有兴趣的同学提供有价值的帮助。

背景

作为后端云服务提供商,我们在底层通过 REST API 与 WebSocket 提供数据、文件存储、短信、推送、实时消息等服务。还为各个目标平台编写了 SDK 来封装这些 API,在 SDK 中实现客户端状态的持久化,为用户提供更加符合直觉的抽象。一个有趣的现象是越来越多的平台使用的都是 JavaScript:

Web(浏览器/WebView/Windows Universal App/…) Node.js Electron/NW.js React Native Cocos2d-x(JavaScript binding) 微信小程序

为什么 SDK 要跨平台? 降低成本 是最为重要的一大原因。对于用户,提供跨平台的 SDK 可以降低学习与切换成本。并且,随着同构应用以及服务端渲染的流行,对于采用这种方案的用户,跨平台 SDK 可以方便地作为「平台无关」代码进行共享。而对于公司而言,如果能够在多个平台中共享这部分代码,将会减少 SDK 的开发与维护成本。

基于以上前提,我们的目标具体表现为:

使用一套代码; 一致的 API; 各平台提供一致的安装加载方式。

接下来,我们分 API、编译打包、小程序、测试四个部分详细了解 SDK 在跨平台实践中遇到的常见问题及解决方案。

API 平台间的相同点 JavaScript Engine

这些平台都会使用内置或者外部的 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:地理位置,陀螺仪、电池、MediaCapture; 通讯 API:网络请求、WebSocket、WebRTC、推送; 数据管理 API:文件、本地存储、数据库。

其中 DOM API 在其他平台上都没有,而网络请求 API 在 Node.js 平台上则是完全不同的设计。对于 LeanCloud SDK,我们关心的是实现以下这些功能以及实现所需要用到的 API:

从上表中可以看到平台在设计这些基础能力 API 时,分为三大流派:

客户端平台(Web、React Native、cocos2d-x):(尽可能)内置 W3C 标准 API 的实现; Node.js API:作为唯一的服务端 Runtime,提供底层的能力,上层实现交给社区(上面说到的 W3C API,几乎都能找到基于 Node.js API 的实现); 微信小程序。

API 的本质是对实现的抽象,SDK 就像一个由 API 调用构成的金字塔,越往上抽象越贴近用户。要跨平台,用户就需要将不同的底层 API 抽象成一个。这里有两种思路,假设我们有两个平台的 API A 与 B:

用 B 实现 A; 分别用 A 与 B 实现 C;

具体到我们的实现:

打包

要想达成只使用一套 codebase 的目标,除了统一的 API 在各平台上的不同实现,还需要在不同的平台上运行对应的代码。我们先来看看有哪些工具能完成这个任务,这里以 WebSocket 为例。

运行时判断

最开始,我们的 SDK 是没有编译打包环节的,在运行时进行平台检测来执行不同的代码。

// src/websocket.jslet WebSocket;if (!utils.isNode) { WebSocket = window.WebSocket;} else { WebSocket = require('ws').WebScoket;} ☹️ 随着平台数量增加,文件体积大(这是一个例子,实际上 if else 的内容可能很长); ☹️ 平台检测不可靠,随着平台数量增加难以维护。 条件编译

为了解决这个问题,我们引入了 webpack 来实现「条件编译」:

// src/websocket.jslet WebSocket;if (process.env.PLATFORM === 'Browser') { WebSocket = window.WebSocket;} else { WebSocket = require('ws').WebScoket;} // webpack/browser.jsmodule.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; ☹️ 现在有多个入口了,怎么告诉平台使用哪个呢?(需要提供一致的加载方式) ☹️ ws 怎么也打包进来了?(因为 bundler 不知道不需要 ws,换句话说,我们需要找到一种方式告知 bundler。) package browser field spec

可见: https://github.com/defunctzombie/package-browser-field-spec

// package.json:{ "browser": { "ws": "./src/websocket.js" }} // src/websocket-browser.jsmodule.exports = window.WebSocket; // src/websocket.jsconst 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.jsmodule.exports = { // ... resolve: { aliasFields: ['react-native', 'browser'] }}; 预编译?

刚才说到,市面上几乎所有的 bundler 都支持这个标准,bundler 会按照我们的配置正确的使用对应的模块,所以为目标平台编译出一个文件并不是必须的。事实上这样做是有缺点的

无法与其他模块共享代码 无法自动得到依赖模块的 patch

与此同时,预编译的版本也不会自动得到依赖模块的新 bug,并且考虑到很多 bundler 在具体的实现上总有各种各样的问题,所以我们目前依然在每次一发布时都提供了各个平台的预编译版本。

至此,我们几乎完成了前面所设定的目标:

我们使用一个 codebase,共享了大部分的代码; 统一使用 npm 安装,使用 require 加载,为不同的平台指定不同的入口; 提供了统一的 API。

直到出现了一位新玩家。

小程序带来的新挑战

先来看下小程序的架构。

在第一部分说到,由于 Web API 抽象层级高、后台硬、现有轮子多,各个平台都倾向于实现 Web API。SDK 大部分时候都是直接调用的 Web API。另一方面,我们也使用了 superagent/axios 等第三方库提供更加易用的 API,并不希望去修改这些第三方库。

很自然地,为了适配小程序,最便捷的方案是用小程序的 API 来 polyfill Web API。很快我们就遇到了两个问题:

unpolyfillable runtime

小程序的 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();}); 小程序的 API 的抽象层级在 Web API 之上

还是以 HTTP 请求为例,小程序的 wx.request API 在开发者工具中是用浏览器中的 XMLHttpRequest 实现的。因此小程序的 API 缺少了很多实现 Web API 需要的特性:

Response Headers 无法获得; 不支持上传进度; 不支持 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 的需求,这个库应该能节省你一些时间。

XMLHttpRequest FormData WebSocket localStorage 测试

测试是保证 SDK 质量的重要手段,我们使用了 Mocha 作为测试框架,Sinon.js 作为 spy 与 mock 工具,它们都同时支持浏览器与 Node.js。再加上 SDK 提供的 API 是平台无关的,使得我们能够使用一份测试代码分别在浏览器与 Node.js 中运行测试。

对于跨平台 SDK,测试流程的自动化是必不可少的。我们使用 travis-ci 来运行 Node.js 的测试,使用 Saucelabs(Selenium)来运行浏览器测试,保证每次提交在我们支持的所有 Node.js 版本与我们支持的所有浏览器中都能通过测试。

遗憾的是,对于其他平台,由于工具的缺失,目前并没有良好的测试方案,我们现在也只是在发布之前手动进行冒烟测试。

了解最新移动开发、VR/AR 干货技术分享,请关注 mobilehub 微信公众号(ID: mobilehub)。
第七城市

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台