Kể từ khi xuất hiện, Node.js đã nhận được rất nhiều ý kiến khen chê. Những cuộc tranh luận về nó vẫn đang diễn ra chưa biết khi nào kết thúc. Điều mà chúng ta thường xem nhẹ trong các cuộc tranh luận này là mọi ngôn ngữ lập trình và nền tảng đều bị đưa ra phê bình dựa trên những vấn đề nào đó được tạo ra bởi cách chúng ta sử dụng nền tảng ấy. Bỏ qua vấn đề về sự khó khắn để viết code Node.js an toàn và sự dễ dàng tạo ra code chạy đồng thời, nền tảng này đã xuất hiện một thời gian dài và đã được sử dụng để xây dựng nhiều trang web lớn và phức tạp. Những website này chịu tải tốt và chứng tỏ được khả năng của mình sau thời gian dài hoạt động trên Internet.

Tuy nhiên, như bất kỳ nền tảng nào khác, Node.js cũng bị ảnh hưởng bởi lỗi của lập trình viên. Một số lỗi gây giảm hiệu suất trong khi một số khác khiến Node.js hoàn toàn không thể đáp ứng được điều bạn mong muốn. Trong bài viết này, chúng ta sẽ xem xét tới mười lỗi thường gặp ở những lập trình viên mới làm quen Node.js và cách để tránh những lỗi này.

Học lập trình Node.js trực tuyến

Lỗi #1: Blocking the event loop (chặn sự kiện lặp)

Javascript trong Node.js (cũng như trong trình duyệt) cung cấp một môi trường đơn luồng. Điều này có nghĩa là sẽ không có hai phần của ứng dụng được chạy đồng thời; thay vào đó, những tác vụ đồng thời này được thực hiện thông qua sự điều chỉnh hoạt động I/O không đồng bộ. Ví dụ, một request từ Node.js tới cơ sở dữ liệu để lấy dữ liệu là một yêu cầu cho phép Node.js tập trung vào một vài phần khác của ứng dụng.

// Trying to fetch an user object from the database. Node.js 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
})

Học lập trình Node.js nâng cao

Tuy nhiên một đoạn mã hướng xử lý (CPU-bound) trong trường hợp Node.js có hàng ngàn máy khách kết nối tới có thể chặn vòng lặp, khiến cho tất cả các máy khách khác phải chờ. Mã hướng xử lý cố gắng sắp xếp một mảng lớn, chạy một vòng lặp kéo dài vô cùng. Ví dụ:

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

Gọi hàm "sortUsersByAge" có thể vẫn ổn nếu chạy với một mảng "users" nhỏ nhưng nếu mảng này lớn thì sẽ tác động rất lớn tới hiệu suất tổng thể của hệ thống. Nếu đây là một công việc nhất thiết phải hoàn thành và bạn chắc chắn rằng không hoạt động nào đang chờ trong vòng lặp này thì không sao cả. Ngược lại, nếu hệ thống của bạn đang cố gắng phục vụ hàng ngàn khách hàng cùng lúc thì đây sẽ là thảm họa.

Nếu mảng "users" này được lấy ra từ cơ sở dữ liệu thì giải pháp nên dùng ở đây là sắp xếp nó ngay từ cơ sở dữ liệu. Nếu sự kiện lặp bị chặn bởi một vòng lặp nào đó đang cố gắng tính toán tổng của một loạt giao dịch trong lịch sử giao dịch rất dài chẳng hạn thì nó nên được hoãn lại và đưa vào hàng đợi thay vì gây ảnh hưởng tới sự kiện vòng lặp.

Như bạn thấy, không có giải pháp trực tiếp nào đối với loại lỗi này bởi mỗi trường hợp cần có cách giải quyết riêng. Ý tưởng cơ bản là không làm việc chuyên sâu về CPU trong phần mặt của Node.js - những máy khách đồng thời kết nối vào.

Lỗi #2: Gọi hàm Callback nhiều hơn một lần

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)
    })
}

