Author Archives: Phở Code

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.

NodeJS – Tùy biến giao diện Express

Trong các bài trước chúng ta đã tìm hiểu cách xây dựng ứng dụng với Express, và các ứng dụng này có giao diện phía client (hay frontend) cũng khá bắt mắt, tuy nhiên nếu muốn chúng ta có thể tùy biến để giao diện hiển thị theo ý thích riêng của chúng ta.

Khi tạo một project Express thì file index.ejs (mà chúng ta hay tách thành top.ejsbottom.ejs) có dòng code này tròng thẻ <head>:

<link rel='stylesheet' href='/stylesheets/style.css' />

Đây là dòng code tham chiếu đến file style.css được đặt trong thư mục public/stylesheets. Ngoài thư mục stylesheets thì còn có 2 thư mục khác trong này là imagesjavascripts. Đây là các thư mục chứa các file tài nguyên để sử dụng cho phần frontend.

Chúng ta sẽ thử thay đổi một số thứ trong file style.css của project Notes đã làm trong phần trước để có giao diện như ý muốn.

h1 {
    text-align: center;
    text-decoration: underline;
}

Đoạn code CSS trên rất đơn giản nếu bạn đã từng học CSS. Chỉ là đưa các element h1 ra giữa trình duyệt và thêm dấu gạch dưới phía dưới.

capture

Tất nhiên là ngoài file style.css thì chúng ta cũng có thể tự viết nhiều file khác để việc tùy biến giao diện được linh hoạt hơn, bạn chỉ cần viết các file đó rồi đưa link tham chiếu trong thẻ <link> là được.

Nếu muốn bạn có thể dùng thêm một framework khác rất nổi tiếng đó là Bootstrap. Boostrap được phát triển bởi Twitter, hỗ trợ làm frontend rất mạnh, rất dễ sử dụng, có thể dùng trên nhiều loại trinh duyệt khác nhau.

Để sử dụng Bootstrap thì chúng ta có thể lên trang chủ của Bootstrap tại địa chỉ: http://getbootstrap.com/getting-started/#download

Sau khi tải về thì chúng ta giải nén ra rồi bỏ vào thư mục public, tiếp theo chúng ta chỉ cần khai báo thẻ <link> tham chiếu đến file bootstrap.min.css trong thư mục css là được, Ví dụ mình giải nén ra được thư mục bootstrap-3.3.7-dist thì mình khai báo như sau:

<link href="/bootstrap-3.3.7-dist/css/bootstrap.min.css" rel="stylesheet" media="screen">

Sau đó trong file bottom.ejs chúng ta phải khai báo hai dòng sau trước khi đóng thẻ </body>:

<script src="http://code.jquery.com/jquery.js"></script>
<script src="/bootstrap/js/bootstrap.min.js"></script>
</body></html>

Hoặc dùng cách khác mà hầu hết mọi người thường dùng là chèn link CDN trực tiếp trong thẻ <link> luôn:

<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">

<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

Dùng cách này thì bạn không cần phải tốn bộ nhớ lưu trữ Bootstrap, ngoài ra Bootstrap sẽ được tải về nhanh hơn vì có rất nhiều trang web sử dụng Bootstrap bằng CDN, do đó trình duyệt sẽ lưu lại Bootsrap trong bộ nhớ cache, và đến khi gặp trang web của chúng ta cũng sử dụng CDN thì trình duyệt không cần phải tải lại nữa mà dùng thẳng cái đã được lưu trong cache luôn.

Sau khi khai báo Bootstrap chúng ta vẫn có thể khai báo file style.css của riêng chúng ta, như thế chúng ta có thể ghi đè Bootstrap đẻ tùy biến theo ý chúng ta.

Ngoài ra khi sử dụng Bootstrap bạn phải khai báo thêm dòng này ở đầu file top.ejs:

<!DOCTYPE html>

 

NodeJS – Xây dựng ứng dụng MVC với Express

Trong các bài trước chúng ta đã tìm hiểu sơ qua về Express, trong phần này chúng ta sẽ dùng Express để xây dựng một ứng dụng ghi chú đơn giản, sử dụng mô hình MVC.

Mô hình MVC trong Express

