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:
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.ejs
và bottom.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.js
và routes/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ộ.