13

Usando socket.io en aplicaciones de Next.js

21837Puntos

hace 7 años

Next.js nos permite crear aplicaciones de React fácilmente y socket.io nos permite crear aplicaciones en tiempo real fácilmente ¿Qué mejor que combinarlos para crear aplicaciones de React en tiempo real fácilmente?

En este artículo vamos a ver cómo combinarlos para crear una aplicación que use ambas tecnologías para crear una app de chat simple, que muestre los mensajes antiguos al entrar a la página y luego mediante socket.io nos permita enviar y recibir mensajes.

Iniciando el proyecto

Lo primero, como siempre, es iniciar el proyecto.

npm init --yes
# o con yarn
yarn init --yes

Luego vamos a instalar nuestras dependencias:

npm i next react react-dom express socket.io socket.io-client isomorphic-fetch
# o con yarn
yarn add next react react-dom express socket.io socket.io-client isomorphic-fetch

Y vamos a agregar estos scripts a nuestro package.json:

{
	...
	"scripts": {
		"dev": "next",
		"build": "next build",
		"start": "next start"
	}
	...
}

Creando el servidor de API y Sockets

Vamos a crear un simple API Rest con un endpoint y un servidor de sockets. Este endpoints del API nos van a permitir pedir nuestros mensajes viejos. La parte de sockets nos permite enterarnos cuando un usuario crea un mensaje y crear mensajes.

Por simplicidad del ejemplo, vamos a meter todo en un solo servidor en el que también vamos a correr Next.js. En un proyecto real deberíamos tener un servidor para el API Rest, un servidor para sockets y un servidor para Next.js, de forma que puedan escalar independientemente.

// cargamos express e iniciamos una aplicaciónconst app = require('express')()
// creamos un servidor HTTP desde de nuestra aplicación de Expressconst server = require('http').Server(app)
// creamos una aplicación de socket.io desde nuestro servidor HTTPconst io = require('socket.io')(server)
// cargamos Next.jsconst next = require('next')

// verificamos si estamos corriendo en desarrollo o producciónconst dev = process.env.NODE_ENV !== 'production'// iniciamos nuestra aplicación de Next.jsconst nextApp = next({ dev })
// obtenemos el manejador de Next.jsconst nextHandler = nextApp.getRequestHandler()

// este array va a ser nuestra base de datos// no es una base de datos de verdad, pero para el ejemplo nos sirveconst messages = []

// cuando un usuario se conecte al servidor de sockets
io.on('connection', socket => {
	// escuchamos el evento `message`
	socket.on('message', (data) => {
		// guardamos el mensaje en nuestra "DB"
		messages.push(data)
		// enviamos el mensaje a todos los usuarios menos a quién los envió
		socket.broadcast.emit('message', data)
	})
})

// iniciamos nuestra aplicación de Next.js
nextApp.prepare().then(() => {
	// definimos una URL para obtener los mensajes
	app.get('/messages', (req, res) => {
		// y respondemos con la lista de mensajes serializada como JSON
		res.json(messages)
	})

	// para cualquier otra ruta de la aplicación
	app.get('*', (req, res) => {
		// dejamos que el manejador de Next se encargue y responda con el HTML o un 404return nextHandler(req, res)
	})

	// iniciamos el servidor HTTP en el puerto 3000
	server.listen(3000, (err) => {
		// si ocurre un error matamos el procesoif (err) process.exit(0)
		// si todo está bien dejamos un log en consolaconsole.log('> Ready on http://localhost:3000')
	})
})

Ese es nuestro servidor, como vemos usamos un array en memoria para guardar los mensajes por simplicidad del ejemplo, en la vida real deberíamos tener una base de datos (laquesea) y guardar los mensajes ahí.

También tenemos un muy simple servidor de sockets y una URL donde pedir los mensajes guardados en el servidor.

Ahora vamos a hacer el frontend. Para eso creamos una página de Next.js en pages/index.js. Esta página va a pedir los mensajes viejos en el método getInitialProps y luego cuando se renderice en el navegador va a conectarse al servidor de sockets para enviar y recibir nuevos mensajes.

