Rastreamento de localização em tempo real com Change Streams e Socket.io
Ashiq Sultan8 min read • Published Feb 09, 2023 • Updated Aug 13, 2024
Avalie esse Tutorial
Neste artigo, você aprenderá como usar MongoDB Change Streams e Socket.io para criar um aplicativo de rastreamento de localização em tempo real. Para demonstrar isso, criaremos um serviço local de entrega de pacotes.
Os change streams são usados para detectar atualizações de documentos, como localização e status de remessa, e o Socket.io é usado para transmitir essas atualizações para os clientes conectados. Um servidor Express.js será executado em segundo plano para criar e manter os websockets.
Este artigo destacará as partes importantes deste projeto de demonstração, mas você pode encontrar o código completo, juntamente com instruções de configuração, no Github.
Conectar Express.js ao MongoDB requer o uso do driver MongoDB, que pode ser instalado como um pacote npm. Neste projeto, usei o MongoDB Atlas e a camada gratuita para criar um cluster. Você pode criar seu próprio cluster gratuito e gerar a connection string a partir do dashboard do Atlas.
Implementei um padrão singleton para conexão com o MongoDB para manter uma conexão única em todo o aplicativo.
O código define uma
db
variável singleton que armazena a instância do MongoClient após a primeira conexão bem-sucedida com o banco de dados MongoDB.O dbConnect()
é uma função assíncrona que retorna a instância do MongoClient. Ele primeiro verifica se a variável db já foi inicializada e a retorna se tiver sido. Caso contrário, ele criará uma nova instância MongoClient e a retornará. A função dbConnect
é exportada como exportação padrão, permitindo que outros módulos a usem.1 // dbClient.ts 2 import { MongoClient } from 'mongodb'; 3 const uri = process.env.MONGODB_CONNECTION_STRING; 4 let db: MongoClient; 5 const dbConnect = async (): Promise<MongoClient> => { 6 try { 7 if (db) { 8 return db; 9 } 10 console.log('Connecting to MongoDB...'); 11 const client = new MongoClient(uri); 12 await client.connect(); 13 db = client; 14 console.log('Connected to db'); 15 return db; 16 } catch (error) { 17 console.error('Error connecting to MongoDB', error); 18 throw error; 19 } 20 }; 21 export default dbConnect;
Agora podemos chamar a função dbConnect no arquivo
server.ts
ou qualquer outro arquivo que sirva como ponto de entrada para seu aplicativo.1 // server.ts 2 import dbClient from './dbClient'; 3 server.listen(5000, async () => { 4 try { 5 await dbClient(); 6 } catch (error) { 7 console.error(error); 8 } 9 });
Agora temos nosso servidor Express conectado ao MongoDB. Com a configuração básica pronta, podemos continuar a incorporar change stream e socket.io em nosso aplicativo.
O MongoDB Change Stream é um recurso poderoso que permite ouvir alterações em sua collection do MongoDB em tempo real. Os change stream fornecem um mecanismo semelhante a uma notificação de alteração que permite que você seja notificado de quaisquer alterações em seus dados à medida que elas acontecem.
Para usar change streams, use a função
watch()
do driver do MongoDB. Aqui está um exemplo simples de como você usaria change streams em uma coleção.1 const changeStream = collection.watch() 2 changeStream.on('change', (event) => { 3 // your logic 4 })
A função de retorno de chamada será executada toda vez que um documento for adicionado, excluído ou atualizado na collection monitorada.
Socket.IO é uma biblioteca JavaScript popular. Ele permite a comunicação em tempo real entre o servidor e o cliente, sendo ideal para aplicativos que exigem atualizações em tempo real e transmissão de dados. Em nosso aplicativo, ele é usado para transmitir atualizações de localização e status de remessa para os clientes conectados em tempo real.
Um dos principais recursos do Socket.IO é a capacidade de criar “salas”. As salas são uma maneira de segmentar conexões e permitir que você transmita mensagens para grupos específicos de clientes. Em nosso aplicativo, as salas são usadas para garantir que as atualizações de localização e status da remessa sejam transmitidas apenas para os clientes que estão rastreando aquele pacote ou motorista específico.
O código para incluir o Socket.IO e seus manipuladores pode ser encontrado nos arquivos
src/server.ts
e src/socketHandler.ts
Estamos definindo todos os eventos Socket.IO dentro do arquivo
socketHandler.ts
para que o código relacionado ao soquete fique separado do restante do aplicativo. Abaixo está um exemplo para implementar os eventos básicos de conexão e desconexão do Socket.IO no Node.js.1 // socketHandler.ts 2 const socketHandler = (io: Server) => { 3 io.on('connection', (socket: any) => { 4 console.log('A user connected'); 5 socket.on('disconnect', () => { 6 console.log('A user disconnected'); 7 }); 8 }); 9 }; 10 export default socketHandler;
Agora podemos integrar a função socketHandler ao nosso
server.ts file
(o ponto de partida do nosso aplicativo) importando-a e chamando-a assim que o servidor começar a escutar.1 // server.ts 2 import app from './app'; // Express app 3 import http from 'http'; 4 import { Server } from 'socket.io'; 5 const server = http.createServer(app); 6 const io = new Server(server); 7 server.listen(5000, async () => { 8 try { 9 socketHandler(io); 10 } catch (error) { 11 console.error(error); 12 } 13 });
Agora temos a configuração do Socket.IO com nosso aplicativo Express. Na próxima seção, vamos ver como os dados de localização são armazenados e atualizados.
O MongoDB tem suporte integrado para armazenar dados de localização como GeoJSON, o que permite a query e indexação eficientes de dados espaciais. Em nosso aplicativo, a localização do motorista é armazenada no MongoDB como um ponto GeoJSON.
Para simular o movimento do motorista, no front-end, há uma opção para fazer login como motorista e mover o marcador do motorista pelo mapa, simulando a localização do motorista. (Mais sobre isso abordado na seção front-end.)
Quando o driver se move, um evento de soquete é acionado, enviando a localização atualizada para o servidor, que é então atualizada no banco de dados.
1 socket.on("UPDATE_DA_LOCATION", async (data) => { 2 const { email, location } = data; 3 await collection.findOneAndUpdate({ email }, { $set: { currentLocation: location } }); 4 });
O código acima trata o evento de soquete "UPDATE_DA_LOCATION". Ele recebe os dados de e-mail e localização da mensagem do soquete e atualiza a localização atual do driver correspondente no MongoDB database.
Até agora, abordamos como configurar um servidor Express e conectá-lo ao MongoDB. Também vimos como configurar o Socket.IO e ouvir as atualizações. Na próxima seção, abordaremos como usar change stream e emitir um evento de soquete do servidor para o front-end.
Este é o ponto central da discussão neste artigo. Quando uma nova entrega é solicitada da interface do usuário, uma entrada de remessa é criada no banco de dados. A remessa ficará em estado pendente até que um motorista a aceite.
Depois que o driver aceita a remessa, é criada uma sala de soquete com o ID do driver como o nome da sala, e o usuário que criou a remessa é inscrito nessa sala.
Veja um diagrama simples para ajudá-lo a visualizar melhor o fluxo.
Com o usuário inscrito na sala de soquete, tudo o que precisamos fazer é ouvir as mudanças na localização do driver. É aqui que o fluxo de alterações entra em cena.
Temos um change stream em vigor, que está ouvindo a collection Delivery Associate (Driver). Sempre que houver uma atualização na collection, ela será acionada. Usaremos essa função de retorno de chamada para executar nossa lógica de negócios.
Observe que estamos passando uma opção para a função de observação do fluxo de alterações
{ fullDocument: 'updateLookup' }
. Ela especifica que o documento atualizado completo deve ser incluído no evento de alteração, em vez de apenas o delta ou as alterações feitas no documento.1 const watcher = async (io: Server) => {\ 2 const collection = await DeliveryAssociateCollection();\ 3 const changeStream = collection.watch([], { fullDocument: 'updateLookup' });\ 4 changeStream.on('change', (event) => {\ 5 if (event.operationType === 'update') {\ 6 const fullDocument = event.fullDocument;\ 7 io.to(String(fullDocument._id)).emit("DA_LOCATION_CHANGED", fullDocument);\ 8 }});};
No código acima, estamos escutando todas as operações CRUD na collection Delivery Associado (Driver) e emitimos eventos de soquete apenas para operações de atualização. Como os nomes das salas são apenas IDs de driver, podemos obter a ID do driver no documento atualizado.
Dessa forma, podemos ouvir as alterações na localização do driver usando change streams e enviá-las ao usuário.
Na base de código, todo o código de fluxo de alterações do aplicativo estará dentro da pasta
src/watchers/
. Você pode especificar os observadores onde desejar, mas, para manter o código limpo, estou seguindo essa abordagem. O código abaixo mostra como a função watcher é executada no ponto de entrada do aplicativo, ou seja, no arquivo server.ts.1 // server.ts 2 import deliveryAssociateWatchers from './watchers/deliveryAssociates'; 3 server.listen(5000, async () => { 4 try { 5 await dbClient(); 6 socketHandler(io); 7 await deliveryAssociateWatchers(io); 8 } catch (error) { 9 console.error(error); 10 } 11 });
Nesta seção, vimos como os fluxos de alterações são usados para monitorar atualizações na coleção Associado à entrega (driver). Também vimos como a opção
fullDocument
na função de inspetor foi usada para recuperar o documento atualizado completo, o que nos possibilitou enviar os dados de localização atualizados para o usuário inscrito por meio de soquetes. A próxima seção se concentra em explorar a base de código front-end e como os dados emitidos são usados para atualizar o mapa em tempo real.Não entrarei em muitos detalhes no front-end, mas apenas para lhe dar uma visão geral, ele é baseado no React e usa Leaflet.js para Map.
Incluí todo o frontend como um subaplicativo no repositório do Github na pasta
/frontend
. O Readme contém as etapas sobre como instalar e iniciar o aplicativo.Iniciar o frontend oferece duas opções:
1. Faça login como usuário.2. Faça login como driver.
Use a opção "log in as driver" para simular a localização do motorista. Isto pode ser feito simplesmente arrastando o marcador pelo mapa.
Fazer login como motorista permitirá simular a localização do motorista. O snippet de código fornecido demonstra o uso de ganchos de
useState
e useEffect
para simular as atualizações de localização de um driver. O <MapContainer>
e o<DraggableMarker>
são componentes do folheto. Um é o mapa real que vemos na interface do usuário e o outro é, como o nome sugere, um marcador que pode ser movido usando o mouse.1 // Driver Simulator 2 const [position, setPosition] = useState(initProps.position); 3 const gpsUpdate = (position) => { 4 const data = { 5 email, 6 location: { type: 'Point', coordinates: [position.lng, position.lat] }, 7 }; 8 socket.emit("UPDATE_DA_LOCATION", data); 9 }; 10 useEffect(() => { 11 gpsUpdate(position); 12 }, [position]); 13 return ( 14 <MapContainer> 15 <DraggableMarker position={position}/> 16 </MapContainer> 17 )
O estado daposição é inicializado com as props iniciais. Quando o marcador arrastável é movido, a posição é atualizada. Isso aciona a função gpsUpdate dentro de seu gancho useEffect, que envia um evento de soquete para atualizar a localização do driver.
No lado do aplicativo do usuário, quando uma nova remessa é criada e um associado de entrega é atribuído, o evento de soquete
SHIPMENT_UPDATED
é acionado. Em resposta, o aplicativo do usuário emite o eventoSUBSCRIBE_TO_DA
para assinar o socket room do motorista (DA é a abreviação de Delivery Associate, associado de entrega).1 socket.on('SHIPMENT_UPDATED', (data) => { 2 if (data.deliveryAssociateId) { 3 const deliveryAssociateId = data.deliveryAssociateId; 4 socket.emit('SUBSCRIBE_TO_DA', { deliveryAssociateId }); 5 } 6 });
Uma vez inscrito, qualquer alteração na localização do motorista trigger o evento de soquete DA_LOCATION_CHANGED. O estado
driverPosition
representa a posição atual do motorista de entrega. Ele é atualizado sempre que novos dados são recebidos do evento de soquete.1 const [driverPosition, setDriverPosition] = useState(initProps.position); 2 socket.on('DA_LOCATION_CHANGED', (data) => { 3 const location = data.location; 4 setDriverPosition(location); 5 }); 6 return ( 7 <MapContainer> 8 <Marker position={driverPosition}/> 9 </MapContainer> 10 )
O código demonstra como o aplicativo do usuário atualiza a posição do marcador do motorista no mapa em tempo real usando eventos de soquete. O estado driverPosition é passado para o componente e atualizado com os dados de localização mais recentes do evento de soquete DA_LOCATION_CHANGED.
Neste artigo, vimos como o MongoDB Change Streams e Socket.IO podem ser usados em um aplicativo Node.js Express para desenvolver um sistema de tempo real.
Avaliamos como monitorar uma coleção do MongoDB usando o método change stream watcher. Também aprendemos como as salas Socket.IO podem ser usadas para segmentar conexões de soquete para transmitir atualizações. Também vimos um pouco de código de front-end sobre como as props são manipuladas com eventos de soquete.
Se quiser saber mais sobre Change Streams, confira nosso tutorial sobre Change Streams e triggers com Node.jsou a versão em vídeo dele. Para obter um tutorial mais detalhado sobre como usar o Change Streams diretamente em seu aplicativo React, você também pode conferir este tutorial sobre dados em tempo real em um front-end JavaScript do React.
Relacionado
Artigo
Análise de moeda com coleções de séries temporais # 3 — Cálculo de MACD e RSI
Sep 11, 2024 | 8 min read
Início rápido
Como conectar o MongoDB Atlas ao Vercel usando a nova integração
Aug 05, 2024 | 4 min read