Bản thân Express không được xây dựng theo mô hình MVC, nhưng module express-generator dùng để tạo một project Express thì lại tạo cho chúng ta một ứng dụng gần như là giống với MVC bởi vì các project này tồn tại 2 thứ:

  • Thư mục views chứa các file template (file có phần mở rộng là .ejs), các file này được dùng để hiển thị dữ liệu, tức là tương tự với phần Views trong MVC.
  • Thư mục routes được dùng để chuyển hướng các URL đến các hàm xử lý tương ứng, tức là tương tự với Controller trong MVC.

Vậy thì để ứng dụng của chúng ta vận hành theo đúng mô hình MVC thì thành phần còn thiếu là Model. Model có chức năng lưu trữ dữ liệu, thay đổi/cập nhật dữ liệu, hỗ trợ truy vấn dữ liệu. Và dĩ nhiên là code lưu trữ model cũng nên được lưu trong một thư mục riêng tách rời với viewsroutes.

Tạo ứng dụng Notes

Chúng ta sẽ xây dựng ứng dụng Notes (quản lý ghi chú) đơn giản.

Đầu tiên chúng ta tạo project:

C:\NodeJS>express --ejs notes
...

Sau đó cài các module đi kèm:

C:\NodeJS>cd notes
C:\NodeJS\notes>npm install
...

Tạo model

Trong thư mục gốc của project, chúng ta tạo một thư mục có tên models nằm chung với các thư mục views, routes… Trong thư mục models, chúng ta tạo một file có tên notes.js với nội dung sau đây:

var notes = [];
exports.update = exports.create = function(key, title, body) {
    notes[key] = { title: title, body: body };
}

exports.read = function(key) {
    return notes[key];
}

exports.destroy = function(key) {
    delete notes[key];
}

exports.keys = function() {
    return Object.keys(notes);
}

Trong đoạn code trên chúng ta tạo một mảng có tên notes dùng để lưu trữ các ghi chú, mỗi ghi chú bao gồm key (id), title (tiêu đề) và body (nội dung). Trong phần này chúng ta chỉ thực hiện lưu trữ trong bộ nhớ RAM cho đơn giản, tức làm mỗi lần tắt/khởi động server thì các ghi chú sẽ bị xóa, trong các phần sau chúng ta sẽ sử dụng cơ sở dữ liệu để lưu dữ liệu.

Ở đoạn code trên hàm update và hàm create giống nhau vì ở đây chúng ta chưa sử dụng cơ sở dữ liệu, trong các phần sau khi sử dụng cơ sở dữ liệu thì các hàm này sẽ phải tách ra.

Mỗi ghi chú sẽ được quản lý bằng key (khóa hay id).

Tùy chỉnh trang chủ

Đầu tiên chúng ta tạo một file có tên notes.js trong thư mục routes rồi để đó, chúng ta sẽ viết file này sau. Tiếp theo trong file app.js chúng ta thêm dòng sau vào cùng với các dòng require() ở đầu file:

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

Tiếp theo, cũng như trong các phần trước, chúng ta tách phần trang web ra thành các file top.ejs, bottom.ejs để có thể dùng một cách linh hoạt khi cần.

Trong thư mục views chúng ta tạo các file top.ejs, bottom.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>
    </p>
    </div>

Chúng ta in tiêu đề trang chủ và 2 đường link đến trang '/''/noteadd', trang /noteadd sẽ được viết sau.

</body>
</html>

Sau đó chỉnh lại file index.ejs như sau:

<% include top %>
<%
    var keys = notes.keys();
    if(keys) {
        keys.forEach(function(key) {
            var note = notes.read(key);
            %><p><%= key %>:
                <a href="/noteview?key=<%= key %>"><%= note.title %></a>
            </p><%
        });
    }
%>
<% include bottom %>

Trong file index.ejs, chúng ta có dùng đến biến notes, chúng ta dùng biến này để lấy key của từng ghi chú, sau đó hiển thị một đường link đến key đó, biến notes sẽ được gửi đến từ hàm render() nào đó, và đó sẽ là dòng code routing trong file routes/index.js, chúng ta sửa lại file routes/index.js như sau:

