如何快速開發 Web 聊天室

EER發表於2020-04-06

當雨季來臨的時候,意味著要宅在家裡做點什麼。

接下來,帶著大家快速開發一個 Web 版聊天室。心急的小夥伴可以直接看原始碼

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

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

目標

實現多人線上聊天,可傳送文字、表情、圖片。

接著來看下我們要實現的頁面長什麼樣子:

如何快速開發 Web 聊天室

開發計劃與專案初始化

通過需求分析後,制定如下開發計劃:

如何快速開發 Web 聊天室

基礎配置

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

  1. 新增 eslint、prettier、husky 用於程式碼規範、git 提交規範
  2. 新增 Lerna 配置,yarn workspaces
  3. 在 packages 目錄建立 @im/component@im/app@im/server

這裡說明下,個人習慣在用 TS 時,將 prettierprintWidth 設定為 120 (標準是 80)。目的是,能用一行程式碼表達的,絕不用兩行,程式碼格式化造成的也不行。

接著分別介紹每個包的具體細節

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:沒有文件的元件庫,不叫元件庫):

如何快速開發 Web 聊天室

客戶端

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 的方式進行開發,比如:

如何快速開發 Web 聊天室

把需求拆分成若干個任務,每個任務關聯到一個 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 在客戶端和服務端的應用,想必大家對如何快速開發聊天室也有了大致的認識。希望本教程有幫助到大家,謝謝。

相關文章