// importamos Component de Reactimport { Component } from'react'// importamos el client de socket.ioimport io from'socket.io-client'// importamos fetchimport fetch from'isomorphic-fetch'classHomePageextendsComponent{
	// acá pedimos los datos de los mensajes viejos, esto se ejecuta tanto en el cliente como en el servidorstaticasync getInitialProps ({ req }) {
		const response = await fetch('http://localhost:3000/messages')
		const messages = await response.json()
		return { messages }
	}

	static defaultProps = {
		messages: []
	}

	// en el estado guardamos un string vacío (el campo del formulario) y los mensajes que recibimos del API
	state = {
		field: '',
		messages: this.props.messages
	}

	// una vez que el componente se montó en el navegador nos conectamos al servidor de sockets// y empezamos a recibimos el evento `message` del servidor
	componentDidMount () {
		this.socket = io('http://localhost:3000/')
		this.socket.on('message', this.handleMessage)
	}

	// cuando el componente se va a desmontar es importante que dejemos de escuchar el evento// y que cerremos la conexión por sockets, esto es para evitar problemas de que lleguen mensajes
	componentWillUnmount () {
		this.socket.off('message', this.handleMessage)
		this.socket.close()
	}

	// cuando llega un mensaje del servidor lo agregamos al estado de nuestra página
	handleMessage = (message) => {
		this.setState(state => ({ messages: state.messages.concat(message) }))
	}

	// cuando el valor del input cambia actualizamos el estado de nuestra página
	handleChange = event => {
		this.setState({ field: event.target.value })
	}

	// cuando se envía el formulario enviamos el mensaje al servidor
	handleSubmit = event => {
		event.preventDefault()

		// creamos un objeto message con la fecha actual como ID y el valor del inputconst message = {
			id: (newDate()).getTime(),
			value: this.state.field
		}

		// enviamos el objeto por socket al servidorthis.socket.emit('message', message)

		// lo agregamos a nuestro estado para que se muestre en pantalla y limpiamos el inputthis.setState(state => ({
			field: '',
			messages: state.messages.concat(message)
		}))
	}

	render () {
		return (
			<main><div><ul>
						{/* acá renderizamos cada mensaje */}
						{this.state.messages.map(message =>
							<likey={message.id}>{message.value}</<span class="hljs-name">li>
						)}
					</<span class="hljs-name">ul>
					{/* nuestro formulario *}
					<formonSubmit={this.handleSubmit}><inputonChange={this.handleChange}type='text'placeholder='Hola Platzi!'value={this.state.field}
						/><button>Enviar</<span class="hljs-name">button>
					</<span class="hljs-name">form>
				</<span class="hljs-name">div>
			</<span class="hljs-name">main>
		)
	}
}

export default HomePage

Con eso ya tenemos nuestra aplicación hecha. Es importante ver que la conexión a socket solo la hacemos en el cliente, por eso lo hacemos en componentDidMount y no en componentWillMount. Si lo hiciéramos en WillMount o en getInitialProps como se ejecuta en el servidor, entonces tendríamos el problema que se inicia una conexión pero nunca se cierra, lo que puede causar problema de quedarnos sin memoria en nuestro servidor.

Pueden ver este mismo ejemplo funcionando en => https://next-socket-io.now.sh/

Conclusiones

Combinar socket.io y Next.js es muy fácil: hacer la petición HTTP para el server render nos ayuda a mejorar la experiencia de carga de la página, ya que podemos mostrar mensajes desde el primer momento incluso si luego la conexión por socket no funciona.

Se pueden hacer validaciones más complejas verificando si estamos de verdad conectados al servidor de socket y ahí deshabilitar o habilitar el formulario para que no se pueda usar si no estamos conectados, por ejemplo en el server render.

Sergio Daniel
Sergio Daniel
sergiodxa

21837Puntos

hace 7 años

Todas sus entradas
Escribe tu comentario
+ 2