Category Archives: NodeJS

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

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.

NodeJS – Cập nhật dữ liệu thời gian thực với Socket.IO

Công nghệ web thời sơ khai có một nhược điểm hiển thị dữ liệu không nhất quán, tức là sẽ có trường hợp 2 trình duyệt cùng trỏ đến một địa chỉ URL nhưng nội dung trả về lại khác nhau. Ví dụ như có 2 người cùng truy cập một trang wiki, sau đó một trong hai người chỉnh sửa lại trang này, thì sau khi người chỉnh sửa xong bấm lưu trang, chỉ có người đó mới thấy được sự thay đổi, người kia nếu muốn thấy sự thay đổi ngay lúc đó thì phải refresh lại trang đó mới thấy được. Với sự phát triển của công nghệ web ngày nay thì chúng ta đã có thể làm cho web thực hiện các công việc theo thời gian thực (real-time), tức là khi trang web của trình duyệt bên này có sự thay đổi thì lập tức trình duyệt bên kia bằng cách nào có cũng sẽ tự cập nhật thay đổi đó luôn. Ví dụ điển hình nhất là Facebook, mỗi khi có người bấm “like” hoặc bình luận là ngay lập tức bạn sẽ thấy ngay, hoặc chí ít là sẽ có thông báo đến cho bạn.

Trong phần này chúng ta sẽ tìm hiểu cách xây dựng các tính năng real-time như vậy trong ứng dụng Notes, thực ra ngay từ đầu thì mục đích phát minh ra Node là để hỗ trợ các tính năng real-time rồi.

Việc code tính năng real-time cũng sẽ đụng chạm rất nhiều đến phần giao thức mạng, vốn dĩ là thứ mà chúng ta không nên quan tâm, do đó chúng ta sẽ sử dụng thư viện cho nhanh 🙂 Ở đây chúng ta sẽ dùng thư viện Socket.IO, thư viện này đơn giản hóa quá trình tuyền thông giữa trình duyệt và server, hỗ trợ rất nhiều giao thức, ngoài ra còn hỗ trợ cả Internet Explorer tới cả phiên bản 5.5 nữa.

Một thư viện khác thường đi chung với Socket.IO là Backbone.js, đây là một thư viện hỗ trợ xây dựng model nhanh chóng. Tuy nhiên chúng ta sẽ không dùng đến thư viện này vì dữ liệu model của chúng ta rất đơn giản, không phức tạp.

Khởi tạo Socket.IO với Express

Trong các bài trước, chúng ta đã biết là đối tượng http.Server là đối tượng chính thực hiện phần mạng, Express được xây dựng dựa trên đối tượng http.Server, và Socket.IO cũng hoạt động tương tự như vậy.

Đầu tiên chúng ta khai báo trong file package.json:

{
    "name": "notes",
    "version": "0.0.0",
    "private": true,
    "scripts": {
        "start": "node ./bin/www"
    },
    "dependencies": {
        "body-parser": "~1.15.1",
        "cookie-parser": "~1.4.3",
        "debug": "~2.2.0",
        "ejs": "~2.4.1",
        "express": "~4.13.4",
        "morgan": "~1.7.0",
        "serve-favicon": "~2.3.0",
        "async": "*",
        "sqlite3": "*",
        "mongoose": "*",
        "sequelize": "*",
        "connect-flash": "*",
        "passport": "*",
        "passport-local": "*",
        "express-session": "*",
        "workforce": "*",
        "socket.io": "*"
    }
}

Sau đó chạy lệnh npm install để cài module socket.io.

Chỉnh sửa app.js

File app.js được sửa lại như sau

var http = require('http');

var flash = require('connect-flash');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var expressSession = require('express-session');

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var routes = require('./routes/index');
var users = require('./routes/users');
var notes = require('./routes/notes');
//var models = require('./models-fs/notes');
//var models = require('./models-sqlite3/notes');
//var models = require('./models-mongoose/notes');
var models = require('./models-sequelize/notes');
var usersModels = require('./models-sequelize/users');

models.connect(require('./sequelize-params'),
    function(err) {
        if(err)
            throw err;
    });
usersModels.connect(require('./sequelize-params'),
    function(err) {
        if(err)
            throw err;
    });
users.configure({
    users: usersModels,
    passport: passport
});
notes.configure(models);
routes.configure(models);
var app = express();

passport.serializeUser(users.serialize);
passport.deserializeUser(users.deserialize);
passport.use(users.strategy);

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use(expressSession({secret: 'keyboard cat'}));
app.use(flash());
app.use(passport.initialize());
app.use(passport.session());

app.use('/', routes.index);
app.use('/noteview', notes.view);
app.use('/noteadd', users.ensureAuthenticated, notes.add);
app.use('/noteedit', users.ensureAuthenticated, notes.edit);
app.use('/notedestroy', users.ensureAuthenticated, notes.destroy);
app.post('/notedodestroy', users.ensureAuthenticated, notes.dodestroy);
app.post('/notesave', users.ensureAuthenticated, notes.save);
app.use('/account', users.ensureAuthenticated, users.doAccount);
app.use('/login', users.doLogin);
app.post('/doLogin', passport.authenticate('local', {
    failureRedirect: '/login',
    failureFlash: true
}), users.postLogin);
app.use('/logout', users.doLogout);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
            message: err.message,
            error: err
        });
    });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: {}
    });
});


module.exports = app;

var server = http.Server(app);
var io = require('socket.io').listen(server);
app.set('port', 3000);

server.listen(app.get('port'), function() {
    console.log("Express server listening on port " + app.get('port'));
});

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);
    });

});

Chúng ta sẽ chạy trực tiếp server từ file app.js chứ không chạy theo file www trong thư mục bin do Express tự tạo nữa.

var http = require('http');

Chúng ta sẽ cần đến module http.

var server = http.Server(app);
var io = require('socket.io').listen(server);
app.set('port', 3000);
server.listen(app.get('port'), function() {
    console.log("Express server listening on port " + app.get('port'));
});

Tiếp theo chúng ta tạo đối tượn http.Server và đối tượng socket.io, về cơ bản thì đối tượng socket.io sẽ bọc lấy đối tượng server. Sau đó chúng ta thiết lập biến môi trường port là 3000 và cho server lắng nghe trên cổng port.

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);
    });

});

Tiếp theo chúng ta có đoạn code chính như trên, như đã nói, Socket.IO được xây dựng dựa trên lớp EventEmitter, tức là đối tượng này sẽ lắng nghe các sự kiện và trả lời các sự kiện đó. Bắt đầu từ đây mọi thứ sẽ hơi rối rắm một chút xíu.

Đầu tiên chúng ta cho socket lắng nghe sự kiện connection, sự kiện này sẽ được phát ra khi có trình duyệt trỏ tới website. Trong đó, chúng ta cho socket lắng nghe 2 sự kiện là notetitlesdisconnect, cả 2 sự kiện này đều được trình duyệt phát đi, tức là chúng ta sẽ code phần phát sự kiện này trong các file .ejs, nhưng chúng ta không dùng Node mà dùng jQuery. Ngoài ra khi người dùng chỉnh sửa một ghi chú nào đó hoặc xóa một ghi chú thì đối tượng models sẽ phát đi 2 sự kiện là noteupdatednotedeleted, chúng ta sẽ code trong file models-sequelize/notes.js. Cuối cùng sự kiện disconnect được phát đi khi người dùng thoát hẳn khỏi website.

Trong đối tượng models (ở file sequelize-models/notes.js) cũng sẽ có một đối tượng EventEmitter mà chúng ta sẽ định nghĩa sau, đối tượng này được dùng để phát đi sự kiện noteupdatednotedeleted đã nói ở trên, trong file app.js chúng ta lại dùng chính đối tượng emitter đó để lắng nghe 2 sự kiện này, việc xảy ra tiếp theo sẽ là gọi hàm broadcastUpdated() hoặc broadcastDeleted(), 2 hàm này sẽ lại phát sinh 2 sự kiện cùng tên là noteupdatednotedelete bằng đối tượng socket, tuy nhiên 2 sự kiện này sẽ không được bắt bởi đối tượng models mà sẽ được bắt ở trình duyệt vì chúng ta dùng đối tượng socket để gửi đi.

Phát sự kiện từ model

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

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

var Sequelize = require('sequelize');
var Note = undefined;
module.exports.connect = function(params, callback) {
    var sequlz = new Sequelize(
        params.dbname, 
        params.username, 
        params.password,
        params.params
    );
    Note = sequlz.define('Note', {
        notekey: {
            type: Sequelize.STRING,
            primaryKey: true,
            unique: true
        },
        title: Sequelize.STRING,
        body: Sequelize.TEXT
    });
    Note.sync().then(function() {
        callback();
    }).error(function(err) {
        callback(err);
    });
}
exports.disconnect = function(callback) {
    callback();
}

exports.create = function(key, title, body, callback) {
    Note.create({
        notekey: key,
        title: title,
        body: body
    }).then(function(note) {
        exports.emitter.emit('noteupdated', {
        key: key,
        title: title,
        body: body
    });
        callback();
    }).error(function(err) {
        callback(err);
    });
}
exports.update = function(key, title, body, callback) {
    Note.find({ where:{ notekey: key} }).then(function(note) {
        if(!note) {
            callback(new Error("No note found for key " + key));
        } else {
            note.updateAttributes({
                title: title,
                body: body
            }).then(function() { 
                exports.emitter.emit('noteupdated', {
                    key: key,
                    title: title,
                    body: body
                });
                callback();
            }).error(function(err) {
                callback(err);
            });
        }
    }).error(function(err) {
        callback(err);
    });
}

exports.read = function(key, callback) {
    Note.find({ where:{ notekey: key} }).then(function(note) {
        if(!note) {
            callback("Nothing found for " + key);
        } else {
            callback(null, {
                notekey: note.notekey,
                title: note.title,
                body: note.body
            });
        }
    });
}

exports.destroy = function(key, callback) {
    Note.find({ where:{ notekey: key} }).then(function(note) {
        note.destroy().then(function() {
            emitter.emit('notedeleted', key);
            callback();
        }).error(function(err) {
            callback(err);
        });
    });
}

exports.titles = function(callback) { 
    Note.findAll().then(function(notes) { 
        var noteList = []; 
        notes.forEach(function(note) { 
            noteList.push({
                key: note.notekey,
                title: note.title
            }); 
        });
        callback(null, noteList);
    });
}

Chúng ta chỉnh sửa lại để mỗi khi người dùng tạo mới, cập nhật hoặc xóa một ghi chú thì sẽ có các sự kiện phát ra tương ứng.

var events = require('events');
var emitter = module.exports.emitter = new events.EventEmitter();
...
exports.emitter.emit('noteupdated', {
    key: key,
    title: title,
    body: body
});
...
emitter.emit('notedeleted', key);

Việc code cũng khá đơn giản, chúng ta chỉ cần tạo đối tượng EventEmitter, rồi gọi phương thức emit() khi cần là được.

Khởi tạo Socket.IO và jQuery trên trình duyệt

Ở đây ứng dụng của chúng ta cần phải có sự tương tác giữa cả server và client, và Socket.IO đều được viết ra để chạy trên server và client.

Trong file views/top.ejs chúng ta sửa lại 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 = io.connect('/'); 
    </script>
</head>
<body>
    <h1><%= title %></h1>
    <div class='navbar'>
    <p>
        <a href='/'>Home</a> | <a href='/noteadd'>ADD Note</a>
        <% if(user) { %>
        | <a href='/logout'>Log Out</a>
        | logged in as <a href='/account'><%= user.username %></a>
        <% } else { %>
        | <a href='/login'>Log in</a>
        <% } %>
    </p>
    </div>

Chủ yếu là thêm các thẻ <script> tham chiếu đến file socket.io.js, file này sẽ tự động tải về khi chúng ta gọi phương thức io.connect(). Ngoài ra ở đây chúng ta còn cần dùng thêm jQuery nữa để thực hiện việc thay đổi nội dung trên trang web.

Xử lý sự kiện trên client

Đầu tiên chúng ta sửa file views/index.ejs như sau:

<% include top %>
<div id='notetitles'>
<%
     if(notes) {
         for(var i in notes) {
             %><p><%= notes[i].key %>:
             <a href="/noteview?key=<%= notes[i].key %>"><%= notes[i].title %></a>
             </p><% 
         }
     }
%>
</div>

<script>
    $(document).ready(function() { 
        var getnotetitles = function() { 
            socket.emit('notetitles', function(notes) { 
                $('#notetitles').empty();
                for(var i in notes) {
                    var str = '<p>' + notes[i].key + ': <a href="noteview?key=' + notes[i].key + '">' + notes[i].title + '</a>' + '</p>';
                    $('#notetitles').append(str);
                }
            });
        }
 
        socket.on('noteupdated', getnotetitles);
        socket.on('notedeleted', getnotetitles);
    });
