Chat realtime sử dụng Nestjs + Socket.io và React + Redux-Saga

Chào mừng các bạn trở lại với series tutorial Nestjs của mình. Đến hẹn lại lên như đã nói ở bài viết trước bài viết này mình lại cùng xây dựng React App Chat realtime : Nestjs + Socket.io, React + Redux-Saga nhé . Bắt đầu thôi Index series Giới thiệu về setup repository +

Chào mừng các bạn trở lại với series tutorial Nestjs của mình.

Đến hẹn lại lên như đã nói ở bài viết trước bài viết này mình lại cùng xây dựng React App Chat realtime : Nestjs + Socket.io, React + Redux-Saga nhé . Bắt đầu thôi

Index series

  1. Giới thiệu về setup repository + typeorm.
  2. Xác thực người dùng trong Nestjs sử dụng Passport JWT.
  3. Nestjs – Create relationship với Typeorm + mysql
  4. Tiếp tục series mình lại cùng xây dựng React App Chat realtime : Nestjs + Socket.io, React + Redux-Saga.

1. Cấu trúc

  • Cấu trúc sẽ bao gồm :
    • Server Side: Nestjs + socket.io
    • Client Side: ReactJs + socket-client + redux saga

2. Hướng xử lý

  • Server :
    • Tạo mới table “devices”
    • Khi client connect đến socket gateway: thực hiện lưu socket_id vào table “devices”
    • Khi client disconnect thực hiện xóa socket_id trong table “devices”
    • Khi có tin nhắn được emit thì tìm tất cả các socket_id của user trong conversation để gửi tin nhắn
  • Client: emit message và receive message

3. Server side

  1. Cài đặt các packet
    Ở đây thì mình vẫn sử dụng các pakage của các bài trước và sẽ cần thêm 1 số pakage khác nữa

    > npm install @nestjs/websockets
    
     > npm i socket.io
    
    
  2. Xử lý validation cho socket
    app.gateway.ts

    @WebSocketGateway(3006,{ cors:true})exportclassAppGatewayimplementsOnGatewayInit,
      OnGatewayConnection,
      OnGatewayDisconnect
    {
      @WebSocketServer() server: Server;private logger: Logger =newLogger('MessageGateway');constructor(private userService: UsersService,private jwtService: JwtService,){}//function get user from tokenasyncgetDataUserFromToken(client: Socket): Promise<UserEntity>{const authToken: any = client.handshake?.query?.token;try{const decoded =this.jwtService.verify(authToken);returnawaitthis.userService.getUserByEmail(decoded.email);// response to function}catch(ex){thrownewHttpException('Not found', HttpStatus.NOT_FOUND);}}}
  3. Xử lý logic Client connect

    Vẫn là trong app.gateway.ts

    @WebSocketGateway(3006,{ cors:true})exportclassAppGatewayimplementsOnGatewayInit,
      OnGatewayConnection,
      OnGatewayDisconnect
    {
      @WebSocketServer() server: Server;private logger: Logger =newLogger('MessageGateway');constructor(private userService: UsersService,private deviceService: DeviceService,private jwtService: JwtService,){}...asynchandleConnection(client: Socket){this.logger.log(client.id,'Connected..............................');const user: UserEntity =awaitthis.getDataUserFromToken(client);const device ={
          user_id: user.id,
          type: TypeInformation.socket_id,
          status:false,
          value: client.id,};awaitthis.deviceService.create(information);}}...
  4. Xử lý logic client disconnect

    Vẫn là trong app.gateway.ts

    @WebSocketGateway(3006,{ cors:true})exportclassAppGateway{...asynchandleDisconnect(client: Socket){const user =awaitthis.getDataUserFromToken(client);awaitthis.deviceService.deleteByValue(user.id, client.id);// need handle remove socketId to tablethis.logger.log(client.id,'Disconnect');}awaitthis.deviceService.create(information);}}...
  5. Xử lý Listen message và emit message to client

       @WebSocketGateway(3006,{ cors:true})exportclassAppGateway{...
    
         @SubscribeMessage('messages')asyncmessages(client: Socket, payload: MessagesInterface){// get all user trong conversation bằng conversation_idconst conversation =awaitthis.conversationService.findById(
             payload.conversation_id,['users'],);// get all socket id đã lưu trước đó của các user thuộc conversationconst dataSocketId =awaitthis.deviceService.findSocketId(userId);// Lưu dữ liệu vào bảng messageconst message =awaitthis.messageService.create({
             user_id: payload.user_id,
             status:false,
             message: payload.message,
             conversation_id: payload.conversation_id,
             createdAt:newDate(),
             updatedAt:newDate(),});//emit message đến socket_id
           dataSocketId.map((value)=>{
             emit.to(value.value).emit('message-received',{
               id: message.id,
               message: message.message,
               conversation_id: message.conversation_id,
               user_id: message.user_id,
               status: message.status,
               createdAt: message.createdAt,
               updatedAt: message.updatedAt,});});}}...

4. Client Side

> npm i socket.io-client

       > npm i redux-saga

       > npm i @reduxjs/toolkit

       > npm i redux
  1. Setting Redux + Redux saga:

    store.ts :

    const rootReducer =combineReducers({
        router:connectRouter(history),
        auth: authReducer,
        chat: chatReducer,})const sagaMiddleware =createSagaMiddleware()exportconst store =configureStore({
        reducer: rootReducer,middleware:(getDefaultMiddleware)=>getDefaultMiddleware().concat(sagaMiddleware,routerMiddleware(history)),});
    
      sagaMiddleware.run(rootSaga)exportfunction*rootSaga(){yieldall([authSaga(),chatSaga(),])}export type AppDispatch =typeof store.dispatch;export type RootState = ReturnType<typeof store.getState>;export type AppThunk<ReturnType =void>= ThunkAction<
        ReturnType,
        RootState,
        unknown,
        Action<string>>;

    Tiếp đó cần use redux saga trong file App.tsx

    import{store}from'./store'
        
        ReactDOM.render(// <React.StrictMode><Provider store={store}><ConnectedRouter history={history}>// thay vì dùng redux các bạn cũng có thể dùng Connect API cũng dễ dàng cho việc code hơn nhé// trong source mình cũng có để nhé{/*<SocketContext.Provider value={{socket}}>*/}// trong đây mình có dùng style componet material-ui các bạn cũng có thể tham khảo nhé<MuiThemeProvider theme={themes}><App /><RouterComponent /></MuiThemeProvider>{/*</SocketContext.Provider>*/}</ConnectedRouter></Provider>,// </React.StrictMode>,
        document.getElementById('root'));
  2. Tiền hành xử lý logic khi 1 action được dispatch

    NOTE: ở đây mình có sử dụng redux toolkit mọi người có thể tìm hiều về nó trước khi đọc tiếp nhé

    exportinterfaceMessage{
     id: number;
     user_id: number | string;
     conversation_id: number | string;
     message: string;}exportinterfaceConversation{
     messages: Message[];
     id: number;
     title: string |null;
     sending: boolean;}const initialState: ListConversationState ={
         loading:false,
         error:'',
         conversations:[],
         loaded:false,}exportconst chatSlice =createSlice({
         name:'chat',
         initialState,
         reducers:{sendMessage(state, action: PayloadAction<Message>){
             state.conversations = state.conversations.map(conversation=>{if(conversation.id === action.payload.conversation.id ){// cái này để hiển thị sending
                        conversation.sending =true}return conversation;});return state;},sendMessageSuccess(state, action: PayloadAction<Message>){// ở đây ta cần push message received vào list message của conversation đang activce 
               state.conversations = state.conversations.map(conversation=>{if(conversation.id === action.payload.conversation.id ){// cái này để hiển thị sending
                        conversation.sending =false
                          conversation.messages = conversation.messages 
                          ?[ action.payload,...conversation.messages]:[action.payload]}return conversation;});}}})
  3. Saga middleware
    trong file chatSaga.ts :

    import{ io, Socket }from'socket.io-client';functionconnect(){const token =getAccessToken();const url = process.env.REACT_APP_SOCKET_URL??'';const socket =io(url,{
        query:{ token }});returnnewPromise(resolve=>{
        socket.on('connect',()=>{// socket.emit('room', 'room1');resolve(socket);});})}//receive messagefunction*read(socket: Socket){while(true){
            socket.on('message-received',(message)=>{// dispatch sendMessageSuccessyieldput(chatActions.sendMessageSuccess, message)});;}}//handle send messagefunction*send(socket: Socket){while(true){const{ payload }=yieldtake(chatActions.sendMessage.type)
    
        socket.emit('messages', payload)}}function*handleIO(socket: Socket){yieldfork(read, socket);yieldfork(send, socket);}function*flowSocket(){const socket: Socket =yieldcall(connect)// ta cần 1 task thực hiện send and receive messageconst task: Task =yieldfork(handleIO, socket)// ở đây nếu logout thì cần close connect socketyieldtake(authAction.logout.type)yieldcancel(task)}//flow function*flow(){while(true){const isLoggedIn =Boolean(getToken())const currentUser =Boolean(getUser());// ở đây mình cần check điều kiện đã đăng nhập chưa if(isLoggedIn && currentUser){// đã đăng nhập thì cho phép nextyieldcall(flowSocket)}else{// nếu chưa đăng nhạp thì cần nắng nghe việc loginSuccess thì nextyieldtake(authAction.loginSuccess)yieldcall(flowSocket)}}}//root handleexportdefaultfunction*chatSaga(){yieldfork(flow)}
  4. Sử dụng trong component

    const Index: React.FC=()=>{const dispatch =useDispatch();const chat: ListConversationState =useSelector((state: RootState)=> state.chat)// const { socket } = useContext(SocketContext);const[message, setMessage]=useState('')constsendData=()=>{dispatch(chatActions.sendMessage({
                   message,
                   conversation_id: conversationActive.id,
                   user_id:getUser().id,}))setMessage('')}return(<div>{
                chat.messages && chat.messages.map((message, index)=><p key={index}>{message.message}</p>)}<input type='text' value={message} onChange={e=>setMessage(e.target.value)}/><button onClick={sendData}>{/*{ chat.sending ? 'Sending.........' : 'Send'}*/}
               send
             </button></div>)}

5. Kết quả và kết luận

Cuối cùng kết quá sẽ là :

Cảm ơn các bạn đã theo dõi series của mình.

Server side: tại đây

Client side: tại đây

Sắp tới mình đang định xây dựng series về Nextjs mời các bạn đón đọc

Nguồn: viblo.asia

Bài viết liên quan

WebP là gì? Hướng dẫn cách để chuyển hình ảnh jpg, png qua webp

WebP là gì? WebP là một định dạng ảnh hiện đại, được phát triển bởi Google

Điểm khác biệt giữa IPv4 và IPv6 là gì?

IPv4 và IPv6 là hai phiên bản của hệ thống địa chỉ Giao thức Internet (IP). IP l

Check nameservers của tên miền xem website trỏ đúng chưa

Tìm hiểu cách check nameservers của tên miền để xác định tên miền đó đang dùn

Mình đang dùng Google Domains để check tên miền hàng ngày

Từ khi thông báo dịch vụ Google Domains bỏ mác Beta, mình mới để ý và bắt đầ