项目最佳实践 - 数据SDK开发 - 图1

前言

本章主要和大家一起实现一个具有中间件,事件等功能的可扩展的SDK,基于此基础SDK, 从而实现云信聊天SDK模块。

项目最佳实践 - 数据SDK开发 - 图2

上图是这样和大家讲解的核心模块,具体源码可以参考如下仓库地址,对应的NPM安装包如下:tbms-middlewaretbms-sdktbms-brandsdk-yunxingenerator-typescript-jest-sdk

  1. 中间件源码(tbms-middleware): https://github.com/ge-tbms/tbms-packages/tree/master/packages/tbms-middleware
  2. 基础SDK源码:https://github.com/ge-tbms/tbms-packages/tree/master/packages/tbms-sdk
  3. 云信SDK源码:https://github.com/ge-tbms/tbms-brandsdk-yunxin
  4. SDK生成脚手架源码:https://github.com/ge-tbms/generator-typescript-jest-sdk

tbms-midddleware 设计思想

tbms-middleware 的设计参考了Koajs的设计原理。Koajs的中间件思路: 中间件对于一次请求来处理,context分别集成了request和response对象。

同理可以映射成对一条收发消息的处理,通过dispatch,经过中间件流转,转化成系统期望的数据结构

在context中会集成 message(消息) , session(会话) , app(如用户,初始化sdk信息等其他信息)

项目最佳实践 - 数据SDK开发 - 图3
解释说明:websocket 接受一条数据流,通过 action 触发 dispatch 方法, dispatch 会触发各个 middleware 模块,同时一直保存着 context执行上下文。在视图层同样通过 action 触发 dispatch, 回流到 view 层。

tbms-middleware 核心实现

tbms-middleware 模块继承于 tbms-util 的 EventEmitter 事件类(此实现源码在通用SDK设计中实现过),因此 tbms-middleware 模块具有事件发布-订阅模式。

tbms-middleware-compose 核心代码

  1. export default function compose(middleware: ICallback[]) {
  2. /**
  3. * 中间件返回函数
  4. * @param {Array} middleware
  5. * @return {Function}
  6. *
  7. */
  8. return function(context: object, next?: Promise<any> | ICallback) {
  9. let index: number = -1;
  10. // 0. 执行 dispatch 递归模块
  11. return dispatch(0);
  12. // 1. 实现 dispatch 函数,返回Promise链
  13. function dispatch(i: number): Promise<any> {
  14. if (i <= index)
  15. return Promise.reject(new Error("next() called multiple times"));
  16. index = i;
  17. let fn: any = middleware[i];
  18. // 2.1 如果递归索引值为模块长度,赋值next,
  19. // 2.2 同时next为空的时候,返回 promise resolve,跳出递归。
  20. if (i === middleware.length) fn = next;
  21. if (!fn) return Promise.resolve(context);
  22. try {
  23. // 3. i+1 递归执行下一个Middleware模块
  24. return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
  25. } catch (err) {
  26. // 4. 异常情况跳出递归,返回 Promise reject
  27. return Promise.reject(err);
  28. }
  29. }
  30. };
  31. }

代码详细解析:

  1. 内部实现 dispatch 函数, 返回一个 Promise
  2. 通过高阶函数,内部闭包维护了 middleware 数组。同时以 0 为索引执行 dispatch 模块。每个middleware函数模块输入参数为两个 context 和 next。 1. context执行上下文对象,存储各个middleware修改的状态。 2. next 为 dispatch.bind(null, i + 1)) 通过 bind 函数,递归执行 Promise 链。同时此中间件方法适用于异步方法。
  3. 如果递归索引值为模块长度,赋值next,同时next为空的时候,返回 promise resolve,跳出递归。