</script>
<% include bottom %>

Đoạn code trên sẽ thay đổi nội dung trên trình duyệt khi có thay đổi phát ra.

<div id='notetitles'>
...
</div>

Đầu tiên chúng ta bọc danh sách các ghi chú trong thẻ <div>idnotetitles để có thể dễ dàng tham chiếu tới từ jQuery.

$(document).ready(function() { 
        var getnotetitles = function() { 
            socket.emit('notetitles', function(notes) { 
                $('#notetitles').empty();
                for(var i in notes) {
                    var str = '<p>' + notes[i].key + ': <a href="noteview?key=' + notes[i].key + '">' + notes[i].title + '</a>' + '</p>';
                    $('#notetitles').append(str);
                }
            });
        }
        socket.on('noteupdated', getnotetitles);
        socket.on('notedeleted', getnotetitles);
     });

Tiếp theo chúng ta viết đoạn code lắng nghe sự kiện noteupdatednotedeleted từ server, và ở đây chúng ta xử lý 2 sự kiện này bằng cách phát ra sự kiện notetitles, và server sẽ bắt sự kiện này rồi trả về danh sách các ghi chú mới cho chúng ta, sau đó chúng ta tham chiếu đến thẻ <div> ở trên và cập nhật lại danh sách ghi chú mới này bằng jQuery.

Bây giờ chúng ta sửa lại file views/noteview.ejs như sau:

<% include top %>
<div id="noteview">
<h3 id="notetitle"><%= note ? note.title : "" %></h3>
<p id="notebody"><%- note ? note.body : "" %></p>
<p>Key: <%= notekey %></p>
<% if(user && notekey) { %>
    <hr/>
    <p><a href="/notedestroy?key=<%= notekey %>">Delete</a>
    | <a href="/noteedit?key=<%= notekey %>">Edit</a></p>
<% } %>
</div>

<script>
    $(document).ready(function() {
        var updatenote = function(newnote) {
            $('#notetitle').empty();
            $('#notetitle').append(newnote.title);
            $('#notebody').empty();
            $('#notebody').append(newnote.body);
        }
        socket.on('noteupdated', function(newnote) {
            if(newnote.key === "<%= notekey %>") {
                updatenote(newnote);
            }
        });
        socket.on('notedeleted', function(notekey) {
            if(notekey === "<%= notekey %>") {
                document.location.href = "/";
            }
        });
   });
</script>
<% include bottom %>

Trong ứng dụng Notes chúng ta không xây dựng chức năng phân quyền, tức là ở đây bất cứ ai đăng nhập vào cũng có thể chỉnh sửa một ghi chú bất kỳ, giả sử có 2 người đang cùng thực hiện chỉnh sửa một ghi chú, thì khi một người lưu lại ghi chú đó, chúng ta sẽ cập nhật lại nội dung mới đó trên trang web của người kia luôn. Và để làm việc này thì chúng ta cũng làm tương tự như trong file views/index.ejs

Vậy là xong, bây giờ chúng ta có thể chạy ứng dụng được rồi. Tuy nhiên chúng ta sẽ không dùng lệnh npm start để chạy server của Express mà chúng ta phải dùng lệnh node app.js để chạy server do chúng ta cấu hình.

capture

Để kiểm tra thì bạn có thể mở 2 trình duyệt khác nhau như Chrome và Firefox rồi cùng trỏ đến website, sau đó một bên chỉnh sửa một ghi chú, thì bên kia cũng sẽ tự động cập nhật lại ghi chú mới.

NodeJS – Tăng hiệu suất server – Phần 2

Chúng ta đều biết rằng engine V8 là một engine đơn luồng (single-thread), tức là ở đây không thể chạy song song nhiều công việc cùng một lúc được, cơ chế bất đồng bộ (Asynchronous) của Node chẳng qua cũng chỉ là để chống server bị blocked thôi chứ thực chất các công việc vấn theo thứ tự trước-sau. Trong bài Tăng hiệu suất server chúng ta cũng đã tìm hiểu một cách để chạy nhiều server đó là tạo nhiều instance chạy trên nhiều cổng khác nhau, tuy nhiên như thế sẽ rất bất tiện và đem lại trải nghiệm không tốt cho người dùng, chẳng ai lại muốn gõ địa chỉ website rồi thêm số cổng đằng sau cả.

Do đó trong phần này chúng ta sẽ tìm hiểu một cách khác để tăng hiệu suất của server là chạy nhiều tiến trình để tận dụng các core của CPU. Kể từ phiên bản Node 0.8 trở lên, Node cho ra đời module cluster cho phép lập trình viên “tạo nhiều tiến trình mạng sử dụng chung cổng”, các tiến trình này đều nằm trên một máy, module này không cho phép chúng ta thao tác với cùng tiến trình trên các máy khác.

Module cluster cung cấp các hàm API ở cấp độ rất thấp, do đó bạn có thể làm rất nhiều trò với CPU, và cũng vì thế mà module này rất khó dùng nếu bạn không có nhiều kiến thức về cách hệ điều hành quản lý tiến trình, bộ nhớ… Tuy nhiên trên thực tế thì chúng ta cũng không nên quan tâm đến các thứ cấp thấp đó mà chỉ nên tập trung phát triển các tính năng của website, do đó chúng ta sẽ sử dụng các module ở cấp cao hơn để đơn giản hóa việc sử dụng, ở đây chúng ta sẽ dùng module workforce .

Cài đặt

Đầu tiên chúng ta khai báo module này trong file package.json:

{
    "name": "notes",
    "version": "0.0.0",
    "private": true,
    "scripts": {
    "start": "node ./bin/www"
 },
    "dependencies": {
        "body-parser": "~1.15.1",
        "cookie-parser": "~1.4.3",
        "debug": "~2.2.0",
        "ejs": "~2.4.1",
        "express": "~4.13.4",
        "morgan": "~1.7.0",
        "serve-favicon": "~2.3.0",
        "async": "*",
        "sqlite3": "*",
        "mongoose": "*",
        "sequelize": "*",
        "connect-flash": "*",
        "passport": "*",
        "passport-local": "*",
        "express-session": "*",
        "workforce": "*"
    }
}

Sau đó chạy lệnh npm install để cài đặt.

Tiếp theo chúng ta tạo file workforce.js trong thư mục gốc của project với đoạn code sau:

var workforce = require('workforce');
var manager = workforce('./app.js');
manager.set('workers', 4);
manager.set('title', 'Notes');
manager.set('restart threshold', '10s');
manager.set('exit timeout', '5s');
manager.listen(process.env.PORT || 3000);

Trong đoạn code trên chúng ta tạo một đối tượng workforce và thiết lập một số thông số cần thiết. Trong đó:

  • workers là số tiến trình tối đa được mở, thường thì con số này nên bằng số core của CPU
  • title sẽ được thêm trước vào tên mỗi tiến trình được tạo ra
  • restart threshold là thời gian một tiến trình được phép tồn tại, sau khi hết thời gian đó thì workforce sẽ hủy tiến trình này và tạo lại một tiến trình mới thay thế
  • exit timeout là thời gian chờ sau khi có lệnh hủy một tiến trình

Trong file app.js, ở cuối file có dòng:

module.exports = app;

Dòng này sẽ cho phép đối tượng app có thể được gọi từ các module khác. Nếu của bạn không có thì bạn thêm dòng này vào.

Vậy là xong, bây giờ chúng ta có thể chạy nhiều tiến trình server được rồi, và chúng ta sẽ không dùng lệnh npm start nữa mà dùng lệnh:

C:\NodeJS\notes>node workforce.js

Bạn có thể mở Task Manager trên Windows để xác nhận.

capture

Sở dĩ ở đây có 5 tiến trình là vì tiến trình đầu tiên là tiến trình chạy file workfoce.js, từ tiến trình này 4 tiến trình app.js được tạo ra.

NodeJS – Xác thực người dùng với PassportJS

Trong phần này chúng ta sẽ xây dựng tính năng xác thực user.

HTTP là một giao thức vô trạng thái, nghĩa là chúng ta không thể biết user đang lướt web đó có đăng nhập hay không, hay thậm chí chúng ta cũng không biết có đúng là hành động lướt web đó có do con người làm hay không.

Do đó cách xác thực thông thường đối với các ứng dụng sử dụng giao thức HTTP là gửi một đoạn token (một chuỗi id) vào cookie của trình duyệt. Chuỗi token sẽ được dùng để xác định xem người dùng ở trình duyệt đó có đang đăng nhập hay không. Và cứ mỗi lần trình duyệt truy cập đến website thì ngoài các thông tin bình thường, trình duyệt sẽ phải gửi cả chuỗi token đó, và chúng ta sẽ biết được là user nào đang đăng nhập vào với trình duyệt đó.

Node có khá nhiều module hỗ trợ xác thực user thông qua cookie, trong đó 2 module Passport (http://passportjs.org) và Everyauth (http://everyauth.com) là phổ biến nhất. Ở đây chúng ta sẽ dùng module Passport.

Cài module

Ngoài module chính là passport thì chúng ta sẽ cần thêm một số module khác nữa bao gồm connect-flash, passport-local và express-session, đầu tiên chúng ta khai báo các module này trong file package.json:

{
    "name": "notes",
    "version": "0.0.0",
    "private": true,
    "scripts": {
    "start": "node ./bin/www"
    },
    "dependencies": {
        "body-parser": "~1.15.1",
        "cookie-parser": "~1.4.3",
        "debug": "~2.2.0",
        "ejs": "~2.4.1",
        "express": "~4.13.4",
        "morgan": "~1.7.0",
        "serve-favicon": "~2.3.0",
        "async": "*",
        "sqlite3": "*",
        "mongoose": "*",
        "sequelize": "*",
        "connect-flash": "*",
        "passport": "*",
        "passport-local": "*",
        "express-session": "*"
    }
}

Sau đó chạy lệnh npm install để cài các module này vào project.

Ở đây module connect-flash có chức năng hỗ trợ hiển thị thông báo, passport là module chính dùng để xác thực, passport-local là module con trong module passport có chức năng xác thực bằng dữ liệu cục bộ (khác với xác thực thông qua các dịch vụ trung gian như Facebook, Twitter…), express-session hỗ trợ lưu trữ các chuỗi token thông qua session. Chúng ta sẽ lần lượt tìm hiểu các module này kỹ hơn.

Cấu hình app.js

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

var flash = require('connect-flash');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var expressSession = require('express-session');

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var routes = require('./routes/index');
var users = require('./routes/users');
var notes = require('./routes/notes');
//var models = require('./models-fs/notes');
//var models = require('./models-sqlite3/notes');
//var models = require('./models-mongoose/notes');
var models = require('./models-sequelize/notes');
var usersModels = require('./models-sequelize/users');

models.connect(require('./sequelize-params'),
    function(err) {
        if(err)
            throw err;
    });
usersModels.connect(require('./sequelize-params'),
    function(err) {
        if(err)
            throw err;
    });
users.configure({
    users: usersModels,
    passport: passport
});
notes.configure(models);
routes.configure(models);
var app = express();

passport.serializeUser(users.serialize);
passport.deserializeUser(users.deserialize);
passport.use(users.strategy);

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use(expressSession({secret: 'keyboard cat'}));
app.use(flash());
app.use(passport.initialize());
app.use(passport.session());

app.use('/', routes.index);
app.use('/noteview', notes.view);
app.use('/noteadd', users.ensureAuthenticated, notes.add);
app.use('/noteedit', users.ensureAuthenticated, notes.edit);
app.use('/notedestroy', users.ensureAuthenticated, notes.destroy);
app.post('/notedodestroy', users.ensureAuthenticated, notes.dodestroy);
app.post('/notesave', users.ensureAuthenticated, notes.save);
app.use('/account', users.ensureAuthenticated, users.doAccount);
app.use('/login', users.doLogin);
app.post('/doLogin', passport.authenticate('local', {
    failureRedirect: '/login',
    failureFlash: true
}), users.postLogin);
app.use('/logout', users.doLogout);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
            message: err.message,
            error: err
        });
    });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: {}
    });
});


module.exports = app;

Chúng ta sẽ thêm và sửa khá nhiều thứ.

var flash = require('connect-flash');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var expressSession = require('express-session');

Đầu tiên là 4 dòng require() mới.

passport.serializeUser(users.serialize);
passport.deserializeUser(users.deserialize);
passport.use(users.strategy);

Ba dòng trên dùng để cấu hình module passport, trong đó serializeUser là thiết lập token cho user, deserializeUser là hủy token của user, passport.use(users.strategy) là thiết lập Strategy cho passport. Module passport gọi các cơ chế xác thực là các strategy, chẳng hạn trong phần này chúng ta dùng cách xác thực dữ liệu cục bộ thì gọi là “local strategy”. Cả 3 phương thức trên đều nhận vào tham số là một hàm, chúng ta sẽ định nghĩa các hàm tương ứng đó sau.

app.use(expressSession({secret: 'keyboard cat'}));
app.use(flash());
app.use(passport.initialize());
app.use(passport.session());

Passport sẽ lưu các token vào các biến session và dữ liệu session sẽ được lưu vào cookie. Ở đây chúng ta dùng module express-session. Hàm flash() sẽ khởi tạo module connect-flash, module này chỉ đơn giản là một module hỗ trợ hiển thị các câu thông báo. Phương thức passport.initialize() sẽ khởi tạo module passport, phương thức passport.session() sẽ bật tính năng sử dụng session, lưu ý là chúng ta phải khởi tạo express-session trước rồi mới sử dụng session trong passport.

var models = require('./models-sequelize/notes');
var usersModels = require('./models-sequelize/users');

models.connect(require('./sequelize-params'),
    function(err) {
        if(err)
            throw err;
    });
usersModels.connect(require('./sequelize-params'),
    function(err) {
        if(err)
            throw err;
    });
users.configure({
    users: usersModels,
    passport: passport
});
notes.configure(models);
routes.configure(models);

Bắt đầu từ đây chúng ta không truyền các thông tin về cơ sở dữ liệu trực tiếp nữa mà thay vào đó sẽ lưu trong một module riêng, và khi cần thì chúng ta tham chiếu đến module đó, ở đây là file sequelize-params.js được tạo trong thư mục gốc của project, chúng ta sẽ tạo file này sau. Ngoài ra chúng ta cũng sẽ tạo một file users.js dùng để lưu thông tin của user, file này cũng chứa các thông tin và hàm dùng để kết nối CSDL như file model notes.js, chúng ta cũng sẽ tạo file này sau.

app.use('/', routes.index);
app.use('/noteview', notes.view);
app.use('/noteadd', users.ensureAuthenticated, notes.add);
app.use('/noteedit', users.ensureAuthenticated, notes.edit);
app.use('/notedestroy', users.ensureAuthenticated, notes.destroy);
app.post('/notedodestroy', users.ensureAuthenticated, notes.dodestroy);
app.post('/notesave', users.ensureAuthenticated, notes.save);
app.use('/account', users.ensureAuthenticated, users.doAccount);
app.use('/login', users.doLogin);
app.post('/doLogin', passport.authenticate('local', {
    failureRedirect: '/login',
    failureFlash: true
}), users.postLogin);
app.use('/logout', users.doLogout);

Cuối cùng chúng ta thêm và sửa lại phần routing, trong đó có một số đường dẫn routing sẽ cần thêm một hàm dùng để xác nhận xem người dùng có đang đăng nhập hay không bằng hàm users.ensureAuthenticated. Đường dẫn /login sẽ được dùng để thực hiện xác thực người dùng, trong đó chúng ta gọi phương thức passport.authenticate('local'...), phương thức này sẽ gọi đến phương thức kiểm tra mà chúng ta sẽ viết thêm ở dưới.

Như đã nói ở trên, chúng ta tạo file sequelize-params.js dùng để lưu các thông tin về cơ sở dữ liệu như sau:

module.exports = {
    dbname: "notes",
        username: "<username>",
        password: "<mật khẩu>",
        params: {
            host: "127.0.0.1",
            dialect: "mysql"
        }
};

Bạn thay username và mật khẩu tương ứng với CSDL của mình.

Tạo users model

Trong thư mục models-sequelize, chúng ta tạo file users.js có nội dung như sau:

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);
    });
}

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);
        });
    });
}

File này đại diện cho phần model của user, tất cả đều tương tự như file notes.js trong cùng thư mục. Hàm connect() dùng để tạo bảng, các hàm read(), create(), update(), destroy(), titles() được dùng để xem, sửa, xóa, tạo mới một bản ghi người dùng trong cơ sở dữ liệu.

Routing

Trong thư mục routes, chúng ta tạo một file có tên users.js, khi tạo project thì express cũng có tạo một file như vậy nhưng cũng không có gì nhiều trong đó, nếu bạn có file đó thì khỏi tạo, trong file này chúng ta có thêm code như sau:

var LocalStrategy = require('passport-local').Strategy;
var users = undefined;
var passport = undefined;

exports.configure = function(params) {
    users = params.users;
    passport = params.passport;
}

module.exports.serialize = function(user, done) {
    done(null, user.id);
}

module.exports.deserialize = function(id, done) {
    users.findById(id, function(err, user) {
        done(err, user);
    });
}

module.exports.strategy = new LocalStrategy(
    function(username, password, done) {
        process.nextTick(function() {
            users.findByUsername(username, function(err, user) {
                if(err)
                    return done(err);
                if(!user) {
                    return done(null, false, {
                        message: 'Unknown user ' + username
                    });
                }
                if(user.password !== password) {
                    return done(null, false, {
                        message: 'Invalid password'
                    });
                }
                return done(null, user);
            });
        });
    }
);

module.exports.ensureAuthenticated = function(req, res, next) {
    if(req.isAuthenticated())
        return next();
    return res.redirect('/login');
}

module.exports.doAccount = function(req, res) {
    res.render('account', {
        title: 'Account information for ' + req.user.username,
        user: req.user
     });
}

module.exports.doLogin = function(req, res) {
    res.render('login', {
        title: 'Login to Note',
        user: req.user,
        message: req.flash('error')
    });
}

module.exports.postLogin = function(req, res) {
     res.redirect('/');
}

module.exports.doLogout = function(req, res) {
     req.logout();
     res.redirect('/');
}

Đoạn code trên xử lý phần định tuyến cho các url mới như /login, /account, /doLogin.

var LocalStrategy = require('passport-local').Strategy;
var users = undefined;
var passport = undefined;

exports.configure = function(params) {
    users = params.users;
    passport = params.passport;
}

Hàm configure() có chức năng tương tự như trong file routes/notes.js, đó là nhận thông tin về user hiện tại, và module passport hiện được dùng để xác thực.

module.exports.serialize = function(user, done) {
    done(null, user.id);
}

module.exports.deserialize = function(id, done) {
    users.findById(id, function(err, user) {
        done(err, user);
    });
}

Như đã nói ở trên, hàm serialize() sẽ tạo chuỗi token, ở đây chúng ta chỉ đơn giản là dùng chính id của người dùng để làm token. Hàm deserialize() sẽ hủy chuỗi token đó.

module.exports.strategy = new LocalStrategy(
    function(username, password, done) {
        process.nextTick(function() {
            users.findByUsername(username, function(err, user) {
                if(err)
                    return done(err);
                if(!user) {
                    return done(null, false, {
                        message: 'Unknown user ' + username
                    });
                }
                if(user.password !== password) {
                    return done(null, false, {
                        message: 'Invalid password'
                    });
                }
                return done(null, user);
            });
        });
    }
);

Tiếp theo chúng ta khởi tạo một đối tượng lớp LocalStrategy, khi xác thực bằng cách gọi hàm passport.authenticate() trong file app.js thì hàm trong đối tượng LocalStrategy này sẽ được gọi để thực hiện kiểm tra username và mật khẩu, ở đây chúng ta cũng kiểm tra đơn giản, chỉ là xem username có tồn tại hay không, nếu có thì kiểm tra xem password có trùng hay không, nếu tất cả đều hợp lệ thì trả về dữ liệu của user đó, nếu có vấn đề gì thì chúng ta trả về lỗi. Trên thực tế chúng ta sẽ cần làm nhiều thứ hơn như mã hóa mật khẩu, dùng salt…v.v Ngoài ra ở đây hàm process.nextTick() sẽ làm các công việc trên theo hướng bất đồng bộ.

module.exports.ensureAuthenticated = function(req, res, next) {
    if(req.isAuthenticated())
        return next();
    return res.redirect('/login');
}

Tiếp theo hàm ensureAuthenticated() là hàm kiểm tra xem người dùng có đang đăng nhập hay không khi lướt qua các trang khác, chúng ta chỉ cần gọi hàm req.isAuthenticated() là đủ, đây là hàm do module passport cung cấp.

module.exports.doAccount = function(req, res) {
    res.render('account', {
        title: 'Account information for ' + req.user.username,
        user: req.user
     });
}

Hàm doAccount() sẽ xử lý đường dẫn /account và hiển thị thông tin về user.

module.exports.doLogin = function(req, res) {
    res.render('login', {
        title: 'Login to Note',
        user: req.user,
        message: req.flash('error')
    });
}

module.exports.postLogin = function(req, res) {
     res.redirect('/');
}

Hàm doLogin() sẽ hiển thị form đăng nhập cho người dùng, hàm req.flash() sẽ hiển thị thông báo nếu người dùng đưang nhập sai. Hàm postLogin() sẽ chuyển hướng về trang '/' khi người dùng đăng nhập thành công.

module.exports.doLogout = function(req, res) {
     req.logout();
     res.redirect('/');
}

Hàm doLogout() sẽ xóa thông tin đăng nhập của người dùng, hàm req.logout() sẽ thực hiện xóa các thông tin đó, hàm này do passport thêm vào.

Sửa view

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

<html>
<head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
    <h1><%= title %></h1>
    <div class='navbar'>
        <p>
        <a href='/'>Home</a> | <a href='/noteadd'>ADD Note</a>
        <% if(user) { %>
        | <a href='/logout'>Log Out</a>
        | logged in as <a href='/account'><%= user.username %></a>
        <% } else { %>
        | <a href='/login'>Log in</a>
        <% } %>
    </p>
 </div>

Bây giờ website sẽ hiển thị thêm đường dẫn để đăng nhập, hoặc thông tin người dùng đã đăng nhập.

Tiếp theo chúng ta tạo file account.ejs trong thư mục views như sau:

<% include top %>
<p>Name: <%= user.username %> (<%= user.id %>)</p>
<p>E-Mail: <%= user.email %></p>
<% include bottom %>

File này sẽ được dùng để hiển thị thông tin chi tiết về người dùng.

Cuối cùng chúng ta tạo file login.ejs trong thư mục views như sau:

<% include top %>
<form method="POST" action="/doLogin">
    <p>User name: <input type='text' name='username' /></p>
    <p>Password: <input type='text' name='password' /></p>
    <p><input type='submit' value='Submit' /></p>
</form>
<% include bottom %>

File này sẽ hiển thị form đăng nhập cho người dùng. Khi người dùng đăng nhập thì form sẽ gửi một yêu cầu đến đường dẫn /doLogin với phương thức là POST.

Sửa routing

Chúng ta sửa lại file notes.js trong thư mục routes như sau:

var notes = undefined;
exports.configure = function(params) {
    notes = params;
}

var readNote = function(key, user, res, done) {
    notes.read(key, function(err, data) {
        if(err) {
            res.render('showerror', {
                title: "Could not read note " + key,
                error: err,
                user: user ? user : undefined
            });
            done(err);
        } else
            done(null, data);
    });
}

exports.view = function(req, res, next) { 
    if(req.query.key) {
        readNote(req.query.key, req.user, res, function(err, data) {
            if(!err) {
                res.render('noteview', {
                    title: data.title, 
                    notekey: req.query.key,
                    note: data,
                    user: req.user ? req.user : undefined
                });
            }
        });
    } else {
        res.render('showerror', {
            title: "No key given for Note", 
            error: "Must provide a Key to view a Note",
            user: req.user ? req.user : undefined
        });
    }
}

exports.save = function(req, res, next) { 
    ((req.body.docreate === "create") ? notes.create : notes.update)
    (req.body.notekey, req.body.title, req.body.body,
        function(err) { 
            if(err) {
                res.render('showerror', {
                    title: "Could not update file",
                    error: err,
                    user: req.user ? req.user : undefined
            });
        } else {
            res.redirect('/noteview?key='+req.body.notekey);
        }
    });
}
exports.add = function(req, res, next) {
    res.render('noteedit', {
        title: "Add a Note",
        docreate: true,
        notekey: "",
        note: undefined,
        user: req.user ? req.user : undefined
    });
}

exports.edit = function(req, res, next) {
    if(req.query.key) {
        readNote(req.query.key, req.user, res, function(err, data) {
            if(!err) {
                res.render('noteedit', {
                    title: data ? ("Edit " + data.title) : "Add a Note", 
                    docreate: false,
                    notekey: req.query.key,
                    note: data,
                    user : req.user ? req.user : undefined
                });
            }
        });
    } else {
        res.render('showerror', {
             title: "No key given for Note",
             error: "Must provide a Key to view a Note",
             user : req.user ? req.user : undefined
         });
     }
}

exports.destroy = function(req, res, next) {
    if(req.query.key) {
        readNote(req.query.key, req.user, res, function(err, data) {
            if(!err) {
                res.render('notedestroy', {
                    title: data.title,
                    notekey: req.query.key,
                    note: data,
                    user: req.user ? req.user : undefined
                });
            }
        });
    } else {
        res.render('showerror', {
            title: "No key given for Note", 
            error: "Must provide a Key to view a note",
            user: req.user ? req.user : undefined
        });
    }
}

exports.dodestroy = function(req, res, next) {
    notes.destroy(req.body.notekey, function(err) {
        if(err) {
            res.render('showerror', {
                title: "Could not delete Note " + req.body.notekey,
                error: err
            });
        } else {
           res.redirect('/');
        }
    });
}

Ở đây chúng ta thêm vào đối tượng user để mỗi lần hiển thị lên trình duyệt thì không bị lỗi undefined.

Tạo user

Vậy là mọi thứ đã hoàn tất, bây giờ trước khi chạy thử thì chúng ta phải có tài khoản để sử dụng, do ở đây chúng ta không thực hiện chức năng đăng ký tài khoản nên chúng ta phải làm bằng tay. Để tiện thì trong thư mục models-sequelize, chúng ta tạo một file có tên setup.js với nội dung như sau:

var users = require('./users');
users.connect(require('../sequelize-params'),
    function(err) {
        if(err)
            throw err;
        else {
             users.create('1', 
                         'phocode', 
                         '123', 
                         'admin@phocode.com',
                         function(err) {
                             if(err)
                                 throw err; 
                         });
        }
    });

Đoạn code trên sẽ tạo tài khoản và lưu vào CSDL. Để chạy thì bạn mở cmd lên trong thư mục models-sequelize rồi chạy lệnh node setup.js là được.

Vậy là xong, bây giờ chúng ta có thể chạy ứng dụng và sử dụng chức năng đăng nhập được rồi.

capture

NodeJS – Lưu trữ dữ liệu trên MySQL với Sequelize

Ngoài SQLite ra thì còn có rất nhiều các hệ quản trị cơ sở dữ liệu nổi tiếng và mạnh mẽ, và ứng với mỗi hệ quản trị CSDL thì lại có vài module được dùng để tương tác với CSDL đó từ Node. Trong số đó có một số module cung cấp các hàm ở level cao và cung cấp khả năng sử dụng chỉ một model cho nhiều CSDL khác nhau.

Module Sequelize (http://sequelize.com/) có thể kết nối tới 3 hệ quản trị cơ sở dữ liệu là SQLite3, MySQL và PostgreSQL. Các thao tác với CSDL sẽ được thực hiện theo ORM (Object Relation Mapping).

MySQL

MySQL là một hệ quản trị cơ sở dữ liệu SQL rất mạnh và miễn phí. Nếu bạn đã cài MySQL trong máy rồi thì bạn chỉ cần tạo một CSDL có tên là notes (không tạo bảng) rồi có thể bỏ qua phần này và kéo xuống phần tạo model phía dưới. Nếu chưa thì bạn lên trang chủ của MySQL và tải bản Community về tại địa chỉ http://dev.mysql.com/downloads/. MySQL là một hệ quản trị cơ sở dữ liệu, phần lõi chịu trách nhiệm lưu trữ, đọc/ghi dữ liệu… tất cả đều được thực hiện qua dòng lệnh, do đó nếu muốn bạn có thể tải thêm phần mềm MySQL Workbench về, đây là phần mềm cho phép bạn làm việc với MySQL thông qua giao diện GUI, nghĩa là thay vì dùng dòng lệnh thì bạn chỉ cần thực hiện các thao tác click chuột đơn giản.

capture

Quá trình cài đặt MySQL rất đơn giản, bạn chỉ cần lưu ý một số thứ như tên tải khoản truy cập, thường là root, mật khẩu, chế độ cài (Developement, Deploy…)…

Sau khi đã cài đặt xong, chúng ta có thẻ bắt đầu truy cập server của MySQL để tạo CSDL. Ở đây mình dùng MySQL Workbench cho nhanh. Khi mở MySQL Workbench lên, bạn sẽ phải tạo những cái gọi là Connections, nếu bạn chưa từng làm việc với Connection thì có thể hình dung đây giống như là các nút để chúng ta thực hiện kết nối tới server vậy. Để tạo một connection thì chúng ta click vào nút dấu + phía trên bên trái màn hình, sau đó điền các thông tin như tên connection, host là 127.0.0.1, port là 3306, username là tên đăng nhập mà bạn tạo khi cài MySQL, vậy là xong.

capture

Sau đó bạn click Ok để tạo, bây giờ bên màn hình sẽ xuất hiện một nút Connection và chúng ta có thể click vào để đăng nhập vào server. Khi đăng nhập thì MySQL Workbench sẽ hỏi password, bạn nhập password lúc cài đặt MySQL là xong.

capture

Tiếp theo chúng ta phải tạo một CSDL để lưu các ghi chú cho ứng dụng Notes. Bạn click vào nút có hình trụ với dấu + để tạo, trong này chúng ta cần cung cấp nên CSDL và collation là tập kí tự, chúng ta sẽ chọn là uttf8_unicode_ciỞ đây chúng ta không tạo bảng vì module Sequelize sẽ tự động tạo các bảng đó cho chúng ta. Vậy là quá trình tạo CSDL đã xong, bây giờ chúng ta sẽ tạo mdel.

capture

Tạo model

Chúng ta tạo một thư mục mới có tên models-sequelize trong thư mục gốc của project, trong thư mục này chúng ta tạo một file có tên notes.js với nội dung như sau:

var Sequelize = require('sequelize');
var Note = undefined;
module.exports.connect = function(params, callback) {
    var sequlz = new Sequelize(
        params.dbname, 
        params.username, 
        params.password,
        params.params
    );
    Note = sequlz.define('Note', {
        notekey: {
            type: Sequelize.STRING,
            primaryKey: true,
            unique: true
        },
        title: Sequelize.STRING,
        body: Sequelize.TEXT
    });
    Note.sync().then(function() {
        callback();
    }).error(function(err) {
        callback(err);
    });
}
exports.disconnect = function(callback) {
    callback();
}

exports.create = function(key, title, body, callback) {
    Note.create({
            notekey: key,
            title: title,
            body: body
        }).then(function(note) {
            callback();
        }).error(function(err) {
            callback(err);
    });
}

exports.update = function(key, title, body, callback) {
    Note.find({ where:{ notekey: key} }).then(function(note) {
        if(!note) {
            callback(new Error("No note found for key " + key));
        } else {
            note.updateAttributes({
                title: title,
                body: body
            }).then(function() { 
                callback();
            }).error(function(err) {
                callback(err);
            });
        }
    }).error(function(err) {
        callback(err);
    });
}

exports.read = function(key, callback) {
    Note.find({ where:{ notekey: key} }).then(function(note) {
        if(!note) {
            callback("Nothing found for " + key);
        } else {
            callback(null, {
                notekey: note.notekey,
                title: note.title,
                body: note.body
            });
        }
    });
}

exports.destroy = function(key, callback) {
    Note.find({ where:{ notekey: key} }).then(function(note) {
        note.destroy().then(function() {
        callback();
    }).error(function(err) {
        callback(err);
    });
    });
}

exports.titles = function(callback) { 
    Note.findAll().then(function(notes) { 
        var noteList = []; 
        notes.forEach(function(note) { 
            noteList.push({
                key: note.notekey,
                title: note.title
            }); 
        });
        callback(null, noteList);
    });
}

Cũng giống như các phần trước, file này sẽ lưu trữ phần model của ứng dụng.

var Sequelize = require('sequelize');

Chúng ta sẽ cần dùng đến module sequelize.

var Note = undefined;
module.exports.connect = function(params, callback) {
    var sequlz = new Sequelize(
        params.dbname, 
        params.username, 
        params.password,
        params.params
    );
    Note = sequlz.define('Note', {
        notekey: {
            type: Sequelize.STRING,
            primaryKey: true,
            unique: true
        },
        title: Sequelize.STRING,
        body: Sequelize.TEXT
    });
    Note.sync().then(function() {
        callback();
    }).error(function(err) {
        callback(err);
    });
}
exports.disconnect = function(callback) {
    callback();
}

Ở đây hàm connect() không nhận vào một chuỗi thông thường như các phần trước mà sẽ nhận vào một tập các tham số, gồm có tên CSDL (dbname), tên đăng nhập (username), mật khẩu (password), địa chỉ MySQL server và tên loại server (nằm trong tham số params).

Như đã nói ở trên, chúng ta không tạo bảng một cách trực tiếp mà sequelize sẽ tạo các bảng đó cho chúng ta, bằng cách dùng phương thức define(), tham số đầu tiên là tên bảng, ở đây mình đặt là Note, sequelize sẽ tạo bảng với tên tương ứng và thêm kí tự vào sau cùng, tham số thứ hai là một đối tượng chứa các trường tương ứng trong bảng, ở đây bao gồm các trường notekey, titlebody giống như với các model khác, ngoài ra sequelize sẽ tự động chèn thêm 2 trường nữa là createdAtupdatedAt có kiểu dữ liệu datetime để chúng ta có thể biết được các bản ghi được tạo khi nào và được sửa đổi khi nào. Phương thức sync().then() sẽ thực hiện việc tạo thật sự trên CSDL.

exports.create = function(key, title, body, callback) {
    Note.create({
            notekey: key,
            title: title,
            body: body
        }).then(function(note) {
            callback();
        }).error(function(err) {
            callback(err);
    });
}

Hàm create() sẽ thực hiện tạo các bản ghi mới. Để chèn một bản ghi thì chúng ta chỉ cần gọi phương thức Sequelize.create() và đưa các tham số tương ứng vào, sau đó gọi hàm then(), ngoài ra hàm còn có hàm error() có chức năng kiểm tra xem có lỗi hay không.

exports.update = function(key, title, body, callback) {
    Note.find({ where:{ notekey: key} }).then(function(note) {
        if(!note) {
            callback(new Error("No note found for key " + key));
        } else {
            note.updateAttributes({
                title: title,
                body: body
            }).then(function() { 
                callback();
            }).error(function(err) {
                callback(err);
            });
        }
    }).error(function(err) {
        callback(err);
    });
}

Hàm update() sẽ cập nhật các ghi chú. Ở đây thao tác cập nhật hơi khác chút xíu. Đầu tiên chúng ta phải tìm bản ghi đó bằng phương thức find(), phương thức này nhận vào câu truy vấn, bạn có thể xem cấu trúc truy vấn theo đoạn code trên. Sau đó gọi hàm then(), hàm này nhận vào một hàm callback và hàm callback này nhận vào dữ liệu trả về. Chúng ta có thể kiểm tra xem dữ liệu trả về này có rỗng hay không, nếu không rỗng thì chúng ta thực hiện cập nhật mới dữ liệu này bằng cách gọi hàm updateAttributes().

exports.read = function(key, callback) {
    Note.find({ where:{ notekey: key} }).then(function(note) {
        if(!note) {
            callback("Nothing found for " + key);
        } else {
            callback(null, {
                notekey: note.notekey,
                title: note.title,
                body: note.body
            });
        }
    });
}

Hàm read() được dùng để đọc một ghi chú và cũng tương tự như hàm update().

exports.destroy = function(key, callback) {
    Note.find({ where:{ notekey: key} }).then(function(note) {
        note.destroy().then(function() {
        callback();
    }).error(function(err) {
        callback(err);
    });
    });
}

Hàm destroy() được dùng để xóa một ghi chú, tương tự chúng ta cũng tìm một ghi chú trước rồi nếu thấy thì chúng ta xóa ghi chú đó bằng hàm destroy().

exports.titles = function(callback) { 
    Note.findAll().then(function(notes) { 
        var noteList = []; 
        notes.forEach(function(note) { 
            noteList.push({
                key: note.notekey,
                title: note.title
            }); 
        });
        callback(null, noteList);
    });
}

Hàm titles() sẽ liệt kê toàn bộ ghi chú có trong CSDL, để lấy về toàn bộ ghi chú thì chúng ta dùng hàm findAll().

Cấu hình app.js

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

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var routes = require('./routes/index');
var users = require('./routes/users');
var notes = require('./routes/notes');
//var models = require('./models-fs/notes');
//var models = require('./models-sqlite3/notes');
//var models = require('./models-mongoose/notes');
var models = require('./models-sequelize/notes');
models.connect({
    dbname: "notes",
    username: "root",
    password: "<mật khẩu>",
    params: {
        host: "127.0.0.1",
        dialect: "mysql"
    }
},
function(err) {
    if(err)
        throw err;
});
notes.configure(models);
routes.configure(models);
var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', routes.index);
app.use('/users', users);
app.get('/noteadd', notes.add);
app.post('/notesave', notes.save);
app.use('/noteview', notes.view);
app.use('/noteedit', notes.edit);
app.use('/notedestroy', notes.destroy);
app.post('/notedodestroy', notes.dodestroy);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
            message: err.message,
            error: err
        });
    });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: {}
    });
});


module.exports = app;

