NodeJS – Xây dựng tính năng nhắn tin với Socket.IO

4.4/5 - (27 votes)

Trong phần trước chúng ta đã tìm hiểu qua thư viện Socket.IO, trong phần này chúng ta sẽ sử dụng thư viện này để xây dựng tính năng nhắn tin thời gian thực.

Tạo model

Trong file models-sequelize/users.js chúng ta sửa lại như sau:

var events = require('events');
var async = require('async');
var emitter = module.exports.emitter = new events.EventEmitter();

var util = require('util');
var Sequelize = require('sequelize');
var sequelize = undefined;
var User = undefined;

module.exports.connect = function(params, callback) {
    sequelize = new Sequelize(params.dbname,
                              params.username,
                              params.password,
                              params.params);
    User = sequelize.define('User', {
        id: {
            type: Sequelize.INTEGER,
            primaryKey: true,
            unique: true
        },
        username: {
            type: Sequelize.STRING,
            unique: true
        },
        password: Sequelize.STRING,
        email: Sequelize.STRING
    });
    User.sync().then(function() {
        callback()
    }).error(function(err) {
        callback(err);
    });
 
    Messages = sequelize.define('Messages', {
        idTo: { type: Sequelize.INTEGER, unique: false},
        idFrom: { type: Sequelize.INTEGER, unique: false},
        message: { type: Sequelize.STRING, unique: false}
    });
    Messages.sync();
}

module.exports.findById = function(id, callback) {
    User.find({ where: { id: id} }).then(function(user) {
        if(!user) {
            callback('User ' + id + ' does not exist');
        } else {
            callback(null, {
                id: user.id,
                username: user.username,
                password: user.password,
                email: user.email
            });
         }
     });
}

module.exports.findByUsername = function(username, callback) {
    User.find({where: {username: username}}).then(function(user) {
        if(!user) {
            callback('user ' + username + ' does not exist');
        } else {
            callback(null, {
                id: user.id,
                username: user.username,
                password: user.password,
                email: user.email
            });
         } 
     });
}

module.exports.create = function(id, username, password, email, callback) {
    User.create({
        id: id,
        username: username,
        password: password,
        email: email
    }).then(function(user) {
        callback();
    }).error(function(err) {
        callback(err);
    });
}

module.exports.update = function(id, username, password, email, callback) {
    User.find({where: {id: id}}).then(function(user) {
        user.updateAttributes({
            id: id,
            username: username,
            password: password,
            email: email
        }).then(function() {
            callback();
        }).error(function(err) {
            callback(err);
        });
    });
}

module.exports.allUsers = function(callback) {
    User.findAll().then(function(users) {
        if(users) {
            var userList = [];
            users.forEach(function(user) {
                userList.push({
                    id: user.id,
                    name: user.username
                });
            });
            callback(null, userList);
        } else
            callback();        
    });
}

module.exports.sendMessage = function(id, from, message, callback) {
    Messages.create({
        idTo: id,
        idFrom: from,
        message: message
    }).then(function(user) {
        emitter.emit('newmessage', id);
        callback();
    }).error(function(err) {
        callback(err);
    });
}

module.exports.getMessages = function(id, callback) {
    Messages.findAll({
        where: { idTo: id}
    }).then(function(messages) {
        if(messages) {
            var messageList = [];
            async.eachSeries(messages, 
               function(msg, done) {
                   module.exports.findById(msg.idFrom,
                       function(err, userFrom) {
                           messageList.push({
                               idTo: msg.idTo,
                               idFrom: msg.idFrom,
                               fromName: userFrom.username,
                               message: msg.message
                            });
                            done();
                        });
                    },
                    function(err) {
                        if(err) 
                            callback(err);
                        else 
                            callback(null, messageList);
                        }
                    );
        } else {
            callback();
        }
    });
}

module.exports.delMessage = function(id, from, message, callback) {
    Messages.find({
        where: { idTo: id, idFrom: from, message: message}
    }).then(function(msg) {
        if(msg) {
            msg.destroy().then(function() {
                emitter.emit('delmessage');
                callback();
            }).error(function(err) {
                callback(err);
            });
        } else 
            callback();
    });
}

Lẽ ra các tin nhắn nên được lưu trong một file model riêng, nhưng ở đây chúng ta sẽ lưu trong file users.js luôn vì chúng ta chỉ làm đơn giản thôi.

module.exports.connect = function(params, callback) {
    sequelize = new Sequelize(params.dbname,
                              params.username,
                              params.password,
                              params.params);
    ...
 
    Messages = sequelize.define('Messages', {
        idTo: { type: Sequelize.INTEGER, unique: false},
        idFrom: { type: Sequelize.INTEGER, unique: false},
        message: { type: Sequelize.STRING, unique: false}
    });
    Messages.sync();
}

Đầu tiên chúng ta khai báo mô hình dữ liệu và gọi hàm sync() để tạo bảng trong CSDL. Ở đây chúng ta chỉ lưu các tin nhắn đơn giản thôi, chỉ cần id người gửi, id người nhận và nội dung tin nhắn, nếu bạn còn nhớ thì sequelize sẽ tạo thêm 2 trường nữa trong CSDL là createdAtupdatedAt như đã nói trong bài lưu trữ dữ liệu MySQL.

module.exports.allUsers = function(callback) {
    User.findAll().then(function(users) {
        if(users) {
            var userList = [];
            users.forEach(function(user) {
                userList.push({
                    id: user.id,
                    name: user.username
                });
            });
            callback(null, userList);
        } else
            callback();        
    });
}

Tiếp theo chúng ta định nghĩa hàm allUsers() có chức năng lấy danh sách user, đoạn code trên rất đơn giản, chúng ta lấy toàn bộ danh sách các user ra rồi trả về.

module.exports.sendMessage = function(id, from, message, callback) {
    Messages.create({
        idTo: id,
        idFrom: from,
        message: message
    }).then(function(user) {
        emitter.emit('newmessage', id);
        callback();
    }).error(function(err) {
        callback(err);
    });
}

Hàm sendMessage() có chức năng gửi tin nhắn, ở đây chúng ta tạo một đối tượng model Messages, sau đó giải phóng sự kiện newmessage, đây là sự kiện mới, chúng ta sẽ code đoạn bắt sự kiện trong file app.js.

module.exports.getMessages = function(id, callback) {
    Messages.findAll({
        where: { idTo: id}
    }).then(function(messages) {
        if(messages) {
            var messageList = [];
            async.eachSeries(messages, 
               function(msg, done) {
                   module.exports.findById(msg.idFrom,
                       function(err, userFrom) {
                           messageList.push({
                               idTo: msg.idTo,
                               idFrom: msg.idFrom,
                               fromName: userFrom.username,
                               message: msg.message
                            });
                            done();
                        });
                    },
                    function(err) {
                        if(err) 
                            callback(err);
                        else 
                            callback(null, messageList);
                        }
                    );
        } else {
            callback();
        }
    });
}

Hàm getMessages() sẽ lấy toàn bộ tin nhắn được gửi tới một user nhất định, đoạn code trên cũng rất đơn giản, giống hệt với hàm titles() trong file model models-sequelize/notes.js thôi.

module.exports.delMessage = function(id, from, message, callback) {
    Messages.find({
        where: { idTo: id, idFrom: from, message: message}
    }).then(function(msg) {
        if(msg) {
            msg.destroy().then(function() {
                emitter.emit('delmessage');
                callback();
            }).error(function(err) {
                callback(err);
            });
        } else 
            callback();
    });
}

Hàm delMessage() sẽ xóa một tin nhắn nhất định, có một lưu ý là ở đây trong bảng lưu các tin nhắn chúng ta không có một trường nào làm khóa chính cả, do đó việc tìm một tin nhắn rồi xóa sẽ dựa vào cả 3 trường id người nhận, id người gửi và nội dung, nhưng như thế cũng không đúng vì một người có thể nhắn nhiều tin nhắn có nội dung giống nhau đến một người khác, và do đó khi xóa một tin nhắn thì các tin nhắn khác giống nhau cũng sẽ bị xóa luôn, ở đây mình chỉ làm đơn giản nên không kiểm tra vấn đề này, nếu muốn bạn có thể tự cải tiến thêm, chẳng hạn như thêm một trường làm id… Ngoài ra ở đây hàm delMessage() cũng sẽ giải phóng sự kiện delmessage nếu xóa thành công, đây cũng là một sự kiện mới.

