go to part...
Prepare Your Server
To implement the code from this tutorial you'll need to install Redis, node.js, and npm to your machine. I'm not going to explain it here, this is fairly simple, just google for instruction for your OS.
Understanding the Architecture
The whole system will consist of the following parts:
- A traditional web server running a Laravel 5 project. The project will have the chat app's backend and frontend.
- A Node.js server, which will handle the chat message transportation
- A Redis server, which will be used to pass messages from Laravel's backend to the Node.js server
- Socket.io JavaScript library, which will allow real-time, bi-directional communication between the Node.js server and Laravel's frontend
Don't worry if you don't know how to work with those or the whole thing seems confusing. That's what this lesson is primarily for. I do expect you to be familiar with Laravel and Vue.js, though (or at the very least with Laravel alone).
Creating the Project
We'll start with a traditional non-real-time Laravel app and we'll make it real-time later on. Bear with me as the most interesting part will be at the end, since I wanted to make a full working app and not just demonstrate the "real-time app with sockets" part. So go ahead and create a new Laravel project.
laravel new irc-chat
Then, create a new database and enter the required credentials into the .env
file.
Migrations and Seeds
We'll have 2 tables in the database - one to store the list of available channels and one to store messages in those channels. We won't have actual users, so the chat will be anonymous. Each user will simply get a random username upon entering the chat. Anyway, run the commands below to create 2 models and 2 corresponding migration files.
php artisan make:model Channel -m
php artisan make:model Message -m
Now let's add all the required fields to the migrations. First the channels
table:
Schema::create('channels', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->unique();
$table->timestamps();
});
And then the messages
table:
Schema::create('messages', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('channel_id');
$table->string('author_username');
$table->text('message');
$table->timestamps();
});
Let's also create a seeder to seed a few random channels.
php artisan make:seed ChannelsSeeder
Now open ChannelsSeeder
and edit it like this:
<?php
use Illuminate\Database\Seeder;
use App\Channel;
class ChannelsSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Channel::truncate();
$channels = [
'Web Development',
'Android Development',
'iOS Development',
'AI',
];
foreach ($channels as $name) {
Channel::forceCreate(['name' => $name]);
}
}
}
Finally, register the seeder in database/seeds/DatabaseSeeder.php
:
public function run()
{
$this->call(ChannelsSeeder::class);
}
Now you can run the migrations with the seeder:
php artisan migrate --seed
Fixing Frontend Assets
First, let's remove all the unnecessary dependencies from the devDependencies
section of the package.json
file:
"devDependencies": {
"axios": "^0.18",
"cross-env": "^5.1",
"laravel-mix": "^2.0",
"vue": "^2.5.7"
}
Save the file and run npm install
. You've probably noticed that I've also removed Bootstrap. Let's go ahead and install Bulma instead.
npm install bulma --save-dev
Now we should clean the asset files up. First open resources/assets/js/bootstrap.js
and make it look like this:
import axios from 'axios';
import Vue from 'vue';
window.axios = axios;
window.Vue = Vue;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
Note how we've also switched from the require()
syntax to ES6's import
. Next edit app.js
from the same folder:
require('./bootstrap');
const app = new Vue({
el: '#app'
});
Now to the resources/assets/sass
folder. Delete the _variables.scss
file and edit the app.scss
file like this:
// Bulma
@import '~bulma/bulma';
Main Layout
Don't think about it too much, just copy and paste everything. This is the basic layout for our chat app.
First, edit the welcome.blade.php
view file:
<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>IRC-like Chat Tutorial from Voerro</title>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<div id="app">
<vue-chat></vue-chat>
</div>
<script src="/js/app.js"></script>
</body>
</html>
As you can see, we're delegating all the important work to the new vue-chat
component. Let's import that component in resources/assets/js/app.js
.
require('./bootstrap');
// All the Vue component imports will go here
Vue.component('vue-chat', require('./components/VueChat.vue'));
const app = new Vue({
el: '#app'
});
Then create the component at resources/assets/js/components/VueChat.vue
. And here's the component's code:
<template>
<div class="chat">
<div class="columns main-wrapper">
<div class="column is-2">
channels
</div>
<div class="column">
messages
</div>
<div class="column is-2">
participants
</div>
</div>
<div class="message-input-wrapper">
new message input
</div>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
Finally, we need some styling. Create a new file at resources/assets/sass/style.scss
:
html, body {
overflow: hidden;
background: #000;
color: #fff;
font-weight: 500;
height: 100%;
}
#app {
height: 100%;
}
.chat {
height: 100%;
max-height: 100%;
display: flex;
flex-direction: column;
}
.main-wrapper {
flex: 1;
overflow: hidden;
margin: 0 !important;
}
.message-input-wrapper {
margin: 0;
display: flex;
align-items: center;
padding: .5rem;
}
Don't forget to import it in resources/assets/sass/app.scss
:
@import 'style.scss';
To see the results you need to build your assets. Either run npm run dev
to build once or boot up a watcher by running npm run watch
that will re-build everything on every file change.
Displaying the List of Channels
The main layout is ready and we can begin displaying the real data. Let's start with a list of channels. First, we need to pass channels from our backend to the main view. Open routes/web.php
. We'll just put all the code in the routes file to keep things simple as this tutorial is not about the backend per se. In real life you'd use controllers instead.
<?php
use App\Channel;
Route::get('/', function () {
$channels = Channel::orderBy('name')->get();
return view('welcome', compact('channels'));
});
Let's pass this data to our main Vue component inside welcome.blade.php
:
<vue-chat :channels="{{ $channels }}"></vue-chat>
We've made a bunch of placeholders in VueChat.vue
. One by one we'll replace them with individual Vue components. And as you've figured out, we'll start with the channels. Open VueChat.vue
and replace the div with the channels
text in the template section with this:
<vue-chat-channels :channels="channels"
:active-channel="activeChannel"></vue-chat-channels>
Then edit the script section like this:
export default {
props: ['channels'],
data() {
return {
activeChannel: this.channels[0].id,
};
},
}
Here we're accepting a prop with the list of channels and creating a data attribute to store the id
of the currently active (open) channel. By default it'll always be the first channel on the list.
Let's import the channels Vue component in resources/assets/js/app.js
:
Vue.component('vue-chat-channels', require('./components/VueChatChannels.vue'));
And here's the VueChatChannels.vue
itself:
<template>
<div class="column is-2">
<p v-for="channel in channels"
:key="channel.id"
v-text="channel.name"
class="channel"
:class="{ 'active': channel.id == activeChannel }"></p>
</div>
</template>
<script>
export default {
props: ['channels', 'activeChannel'],
}
</script>
<style scoped>
.column {
border-right: 2px dotted greenyellow;
}
.channel {
cursor: pointer;
}
.channel.active {
background: #666;
}
</style>
As always, nothing fancy. Just printing a list of channels while highlighting the active one.
Displaying Messages for a Channel
Next we'll display messages. Similarly we'll create a separate Vue component for that at resources/assets/js/components/VueChatMessages.vue
.
<template>
<div class="column">
<p v-for="message in messages"
:key="message.id"
class="message"
>
<span class="datetime">{{ message.created_at }}</span>
<span class="username"><{{ message.author_username }}></span>
<span class="message-text">{{ message.message }}</span>
</p>
</div>
</template>
<script>
export default {
props: ['messages'],
}
</script>
<style scoped>
.column {
overflow-y: auto;
}
.message {
background: #000;
}
.datetime {
color: turquoise;
}
.username {
color: green;
}
</style>
Register the component in resources/assets/js/app.js
:
Vue.component('vue-chat-messages', require('./components/VueChatMessages.vue'));
Now replace the div with the messages
text in resources/assets/js/components/VueChat.vue
with our new component:
<vue-chat-messages :messages="messages"></vue-chat-messages>
The rest is going to be different from the channels component. We're not going to pass the list of messages all the way from the backend down to the messages component. Instead, we're going to fetch the messages via AJAX inside the VueChat.vue
component. First, add a new data property to store the messages:
messages: [],
Next, create a method to fetch the messages from the backend:
methods: {
fetchMessages() {
let endpoint = `/channels/${this.activeChannel}/messages`;
axios.get(endpoint)
.then(({ data }) => {
this.messages = data;
});
}
},
Finally, call this method on component's creation:
created() {
this.fetchMessages();
},
Now let's implement the endpoint itself inside the routes/web.php
file:
Route::get('/channels/{channel}/messages', function (App\Channel $channel) {
if (!request()->wantsJson()) {
abort(404);
}
$messages = App\Message::where('channel_id', $channel->id)->get();
return $messages;
});
To see the result you need some messages in the database. We can populate our DB with some fake messages using a factory.
php artisan make:factory MessageFactory
database/factories/MessageFactory.php
:
<?php
use Faker\Generator as Faker;
$factory->define(App\Message::class, function (Faker $faker) {
return [
'channel_id' => 4,
'author_username' => $faker->name,
'message' => $faker->paragraph,
];
});
All the fake messages will belong to the channel with id 4 by default. This is a channel called AI
in our database (this channel is the first one in the alphabetical order and will be selected by default on page load). Now run php artisan tinker
and generate a few messages by executing this line:
factory('App\Message', 10)->create();
Close tinker. Refresh your browser's page and you should see all the fake messages we've just created.
Switching Channels
Let's add a bit of interactivity to our chat. Switching channels is very easy. First, open the resources/assets/js/components/VueChatChannels.vue
component. We're going to add a click event to each channel and a click will trigger the setChannel
method. We're going to pass the channel's id to the method.
<p v-for="channel in channels"
:key="channel.id"
v-text="channel.name"
class="channel"
:class="{ 'active': channel.id == activeChannel }"
@click="setChannel(channel.id)"></p>
And here's the method itself:
methods: {
setChannel(id) {
this.$emit('channelChanged', id);
}
}
The only thing it does is emits an event with the id of the channel to be selected. This will be passed up to the VueChat.vue
component, since it is our main hub for all the chat data. Open resources/assets/js/components/VueChat.vue
and add an event listener:
<vue-chat-channels :channels="channels"
:active-channel="activeChannel"
@channelChanged="onChannelChanged"></vue-chat-channels>
The id of the channel to be selected will be passed to the onChannelChanged
method. Inside this method we'll simply update the activeChannel
data property and then re-fetch the messages from the backend.
onChannelChanged(id) {
this.activeChannel = id;
this.fetchMessages();
}
So far we've created a foundation for our chat app, although at the moment we can only read the existing messages. We will turn it into a real-time multi-user chat in the next part.
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!