Ở đây chúng ta lưu ý là tham số để kết nối tới CSDL là một tập các tham số như địa chỉ, username, mật khẩu đăng nhập… bạn điền cho chính xác là được.

Tiếp theo chúng ta khai báo module sequelize trong file package.json như sau:

{
    "name": "notes",
    "version": "0.0.0",
    "private": true,
    "scripts": {
    "start": "node ./bin/www"
    },
    "dependencies": {
        "body-parser": "~1.15.1",
        "cookie-parser": "~1.4.3",
        "debug": "~2.2.0",
        "ejs": "~2.4.1",
        "express": "~4.13.4",
        "morgan": "~1.7.0",
        "serve-favicon": "~2.3.0",
        "async": "*",
        "sqlite3": "*",
        "mongoose": "*",
        "sequelize": "*"
    }
}

Cuối cùng chúng ta chạy lệnh npm install để cài module này rồi chạy npm start để chạy server là xong.

Cứ mỗi lần chạy server, sequelize sẽ tạo mới bảng nếu bảng chưa tồn tại.

C:\NodeJS\notes>npm start

> notes@0.0.0 start C:\NodeJS\notes
> node ./bin/www

  notes:server Listening on port 3000 + 0ms
Executing (default): CREATE TABLE IF NOT EXISTS 'Notes' ('notekey' VARCHAR(255)
UNIQUE , 'title' VARCHAR(255), 'body' TEXT, 'createdAt' DATETIME NOT NULL, 'updatedAt'
DATETIME NOT NULL, UNIQUE 'Notes_notekey_unique' ('notekey'), PRIMARY KEY ('notekey')) 
ENGINE=InnoDB;
Executing (default): SHOW INDEX FROM 'Notes'

Bạn có thể vào MySQL Workbench để kiểm tra dữ liệu.

capture

capture

NodeJS – Lưu trữ dữ liệu với MongoDB

Trong phần này chúng ta sẽ tìm hiểu cách lưu trữ dữ liệu trong cơ sở dữ liệu MongoDB

MongoDB

MongoDB là cơ sở dữ liệu dạng NoSQL mã nguồn mở, có khả năng mở rộng và hiệu suất cao, sử dụng cú pháp JSON, không sử dụng khái niệm lược đồ cơ sở dữ liệu… và còn nhiều tính năng khác nữa.

Để cài đặt MongoDB thì bạn tải MongoDB tại địa chỉ: https://www.mongodb.org/downloads

Bạn tải và cài cho đúng với kiến trúc của hệ điều hành của mình (ví dụ máy bạn chạy hệ điều hành 32 bit thì cài bản 32 bit).

Sau khi cài xong thì bạn đưa đường dẫn đến thư mục bin trong thư mục cài đặt MongoDB vào biến môi trường PATH để tiện sử dụng sau này (nếu bạn không biết cách thiết lập biến này thì tham khảo bài trước hoặc tìm trên Google).

Sau đó bạn mở Command Prompt (cmd) lên và chạy lệnh mongod --version để xem phiên bản MongoDB của mình cũng như xác nhận xem đường dẫn đến thư mục bin đã nằm trong biến PATH đúng chưa:

C:\Users\PhoCode>mongod --version
db version v3.2.10
git version: 79d9b3ab5ce20f51c272b4411202710a082d0317
OpenSSL version: OpenSSL 1.0.1t-fips  3 May 2016
allocator: tcmalloc
modules: none
build environemnt:
    distmod: 2008plus-ssl
    distarch: x86_64
    target_arch: x86_64

Phiên bản MongoDB mình dùng ở đây là phiên bản 3.2.10. Mặc định các cơ sở dữ liệu sẽ được lưu trong thư mục C:\data\db trên Windows.

Tạo cơ sở dữ liệu

Chúng ta sẽ tạo một cơ sở dữ liệu trong MongoDB để lưu trữ các ghi chú trong ứng dụng Notes.

Đầu tiên chúng ta mở một Command Prompt lên rồi gõ lệnh mongod để chạy server:

C:\Users\PhoCode>mongod
...

Tiếp theo chúng ta mở một Command Prompt khác lên rồi chạy các lệnh sau:

C:\Users\PhoCode>mongo
MongoDB shell version: 3.2.10
connection to: test
> use notes
switched to db notes

Lệnh mongo sẽ chạy trình shell của MongoDB, trình shell cho phép chúng ta thao tác với server của MongoDB, mặc định trong server có sẵn một cơ sở dữ liệu có tên là test, bất cứ thao tác ghi/đọc nào cũng sẽ được thực hiện trên CSDL này.

Lệnh use <tên CSDL> sẽ tạo một cơ sở dữ liệu với tên tương ứng và thiết lập trình shell chuyển các thao tác đọc/ghi lên CSDL đó, nếu CSDL đó đã có rồi thì chuyển sang sử dụng luôn.

Tạo model

Giống như các phần trước, chúng ta sẽ tạo model chứa các hàm để đọc/ghi dữ liệu.

Trên npm có module mongoose là module dùng để kết nối với cơ sở dữ liệu Mongoose rất mạnh. Đầu tiên chúng ta tạo một thư mục có tên models-mongoose  trong thư mục gốc của project. Trong thư mục này chúng ta tạo một file có tên notes.js với nội dung như sau:

var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var dburl = undefined;
exports.connect = function(thedburl, callback) {
    dburl = thedburl;
    mongoose.connect(dburl);
}

exports.disconnect = function(callback) {
    mongoose.disconnect(callback);
}

var NoteSchema = new Schema({
    notekey: String,
    title: String,
    body: String
});

mongoose.model('Note', NoteSchema);
var Note = mongoose.model('Note');

exports.create = function(key, title, body, callback) {
    var newNote = new Note();
    newNote.notekey = key;
    newNote.title = title;
    newNote.body = body;
    newNote.save(function(err) {
        if(err)
            callback(err);
        else
            callback();
    });
}

exports.update = function(key, title, body, callback) { 
    exports.read(key, function(err, doc) {    
        if(err)
            callback(err);
        else { 
            doc.notekey = key;
            doc.title = title;
            doc.body = body;
            doc.save(function(err) {
                if(err)
                    callback(err);
                else
                    callback();
            });
        }
    });
}

exports.read = function(key, callback) {
    Note.findOne({ notekey: key }, function(err, doc) {
        if(err) 
            callback(err);
        else
            callback(null, doc);
    });
}

exports.destroy = function(key, callback) {
    exports.read(key, function(err, doc) {
        if(err)
            callback(err);
        else {
            doc.remove();
            callback();
        }
    });
}

exports.titles = function(callback) {
    Note.find().exec(function(err, docs) {
        if(err)
            callback(err);
        else {
            if(docs) {
                var noteList = [];
                docs.forEach(function(note) {
                    noteList.push({
                    key: note.notekey,
                    title: note.title 
                    }); 
                });
                callback(null, noteList);
            } else { 
                callback();
            }
        }
    });
}

File này cũng giống như các file model khác là chứa các hàm dùng để kết nối, ngắt kết nối, thực hiện các chức năng CRUD.

var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var dburl = undefined;
exports.connect = function(thedburl, callback) {
    dburl = thedburl;
    mongoose.connect(dburl);
}

exports.disconnect = function(callback) {
    mongoose.disconnect(callback);
}

Chúng ta sẽ cần đến module mongoose để tương tác với CSDL. Schema là một lớp trong mongoose định nghĩa các trường của một dòng trong một collection. Biến dburl được dùng để lưu đường dẫn kết nối đến CSDL. Hai hàm connect()disconnect() được dùng để kết nối và ngắt kết nối đến CSDL. Để thực hiện kết nối  thì chúng ta dùng hàm mongoose.connect() và truyền vào đường dẫn, ngắt kết nối thì chúng ta gọi hàm mongoose.disconnect() và truyền vào một hàm callback.

var NoteSchema = new Schema({
    notekey: String,
    title: String,
    body: String
});

mongoose.model('Note', NoteSchema);
var Note = mongoose.model('Note');

Chúng ta định nghĩa đối tượng NoteSchema kế thừa từ Schema chứa các trường notekey, title, body tương ứng. Sau đó dùng hàm model() để tạo bảng tương ứng trong CSDL.

exports.create = function(key, title, body, callback) {
    var newNote = new Note();
    newNote.notekey = key;
    newNote.title = title;
    newNote.body = body;
    newNote.save(function(err) {
        if(err)
            callback(err);
        else
            callback();
    });
}

Hàm create() được dùng để tạo các ghi chú, để lưu vào CSDL thì chúng ta gọi hàm save(), hàm này nhận một hàm callback làm tham số.

exports.update = function(key, title, body, callback) { 
    exports.read(key, function(err, doc) {    
        if(err)
            callback(err);
        else {         
            doc.title = title;
            doc.body = body;
            doc.save(function(err) {
                if(err)
                    callback(err);
                else
                    callback();
            });
        }
    });
}

Hàm update() được dùng để cập nhật các ghi chú. Ở đây trước khi cập nhật chúng ta kiểm tra xem ghi chú có tồn tại không bằng cách dùng hàm read() được định nghĩa ở dưới, hàm này nhận vào khóa, nếu không tồn tại thì trả về lỗi, nếu có tồn tại thì chúng ta cập nhật cũng bằng hàm save().

exports.read = function(key, callback) {
    Note.findOne({ notekey: key }, function(err, doc) {
        if(err) 
            callback(err);
        else
            callback(null, doc);
    });
}

Hàm read() được dùng để đọc các ghi chú, hàm này nhận vào khóa key. Hàm findOne() sẽ tìm bản ghi đầu tiên khớp với từ khóa, nếu không tìm thấy thì sẽ báo lỗi, nếu tìm thấy thì dữ liệu tìm được sẽ nằm trong tham số doc trong hàm callback. Nếu bạn muốn tìm toàn bộ thay vì chỉ tìm một bản ghi thì dùng hàm find().

exports.destroy = function(key, callback) {
    exports.read(key, function(err, doc) {
        if(err)
            callback(err);
        else {
            doc.remove();
            callback();
        }
    });
}

Hàm destroy() được dùng để xóa một ghi chú, và cũng tương tự như các hàm trên, hàm này nhận vào khóa key, để xóa một bản ghi trong CSDL thì chúng ta gọi hàm remove().

exports.titles = function(callback) {
    Note.find().exec(function(err, docs) {
        if(err)
            callback(err);
        else {
            if(docs) {
                var noteList = [];
                docs.forEach(function(note) {
                    noteList.push({
                    key: note.notekey,
                    title: note.title 
                    }); 
                });
                callback(null, noteList);
            } else { 
                callback();
            }
        }
    });
}

Hàm titles() sẽ có chức năng tìm toàn bộ ghi chú. Như đã nói ở trên, để lấy toàn bộ bản ghi thì chúng ta gọi hàm find().

Cấu hình project

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

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var routes = require('./routes/index');
var users = require('./routes/users');
var notes = require('./routes/notes');
//var models = require('./models-fs/notes');
//var models = require('./models-sqlite3/notes');
var models = require('./models-mongoose/notes');
models.connect("mongodb://localhost/notes", function(err) {
    if(err)
    throw err;
});
notes.configure(models);
routes.configure(models);
var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', routes.index);
app.use('/users', users);
app.get('/noteadd', notes.add);
app.post('/notesave', notes.save);
app.use('/noteview', notes.view);
app.use('/noteedit', notes.edit);
app.use('/notedestroy', notes.destroy);
app.post('/notedodestroy', notes.dodestroy);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
            message: err.message,
            error: err
        });
    });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: {}
    });
});


module.exports = app;

Ở đây chúng ta khai báo module notes trong thư mục models-mongoose. Sau đó để kết nối đến CSDL thì chúng ta gọi hàm connect() đã định nghĩa trong file notes.js ở trên và truyền vào đường dẫn dạng như sau:

mongodb://localhost/notes

Tiếp theo chúng ta khai báo module mongoose trong file package.json như sau:

{
    "name": "notes",
    "version": "0.0.0",
    "private": true,
    "scripts": {
    "start": "node ./bin/www"
 },
    "dependencies": {
        "body-parser": "~1.15.1",
        "cookie-parser": "~1.4.3",
        "debug": "~2.2.0",
        "ejs": "~2.4.1",
        "express": "~4.13.4",
        "morgan": "~1.7.0",
        "serve-favicon": "~2.3.0",
        "async": "*",
        "sqlite3": "*",
        "mongoose": "*"
    }
}

Cuối cùng bạn chạy lệnh npm install để cài đặt module.

Bây giờ bạn có thể chạy server để thực hiện các thao tác CRUD như bình thường, và dữ liệu sẽ được lưu trong CSDL MongoDB.

Để kiểm tra, bạn có thể mở lại trình shell của MongoDB và chạy lệnh db.notes.find() như sau:

