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


Được đăng vào ngày 06/10/2016 | 2 bình luận
NodeJS – Lưu trữ dữ liệu trên file
5 (100%) 2 votes

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:

<% 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.

capture

untitled







Bình luận (2)

  1. tuoi

    ad cho mình hỏi sao mình làm giống như hướng dẫn nhưng trong folder Notes chưa có file nào cả hoặc có thì khi gừi request http://localhost:3000/ thì không hiện gì cả mà có 7 dấu : trên 7 hàng. khi console.log thì biến notes có nội dung là

    { connect: [Function],
    disconnect: [Function],
    create: [Function],
    update: [Function],
    read: [Function],
    destroy: [Function],
    titles: [Function] }

    1. Phở Code Admin

      à xin lỗi bạn, mình quên gọi hàm titles(), bạn sửa lại hàm index trong file routes/index.js như thế này nhé:

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

      Mình đã sửa lại bài viết, cám ơn bạn đã tìm ra lỗi!

Trả lời


Lưu ý: bọc code trong cặp thẻ [code language="x"][/code] để highlight code.


Ví dụ:


[code language="cpp"]


    std::cout << "Hello world";


[/code]



Các ngôn ngữ được hỗ trợ gồm: actionscript3, bash, clojure, coldfusion, cpp, csharp, css, delphi, diff, erlang, fsharp, go, groovy, html, java, javafx, javascript, latex, matlab, objc, perl, php, powershell, python, r, ruby, scala, sql, text, vb, xml.

Thư điện tử của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *