+39

Xây dựng ứng dụng “động vật ẩn danh” như Google – Tại sao không?

Chào các bạn, gần đây mình ít cập nhật bài viết trên Viblo, tuy nhiên mình vẫn làm content trên kênh YouTube đều đặn, nếu bạn nào quan tâm tới nhiều nội dung thú vị về lập trình, có thể thử qua kênh của mình để xem và ủng hộ mình bằng cách tặng mình một lượt đăng ký kênh nhé ^^ Cảm ơn các bạn!

Link YouTube của mình: https://www.youtube.com/@trungquandev





Xin chào tất cả các bạn, như tiêu đề bài viết, hôm nay mình sẽ code một ứng dụng nhỏ có tên là "động vật ẩn danh". Vì sao mình lại nói ứng dụng này như Google, các bạn xem cái hình này nhé.



Chính là nó đó, cái này bạn nào hay dùng mấy cái ứng dụng văn phòng của google chắc là khá quen thuộc rồi. Cá nhân mình thấy nó rất hay, tranh thủ hiện tại đang nghiên cứu về Nodejs, code luôn, coi như bài tập giải trí trước khi tập trung sm vào đồ án tốt nghiệp =))

– Demo ứng dụng: https://anonymous-animal-trungquandev.herokuapp.com/

(“Lưu ý do Heroku để lâu nó ngủ đông nên có thể mất chút thời gian load lần đầu truy cập.”)

– Link Github: https://github.com/trungquan17/anonymous-animals/

– Bài viết cũng đồng thời được Post lên blog cá nhân của mình:

https://trungquandev.com/xay-dung-ung-dung-dong-vat-an-danh-nhu-google/

Những nội dung có trong bài này:

1. Phân tích ý tưởng ứng dụng

2. Khởi tạo Project Nodejs và cài các package cần thiết

3. Xây dựng khung ứng dụng theo một chuẩn (RESTful APIs)

4. Code ứng dụng:

4.1. Viết code xử lý routes

4.2. Viết code xử lý controllers

4.3. Viết code xử lý views

4.4. Viết code xử lý models

4.5. Viết code xử lý socket – realtime phía server

4.6. Viết code xử lý socket – realtime – (javascript + css) phía client

4.7. Viết code css cho tooltip

4.8. Viết code cho file server chính


1. Phân tích ý tưởng ứng dụng

Như đã giới thiệu, ý tưởng thì cũng là ăn cắp lại của Google mà nên là chức năng mà sau khi mình phân tích để làm thì đơn giản chỉ có mấy cái gạch đầu dòng dưới đây:

– Khi có người dùng truy cập trang web, server sẽ gán cho người đấy tên của một con vật ngẫu nhiên, cộng với màu của con vật đó, đồng thời trả về cho tất cả những người khác cũng đang truy cập thấy người này luôn.

– Khi một người click vào một ô nào đó, thì những người còn lại cũng sẽ thấy ô đó bị thay đổi màu đường viền, trùng với màu của cái con vật đã gán cho họ.

– Khi hover chuột qua cái ô mà người khác chọn, thì hiển thị tên con vật mà đã gán cho người đó.

– Cuối cùng khi một người thoát khỏi trang hoặc click ra ngoài các ô input, thì đường viền của ô đó trở lại ban đầu. Và khi thoát trang thì không hiển thị con vật đó online nữa.


2. Khởi tạo Project Nodejs và cài các package cần thiết

Đầu tiên, khởi tạo project Nodejs:

npm init

Tiếp theo chúng ta đi cài đặt một số package cần thiết cho ứng dụng ngày hôm nay, cụ thể chúng ta cần: express để build server, socket.io để xử lý các chức năng real-time, ejs là template engine cho phần frontend, và cuối cùng là fs, dùng để đọc file.

npm install --save express

npm install --save socket.io

npm install --save ejs

npm install --save fs


3. Xây dựng khung ứng dụng theo một chuẩn (RESTful APIs)