C:\Users\PhoCode>mongo
MongoDB shell version: 3.2.10
connecting to: test
> use notes
switched to db notes
> db.notes.find()
{ "_id" : ObjectId("57faf19e77a44c30b858315a"), "body" : "Open source is the future", 
"title" : "Phở Code", "notekey" : "phocode", "__v" : 0 }

capture

NodeJS – Lưu trữ dữ liệu với SQLite3

Trong bài này chúng ta sẽ tìm hiểu cách lưu trữ dữ liệu cho ứng dụng ghi chú trong cơ sở dữ liệu SQLite3.

SQLite3

SQLite3 là một cơ sở dữ liệu SQL, đặc điểm của SQLite3 là rất nhỏ, nhẹ, dễ cài đặt, phù hợp với nhiều loại ứng dụng. Điểm đặc biệt của SQLite3 là chúng ta không cần một server, không cần các bước cài đặt phức tạp, rườm rà.

Nếu bạn đã có trình sqlite3.exe trên máy thì bạn có thể bỏ qua phần này và kéo xuống phần tạo cơ sở dữ liệu để đọc tiếp. Nếu chưa thì đầu tiên bạn tải công cụ của sqlite3 tại địa chỉ https://sqlite.org/download.html

Bạn tải các tool về và giải nén theo hệ điều hành mà bạn đang dùng, chẳng hạn như bạn dùng Windows thì tải file sqlite-tools-win32-x86-3140200.zip (1.52 MiB) về:

capture:

Tiếp theo bạn nên (hoặc phải) đưa đường dẫn đến thư mục chứa các file vừa được giải nén này vào biến môi trường PATH để tiện sử dụng sau này:

Ví dụ với Windows 10:

Bạn bấm chuột phải vào My Computer → Properties → Advanced system settings → Environment Variables, sau đó click chọn biến Path trong phần System variables rồi chèn thêm đường dẫn đến thư mục sqlite3 vừa giải nén vào. Chẳng hạn ở đây mình giải nén vào thư mục F:\DevSoft\sqlite3 thì mình có hình như sau:

capture

Bây giờ chúng ta kiểm tra xem sqlite3 đã được đưa vào biến môi trường Path chưa thì bạn mở terminal lên (Cmd trong Windows) rồi gõ lệnh sqlite3 -version để xem phiên bản sqlite3.

capture

Nếu bạn ra được giống như hình trên thì đường dẫn đến thư mục chứa sqlite3 của bạn đã hoàn toàn nằm trong biến môi trường Path rồi. Ở đây mình dùng SQLite3 phiên bản 3.14.2 như trong hình.

Tạo cơ sở dữ liệu

Bây giờ chúng ta sẽ tạo cơ sở dữ liệu để lưu các ghi chú cho ứng dụng Notes.

Đầu tiên bạn mở terminal lên rồi trỏ đến đường dẫn thư mục của project, sau đó gõ đoạn lệnh sau:

C:\NodeJS\notes>sqlite3 db.sqlite3
SQLite version 3.7.15.2 2013-01-09 11:53:05
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> CREATE TABLE notes (
   ...> notekey VARCHAR(255),
   ...> title VARCHAR(255),
   ...> author VARCHAR(255),
   ...> body TEXT
   ...> );
sqlite>

Lệnh sqlite3 db.sqlite3 sẽ tạo một file có tên db.sqlite3 ở thư mục hiện tại, nếu file đã tồn tại thì mở file đó ra để đọc/ghi, file này sẽ được dùng để lưu dữ liệu. Sau đó bạn có thể gõ các lệnh truy vấn trong ngôn ngữ SQL như bình thường.

Vậy là xong, đoạn lệnh trên sẽ tạo cơ sở dữ liệu lưu trong file db.sqlite3, trong cơ sở dữ liệu này có một bảng tên là notes với các trường như trên.

Tạo model

Giống như các phần trước, bây giờ chúng ta sẽ tạo một thư mục có tên models-sqlite3 nằm trong thư mục gốc của project (thư mục chứa các thư mục như bin, public, models, models-fs…). Trong thư mục này chúng ta tạo một file có tên notes.js với nội dung như sau:

var sqlite3 = require('sqlite3');
sqlite3.verbose();
var db = undefined;
exports.connect = function(dbname, callback) {
    db = new sqlite3.Database(dbname, 
        sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE,
        function(err) {
            if(err)
                callback(err);
            else
                callback(err);
    });
}
exports.disconnect = function(callback) {
     callback();
}

exports.create = function(key, title, body, callback) { 
     db.run("INSERT INTO notes(notekey, title, body) " +
         "VALUES (?, ?, ?);",
         [key, title, body],
         function(err) {
             if(err)
                 callback(err);
             else
                 callback();
     });
}

exports.update = function(key, title, body, callback) { 
     db.run("UPDATE notes " + 
         "SET title = ?, body = ? " +
         "WHERE notekey = ?;",
         [ title, body, key ],
         function(err) { 
             if(err)
                 callback(err);
             else
                 callback();
     });
}

exports.read = function(key, callback) { 
     db.get("SELECT * FROM notes WHERE notekey = ?;",
     [key],
     function(err, row) { 
         if(row == undefined)
             callback("Something wrong");
         else if(err) 
             callback(err);
         else
             callback(null, row);
     });
}

exports.destroy = function(key, callback) {
     db.run("DELETE FROM notes WHERE notekey = ?;",
         [ key ],
         function(err) {
             if(err)
                 callback(err);
             else
                 callback(err);
     });
}

exports.titles = function(callback) {
    var titles = [];
    db.each("SELECT notekey, title FROM notes",
        function(err, row) {
            if(err)
                callback(err);
            else
                titles.push({
                key: row.notekey,
                title: row.title
            });
        },
        function(err, num) {
            if(err)
                callback(err);
            else
                callback(null, titles);
    });
}

File này sẽ chứa các hàm hỗ trợ đọc/ghi dữ liệu với cơ sở dữ liệu SQLite3.

var sqlite3 = require('sqlite3');
sqlite3.verbose();

Ở đây chúng ta dùng module sqlite3. Hàm verbose() có chức năng bật chế độ stack trace, tức là khi có lỗi xảy ra, bạn sẽ biết câu lệnh nào đã gây ra lỗi đó để sửa cho dễ.

var db = undefined;
exports.connect = function(dbname, callback) {
    db = new sqlite3.Database(dbname, 
        sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE,
        function(err) {
            if(err)
                callback(err);
            else
                callback(err);
    });
}
exports.disconnect = function(callback) {
     callback();
}

Giống với bài trước, ở đây chúng ta cũng tạo một biến để lưu đối tượng cơ sở dữ liệu và định nghĩa 2 hàm để kết nối và ngắt kết nối với cơ sở dữ liệu. Chúng ta viết hàm connect() có chức năng là kết nối CSDL, hàm này nhận vào tên file CSDL và một hàm callback. Hàm sqlite3.Database() sẽ thực hiện việc kết nối, hàm này nhận vào tên CSDL, chế độ mở và một hàm callback. Chế độ mở ở trên là OPEN_READWRITEOPEN_CREATE, tức là mở để đọc/ghi và tạo CSDL.

exports.create = function(key, title, body, callback) { 
     db.run("INSERT INTO notes(notekey, title, body) " + 
         "VALUES (?, ?, ?);",
         [key, title, body],
         function(err) {
             if(err)
                 callback(err);
             else
                 callback();
     });
}

Hàm create() sẽ được dùng để tạo các ghi chú và lưu vào cơ sở dữ liệu. Ở đây chúng ta dùng hàm run() để thực thi các câu truy vấn SQL. Trong các câu truy vấn chúng ta có thể truyền tham số vào dưới dạng dấu chấm hỏi. Các tham số truyền vào sẽ nằm trong mảng phía sau câu truy vấn. Mục đích của việc truyền tham số là để chống lại tấn công SQL Injection.

exports.update = function(key, title, body, callback) { 
     db.run("UPDATE notes " + 
         "SET title = ?, body = ? " +
         "WHERE notekey = ?;",
         [ title, body, key ],
         function(err) { 
             if(err)
                 callback(err);
             else
                 callback();
     });
}

Không như các phần trước, hàm update()create() là một thì ở đây 2 hàm này là 2 hàm khác nhau, vì chúng ta phải chạy các câu truy vấn khác nhau. Để chạy câu truy vấn UPDATE thì chúng ta cũng làm giống như trên là dùng hàm run().

exports.read = function(key, callback) { 
     db.get("SELECT * FROM notes WHERE notekey = ?;",
     [key],
     function(err, row) { 
         if(row == undefined)
             callback("Something wrong");
         else if(err) 
             callback(err);
         else
             callback(null, row);
     });
}

Hàm read() được dùng để truy vấn thông tin của một ghi chú cụ thể, khác với UPDATEINSERT, câu truy vấn SELECT phải dùng hàm get() chứ không dùng hàm run(). Tức là các câu truy vấn nào mà có trả về dữ liệu thì chúng ta dùng get(), còn không trả về dữ liệu thì dùng run(), cú pháp của hàm get() cũng giống với cú pháp của hàm run(), chỉ khác ở chỗ là hàm callback sẽ nhận thêm một tham số nữa là dữ liệu trả về từ CSDL (tham số row), ngoài ra tham số row có thể sẽ là undefined nếu câu truy vấn trả về là rỗng, nên chúng ta phải kiểm tra trước.

exports.destroy = function(key, callback) {
     db.run("DELETE FROM notes WHERE notekey = ?;",
         [ key ],
         function(err) {
             if(err)
                 callback(err);
             else
                 callback(err);
     });
}

Hàm destroy() được dùng để xóa một ghi chú, tương tự như 2 hàm create()update(), ở đây chúng ta cũng dùng hàm run().

exports.titles = function(callback) {
    var titles = [];
    db.each("SELECT notekey, title FROM notes",
        function(err, row) {
            if(err)
                callback(err);
            else
                titles.push({
                key: row.notekey,
                title: row.title
            });
        },
        function(err, num) {
            if(err)
                callback(err);
            else
                callback(null, titles);
    });
}

Hàm titles() được dùng để truy vấn danh sách các ghi chú có trong CSDL, chúng ta dùng hàm each() để truy vấn, tham số đầu tiên của hàm này là câu truy vấn, tham số thứ 2 và thứ 3 là một hàm callback. Khi kết quả truy vấn từ CSDL trả về lại hàm này thì hàm Node sẽ thực hiện lặp qua từng dòng dữ liệu và mỗi lần lặp thì gọi hàm callback thứ 2 và truyền dữ liệu của dòng đó vào hàm này, ở đây mỗi lần lặp chúng ta chèn dữ liệu của hàm đó vào mảng titles[]. Đến khi lặp hết thì hàm callback thứ 3 sẽ được gọi, hàm này ngoài tham số lỗi err thì còn có tham số num là số dòng đã trả về từ CSDL, tại đây chúng ta gọi lại hàm đã gọi titles() và truyền vào mảng titles[] nếu không có lỗi.

Cấu hình project

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

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var routes = require('./routes/index');
var users = require('./routes/users');
var notes = require('./routes/notes');
//var models = require('./models-fs/notes');
var models = require('./models-sqlite3/notes');
models.connect("./db.sqlite3", function(err) {
    if(err)
    throw err;
});
notes.configure(models);
routes.configure(models);
var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', routes.index);
app.use('/users', users);
app.get('/noteadd', notes.add);
app.post('/notesave', notes.save);
app.use('/noteview', notes.view);
app.use('/noteedit', notes.edit);
app.use('/notedestroy', notes.destroy);
app.post('/notedodestroy', notes.dodestroy);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
            message: err.message,
            error: err
        });
    });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
       message: err.message,
       error: {}
    });
});


module.exports = app;

Ở đây chúng ta tạo lại đối tượng model mới từ module notes trong thư mục models-sqlite3.

Như đã nói, ở đây chúng ta dùng module sqlite3 nên chúng ta phải khai báo trong file package.json để npm cài thêm module này cho chúng ta như sau:

{
    "name": "notes",
    "version": "0.0.0",
    "private": true,
    "scripts": {
    "start": "node ./bin/www"
 },
    "dependencies": {
        "body-parser": "~1.15.1",
        "cookie-parser": "~1.4.3",
        "debug": "~2.2.0",
        "ejs": "~2.4.1",
        "express": "~4.13.4",
        "morgan": "~1.7.0",
        "serve-favicon": "~2.3.0",
        "async": "*",
        "sqlite3": "*"
    }
}

Sau khi đã khai báo xong thì chúng ta chạy lệnh npm install để cài, tuy nhiên module này có hơi không được bình thường cho lắm, nếu bạn chạy lệnh này mà thấy một đống dòng thông báo lỗi dạng như hình dưới đây:

 

