visit
In this article, we are going to create a chat application that connects people, anonymously, to different rooms together in pairs of two. The chat application would make use of for the server-side code, listen to web socket communication using and the client-side will be developed with vanilla JavaScript.
chat-app
and change the directory to the directory using the command.$ mkdir chat-app && cd chat-app
$ yarn init -y
$ yarn add express
8001
.// app.js
const http = require("http")
const express = require("express")
const app = express()
app.get("/index", (req, res) => {
res.send("Welcome home")
})
const server = http.createServer(app)
server.on("error", (err) => {
console.log("Error opening server")
})
server.listen(8001, () => {
console.log("Server working on port 8001")
})
$ node app.js
You can visit [//localhost:8001/index](//localhost:8001/index)
on your browser to test that the application works
$ yarn add socket.io
// app.js
const http = require("http");
const { Server } = require("socket.io");
const express = require("express");
const app = express();
app.get("/index", (req, res) => {
res.send("Welcome home");
});
const server = http.createServer(app);
const io = new Server(server);
io.on("connection", (socket) => {
console.log("connected");
});
server.on("error", (err) => {
console.log("Error opening server");
});
server.listen(8001, () => {
console.log("Server working on port 3000");
});
We’d create a public directory including files to build up our UI, making our project structure look like this.
chat-app/
|- node_modules/
|- public/
|- index.html
|- main.js
|- app.js
|- package.json
|- yarn.lock
We are going to be making use of to style the Client UI to reduce the amount of custom CSS we’d be writing.
In the index.html
, create a template for our chat window.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="//cdn.tailwindcss.com"></script>
<title>Anon Chat App</title>
</head>
<body>
<div class="flex-1 p:2 sm:p-6 justify-between flex flex-col h-screen">
<div id="messages" class="flex flex-col space-y-4 p-3 overflow-y-auto scrollbar-thumb-blue scrollbar-thumb-rounded scrollbar-track-blue-lighter scrollbar-w-2 scrolling-touch">
</div>
<div class="border-t-2 border-gray-200 px-4 pt-4 mb-2 sm:mb-0">
<div class="relative flex">
<input type="text" placeholder="Write your message!" class="w-full focus:outline-none focus:placeholder-gray-400 text-gray-600 placeholder-gray-600 pl-12 bg-gray-200 rounded-md py-3">
<div class="absolute right-0 items-center inset-y-0 hidden sm:flex">
<button type="button" class="inline-flex items-center justify-center rounded-lg px-4 py-3 transition duration-500 ease-in-out text-white bg-blue-500 hover:bg-blue-400 focus:outline-none">
<span class="font-bold">Send</span>
<svg xmlns="//www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-6 w-6 ml-2 transform rotate-90">
<path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="./main.js"></script>
</body>
</html>
In the HTML file above we included two JavaScript files, the first one to initialize on the client side and another main.js
file to write our custom JavaScript code.
Then in the main.js
file, we would create a function that can add a message to the chatbox. The function createMessage
will expect two arguments. The first argument is the message string and the second argument is a boolean to determine if the message is from the user or
// main.js
const messageBox = document.querySelector("#messages");
function createMessage(text, ownMessage = false) {
const messageElement = document.createElement("div");
messageElement.className = "chat-message";
const subMesssageElement = document.createElement("div");
subMesssageElement.className =
"px-4 py-4 rounded-lg inline-block rounded-bl-none bg-gray-300 text-gray-600";
if (ownMessage) {
subMesssageElement.className += " float-right bg-blue-800 text-white";
}
subMesssageElement.innerText = text;
messageElement.appendChild(subMesssageElement);
messageBox.appendChild(messageElement);
}
createMessage("Welcome to vahalla");
createMessage("Who are you to talk to me", true);
Change the code in the server application, app.js
and make use of static files to render the client UI.
// app.js
const http = require("http");
const { Server } = require("socket.io");
const express = require("express");
const path = require("path");
const app = express();
app.use(express.static(path.join(__dirname, "public")));
const server = http.createServer(app);
const io = new Server(server);
io.on("connection", (socket) => {
console.log("connected");
});
server.on("error", (err) => {
console.log("Error opening server");
});
server.listen(8001, () => {
console.log("Server working on port 8001");
});
NOTE: To view the changes made in our application, we have to stop the running server application and re-run it for the new changes to take effect. So we are making use of
nodemon
to automate this process for us.
Install nodemon by running.
$ npm install -g nodemon
Then run the node application using nodemon.
$ nodemon ./app.js
Open [//localhost:8001](//localhost:3000)
on your browser to view what the chat app would look like.
To keep track of the rooms created and the number of users connected to each room, we will create a Room
class to manage this data for us.
We’d create a new file named room.js
in the root directory of our project. Then we create the Room
class and have the constructor initialize a property for keeping the state of our room.
// room.js
// the maximum number of people allowed in each room
const ROOM_MAX_CAPACITY = 2;
class Room {
constructor() {
this.roomsState = [];
}
}
module.exports = Room;
The roomsState
is an array of objects that keeps the information about each room ID created and the number of users in that room. So a typical roomsState
would look like this.
// rooms state
[
{
roomID: "some id",
users: 1
},
{
roomID: "a different id",
users: 2
}
]
To generate a unique id, we would be making use of a package known as UUID in our application.
Install uuid by running this command in our terminal.
$ yarn add uuid
Then import the package into our application by running as follows.
// room.js
const { v4: uuidv4 } = require("uuid");
class Room {
constructor() {
/**/
}
joinRoom() {
return new Promise((resolve) => {
for (let i = 0; i < this.roomsState.length; i++) {
if (this.roomsState[i].users < ROOM_MAX_CAPACITY) {
this.roomsState[i].users++;
return resolve(this.roomsState[i].id);
}
}
// else generate a new room id
const newID = uuidv4();
this.roomsState.push({
id: newID,
users: 1,
});
return resolve(newID);
});
}
}
module.exports = Room;
NOTE: Making use of an array to manage the rooms' state is, obviously, not the best way to do so. Imagine having thousands of rooms in your application and you have to loop through each room for each join request. It would execute at O(n). For the purpose of this tutorial, we will stick to this approach.
We’d add another method to the Room
class, leaveRoom()
, to reduce the number of users in a particular room.
// room.js
class Room {
constructor() {
/**/
}
joinRoom() {}
leaveRoom(id) {
this.roomsState = this.roomsState.filter((room) => {
if (room.id === id) {
if (room.users === 1) {
return false;
} else {
room.users--;
}
}
return true;
});
}
}
module.exports = Room;
The leaveRoom()
the method takes a room ID, and loops through the array of rooms to find if any of the rooms match the ID provided in the argument.
If it finds the matching room, it checks if the user in the room is one to delete that particular room state. If the user in the room is greater than 1, the leaveRoom()
the method just deducts the number of users in that room by one.
Finally, our room.js
code should be similar to this.
// room.js
const { v4: uuidv4 } = require("uuid");
// the maximum number of people allowed in a room
const ROOM_MAX_CAPACITY = 2;
class Room {
constructor() {
this.roomsState = [];
}
joinRoom() {
return new Promise((resolve) => {
for (let i = 0; i < this.roomsState.length; i++) {
if (this.roomsState[i].users < ROOM_MAX_CAPACITY) {
this.roomsState[i].users++;
return resolve(this.roomsState[i].id);
}
}
const newID = uuidv4();
this.roomsState.push({
id: newID,
users: 1,
});
return resolve(newID);
});
}
leaveRoom(id) {
this.roomsState = this.roomsState.filter((room) => {
if (room.id === id) {
if (room.users === 1) {
return false;
} else {
room.users--;
}
}
return true;
});
}
}
module.exports = Room;
allows us to create arbitrary channels that sockets can join and leave. It can be used to broadcast events to a subset of clients.
(source: )
To join a room, we would join a room with a unique room ID.io.on("connection", socket => {
// join a room
socket.join("some room id");
socket.to("some room id").emit("some event");
});
In our server application, once new users join the connection, the Room.joinRoom()
returns a unique ID which is our unique room ID. So we can join and leave room in our rooms as follow.
// app.js
io.on("connection", async (socket) => {
const roomID = await room.joinRoom();
// join room
socket.join(roomID);
socket.on("disconnect", () => {
// leave room
room.leaveRoom(roomID);
});
});
// main.js
socket.on("receive-message", (message) => {
createMessage(message);
});
sendButton.addEventListener("click", () => {
if (textBox.value != "") {
socket.emit("send-message", textBox.value);
createMessage(textBox.value, true);
textBox.value = "";
}
});
NOTE: In our chat application, we directly add the message from user to the chatbox without confirming if the message is received by the socket server. This is not usually the case.
// app.js
io.on("connection", async (socket) => {
const roomID = await room.joinRoom();
// join room
socket.join(roomID);
socket.on("send-message", (message) => {
socket.to(roomID).emit("receive-message", message);
});
socket.on("disconnect", () => {
// leave room
room.leaveRoom(roomID);
});
});
// app.js
const http = require("http");
const { Server } = require("socket.io");
const express = require("express");
const path = require("path");
const Room = require("./room");
const app = express();
app.use(express.static(path.join(__dirname, "public")));
const server = http.createServer(app);
const io = new Server(server);
const room = new Room();
io.on("connection", async (socket) => {
const roomID = await room.joinRoom();
// join room
socket.join(roomID);
socket.on("send-message", (message) => {
socket.to(roomID).emit("receive-message", message);
});
socket.on("disconnect", () => {
// leave room
room.leaveRoom(roomID);
});
});
server.on("error", (err) => {
console.log("Error opening server");
});
server.listen(8001, () => {
console.log("Server working on port 8001");
});
// main.js
const messageBox = document.querySelector("#messages");
const textBox = document.querySelector("input");
const sendButton = document.querySelector("button");
function createMessage(text, ownMessage = false) {
const messageElement = document.createElement("div");
messageElement.className = "chat-message";
const subMesssageElement = document.createElement("div");
subMesssageElement.className =
"px-4 py-4 rounded-lg inline-block rounded-bl-none bg-gray-300 text-gray-600";
if (ownMessage) {
subMesssageElement.className += " float-right bg-blue-800 text-white";
}
subMesssageElement.innerText = text;
messageElement.appendChild(subMesssageElement);
messageBox.appendChild(messageElement);
}
const socket = io();
socket.on("connection", (socket) => {
console.log(socket.id);
});
socket.on("receive-message", (message) => {
createMessage(message);
});
sendButton.addEventListener("click", () => {
if (textBox.value != "") {
socket.emit("send-message", textBox.value);
createMessage(textBox.value, true);
textBox.value = "";
}
});
To text our chat app, we will open four different browsers to confirm that two rooms are created.
Also Published