var express = require('express');
var router = express.Router();
var notes = require('../models/notes');

/* GET home page. */

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

module.exports = router;

Chúng ta thêm dòng require() đến module models/notes, sau đó truyền vào lời gọi res.render() để file index.ejs có thể sử dụng.

Đến đây chúng ta có thể chạy thử ứng dụng:

C:\NodeJS\notes>npm start

capture

Ở đây chúng ta chỉ thấy trang chủ và 2 đường link trống không, vì chúng ta chưa tạo một ghi chú nào cả. Khi bấm vào ADD Note thì sẽ có lỗi xảy ra vì chúng ta cũng chưa viết code cho trang này.

Tạo ghi chú

Do đó bây chúng ta sẽ thêm phần tạo ghi chú.

Trong file app.js chúng ta thêm dòng sau cùng với các dòng routing khác:

...
app.use('/noteadd', notes.add);
...

Trong file routes/notes.js, chúng ta thêm đoạn code sau:

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

Đoạn code trên rất đơn giản, chúng ta dùng hàm res.render() để gọi file noteedit.ejs và truyền các tham số title, docreate, notekey, note vào file này rồi trả về cho người dùng. Ở đây tham số docreate có cho biết chúng ta đang cập nhật hay tạo mới một ghi chú, nếu là cập nhật thì tham số note sẽ là một đối tượng nào đó, còn tạo mới thì đối tượng này rỗng (undefined), trong phần cập nhật ghi chú chúng ta sẽ hiểu rõ thêm 2 tham số này,.

Tiếp theo trong thư mục views, chúng ta tạo file noteedit.ejs với nội dung như sau:

<% include top %>
<form method='POST' action='/notesave'>
    <input type='hidden' name='docreate' value='<%= docreate ? "create" : "update"%>'>
    <p>Key: <input type='text' name='notekey' value='<%= note ? notekey : "" %>'></p>
    <p>Title: <input type='text' name='title' value='<%= note ? note.title : "" %>'></p>
    <br/>
    <textarea rows=5 cols=40 name='body'><%= note ? note.body : "" %></textarea>
    <br/>
    <input type='submit' value='Submit' />
</form>
<% include bottom %>

Như chúng ta đã biết, cả phần tạo và cập nhật ghi chú dùng chung một hàm, và dùng chung một template là file noteedit.ejs. Ở đây chúng ta tạo một form có các trường để nhập dữ liệu cho một ghi chú, nếu thao tác là cập nhật thì chúng ta điền các dữ liệu có sẵn vào các trường này, người dùng chỉ việc thay đổi các trường cần thiết, nếu là tạo mới thì các trường này sẽ rỗng.

Form này sẽ gửi đến trang /notesave, và form này dùng phương thức POST để gửi, do đó bây giờ chúng ta phải làm thêm trang này. Trong file app.js chúng ta thêm dòng sau:

...
app.post('/notesave', notes.save);
...

Tiếp theo trong file routes/notes.js chúng ta thêm đoạn code sau để xử lý đương dẫn /notesave:

exports.save = function(req, res, next) {
    if (req.body.docreate === 'create') {
        notes.create(req.body.notekey,
                     req.body.title,
                     req.body.body);
    } else {
        notes.update(req.body.notekey,
                     req.body.title,
                     req.body.body);
    }
    res.redirect('/noteview?key='+req.body.notekey);
}

Đoạn code trên cũng rất đơn giản, chúng ta kiểm tra tham số docreate là gì, nếu là tạo mới (create) thì chúng ta tạo một phần tử mới trong mảng notes bằng phương thức notes.create(), nếu là cập nhật thì chúng ta cập nhật notes.update().

Sau đó chúng ta chuyển hướng trang web đến trang /noteview.

Và bởi vì form truyền lên theo phương thức POST lên dữ liệu sẽ nằm trong thuộc tính req.body được tạo từ module bodyParser. Trong file app.js Express đã tự động thêm module này cho chúng ta trong dòng app.use(bodyParser.json());

Bạn có thể chạy lại project và có thể bấm nút ADD Note để tạo ghi chú mới. Khi bấm vào nút submit thì bạn sẽ được một trang báo lỗi 404, đơn giản là vì chúng ta chưa làm trang /noteview.