Chú ý tới cách sử dụng return sau mỗi lần "done" được gọi, ngoại trừ hàm done ở cuối cùng, đây là do gọi hàm callback không tự động kết thúc xử lý hàm hiện tại. Nếu bỏ lần gọi "return" đầu tiên đi thì dù truyền vào mật khẩu không phải là string thì hàm computeHash cũng vẫn được gọi. Tùy vào hoàn cảnh cụ thể khi hàm computeHash được gọi mà callback "done" có thể được gọi nhiều lần. Bất kì ai dùng hàm này từ bất cứ đâu cũng có thể mất cảnh giác khi callback bị gọi nhiều lần.

Để tránh lỗi này thì cần phải thật cẩn thận trong quá trình lập trình. Một vài lậ trình viên Node.js có thói quen thêm return trước mỗi callback:

if(err) {
	return done(err)
}

Trong nhiều hàm bất đồng bộ, giá trị trả về gần như không có ý nghĩa nên cách này thường có thể giảm bớt lỗi.

Lỗi #3: Dùng callback lồng nhau nhiều cấp

Callback lồng nhiều cấp thường được gọi là "callback hell". Bản thân nó không phải là lỗi nhưng nó thường gây ra lỗi khiến các đoạn mã vượt ra khỏi tầm kiểm soát.

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’)
            })
        })
    })
}

Học lập trình Node.js kiếm việc làm

Tác vụ càng phức tạp thì cách này càng tệ. Với việc lồng nhiều callback vào nhau như vậy chúng ta có thể kết thúc với một đống lỗi khó nhằn, những đoạn mã khó đọc và khó bảo trì. Một cách để giải quyết vấn đề này là khai báo các tác vụ thành các hàm nhỏ sau đó liên kết tới chúng. Một trong số những giải pháp được cho là tốt nhất đối với vấn đề này đó là dùng một gói tiện ích Node.js có thể hoạt động với mô hình Javascript bất đồng bộ, ví dụ như Async.js

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() {
        // ...
    })
}

Tương tự như "async.waterfall" còn có một số hàm khác mà Async.js cung cấp để tương tác với các mô hình bất đồng bộ khác. Để ngắn gọn, chúng ta dùng các ví dụ đơn giản trong bài viết này nhưng thực tế thì thường phức tạp hơn.

Lỗi #4: Trông đợi Callback chạy tuần tự

Lập trình bất đồng bộ với callback có thể không lạ lẫm gì đối với Javascript và Node.js nhưng không hẳn đã là phổ biến. Với các ngôn ngữ lập trình khác, chúng ta đã quen với việc thứ tự thực hiện các câu lệnh có thể dễ dàng thấy trước, trừ một số lệnh được dẫn dắt đặc biệt để nhảy tới các đoạn mã khác. Cho dù thế thì nhưng cá biệt ấy cũng bị giới hạn trong các mệnh đề điều kiện, vòng lặp hoặc lời gọi hàm.

Tuy nhiên, trong Javascript, với callback thì một hàm sẽ không chạy tốt được cho tới khi tác vụ mà nó chờ kết thúc. Việc thực hiện hàm hiện tại sẽ chạy liên tục cho tới khi kết thúc mà không bị gián đoạn.

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

Như bạn thấy, gọi hàm "testTimeout" đầu tiên sẽ in ra "Begin", sau đó in ra "Waiting .." kèm theo thông báo "Done!" sau khoảng một giây.
Bất cứ điều gì cần phải xảy ra sau khi gọi callback cần phải được gọi từ bên trong nó.

Lỗi #5: gắn "exports" thay vì "module.export"

Node.js coi mỗi file là một module nhỏ độc lập, nếu gói của bạn có hai file, giả sử tên là "a.js" và "b.js", khi "b.js" gọi tới các hàm của "a.js" thì "a.js" phải export ra bằng cách thêm các thuộc tính vào đối tượng export:

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

Khi thực hiện xong việc này, bất cứ module nào sử dụng require "a.js" sẽ nhận được một đối tượng với hàm thuộc tính "verifyPassword":

// b.js
require(‘a.js’) // { verifyPassword: function(user, password, done) { ... } } 

Tuy nhiên, nếu như chúng ta muốn export trực tiếp hàm này hoặc không giống như một vài đối tượng khác thì có thể export chồng lên nhau và chúng ta sẽ không coi nó như một biến toàn cục: 

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

Chú ý cách chúng ta xử lý "exports" như một thuộc tính của đối tượng module. Sự khác biệt giữa "module.exports" và "exports" là rất quan trọng và thường là nguyên nhân khiên nhiều lập trình viên mới tiếp xúc Node.js thất vọng.

