项目最佳实践 - UI组件化 - 图1

前言

上一章我们实战了数据层的开发,这样我们来实现UI组件化, 技术方案选用 Alibaba/rax, 类 React 语法,适用于移动端开发。这章重点会介绍 组件化的设计分层, HOC组件设计,解析器模块设计。

源码地址(组件化代码合集)

https://github.com/ge-tbms/tbms-components

需求交互

项目最佳实践 - UI组件化 - 图2

针对交互稿我们有如下业务诉求分析:

  1. 对于一个聊天的UI可以拆分为 普通消息, 自定义(卡片)和系统消息组件。
  2. 对上面提到的消息展示, 抽象成一个HOC组件,封装不可变的UI,包裹可变组件。
  3. 封装输入框组件,表情组件。

组件化架构框图

项目最佳实践 - UI组件化 - 图3

对聊天消息组件化能力划分三个层级:基础消息组件,自定义消息组件和业务组件。

  • 基础组件(rax-tbms-basemsg):包括 文本消息、图片消息、系统消息等
  • 自定义消息(rax-tbms-custommsg): 卡片消息, 抽屉消息等。
  • 业务组件(rax-tbms-chat-plugin): 表情,输入框,加载组件等

消息HOC组件

高阶组件(HOC)是组件开发中的高级技术,用来重用组件逻辑。

具体而言,高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。表达式如下:

  1. const EnhancedComponent = higherOrderComponent(WrappedComponent);

我们观察交互可以发现,聊天项可以抽象分离出一个HOC高阶组件。

  • 不可变区域为:头像和头像标题。
  • 可变区域:中间的消息流,根据不同消息展示不同消息UI。
  1. const leftChatItemHOC = (WrappedComponent) => (conversation) => {
  2. const avator = conversation.targetAvator;
  3. // 1. 返回一个装饰过的组件
  4. return class extends PureComponent {
  5. render() {
  6. return (
  7. <View style={style.container}>
  8. <View style={style.containerAvator}>
  9. <Image source={{uri: avator}} resizeMode="cover" style={style.containerAvator} />
  10. </View>
  11. <View style={[style.containerCnt, style.containerLeft]} >
  12. <Text style={style.containerNick}>{conversation.targetNick}</Text>
  13. <View style={style.containerWrappedCard}>
  14. // 2. 返回传入的可变组件, 同时注入组件
  15. <WrappedComponent {...this.props} />
  16. </View>
  17. </View>
  18. </View>
  19. );
  20. }
  21. }
  22. }

代码解释:封装了一个左侧消息展示的高阶函数,用来重用组件逻辑,返回一个传入的可变组件,同时注入属性。

使用事例

高阶组件rax-tbms-chat-item 封装了 leftChatItemHOCrightChatItemHOC,分别是左侧对方发送消息高阶组件和右侧自己发送消息高阶组件。

WrappedComponent 为传入的组件, 通过高阶组件返回一个新的组件。
源码地址:

https://github.com/ge-tbms/tbms-components/blob/master/packages/rax-tbms-chat-item/src/index.js

使用事例参考如下:

  1. import { leftChatItemHOC } from 'tbms-ui-chat-item';
  2. import { TextMsg } from 'tbms-ui-chat-basemsg';
  3. // 1. 设置会话基础信息
  4. const conversation = {
  5. avator: 'https://gw.alicdn.com/tfs/TB1aRryvSzqK1RjSZFpXXakSXXa-640-640.png',
  6. targetNick: '伊芙丽旗舰店小徐',
  7. fromNick: 'moliy'
  8. };
  9. // 2. 设置消息格式
  10. const message = {
  11. type: 'text',
  12. content: '其他的小伙伴有需要一起来看看吗?'
  13. };
  14. // 3. 返回HOC包裹的高阶组件
  15. const ItemComponent = leftChatItemHOC(TextMsg)(conversation);
  16. // 文本消息的消息流组件
  17. <ItemComponent {...message} />

代码解释说明如下:

  1. 设置会话基础信息
  2. 设置消息格式
  3. 返回HOC包裹的高阶组件

基础组件

聊天基础消息组件可以分为 文本消息、图片消息、系统消息 和 富文本消息 ,下面举一个简单的文本消息组件。

  1. /**
  2. * @class
  3. * @name tbms-text 基础组件
  4. * @property {Object} props 属性
  5. * @property {String} props.text 文字
  6. */
  7. export default class extends BaseComponent {
  8. render() {
  9. const styles = this.styles;
  10. // 1. 解析富文本,表情消息
  11. const richText = wwParser(this.props.content, styles);
  12. return <View style={styles.container}>{richText}</View>
  13. }
  14. }