Thực ra để demo ứng dụng của ngày hôm nay thì không cần thiết phải thiết kế theo chuẩn Resful, nhưng mình đưa vào luôn, để cho những bạn nào chưa biết thì tìm hiểu luôn một thể, code cho nó dễ đọc. Bạn nào không thích thì cũng có thể dùng package express-generator để tạo nhanh project cũng được. Còn trong bài này thì mình tự tạo cấu trúc thư mục ứng dụng như sau:


4. Code ứng dụng

4.1. Viết code xử lý routes: Ở trong thư mục routes mình sẽ tạo một file có tên là web.js, file này dùng để xử lý các url của chúng ta:

/**
 * Require Express Router
 */
var express = require('express');
var router = express.Router();

/**
 * Require Controller
 */
var homeController = require('../controllers/homeController');

/**
 * Routes
 */
router.get('/', homeController.index);

/**
 * Export Router
 */
module.exports = router;
//

Chỉ có vài dòng code như trên thôi, mình cũng có comment lại chức năng của chúng rồi đó.

  • Đầu tiên là require thư viện express để sử dụng express router.

  • Tiếp theo require file homeController.js để viết code xử lý cho route, mục tiếp theo mình sẽ tạo file controller này.

  • Sau đó tạo đường dẫn và điều hướng xử lý của cái route này đến hàm xử lý index trong file** homeController.js**, trong bài này chỉ cần một cái route là đủ.

  • Cuối cùng là export cái route đó ra để lát nữa ở file server chúng ta sẽ require để dùng.


4.2. Viết code xử lý controllers Trong thư mục controllers mình tạo 1 file homeController.js như đã nói ở trên. Nội dung của nó:

/**
 * Create & Export function index
 */
exports.index = (request, response) => {
    response.render('home');
}
//

Ngắn gọn không :v, có đúng một hàm index và chức năng của nó chỉ có việc render ra file home.ejs để trả về cho người dùng truy cập thôi. Đồng thời mình export nó luôn để cho cái file route web.js ở phần trên sử dụng được.

Đúng ra thì controller sẽ nhận dữ liệu từ model nữa rồi mới trả về view, nhưng trong ứng dụng này thì phần dữ liệu model mình sẽ sử dụng ở module xử lý socket nên controller chỉ nhẹ nhàng đơn giản vậy thôi.


4.3. Viết code xử lý views Trong thư mục views mình tạo file home.ejs như đã nói ở phần controlers trên. Nội dung của file này:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Anonymous Animals</title>
    <link rel="shortcut icon" href="images/favicon-trungquandev.png" />
    <link href="library/bootstrap/dist/css/bootstrap.css" rel="stylesheet" type="text/css">
    <link href="css/trungquan.css" rel="stylesheet" type="text/css">
</head>
<body>
    <div class="container">
        <h2 style="text-align: center;">Anonymous Animals Example</h2>
        <p>
            <span style="font-weight:bold;">Trung Quân:</span> 
            <a href="https://cv.trungquandev.com/" target="_blank">https://cv.trungquandev.com/</a>
        </p>                                                                                      
        <div class="table-responsive">          
            <table class="table" id="animals-table">
                <tbody>
                </tbody>
            </table>
        </div>
        <p style="font-weight:bold;">Animals Online:</p>
        <div class="list-group" id="alimals-online"></div>
    </div>
 
    <script src="library/jquery/dist/jquery.min.js"></script>
    <script src="library/bootstrap/dist/js/bootstrap.min.js"></script>
    <script src="socket.io/socket.io.js"></script>
    <script src="js/handle.js"></script>
</body>
</html>

Đơn giản nó là file html để mình trả về cho client. Có một số chú ý sau, mình đi lần lượt từ trên xuống nhé:

- Phần CSS: mình sử dụng thư viện bootstrap để lát tạo cái table cho đẹp =)). Và một file css tự viết nữa để tạo cái tooltip mà sẽ xuất hiện khi hover qua mấy con vật ẩn danh, không biết đặt tên file này là gì, mình để trungquan.css luôn :v