Lỗi #6: Throw error từ bên trong Callback

Javascript có khái niệm "ngoại lệ" (exception). Bắt chước cú pháp của hầu hết tất cả các ngôn ngữ truyền thống với sự hỗ trợ xử lý ngoại lệ, chẳng hạn như Java và C++, JavaScript có thể "throw" và "catch" trường hợp ngoại lệ trong khối try-catch:

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 sẽ không như những gì bạn mong đợi trong những tình huống không đồng bộ. Ví dụ, nếu bạn muốn bảo vệ một phần lớn mã với rất nhiều hoạt động không đồng bộ với một khối try-catch lớn, try-catch không chắc đã có thể làm điều bạn muốn:

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

Nếu lời gọi tới "db.User.get" được gọi bất đồng bộ, phạm vi chứa khối try-catch sẽ nằm ngoài phạm vi mà khối try-catch này có thể bắt lỗi từ bên trong các callback.

Đây là một cách khác mà Node.js xử lý lỗi và điều này  tạo nên mô hình (err, ...) trong tất cả các tham số của hàm callback - tham số đầu tiên là lỗi nếu như có lỗi xảy ra.

Lỗi #7: Coi kiểu Number như kiểu Integer

Số trong Javascript là dạng dấu chấm động (floating points), không có kiểu số nguyên. Bạn có thể không cho rằng đây là một vấn đề, chẳng mấy khi có số đủ lớn để gây tràn bộ nhớ của số dạng float. Đó chính xác là lúc các lỗi liên quan tới vấn đề này xảy ra. Do số dạng dấu chấm động chỉ có thể chứa các biểu diễn của số nguyên tới một giá trị nhất định, ngoài mức giới hạn đó, bất kì một phép tính nào cũng lập tức rối loạn hết. Có vẻ kì lạ nhưng phép so sánh sau đây cho kết quả là đúng trong Node.js:

Math.pow(2, 53)+1 === Math.pow(2, 53)

Thật không may, những khuyết thiếu với những con số trong JavaScript không kết thúc ở đây. Mặc dù Number là dạng dấu chấm động, các toán tử làm việc với kiểu dữ liệu số nguyên vẫn hoạt động tốt:

5 % 2 === 1 // true
5 >> 1 === 2 // true

Tuy nhiên không giống như các toán sử số học, toán tử nhị phân và toán tử dịch chỉ hoạt động trên dãy 32 bit của các số nguyên lớn. Ví dụ, thử dịch 1 bit của "Math.pow(2, 53)" sẽ luôn cho kết quả là 0. Thử phép OR nhị phân với 1 của cùng số đó cũng cho kết quả 0.

Math.pow(2, 53) / 2 === Math.pow(2, 52) // true
Math.pow(2, 53) >> 1 === 0 // true
Math.pow(2, 53) | 1 === 0 // true

Bạn có thể ít khi cần xử lý các số lớn nhưng nếu cần tới thì có rất nhiều thư viện cho phép áp dụng các toán tử quan trọng trên các số lớn cho kết quả chính xác, ví dụ node-bigint.

Lỗi #8: Bỏ qua các tiện ích của Streaming APIs

Giả sử chúng ta muốn xây dựng một web server nhỏ tương tự proxy để phục vụ các yêu cầu lấy dữ liệu từ một website khác. Ví dụ, chúng ta xây dựng một web server nhỏ cung cấp ảnh từ Gravatar:

var http = require('http')
var crypto = require('crypto')

http.createServer()
    .on('request', function(req, res) {
        var email = req.url.substr(req.url.lastIndexOf('/')+1)
        if(!email) {
            res.writeHead(404)
            return res.end()
        }

        var buf = new Buffer(1024*1024)
        http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) {
            var size = 0
            resp.on('data', function(chunk) {
                chunk.copy(buf, size)
                size += chunk.length
            })
                .on('end', function() {
                    res.write(buf.slice(0, size))
                    res.end()
                })
        })
    })
    .listen(8080)

