项目最佳实践 - 聊天原型系统串联 - 图1

前言

上文介绍了数据层SDK和UI组件的开发,大家了解了系统的分层,分别知道各个层如何实现,这章主要和读者一起串联各个模块,介绍里面串联的设计技巧,完整的搭建一个原型聊天系统,感兴趣的同学可以在这基础上扩展。

技术栈准备

  1. Alibab/RAX 技术栈,开源地址
  2. Yeoman 使用 自建 generator-rax-component 脚手架构建聊天项目环境
  3. 数据SDK开发章节封装的 tbms-brandsdk-yunxin NPM包,提供标准SDK。
  4. UI组件章节封装的 tbms-components 的NPM包,提供UI组件。

相关技术栈源码

generator-rax-component: https://github.com/ge-tbms/generator-rax-component

tbms-brandsdk-yunxin: https://github.com/ge-tbms/tbms-brandsdk-yunxin

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

数据和UI层结合

整体结合思路,通过数据层SDK的标准数据结构和回调,通过 dispatch 和 UI层组件结合起来,在封装消息HOC组件, 实现所见即所得。

数据层 + UI层 => 聊天系统

项目最佳实践 - 聊天原型系统串联 - 图2

聊天系统源码地址:

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

标准SDK使用

初始化SDK

  1. import {createElement, Component, render, findDOMNode} from 'rax';
  2. // 1、数据层沉淀的 `tbms-brandsdk-yunxin` SDK NPM包
  3. import YUNXINSDK from 'tbms-brandsdk-yunxin';
  4. // 2、组件容器
  5. class App extends Component {
  6. constructor() {
  7. super();
  8. // 3、初始化SDK,初始化参数及标准事件回调
  9. this.sdk = new YUNXINSDK({
  10. uid: uid, // 登录用户ID
  11. touid: touid, // 目标用户ID
  12. onmsg: this.onMsg.bind(this), // 收到及时消息回调
  13. onofflinemsg: this.onOfflineMsg.bind(this), // 收到离线消息回调
  14. onerror: this.onError.bind(this), // 错误情况回调
  15. onconversation: this.onConversation.bind(this), // 建立或者更改会话回调
  16. onsystemmsg: this.onSystemMsg.bind(this), // 系统消息回调
  17. onlogin: this.onLogin.bind(this) // 登录信息回调
  18. });
  19. }
  20. }

注释:在组件构造器生命周期中初始化SDK,初始化参数,回调各种标准事件。

编写 onmsg 回调

  1. // 通过scrollTop 设置滚动偏移高度
  2. const SCROLLTOP = 100000;
  3. class App extends Component {
  4. onMsg(msg) {
  5. const MessageList = this.state.MessageList;
  6. componentParser.dispatch(msg, this.conversation).then(ctx => {
  7. const ItemComponent = ctx.ItemComponent;
  8. // 设置页面title
  9. this.titleParse(ctx.message);
  10. // push消息组件
  11. MessageList.push(<ItemComponent {...ctx.message} />);
  12. // 更新节点
  13. this.setState({
  14. MessageList
  15. }, () => {
  16. // 滚动到页面底部
  17. this.horizontalScrollView.scrollTo({y: SCROLLTOP});
  18. })
  19. })
  20. }
  21. }

注释:1. 维护消息队列,通过上文提到解析器模块流转数据,从而在 then 中获取相应的组件。2. 设置标题,Push 消息组件到 MessageList 消息队列中。3. setState更新状态,在更新状态之后把导航栏至底部。

编写 offlinemsg 回调

  1. class App extends Component {
  2. onOfflineMsg(msg) {
  3. const MessageList = this.state.MessageList;
  4. if (_.isObject(msg)) {
  5. componentParser.dispatch(msg, this.conversation).then((ctx) => {
  6. const ItemComponent = ctx.ItemComponent;
  7. // 1、通过unshift顶部队列
  8. MessageList.unshift(<ItemComponent {...ctx.message} />);
  9. // 2、更新节点
  10. this.setState({
  11. MessageList,
  12. }, () => {
  13. // 3、导航栏定位到相应位置
  14. const scrollTop = findDOMNode(this.refs['body']).offsetHeight - this.containerHeight;
  15. this.horizontalScrollView.scrollTo({y: scrollTop});
  16. })
  17. })
  18. } else {
  19. // 4、没有历史消息(离线消息),则为暂无消息
  20. this.setState({
  21. isEmpty: true
  22. })
  23. }
  24. }
  25. }

