NodeJS – Hàm callback

5/5 - (1 vote)

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

0 0 votes
Article Rating
Subscribe
Thông báo cho tôi qua email khi
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

3 Comments
Inline Feedbacks
View all comments
vic
vic
6 năm trước

Admin cho minh hỏi:

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

Vậy cuối cùng đoạn trên dùng để làm gì nhỉ, mình thấy khai báo ra nhưng k biết mục đích làm gi trong phần trên.

vic
vic
6 năm trước

Va khi chạy nó báo lỗi đoạn này:

routes.configure(models);