Cấu hình routing

Chúng ta sửa lại file app.js như sau:

...
app.use('/sendmessage', users.ensureAuthenticated, users.sendMessage);
app.post('/dosendmessage', users.ensureAuthenticated, users.doSendMessage);

...
 
io.sockets.on('connection', function(socket) {
    socket.on('notetitles', function(fn) { 
        models.titles(function(err, titles) {
            if(err) {
                util.log(err); 
            } else {
                fn(titles);
            }
        });
    });
 
    var broadcastUpdated = function(newnote) {
        socket.emit('noteupdated', newnote);
    }
    models.emitter.on('noteupdated', broadcastUpdated);
    socket.on('disconnect', function() {
        models.emitter.removeListener('noteupdated', broadcastUpdated);
    });
 
    var broadcastDeleted = function(notekey) {
        socket.emit('notedeleted', notekey);
    }
    models.emitter.on('notedeleted', broadcastDeleted);
    socket.on('disconnect', function() {
        models.emitter.removeListener('notedeleted', broadcastDeleted);
    });
 
    socket.on('getmessages', function(id, fn) {
        usersModels.getMessages(id, function(err, messages) {
            if(err) {
                util.log('getmessages ERROR ' + err);
            } else 
                fn(messages);
        });
    });
 
    var broadcastNewMessage = function(id) {
        socket.emit('newmessage', id);
    }
    usersModels.emitter.on('newmessage', broadcastNewMessage);
 
    var broadcastDelMessage = function() {
        socket.emit('delmessage');
    }
    usersModels.emitter.on('delmessage', broadcastDelMessage);
 
    socket.on('disconnect', function() {
        usersModels.emitter.removeListener('newmessage', broadcastNewMessage);
        usersModels.emitter.removeListener('delmessage', broadcastDelMessage);
    });
 
    socket.on('dodelmessage', function(id, from, message, fn) {
        // do nothing
    });
});

Ở đây chúng thêm routing 2 đường dẫn mới là /sendmessage/dosendmessage, và viết một số hàm bắt sự kiện mới.

Các hàm bắt sự kiện mới gồm có sự kiện getmessages, newmessagedelmessage, dodelmessage, các sự kiện này sẽ được phát ra từ trình duyệt của client giống như với đoạn code xử lý sự kiện tạo, sửa, xóa ghi chú vây. Chỉ có một lưu ý là hàm bắt sự kiện dodelmessage không làm gì cả vì ở đây mình chưa hoàn thiện phần xóa tin nhắn như đã nói ở trên (đây là bài tập cho bạn đó, bạn tự code sao cho có thể xóa được tin nhắn đi :))

Tiếp theo chúng ta viết thêm 2 hàm sau vào file routes/users.js để xử lý 2 URL /sendmessage /dosendmessage như sau:

...
module.exports.sendMessage = function(req, res) {
    users.allUsers(function(err, userList) {
       res.render('sendmessage', {
           title: "Send a message",
           user: req.user,
           users: userList,
           message: req.flash('error')
       });
    });
}

module.exports.doSendMessage = function(req, res) {
    users.sendMessage(req.body.seluserid, 
        req.user.id, req.body.message, function(err) {
            if(err) {
                res.render('showerror', {
                    user: req.user ? req.user : undefined,
                    title: "Could not send message",
                    error: err
                });
            } else {
                res.redirect('/');
            }
    });
}

Hàm sendMessage() sẽ trả về nội dung trong file sendmessage.ejs (chúng ta sẽ viết ở dưới), hàm doSendMessage() sẽ gọi hàm sendMessage() ở bên phía user để tạo và lưu một tin nhắn mới.

Tạo view

Chúng ta tạo một file có tên sendmessage.ejs trong thư mục views có nội dung như sau:

<% include top %>
<form method="POST" action="/dosendmessage">
    <p>To:
    <select name="seluserid">
        <% for(var i in users) { %>
            <option value="<%= users[i].id %>">
                <%= users[i].name %>
            </option>
        <% } %>
    </select>
    </p>
    <p>Message: <input type='text' name='message' /></p>
    <p><input type='submit' value="Submit" /></p>