capture

Tuy nhiên nếu về lại trang Home thì chúng ta vẫn thấy ghi chú đã được thêm vào và đã hiển thị lên trang chủ.

capture

Xem ghi chú

Bây giờ chúng ta sẽ làm trang /noteview để xem chi tiết một ghi chú.

Trong file app.js chúng ta thêm dòng sau:

...
app.use('/noteview', notes.view);
...

Trong file routes/notes.js chúng ta thêm đoạn code sau:

exports.view = function(req, res, next) {
    var note = undefined;
    if(req.query.key) {
        note = notes.read(req.query.key);
    }
    res.render('noteview', {
        title: note ? note.title : "",
        notekey: req.query.key,
        note: note
    });
}

Đoạn code trên sẽ xử lý đường dẫn URL /noteview, ở đây chúng ta phải kiểm tra trước khi trả về dữ liệu cho trình duyêt, bằng cách kiểm tra xem khóa có rỗng hay không, sau đó trong hàm res.render() chúng ta cũng kiểm tra xem title có rỗng hay không để trả về dữ liệu cho thích hợp. Bởi vì người dùng có thể nhập đường dẫn bằng tay chứ không chỉ có dùng chuột để click vào đường link trên trang web.

Cuối cùng chúng ta tạo một file có tên noteview.ejs trong thư mục views với nội dung như sau:

<% include top %>
<h3><%= note ? note.title : "" %></h3>
<p><%= note ? note.body : "" %></p>
<p>Key: <%= notekey %></p>
<% if (notekey) { %>
    <hr/>
    <p>
        <a href="notedestroy?key=<%= notekey %>">Delete<a/>
        <a href="noteedit?key=<%= notekey %>">Edit</a>
    </p>
<% } %>
<% include bottom %>

Các đoạn code trên sẽ chịu trách nhiệm hiển thị nội dung ghi chú. Ngoài ra còn hiển thị 2 đường link đến trang /notedestroy dùng để xóa ghi chú và /noteedit dùng để chỉnh sửa ghi chú.

capture

Tuy nhiên nếu click vào 2 đường link đó thì chúng ta sẽ được một trang báo lỗi 404, lý do cũng đơn giản là vì chúng ta chưa viết hàm routing cho 2 đường dẫn này.

Chỉnh sửa ghi chú

Trong file app.js chúng ta thêm dòng sau:

...
app.use('/noteedit', notes.edit);
...

Trong file routes/notes.js chúng ta thêm đoạn code sau:

exports.edit = function(req, res, next) {
    var note = undefined;
    if(req.query.key) {
        note = notes.read(req.query.key);
    }
    res.render('noteedit', {
        title: note ? ("Edit " + note.title) : "Add a Note",
        docreate: note ? false : true,
        notekey: req.query.key,
        note: note
    });
}

Như bạn đã biết, cả 2 hàm tạo và chỉnh sửa đều dùng chung một template là file notedit.ejs, do đó ở đây chúng ta không cần phải tạo một file .ejs nào khác. Khác với hàm tạo ghi chú, ở đây chúng ta nhận được một key của một ghi chú có sẵn, chúng ta sẽ truyền dữ liệu của ghi chú này vào các tham số trả về. Đầu tiên chúng ta khai báo một biến note có giá trị undefined, sau đó chúng ta tìm đối tượng ghi chú trong mảng notes dựa theo key, nếu tìm thấy thì truyền vào hàm res.render(), nếu không thì chúng ta truyền các tham số giống như khi tạo mới một ghi chú. Như thế sẽ phòng ngừa được việc người dùng nhập đường dẫn bằng tay lên trình duyệt và đưa đường dẫn sai.

Bây giờ bạn có thể thực hiện cập nhật ghi chú.

Xóa ghi chú

Tương tự với các chức năng trên, đầu tiên chúng ta thêm dòng sau vào file app.js:

...
app.use('/notedestroy', notes.destroy);

Tiếp theo trong file routes/notes.js, chúng ta thêm đoạn code sau:

