基於 React、TS 的聊天室 monorepo 實戰

最近在思考如何編寫高質量的 React 項目,恰好接到聊天室的需求,因而決定寫一篇關於 React、TS 的實戰教程,採用 monorepo+lerna 管理包。如何關注代碼質量與規範的同時,快速實現需求。前端

接下來,帶着你們快速開發一個 Web 版聊天室。心急的小夥伴能夠直接看源碼node

PS:該教程面向有必定 React、TS 、Node 經驗的前端開發者,經過學習您將得到:react

  • UI 組件庫搭建
  • Lerna + monorepo 的開發模式
  • 基於 React hook 的狀態管理
  • socket.io 在客戶端和服務端的應用

目標

實現多人在線聊天,可發送文本、表情、圖片。webpack

接着來看下咱們要實現的頁面長什麼樣子:git

開發計劃與項目初始化

經過需求分析後,制定以下開發計劃:github

基礎配置

基礎配置是經過自研腳手架快速搭建的,其中包括:web

  1. 添加 eslint、prettier、husky 用於代碼規範、git 提交規範
  2. 添加 Lerna 配置,yarn workspaces
  3. 在 packages 目錄創建 @im/component@im/app@im/server

這裏說明下,我的習慣在用 TS 時,將 prettierprintWidth 設置爲 120 (標準是 80)。目的是,能用一行代碼表達的,毫不用兩行,代碼格式化形成的也不行。typescript

接着分別介紹每一個包的具體細節express

UI 庫

秉承快速開發的節奏,直接採用 create-react-app cli 初始化 UI 庫。命令以下:數組

  1. 初始化 React+TS 環境
npx create-react-app component --typescript
複製代碼
  1. 初始化 Storybook
cd component
npx -p @storybook/cli sb init --story-format=csf-ts
複製代碼
  1. 添加 storybook addons
{
  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-nodenodemonexpress 便可知足需求。

啓動命令以下:

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
複製代碼

主要的設計思路:

  1. 以組合的方式開發組件
  2. 保持組件 API 一致性
  3. 儘量簡單,不過分設計

目前須要實現的消息組件比較簡單,具體實現,能夠看源碼。這裏主要傳達的是文件組織方式和基本設計思路。

數據流設計

先來看下,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>;
}
複製代碼
  1. 經過 React.createContext 建立 context
  2. 經過 React.useReducer 管理 reducer,生成 state 與 dispatch
  3. 經過 React.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 等,可有效避免父子組件狀態傳達,但這裏須要開發者自行權衡複用性。

客戶端 Socket

  1. 組件掛載完成後,創建 socket 連接,並保存當前 socket 實例,卸載後記得斷開鏈接。
React.useEffect(() => {
  const socket: SocketIOClient.Socket = io('http://localhost:3002');
  dispatch({ type: Type.INSERT_SOCKET, payload: socket });
  return () => {
    socket.close();
  };
}, [dispatch]);
複製代碼
  1. 經過如下方式通知服務端,好比用戶加入聊天室
state.socket.emit('add user', username);
複製代碼
  1. 監聽服務端事件,好比用戶發送消息
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

這是一個 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 已經完成通訊,貼代碼老是很累的,具體細節參看源碼。

QA

這一節我經過問答的方式來快速過一下開發聊天室中可能遇到的問題:

  1. 如何實現表情發送

簡單的表情能夠當作文原本處理,若是須要考慮兼容性的話,能夠用圖片。這裏不作具體展開

  1. 如何滾動到最新消息
React.useEffect(() => {
  if (lastMessage) {
    // 獲取最後一個消息元素
    lastMessage.scrollIntoView();
  }
}, [lastMessage]);
複製代碼

總結

快速的帶你們實現了一個簡易的 Web 版聊天室,從需求分析,到代碼規範組織,在到數據流設計,最後介紹了 socket 在客戶端和服務端的應用,想必你們對如何快速開發聊天室也有了大體的認識。但願本教程有幫助到你們,謝謝。

相關文章
相關標籤/搜索