go to part...
Writing New Messages
You've probably been waiting for this. Yes, we're finally going to let our users to write messages to the chat. We'll stick with the familiar workflow here. First, create a new component for the message input at resources/assets/js/components/VueChatNewMessage.vue
:
<template>
<div class="message-input-wrapper">
<input type="text"
class="input"
placeholder="New message"
v-model="message"
@keyup.enter.prevent="sendMessage">
</div>
</template>
<script>
export default {
props: ['activeChannel', 'username'],
data() {
return {
message: '',
};
},
methods: {
sendMessage() {
let endpoint = `/channels/${this.activeChannel}/messages`;
let data = {
username: this.username,
message: this.message
};
axios.post(endpoint, data);
this.message = '';
}
}
}
</script>
<style>
.message-input-wrapper {
border-top: 2px dotted greenyellow;
}
</style>
It consists of a single text input bound to the message
data attribute. When a user presses enter while the input field is focused - the sendMessage
method gets triggered. Inside the method we're just preparing the data and sending it to the backend, clearing the text input afterwards.
Now, as usually, replace the div with the new message input
text inside resources/assets/js/components/VueChat.vue
with the new component:
<vue-chat-new-message :active-channel="activeChannel"
:username="username"></vue-chat-new-message>
If you've noticed we're passing down not only the active channel id, but also the current user's username. The username is generated randomly on each page refresh. Add this to the list of data properties:
username: 'username_' + Math.random().toString(36).substring(7),
Finally, register the component inside resources/assets/js/app.js
:
Vue.component('vue-chat-new-message', require('./components/VueChatNewMessage.vue'));
The only thing that's left is to add a new endpoint to the routes/web.php
file which will receive new messages and permanently store them to the database.
use App\Message;
...
Route::post('/channels/{channel}/messages', function (App\Channel $channel) {
$message = Message::forceCreate([
'channel_id' => $channel->id,
'author_username' => request('username'),
'message' => request('message'),
]);
return $message;
});
At this point, in order to see the new messages appear in the chat you have to either refresh the page or switch the channel to another one and then back.
Broadcasting an Event on New Message
Here is where the interesting stuff begins. We're going to start building the communication chain as described at the top of part one of this tutorial. As we receive a new message from a user, we're going to not just store it in the database, but also send it to Redis.
Redis "is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker." You are supposed to have Redis installed on your server by now. In order for Laravel to be able to work with Redis we need to install a package called predis
via composer:
composer require predis/predis
Now, what we are going to do is simply emit a Laravel event whenever the server receives a new message. Laravel have mechanisms to automatically broadcast the events and Redis is one of the supported broadcast drivers. Let's go ahead and set the broadcast driver to Redis in our .env
file.
BROADCAST_DRIVER=redis
Now create a new Laravel event:
php artisan make:event MessageSent
Open the file. The class should implement the ShouldBroadcast
interface in order to be able to broadcast the event. The constructor is going to accept a name of a channel to broadcast on (in our case these names will correspond to the chat channels' names) and some data which we'll need to display the chat messages real-time. The broadcasting is performed on public (as opposed to private) channels. Below is the full code of the file. Note that all the class' public properties will be automatically appended to the broadcast message (in our case it's $data
).
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class MessageSent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
protected $channel;
public $data;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($channel, $data)
{
$this->channel = $channel;
$this->data = $data;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new Channel($this->channel);
}
}
The only thing left is to dispatch the event whenever the Laravel server receives a new message. Open routes/web.php
and alter the route we added in the last part like this:
use App\Events\MessageSent;
...
Route::post('/channels/{channel}/messages', function (App\Channel $channel) {
$message = Message::forceCreate([
'channel_id' => $channel->id,
'author_username' => request('username'),
'message' => request('message'),
]);
// Dispatch an event. Will be broadcasted over Redis.
event(new MessageSent($channel->name, $message));
return $message;
});
Node.js Server
Now we need to setup a Node.js server. You should have Node
installed on your system before you continue. Node.js will listen for messages from Redis
and then pass them further to Socket.io
. Frontend (browser) clients connected to Socket.io
will receive those messages.
You need to create a socket.js
file in the root of your laravel project (at the same level with .env
). Below is the content of the file. I did leave some comments for those who've never worked with Node.js before.
// A Node.js server
var server = require('http').Server();
// Requiring the ioredis package
var Redis = require('ioredis');
// A redis client
var redis = new Redis();
// Subscribe to all channels which name complies with the '*' pattern
// '*' means we'll subscribe to ALL possible channels
redis.psubscribe('*');
// Listen for new messages
redis.on('pmessage', function (pattern, channel, message) {
message = JSON.parse(message);
// Just to check that things really work
console.log(message);
});
// Start the server at http://localhost:3000
server.listen(3000);
// Just to be sure it's working
console.log('Server started');
Then install ioredis
. This is just one of the packages out there allowing Node.js to work with Redis.
npm install ioredis
After that you can start the Node.js server by running the command below.
node socket.js
You should see the Server started
text in your console. Please remember that you'll have to restart the server each time you change socket.js
. To kill the server simply press Ctrl+C
in your terminal. Now, you can go to your website, leave a new message and you should see it in the Node's terminal. This means everything works as we intended it to so far.
Displaying New Messages Real-Time with Socket.io
Socket.io
will be responsible for connecting our end users (or our frontend) with the Node.js server. Naturally, it has a server and a client version. Let's install both:
npm install socket.io socket.io-client
This is what socket.js
should look like now (I added comments to the new lines):
var server = require('http').Server();
var Redis = require('ioredis');
var redis = new Redis();
// Create a new Socket.io instance
var io = require('socket.io')(server);
redis.psubscribe('*');
redis.on('pmessage', function (pattern, channel, message) {
message = JSON.parse(message);
// Pass data to Socket.io every time we get a new message from Redis
// "channel + ':' + message.event" is a unique channel id to broadcast to
//
// message.data corresponds to the $data variable in the MessageSent event
// in Laravel
io.emit(channel + ':' + message.event, message.data);
});
server.listen(3000);
Now to the client side. First we need to import Socket.io
in resources/assets/js/bootstrap.js
:
import io from 'socket.io-client';
...
window.io = io;
We're going to connect to Socket.io
inside resources/assets/js/components/VueChat.vue
and listen for messages. We're going to connect to all the channels at the same time but only append new messages if the currently active channel is the same as the new message's channel. Might be not the best solution, but it is an easier one. It will also keep the client connected to all the chat channels at the same time. Edit the created()
method of VueChat.vue
component like this:
created() {
this.fetchMessages();
// Connect to Socket.io
let socket = io(`http://localhost:3000`);
// For each channel...
for (let channel of this.channels) {
// ... listen for new events/messages
socket.on(`${channel.name}:App\\Events\\MessageSent`, data => {
if (this.activeChannel == channel.id) {
this.messages.push(data.data);
}
});
}
},
The real-time aspect should work now. To test it open your website in two different browser tabs or windows and try chatting with yourself.
You've probably thought that it would be nice if the chat window would automatically scroll to the bottom at page load and when a new message is received. Let's make this happen by editing resources/assets/js/components/VueChatMessages.vue
. First, add a reference name to the topmost div in the template:
<div class="column" ref="message-window">
Then we're going to create a method called scrollToBottom()
and call it after the Vue component mounts and every time the array of messages changes.
mounted() {
this.scrollToBottom();
},
watch: {
messages() {
this.scrollToBottom();
}
},
methods: {
scrollToBottom() {
this.$nextTick(() => {
this.$refs['message-window'].scrollTop = this.$refs['message-window'].scrollHeight;
});
}
},
Go ahead and try the chat again.
Notifications when Someone Enters or Leaves the Chat
Let's display system notifications whenever someone enters or leaves the chat. First open resources/assets/js/components/VueChat.vue
. We need to change the way we instantiate a socket connection from the client side inside the created()
method. This is what it should look like now:
this.socket = io(`http://localhost:3000?username=${this.username}`);
We're passing a query parameter with the username to the Node.js server. Now open socket.js
. Socket.io lets us listen for when someone connects to or disconnects from the server. We can read the username
query parameter upon connection and store it for later until the user disconnects. We're going to emit additional events to the clients on both connection and disconnection. At this block near the bottom of the file:
io.on('connect', function (socket) {
var username = socket.handshake.query.username;
io.emit('user-joined', { username: username });
socket.on('disconnect', function (socket) {
io.emit('user-left', { username: username });
});
});
server.listen(3000);
The events with the required data are being emitted, now we need to react to them on the client side. Go back to VueChat.vue
. This is what the whole created()
method should look like now:
created() {
this.fetchMessages();
this.socket = io(`http://localhost:3000?username=${this.username}`);
for (let channel of this.channels) {
this.socket.on(`${channel.name}:App\\Events\\MessageSent`, data => {
this.messages.push(data.data);
});
}
// Push a new "virtual" message to the messages array after someone has
// entered the chat. "virtual" means this message won't be persisted in the
// database and will be only shown once
this.socket.on(`user-joined`, data => {
this.messages.push({
message: `${data.username} has joined the chat.`,
author_username: 'system',
});
});
// Same thing for after someone has disconnected from the chat
this.socket.on(`user-left`, data => {
this.messages.push({
message: `${data.username} has left the chat.`,
author_username: 'system',
});
});
},
Everything should work now. Open your app in a few tabs or windows and see the results for yourself.
Displaying the List of Active Participants
Lastly, it would be nice to see who's currently online. We'll start with the socket.js
file. We're simply going to create a global array of participants. In the event of a user's connection or disconnection we're going to add or remove their username to/from the array, as well as attach the array to the corresponding events we emit. Supposedly, all usernames are unique at any given moment.
var participants = [];
io.on('connect', function (socket) {
var username = socket.handshake.query.username;
// Push the user to the array
participants.push(username);
// The "participants" array is now included in the message
io.emit('user-joined', { username: username, participants: participants });
socket.on('disconnect', function (socket) {
// Remove the user to the array
participants.splice(participants.indexOf(username), 1);
// The "participants" array is now included in the message
io.emit('user-left', { username: username, participants: participants });
});
});
Now to the resources/assets/js/components/VueChat.vue
component. First, add a new data property to store the list of participants currently online on the client side:
participants: [],
Then alter the user-joined
and user-left
events like shown below. What we are doing is accepting the list of participants broadcasted by Socket.io.
this.socket.on(`user-joined`, data => {
this.participants = data.participants;
this.messages.push({
...
...
this.socket.on(`user-left`, data => {
this.participants = data.participants;
this.messages.push({
...
And finally, as always, we're going to replace the div with the participants
text with a separate Vue component. Obviously, the component will accept the array of participants via props.
<vue-chat-participants :participants="participants"></vue-chat-participants>
The component itself will be located at resources/assets/js/components/VueChatParticipants.vue
and will consist of a simple For loop outputting the list of currently active usernames.
<template>
<div class="column is-2">
<p v-for="(username, index) in participants"
:key="index"
v-text="username"></p>
</div>
</template>
<script>
export default {
props: ['participants']
}
</script>
And, of course, register the component in resources/assets/js/app.js
:
Vue.component('vue-chat-participants', require('./components/VueChatParticipants.vue'));
Now go ahead and play around with your brand new chat app :) Don't forget to (re-)start the node server.
Final Words
Our simple IRC-like real-time chat is done! This is a simplified version of a real-world chat. The purpose of this tutorial is to demonstrate the basics of Node.js and Socket.io, and to show how to connect it with a Laravel or some other PHP backend. This should be a good starting point if you want to explore the field further.
All the materials at voerro are absolutely free and are worked on in the author's spare time. If you found any of the tutorials helpful, please consider supporting the project. Thank you!