thì tức là trên npm không có sẵn thư viện này cho hệ điều hành cùng với phiên bản Node.js của bạn, và npm đã chuyển sang cách cài khác là dịch thư viện sqlite3 từ mã nguồn để cài, tuy nhiên việc dịch vẫn không thành công. Lý do là vì máy của bạn thiếu một trong các trình biên dịch Visual C++ hoặc Python 2, hoặc có thể thiếu cả 2. Việc bạn cần làm là lên mạng tải và cài đặt cho đủ 2 trình biên dịch này, sau đó chạy lại lệnh npm install là được, Visual C++ thì bạn có thể cài từ Visual Studio Community cũng được, Python phải là dòng Python 2 chứ không phải dòng Python 3.

Sau đó bạn có thể chạy lệnh npm start để chạy ứng dụng được rồi, và lần này dữ liệu đã được lưu trong file cơ sở dữ liệu db.sqlite3 ở thư mục gốc của project.

Bạn có thể mở terminal lên và chạy sqlite3 bằng dòng lệnh để kiểm tra:

capture

capture

NodeJS – Lưu trữ dữ liệu trên file

Trong phần này chúng ta sẽ sửa ứng dụng Notes để có thể đọc/ghi dữ liệu trên file dưới dạng JSON chứ không lưu vào RAM nữa, ngoài ra chúng ta sẽ sử dụng module async để đọc dữ liệu từ file theo hướng bất đồng bộ.

Tạo model

Đầu tiên chúng ta tạo một thư mục có tên models-fs nằm trong thư mục gốc của project (nằm cùng với các thư mục models, bin, public…). Trong thư mục này chúng ta tạo một file notes.js để viết các hàm xử lý dữ liệu với file, file này sẽ có nội dung như sau:

var fs = require('fs');
var path = require('path');
var async = require('async');
var _dirname = undefined;
exports.connect = function(dirname, callback) {
    _dirname = dirname;
    callback();
}

exports.disconnect = function(callback) {
    callback();
}

exports.create = function(key, title, body, callback) {
    fs.writeFile(path.join(_dirname, key + '.json'),
        JSON.stringify({
            title: title,
            body: body
        }),
        'utf8',
        function(err) {
            if(err) 
                callback(err);
            else
                callback();
    });
}
exports.update = exports.create;

exports.read = function(key, callback) {
    fs.readFile(path.join(_dirname, key + '.json'),
        'utf8',
        function(err, data) {
            if(err)
                callback(err);
            else
                callback(undefined, JSON.parse(data));
    });
}

exports.destroy = function(key, callback) {
    fs.unlink(path.join(_dirname, key + '.json'),
        function(err) {
            if(err) 
                callback(err);
            else
                callback();
    });
}

exports.titles = function(callback) {
    fs.readdir(_dirname, function(err, filez) {
        if(err)
            callback(err);
        else {
            var noteList = []; 
            async.eachSeries(filez,
                function(fname, callback) { 
                    var key = path.basename(fname, '.json');
                    exports.read(key, function(err, note) {
                        if(err)
                            callback(err);
                        else {
                            noteList.push({
                            key: fname.replace('.json',''),
                            title: note.title
                    });
                    callback();
                }
            });
        },
        function(err) {
            if(err)
                callback(err);
            else 
                callback(null, noteList);
            });
        }
    });
}

Bản chất thì file notes.js này cũng giống như file notes.js trong thư mục models vậy, tức là file này dùng để lưu trữ phần model trong mô hình MVC, chỉ khác là ở đây chúng ta lưu trữ trên file, trong các bài sau chúng ta cũng sẽ cần thêm các file tương tự để lưu trữ ở những nơi khác nhau như cơ sở dữ liệu chẳng hạn.

var fs = require('fs');
var path = require('path');
var async = require('async');

Ở đây chúng ta sẽ dùng đến module fs, path để đọc/ghi file. Module async để thực hiện đọc file bất đồng bộ. Chúng ta sẽ tìm hiểu async ở dưới.

var _dirname = undefined;
exports.connect = function(dirname, callback) {
    _dirname = dirname;
    callback();
}

exports.disconnect = function(callback) {
    callback();
}

Biến _dirname là biến dùng để lưu đường dẫn đến thư mục gốc của project, chúng ta không gán giá trị cho biến này bằng tay mà viết một hàm có chức năng làm việc này là hàm connect(), hàm này nhận vào đường dẫn thư mục. Hàm disconnect() cũng có ý nghĩa là xóa đường dẫn hay ngắt kết nối tới nguồn dữ liệu, tuy nhiên ở đây chúng ta chưa dùng tới.

exports.create = function(key, title, body, callback) {
    fs.writeFile(path.join(_dirname, key + '.json'),
        JSON.stringify({
            title: title,
            body: body
        }),
        'utf8',
        function(err) {
            if(err) 
                callback(err);
            else
                callback();
    });
}
exports.update = exports.create;

Hàm create() có chức năng tạo ghi chú, hàm này nhận vào khóa, tiêu đề, nội dung và một hàm callback. Ở đây chúng ta ghi dữ liệu vào file theo định dạng JSON. Hàm update() dùng để cập nhật ghi chú sẽ chính là hàm create() luôn.

exports.read = function(key, callback) {
    fs.readFile(path.join(_dirname, key + '.json'),
        'utf8',
        function(err, data) {
            if(err)
                callback(err);
            else
                callback(undefined, JSON.parse(data));
    });
}

Hàm read() được dùng để đọc nội dung một file ghi chú dựa theo khóa, hàm này nhận vào khóa và một hàm callback.

exports.destroy = function(key, callback) {
    fs.unlink(path.join(_dirname, key + '.json'),
        function(err) {
            if(err) 
                callback(err);
            else
                callback();
    });
}

Hàm destroy() sẽ xóa một ghi chú, hàm này cũng nhận vào một khóa và một hàm callback.

exports.titles = function(callback) {
    fs.readdir(_dirname, function(err, filez) {
        if(err)
            callback(err);
        else {
            var noteList = []; 
            async.eachSeries(filez,
                function(fname, callback) { 
                    var key = path.basename(fname, '.json');
                    exports.read(key, function(err, note) {
                        if(err)
                            callback(err);
                        else {
                            noteList.push({
                            key: fname.replace('.json',''),
                            title: note.title
                    });
                    callback();
                }
            });
        },
        function(err) {
            if(err)
                callback(err);
            else 
                callback(null, noteList);
            });
        }
    });
}

Hàm titles() sẽ đọc các file ghi chú có đuôi .json và trả về hàm callback trong tham số của nó.

Ở đây có một lưu ý là hàm này sẽ thực hiện đọc file theo hướng bất đồng bộ (Asynchronous), ở đây mình sẽ không nói cách bất đồng bộ hoạt động như thế nào, bạn chỉ cần hình dung là giả sử số lượng file quá nhiều (tầm 100,000 file chẳng hạn), việc đọc toàn bộ các file đó rồi hiển thị lên web sẽ rất chậm, việc này là bình thường. Tuy nhiên khi đọc theo mô hình lập trình đồng bộ (Synchronous) thì trong quá trình server đang đọc, server của bạn sẽ bị blocked, tức là nếu bạn mở file tab khác trên trình duyệt và trỏ đến localhost:3000 thì bạn sẽ không thấy server trả về cái gì cả vì server đang bận đọc file, nhưng nếu dùng bất đồng bộ thì bạn vẫn có thể mở một tab khác và thực hiện các thao tác khác với server.

Việc thực hiện các thao tác bất đồng bộ rất đơn giản, ở đây chúng ta dùng hàm eachSeries() trong module async. Hàm này nhận vào một mảng hoặc list hoặc bất cứ đối tượng nào lưu trữ dữ liệu theo dạng danh sách, một hàm dùng để thực thi với các phần tử trong mảng và một hàm callback. Trong đoạn code trên, đầu tiên chúng ta dùng hàm fs.readdir(), hàm này nhận vào đường dẫn thư mục và một hàm callback, hàm callback sẽ nhận vào tham số lỗi và dữ liệu là danh sách các file hoặc thư mục có trong thư mục được truyền vào. Trong hàm callback này chúng ta gọi async.eachSeries() để xử lý từng file .json, tất cả sẽ được truyền vào mảng noteList, mảng này sau đó sẽ được truyền vào hàm callback của hàm fs.readdir().

Đọc dữ liệu

Tiếp theo chúng ta sửa lại phương thức index() trong file routes/index.js như sau:

var express = require('express');
var router = express.Router();
var notes = undefined;
exports.configure = function(params) {
    notes = params;
}

exports.index = router.get('/', function(req, res, next) { 
    notes.titles(function(err, noteList) {
        if(err) 
            res.render('showerror', { title: 'Notes', error: 'Could not read data'});
        else
            res.render('index', { title: 'Notes', notes: noteList });
    }); 
});

Chúng ta truyền module vào biến notes trong phương thức configure(), chứ bản thân biến này không lưu trữ dữ liệu, do đó chúng ta gọi phương thức titles() để lấy dữ liệu, phương thức này trả về dữ liệu trong hàm callback() và chúng ta lấy dữ liệu đó để truyền vào phương thức render().

Cấu hình app.js

Trong file app.js chúng ta sửa lại thành như sau:

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var routes = require('./routes/index');
var users = require('./routes/users');
var notes = require('./routes/notes');
var models = require('./models-fs/notes');
models.connect("./Notes", function(err) {
    if(err)
        throw err;
});
notes.configure(models);
routes.configure(models);
var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', routes.index);
app.use('/users', users);
app.get('/noteadd', notes.add);
app.post('/notesave', notes.save);
app.use('/noteview', notes.view);
app.use('/noteedit', notes.edit);
app.use('/notedestroy', notes.destroy);
app.post('/notedodestroy', notes.dodestroy);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
            message: err.message,
            error: err
        });
    });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
        res.render('error', {
        message: err.message,
        error: {}
    });
});

module.exports = app;

Ở đây chúng ta gọi hàm connect() để tạo đường dẫn thư mục cho đối tượng models.

Sau đó trong thư mục gốc của project, bạn cũng phải tạo một thư mục có tên là Notes.

capture

Sửa template

File index.ejs trong thư mục views sẽ được sửa lại một tí như sau:

&lt;% include top %&gt;
&lt;%
    if(notes) {
    for(var i in notes) {
        %&gt;&lt;p&gt;&lt;%= notes[i].key %&gt;:
        &lt;a href="/noteview?key=&lt;%= notes[i].key %&gt;"&gt;&lt;%= notes[i].title %&gt;&lt;/a&gt;
    &lt;/p&gt;&lt;%
 }
 }
%&gt;
&lt;% include bottom %&gt;

Ở đây đối tượng notes chính là mảng noteList được tạo từ hàm titles() trong file models-fs/notes.js chứ không phải một module nữa như trong bài trước, do đó chúng ta sửa lại để đọc dữ liệu cho đúng.

Cài dependencies

Như đã nói, ở đây chúng ta dùng thêm module async, nhưng module này không được express-generator đưa vào nên chúng ta phải tự chèn thêm trong file package.json như sau:

{
    "name": "notes",
    "version": "0.0.0",
    "private": true,
    "scripts": {
    "start": "node ./bin/www"
 },
    "dependencies": {
        "body-parser": "~1.15.1",
        "cookie-parser": "~1.4.3",
        "debug": "~2.2.0",
        "ejs": "~2.4.1",
        "express": "~4.13.4",
        "morgan": "~1.7.0",
        "serve-favicon": "~2.3.0",
        "async": "*"
    }
}

Sau đó chúng ta chạy lại lệnh npm install để npm cài thêm module async vào.

Vậy là xong, bạn có thể chạy ứng dụng như bình thường, nhưng ở đây các ghi chú sẽ được lưu vào trong các file .json trong thư mục Notes, và khi tắt server rồi mở lại thì tất nhiên là các ghi chú này vẫn sẽ hiện ra (vì chúng vẫn còn nằm trong file) chứ không biến mất như khi lưu vào RAM.

capture

untitled

NodeJS – Hàm callback

Trong phần này chúng ta sẽ code lại ứng dụng Notes có sử dụng các hàm callback.

Hàm callback

Các hàm được truyền vào một hàm khác được gọi là hàm callback, thường chúng ta sẽ gọi các hàm này cuối cùng sau khi đã thực hiện các công việc khác. Đầu tiên chúng ta sẽ tìm hiểu cách Node chạy các hàm callback.

Trong vùng bộ nhớ của Javascript có một khu vực gọi là Callstack, các hàm được gọi sẽ được sắp xếp lần lượt trong stack này, và được gọi lần lượt theo thứ tự “vào trước ra sau” (FILO – First In Last Out). Bạn xem trong hình dưới đây:

untitled

Trong hình trên, chúng ta có đoạn code, các hàm được gọi lần lượt theo thứ tự từ main() → printSquare() → square() → multiply(), sau đó hàm multiply() trả về trước, rồi đến hàm square() trả về, rồi hàm printSquare() gọi console.log() và kết thúc, đây là các lời gọi bình thường, không phải callback.