- Phần Body trang web: Các bạn chỉ cần để ý đến cái <table>id="animals-table" để hiển thị ra cái bảng cùng với một loạt các ô input cho người dùng click vào. Và cái thẻ

id="animals-online" để hiển thị danh sách những người đang online. Chút nữa mình sẽ viết code javascript để xử lý 2 phần chính này sau.

- Phần Javascript: Mình sử dụng 3 thư viện jquey.min.js, bootstrap.min.js và socket.io.js. Và một file javascript tự viết là handle.js, file này khá quan trọng nhé, chút nữa sẽ tới phần code cho nó.


4.4. Viết code xử lý models Trong thư mục models mình tạo 2 file:

animals.json, đây là file chứa danh sách tên của mấy con động vật mà mình nghĩ ra, cụ thể mình có thêm vào 17 con vật, cùng với idcolor của từng con. Đúng ra thì chúng ta nên tạo cơ sở dữ liệu để lưu danh sách này, nhưng trong khuôn khổ bài viết này, mình không làm thế để tránh loãng cũng như bài viết bị dài quá. Các bạn có thể dùng mongoDB, Mysql hay bất kỳ cơ sở dữ liệu nào cũng được.

Vì nội dung file hơi dài nên các bạn vào link này để copy nhé, chứ mình bỏ vào đây thì nó chiếm nhiều diện tích chiều cao quá =))

https://github.com/trungquan17/anonymous-animals/blob/master/models/animals.json

database.js, file này sẽ đọc và lấy ra danh sách 17 con vật trong file animals.json ở trên sau đó export ra để lát nữa module socket sẽ dùng đến.


4.5. Viết code xử lý socket – realtime phía server: Trong thư mục socket mình tạo 1 file có tên là returnAnimals.js với những chức năng là:

Lắng nghe người truy cập.

Gán cho mỗi người truy cập ứng với một con vật và trả về cho những người khác thấy.

Xử lý các hành động của người truy cập và gửi về cho toàn bộ những người còn lại.

/**
 * Require module socket.io & Model Animals
 */
var socketio = require('socket.io');
var DB_Animals = require('../models/database');
 
module.exports.listen = (app) => {
    /**
     * Create an array to contain animals online
     */
    var animalArray = [];
 
    /**
     * Initialization module io
     */
    io = socketio.listen(app);
    io.on('connection', (socket) => {
        /**
         * Take out a random animal
         */
        var randomAnimal = DB_Animals[Math.floor(Math.random() * DB_Animals.length)];
 
        /**
         * Check whether the visitor exceeds the animal limit list?
         */
        if (animalArray.length == DB_Animals.length) {
            return false;
        }
        
        /**
         * Check if the selected random animal is repeated or not?
         */
        while (animalArray.indexOf(randomAnimal.id) != -1) {
            randomAnimal = DB_Animals[Math.floor(Math.random() * DB_Animals.length)];
        }
 
        /**
         * Push this animal's id in to the animals array
         */
        animalArray.push(randomAnimal.id);
 
        console.log("Animal: " + randomAnimal.name + " connected!");
        /**
         * Send animal list online to the client
         */
        socket.broadcast.emit("animal-online", {
            animalId: randomAnimal.id,
            animalName: randomAnimal.name,
            animalColor: randomAnimal.color
        });
 
        /**
         * Listen to an event from client when an animal is offline
         */
        socket.on("disconnect", () => {
            console.log("User: " + randomAnimal.name + " disconnected!");
            animalArray.splice(animalArray.indexOf(randomAnimal.id), 1);
            socket.broadcast.emit("animal-offline", randomAnimal.id);
        });
 
        /**
         * Listen to an event from client when an animal clicks the input box
         */
        socket.on('animal-check', (inputId) => {
            var dataCallback = { 
                    inputId: inputId,
                    animalName: randomAnimal.name,
                    animalColor: randomAnimal.color
            };
            socket.broadcast.emit("server-send-check", dataCallback);
        });
 
        /**
         * Listen to an event from client when an animal leaves the input box
         */
        socket.on('animal-uncheck', (inputId) => {
            socket.broadcast.emit("server-send-uncheck", inputId);
        });
    });
 
    /**
     * return module io for server
     */
    return io;
}
//