注释:和 onmsg 差别在于顶部更新消息队列,同时导航栏定位到相应位置。如果没有历史消息(离线消息)则设置为为空表现。

编写 onConversation,onLogin 和 onError 回调

  1. import merge from 'lodash/merge';
  2. import Toast from 'universal-toast';
  3. class App extends Component {
  4. onConversation(conversation) {
  5. // 1、存储会话信息
  6. this.conversation = merge(this.conversation, conversation);
  7. }
  8. onLogin() {
  9. Toast.show('登入成功');
  10. // 2、登入成功,则开始获取历史消息
  11. this.sdk.getHistoryMessage({
  12. scene: 'p2p',
  13. to: this.conversation.touid,
  14. })
  15. }
  16. onError(err) {
  17. // 统一错误信息处理
  18. Toast.show(JSON.stringify(err));
  19. }
  20. }

注释:onLogin 登录成功之后,拉取历史消息

UI层使用

整体UI结构

  1. class App extends Component {
  2. renderPlugin() {
  3. // 1、插件选择,原型聊天实现 emoji 表情插件
  4. if (this.state.pluginVisible === 'emoji') {
  5. return <EmojiPlugin onChange={this.handleEmojiChange} onSend={this.handleSendText} type="ww" />;
  6. } else {
  7. return null;
  8. }
  9. }
  10. render() {
  11. return (
  12. // 2、聊天View容器
  13. <View style={styles.container}>
  14. // 3、消息流滚动组件
  15. <ScrollView
  16. style={styles.containerBody}
  17. ref={(scrollView) => {this.horizontalScrollView = scrollView;}}
  18. >
  19. <View ref="body">
  20. {this.state.MessageList}
  21. </View>
  22. </ScrollView>
  23. // 4、输入框组件
  24. <InputText text={this.state.inputText}
  25. onPluginChange={this.handlePluginChange}
  26. onFocus={this.handleFocus}
  27. onChange={this.handleChangeText}
  28. onSubmit={this.handleSendText}
  29. showPlugin={false}
  30. />
  31. // 5、插件组件
  32. {this.renderPlugin()}
  33. </View>
  34. );
  35. }
  36. }

注释:整体聊天UI结构主要分为三个区块:消息流区域,输入框区域和插件区域。

插件和输入框组件通过自定义组件包 rax-tbms-chat-plugin 引入。

消息发送

  1. class App extends Component {
  2. handleSendText = (text) => {
  3. const content = (text || this.state.inputText).trim();
  4. // 1、判断内容是否为空
  5. if (content) {
  6. // 2、 发送文本消息
  7. this.sdk.sendMsg({
  8. type: 'text',
  9. content: content
  10. });
  11. this.setState({
  12. inputText: ''
  13. })
  14. } else {
  15. Toast.show('亲,输入内容不能为空哦!')
  16. }
  17. }
  18. }

注释:通过 sdk 封装的 sendMsg 发送消息,同时也可以指定其他数据类型(例如image,card等数据类型)。

结语

这个章节把之前封装的模块进行了串联,总结起来,作者认为做好分层,明确每一层的职能和划分, 基本可以从容面对一个负责的系统。

就像IM聊天,我们把复杂的数据逻辑放在了SDK层,同时接口数据方面做了单元测试来保证稳定性。 在UI方面,拆分了单元化组件,最后再把两者结合起来构建了一个聊天系统。

复盘提升

这是一次完整的项目实践过程,用到了前端各种技术栈和思考模型。同时小册精讲了各个知识点,我们需要辩证的来看各个知识点解决的问题,最重要的是通过项目实践,形成体系化的思考模型。

如下图所示:原先我们是发散性思维模式到现在的体系化思维模式。

项目最佳实践 - 聊天原型系统串联 - 图3