Node cho phép chúng ta truyền tham số vào một hàm là một hàm khác chứ không chỉ có các giá trị thô như số nguyên, chuỗi, ví dụ:

var click = function(str, callback) {
    console.log("Click() called: " + str);
    callback();
}

click("Something", function() { 
    console.log("Callback() called");
});

var callback2 = function() {
    console.log("callback2() called");
}

click("Something", callback2);

Trong đoạn code trên, chúng ta có hàm click() nhận vào một tham số là str và một tham số là một hàm khác, khi chúng ta gọi hàm click(), chúng ta có thể tryền vào một hàm đã được định nghĩa trước hoặc định nghĩa ngay trong lời gọi hàm luôn cũng được.

Click() called: Something 1
Callback() called
Click() called: Something 2
callback2() called

Chúng ta tìm hiểu hàm callback là để sử dụng kỹ thuật bất đồng bộ trong các bài sau.

Tạo hàm callback

Bây giờ chúng ta sẽ chuyển đổi một số code trong ứng dụng Notes đã làm trong các phần trước để chúng sử dụng hàm callback.

Chúng ta sửa lại file views/index.ejs như sau:

<% include top %>
<%
    if(notes) {
        for(var i in notes.notes) {
            %><p><%= i %>: 
                <a href="/noteview?key=<%= i %>"><%= notes.notes[i].title %></a>
            </p><%
        }
    }
%>
<% include bottom %>

Ở đây chúng ta không kiểm tra đối tượng notes nữa mà đối tượng này sẽ được truyền vào từ các hàm res.render().

Trong file routes/index.js, chúng ta xóa tất cả và sửa lại như sau:

var express = require('express');
var router = express.Router();
var notes = undefined;
exports.configure = function(params) {
    notes = params;
}

exports.index = router.get('/', function(req, res, next) {
    res.render('index', { title: 'Notes', notes: notes });
});

Hàm configure() sẽ được dùng để truyền đối tượng notes từ bên ngoài vào. Ngoài ra đối tượng Router có chút thay đổi, trong các phần trước chúng ta để mặc định đối tượng exports là đối tượng router, ở đây chúng ta gán vào trường index trong đối tượng exports, như thế sẽ linh hoạt hơn.

Tiếp theo chúng ta tạo một file có tên showerror.ejs trong thư mục views dùng để hiển thị lỗi với nội dung như sau:

<% include top %>
<%= error %>
<% include bottom %>

File này rất đơn giản, chúng ta include nội dung từ file top.ejsbottom.ejs, còn phần lỗi sẽ được truyền vào từ các hàm res.render().

Bây giờ chúng ta xóa toàn bộ nội dung file routes/notes.js và thay bằng đoạn code sau:

var notes = undefined;
exports.configure = function(params) {
    notes = params;
}

var readNote = function(key, res, done) {
    notes.read(key, 
        function(err, data) {
            if(err) {
            res.render('showerror', {
                title: "Could not read note " + key,
                error: err
            });
        done(err);
    } else
        done(null, data);
    });
}

exports.view = function(req, res, next) {
    if(req.query.key) {
        readNote(req.query.key, res, function(err, data) {
            if(!err) {
                res.render('noteview', {
                    title: data.title,
                    notekey: req.query.key,
                    note: data
                });
            }
        });
    } else {
        res.render('showerror', {
            title: "No key given for Note",
            error: "Must provide a Key to view a Note"
        });
    }
}

exports.save = function(req, res, next) {
    ((req.body.docreate === "create") ? notes.create : notes.update)
    (req.body.notekey, req.body.title, req.body.body,
        function(err) {
        if(err) {
            res.render('showerror', {
                title: "Could not update file",
                error: err
            });
        } else {
            res.redirect('/noteview?key='+req.body.notekey);
        }
        });
   }

exports.add = function(req, res, next) {
    res.render('noteedit', {
        title: "Add a Note",
        docreate: true,
        notekey: "",
        note: undefined
    });
}

exports.edit = function(req, res, next) {
    if(req.query.key) {
        readNote(req.query.key, res, function(err, data) {
            if(!err) {
                res.render('noteedit', {
                    title: data ? ("Edit " + data.title) : "Add a Note",
                    docreate: false,
                    notekey: req.query.key,
                    note: data
                });
            }
        });
    } else {
        res.render('showerror', {
            title: "No key given for Note",
            error: "Must provide a Key to view a Note"
        });
    }
}

exports.destroy = function(req, res, next) {
    if(req.query.key) {
        readNote(req.query.key, res, function(err, data) {
            if(!err) {
                res.render('notedestroy', {
                    title: data.title,
                    notekey: req.query.key,
                    note: data
                });
            }
        });
    } else {
        res.render('showerror', {
            title: "No key given for Note",
            error: "Must provide a Key to view a note"
        });
    }
}

exports.dodestroy = function(req, res, next) {
    notes.destroy(req.body.notekey, function(err) {
        if(err) {
            res.render('showerror', {
            title: "Could not delete Note " + req.body.notekey,
            error: err
            });
        } else {
            res.redirect('/');
        }
    });
}

Trong file routes/notes.js chúng ta có 2 hàm mới là configure() có chức năng tương tự như hàm configure() trong file routes/index.js ở trên và hàm readNote() có chức năng đọc dữ liệu của mảng notes trong file models/notes.js. Các hàm còn lại được viết lại và thay vì đọc trực tiếp từ đối tượng mảng notes, chúng ta gọi hàm readNote().

Ngoài ra hàm readNote() còn nhận vào một hàm callback có tham số tên là done, về cơ bản thì hàm này được dùng để trả về lỗi, tức là sau khi đã thực hiện các công việc khác mà nếu có lỗi thì hàm done sẽ trả về lỗi, không thì trả về dữ liệu bình thường.

Tương tự, trong hàm readNote(), save() và hàm dodestroy(), chúng ta gọi đến hàm read(), create() và destroy() trong file models/notes.js, các hàm này cũng nhận vào một callback và cũng sẽ làm nhiệm vụ trả về lỗi nếu có.

Do đó bây giờ chúng ta sửa lại file modes/notes.js như sau:

var notes = [];
exports.notes = notes;
exports.update = exports.create = function(key, title, body, callback) {
    notes[key] = { title: title, body: body };
    callback(null);
}
 
exports.read = function(key, callback) {
    if(!(key in notes))
        callback("No such Key existed", null);
    else 
        callback(null, notes[key]);
}
 
exports.destroy = function(key, callback) {
    if(!(key in notes))
        callback("Wrong Key");
    else {
        delete notes[key];
        callback(null);
    }
}
 
exports.keys = function() {
    return Object.keys(notes);
}

Cuối cùng trong file app.js chúng ta sửa lại thành như sau:

var express       = require('express');
var path          = require('path');
var favicon       = require('serve-favicon');
var logger        = require('morgan');
var cookieParser  = require('cookie-parser');
var bodyParser    = require('body-parser');

var routes = require('./routes/index');
var users = require('./routes/users');
var notes = require('./routes/notes');
var models = require('./models/notes');
notes.configure(models);
routes.configure(models);
var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', routes.index);
app.use('/users', users);
app.get('/noteadd', notes.add);
app.post('/notesave', notes.save);
app.use('/noteview', notes.view);
app.use('/noteedit', notes.edit);
app.use('/notedestroy', notes.destroy);
app.post('/notedodestroy', notes.dodestroy);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
            message: err.message,
            error: err
        });
    });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
    message: err.message,
    error: {}
    });
});

module.exports = app;

Ở đây chúng ta gọi module modes/notes và truyền vào hàm configure() trong các module routes/index.jsroutes/notes.js để sử dụng.

var models = require('./models/notes');
notes.configure(models);
routes.configure(models);

Như đã nói ở trên, đối tượng router trong file routes/index.js bây giờ là hàm index() trong đối tượng exports trong file đó, do đó chúng ta cũng sửa lại cho đường dẫn '/' trỏ đến hàm này.

app.use('/', routes.index);

Vậy là xong, bạn có thể chạy lại ứng dụng và thấy mọi thứ vẫn như trước, không có gì thay đổi, tuy nhiên ở đây chúng ta có sử dụng các hàm callback và từ bài sau chúng ta sẽ sử dụng hàm này cho các công việc xử lý bất đồng bộ.

NodeJS – Tăng hiệu suất server

Trong các phần trước chúng ta đã xây dựng một ứng dụng có đầy đủ các chức năng CRUD (các thao tác thêm, sửa, xóa, cập nhật) giống như một ứng dụng bình thường.

Bây giờ giả sử dụng ứng dụng của chúng ta nổi tiếng đến mức có đến 1 triệu người dùng truy cập mỗi ngày. Một tiến trình server phải phục vụ 1 triệu người mỗi ngày là rất mệt, có một cách giải quyêt đó là chạy nhiều instance cùng một lúc để “san sẻ gánh nặng” cho nhau. Nếu bạn chưa bao giờ nghe qua khái niệm instance thì có thể hình dung nó giống như một biến hay một đối tượng vậy, tức là ở đây chúng ta tạo nhiều đối tượng server để chạy.

Khi bạn tạo một ứng dụng Express thì trong thư mục bin của project, có file www, file này không có phần mở rộng tuy nhiên nó cũng chỉ là một file text nên chúng ta có thể mở ra xem với bất kỳ trình text editor nào:

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');
var debug = require('debug')('notes:server');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
    var port = parseInt(val, 10);

    if (isNaN(port)) {
        // named pipe
        return val;
    }

    if (port >= 0) {
        // port number
        return port;
    }

    return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
    if (error.syscall !== 'listen') {
        throw error;
    }

    var bind = typeof port === 'string'
      ? 'Pipe ' + port
      : 'Port ' + port;

    // handle specific listen errors with friendly messages
    switch (error.code) {
    case 'EACCES':
        console.error(bind + ' requires elevated privileges');
        process.exit(1);
        break;
    case 'EADDRINUSE':
        console.error(bind + ' is already in use');
        process.exit(1);
        break;
    default:
        throw error;
    }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
    var addr = server.address();
    var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
    debug('Listening on ' + bind);
}

Trong file này có 2 dòng là:

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

Ý nghĩa của 2 dòng này là chạy server trên cổng được định nghĩa trong biến môi trường PORT, nếu biến môi trường PORT không có thì chạy trên cổng mặc định là 3000.

Để có thể chạy nhiều instance server cùng một lúc thì chúng ta chỉ cần chạy chúng trên các cổng khác nhau là được. Bằng cách thiết lập biến môi trường PORT cho mỗi lần chạy, chúng ta sẽ chạy được nhiều instance server.

Ví dụ:

C:\NodeJS\notes>SET PORT=3030
C:\NodeJS\notes>npm start

> notes@0.0.0 start F:\NodeJS\notes
> node ./bin/www

  notes:server Listening on port 3030 +0ms

Chúng ta thiết lập biến môi trường PORT có giá trị là 3030 bằng lệnh SET, lưu ý là việc thiết lập biến môi trường thông qua lệnh SET chỉ có hiệu lực trong terminal (Command Prompt) đó thôi, khi thoát terminal thì biến này sẽ mất. Sau đó chúng ta chạy server như thường thì server lúc này sẽ chạy trên cổng 3030.

Sau đó chúng ta lại mở một trình terminal khác lên và cũng chạy lại những lệnh như trên, chỉ khác số PORT là sẽ có thêm một instance khác, ví dụ:

C:\NodeJS\notes>SET PORT=4040
C:\NodeJS\notes>npm start

> notes@0.0.0 start F:\NodeJS\notes
> node ./bin/www

  notes:server Listening on port 4040 +0ms

capturecapture

Thoạt nhìn thì có vẻ 2 instance đều là một, nhưng thực chất chúng là 2 đối tượng server khác nhau, chẳng qua là vì chúng đều trả về cùng một tài nguyên nên nội dung HTML trả về giống nhau. Mỗi instance đều có bộ nhớ riêng, có dữ liệu riêng. Đặc biệt là ở đây chúng ta không lưu các ghi chú trong cơ sở dữ liệu mà lưu trong bộ nhớ RAM, do đó nếu bạn tạo một ghi chú mới ở instance có cổng 3030 thì chỉ có bên đó mới thấy được, còn ở cổng 4040 thì không thể thấy được.

capturecapture

Tất nhiên là trong thực tế thì chúng ta muốn dữ liệu trả về phải đồng nhất (tức là 2 bên phải giống nhau), do đó dữ liệu nên/phải được lưu trong các file tài nguyên dùng chung, chẳng hạn như sử dụng cơ sở dữ liệu. Mục đích của việc sử dụng cơ sở dữ liệu là để lưu trữ lâu dài, đặc biệt là với các tập dữ liệu có các mối quan hệ phức tạp.Trong các phần sau chúng ta sẽ bắt đầu đi vào tìm hiểu cách lưu trữ dữ liệu.