exports.destroy = function(req, res, next) {
    var note = undefined;
    if(req.query.key) {
        note = notes.read(req.query.key);
    }
    res.render('notedestroy', {
        title: note ? note.title : "",
        notekey: req.query.key,
        note: note
    });
}

Cũng tương tự như các hàm khác, ở đây chúng ta kiểm tra xem dữ liệu gửi lên có hợp lệ hay không rồi trả về trong trang /notedestroy.

Kế tiếp chúng ta tạo file notedestroy.ejs trong thư mục views như sau:

<% include top %>
<form method="POST" action='notedodestroy'>
    <input type='hidden' name='notekey' value='<%= note ? notekey : "" %>'>
    <p>Delete <%= note.title %> ?</p>
    <br/>
    <input type="submit" value="DELETE" />
    <a href="/noteview?key=<%= notekey %>">Cancel</a>
</form>
<% include bottom %>

Ở đây chúng ta hiển thị một form cho người dùng xác nhận việc xóa file, form này sẽ gửi đến đường dẫn /notedodestroy với phương thức POST. Do đó bây giờ chúng ta phải xử lý đường dẫn này.

Đầu tiên chúng ta thêm dòng sau vào file app.js:

...
app.post('/notedodestroy', notes.dodestroy);
...

Tiếp theo chúng ta thêm đoạn code này vào file routes/notes.js:

exports.dodestroy = function(req, res, next) {
   notes.destroy(req.body.notekey);
   res.redirect('/');
}

Đoạn code trên sẽ xóa ghi chú ra khỏi mảng notes rồi chuyển hướng về trang '/'.

Bây giờ chúng ta có thể sử dụng chức năng xóa được rồi.

capture

NodeJS – Tạo REST Service

Trong bài trước chúng ta đã tìm hiểu cách tạo một gói tin HTTP, trong phần này chúng ta sẽ tìm hiểu cách tạo các truy vấn REST và cách viết một REST service.

Bản thân việc gọi các REST service đã là bất đồng bộ, tức là khi chúng ta truy vấn một hàm REST thì REST server sẽ gọi một hàm để xử lý và một hàm callback để gửi kết quả trả về, tất cả đều thông qua giao thức HTTP.

Về REST service

REST là viết tắt của REpresentational State Transfer. Đây là một chuẩn web dựa trên giao thức HTTP. Mục đích chính của REST là hỗ trợ truy cập tài nguyên thông qua giao thức HTTP. REST được giới thiệu lần đầu bởi Roy Fielding vào năm 2000.

Công việc của một REST server chỉ đơn giản là cung cấp quyền truy cập tài nguyên, một REST client sẽ dùng các quyền truy cập đó để lấy tài nguyên, tất cả đều thông qua giao thức HTTP. Các tài nguyên được xác định thông qua URI. REST sử dụng 2 định dạng dữ liệu là JSON và XML, nhưng JSON phổ biến hơn.

Nói một cách đơn giản thì giao thức HTTP ban đầu được tạo ra là để trả các website về cho trình duyệt. Sau này người ta phát minh ra chuẩn REST, HTTP không chỉ được dùng để trả về các nội dung HTML mà có thể là bất cứ thứ gì.

Ví dụ

Chúng ta sẽ viết lại server tính số fibonacci, nhưng lần này thay vì trả về một trang web HTML thì server chỉ trả về một chuỗi JSON.

Mặc dù Express ra đời với mục đích chính là để xây dựng các trang web, nhưng chúng ta cũng có thể dùng chính module Express để viết các hàm REST API. Express cho phép so sánh/tìm kiếm chuỗi trong các đường dẫn URL.

Đoạn code routing một đường dẫn URL tới một hàm xử lý cũng tương tự như khi viết một routing bình thường, ví dụ:

app.get('/user/:id', function(req, res) {
    res.send('user ' + req.params.id);
});

Trong dòng code trên, URL /user/:id có một tham số là :id, Express sẽ tách phần tham số này ra và gán vào trường req.params.id.

Bây giờ chúng ta viết lại một server để tính số fibonacci và trả về kiểu JSON, chúng ta tạo một file có tên fiboserver.js với nội dung như sau:

var math = require('./math');
var express = require('express');
var app = express();
app.get('/fibonacci/:n', function(req, res, next) {
    var result = math.fibonacciLoop(req.params.n);
    res.send({
        n: req.params.n,
        res: result
    });
});
app.listen(3002);
console.log('Fibonacci REST service is listening on port 3002');

Ở đoạn code trên chúng ta dùng 2 module là Expressmath do chúng ta viết trong các phần trước.

app.get('/fibonacci/:n', function(req, res, next) {
    ...
});

Ở đây chúng ta dùng phương thức app.get() thay vì dùng app.use() để xử lý phần routing. Tham số đầu tiên là URL, tham số thứ 2 là hàm callback dùng để gửi kết quả về client.

var result = math.fibonacciLoop(req.params.n);
res.send({
    n: req.params.n,
    res: result
});

Trong hàm callback đó chúng ta tính số fibonacci rồi lưu trong biến result, sau đó chúng ta gửi kết quả về thông qua phương thức res.send(). Tham số của phương thức res.send() là một đối tượng  được tạo theo cú pháp JSON, ở đây đối tượng này gồm có 2 tham số là nres, trong đó n chỉ đơn giản là tham số được gửi lên từ client, còn res là kết quả tính số fibonacci từ module math.

Chúng ta chạy file này, sau đó bạn có thể vào trình duyệt và gõ đường dẫn localhost:3002/fibonacci/<tham số> với tham số là một số nguyên bất kỳ, server sẽ trả về chuỗi JSON kết quả là số fibonacci cho bạn.

capture

Tuy nhiên mục đích chính của REST không phải chỉ là để trả kết quả về cho trình duyệt, bất kỳ chương trình nào có hỗ trợ giao thức HTTP đều có thể gọi các hàm REST API, kể cả smartphone, tablet, smartwatch… Chúng ta sẽ viết một client đơn giản để gửi yêu cầu đến REST service này.

Chúng ta tạo một file có tên fiboclient.js với nội dung như sau:

var http = require('http');
var util = require('util');
[
 "/fibonacci/30",
 "/fibonacci/20",
 "/fibonacci/10",
 "/fibonacci/9",
 "/fibonacci/8",
 "/fibonacci/7",
 "/fibonacci/6",
 "/fibonacci/5",
 "/fibonacci/4",
].forEach(function(path) {
    var req = http.request({
        host: "localhost",
        port: 3002,
        path: path,
        method: 'GET' 
    }, function(res) {
        res.on('data', function(chunk) {
            util.log('BODY :' + chunk);
        });
    });
    req.end();
});

Trong đoạn code trên chúng ta tạo một mảng là danh sách các đường dẫn, sau đó chúng ta lặp qua mảng này, mỗi lần lặp chúng ta gửi một yêu cầu HTTP lên server với đường dẫn tương ứng rồi in kết quả trả về.

var req = http.request({
    host: "localhost",
    port: 3002,
    path: path,
    method: 'GET' 
}, function(res) {
    res.on('data', function(chunk) {
        util.log('BODY :' + chunk);
    });
});

Chúng ta tạo một đối tượng http.ServerResponse thông qua phương thức http.request(). Trong đó chúng ta điền các thông tin cơ bản như host là địa chỉ server, port là cổng, path là đường dẫn, method là phương thức, và một hàm callback để nhận kết quả trả về từ server, hàm này nhận một tham số là res, tham số này lắng nghe sự kiện data, sự kiện này xảy ra khi có dữ liệu gửi về từ server.

Chạy đoạn code trên chúng ta được kết quả như sau:

C:\Users\PhoCode>node fiboclient.js
26 Sep 08:59:32 - BODY :{"n":"30","res":832040}
26 Sep 08:59:32 - BODY :{"n":"20","res":6765}
26 Sep 08:59:32 - BODY :{"n":"10","res":55}
26 Sep 08:59:32 - BODY :{"n":"9","res":34}
26 Sep 08:59:32 - BODY :{"n":"8","res":21}
26 Sep 08:59:32 - BODY :{"n":"7","res":13}
26 Sep 08:59:32 - BODY :{"n":"6","res":8}
26 Sep 08:59:32 - BODY :{"n":"5","res":5}
26 Sep 08:59:32 - BODY :{"n":"4","res":3}