tbms-middleware 核心代码

  1. /**
  2. * 触发函数
  3. * @param {Object} message 消息体
  4. */
  5. dispatch(val: ContextObject) {
  6. // 1. 创建一个上下文,通过Object.create创建一个新的对象
  7. let context = this.createContext(val);
  8. // 2. 原型SDK返回一个上下文(ctx), 用于yunxin-sdk等基础的SDK扩展。
  9. context = this.handleContextExternal(context, val);
  10. // 3. 执行👆的compose函数,实现promise中间件
  11. const fnMiddleware = compose(this.middleware);
  12. // 4. 返回promise实例,以及结果
  13. return fnMiddleware(context).catch(this.onerror.bind(this))
  14. }
  15. /**
  16. * 处理上下文,给上下文添加额外参数
  17. * 子类继承扩展
  18. * @param {Object} context 上下文
  19. */
  20. handleContextExternal(ctx: ContextObject, val: ContextObject) {
  21. return ctx
  22. }
  23. /**
  24. * 创建新的上下文
  25. * @param {Object} message 创建'`新上下文`'
  26. */
  27. createContext(val: ContextObject) {
  28. const ctx = Object.create(this.context);
  29. // 对原有ctx扩展
  30. return Object.assign(ctx, val);
  31. }

代码详细解析:

  1. 每次 dispatch 通过Object.create创建一个新的上下文对象。
  2. handleContextExternal 用于继承原型 Middleware 进行扩展,子类实现。
  3. 执行上文的compose函数,实现promise中间件。此中间件支持异步请求
  4. 返回一个 promise 实例,以及最终处理结果。

tbms-middleware 单元测试

  1. import Middleware from '../src/index';
  2. test('basic', (done) => {
  3. const middle = new Middleware({})
  4. // 1. 添加中间件1,同时支持异步返回
  5. middle.use((ctx, next) => {
  6. ctx.test = 1;
  7. console.log('use1 >>>')
  8. next().then(() => {
  9. ctx.userDeffer1 = '1'
  10. console.log('use1 <<< promise')
  11. });
  12. console.log('use1 <<<')
  13. });
  14. // 1. 添加中间件2,同时支持异步返回
  15. middle.use((ctx, next) => {
  16. ctx.testTwo = 2;
  17. console.log('use2 >>>')
  18. next().then(() => {
  19. ctx.userDeffer2 = '2'
  20. console.log('use2 <<< promise')
  21. });
  22. console.log('use2 <<< ')
  23. })
  24. middle.dispatch({message:{message: 1, id: '12'}}).then((result: any) => {
  25. expect(result.userDeffer1).toBe('1');
  26. expect(result.userDeffer2).toBe('2');
  27. done();
  28. })
  29. });
  30. // 测试 async await 写法
  31. test('await async function ', (done) => {
  32. const middle = new Middleware({})
  33. async function asyncTest() {
  34. const result = await middle.dispatch({message:{message: 2, id: '12'}});
  35. expect(result.message.message).toBe(2);
  36. done();
  37. }
  38. asyncTest()
  39. })

我们可以在源码 tbms-middleware 目录下运行 npm run test 查看结果。结果如下:
项目最佳实践 - 数据SDK开发 - 图4
测试结果解析:同步的方法先执行(从外到内),异步的方法(从内到外),洋葱圈模型。

tbms-sdk 核心实现

tbms-sdk 是一个标准的IM-SDK模块,tbms-sdk 继承与 tbms-middlware 模块,因此它同时具有 中间件 和 事件监听发布 能力。在此模块主要实现统一的API接口以及标准事件回调,初始化聊天的参数配置以及一些通用的业务逻辑处理。 如图是标准API接口 和 tbms-sdk测试用例(测试用例)

项目最佳实践 - 数据SDK开发 - 图5

初始化参数配置

初始化参数配置依赖于IM的基本概念和基本流程。我们需要传入 appkey, touid, uid必填参数。同时需要有些通用事件回调, onlogin, onmsg, onofflinemsg 等等。

