背景 作为后端云服务提供商,我们在底层通过 REST API 与 WebSocket 提供数据、文件存储、短信、推送、实时消息等服务。还为各个目标平台编写了 SDK 来封装这些 API,在 SDK 中实现客户端
背景作为后端云服务提供商,我们在底层通过 REST API 与 WebSocket 提供数据、文件存储、短信、推送、实时消息等服务。还为各个目标平台编写了 SDK 来封装这些 API,在 SDK 中实现客户端状态的持久化,为用户提供更加符合直觉的抽象。一个有趣的现象是越来越多的平台使用的都是 JavaScript:
为什么 SDK 要跨平台?降低成本是最为重要的一大原因。对于用户,提供跨平台的 SDK 可以降低学习与切换成本。并且,随着同构应用以及服务端渲染的流行,对于采用这种方案的用户,跨平台 SDK 可以方便地作为「平台无关」代码进行共享。而对于公司而言,如果能够在多个平台中共享这部分代码,将会减少 SDK 的开发与维护成本。 基于以上前提,我们的目标具体表现为:
接下来,我们分 API、编译打包、小程序、测试四个部分详细了解 SDK 在跨平台实践中遇到的常见问题及解决方案。 API平台间的相同点
这些平台都会使用内置或者外部的 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 来实现「条件编译」:
webpack 后:
var WebSocket;WebSocket=window.WebSocket;
package browser field spec
除了对内告诉 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 使用了一种更通用的方式支持了这个特性。
预编译?刚才说到,市面上几乎所有的 bundler 都支持这个标准,bundler 会按照我们的配置正确的使用对应的模块,所以为目标平台编译出一个文件并不是必须的。事实上这样做是有缺点的
与此同时,预编译的版本也不会自动得到依赖模块的新 bug,并且考虑到很多 bundler 在具体的实现上总有各种各样的问题,所以我们目前依然在每次一发布时都提供了各个平台的预编译版本。 至此,我们几乎完成了前面所设定的目标:
直到出现了一位新玩家。 小程序带来的新挑战先来看下小程序的架构。
在第一部分说到,由于 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 中申明这些变量:
这意味着即使能够为 global object 增加 Web API,也无法在其他文件中访问到。
小程序的 API 的抽象层级在 Web API 之上还是以 HTTP 请求为例,小程序的 wx.request API 在开发者工具中是用浏览器中的 XMLHttpRequest 实现的。因此小程序的 API 缺少了很多实现 Web API 需要的特性:
一方面,我们只能在微信小程序中禁用掉 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 版本与我们支持的所有浏览器中都能通过测试。 遗憾的是,对于其他平台,由于工具的缺失,目前并没有良好的测试方案,我们现在也只是在发布之前手动进行冒烟测试。 |