最近在思考如何编写高质量的 React 项目,恰好接到聊天室的需求,因而决定写一篇关于 React、TS 的实战教程,采用 monorepo+lerna 管理包。如何关注代码质量与规范的同时,快速实现需求。前端
接下来,带着你们快速开发一个 Web 版聊天室。心急的小伙伴能够直接看源码node
PS:该教程面向有必定 React、TS 、Node 经验的前端开发者,经过学习您将得到:react
实现多人在线聊天,可发送文本、表情、图片。webpack
接着来看下咱们要实现的页面长什么样子:git
经过需求分析后,制定以下开发计划:github
基础配置是经过自研脚手架快速搭建的,其中包括:web
@im/component
、@im/app
、@im/server
包这里说明下,我的习惯在用 TS 时,将
prettier
的printWidth
设置为 120 (标准是 80)。目的是,能用一行代码表达的,毫不用两行,代码格式化形成的也不行。typescript
接着分别介绍每一个包的具体细节express
秉承快速开发的节奏,直接采用 create-react-app cli 初始化 UI 库。命令以下:数组
npx create-react-app component --typescript
复制代码
cd component
npx -p @storybook/cli sb init --story-format=csf-ts
复制代码
{
addons: [
'@storybook/preset-create-react-app',
'@storybook/addon-viewport', // 手机预览效果
'@storybook/addon-notes/register-panel', // API 文档
'@storybook/addon-actions',
'@storybook/addon-links',
],
};
复制代码
最终以这种模式去规范组件库的开发(PS:没有文档的组件库,不叫组件库):
APP 的开发采用咱们最熟悉的模式,直接用 create-react-app 初始化环境。
npx create-react-app app --typescript
复制代码
整个聊天室项目采用的是多包管理模式,因此在开发时咱们会直接经过 lerna link
命令来建立软链接,所以能够没必要经过发布包来完成依赖的使用。
但这里要注意的是,因为 create react app
命令生成的项目中 babel 配置是忽略编译 node_modules
的。因此,不得不覆盖其 webpack 配置。
这里简单经过 react-app-rewired
到方式来达成目的,但并非最佳实践。
这里,服务端的代码,仅做为辅助演示的做用,所以暂不考虑健壮性。标配 ts-node
、nodemon
、express
便可知足需求。
启动命令以下:
nodemon --watch 'src/**/*.ts' --ignore 'src/**/*.spec.ts' --exec 'ts-node' src/index.ts
复制代码
至此,基本的环境以及搭建完毕。接下来说下聊天室核心实现逻辑
你们能够用个 TODO 的方式进行开发,好比:
把需求拆分红若干个任务,每一个任务关联到一个 TODO,并以此规范 git commit。
虽然项目是基于 Material-UI 开发的,但考虑到业务带来的差别性,组件库可能须要高度定制,故直接采用全量导出的方式来使用基础 UI 组件。
聊天室用到比较可能是消息流组件,好比:纯文本消息组件,纯图片消息组件,系统消息组件,推荐组件等。
├── MessageBase.tsx # 包含头像、反向显示的基础消息组件
├── MessageMedia.tsx # 图片、音频等
├── MessageSystem.tsx # 系统消息
├── MessageText.tsx # 文本组件
├── __stories__ # 文档相关
│ ├── Demo.tsx
│ ├── Message.stories.tsx
│ ├── README.md
│ └── img.jpg
└── index.tsx
复制代码
主要的设计思路:
目前须要实现的消息组件比较简单,具体实现,能够看源码。这里主要传达的是文件组织方式和基本设计思路。
先来看下,React hook 出现后,前端能够如何更优雅地共享状态
export const ChatContext = React.createContext<{
state: typeof initialState;
dispatch: (action: Action) => void;
}>({
state: initialState,
dispatch: () => {},
});
export const useChatStore = () => React.useContext(ChatContext);
export function ChatProvider(props: any) {
const [state, dispatch] = React.useReducer(reducer, initialState);
const value = { state, dispatch };
return <ChatContext.Provider value={value}>{props.children}</ChatContext.Provider>;
}
复制代码
React.createContext
建立 contextReact.useReducer
管理 reducer,生成 state 与 dispatchReact.useContext
获取状态源这样,咱们就能够很方便的维护局部或全局状态。至因而否要将全部的状态都放到根状态树里以及 domain 数据是否须要状态化,就是另一个故事了,这里就留给读者本身去深究。
接着咱们来设计一个聊天室所需的数据结构:
interface State {
messages: Message[]; // 数组的方式存储全部消息,保持有序
members: { [id: string]: Member }; // map 的形式存储当前聊天室全部用户,便于查询
}
复制代码
数据尽量地保持简单,好比一个 message 的结构能够是这样:
interface Message {
id: string;
type: MESSAGE_TYPE; // 消息类型,用于渲染不用的消息组件
userId: string; // 发送消息的用户标识
content: object; // 根据消息组件类型收敛的数据结构
}
复制代码
MESSAGE_TYPE
消息类型枚举,用于与消息流组件隐射一一对应,以及 socket 消息发送时的 type 数据。建议能够在 @im/helper
里统一维护这类的常量。
interface Member {
id: string;
avatar: string;
name: string;
}
复制代码
经过消息中的 userId
去 members 获取对应用户数据来渲染头像和用户昵称等。
按以上的约定基本能够知足一个简单的聊天室了。另外,若是组件层级比较多,组件粒度拆得比较细的话,在不考虑业务组件复用的状况下,能够引入一些共享状态,如:currentUserId、socket、activeTool 等,可有效避免父子组件状态传达,但这里须要开发者自行权衡复用性。
React.useEffect(() => {
const socket: SocketIOClient.Socket = io('http://localhost:3002');
dispatch({ type: Type.INSERT_SOCKET, payload: socket });
return () => {
socket.close();
};
}, [dispatch]);
复制代码
state.socket.emit('add user', username);
复制代码
React.useEffect(() => {
if (!state.socket) {
return;
}
state.socket.removeAllListeners();
state.socket.on('login', handleLogin);
state.socket.on('user joined', handleUserJoin);
state.socket.on('user left', handleUserLeft);
state.socket.on('new message', handelNewMessage);
}, [state.socket, handleLogin, handleUserJoin, handelNewMessage, handleUserLeft]);
复制代码
这是一个 socket 官方的 demo,比较简单。不考虑其余的场景,这样就能够了!
import express from 'express';
import socket from 'socket.io';
const server = require('http').createServer(app);
const io = socket(server);
server.listen(port);
io.on('connection', socket => {
// 处理接收的新消息
socket.on('new message', data => {
// 通知其余客户端
socket.broadcast.emit('new message', {
id: v4(),
username: socket.username,
userId: socket.userId,
message: data.message,
type: data.type,
});
});
});
复制代码
客户端和服务端的 socket 已经完成通讯,贴代码老是很累的,具体细节参看源码。
这一节我经过问答的方式来快速过一下开发聊天室中可能遇到的问题:
简单的表情能够当作文原本处理,若是须要考虑兼容性的话,能够用图片。这里不作具体展开
React.useEffect(() => {
if (lastMessage) {
// 获取最后一个消息元素
lastMessage.scrollIntoView();
}
}, [lastMessage]);
复制代码
快速的带你们实现了一个简易的 Web 版聊天室,从需求分析,到代码规范组织,在到数据流设计,最后介绍了 socket 在客户端和服务端的应用,想必你们对如何快速开发聊天室也有了大体的认识。但愿本教程有帮助到你们,谢谢。