Name Type Description
appkey String 应用APPKEY
touid String 目标用户Id, 可以是群ID或者用户Nick
uid String 账号Id或者Nick
onlogin function 登入回调,可以拿到用户信息
onconnect function 连接建立后的回调, 会传入一个对象, 包含登录的信息
onerror function 发生错误回调
onmsg function 实时消息回调
onsystemmsg function 系统消息回调
onofflinemsg function 离线消息,漫游消息,历史消息回调
onconversation function 同步最近会话列表回调, 会传入会话列表。

SDK实例

  1. const imsdk = new IMSDK({
  2. appkey: 'b652154953697d814225f7aa707491b1',
  3. touid: 'alice',
  4. uid: 'bob',
  5. onlogin: onLogin,
  6. onclose: onClose,
  7. onerror: onError,
  8. onmsg: onMsg,
  9. onsystemmsg: onSystemMsg,
  10. onofflinemsg: onOfflineMsg,
  11. onconversation: onConversation
  12. })
  13. const onLogin = (user: IMUser) => {
  14. // user 用户信息
  15. }
  16. const onError = (error: IMError) => {
  17. // 错误对象处理
  18. }
  19. const onMsg = (msgs: IMMessage[]) => {
  20. // 实时消息同步
  21. }
  22. const onSystemMsg = (msgs: IMSystemMessage[]) => {
  23. // 系统通知消息
  24. // 通知消息属于`会话内`的一种消息,用于会话内通知和提示场景。例如:群名称更新、某某某退出了群聊等
  25. }
  26. const onOfflineMsg = (msgs: IMMessage[]) => {
  27. // 离线消息,漫游消息,历史消息回调
  28. }
  29. const onConversation = (conversation: Conversation[]) => {
  30. // 最近会话
  31. }

tbms-sdk 核心代码

代码详细见 tbms-sdk/src/index.ts, tbms-sdk 继承与 tbms-middlware 模块,因此它同时具有 中间件 和 事件监听发布 能力。

tbms-sdk 对标准接口进行了封装,同时对消息流 action 统一通过 dispatch 方法走中间件模块。

  1. /**
  2. * 触发实时消息
  3. * @param {object | MessageObject} message 消息体
  4. * @api dispatchMsg
  5. */
  6. dispatchMsg(message: MessageObject) {
  7. this.dispatch({ message: message }).then((result: any) => {
  8. this.options.onmsg(result.message, result)
  9. })
  10. }

代码详解:对新消息,调用 dispatchMsgaction, 通过 dispatch 流转中间件。 得到最终标准化消息数据。

tbms-yunxin-sdk 核心实现

代码详细见 tbms-sdk/src/core.ts,主要实现的功能是把云信的SDK通过事件的方式转化到标准SDK中

  1. // 底层调用云信SDK
  2. this.sdk = NIM.getInstance({
  3. appKey: APP_CONFIG.appkey,
  4. token: options.token,
  5. account: options.accid,
  6. onconnect: (event: any) => {
  7. // 接受登录成功回调,同时分发这个事件。
  8. this.emit(MSG_EVENT_CONSTANT.LOGIN_SUCCESS, event);
  9. },
  10. onerror: (event: any) => {
  11. // 接受错误回调,同时分发这个事件。
  12. this.emit(MSG_EVENT_CONSTANT.LOGIN_ERROR, event);
  13. },
  14. onroamingmsgs: (obj: any) => {
  15. const msgs = obj.msgs;
  16. // 接受漫游消息回调,同时分发这个事情
  17. this.emit(MSG_EVENT_CONSTANT.GET_OFFLINE_MSG, msgs);
  18. },
  19. onofflinemsgs: (obj: any) => {
  20. const msgs = obj.msgs;
  21. // 接受离线消息回调,同时分发这个事情
  22. this.emit(MSG_EVENT_CONSTANT.GET_OFFLINE_MSG, msgs);
  23. },
  24. onsessions: (sessions: any[]) => {
  25. // 单聊有且只有一个会话对象
  26. this.conversation = sessions[0] || {};
  27. // 由于会话属于中间件字段,需要通过 middleware 流转
  28. this.dispatchConversation(this.conversation);
  29. },
  30. onmsg: (msg: any) => {
  31. // 取唯一标识
  32. msg.id = msg.idClient;
  33. // 接受实时消息回调,同时分发这个事情
  34. this.emit(MSG_EVENT_CONSTANT.RECEIVE_MSG, msg);
  35. }
  36. })