Trong ví dụ này chúng ta tải các hình ảnh từ Gravatar, đọc nó vào bộ đệm rồi trả kết quả về cho các yêu cầu. Đây không phải là một cách làm tệ, các hình ảnh của Gravatar không lớn lắm. Mặc dù thế, nếu bạn thử tưởng tượng kích cỡ nội dung mà chúng ta chuyển về lên tới hàng ngàn megabyte, cách sau đây sẽ tốt hơn nhiều:

http.createServer()
    .on('request', function(req, res) {
        var email = req.url.substr(req.url.lastIndexOf('/')+1)
        if(!email) {
            res.writeHead(404)
            return res.end()
        }

        http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) {
            resp.pipe(res)
        })
    })
    .listen(8080)

Theo cách này, chúng ta quét lấy dữ liệu ảnh và chỉ đơn giản truyền về cho các máy khách. Chúng ta không cần phải đọc toàn bộ nội dung vào bộ đệm trước khi gửi nó tới máy khác.

Lỗi #9: Dùng Console.log với mục đích gỡ lỗi

Trong Node.js. "console.log" cho phép bạn in hầu hết mọi thứ ra console. Truyền vào một đối tượng thì nó sẽ in ra đúng như một đối tượng Javascript bình thường. Nó chấp nhận mọi tham số tùy ý và in ra tất cả một cách gọn gàng. Có nhiều lý do vì sao một lập trình viên cảm thấy thích dùng console.log để gỡ lỗi, tuy nhiên bạn không nên sử dụng "console.log" trong code thực tế. Bạn nên tránh viết "console.log" trong toàn bộ code của mình rồi chuyển nó thành chú thích sau khi không cần gỡ lỗi. Thay vào đó, dùng một thư viên rất hay được xây dựng để làm đúng việc này, ví dụ như debug.

Những gói thư viện này cung cấp một cách thuật tiện để bật tắt các dòng lệnh debug khi bắt đầu ứng dụng. Ví dụ, với debug, nó cho phép ngăn bất kì dòng debug nào in ra terminal bằng cách không thiết lập giá trị môi trường DEBUG.

// app.js
var debug = require(‘debug’)(‘app’)
debug(’Hello, %s!’, ‘world’)

Để cho phép các dòng debug hoạt động, chỉ cần chạy lệnh với giá trị môi trường DEBUG được đặt là "app" hoặc "*"

DEBUG=app node app.js

Lỗi #10: Không dùng các chương trình giám sát

Không phân biệt mã lệnh Node.js của bạn đang chạy chính thức hay thử nghiệm trên môi trường cục bộ, một chương trình quản lý có thể sắp xếp chương trình của bạn là một điều rất có ích. Một khuyến cáo thực tế của các nhà phát triển thiết kế và triển khai ứng dụng hiện đại đó là "fail fast" (thất bại nhanh chóng".

Nếu có một lỗi nào đó xảy ra, đừng cố gắng xử lý lỗi đó, thay vào đó hãy để chương trình của bạn bị dừng và khởi động lại nó chỉ sau một vài giây. Lợi ích của các chương trình giám sát không chỉ là khởi động lại chương trình khi bị lỗi. Những công cụ này cho phép khởi động lại chương trình khi gặp lỗi, khi có sự thay đổi các file trong chương trình, tạo cho ta trải nghiệmn thú vị hơn khi phát triển ứng dụng Node.js

Tất cả các công cụ này đều có ưu và nhược điểm riêng. Một số cái tốt cho việc xử lý chạy nhiều ứng dụng trên một máy, một số thì làm tốt việc quản lý log. Tuy nhiên để bắt đầu với một chương trình thì những công cụ trên là như nhau.

Kết luận

Như bạn biết đến, một số lỗi có thể có tác động hủy hoại đối với chương trình của bạn. Một số lỗi khiến bạn thất vọng khi mà bạn đang cố gắng làm những điều đơn giản nhất với Node.js. Dù Node.js khá dễ tiếp cận nhưng nó vẫn tồn tại những phần dễ gây ra lỗi. Lập trình viên từ ngôn ngữ lập trình khác có thể liên quan đến một số vấn đề khác, nhưng những sai lầm trên là khá phổ biến ở những lập trình viên Node.js non tay. Cũng may là những lỗi này không khó tránh. Hi vọng bài viết này sẽ giúp ích cho các lập trình viên Node.js viết code tốt hơn, tạo ra các ứng dụng ổn định và hiệu quả.

Nguồn: toptal.com