Code bắt đầu trông nhiều rồi phải không, đừng lo. Các bạn chỉ cần nhìn vào những đoạn mà mình comment ở trên, và mình sẽ giải thích nó ở ngay dưới đây:

- Require module socket.io & Model Animals:

Chúng ta sẽ cần require module socket.io để xử lý real-time, và require model animals để lấy ra danh sách các con vật.

- Create an array to contain animals online

Bước này là khởi tạo một mảng animalArray để chứa những con vật đang online.

- Initialization module io

Khởi tạo module io từ socketio để xử lý real-time.

- Take out a random animal

Bước này mình sẽ lấy ra ngẫu nhiên một con vật trong danh sách 17 con vật mà lấy được từ model.

- Check whether the visitor exceeds the animal limit list?

Ở đây mình kiểm tra xem số người truy cập đã bằng giới hạn số lượng con vật chưa, nếu bằng rồi thì return false luôn tránh crash server. Có nghĩa là từ người thứ 18 trở đi, họ không còn được gán với con vật nào nữa cả.

- Check if the selected random animal is repeated or not?

Kiểm tra xem con vật mà mình random ngẫu nhiên có bị trùng với con nào trong mảng animalArray hay không, nếu trùng thì cho nó random lại. Vì ứng dụng nhỏ, có 17 con vật nên tỉ lệ random trùng cũng khá cao mà.

- Push this animal's id in to the animals array

Sau khi mà random con vật oke, không bị trùng, mình sẽ push id của con vật đó vào mảng animalArray.

- Send animal list online to the client

Khi con vật đó online, sẽ gửi một sự kiện animal-online bao gồm id, name, color của nó về Client, và mình muốn chỉ gửi cho tất cả những con vật khác thấy chứ không gửi cho bản thân nó thấy chính nó. Nên ở đây sẽ sử dụng socket.broadcast.emit();

- Listen to an event from client when an animal is offline

Đã lắng nghe online thì cũng phải có offline chứ nhỉ, chính là ở bước này, khi một con vật offline, mình sẽ xóa id của nó ra khỏi mảng animalArray và đồng thời gửi một sự kiện animal-offline với dữ liệu là id của con vật đó về client. Cách gửi tương tự lúc con vật đó online, dùng socket.broadcast.emit();

- Listen to an event from client when an animal clicks the input box

Đứng từ Server mình sẽ lắng nghe sự kiện từ Client khi con vật click vào ô input, Server lắng nghe sự kiện animal-check và mình nhận được id của thẻ input đó, sau đó tiếp tục gửi một sự kiện server-send-check khác về phía Client cho tất cả những con vật khác, bao gồm id của thẻ input, tên và màu của con vật hiện tại. Mục đích để ở client mình có thể đổi màu đường viền của thẻ input đó và gán tên con vật vào tooltip của input.

- Listen to an event from client when an animal leaves the input box

Tương tự như việc lắng nghe sự kiện click ở trên, thì đây là sự kiện animal-uncheck, khi con vật rời khỏi ô input, Server của mình sẽ lắng nghe sự kiện đó và gửi một sự kiện server-send-uncheck về cho Client, dữ liệu gửi về chỉ cần id của thẻ input thôi, để mình remove css của thẻ đó về hiện trạng ban đầu.

- Return module io for server

Cuối cùng là trả về module io cho file Server lát nữa sử dụng.


4.6. Viết code xử lý socket – realtime – (javascript + css) phía client: Ở phần 4.5 bên trên, là mình viết code xử lý real-time phía server, bây giờ chúng ta cần viết cả phần xử lý real-time javascript ở phía Client nữa, đó chính là nội dung của phần này.

Trong thư mục public/js/ mình tạo file handle.js

/**
 * initialize socket module
 */