tbms-yunxin-sdk 的 middleware 代码实现

代码详细见 tbms-sdk/src/middleware.ts, 主要是编码和解码中间件模块,插入到 tbms-yunxin-sdk 中。

  1. /**
  2. * 解码中间件流
  3. * @param ctx
  4. * @param next
  5. */
  6. export const messageDecodeFlow = function(ctx:any, next:any) {
  7. let message = ctx.message;
  8. if (message.from && message.to && message.from !== message.to) {
  9. message.conversationId = message.sessionId;
  10. message.scene = 'single';
  11. message.status = 'success';
  12. switch(message.type) {
  13. case 'text': // 文本消息
  14. merge(message, {
  15. type: 'text',
  16. content: message.text
  17. });
  18. break;
  19. default:
  20. merge(message, {
  21. type: 'text',
  22. content: '目前版本暂不支持该功能'
  23. })
  24. break;
  25. }
  26. }
  27. next();
  28. }
  29. /**
  30. * 编码中间件流
  31. * @param ctx
  32. * @param next
  33. */
  34. export const messageEncodeFlow = function(ctx: any, next: any) {
  35. let message = ctx.message;
  36. if (message.from && message.to && message.from === message.to) {
  37. message.conversationId = ctx.conversation.conversationId;
  38. message.scene = 'single';
  39. message.status = 'success';
  40. message.idClient = message.id;
  41. }
  42. next();
  43. }

代码详解,传入两个参数 contextnext

  • 编码模块:把非标准的数据流解析成标准化消息格式。
  • 解码模块:把标准化消息格式解析成服务器请求的参数消息格式。

tbms-yunxin-sdk 的 主模块实现

代码详见 tbms-yunxin-sdk/src/index.ts

  1. constructor(options: any) {
  2. this.options = options;
  3. // 实例化Core模块
  4. this.core = new Core(options);
  5. // 添加中间件实现,主要是编码模块,解码模块
  6. this.core.useBatch([messageEncodeFlow, messageDecodeFlow])
  7. this.init();
  8. }
  9. /**
  10. * 初始化,事件监听
  11. */
  12. init() {
  13. this.core.on(MSG_EVENT_CONSTANT.RECEIVE_MSG, (msg: any) => {
  14. this.core.dispatchMsg(msg);
  15. });
  16. this.core.on(MSG_EVENT_CONSTANT.LOGIN_SUCCESS, (event: any) => {
  17. this.core.dispatchLogin(event);
  18. });
  19. this.core.on(MSG_EVENT_CONSTANT.LOGIN_ERROR, (event: any) => {
  20. this.options.onerror(event);
  21. });
  22. this.core.on(MSG_EVENT_CONSTANT.GET_OFFLINE_MSG, (msgs: any) => {
  23. msgs.forEach((msg: any) => {
  24. this.core.dispatchOfflineMsg(msg);
  25. });
  26. });
  27. }

代码解析: 在主函数模块中,主要是实例化 Core 模块,同时添加中间件模块。 另一方面通过监听标准化事件,统一处理消息(dispatch 到中间件模块)。

结语

看完 tbms-yunxin-sdk 代码实现, 读者可能会想,作者为什么要这么来实现,直接通过云信的SDK来实现不是很方便直接,为什么要去对接标准SDK。这是一个非常好的问题,这样做的目的,今天我们架构的是一个通用解决方案,不仅仅为了云信来实现,这套实现方案以后可以对接微信IM云,淘宝IM服务等。
使用这套框架,之后对接IM服务厂商的时候,我们只需要扩展实现 Middleware 模块,其他能力都是可以共用。

参考文档