- vừa được xem lúc

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

0 0 108

Người đăng: Đinh Xuân Dương

Theo Viblo Asia

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 })
    export class AppGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
    { @WebSocketServer() server: Server; private logger: Logger = new Logger('MessageGateway'); constructor( private userService: UsersService, private jwtService: JwtService, ) {} //function get user from token async getDataUserFromToken(client: Socket): Promise<UserEntity> { const authToken: any = client.handshake?.query?.token; try { const decoded = this.jwtService.verify(authToken); return await this.userService.getUserByEmail(decoded.email); // response to function } catch (ex) { throw new HttpException('Not found', HttpStatus.NOT_FOUND); } }
    } 
  3. Xử lý logic Client connect

    Vẫn là trong app.gateway.ts

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

    Vẫn là trong app.gateway.ts

    @WebSocketGateway(3006, { cors: true })
    export class AppGateway
    { ... async handleDisconnect(client: Socket) { const user = await this.getDataUserFromToken(client); await this.deviceService.deleteByValue(user.id, client.id); // need handle remove socketId to table this.logger.log(client.id, 'Disconnect'); } await this.deviceService.create(information); }
    }
    ... 
  5. Xử lý Listen message và emit message to client

     @WebSocketGateway(3006, { cors: true }) export class AppGateway { ... @SubscribeMessage('messages') async messages(client: Socket, payload: MessagesInterface) { // get all user trong conversation bằng conversation_id const conversation = await this.conversationService.findById( payload.conversation_id, ['users'], ); // get all socket id đã lưu trước đó của các user thuộc conversation const dataSocketId = await this.deviceService.findSocketId(userId); // Lưu dữ liệu vào bảng message const message = await this.messageService.create({ user_id: payload.user_id, status: false, message: payload.message, conversation_id: payload.conversation_id, createdAt: new Date(), updatedAt: new Date(), }); //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() export const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(sagaMiddleware, routerMiddleware(history)), }); sagaMiddleware.run(rootSaga) export function* rootSaga() { yield all([ 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é

    export interface Message { id: number; user_id: number | string; conversation_id: number | string; message: string;
    } export interface Conversation { messages: Message[]; id: number; title: string | null; sending: boolean;
    } const initialState: ListConversationState = { loading: false, error: '', conversations: [], loaded: false,
    }
    export const 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';
    function connect() { const token = getAccessToken(); const url = process.env.REACT_APP_SOCKET_URL ?? ''; const socket = io(url, { query: { token } }); return new Promise(resolve => { socket.on('connect', () => { // socket.emit('room', 'room1'); resolve(socket); }); })
    } //receive message
    function* read(socket: Socket) { while (true) { socket.on('message-received', (message) => { // dispatch sendMessageSuccess yield put(chatActions.sendMessageSuccess, message) }); ; }
    } //handle send message
    function* send(socket: Socket) { while (true) { const { payload } = yield take(chatActions.sendMessage.type) socket.emit('messages', payload) }
    } function* handleIO(socket: Socket) { yield fork(read, socket); yield fork(send, socket);
    } function* flowSocket() { const socket: Socket = yield call(connect) // ta cần 1 task thực hiện send and receive message const task: Task = yield fork(handleIO, socket) // ở đây nếu logout thì cần close connect socket yield take(authAction.logout.type) yield cancel(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 next yield call(flowSocket) } else { // nếu chưa đăng nhạp thì cần lắng nghe việc loginSuccess thì next yield take(authAction.loginSuccess) yield call(flowSocket) } }
    } //root handle
    export default function* chatSaga() { yield fork(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('') const sendData = () => { 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

Bình luận

Bài viết tương tự

- vừa được xem lúc

Cùng tìm hiểu về các hook trong React hooks

Đối với ai đã từng làm việc với React thì chắc hẳn đã có những lúc cảm thấy bối rối không biết nên dùng stateless (functional) component hay là stateful component. Nếu có dùng stateful component thì cũng sẽ phải loay hoay với đống LifeCycle 1 cách khổ sở Rất may là những nhà phát triển React đã kịp

0 0 100

- vừa được xem lúc

Khi nào nên (và không nên) sử dụng Redux

. Công việc quản lý state với những hệ thống lớn và phức tạp là một điều khá khó khăn cho đến khi Redux xuất hiện. Lấy cảm hứng từ design pattern Flux, Redux được thiết kế để quản lý state trong các project JavaScript.

0 0 127

- vừa được xem lúc

ReactJS: Props và State

Nếu bạn đã học ReactJS hay React Native, bạn sẽ thấy các Props và State được sử dụng rất nhiều. Vậy chính xác chúng là gì? Làm thế nào để chúng ta sử dụng chúng đúng mục đích đây.

0 0 59

- vừa được xem lúc

State và Props trong Reactjs

Hello các bạn, tiếp tục seri tìm hiểu về ReactJs hôm nay mình xin giới thiệu đến các bạn hai thứ mình cho là thú vị nhất của ReactJs là State và Props. State bạn có thể hiểu đơn giản là một nơi mà bạn lưu trữ dữ liệu của Component, từ đó bạn có thể luân chuyển dữ liệu đến các thành phần trong Compon

0 0 54

- vừa được xem lúc

Memoization trong React

. 1.Introduction. Memoization có liên quan mật thiết đến bộ nhớ đệm, và dưới đây là một ví dụ đơn giản:. const cache = {}.

0 0 51

- vừa được xem lúc

Nâng cao hiệu suất React Hooks với React.memo, Memoization và Callback Functions

1.Ngăn Re-render và React.memo. React.

0 0 80