Sai lầm phổ biến khi học NodeJS hay mắc phải


1055

Tạo vòng lặp dữ liệu lớn làm treo ứng dụng

Javascript trong NodeJS (cũng giống như trong trình duyệt) là ngôn ngữ đơn luồng. Hay nói cách khác, ở cùng một thời điểm, chỉ có một luồng được thực thi, một task được thực hiện. Thay vào đó, để tăng hiệu suất ứng dụng, NodeJS sẽ xử lý theo kiểu bất đồng bộ.

Bạn có thể tham khảo tại đây : Async/Await là gì ? Tính năng tuyệt vời của Javascript

An Introduction into Event Loops in PHP — SitePoint

Ví dụ, truy xuất vào cơ sở dữ liệu từ ứng dụng NodeJS để lấy dữ liệu. Trong lúc chờ cơ sở dữ liệu trả về kết quả thì NodeJS có thể tranh thủ làm các task khác.

// Trying to fetch an user object from the database.
// NodeJS is free to run other parts of the code from
// the moment this function is invoked..
db.User.get(userId, function(err, user) {
// .. until the moment the user object has been retrieved here
})

Tuy nhiên, không phải lúc nào cũng bất đồng bộ, bản thân Javascript là ngôn ngữ đồng bộ, nên cũng giống như các ngôn ngữ khác.

Nếu một đoạn code mà cần thời gian dài để thực hiện có thể làm treo ứng dụng. Đặc biệt là các vòng lặp. Bạn có thể xem đoạn code bên dưới đây:

function sortUsersByAge(users) {
users.sort(function(a, b) {
return a.age < b.age ? -1 : 1
})
}

Đây là hàm dùng để sắp xếp một mảng các users. Đây là hàm thuần túy đồng bộ, tức là phải làm xong hàm này thì mới thực hiện các hàm khác. Nếu mảng users có số lượng ít thì không sao. Nhưng nếu mảng users mà có rất nhiều phần tử thì sao?

Đấy là chưa kể ứng dụng NodeJS còn phải phục vụ hàng ngàn khách truy cập nữa?

Đây chính là vấn đề, có thể làm sập server như chơi.

Để khắc phục vấn đề này thì có nhiều giải pháp, tùy vào hoàn cảnh ứng dụng. Ví dụ, nếu mảng users được lấy từ cơ sở dữ liệu (CSDL)  thì tốt nhất nên để CSDL tự sắp xếp trước khi trả về cho ứng dụng NodeJS.

Về nguyên tắc là không nên để ứng dụngNodeJS (ứng dụng trực tiếp xử lý request từ người dùng) phải thực hiện một tác vụ quá lâu.

Sử dụng Return rỗng với Callback

Callback có lẽ là khái niệm mà chắc hẳn ai học Nodejs đều phải biết. Tuy nhiên, cách sử dụng như nào cho đúng thì mỗi người một cách. Trong NodeJS, callback là cách để cách thành phần bất đồng bộ tương tác với nhau.

Một vấn đề phổ biến của NodeJS mà các bạn developer ít kinh nghiệm hay mắc phải đó chính là gọi callback nhiều lần.

Thông thường, một API bất đồng bộ sẽ có một tham số cuối cùng là một hàm, được sử dụng task được thực hiện bởi API hoàn thành.

module.exports.verifyPassword = function(user, password, done) {
if(typeof password !== ‘string’) {
done(new Error(‘password should be a string’))
return
}

computeHash(password, user.passwordHashOpts, function(err, hash) {
if(err) {
done(err)
return
}

done(null, hash === user.passwordHash)
})
}

Bạn nhìn đoạn code trên có thấy điều gì lạ không?

Tại sao lại phải sử dụng từ khóa return dưới mỗi callback vậy? Lý do họ làm như vậy vì khi gọi callback không làm kết thúc hàm, chương trình vẫn tiếp tục thực hiện các lệnh bên dưới. Đó là điều không đúng logic.

Tuy nhiên, nhìn việc return không có giá trị trả về cứ thế nào ấy. Mặc dù, trong các hàm kiểu bất đồng bộ thì việc return giá trị không có nhiều ý nghĩa lắm, ngoại trừ việc dùng return để kết thúc hàm.

Thay vào đó, chúng ta có  thể return chính hàm callback, nhìn code sẽ đẹp hơn nhiều.

if(err) {
return done(err)
}

Sử dụng Callback lồng nhau NodeJS

Sử dụng Callback lồng nhau tới mức mất kiểm soát, người ta gọi là “callback hell”. Đây là một bad smell cực tệ với các ứng dụng NodeJS, làm cho việc bảo trì mã nguồn trở nên vô vàn khó khăn.

Ví dụ một callback lồng nhau:

function handleLogin(..., done) {
db.User.get(..., function(..., user) {
if(!user) {
return done(null, ‘failed to log in’)
}
utils.verifyPassword(..., function(..., okay) {
if(okay) {
return done(null, ‘failed to log in’)
}
session.login(..., function() {
done(null, ‘logged in’)
})
})
})
}

Có nhiều cách để xử lý callback lồng nhau.

Mình có thể khắc phục điều này bằng cách sử dụng async/await

