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.
Sửa template
File index.ejs
trong thư mục views
sẽ được sửa lại một tí như sau:
<% include top %> <% if(notes) { for(var i in notes) { %><p><%= notes[i].key %>: <a href="/noteview?key=<%= notes[i].key %>"><%= notes[i].title %></a> </p><% } } %> <% include bottom %>
Ở đâ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.