var socket = io("http://localhost:3000");
// var socket = io("https://anonymous-animal-trungquandev.herokuapp.com");
 
/**
 * Listen to an event from server when an animal online
 */
socket.on('animal-online', (data) => {
    var animal = '<a id="online-' + data.animalId + '" class="list-group-item" style="color: white; background-color: ' + data.animalColor + ';">' + data.animalName + ' ẩn danh.</a>';
    $('#alimals-online').prepend(animal);
});
 
/**
 * Listen to an event from SERVER when an animal offline
 */
socket.on('animal-offline', (animalId) => {
    $('#online-' + animalId).remove();
});
 
/**
 * Listen to an event from SERVER when an animal clicks the input box
 */
socket.on('server-send-check', (data) => {
    $('#' + data.inputId).css('border-color', data.animalColor);
    var tooltip = '<span class="tooltiptext" style="color: white; background-color: ' + data.animalColor + ';">' + data.animalName + ' ẩn danh</span>';
    $('#td-' + data.inputId).append(tooltip);
});
 
/**
 * Listen to an event from SERVER when an animal leaves the input box
 */
socket.on('server-send-uncheck', (inputId) => {
    $('#' + inputId).css('border-color', '');
    $('#td-' + inputId + '>span').remove();
});
 
$(document).ready(() => {
    /**
     * Add a table has 6 cols and 6 rows
     */
    for (var i = 0; i < 6; i++) {
        var data = "<tr>";
            for(var j = 0; j < 6; j++) {
                data += "<td class='td-animal' id='td-" + i + j + "'><input type='text' class='animal' id='" + i + j + "' placeholder='check me'></td>";
            }
        data += "</tr>";
        $('#animals-table>tbody').append(data);
    }
 
    /**
     * Initialize a variable inputCheckedId
     */
    var inputCheckedId;
 
    /**
     * Send Event when an animal clicks the input box
     */
    $('.animal').bind('click', function () {
        socket.emit('animal-check', this.id);
        inputCheckedId = this.id;
    });
    
    /**
     * Send Event when an animal leaves the input box
     */
    $('.animal').bind('blur', function () {
        socket.emit('animal-uncheck', this.id);
    });
 
    /**
     * Send Event when an animal leaves the site or f5 page.
     */
    $(window).bind('beforeunload', function () {
        socket.emit('animal-uncheck', inputCheckedId);
    });
});
//

Các bạn cũng để ý đến những dòng comment nhé, mình sẽ đi lần lượt các comment theo chức năng, việc quan trọng đầu tiên là:

- Initialize socket module:

Khởi tạo module socket phía Client, và truyền domain của trang web để nó biết rằng đang gửi nhận sữ liệu socket tới server nào.

Bây giờ bắt đầu đi từ code trong $(document).ready() trước:

- Add a table has 6 columns and 6 rows:

Mình dùng vòng lặp for để thêm 6 cột và 6 hàng vào table mà mình đã tạo ở phần 4.3, mỗi <td> sẽ gán vào một thẻ <input> kèm theo, đồng thời set class và id cho từng thẻ td và từng thẻ input một, class thì giống nhau, còn riêng id thì mình cho chúng khác nhau bằng cách cho thêm biến chạy i, j vào.

- Initialize a variable inputCheckedId

Khởi tạo một biến inputCheckedId để lát sử dụng.

- Send Event when an animal clicks the input box

Khi con vật click vào một input, mình sẽ lấy id của thẻ input đó, gửi lên Server với sự kiện animal-check, đồng thời cũng gán luôn cái id đó vào biến inputCheckedId vừa tạo ở trên.

- Send Event when an animal leaves the input box

Tương tự như khi con vật click input, thì đây là sự kiện con vật rời khỏi cái input đó, mình gửi lên server một event animal-uncheck với id của thẻ input. Chỉ đơn giản vậy thôi ^^.

- Send Event when an animal leaves the site or f5 page.

Một trường hợp nữa là khi con vật đó thoát trang web hoặc refresh trang thì sao? Đơn giản khi này mình dùng BOM javascript, bind sự kiện beforeunload và cũng gửi lên server cái event animal-uncheck kèm id của thẻ input mà đã gán vào biến inputCheckedId ở trên.

Tiếp theo là đoạn code lắng nghe sự kiện từ server:

- Listen to an event from server when an animal online

Như phần 4.5 trước, Server có gửi về cho  Client sự kiện animal-online, đoạn code này chúng ta lắng nghe sự kiện đó và nhận được một chuỗi dữ liệu json bao gồm, id + name + color của con vật, mình dùng 3 cái này để tạo ra một thẻ <a> có id có tên và có màu mè của con vật đó xong rồi prepend vào thẻ <div id="animals-online"> đã tạo ở phần 4.3.

- Listen to an event from SERVER when an animal offline

Tương tự như trên, chỉ đơn giản là ngược lại khi con vật offline thì mình remove cái thẻ <a> vừa tạo đi thôi.

- Listen to an event from SERVER when an animal clicks the input box

Lắng nghe sự kiện server-send-check, nhận được một chuỗi json bao gồm id của thẻ input, tên của con vật, màu của con vật. Mình sử dụng 3 dữ liệu trên để đổi màu border thẻ input, tạo ra một thẻ <span> làm tooltip để khi hover qua ô input, tên của con vật sẽ hiển thị lên.

- Listen to an event from SERVER when an animal leaves the input box

Ngược lại ở trên, khi nhận được sự kiện server-send-uncheck từ Server, đơn giản chúng ta đưa css của thẻ <input> về lại ban đầu, và xóa cái thẻ <span> tooltip đi.


4.7. Viết code css cho tooltip:

Để khi hover chuột qua input xuất hiện tooltip được thì mình cần css cho nó một chút, tạo file trungquan.css trong public/css/ và thêm đoạn code này vào:

.td-animal {
    position: relative;
    display: inline-block;
}
.td-animal .tooltiptext {
    visibility: hidden;
    width: 120px;
    background-color: black;
    color: #fff;
    text-align: center;
    border-radius: 6px;
    padding: 5px 0;
    position: absolute;
    z-index: 1;
}
.td-animal:hover .tooltiptext {
    visibility: visible;
}
/**/

4.8. Viết code cho file server chính:

Phù, viết đến đây là nhẹ cả người rồi, còn một bước ngắn nữa, tuy ngắn nhưng rất là quan trọng, đây là file server.js để chạy ứng dụng của chúng ta, nội dung nó như sau:

const express = require('express');
const app = express();
const server = require('http').createServer(app);
 
app.use(express.static('./public'));
app.set('view engine', 'ejs');
app.set('views', './views');
 
var routes = require('./routes/web');
app.use('/', routes);
 
var io = require('./socket/returnAnimals').listen(server);
 
const port = process.env.PORT || 3000;
server.listen(port, () => {
    console.log("Server is running on port: " + port);
});
//

Chắc cũng không cần phải nói gì nhiều ở file này nhỉ, vì mình đã chia nhỏ các module code ra rồi nên nội dung file server.js này gọn gàng nhỏ nhắn rồi ^^, chỉ cần chút kiến thức Nodejs cơ bản là các bạn hiểu ngay thôi.

Tóm gọn là mình khởi tạo server, cấu hình các view engine, router và io xử lý real-time sau đó lắng nghe server chạy trên PORT 3000.


Và đây là kết quả sau khi chạy ứng dụng, mình có deploy cái app này lên Heroku và nhờ một số bạn bè vào để demo thử:


Như vậy mình đã hướng dẫn xây dựng xong ứng dụng “động vật ẩn danh” với Nodejs + Socket.io, bài này khá dài, và cũng là bài dài nhất mà mình viết cho đến thời điểm hiện tại. Bạn nào kiên trì làm được đến đây thật đáng khâm phục đấy ^^

Cảm ơn các bạn đã xem bài viết của mình !!!

Best Regards – Trung Quân – Green Cat


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí