前端进阶能力 - 通用SDK设计 - 图1

前言

上一章节我们学习设计模式,了解前端应用最广泛的几个设计模式,订阅发布模式、单例模式、工厂模式等。 本章节我们会通过设计一个通用 SDK 模型来学习应用这些设计模式,和读者一起搭建 SDK 的 TypeScript 开发环境,一起探讨如何设计一个通用的 SDK 原型。

通用SDK原型代码

源码地址:https://github.com/dkypooh/front-end-develop-demo/tree/master/senior/sdk

SDK设计指南

软件开发工具包(缩写:SDK、外语全称:Software Development Kit)一般都是一些软件工程师为特定的软件包、软件框架、硬件平台、操作系统等建立应用软件时的开发工具的集合。
SDK 是根据业务需求来设计的,一方面提供各种业务需要的 API 接口,另一方面 SDK 的设计需要具备扩展性和兼容性。
作者的理解一个好的 SDK 应该具备小而美且五脏俱全的特性

API设计准则

API 是模块或者子系统之间交互的接口定义。好的系统架构离不开好的 API 设计。好的 API 设计有如下准则:

  • 提供清晰的思维模型: API 是用于程序之间的交互,但是一个 API 如何被使用,以及 API 本身如何被维护,是依赖于维护者和使用者能够对该API有清晰的、一致的认识。
  • 少即是多: 系统随着需求的增加不断的演化,SDK 承载的逻辑会越来越多,为了减少使用者的使用成本,SDK 提供的 API 应该是必须且少的。
  • 单一职责: 接口设计尽量要做到 单一职责,最细粒度化,每个接口职责是明确的。
  • 插件化: 随着系统业务需求增加,带来了越来越多的不确定性,基于最核心的 SDK 模块去扩展,不同业务可以去扩展不同需求。

Typescript 通用 SDK 开发环境搭建

npm 包环境安装如下:typescript,jest 和 eslint。

  1. ## 安装typescript支持
  2. $npm i typescript -D
  3. ## 安装jest 支持
  4. $ npm i jest @types/jest ts-jest -D
  5. ## 安装tslint支持
  6. $ npm i tslint tslint-config-standard -D

配置 package 文件

配置 package 文件的 scripts 脚本如下:

  1. build: 通过 tsc 编译 ts 成 js 文件。
  2. test: 运行 jest 测试环境。具体 TS 的 Jest 测试环境说明参考 [前端基础能力 - Jest前端测试框架]。
  3. fix: 运行 tsconfig 语法检查,同时修复语法问题。
  1. {
  2. "name": "tbms-sdk",
  3. "version": "1.0.0",
  4. "description": "sdk, middleware",
  5. "main": "build/index.js",
  6. "scripts": {
  7. "build": "npx tsc --build tsconfig.json -w",
  8. "test": "npx jest -c jest.config.js --colors",
  9. "fix": "tslint --fix src/*.ts -t verbose",
  10. "tslint": "tslint -c tslint.json src/*.ts"
  11. },
  12. "keywords": [
  13. "sdk",
  14. "middleware"
  15. ],
  16. "author": "",
  17. "license": "ISC",
  18. "devDependencies": {
  19. "typescript": "^3.3.1",
  20. "@types/jest": "^24.0.0",
  21. "jest": "^24.1.0",
  22. "ts-jest": "^23.10.5",
  23. "tslint": "^5.12.1",
  24. "tslint-config-standard": "^8.0.1"
  25. }
  26. }

Yoeman SDK 脚手架环境

Typescript + Jest SDK 脚手架: https://github.com/ge-tbms/generator-typescript-jest-sdk 使用 generator-typescript-jest-sdk脚手架工具,可以如下操作:

  1. ## 安装yo 以及 generator
  2. npm install -g yo
  3. npm install -g generator-typescript-jest-sdk
  4. ## 运行 generator-typescript-jest-sdk 生成目录
  5. yo typescript-jest-sdk

Typescript 通用SDK目录结构

  1. ├── build
  2. ├── jest.config.js
  3. ├── package.json
  4. ├── src
  5. ├── event.ts
  6. ├── global.ts
  7. ├── index.ts
  8. ├── middleware.ts
  9. └── util.ts
  10. ├── test
  11. └── sdk.test.ts
  12. ├── tsconfig.json
  13. └── tslint.json

文件结构解释如下,详情可见:

  • event模块: EventEmitter类,用于实现sub/pub模式, 代码可以参考上一章节 [设计模式 - EventEmitter实现]
  • middleware模块: 通过 Promise 队列实现一个中间件模块,同时维护一个 middleware 数组。
  • util.ts: 集成了一些通用函数,例如判断数据类型、获取 URL 参数、甚至动态增加 CSS 样式

通用 SDK 能力

一个具备可扩展以及兼容性的SDK,最基本应该两个基础能力:事件订阅发布 和 中间件模块 能力。在此基础上再根据业务需求扩展合理的API接口。

  • 事件发布监听能力: 继承上一章实现的 EventEmitter 基类,实现子类实例的 emiton 方法。
  • 中间件模块: 下面重点分析中间件模块的实现,和 项目最佳实践- 数据SDK开发实现 中间件模块有所差别。 这次实现是通过 Promise Queue链表,实现顺序执行中间件。项目最佳实践- 数据SDK开发实现的中间件模块可以处理异步请求,洋葱圈模型。

promiseMiddleware 代码实现

源码参考文件地址: https://github.com/dkypooh/front-end-develop-demo/blob/master/senior/sdk/src/util.ts#L40 promiseMiddleware 作为 src/util.ts 模块的一个函数方法提供,下文会使用到。

  1. const promiseMiddleware = (middlewares: any[], ctx: any) => {
  2. let promise = Promise.resolve(null);
  3. let next;
  4. // 1. 通过bind把执行上下文对象,绑定到中间件第一个参数
  5. middlewares.forEach((fn, i) => {
  6. middlewares[i] = fn.bind(null, ctx);
  7. });
  8. // 2. 通过while循环执行promise实例
  9. while ((next = middlewares.shift())) {
  10. promise = promise.then(next);
  11. }
  12. // 3. 最终返回一个promise实例结果
  13. return promise.then(() => {
  14. return ctx;
  15. });
  16. }

代码详解:此段代码执行思想比较简单,但是开发者很难想到通过 promise 链表来实现中间件模块,提供一种可借鉴比较好的思路。

middleware 中间件类代码实现

此源代码文件 src/middleware, 通过 util 实现的 promiseMiddleware 方法,同时继承 EventEmitter 事件类。

源码参考地址: https://github.com/dkypooh/front-end-develop-demo/blob/master/senior/sdk/src/middleware.ts

  1. import _ from './util';
  2. import EventEmitter from './event';
  3. export default class extends EventEmitter{
  4. public middlewares:any[] = [];
  5. public ctx = {
  6. message: {},
  7. conversation: {}
  8. }
  9. // 1. 构造器函数,初始化添加 middlewares
  10. constructor(middlewares: any[]) {
  11. super();
  12. this.middlewares = middlewares;
  13. }
  14. // 2. 通过批量添加中间件接口
  15. useBatch(steps: any[]) {
  16. if (_.isArray(steps)) {
  17. this.middlewares = this.middlewares.concat(steps);
  18. } else {
  19. throw TypeError('useBatch must be arrary!!!')
  20. }
  21. }
  22. // 3. 核心实现,每个Action都需要进过Dispatch进行触发
  23. dispatch(msg: any, conversation: any) {
  24. // 3.1 使用Object.create 创建新的 middlewares 和 ctx对象,防止对象引用
  25. let steps = Object.create(this.middlewares);
  26. let ctx = Object.create(this.ctx);
  27. // 3.2 赋值 会话和消息 对象
  28. ctx.conversation = conversation;
  29. ctx.message = msg;
  30. // 3.3 执行中间件模块,同时返回一个 promise 实例
  31. return _.promiseMiddleware(steps, ctx);
  32. }
  33. }

代码详解:

  1. 构造器函数,初始化添加 middlewares 模块
  2. 使用 useBatch 接口,批量添加中间件接口
  3. 核心实现 dispatch 函数,每个 Action 都需要进过 dispatch进行触发, 主要如下三件事情:
    • 使用 Object.create 创建新的 middlewaresctx 对象,防止对象引用
    • 给执行上下文赋值 会话和消息 对象
    • 最终 执行中间件模块,同时返回一个 promise 实例

通用SDK实现

通用SDK的实现相对比较简单,只需要集成 Middlware 类,它就具备两个通用能力:事件订阅发布中间件 能力。SDK的职责是根据业务需求扩展标准API接口。

  1. import MiddleWare from './middleware';
  2. export default class extends MiddleWare {
  3. constructor(middlewares: any[]) {
  4. super(middlewares);
  5. }
  6. }

通用SDK的单元测试

通用SDK就具备两个通用能力:事件订阅发布中间件 能力。为了确保通用SDK可用,我们在项目中运行 npm run test 对代码进行单元测试。

单元测试源码:https://github.com/dkypooh/front-end-develop-demo/blob/master/senior/sdk/test/sdk.test.ts

  1. import SDK from '../src/index'
  2. describe('SDK Test', () => {
  3. const sdk = new SDK([]);
  4. it('subscribe and publish', (done) => {
  5. sdk.on('publish', (obj) => {
  6. expect(obj).toEqual({cmd: 'publish'});
  7. done();
  8. })
  9. sdk.emit('publish', {cmd: 'publish'});
  10. });
  11. it('add middleware modules', (done) => {
  12. sdk.useBatch([(ctx: any) => {
  13. ctx.message.content = 'test';
  14. }, (ctx: any) => {
  15. ctx.conversation.lastMsg = 'test';
  16. }])
  17. sdk.dispatch({type: 'text'}, {id: 'yyy'}).then((ctx) => {
  18. expect(ctx.message).toEqual({ type: 'text', content: 'test' })
  19. expect(ctx.conversation).toEqual({ id: 'yyy', lastMsg: 'test' })
  20. done();
  21. })
  22. })
  23. })

代码详解:两段测试代码,分别测试 事件订阅发布 和 中间件 能力。

  • 事件订阅发布:emit 发布一个 publish 事件,同时 on 一个 publish 事件,同时传递数据 {cmd: 'publish'}
  • 中间件: 使用 useBatch 添加中间件两个中间件模块,分别修改 messageconversation 的内容。检查通过 dispatch 是否达到预期。

测试结果

  1. PASS test/sdk.test.ts
  2. SDK Test
  3. subscribe and publish (8ms)
  4. add middleware module (3ms)
  5. Test Suites: 1 passed, 1 total
  6. Tests: 2 passed, 2 total
  7. Snapshots: 0 total
  8. Time: 1.421s

结语

本章最后通过Jest的单元测试,测试了本章实现的通用SDK的两个能力:事件订阅发布中间件,单元测试通过保证了SDK的可靠性和稳定性。
最后,作者一直认为SDK是根据业务需求来设计的,SDK的设计一方面提供各种业务需要的API接口,另一方面SDK的设计需要具备扩展性和兼容性。 一个好的SDK它应该是 麻雀虽小,但五脏俱全

参考文献