function handleLogin(done) {
async.waterfall([
function(done) {
db.User.get(..., done)
},
function(user, done) {
if(!user) {
return done(null, ‘failed to log in’)
}
utils.verifyPassword(..., function(..., okay) {
done(null, user, okay)
})
},
function(user, okay, done) {
if(okay) {
return done(null, ‘failed to log in’)
}
session.login(..., function() {
done(null, ‘logged in’)
})
}
], function() {
// ...
})
}

Sử dụng Callback để chạy đồng bộ

Kỹ thuật bất đồng bộ với Callback tuy không phải là kỹ thuật chỉ có trên Javascript hay NodeJS. Nhưng dường như sự phổ biến đã biến nó trở thành “tiêu chuẩn”. Giống như việc nói tới lập trình hướng đối tượng là người ta nghĩ ngay tới java vậy.

Với nhiều developer quen với các ngôn ngữ chạy đồng bộ như PHP, Java… Thì có xu hướng viết code đồng bộ trong NodeJS. Và họ sử dụng Callback như một công cụ để viết code đồng bộ.

Tuy nhiên, Callback trong Javascript có thể không hoạt động như ý muốn của bạn.

Tham khảo đoạn code bên dưới:

function testTimeout() {
console.log(“Begin”)
setTimeout(function() {
console.log(“Done!”)
}, duration * 1000)
console.log(“Waiting..”)
}

Như đoạn code trên thì màn hình sẽ in log theo thứ tự như sau, có vẻ không đúng thứ tự phải không?

'Begin'
'Waiting..'
'Done!'

Bất kể điều gì bạn muốn thực hiện sau callback đều phải được gọi bên trong hàm đó.

Nhầm lẫn giữa “exports” và “module.exports” trong NodeJS

NodeJS coi mỗi một file javascript là một module nhỏ , độc lập với các file khác. Người ta gọi là tính đóng gói. Để một hàm trong file javascript có thể được sử dụng bởi file thì bạn cần phải export nó ra.

Giả sử trong package của bạn có 2 file javascript: one.js và two.js

Để two.js có thể gọi một hàm verifyPassword(...) trong one.js thì nó phải được export.

// one.js
exports.verifyPassword = function(user, password, done) {
 ...
}

Khi bạn làm điều này, bất kể file nào require('one.js') đều có thể sử dụng hàm verifyPassword(...).

// two.js
require('one.js')
{ verifyPassword: function(user, password, done) { ... } }

Tuy nhiên, làm thế nào để export trực tiếp hàm verifyPassword() mà không phải định nghĩa nó như một thuộc tính của one.js? Câu trả lời là sử dụng module.export

// one.js
module.exports = function(user, password, done) { ... }

Nhìn có vẻ đơn giản vậy thôi nhưng sự khác nhau giữa exports và module.exports đang trở thành chủ đề bàn tán của rất nhiều NodeJS developer.

Throwing Errors bên trong Callbacks

Javascript cũng có Exception như bao ngôn ngữ khác như Java, C++. Bạn hoàn toàn có thể “Throw” hoặc try-catch một Exception.

Ví dụ như đoạn code dưới đây, try-catch sẽ bắt được Exception.

function slugifyUsername(username) {
if(typeof username === ‘string’) {
throw new TypeError(‘expected a string username, got '+(typeof username))
}
// ...
}

try {
var usernameSlug = slugifyUsername(username)
} catch(e) {
console.log(‘Oh no!’)
}

Tuy nhiên,  try-catch lại hoạt động không như mong đợi trong trường hợp của hàm bất đồng bộ.

Ví dụ, bạn muốn đoạn code an toàn, không bị crash khi có Exception, nên bạn thêm try-catch cho cả một đoạn code lớn. Mà trong đó có nhiều hàm bất đồng bộ. Thì try-catch sẽ không hoạt động đúng.

try {
db.User.get(userId, function(err, user) {
if(err) {
throw err
}
// ...
usernameSlug = slugifyUsername(user.username)
// ...
})
} catch(e) {
console.log(‘Oh no!’)
}

Như trong trường hợp này, khi callback b.User.get(...) có lỗi mà bạn lại throw lỗi thì các Exception của các hàm tiếp theo sẽ không thể catch được nữa.

Sử dụng Console.log một cách bừa bãi trong NodeJS

Trong NodeJS, Console.log cho phép bạn in tất cả mọi thứ ra màn hình console. Không giống như Java, bạn chỉ có thể truyền vào một String. Với Console.log, bạn có thể truyền vào một Object, nó sẽ in ra dưới dạng một Object theo đúng nghĩa đen.

Chính vì Console.log tiện lợi như vậy nên nhiều bạn developer sử dụng console.log ở khắp mọi nơi. Tuy nhiên, bạn nên tránh việc đặt console.log ở tất cả mọi nơi để debug. Sau này không cần thì comment nó lại.

Thay vào đó, bạn nên sử dụng một thư viện hỗ trợ đặt log. Tùy thuộc vào hình thức bạn release sản phẩm như: release chính thức, hay debug… mà bật/tắt việc in các log một cách tự động thay vì phải đi xóa console.log thủ công.

Hãy tham khảo thêm tại đây :

Kết luận

Qua bài viết, chúng ta đã hiểu được những sai lầm thường mắc phải. Vì vậy hãy tìm hiểu rõ những điều đó từng chút một. Không lạm dụng hoặc làm theo cách làm trên mạng mà không có chọn lọc. Để viết code thật clean.


Like it? Share with your friends!

1055