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 + typeorm.
- Xác thực người dùng trong Nestjs sử dụng Passport JWT.
- Nestjs – Create relationship với Typeorm + mysql
- 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
-
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
-
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);}}}
-
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);}}...
-
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);}}...
-
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
-
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'));
-
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;});}}})
-
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)}
-
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