最近在思考如何編寫高質量的 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 在客戶端和服務端的應用,想必你們對如何快速開發聊天室也有了大體的認識。但願本教程有幫助到你們,謝謝。