10 Kesalahan Paling Umum Yang Dilakukan Pengembang Node.js
Diterbitkan: 2022-03-11Sejak Node.js diluncurkan ke dunia, telah mendapat pujian dan kritik yang adil. Perdebatan masih berlanjut, dan mungkin tidak akan berakhir dalam waktu dekat. Apa yang sering kita abaikan dalam perdebatan ini adalah bahwa setiap bahasa pemrograman dan platform dikritik berdasarkan isu-isu tertentu, yang diciptakan oleh cara kita menggunakan platform. Terlepas dari betapa sulitnya Node.js membuat penulisan kode yang aman, dan betapa mudahnya membuat penulisan kode yang sangat bersamaan, platform ini telah ada cukup lama dan telah digunakan untuk membangun sejumlah besar layanan web yang kuat dan canggih. Layanan web ini berskala baik, dan telah membuktikan stabilitasnya melalui daya tahan waktu mereka di Internet.
Namun, seperti platform lainnya, Node.js rentan terhadap masalah dan masalah pengembang. Beberapa kesalahan ini menurunkan kinerja, sementara yang lain membuat Node.js tampak tidak dapat digunakan untuk apa pun yang ingin Anda capai. Pada artikel ini, kita akan melihat sepuluh kesalahan umum yang sering dilakukan oleh pengembang baru di Node.js, dan bagaimana mereka dapat menghindarinya untuk menjadi pro Node.js.
Kesalahan #1: Memblokir loop acara
JavaScript di Node.js (seperti di browser) menyediakan lingkungan berulir tunggal. Ini berarti bahwa tidak ada dua bagian dari aplikasi Anda yang berjalan secara paralel; sebaliknya, konkurensi dicapai melalui penanganan operasi terikat I/O secara asinkron. Misalnya, permintaan dari Node.js ke mesin database untuk mengambil beberapa dokumen memungkinkan Node.js untuk fokus pada beberapa bagian lain dari aplikasi:
// 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 })
Namun, hanya sepotong kode terikat CPU dalam instance Node.js dengan ribuan klien yang terhubung yang diperlukan untuk memblokir loop peristiwa, membuat semua klien menunggu. Kode terikat CPU termasuk mencoba mengurutkan array besar, menjalankan loop yang sangat panjang, dan seterusnya. Sebagai contoh:
function sortUsersByAge(users) { users.sort(function(a, b) { return a.age < b.age ? -1 : 1 }) }
Menjalankan fungsi "sortUsersByAge" ini mungkin baik-baik saja jika dijalankan pada larik "pengguna" kecil, tetapi dengan larik besar, itu akan berdampak buruk pada kinerja keseluruhan. Jika ini adalah sesuatu yang mutlak harus dilakukan, dan Anda yakin bahwa tidak akan ada lagi yang menunggu di loop acara (misalnya, jika ini adalah bagian dari alat baris perintah yang Anda buat dengan Node.js, dan itu tidak masalah jika semuanya berjalan secara serempak), maka ini mungkin tidak menjadi masalah. Namun, dalam contoh server Node.js yang mencoba melayani ribuan pengguna sekaligus, pola seperti itu bisa berakibat fatal.
Jika larik pengguna ini diambil dari database, solusi ideal adalah mengambilnya yang sudah diurutkan langsung dari database. Jika loop peristiwa diblokir oleh loop yang ditulis untuk menghitung jumlah riwayat panjang data transaksi keuangan, itu dapat ditangguhkan ke beberapa pengaturan pekerja/antrean eksternal untuk menghindari memonopoli loop peristiwa.
Seperti yang Anda lihat, tidak ada solusi tepat untuk masalah Node.js semacam ini, melainkan setiap kasus perlu ditangani secara individual. Ide dasarnya adalah untuk tidak melakukan pekerjaan intensif CPU di depan instance Node.js - yang terhubung dengan klien secara bersamaan.
Kesalahan #2: Meminta Panggilan Balik Lebih dari Sekali
JavaScript telah mengandalkan panggilan balik sejak lama. Di browser web, peristiwa ditangani dengan meneruskan referensi ke (seringkali anonim) fungsi yang bertindak seperti panggilan balik. Di Node.js, callback dulunya merupakan satu-satunya cara elemen asinkron dari kode Anda berkomunikasi satu sama lain - hingga janji diperkenalkan. Callback masih digunakan, dan pengembang paket masih mendesain API mereka seputar callback. Salah satu masalah umum Node.js terkait dengan penggunaan callback adalah memanggilnya lebih dari sekali. Biasanya, fungsi yang disediakan oleh paket untuk melakukan sesuatu secara asinkron dirancang untuk mengharapkan fungsi sebagai argumen terakhirnya, yang dipanggil ketika tugas asinkron telah selesai:
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) }) }
Perhatikan bagaimana ada pernyataan kembali setiap kali "selesai" dipanggil, hingga terakhir kali. Ini karena memanggil panggilan balik tidak secara otomatis mengakhiri eksekusi fungsi saat ini. Jika "pengembalian" pertama dikomentari, meneruskan kata sandi non-string ke fungsi ini masih akan menghasilkan "computeHash" dipanggil. Bergantung pada bagaimana "computeHash" menangani skenario seperti itu, "selesai" dapat dipanggil beberapa kali. Siapa pun yang menggunakan fungsi ini dari tempat lain mungkin akan benar-benar lengah ketika panggilan balik yang mereka lewati dipanggil beberapa kali.
Berhati-hatilah untuk menghindari kesalahan Node.js ini. Beberapa pengembang Node.js mengadopsi kebiasaan menambahkan kata kunci kembali sebelum setiap permintaan panggilan balik:
if(err) { return done(err) }
Dalam banyak fungsi asinkron, nilai kembalian hampir tidak memiliki signifikansi, sehingga pendekatan ini sering kali memudahkan untuk menghindari masalah seperti itu.
Kesalahan #3: Callback yang Sangat Bersarang
Callback yang sangat dalam, sering disebut sebagai "callback hell", bukanlah masalah Node.js itu sendiri. Namun, ini dapat menyebabkan masalah membuat kode dengan cepat lepas kendali:
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') }) }) }) }
Semakin kompleks tugasnya, semakin buruk hasilnya. Dengan bersarang callback sedemikian rupa, kita dengan mudah berakhir dengan rawan kesalahan, sulit dibaca, dan sulit untuk mempertahankan kode. Salah satu solusinya adalah mendeklarasikan tugas-tugas ini sebagai fungsi kecil, dan kemudian menautkannya. Meskipun, salah satu (bisa dibilang) solusi terbersih untuk ini adalah dengan menggunakan paket utilitas Node.js yang menangani pola JavaScript asinkron, seperti 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() { // ... }) }
Mirip dengan "async.waterfall", ada sejumlah fungsi lain yang disediakan Async.js untuk menangani pola asinkron yang berbeda. Untuk singkatnya, kami menggunakan contoh yang lebih sederhana di sini, tetapi kenyataannya seringkali lebih buruk.
Kesalahan #4: Mengharapkan Panggilan Balik Berjalan Secara Sinkron
Pemrograman asinkron dengan panggilan balik mungkin bukan sesuatu yang unik untuk JavaScript dan Node.js, tetapi mereka bertanggung jawab atas popularitasnya. Dengan bahasa pemrograman lain, kita terbiasa dengan urutan eksekusi yang dapat diprediksi di mana dua pernyataan akan dieksekusi satu demi satu, kecuali ada instruksi khusus untuk melompat di antara pernyataan. Bahkan kemudian, ini sering terbatas pada pernyataan kondisional, pernyataan loop, dan pemanggilan fungsi.
Namun, dalam JavaScript, dengan panggilan balik, fungsi tertentu mungkin tidak berjalan dengan baik hingga tugas yang ditunggunya selesai. Eksekusi fungsi saat ini akan berjalan hingga akhir tanpa henti:
function testTimeout() { console.log(“Begin”) setTimeout(function() { console.log(“Done!”) }, duration * 1000) console.log(“Waiting..”) }
Seperti yang akan Anda perhatikan, memanggil fungsi "testTimeout" pertama-tama akan mencetak "Mulai", lalu mencetak "Menunggu.." diikuti dengan pesan "Selesai!" setelah sekitar satu detik.
Apa pun yang perlu terjadi setelah panggilan balik diaktifkan perlu dipanggil dari dalamnya.
Kesalahan #5: Menetapkan ke "ekspor", Alih-alih "module.exports"
Node.js memperlakukan setiap file sebagai modul kecil yang terisolasi. Jika paket Anda memiliki dua file, mungkin “a.js” dan “b.js”, maka agar “b.js” dapat mengakses fungsionalitas “a.js”, “a.js” harus mengekspornya dengan menambahkan properti ke objek ekspor:

// a.js exports.verifyPassword = function(user, password, done) { ... }
Ketika ini selesai, siapa pun yang membutuhkan "a.js" akan diberikan objek dengan fungsi properti "verifyPassword":
// b.js require('a.js') // { verifyPassword: function(user, password, done) { ... } }
Namun, bagaimana jika kita ingin mengekspor fungsi ini secara langsung, dan bukan sebagai properti dari beberapa objek? Kita dapat menimpa ekspor untuk melakukan ini, tetapi kita tidak boleh memperlakukannya sebagai variabel global, maka:
// a.js module.exports = function(user, password, done) { ... }
Perhatikan bagaimana kita memperlakukan "ekspor" sebagai properti dari objek modul. Perbedaan di sini antara "module.exports" dan "ekspor" sangat penting, dan sering kali menjadi penyebab frustrasi di antara pengembang Node.js baru.
Kesalahan #6: Melempar Kesalahan dari Panggilan Balik Orang Dalam
JavaScript memiliki gagasan pengecualian. Meniru sintaks hampir semua bahasa tradisional dengan dukungan penanganan pengecualian, seperti Java dan C++, JavaScript dapat "melempar" dan menangkap pengecualian di blok 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!') }
Namun, try-catch tidak akan berperilaku seperti yang Anda harapkan dalam situasi asinkron. Misalnya, jika Anda ingin melindungi sebagian besar kode dengan banyak aktivitas asinkron dengan satu blok try-catch yang besar, itu tidak selalu berhasil:
try { db.User.get(userId, function(err, user) { if(err) { throw err } // ... usernameSlug = slugifyUsername(user.username) // ... }) } catch(e) { console.log('Oh no!') }
Jika panggilan balik ke "db.User.get" diaktifkan secara tidak sinkron, cakupan yang berisi blok coba-tangkap akan lama keluar dari konteksnya untuk tetap dapat menangkap kesalahan yang dilemparkan dari dalam panggilan balik.
Beginilah cara kesalahan ditangani dengan cara yang berbeda di Node.js, dan itu membuatnya penting untuk mengikuti pola (err, ...) pada semua argumen fungsi panggilan balik - argumen pertama dari semua panggilan balik diharapkan menjadi kesalahan jika terjadi .
Kesalahan #7: Menganggap Angka Menjadi Tipe Data Integer
Angka dalam JavaScript adalah floating point - tidak ada tipe data integer. Anda tidak akan mengharapkan ini menjadi masalah, karena angka yang cukup besar untuk menekankan batas float tidak sering ditemui. Saat itulah kesalahan yang terkait dengan ini terjadi. Karena angka floating point hanya dapat menampung representasi bilangan bulat hingga nilai tertentu, melebihi nilai itu dalam perhitungan apa pun akan segera mulai mengacaukannya. Meski terlihat aneh, berikut ini bernilai true di Node.js:
Math.pow(2, 53)+1 === Math.pow(2, 53)
Sayangnya, keanehan dengan angka dalam JavaScript tidak berakhir di sini. Meskipun Numbers adalah floating point, operator yang bekerja pada tipe data integer juga berfungsi di sini:
5 % 2 === 1 // true 5 >> 1 === 2 // true
Namun, tidak seperti operator aritmatika, operator bitwise dan operator shift hanya bekerja pada 32 bit tambahan dari angka “bilangan bulat” yang begitu besar. Misalnya, mencoba menggeser “Math.pow(2, 53)” dengan 1 akan selalu bernilai 0. Mencoba melakukan bitwise-atau 1 dengan angka besar yang sama akan bernilai 1.
Math.pow(2, 53) / 2 === Math.pow(2, 52) // true Math.pow(2, 53) >> 1 === 0 // true Math.pow(2, 53) | 1 === 1 // true
Anda mungkin jarang perlu berurusan dengan angka besar, tetapi jika Anda melakukannya, ada banyak perpustakaan bilangan bulat besar yang mengimplementasikan operasi matematika penting pada angka presisi besar, seperti node-bigint.
Kesalahan #8: Mengabaikan Keuntungan Streaming API
Katakanlah kita ingin membangun server web kecil seperti proxy yang melayani tanggapan terhadap permintaan dengan mengambil konten dari server web lain. Sebagai contoh, kita akan membangun server web kecil yang menyajikan gambar 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)
Dalam contoh khusus masalah Node.js ini, kami mengambil gambar dari Gravatar, membacanya ke dalam Buffer, dan kemudian menanggapi permintaan tersebut. Ini bukan hal yang buruk untuk dilakukan, mengingat gambar Gravatar tidak terlalu besar. Namun, bayangkan jika ukuran konten yang kita proksi berukuran ribuan megabita. Pendekatan yang jauh lebih baik adalah ini:
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)
Di sini, kami mengambil gambar dan hanya menyalurkan respons ke klien. Kami tidak perlu membaca seluruh konten ke dalam buffer sebelum menyajikannya.
Kesalahan #9: Menggunakan Console.log untuk Tujuan Debugging
Di Node.js, "console.log" memungkinkan Anda untuk mencetak hampir semua hal ke konsol. Berikan objek padanya dan itu akan mencetaknya sebagai objek JavaScript literal. Ia menerima sejumlah argumen yang berubah-ubah dan mencetak semuanya dipisahkan oleh ruang dengan rapi. Ada sejumlah alasan mengapa pengembang mungkin merasa tergoda untuk menggunakan ini untuk men-debug kodenya; namun, sangat disarankan agar Anda menghindari "console.log" dalam kode sebenarnya. Anda harus menghindari penulisan "console.log" di seluruh kode untuk men-debug-nya dan kemudian mengomentarinya saat tidak lagi diperlukan. Sebagai gantinya, gunakan salah satu perpustakaan luar biasa yang dibuat hanya untuk ini, seperti debug.
Paket seperti ini menyediakan cara mudah untuk mengaktifkan dan menonaktifkan baris debug tertentu saat Anda memulai aplikasi. Misalnya, dengan debug dimungkinkan untuk mencegah setiap baris debug dicetak ke terminal dengan tidak menyetel variabel lingkungan DEBUG. Menggunakannya sederhana:
// app.js var debug = require('debug')('app') debug('Hello, %s!', 'world')
Untuk mengaktifkan baris debug, cukup jalankan kode ini dengan variabel lingkungan DEBUG disetel ke "aplikasi" atau "*":
DEBUG=app node app.js
Kesalahan #10: Tidak Menggunakan Program Supervisor
Terlepas dari apakah kode Node.js Anda berjalan dalam produksi atau di lingkungan pengembangan lokal Anda, monitor program supervisor yang dapat mengatur program Anda adalah hal yang sangat berguna untuk dimiliki. Satu praktik yang sering direkomendasikan oleh pengembang yang merancang dan mengimplementasikan aplikasi modern menyarankan agar kode Anda gagal dengan cepat. Jika kesalahan tak terduga terjadi, jangan coba-coba menanganinya, lebih baik biarkan program Anda mogok dan minta supervisor memulai ulang dalam beberapa detik. Manfaat program supervisor tidak hanya terbatas pada memulai kembali program yang macet. Alat-alat ini memungkinkan Anda untuk me-restart program saat crash, serta me-restart mereka ketika beberapa file berubah. Ini membuat pengembangan program Node.js menjadi pengalaman yang jauh lebih menyenangkan.
Ada banyak program supervisor yang tersedia untuk Node.js. Sebagai contoh:
pm2
selama-lamanya
nodemon
pengawas
Semua alat ini datang dengan pro dan kontra. Beberapa dari mereka bagus untuk menangani banyak aplikasi pada mesin yang sama, sementara yang lain lebih baik dalam manajemen log. Namun, jika Anda ingin memulai program seperti itu, semua ini adalah pilihan yang adil.
Kesimpulan
Seperti yang Anda ketahui, beberapa masalah Node.js ini dapat berdampak buruk pada program Anda. Beberapa mungkin menjadi penyebab frustrasi saat Anda mencoba mengimplementasikan hal-hal paling sederhana di Node.js. Meskipun Node.js telah membuatnya sangat mudah bagi pendatang baru untuk memulai, Node.js masih memiliki area yang mudah dikacaukan. Pengembang dari bahasa pemrograman lain mungkin dapat berhubungan dengan beberapa masalah ini, tetapi kesalahan ini cukup umum di antara pengembang Node.js baru. Untungnya, mereka mudah dihindari. Saya harap panduan singkat ini akan membantu pemula untuk menulis kode yang lebih baik di Node.js, dan untuk mengembangkan perangkat lunak yang stabil dan efisien untuk kita semua.