rax-tbms-basemsg 基础消息组件

  1. import { TextMsg } from 'rax-tbms-basemsg';

组件解析器模块

设计框图

项目最佳实践 - UI组件化 - 图4

上一章我们通过中间件的方式,整合输出了标准 IM 数据结构:消息和会话。 这章我们要介绍下一条消息如何通过 pipline 表现出标准化UI,原理图如上所示。

设计思想

组件中间件的设计思路来源于数据驱动,如何根据不同的消息格式展示不同的UI组件。作者设计了一种管道模型( 消息 ---> UI解析器 --> 组件 ),每条消息都通过解析器管道,最终得到上下文对象,包含高阶组件和消息数据两个属性对象。

设计的好处:入口和出口统一,对于数据和UI组件的映射关系放在 UI解析器模块。同时管道模型设计成插件可扩展方式,可以插拔不同UI解析器模块。

解析器参考代码

https://github.com/ge-tbms/tbms-components/blob/master/packages/rax-tbms-chat-parser/src/parse.js

中间件模型

promiseMiddleware 模块参考 前端进阶能力 - 通用SDK设计 的中间件函数。

解析器中间件模块 middleware

  1. export default class {
  2. public middlewares:any[] = [];
  3. public ctx = {
  4. ItemComponent: null,
  5. message: {},
  6. conversation: {}
  7. }
  8. constructor(middlewares: any[]) {
  9. this.middlewares = middlewares;
  10. }
  11. // 1. 批量添加中间件
  12. useBatch(steps: any[]) {
  13. if (_.isArray(steps)) {
  14. this.middlewares = this.middlewares.concat(steps);
  15. } else {
  16. throw TypeError('useBatch must be arrary!!!')
  17. }
  18. }
  19. // 2. 触发消息数据,每条消息都经过中间件流转
  20. dispatch(msg: any, conversation: any) {
  21. let steps = Object.create(this.middlewares);
  22. let ctx = Object.create(this.ctx);
  23. ctx.conversation = conversation;
  24. ctx.message = msg;
  25. // 3. 绑定执行上下文,批量处理解析器模块
  26. return _.promiseMiddleware(steps, ctx);
  27. }
  28. }

代码解释说明:

ctx保持了这次调用的引用。所有对象都挂载到ctx上,统一接口通过 dispatch 函数进行流转。 通过解析器 (parser) 会多挂载一个 高阶组件包装后的 ItemComponent 组件对象。

解析器模块 parser

解析模块的作用是根据不同的消息 映射成 不同的消息UI组件,具体参考代码如下:

  1. import { TextMsg } from 'rax-tbms-basemsg';
  2. import { leftChatItemHOC, rightChatItemHOC } from 'rax-tbms-chat-item';
  3. export default function (ctx) {
  4. const msg = ctx.message;
  5. let message = merge({ type: msg.type }, msg);
  6. switch(msg.type) {
  7. // 1. 区分消息类型
  8. case 'text':
  9. // 2. 合并消息数据
  10. ctx.message = merge(message, {
  11. content: msg.content
  12. })
  13. // 根据数据流向,通过HOC组件包装消息
  14. ctx.ItemComponent = msg.flow === 'in' ? leftChatItemHOC(TextMsg)(ctx.conversation) : rightChatItemHOC(TextMsg)(ctx.conversation);
  15. break;
  16. default:
  17. break;
  18. }
  19. }

实例调用

  1. import baseParser from './parser.js'
  2. // 0. 实例化中间件模块
  3. const componentParser = new Middleware([baseParser]);
  4. // 1. 自定义解析器,现为空
  5. const cosutomParser = (ctx) => ({});
  6. // 2. 批量添加额外的UI解析器
  7. componentParser.useBatch([customParser])
  8. // 3. 注入 msg 和 conversation 两个对象
  9. componentParser.dispatch(msg, conversation).then(ctx => {
  10. // 3.1 返回由解析器流转后的消息实体
  11. const message = ctx.message;
  12. // 3.2 返回由解析器流转后的高阶组件
  13. const ItemComponent = ctx.ItemComponent;
  14. // 4. render 实体组件
  15. render(<ItemComponent {...ctx.message} />)
  16. })

代码解释说明:通过 middlewareparser 两个模块就实现了 pipeline 功能。 实例化 中间件模块,所有数据通过 dispatch 分发。

结语

本章提供了一种通用的数据驱动UI的设计模式,称作 pipeline 设计模式,同时介绍了组件化开发的通用思路: HOC组件开发,组件化分层的思想。这章介绍的实践能力和思想同样可以适用于其他业务,有助于提升大家的前端设计架构能力。