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

Thay đổi Package Name của Android Studio dể dàng với plugin APR

Nếu bạn đang gặp khó khăn hoặc bế tắc trong việc thay đổi package name trong And

Lỗi không Update Meta_Value Khi thay thế hình ảnh cũ bằng hình ảnh mới trong WordPress

Mã dưới đây hoạt động tốt có 1 lỗi không update được postmeta ” meta_key=

Bài 1 – React Native DevOps các khái niệm và các cài đặt căn bản

Hướng dẫn setup jenkins agent để bắt đầu build mobile bằng jenkins cho devloper an t

Chuyển đổi từ monolith sang microservices qua ví dụ

1. Why microservices? Microservices là kiến trúc hệ thống phần mềm hướng dịch vụ,