Al finalizar este tutorial el estudiante estará en capacidad de utilizar WebSockets en una aplicación React.
Una aplicación compuesta por un back y un front. El back (desarrollado en express) se conectará a una colección de Mongo. Se hará la configuración necesaria para que el back sea notificado cuando hayan cambios en la colección. El front establecerá una conexión con el back vía WebSockets. Esto permitirá que el front también sea notificado de cambios en la colección para que modifica la vista en consecuencia.
Para poder realizar este taller Ud. debe haber tener claro:
Inicie creando un nuevo proyecto en express con el comando express websockets --git
.
Ingrese al proyecto e instale las dependencias con npm install
Vaya al archivo bin/www
y en la línea 15 cambie el puerto en el cual express escucha peticiones por el 3001.
var port = normalizePort(process.env.PORT || "3001");
En el archivo package.json
cambie el valor del atributo scripts.start
por "nodemon ./bin/www
". Recuerde que para esto debe tener previamente instalado nodemon (npm install -g nodemon
).
{
"scripts": {
"start": "nodemon ./bin/www"
}
...
}
En la raíz del proyecto cree una carpeta denominada lib
, la cual contendrá la librería para el manejo de la base de datos.
Dentro de lib
cree un archivo mongolib.js
con el siguiente contenido:
const MongoClient = require("mongodb").MongoClient;
const assert = require("assert");
const url = "mongodb://localhost:27017";
const dbName = "reactivedb";
const client = new MongoClient(url, { useUnifiedTopology: true });
const getDatabase = (callback) => {
client.connect(function (err) {
assert.equal(null, err);
console.log("Connected successfully to server");
const db = client.db(dbName);
callback(db, client);
});
};
const getDocuments = function (db, callback) {
const collection = db.collection("reactive");
collection.find({}).toArray(function (err, docs) {
assert.equal(err, null);
callback(docs);
});
};
exports.getDatabase = getDatabase;
exports.getDocuments = getDocuments;
Para que la conexión con la base de datos se pueda establecer, se requiere instalar la dependencia mongodb (npm install mongodb
).
En este archivo se hace una conexión a una base de datos local en Mongo denominada reactivedb
. La función getDocuments
pasa como parámetro al callback el listado de documentos almacenados en la colección reactive
.
En el archivo app.js
en la línea 22 defina una nueva ruta para obtener los documentos de la colección así:
app.use("/docs", indexRouter);
En el archivo routes/index.js
, modifique la ruta por defecto así:
var express = require("express");
var router = express.Router();
const Mongolib = require("../lib/mongolib");
router.get("/", function (req, res, next) {
Mongolib.getDatabase((db, client) => {
Mongolib.getDocuments(db, (docs) => {
res.json(docs);
});
});
});
module.exports = router;
Para probar el nuevo endpoint vamos a crear una aplicación React dentro del proyecto express.
En el archivo app.js
del back cambie la línea 22 por esta.
app.use(express.static(path.join(__dirname, "front/build")));
De este modo, todo lo que esté en la carpeta front/build
será tratado por express como assets estáticos.
Ahora, en la terminal ingrese el comando npx create-react-app front
. Esto crea una nueva aplicación React.
Abra el archivo front/package.json
y agregue un atributo "proxy
" con el valor "http://localhost:3001
".
{
...
"proxy": "http://localhost:3001",
...
}
De este modo, las peticiones que se hagan desde el front al back pueden tener la forma "/docs
", por ejemplo.
En el front, borre todo el contenido de la carpeta src
.
Dentro de front/src
cree un componente App
con el siguiente contenido:
import React, { Component } from "react";
class App extends Component {
state = {
docs: [],
};
componentDidMount() {
fetch("/docs")
.then((docs) => docs.json())
.then((docs) => {
this.setState({ docs });
});
}
render() {
return (
<div>
<ul>
{this.state.docs.map((e, i) => (
<li key={i}>{e.name}</li>
))}
</ul>
</div>
);
}
}
export default App;
Este componente se conecta al endpoint http://localhost:3001/docs
del back, trae la colección, la setea en el estado y luego renderiza un div con una viñeta por cada documento de la colección. Por simplicidad, en la colección solo se guarda un atributo denominado name
.
Dentro de front/src
cree un archivo index.js
con el siguiente contenido:
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
Abra dos terminales. En la primera terminal y ubicado en el proyecto back ejecute npm start
. Esto iniciará el back en el puerto 3001.
En la segunda terminal ingrese a la carpeta front y ejecute npm start.
Esto iniciará el front en el puerto 3000.
Dependiendo de los datos que tenga almacenados en la colección, deberá ver una página con un contenido semejante a este:
Hasta el momento, si hay un cambio en la colección, la única forma para observarlo es refrescando la ventana del navegador. No obstante queremos que la página se actualice de forma automática cuando se actualice la colección.
Para esto debemos hacer varios ajustes. El primero de ellos es iniciar mongo no como una instancia standalone sinó como una replica set. En una consola ingrese mongo
. Luego, cambie la base de datos a admin (use admin
) y pare el servicio (db.shutdownServer()
).
Ahora, arranque el servicio mongo con la opción de réplica así:
mongod --config /usr/local/etc/mongod.conf --fork --replSet rs
El anterior comando es para una instancia de mongo corriendo en un equipo con MacOS. Para Windows puede tomar como referencia este documento:
Luego, debe conectarse al shell de mongo y ejecutar el comando rs.initiate()
Ahora, dentro del archivo lib/mongolib.js
vamos a incluir una función que se ejecuta cada vez que hay un cambio en la colección.
...
const listeningForChanges = (db) => {
const cursor = db.collection("reactive").watch();
cursor.on("change", (data) => {
console.log("Collection changing...", data);
});
};
...
exports.listeningForChanges = listeningForChanges;
En el archivo bin/www
incluya la referencia a mongolib
...
const Mongolib = require("../lib/mongolib");
...
Y cuando el servidor express esté escuchando peticiones haga el llamado al método listeningForChanges
de mongolib
.
...
server.listen(port);
server.on("error", onError);
server.on("listening", onListening);
Mongolib.getDatabase((db, client) => {
Mongolib.listeningForChanges(db);
});
...
Ahora, si desde la consola de mongo inserta un nuevo documento podrá ver en la consola de express que se despliega el mensaje "Collection changing
" y el detalle del nuevo documento ingresado.
Ahora vamos a incluir WebSockets para notificar a los clientes del back sobre los cambios en la colección.
Cree un nuevo archivo lib/wslib.js
con el siguiente contenido:
const WebSocket = require("ws");
let clients = [];
const setWs = (server) => {
const ws = new WebSocket.Server({ server });
ws.on("connection", (ws) => {
console.log("New socket connection");
clients.push(ws);
});
};
const notifyAll = (data) => {
console.log("notify all");
clients.forEach((ws) => ws.send(JSON.stringify(data)));
};
exports.setWs = setWs;
exports.notifyAll = notifyAll;
Este archivo usa la librería ws para la creación de WebSockets. Para instalarla ejecute npm install ws
.
En el arreglo clients
se agregarán todos los clientes que se vayan conectando a la aplicación para notificarlos de los cambios.
En la función setWs
se crea una nueva conexión. Esta función recibe la variable server
definida por express.
Luego, cada vez que alguien se conecta al socket se incluye en el arreglo de clientes.
La función notifyAll
envía una notificación a todos los clientes conectados. En este caso se usará para enviar todos los datos de la colección.
Ahora, vamos a modificar la función listeningForChanges
de lib/mongolib.js
así:
const listeningForChanges = (db, notifyAll) => {
const cursor = db.collection("reactive").watch();
cursor.on("change", (data) => {
getDocuments(db, (docs) => {
console.log("Collection changing...", docs);
notifyAll(docs);
});
});
};
También incluya las siguientes modificaciones en bin/www
:
Importe la librería wslib
.
const Mongolib = require("../lib/mongolib");
const WebSocket = require("../lib/wslib");
Llame la función WebSocket.setWs
pasando como parámetro la variable server
y modifique el llamado a la función listeningForChanges
pasando un nuevo parámetro WebSocket.nofityAll
.
WebSocket.setWs(server);
Mongolib.getDatabase((db, client) => {
Mongolib.listeningForChanges(db, WebSocket.notifyAll);
});
Ahora modifique el archivo front/src/App.js
así:
import React, { Component } from "react";
class App extends Component {
state = {
docs: [],
};
ws = new WebSocket("ws://localhost:3001");
setupWs = () => {
this.ws.onopen = () => {
console.log("WS client connected");
};
this.ws.onmessage = (msg) => {
let docs = JSON.parse(msg.data);
console.log("Get message", docs);
this.setState({ docs });
};
};
componentDidMount() {
this.setupWs();
fetch("/docs")
.then((docs) => docs.json())
.then((docs) => {
this.setState({ docs });
});
}
render() {
return (
<div>
<ul>
{this.state.docs.map((e, i) => (
<li key={i}>{e.name}</li>
))}
</ul>
</div>
);
}
}
export default App;
Vaya al front. Desde la consola de mongo inserte un nuevo documento y observe cómo se actualiza el front.