Let's Build #001: Realtime Messenger App
What is a web socket?
A WebSocket is a communication protocol that provides full-duplex communication channels over a single TCP connection. It enables interactive communication between a client (such as a web browser) and a server, allowing real-time data transfer and bi-directional communication.
Unlike traditional HTTP connections that follow a request-response model, where the client sends a request and the server responds, WebSocket allows both the client and server to send data at any time without the need for a new HTTP request.
WebSockets use a standardized protocol that starts with an HTTP handshake, and once established, the connection is upgraded to the WebSocket protocol. The data is then exchanged in frames, allowing for efficient and structured communication.
WebSockets have become popular for developing real-time web applications that require instant data updates and interactivity. They provide a reliable and efficient means of communication between clients and servers, enabling developers to build dynamic and responsive web applications.
I've been following Socket.IO for a while and wanted to give it a try. So what better way to build a real-time messenger application using our favourite frontend framework, Vue? You can check out the github repository for the code here: https://github.com/mrohner94/lets-build-001-websockets
This project provides the perfect opportunity to work a bit with some popular frontend frameworks such as Vue and Vuetify, as well as some popular backend technologies such as Node.js and Socket.IO
I will be using version 18.13.0 of node for this project. There are many ways that we could go about this project - but for sake of learning this as a fullstack application, we will be treating the frontend and backend of this as two separate applications.
mkdir messenger-app
cd messenger-app
mkdir backend
cd backend
npm init
npm i socket.io
npm i -D @types/node nodemon ts-node typescript
{
"compilerOptions": {
"sourceMap": true,
"outDir": "dist",
"lib": ["esnext"],
"esModuleInterop": true
}
}
import http from "http";
const server = http.createServer();
server.listen(3000, () => {
console.log("listening on *:3000");
});
"scripts": {
"dev": "nodemon src/index.ts"
}
npm run dev
and you will see that your server is listening on port 3000!
npm init vue@latest
cd frontend
npm i uuid socket.io-client Vuetify
npm i -D @types/uuid
export interface AppState {
userId: string;
hex: string;
}
export interface AllMessage {
userId: string;
hex: string;
msg: string;
}
import type { AppState } from "@/types";
import { defineStore } from "pinia";
export const useAppState = defineStore("app", {
state: (): AppState => ({
userId: "",
hex: "",
}),
});
import { createPinia } from "pinia";
export default createPinia();
import "vuetify/styles";
import { createVuetify } from "vuetify";
import * as components from "vuetify/components";
import * as directives from "vuetify/directives";
export default createVuetify({
components,
directives,
});
import vuetify from "./vuetify";
import pinia from "../stores";
import router from "../router";
import type { App } from "vue";
export function registerPlugins(app: App) {
app.use(vuetify).use(router).use(pinia);
}
import { createApp } from "vue";
import App from "./App.vue";
import { registerPlugins } from "@/plugins";
const app = createApp(App);
registerPlugins(app);
app.mount("#app");
<template>
<v-app>
<v-main>
<RouterView />
</v-main>
</v-app>
</template>
import http from "http";
import { Server } from "socket.io";
const server = http.createServer();
const io = new Server(server, {
cors: {
origin: "http://localhost:1234",
methods: ["GET", "POST"],
},
});
io.on("connection", (socket) => {
console.log("a user connected");
socket.on("disconnect", () => {
console.log("user disconnected");
});
});
server.listen(3000, () => {
console.log("listening on *:3000");
});
import { createRouter, createWebHistory } from "vue-router";
import Chat from "../views/Chat.vue";
const router = createRouter({
history: createWebHistory(globalThis._importMeta_.env.BASE_URL),
routes: [
{
path: "/",
name: "chat",
component: Chat,
},
],
});
export default router;
<style scoped>
.chat-window {
height: calc(100vh - 48px);
}
#form {
background: rgba(0, 0, 0, 0.15);
padding: 0.25rem;
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
height: 3rem;
box-sizing: border-box;
backdrop-filter: blur(10px);
}
#input:focus {
outline: none;
}
#form > button {
background: #333;
border: none;
padding: 0 1rem;
margin: 0.25rem;
border-radius: 3px;
outline: none;
color: #fff;
}
</style>
<template>
<v-list id="chat-window" :lines="false" class="pa-0 ma-0 chat-window">
<v-list-item
v-for="message in messages"
:key="uuidv4()"
:style="`background: ${message.hex}`"
prepend-avatar="https://placekitten.com/64/64"
>
<template v-slot:title>
<div
:class="
isColorDark(message.hex)
? 'text-white font-weight-bold'
: 'text-medium-emphasis font-weight-bold'
"
>
&lcub&lcub message.userId &rcub&rcub
</div>
<div :class="isColorDark(message.hex) ? 'text-white' : ''">
&lcub&lcub message.msg &rcub&rcub
</div>
</template>
</v-list-item>
</v-list>
<v-form
id="form"
@submit.prevent=""
class="d-flex align-center justify-space-between"
>
<input
v-model="inputMessage"
type="text"
class="bg-white rounded w-100 h-100 mr-4"
@keydown.enter="onClickSend"
/>
<v-btn @click="onClickSend">Send</v-btn>
</v-form>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { io } from "socket.io-client";
import { onBeforeUnmount } from "vue";
import { useAppState } from "@/stores/app";
import { type AllMessage } from "@/types";
import { v4 as uuidv4 } from "uuid";
const $state = useAppState();
const socket = io("http://localhost:3000");
const messages = ref<AllMessage[]>([]);
const inputMessage = ref("");
const scrollToBottom = (elementId: string) => {
var element = document.getElementById(elementId);
if (element) {
element.scrollTop = element.scrollHeight;
}
};
const isColorDark = (hexColor: string) => {
if (hexColor.startsWith("#")) {
hexColor = hexColor.slice(1);
}
const red = parseInt(hexColor.slice(0, 2), 16);
const green = parseInt(hexColor.slice(2, 4), 16);
const blue = parseInt(hexColor.slice(4, 6), 16);
const relativeLuminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
const threshold = 128;
return relativeLuminance < threshold;
};
const onClickSend = () => {
if (inputMessage.value) {
const payload: AllMessage = {
userId: $state.userId,
hex: $state.hex,
msg: inputMessage.value,
};
socket.emit("message all", payload);
inputMessage.value = "";
}
};
socket.on("message all", async (payload: AllMessage) => {
await messages.value.push(payload);
scrollToBottom("chat-window");
});
onBeforeUnmount(() => {
socket.disconnect();
});
</script>
<script setup lang="ts">
import { RouterView } from "vue-router";
import { v4 as uuidv4 } from "uuid";
import { useAppState } from "@/stores/app";
const $state = useAppState();
const generateRandomHexColor = () => {
var randomColor = Math.floor(Math.random() * 16777215).toString(16);
while (randomColor.length < 6) {
randomColor = "0" + randomColor;
}
randomColor = "#" + randomColor;
return randomColor;
};
//If you want to have separate tabs open as "different users"
//Then you can't reference localStorage
// let id = localStorage.getItem("userId");
let id = undefined;
// Same here
// let hex = localStorage.getItem("hex");
let hex = undefined;
if (!id) {
id = uuidv4();
localStorage.setItem("userId", id);
}
if (!hex) {
hex = generateRandomHexColor();
localStorage.setItem("hex", hex);
}
$state.userId = id;
$state.hex = hex;
</script>
export interface AllMessage {
userId: string;
hex: string;
msg: string;
}
import http from "http";
import { Server } from "socket.io";
import { AllMessage } from "./types";
const server = http.createServer();
const io = new Server(server, {
cors: {
origin: "http://localhost:1234",
methods: ["GET", "POST"],
},
});
io.on("connection", (socket) => {
console.log("a user connected");
socket.on("disconnect", () => {
console.log("user disconnected");
});
socket.on("message all", (payload: AllMessage) => {
console.log("payload: ", payload);
io.emit("message all", payload);
});
});
server.listen(3000, () => {
console.log("listening on *:3000");
});
npm run dev
- then you should be able to open it up in two separate tabs and communicate between clients!