
Push notifications in Grails and React.js based web projects
This article is more technical oriented and targeting other developers, not the final client. Usually, systems require communication from the server to the client. This is essential because some events occur on the server and a notification must be sent to the client. A simple use case is a chat, where users can exchange messages between them. We can use other technologies too, for example, the Message Queue system. Depending on your needs you can choose the most suitable one.
Description
In this post we will discuss push messages using WebSocket. My goal is to describe the pedagogy behind the work, when it comes to delivery and production, we obviously have simpler routines at the company to accommodate each client order.
Goals
To be able to implement push notifications in web applications
Requirements
Grails 3.* (Dependencies: Grails Spring WebSocket, Grails Spring Security Rest)
React.js (Dependencies: Sock.js client, Stomp.js)
1 – Server configuration
We need to configure our WebSocket message agent to allow our system to send and receive messages through secure channels.
The following code (configureClientInboundChannel method) shows how to authenticate through WebSocket. We need to be sure that only an authorized user can connect, subscribe and receive messages from our server. Our system uses Spring Security Rest and, consequently, users are authenticated through the access token in each request.
We also configure the endpoint in which our message agent will listen to subscription requests and messages (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 – Sending notifications
We use Grails Spring WebSocket and it will provide us with the necessary functionality ready to use according to the configuration.
In our example, the messages will have a related action, in order to delete, update or create messages in the client. This is also possible if we create different topics for that purpose, but in this case it is not relevant.
BrokerService will be responsible for sending messages to a destination using 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
In the web client we use Sock.js and Stomp.js to subscribe to our messaging channel.
To subscribe to a channel in the connection request, we send the header configuration with the user’s token as authorization.
We set as parameter a callback function that will be executed after receiving a message from the server.
The unsubscribe method allows you to delete a subscription, the user will not receive anymore push notifications afterwards.
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 – Subscription and reception of notifications
We will place the subscription call on componentDidMount using a class based on React Component.
We also need to implement the functionality that will be executed once we receive the notifications.
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/> ) } }
Conclusion
Push notifications make our system more attractive and useful, it also reduces server latency and improves user experience. In many cases, we can add it using WebSockets in an easy way and with good results.
References
/Carlos Senior Developer at Visionmate
All articles