Adição de notificações em tempo real ao Ghost CMS usando MongoDB e eventos enviados pelo servidor
TQ
Tobias Quante7 min read • Published Aug 14, 2023 • Updated Aug 14, 2023
Avalie esse Tutorial
Isso garante que o núcleo do sistema permaneça enxuto. Para integrar aplicativos de terceiros, você nem precisa instalar plugins. Em vez disso, o Ghost oferece um recurso chamado Webhooks, que é executado enquanto você trabalha em sua publicação.
Esses webhooks enviam itens específicos, como uma publicação ou uma página, para um endpoint HTTP definido por você e, assim, fornecem uma base ideal para nosso serviço em tempo real.
Você provavelmente está familiarizado com o conceito de sessão HTTP. Um cliente envia uma solicitação, o servidor responde e fecha a conexão. Ao usar eventos enviados pelo servidor (SSEs), essa conexão permanece aberta. Isso permite que o servidor continue gravando mensagens na resposta.
Como os Websockets (WS), aplicativos e sites usam SSEs para comunicação em tempo real. Enquanto os WSs usam um protocolo dedicado e funcionam em ambas as direções, os SSEs são unidirecionais. Eles usam endpoints HTTP simples para escrever uma mensagem sempre que ocorre um evento no lado do servidor.
1 const subscription = new EventSource("https://example.io/subscribe")
Agora que examinamos a borda do nosso aplicativo, é hora de apresentar seu núcleo. Usaremos o MongoDB para armazenar um subconjunto dos dados do webhook dohost recebidos. Além disso, usaremos o MongoDB Change Streams para assistir à nossa coleção de webhook.
Resumindo, o Change Streams registra os dados que fluem para nosso banco de dados. Podemos assinar esse fluxo de dados e React a ele. Reagir significa enviar mensagens SSE para clientes conectados sempre que um novo webhook é recebido e armazenado.
O código Javascript a seguir mostra uma assinatura simples do Change Stream.
1 import {MongoClient} from 'mongodb'; 2 3 const client = new MongoClient("<mongodb-url>"); 4 const ghostDb = client.db('ghost'); 5 const ghostCollection = ghostDb.collection('webhooks'); 6 const ghostChangeStrem = ghostCollection.watch(); 7 8 ghostChangeStream.on('change', document => { 9 /* document is the MongoDB collection entry, e.g. our webhook */ 10 });
Sua natureza baseada em eventos combina perfeitamente com webhooks e SSEs. Podemos React a webhooks recém-chegados onde os dados são criados, garantindo a integridade dos dados em todo o nosso aplicativo.
Precisamos de uma camada extra de aplicativo para propagar essas alterações para clientes conectados. Decidi usar Typescript e Express, mas você pode usar qualquer outra framework do lado do servidor. Você também precisará de uma instância dedicada do MongoDB*. Para um início rápido, você pode se inscrever no MongoDB Atlas. Em seguida, crie um cluster gratuito e conecte-se a ele.
Vamos começar clonando o branch
1-get-started
deste repositório do Github:1 # ssh 2 $ git clone git@github.com:tq-bit/mongodb-article-mongo-changestreams.git 3 4 # HTTP(s) 5 $ git clone https://github.com/tq-bit/mongodb-article-mongo-changestreams.git 6 7 # Change to the starting branch 8 $ git checkout 1-get-started 9 10 # Install NPM dependencies 11 $ npm install 12 13 # Make a copy of .env.example 14 $ cp .env.example .env
Certifique-se de preencher a variável de ambiente MONGO_HOST com sua connection string!
O Express e o cliente de banco de dados já estão implementados. Então, a seguir, vamos nos concentrar em adicionar MongoDB change stream e eventos enviados pelo servidor.
Depois que tudo estiver configurado, você poderá iniciar o servidor em
http://localhost:3000
digitando1 npm run dev
O aplicativo usa dois endpoints importantes que estenderemos nas próximas seções:
/api/notification/subscribe
<- Usado pelo EventSource para receber mensagens de eventos/api/notification/article/create
< - Usado como alvo de webhook peloGhost
* Se você não estiver usando o MongoDB Atlas, certifique-se de ter osconjuntos de replicação habilitados.
Abra o projeto clonado em seu editor de código favorito. Adicionaremos nossa lógica SSE em
src/components/notification/notification.listener.ts
.Em resumo, a implementação de SSE requer três etapas:
- Escreva um cabeçalho de status HTTP 200 .
- Escreva uma mensagem de abertura.
- Adicione manipuladores de mensagens de resposta baseados em eventos.
Começaremos a enviar uma mensagem estática e revisitaremos este módulo depois de adicionar ChangeStreams.
Você também pode
git checkout 2-add-sse
para ver o resultado final.Escrever o cabeçalho HTTP informa os clientes sobre uma conexão bem-sucedida. Ele também propaga o tipo de conteúdo da resposta e garante que os eventos não sejam armazenados em cache.
Adicione o seguinte código à função
subscribeToArticleNotification
interna:1 // Replace 2 // TODO: Add function to write the head 3 // with 4 console.log('Step 1: Write the response head and keep the connection open'); 5 res.writeHead(200, { 6 'Content-Type': 'text/event-stream', 7 'Cache-Control': 'no-cache', 8 Connection: 'keep-alive' 9 });
A primeira mensagem enviada deve ter um tipo de evento de "abrir". Não é obrigatório, mas ajuda a determinar se a inscrição foi bem-sucedida.
Acrescente o seguinte código à função
subscribeToArticleNotification
:1 // Replace 2 // TODO: Add functionality to write the opening message 3 // with 4 console.log('Step 2: Write the opening event message'); 5 res.write('event: open\n'); 6 res.write('data: Connection opened!\n'); // Data can be any string 7 res.write(`id: ${crypto.randomUUID()}\n\n`);
Podemos personalizar o conteúdo e o tempo de todas as outras mensagens enviadas. Vamos adicionar uma função de espaço reservado que envia mensagens a cada cinco segundos por enquanto. E já que estamos nisso, vamos também adicionar um manipulador para fechar a conexão do cliente:
Acrescente o seguinte código à função
subscribeToArticleNotification
:1 setInterval(() => { 2 console.log('Step 3: Send a message every five seconds'); 3 res.write(`event: message\n`); 4 res.write(`data: ${JSON.stringify({ message: 'Five seconds have passed' })}\n`); 5 res.write(`id: ${crypto.randomUUID()}\n\n`); 6 }, 5000); 7 8 9 // Step 4: Handle request events such as client disconnect 10 // Clean up the Change Stream connection and close the connection stream to the client 11 req.on('close', () => { 12 console.log('Step 4: Handle request events such as client disconnect'); 13 res.end(); 14 });
Para verificar se tudo está funcionando, visite
http://localhost:3000/api/notification/subscribe
.Vamos visitar
src/components/notification/notification.model.ts
a seguir. Adicionaremos um comandoinsert
simples para nosso banco de dados na função createNotificiation
:Você também pode
git checkout 3-webhook-handler
para ver o resultado final.1 // Replace 2 // TODO: Add insert one functionality for DB 3 // with 4 return notificationCollection.insertOne(notification);
E para
src/components/notification/notification.controller.ts
. Para processar webhooks recebidos, adicionaremos uma função de manipulador a handleArticleCreationNotification
:1 // Replace 2 // TODO: ADD handleArticleCreationNotification 3 // with 4 const incomingWebhook: GhostWebhook = req.body; 5 await NotificationModel.createNotificiation({ 6 id: crypto.randomUUID(), 7 ghostId: incomingWebhook.post?.current?.id, 8 ghostOriginalUrl: incomingWebhook.post?.current?.url, 9 ghostTitle: incomingWebhook.post?.current?.title, 10 ghostVisibility: incomingWebhook.post?.current?.visibility, 11 type: NotificationEventType.PostPublished, 12 }); 13 14 res.status(200).send('OK');
Esse manipulador pegará os dados do webhook de entrada e inserirá uma nova notificação.
1 curl -X POST -d '{ 2 "post": { 3 "current": { 4 "id": "sj7dj-lnhd1-kabah9-107gh-6hypo", 5 "url": "http://localhost:2368/how-to-create-realtime-notifications", 6 "title": "How to create realtime notifications", 7 "visibility": "public" 8 } 9 } 10 }' http://localhost:3000/api/notification/article/create
Você também pode testar a funcionalidade de inserção usando o cliente Postman ou VSCode REST e, em seguida, verificar sua MongoDB collection.Para sua conveniência, há uma solicitação de exemplo em
/test/notification.rest
no diretório do projeto.Até agora, podemos enviar SSEs e inserir notificações do Ghost. Vamos colocar esses dois recursos juntos agora.
Anteriormente, adicionamos uma mensagem estática de servidor enviada a cada cinco segundos. Vamos revisar
src/components/notification/notification.listener.ts
e torná-lo mais dinâmico.Primeiro, vamos nos livre de todo o
setInterval
e de sua chamada de resposta. Em vez disso, usaremos nosso notificationCollection
e seu método interno watch
. Este método retorna um ChangeStream
.Você pode criar um change stream adicionando o seguinte código acima do segmento de código
export default
:1 const notificationStream = notificationCollection.watch();
O stream aciona um evento sempre que a coleção relacionada é alterada. Isso inclui o evento
insert
da seção anterior.Podemos registrar funções de retorno de chamada para cada uma delas. O evento que é acionado quando um documento dentro da collection é alterado é "change":
1 notificationStream.on('change', (next) => { 2 console.log('Step 3.1: Change in Database detected!'); 3 });
A variável passada para a função de retorno de chamada é um documento de fluxo de alterações. Inclui duas informações importantes para nós:
- O documento inserido, atualizado ou excluído.
- O tipo de operação na coleção.
Vamos atribuí-las a uma variável, cada uma dentro do callback:
1 notificationStream.on('change', (next) => { 2 // ... previous code 3 const { 4 // @ts-ignore, fullDocument is not part of the next type (yet) 5 fullDocument /* The newly inserted fullDocument */, 6 operationType /* The MongoDB operation Type, e.g. insert */, 7 } = next; 8 });
Vamos escrever a notificação para o cliente. Podemos fazer isso repetindo o método que usamos para a mensagem de abertura.
1 notificationStream.on('change', (next) => { 2 // ... previous code 3 console.log('Step 3.2: Writing out response to connected clients'); 4 res.write(`event: ${operationType}\n`); 5 res.write(`data: ${JSON.stringify(fullDocument)}\n`); 6 res.write(`id: ${crypto.randomUUID()}\n\n`); 7 });
E é isso! Você pode testar se tudo está funcional por:
- Abrir seu navegador em
http://localhost:3000/api/notification/subscribe
. - Usando o arquivo em
test/notification.rest
com o cliente HTTP do VSCode. - Verificar se o seu navegador inclui uma abertura e uma notificação fantasma.
Para uma implementação de webhook HTTP, você precisará de uma instância do Ghost em execução. Adicionei um dockerfile a este repositório para sua conveniência. Você também pode instalar o Ghost localmente.
Para iniciar o Ghost com o dockerfile, certifique-se de ter o Docker Engine ou o Docker Desktop com suporte para
docker compose
instalado.Para uma instalação local e a primeira configuração, você deve seguir o guia oficial de instalação do Ghost.
Depois que a instância do Ghost estiver em funcionamento, abra o navegador em
http://localhost:2368/ghost
. Você pode configurar seu site como quiser, dar um nome a ele, inserir detalhes e assim por diante.Para criar um webhook, você deve primeiro criar uma integração personalizada. Para fazer isso, navegue até as configurações do seu site e clique no ponto de menu “Integrations”. Clique em "Add Webhook, ", insira um nome e clique em "Create. "
Dentro da integração recém-criada, agora você pode configurar um webhook para apontar para seu aplicativo em
http://<host>:<port>/api/notification/article/create
*.* Esse URL pode variar de acordo com a configuração local do Spark. Por exemplo, se você executar o Python em um contêiner, poderá encontrar o IP local da sua máquina usando o terminal e
ifconfig
no Linux ou ipconfig
no Windows.E é isso. Agora, sempre que uma postagem for publicada, seu conteúdo será enviado para nosso endpoint em tempo real. Após ser inserido no MongoDB, uma mensagem de evento será enviada a todos os clientes conectados.
Existem algumas maneiras de adicionar notificações em tempo real ao seu tema Ghost. Entrar em detalhes está além do escopo deste artigo. Eu preparei dois arquivos, um
plugin.js
e um arquivo plugin.css
que você pode injetar no tema Casper padrão.Tente fazer isso iniciando uma instância local do Ghost usando o dockerfile fornecido.
Em seguida, você deve instruir seu aplicativo a exibir os ativos JS e CSS. Adicione o seguinte ao seu arquivo
index.ts
:1 // ... other app.use hooks 2 app.use(express.static('public')); 3 // ... app.listen()
1 <script src="http://localhost:3000/js/plugin.js"></script> 2 <link href="http://localhost:3000/css/plugin.css" rel="stylesheet">
A peça principal do plugin é a API do navegador EventSource. Você vai querer usá-lo ao integrar este aplicativo com outros temas.
Ao voltar para a publicação do Ghost, você deverá ver um pequeno ícone de sino no canto superior direito.
Se você acompanhou, parabéns! Agora você tem um serviço de notificação em tempo real funcionando para o seu blog Ghost. E se ainda não, o que está esperando? Cadastre-se para uma conta gratuita no MongoDB Atlas e comece a construir. Você pode usar a ramificação final deste repositório para começar e explorar todo o poder do kit de ferramentas do MongoDB.