</form>
<% include bottom %>

Ở đây chúng ta hiển thị một form để người dùng chọn người gửi và nhập nội dung tin nhắn, form này sẽ được gửi lên URL /dosendmessage.

Tiếp theo chúng ta sửa lại file top.ejs như sau:

<html>
<head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <script src="/socket.io/socket.io.js"></script>
    <script>
        var socket = undefined;
        var delMessage = function(idTo, idFrom, message) {
            socket.emit('dodelmessage', idTo, idFrom, message);
        }
        $(document).ready(function() {
            socket = io.connect('/'); 
            <% if(user) { %> 
                var getmessages = function() {
                    socket.emit('getmessages', <%= user.id %>,
                    function(msgs) {
                        $('#messageBox').empty();
                        if(msgs.length > 0) {
                            for(var i in msgs) {
                                $('#messageBox').append('<p>');
                                $('#messageBox').append('<button onclick="delMessage('+
                                                   msgs[i].idTo + ', ' +
                                                   msgs[i].idFrom + ', \'' + 
                                                   msgs[i].message + '\')">DEL</button> '); 
                                $('#messageBox').append(msgs[i].fromName + ": ");
                                $('#messageBox').append(msgs[i].message);
                                $('#messageBox').append('</p>');
                            }
                            $('#messageBox').show();
                        } else 
                            $('#messageBox').hide();
                    });
        };
        getmessages();
        socket.on('newmessage', function(id) {
            getmessages();
        });
        socket.on('delmessage', getmessages);
        <% } %>
    });
    </script>
</head>
<body>
    <h1><%= title %></h1>
    <div class='navbar'>
    <p>
        <a href='/'>Home</a> 
        | <a href='/noteadd'>ADD Note</a>
        <% if(user) { %>
        | <a href='/sendmessage'>Send message</a>
        | <a href='/logout'>Log Out</a> 
        | logged in as <a href='/account'><%= user.username %></a>
        <% } else { %>
        | <a href='/login'>Log in</a>
        <% } %>
    </p>
    </div>
    <% if(user) { %>
         <div id='messageBox' style='display:none;'></div>
    <% } %>

Ở đây cái chính là chúng ta viết hàm getmessages(), và hàm này được gọi ngay trong $(document).ready(), tức là khi người dùng đăng nhập vào thì chúng ta sẽ lấy toàn bộ tin nhắn được gửi tới người dùng đó và hiển thị lên cùng với các ghi chú luôn.

socket.on('newmessage', function(id) {
    getmessages();
});
socket.on('delmessage', getmessages);

Ngoài ra ở đây chúng ta còn cho lắng nghe 2 sự kiện delmessagenewmessage, 2 sự kiện này sẽ được server phát đi khi có người gửi tin nhắn hoặc xóa tin nhắn, và trình duyệt sẽ tự động cập nhật lại luôn vì chúng ta xử lý 2 sự kiện này bằng hàm getmessages() vừa được định nghĩa ở trên.

| <a href='/sendmessage'>Send message</a>

Chúng ta thêm một đường link trỏ đến URL /sendmessage để khi người dùng click vào thì ra trang gửi tin nhắn.

 <% if(user) { %>
     <div id='messageBox' style='display:none;'></div>
 <% } %>

Đoạn code trên sẽ bỏ hiển thị thẻ <div> có id là messageBox khi người dùng không đăng nhập.

Nếu muốn bạn có thể chỉnh sửa giao diện hiển thị cho phần tin nhắn một tí trong file public/stylesheets/style.css như sau:

#messageBox {
    border: solid 2px red;
    background-color: #eeeeee;
    padding: 5px;
}

Bây giờ bạn có thể chạy project được rồi, trước khi chạy, bạn có thể mở file models-sequelize/setup.js và tạo một user mới để test.

capture

Bạn cũng có thể mở 2 trình duyệt khác nhau như Chrome và Firefox, mỗi bên đăng nhập một tài khoản, rồi một bên gửi tin nhắn cho bên kia và ghi gửi thành công thì bên nhận cũng sẽ tự động hiển thị tin nhắn đó lên luôn mà không cần tải lại trang web.

0 0 votes
Article Rating
Subscribe
Thông báo cho tôi qua email khi
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments