Push-notiser i Grails & React.js baserade webbprojekt

Det här inlägget riktar sig kanske mer mot dig som utvecklare än dig som beställare. Som oftast med system så krävs det en kommunikation mellan servern och klienten. Det är viktigt eftersom olika händelser kan uppstå på servern och en anmälan ska då skickas till klienten. Ett enkelt user cace är en chatt, där användarna kan utbyta meddelanden mellan varandra. Vi kan också använda annan teknik, till exempel Message Queue system. Beroende på behoven så kan man välja den man har mest användning för.

Beskrivning

I det här inlägget kommer vi att diskutera push-meddelanden med hjälp av WebSocket. Jag vill beskriva pedagogiken bakom arbetet, när det kommer till leverans och produktion så har vi självklart enklare rutiner på företaget för att bemöta varje beställning.

Mål

Att kunna genomföra push-notiser i webbapplikationer

Krav

Grails 3.* (Dependencies: Grails Spring WebSocket, Grails Spring Security Rest)

React.js (Dependencies: Sock.js client, Stomp.js)

 

1 – Server konfiguration

Vi måste konfigurera WebSocket message agent för att låta vårt system skicka och ta emot meddelanden via säkra kanaler.

Följande kod (configureClientInboundChannel metod) visar hur man autentiserar via WebSocket. Vi måste vara säkra på att endast en auktoriserad användare kan ansluta, prenumerera och ta emot meddelanden från vår server. Vårt system använder Spring Security Rest och följaktligen är användarna autentiserade via access token i varje förfrågan.

Vi konfigurerar också den endpoint där vår message agent lyssnar på subscription requests och meddelanden (registerStompEndpoints method)

import grails.plugin.springsecurity.rest.token.storage.TokenStorageService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.messaging.simp.SimpMessageType
import org.springframework.messaging.simp.config.ChannelRegistration
import org.springframework.messaging.simp.stomp.StompHeaderAccessor
import org.springframework.messaging.support.ChannelInterceptorAdapter
import org.springframework.messaging.support.MessageBuilder
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker
import org.springframework.web.socket.config.annotation.StompEndpointRegistry
import org.springframework.messaging.Message
import org.springframework.messaging.MessageChannel
 

@Configuration
@EnableWebSocketMessageBroker
class DefaultWebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

   public static final String AUTHORIZATION = 'Authorization'
   public static final String BEARER = 'Bearer'

   @Autowired
   TokenStorageService tokenStorageService
 

@Override
void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
   stompEndpointRegistry.addEndpoint("/ws")
         .setAllowedOrigins("*")
         .withSockJS()
         .setSessionCookieNeeded(false)
}
 

void configureClientInboundChannel(ChannelRegistration registration) {
   registration.setInterceptors(new ChannelInterceptorAdapter() {
      Message<Object> preSend(Message<Object> message,  MessageChannel channel) {
         String token = null
         Boolean access = false
         StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message)
         List tokens = accessor.getNativeHeader(AUTHORIZATION)
         accessor.removeNativeHeader(AUTHORIZATION)

         if(tokens?.size > 0) {
            token = tokens[0]
         }

         if (token?.startsWith(BEARER)) {
            token = token.substring(BEARER.length()).trim()
         }

         switch (accessor.messageType){
            case SimpMessageType.CONNECT: access = (tokenStorageService.loadUserByToken(token))?true:false
               break
            case SimpMessageType.SUBSCRIBE: access = (tokenStorageService.loadUserByToken(token))?true:false
               break
         }
         accessor.setLeaveMutable(true)
         return (access)?MessageBuilder.createMessage(message.payload, accessor.messageHeaders):null
      }
   })
}

2 – Skicka meddelanden

Vi använder Grails Spring WebSocket vilket ger oss den funktionaliteten som behövs för att kunna använda konfigurationen.

I vårt exempel kommer meddelandena att ha en relaterad åtgärd, för att radera, uppdatera eller skapa meddelanden i klienten. Detta är också möjligt om vi skapar olika ämnen för det ändamålet, men i det här fallet är det inte relevant. BrokerService ansvarar för att skicka meddelanden till en destination med hjälp av brokerMesssagingService.

class BrokerService {

    def brokerMessagingTemplate
    /**
     * Send message to destination
     * @param destination
     * @param message
     */
    void send(String destination, Map message) {
        brokerMessagingTemplate.convertAndSend destination, (message as JSON) as String
    }
}

3 – Web client configuration

I webbklienten använder vi Sock.js och Stomp.js för att prenumerera på vår meddelandekanal. För att prenumerera på en kanal i connection request så skickar vi header configuration med användarens token som behörighet.

Vi ställer som en parameter en callback-funktion som kommer att utföras efter att ha mottagit ett meddelande från servern. Med unsubscribe method kan du ta bort en prenumeration, användaren kommer då inte längre att få push-meddelanden när det är gjort.

import {SERVER_URL} from "../config";
import headers from "../security/headers";
import SockJS from 'sockjs-client';
import Stomp from 'stompjs';

/**
 * WebSockets support
 */
const WebSocket = (endpoint) => {

    const socket = SockJS(`${SERVER_URL}/${endpoint}`);
    const client = Stomp.over(socket);

    function subscribe(destination, callback) {
        client.connect(headers(), function(frame) {
            client.subscribe(destination, function(message){
              callback(JSON.parse(message.body))
              }, headers());
           });
        return client;
    }

    function unsubscribe(client){
        for (const sub in client.subscriptions) {
            if (client.subscriptions.hasOwnProperty(sub)) {
                client.unsubscribe(sub);
            }
        }
    }

    return {
        subscribe,
        unsubscribe
    }

};

export default WebSocket;

4 – Prenumeration och mottagande av anmälningar

Vi lägger subscription call på componentDidMount med en klass baserad på React Component.

Vi måste också implementera funktionaliteten som ska utföras när vi får notiserna.

import React from 'react';
import WebSocket from "../../api/WebSocket";
import {ACTION_STATUS} from '../../constant';
import MessageList from "../../components/MessageList";
import { notifyAddedMessage, notifyDeletedMessage,  notifyUpdatedMessage} from "../../actions/actions";


/**
 * Real-time messages
 */
class MessagePanel extends React.Component {

    componentDidMount() {
        this.subscribe();

    }

    componentWillUnmount() {
        if(this.state.client !== undefined)
            WebSocket('ws').unsubscribe(this.state.client);
    }

    subscribe() {
        const callback = (message) => {
            switch (message.action){
                case ACTION_STATUS.DELETED:
                    this.props.notifyDeletedMessage(message.id);
                    break;
                case ACTION_STATUS.UPDATED:
                    this.props.notifyUpdatedMessage(message);
                    break;
                default:
                    this.props.notifyAddedMessage(message);
            }
        };
        callback.bind(this);

        let client;
        if (user instanceof Object) {
            client = WebSocket('ws').subscribe(‘/topic/messages’, callback);
            this.setState({client});
        }
    }

    render() {
        return (
            <MessageList/>
        )
    }
}

Slutsats

Push-meddelanden gör systemet attraktivt och mer användbart, det minskar också serverns latency och förbättrar användarupplevelsen. I många fall kan vi, på ett enkelt sätt och med ett bra resultat, lägga till det med hjälp av WebSockets .

Referens

https://docs.spring.io/spring/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/html/websocket.html

 

/Carlos Senior Developer på Visionmate

Alla artiklar