Nội dung chính
Trong bài viết này, ITviec đã tổng hợp danh sách đầy đủ các câu hỏi phỏng vấn NodeJS thường xuất hiện trong các cuộc phỏng vấn, cùng một số dạng đề bài thực hành lập trình với Node.js đơn giản. Trước khi đọc đáp án, hãy thử tự mình trả lời trước để “ôn tập” trước.
Đọc bài viết để hiểu rõ hơn:
- Node.js là gì? Tầm quan trọng của Node.js?
- Câu hỏi phỏng vấn NodeJS cho Fresher/ Junior Developer
- Câu hỏi phỏng vấn NodeJS cho Middle Developer
- Câu hỏi phỏng vấn NodeJS cho Senior Developer
Node.js là gì? Tầm quan trọng của Node.js?
Node.js là một môi trường máy chủ mã nguồn mở, sử dụng JavaScript trên máy chủ. Node.js có thể chạy trên nhiều nền tảng khác nhau (Windows, Linux, Unix, Mac OS X,…).
Node.js rất quan trọng đối với lập trình backend do khả năng xử lý các ứng dụng hiệu suất cao, thời gian thực. Đặc biệt, Node.js còn được biết đến với kiến trúc không chặn, hướng sự kiện, nên rất phù hợp để xử lý số lượng lớn các kết nối đồng thời.
Ngoài ra, Node.js hỗ trợ khả năng mở rộng, xử lý dữ liệu nhanh và lý tưởng để xây dựng các ứng dụng như API, ứng dụng trò chuyện và nền tảng cộng tác thời gian thực.
Đọc thêm: NodeJS Backend: Khi nào nên sử dụng NodeJS lập trình backend?
Câu hỏi phỏng vấn NodeJS dành cho Fresher/ Junior Developer
Sự khác biệt giữa Node.js và JavaScript trong trình duyệt là gì?
JavaScript là ngôn ngữ lập trình chủ yếu được sử dụng để phát triển web phía máy khách (client-side), trong khi Node là môi trường thời gian chạy (runtime) cho phép JavaScript được thực thi ở phía máy chủ.
Javascript | NodeJS | |
Khái niệm | Ngôn ngữ lập trình được sử dụng để viết các tập lệnh trên trang web | Môi trường chạy Javascript |
Môi trường chạy | Chỉ có thể chạy trong trình duyệt | Chạy trên máy chủ (server-side), sử dụng V8 JavaScript engine của Google Chrome. |
Vị trí sử dụng | Phía máy khách | Phía máy chủ (server-side)
Công cụ dòng lệnh (CLI tools) Ứng dụng desktop (với Electron) Phát triển IoT |
Khả năng thêm thẻ HTML | Đủ khả năng để thêm HTML và DOM | Không có khả năng thêm thẻ HTM |
Khả năng chạy trong trình duyệt | Có thể chạy trong bất kỳ công cụ trình duyệt nào như JS core trong Safari và Spidermonkey trong Firefox | V8 là engine mặc định của Node.js. Nhưng Node.js không chạy trong trình duyệt, nó chạy độc lập trên môi trường máy tính |
Node.js thường sử dụng các thư viện nào?
Có khá nhiều thư viện thường được sử dụng trong Node.js, trong đó có thể kể đến:
- ExpressJS – Express là một nền tảng ứng dụng web Node.js linh hoạt cung cấp nhiều tính năng để phát triển ứng dụng web và di động. ExpressJS thường được sử dụng để xây dựng các ứng dụng web động, API RESTful và các ứng dụng một trang (single-page application).
- Mongoose – Mongoose cũng là một nền tảng ứng dụng web Node.js giúp kết nối ứng dụng với cơ sở dữ liệu một cách dễ dàng.
Node.js hoạt động như thế nào?
Khi máy khách gửi yêu cầu đến máy chủ web để tương tác với ứng dụng web, yêu cầu có thể là không chặn hoặc chặn, chẳng hạn truy vấn dữ liệu, xóa hoặc cập nhật dữ liệu, Node.js sẽ hoạt động như sau:
- Node.js truy xuất các yêu cầu đến và thêm chúng vào Hàng đợi sự kiện
- Các yêu cầu được xử lý theo từng yêu cầu một thông qua Vòng lặp sự kiện. Vòng lặp này kiểm tra xem các yêu cầu có đơn giản và không cần tài nguyên bên ngoài hay không.
- Với các yêu cầu đơn giản (hoạt động không chặn), như thao tác I/O, vòng lặp sự kiện sẽ xử lý ngay lập tức và trả về phản hồi cho máy khách mà không cần phải chờ đợi.
- Đối với các yêu cầu phức tạp, vòng lặp sự kiện sẽ chỉ định một luồng từ Nhóm luồng để xử lý yêu cầu đó. Luồng này sẽ chịu trách nhiệm hoàn thành các yêu cầu chặn, ví dụ như truy cập cơ sở dữ liệu, hệ thống tệp hoặc thực hiện tính toán.
- Sau khi yêu cầu được hoàn thành, phản hồi sẽ được đưa lại vòng lặp sự kiện và gửi đến máy khách.
Node.js là đơn luồng hay đa luồng?
Node.js sử dụng mô hình luồng đơn, có nghĩa là nó chỉ sử dụng một luồng duy nhất để xử lý nhiều tác vụ. Kiến trúc luồng đơn của Node.js được điều khiển bởi vòng lặp sự kiện và I/O không chặn. Đây là một lựa chọn thiết kế có chủ đích cân bằng giữa tính đơn giản, hiệu suất và khả năng mở rộng.
Nếu Node.js là luồng đơn, vậy nó xử lý đồng thời như thế nào?
Để làm được điều này, Node.js áp dụng mô hình I/O không chặn và hướng sự kiện, giúp xử lý các tác vụ mà không phải chờ đợi nhau, từ đó tăng hiệu suất và khả năng mở rộng của ứng dụng.
Node.js, AJAX và jQuery có gì khác nhau?
Một đặc điểm chung giữa Node.js, AJAX và jQuery là cả 3 đều sử dụng JavaScript. Tuy nhiên, chúng phục vụ các mục đích hoàn toàn khác nhau.
Node.js: Là nền tảng phía máy chủ để phát triển các ứng dụng máy khách – máy chủ. Ví dụ, nếu phải xây dựng một hệ thống quản lý nhân viên trực tuyến, thì lập trình viên sẽ không thực hiện bằng JS phía máy khách.
Nhưng Node.js chắc chắn có thể thực hiện được vì nó chạy trên máy chủ tương tự như Apache, Django chứ không phải trên trình duyệt.
AJAX (hay còn gọi là Javascript và XML không đồng bộ): Là kỹ thuật viết kịch bản phía máy khách, chủ yếu được thiết kế để hiển thị nội dung của một trang mà không cần làm mới trang.
jQuery: Là một mô-đun JavaScript nổi tiếng bổ sung cho AJAX, DOM traversal, vòng lặp,… Thư viện này cung cấp nhiều hàm hữu ích để hỗ trợ phát triển JavaScript.
Node.js hỗ trợ loại hàm API nào?
Có hai loại hàm API được Node.js hỗ trợ:
- Đồng bộ (Synchronous): Các hàm API này được sử dụng để chặn mã.
- Không đồng bộ (Asynchronous): Các hàm API này được sử dụng cho mã không chặn.
Bạn hiểu thuật ngữ I/O là như thế nào?
Thuật ngữ I/O (Input/Output) được dùng để mô tả bất kỳ chương trình, hoạt động, hoặc thiết bị nào có khả năng truyền dữ liệu từ hoặc đến một phương tiện này và từ hoặc đến một phương tiện khác.
Mỗi lần truyền dữ liệu đều bao gồm một đầu ra từ một phương tiện và một đầu vào vào một phương tiện khác. Các phương tiện này có thể là thiết bị vật lý, mạng hoặc các tệp trong một hệ thống.
Hãy giải thích npm install và npm ci?
npm i (hoặc npm install) được sử dụng để cài đặt tất cả các dependency hoặc devDependencies từ tệp package.json. Nó có thể cài đặt thêm package, sửa đổi và cập nhật phiên bản và cũng có thể cập nhật tệp package-lock.json. Cú pháp:
npm install "package-name"
hoặc:
npm i "package-name"
CI là viết tắt của clean install và npm ci được sử dụng để cài đặt tất cả dependency phiên bản chính xác hoặc devDependencies từ tệp package-lock.json. Nó không thể cài đặt các gói bổ sung hoặc sửa đổi các dependency đã cài đặt. Cú pháp: npm ci
Mô-đun (module) trong Node.js là gì? Làm thế nào để import/export một module?
Các mô-đun trong Node.js giống như các thư viện JavaScript, cung cấp một tập hợp các hàm mà bạn có thể sử dụng trong ứng dụng của mình.
- Để chia sẻ một phần mã (chẳng hạn như một hàm hoặc đối tượng) từ mô-đun này sang mô-đun khác, bạn sử dụng module.exports trong mô-đun.
- Để sử dụng một mô-đun đã được export ở nơi khác, bạn dùng require() để import mô-đun đó vào trong mã của bạn.
Làm thế nào để viết “Hello world” trong Node.js
Bước 1: Khởi tạo ứng dụng NodeJS bằng lệnh npm init
Bước 2: Triển khai module express trong dự án của bạn bằng lệnh npm install express. Các phụ thuộc (dependencies) được cập nhật trong tệp package.json sẽ như thế này:
"dependencies": { "express": "^4.19.2" }
Bước 3: Chạy tệp app.js bằng lệnh node app.js
// app.js // Require would make available the // express package to be used in // our code const express = require("express"); // Creates an express object const app = express(); // It listens to HTTP get request. // Here it listens to the root i.e '/' app.get("/", (req, res) => { // Using send function we send // response to the client // Here we are sending html res.send("<h1> Hello World </h1>"); }); // It configures the system to listen // to port 3000. Any number can be // given instead of 3000, the only // condition is that no other server // should be running at that port app.listen(3000, () => { // Print in the console when the // servers starts to listen on 3000 console.log("Listening to port 3000"); });
Bây giờ hãy mở trình duyệt của bạn và truy cập vào http://localhost:3000/, bạn sẽ thấy kết quả như mong đợi.
Sự khác biệt giữa require và import trong Node.js là gì?
require() | import | |
Cú pháp | CommonJS: require(‘module’) | ES6: import module from ‘module’ |
Mục đích sử dụng | Nhập module đồng bộ | Có thể đồng bộ hoặc không đồng bộ, tùy vào cách sử dụng |
Hỗ trợ module | – Hỗ trợ cả module CommonJS và module ES
– Đồng bộ, tải module theo yêu cầu |
– ỗ trợ module ES (yêu cầu phần mở rộng .mjs) hoặc .js nếu được cấu hình đúng.
– Không đồng bộ, chỉ tải module khi cần |
Khả năng export | – Hỗ trợ export có tên thông qua destructuring | – Hỗ trợ export mặc định (import module from ‘module’)
– Hỗ trợ export có tên trực tiếp (import { name } from ‘module’) |
Khả năng phân tích | Không hỗ trợ phân tích tĩnh | Hỗ trợ phân tích tĩnh (cho phép enables tree shaking) |
Khả năng import | Không hỗ trợ import động | Hỗ trợ import động (hàm import()) |
Môi trường trình duyệt | Không được hỗ trợ trong môi trường trình duyệt | Được hỗ trợ trong các trình duyệt hiện đại có hỗ trợ module |
Làm thế nào để đọc tệp (files) trong Node.js?
Để đọc một tệp trong NodeJS, có thể sử dụng phương thức fs.readFile(). Phương thức này đọc không đồng bộ toàn bộ nội dung của tệp và truyền dữ liệu đến hàm callback.
const fs = require('fs'); fs.readFile('example.txt', 'utf8', (error, data) => { if (error) { console.error('An error occurred while reading the file:', error); return; } console.log('File content:', data); });
Làm thế nào để ghi tệp (files) trong Node.js?
Để ghi vào một tệp trong NodeJS, có thể sử dụng phương thức fs.writeFile(). Phương thức này ghi dữ liệu không đồng bộ vào một tệp, thay thế tệp nếu tệp đã tồn tại hoặc tạo tệp mới nếu tệp chưa tồn tại.
const fs = require('fs'); const content = 'Đây là nội dung sẽ ghi trong tệp.'; fs.writeFile('example.txt', content, 'utf8', (error) => { if (error) { console.error('Một lỗi đã xuất hiện khi ghi tệp:', error); return; } console.log('Tệp đã được ghi thành công.'); });
Stream trong Node.js là gì? Có những loại stream nào?
Luồng (Stream) là các đối tượng cho phép lập trình viên đọc dữ liệu từ một nguồn hoặc ghi dữ liệu vào đích theo cách liên tục. Trong Node.js, có bốn loại luồng:
- Có thể đọc (Readable): Luồng được sử dụng cho hoạt động đọc.
- Có thể ghi (Writable): Luồng được sử dụng cho hoạt động ghi.
- Song công (Duplex): Luồng có thể được sử dụng cho cả hoạt động đọc và ghi.
- Biến đổi (Transform): Luồng song công trong đó đầu ra được tính toán dựa trên đầu vào.
Bạn có biết điểm khác biệt nào giữa Node.js và Angular.js không?
Angular là một framework dành cho lập trình Frontend, được viết bằng TypeScript và thường sử dụng để xây dựng các ứng dụng web phía máy khách hoặc ứng dụng đơn trang (SPA) bằng cách chia một ứng dụng web thành các thành phần MVC, ngoài ra còn hỗ trợ MVVM và component-based architecture.
Node.js à một môi trường phía máy chủ, được viết bằng ngôn ngữ C, C++, thường được sử dụng để xây dựng các ứng dụng mạng phía máy chủ nhanh và có khả năng mở rộng bằng cách tạo các truy vấn cơ sở dữ liệu, xây dựng web server, REST APIs và nhiều ứng dụng khác.
Đọc thêm: Top 5 NodeJS framework
Câu hỏi phỏng vấn NodeJS dành cho Middle Developer
Mục đích của lớp (class) Buffer trong Node.js là gì?
Lớp Buffer là một lớp toàn cục, có thể được truy cập trong ứng dụng mà không cần import mô-đun buffer. Buffer là một loại mảng số nguyên và tương ứng với phân bổ bộ nhớ thô bên ngoài heap V8. Không thể thay đổi kích thước của lớp này.
Middleware trong Node.js là gì?
Middleware là chức năng nhận các đối tượng yêu cầu và phản hồi. Middleware được thực thi sau khi máy chủ nhận được yêu cầu và trước khi bộ điều khiển gửi phản hồi. Các hoạt động chính của chức năng này gồm:
- Thực thi code bất kỳ
- Cập nhật hoặc sửa đổi các đối tượng yêu cầu và phản hồi
- Kết thúc chu kỳ yêu cầu-phản hồi
- Gọi middleware tiếp theo trong ngăn xếp
Lập trình hướng sự kiện trong Node.js là gì?
Lập trình hướng sự kiện (Event-driven programming) được sử dụng để đồng bộ hóa sự xuất hiện của nhiều sự kiện và làm chương trình đơn giản nhất có thể.
Trong Node.js, mô hình hướng sự kiện cho phép lập trình viên viết mã không chặn (non-blocking), không đồng bộ (asynchronous), để xử lý các sự kiện khi chúng xảy ra, mà không cần phải đợi các hoạt động chặn hoàn tất. Điều này giúp ứng dụng xử lý nhiều tác vụ cùng lúc mà không bị gián đoạn.
Làm thế nào để sử dụng các công cụ kiểm thử trong Node.js (như Mocha, Jest)?
Kiểm thử đơn vị (unit testing) là việc cô lập các thành phần hoặc đơn vị mã, chẳng hạn như hàm hoặc lớp, và chuyển chúng đến các trường hợp thử nghiệm khác nhau để xác minh hành vi của chúng.
Jest là một framework kiểm thử JavaScript được sử dụng rộng rãi để kiểm thử các ứng dụng React và cơ sở mã (codebase) JavaScript bất kỳ.
Cú pháp như sau:
test('mô tả test case', () => { // Code dùng để test ở đây expect(/* value thật */).toBe(/* value dự kiến */); });
Mocha là một Framework kiểm thử JavaScript linh hoạt, chạy trên Node.js và trong trình duyệt. Nó cung cấp một môi trường kiểm thử tối giản, cho phép lựa chọn các thư viện khẳng định (assertion libraries) và framework mô phỏng (mocking frameworks).
Cú pháp như sau:
describe('mô tả suite', () => { it('mô tả test case', () => { // Code dùng để test ở đây assert.equal(/* value thật */, /* value dự kiến */); }); });
Giải thích cách sử dụng module timer trong Node.js
Module Timers trong Node.js chứa nhiều hàm khác nhau cho phép lập trình viên thực thi một khối mã hoặc một hàm sau một khoảng thời gian nhất định. Module Timers là module toàn cục, lập trình viên không cần sử dụng require() để import module này.
Có thể sử dụng module timer trong Node.js bằng những cách sau:
Phương thức setTimeout()
Hàm setTimeout không chặn mã khác và phần còn lại của mã được thực thi sau một khoảng thời gian chỉ định.
Cú pháp:
setTimeout(function() { console.log('Thực thi sau delay'); }, 1000);
Phương thức setImmediate()
Lớp Immediate giữ vòng lặp sự kiện hoạt động cho đến khi trả về ‘True’ nếu trả về ‘false’ sẽ phá vỡ vòng lặp sự kiện.
Cú pháp:
setImmediate(() => { console.log('Thực thi ở lần lặp tiếp theo của event loop'); });
Phương thức setInterval()
Hàm này cho phép thực hiện một công việc lặp lại sau một khoảng thời gian cố định. Nó trả về một ID duy nhất, mà bạn có thể sử dụng với phương thức clearInterval() để dừng việc lặp lại.
Cú pháp:
const intervalId = setInterval(() => { console.log('Thực thi mỗi 2 giây'); }, 2000); clearInterval(intervalId);
Giải thích ngắn gọn sự khác biệt giữa phương thức process.nextTick() và setImmediate()
Phương thức process.nextTick() được sử dụng để thêm một hàm callback mới vào đầu hàng đợi sự kiện tiếp theo. Nó được gọi trước khi sự kiện được xử lý.
Phương thức setImmediate được gọi ở giai đoạn kiểm tra của hàng đợi sự kiện tiếp theo. Nó được tạo trong giai đoạn thăm dò và được gọi trong giai đoạn kiểm tra.
Sự khác biệt giữa phương thức spawn() và fork() là gì?
Phương thức spawn() được sử dụng để khởi chạy một tiến trình mới bằng một lệnh nhất định.
Trong khi đó, phương thức fork() là phiên bản chuyên biệt của spawn() được thiết kế riêng để tạo ra các tiến trình Node.js mới.
Sự khác biệt giữa phương thức spawn() và fork() được hệ thống qua các tiêu chí sau:
spawn() | fork() | |
Cú pháp | const child = spawn(command, [args], [options]); | const child = fork(modulePath, [args], [options]); |
Mục đích | Đa năng.
Có thể khởi chạy bất kỳ quy trình mới nào, phù hợp để thực thi lệnh shell và tương tác với luồng của chúng. |
Chuyên biệt.
Dùng để tạo quy trình Node.js mới, được tối ưu hóa để giao tiếp giữa quy trình cha và con thông qua IPC. |
Trường hợp sử dụng | Chạy lệnh bên ngoài, tập lệnh shell hoặc bất kỳ quy trình nào cần truyền phát dữ liệu theo thời gian thực. | Chạy các tập lệnh Node.js riêng biệt cần giao tiếp với quy trình cha, chẳng hạn như phân nhánh nhiều phiên bản của ứng dụng Node.js. |
Giao tiếp | Giới hạn ở các luồng đầu vào/đầu ra tiêu chuẩn. | Cung cấp kênh IPC để truyền thông điệp giữa các quy trình cha và con. |
Hiệu suất | Tốt hơn cho các tác vụ liên kết I/O nặng do khả năng truyền phát theo thời gian thực. | Tốt hơn cho các tác vụ yêu cầu phối hợp chặt chẽ giữa các quy trình Node.js, chẳng hạn như quản lý trạng thái được chia sẻ hoặc giao tiếp giữa các quy trình phức tạp. |
Làm thế nào để kết nối Node.js với cơ sở dữ liệu MongoDB?
MongoDB là một cơ sở dữ liệu NoSQL được sử dụng để lưu trữ lượng lớn dữ liệu mà không cần bất kỳ bảng cơ sở dữ liệu quan hệ truyền thống nào.
Để kết nối Node với cơ sở dữ liệu MongoDB, lập trình viên có thể sử dụng thư viện mongoose từ npm.
Bước 1: Cài đặt mongoose vào hệ thống bằng lệnh:
npm install mongoose
Bước 2: Sử dụng thư viện Mongoose để nhập thư viện:
const mongoose = require ("mongoose");
Bước 3: Gọi phương thức kết nối của Mongoose
mongoose.connect("mongodb://localhost:27017/collectionName", { useNewUrlParser: true, useUnifiedTopology: true });
Bước 4: Xác định lược đồ.
Ví dụ: Giả sử chúng ta muốn lưu trữ thông tin từ biểu mẫu liên hệ của một trang web.
const contactSchema = { email: String, query: String, };
Bước 5: Tạo mô hình với lược đồ đã xác định với câu lệnh:
const Contact = mongoose.model("Contact", contactSchema);
Bước 6: Lưu trữ dữ liệu
app.post("/contact", function (req, res) { const contact = new Contact({ email: req.body.email, query: req.body.query, }); contact.save(function (err) { if (err) { res.redirect("/error"); } else { res.redirect("/thank-you"); } }); });
Làm thế nào để xử lý routing trong ứng dụng Node.js?
Định tuyến (routing) trong Node xác định cách các yêu cầu của người dùng được xử lý bởi các điểm cuối (endpoint) khác nhau (URL). Có hai cách để triển khai định tuyến trong Node như sau:
Cách 1: Sử dụng Framework Express và thực hiện theo các bước sau:
- Bước 1: Cài đặt Express.js với lệnh npm install express
- Bước 2: Tạo một ứng dụng Express đơn giản
- Bước 3: Sử dụng các tham số động trong đường dẫn URL để lấy thông tin từ URL
Cách 2: Xử lý thuần túy bằng phương thức app.all()
const express = require('express') const app = express() app.all('/', function(req, res) { console.log('Hello Sir') next() // Pass the control to the next handler })
Body-paser trong Node.js là gì?
Body-parser là một phần mềm trung gian giúp phân tích cú pháp (parse) dữ liệu trong body của yêu cầu HTTP đến trong Node.js. Nó chịu trách nhiệm xử lý dữ liệu body của yêu cầu trước khi thực hiện các thao tác với chúng.
Phần mềm này thường được sử dụng trong các ứng dụng web xây dựng bằng Express.js để xử lý dữ liệu từ các form gửi đi, JSON payload và các loại body yêu cầu khác.
N-API trong Node.js là gì?
N-API là một API giúp xây dựng các Addon gốc cho Node.js. API này hoạt động độc lập với môi trường chạy JavaScript và là một phần tích hợp của Node.js.
Với thiết kế ổn định dựa trên Application Binary Interface (ABI), N-API giúp các Addon không bị ảnh hưởng bởi những thay đổi trong công cụ JavaScript. Nhờ đó, các mô-đun được biên dịch một lần có thể chạy trên nhiều phiên bản Node.js sau này mà không cần phải biên dịch lại.
Non-block trong Node.js là gì?
Trong Node.js, non-blocking đề cập đến khả năng của môi trường thời gian chạy để thực hiện nhiều tác vụ cùng lúc mà không cần chờ một tác vụ hoàn thành trước khi bắt đầu tác vụ tiếp theo. Điều này đạt được thông qua việc sử dụng các hoạt động I/O không đồng bộ, cho phép Node.js xử lý nhiều yêu cầu đồng thời.
Làm thế nào để quản lý session trong Node.js?
Quản lý phiên có thể được thực hiện trong node.js bằng cách sử dụng mô-đun express-session.
Bước 1: Cài đặt mô-đun express-session bằng cách sử dụng lệnh npm install express-session
Bước 2: Kiểm tra phiên bản là một bước tùy chọn, không bắt buộc. Bạn có thể kiểm tra phiên bản đã cài bằng một trong các lệnh sau:
npm list express-session
# Hoặc
npm view express-session version
# Hoặc (trong package.json)
cat package.json | grep express-session
Bước 3: Viết code và sử dụng express-session và khởi chạy chúng với NodeJS
Câu hỏi phỏng vấn NodeJS dành cho Senior Developer
Hãy cho biết tại sao cần có C++ trong Node.js?
Node.js Addon là các đối tượng chia sẻ được liên kết động, viết bằng C++, có thể tải vào Node.js bằng hàm require() và sử dụng như một mô-đun Node.js thông thường, để cung cấp giao diện giữa JavaScript chạy trong Node.js và các thư viện C/C++.
Có nhiều lý do để viết các Addon trong Node.js, ví dụ:
- Bạn muốn truy cập một số API gốc mà JavaScript thông thường không thể xử lý dễ dàng.
- Bạn cần tích hợp một thư viện của bên thứ ba viết bằng C/C++ để sử dụng trực tiếp trong Node.js.
- Bạn muốn viết lại một số mô-đun bằng C++ để cải thiện hiệu suất.
Tại sao bạn nên tách ứng dụng Express và máy chủ?
Đầu tiên, việc tách ứng dụng và máy chủ có thể giúp kiểm tra code dễ dàng hơn. Bằng cách tách biệt hai thành phần này, lập trình viên có thể kiểm tra logic ứng dụng độc lập với máy chủ, từ đó dễ dàng xác định và sửa lỗi hơn.
Thứ hai, việc tách biệt ứng dụng và máy chủ có thể giúp mở rộng quy mô ứng dụng hơn. Nhờ đó lập trình viên có thể chạy nhiều phiên bản ứng dụng trên các máy chủ khác nhau, giúp phân bổ tải và cải thiện hiệu suất.
Cuối cùng, việc tách biệt ứng dụng và máy chủ có thể giúp dễ dàng chuyển sang máy chủ khác, mà không cần phải thực hiện bất kỳ thay đổi lớn nào đối với code.
Viết lại các ứng dụng Node.js dựa trên promise thành Async/Await
Đề bài:
function asyncTask() { return functionA() .then((valueA) => functionB(valueA)) .then((valueB) => functionC(valueB)) .then((valueC) => functionD(valueC)) .catch((err) => logger.error(err)) }
Lời giải:
async function asyncTask() { try { const valueA = await functionA(); const valueB = await functionB(valueA); const valueC = await functionC(valueB); return await functionD(valueC); // Hoặc return functionD(valueC); } catch (err) { logger.error(err); } }
Giải thích khái niệm Domain trong Node.js?
Domains cung cấp cách để quản lý nhiều hoạt động I/O khác nhau. Nếu một event emitter hoặc một callback nào đó được đăng ký trong domain phát sinh lỗi (emit một sự kiện lỗi hoặc quăng ra lỗi), thì đối tượng domain sẽ nhận biết lỗi này.
Điều này giúp tránh mất ngữ cảnh lỗi khi sử dụng process.on(‘uncaughtException’) hoặc ngăn chương trình bị thoát ngay lập tức với mã lỗi.
Lưu ý: Khái niệm này đã bị loại bỏ từ Node.js v16
Làm thế nào để chuyển đổi một callback API hiện có thành promise?
Đề bài
function divisionAPI (number, divider, successCallback, errorCallback) { if (divider == 0) { return errorCallback( new Error("Division by zero") ) } successCallback( number / divider ) }
Lời giải:
function divisionAPI(number, divider) { return new Promise(function(fulfilled, rejected) { if (divider == 0) { return rejected(new Error("Division by zero")) } fulfilled(number / divider) }) } // Promise có thể được dùng chung với async\await trong ES7 để cho program flow phải await kết quả fulfilled async function foo() { var result = await divisionAPI(1, 2); // await kết quả fulfilled! console.log(result); } // Một cách dùng khác với cùng code mà sử dụng .then() method divisionAPI(1, 2).then(function(result) { console.log(result) })
Giải thích một số cách xử lý lỗi trong Node.js mà bạn biết
Có bốn cách xử lý lỗi chính trong node:
- Giá trị trả về lỗi (Error return value) – không hoạt động đồng bộ.
- Quăng lỗi (Error throwing) là một mẫu thiết kế phổ biến, trong đó một hàm thực hiện nhiệm vụ của nó và khi gặp lỗi, nó ngay lập tức dừng lại bằng cách quăng ra một lỗi.
- Gọi lại lỗi (Error callback) trả về lỗi thông qua lệnh callback là mô hình xử lý lỗi phổ biến nhất trong Node.js.
- Phát ra lỗi (Error emitting) là cơ chế gửi lỗi đến các thành phần hoặc hàm đã đăng ký lắng nghe (thuê bao). Khi lỗi được phát ra, nó sẽ được xử lý ngay lập tức trong cùng một chu kỳ của vòng lặp sự kiện, theo thứ tự mà các thuê bao được đăng ký.
Làm thế nào để đọc đối số trong Node.js?
Đối số dòng lệnh (CLI) là chuỗi văn bản được sử dụng để truyền thông tin bổ sung cho chương trình khi ứng dụng đang chạy qua giao diện dòng lệnh của hệ điều hành. Lập trình viên có thể dễ dàng đọc các đối số này bằng đối tượng toàn cục trong nút tức là đối tượng quy trình.
Các bước đọc đối số trong Node.js như sau:
Bước 1: Lưu tệp dưới dạng index.js và dán mã bên dưới vào tệp.
let arguments = process.argv ; console.log(arguments) ;
Bước 2: Chạy tệp index.js bằng lệnh node index.js
Hãy viết lại mẫu code bên dưới mà không sử dụng khối catch/try
Đề bài:
async function check(req, res) { try { const a = await someOtherFunction(); const b = await somethingElseFunction(); res.send("result") } catch (error) { res.send(error.stack); } }
Lời giải:
async function getData(){ const a = await someFunction().catch((error)=>console.log(error)); const b = await someOtherFunction().catch((error)=>console.log(error)); if (a && b) console.log("some result") }
Hoặc nếu bạn muốn biết chức năng cụ thể nào gây ra lỗi có thể viết lại như sau:
async function loginController() { try { const a = await loginService(). catch((error) => { throw new CustomErrorHandler({ code: 101, message: "a failed", error: error }) }); const b = await someUtil(). catch((error) => { throw new CustomErrorHandler({ code: 102, message: "b failed", error: error }) }); //someoeeoe if (a && b) console.log("no one failed") } catch (error) { if (!(error instanceof CustomErrorHandler)) { console.log("gen error", error) } } }
Libuv hoạt động như thế nào?
Libuv cung cấp một vòng lặp sự kiện (event loop), trong đó chỉ có một luồng duy nhất thực thi mã JavaScript. Tất cả các callback (mã người dùng trong ứng dụng Node.js) đều được thực thi thông qua vòng lặp sự kiện này.
Mặc định, libuv tạo ra một thread pool gồm bốn luồng để xử lý các công việc bất đồng bộ. Tuy nhiên, trong hầu hết trường hợp, libuv ưu tiên sử dụng các giao diện bất đồng bộ do hệ điều hành cung cấp (như AIO trên Linux) để giảm tải cho thread pool.
Hãy giải thích kết quả khi thực thi code bên dưới
Đề bài:
var EventEmitter = require("events"); var crazy = new EventEmitter(); crazy.on('event1', function () { console.log('event1 fired!'); crazy.emit('event2'); }); crazy.on('event2', function () { console.log('event2 fired!'); crazy.emit('event3'); }); crazy.on('event3', function () { console.log('event3 fired!'); crazy.emit('event1'); }); crazy.emit('event1');
Bạn sẽ gặp một lỗi ngoại lệ thông báo rằng “call stack đã bị tràn”. Nguyên nhân là do mỗi lệnh phát ra sẽ gọi mã theo cách đồng bộ. Vì tất cả các lệnh gọi lại đều được thực thi đồng bộ, nên chương trình sẽ đệ quy chính nó không ngừng, dẫn đến tràn bộ nhớ stack.
Output:
console.js:165 if (isStackOverflowError(e)) ^ RangeError: Maximum call stack size exceeded at write (console.js:165:9) at Console.log (console.js:197:3) at EventEmitter.<anonymous> (C:\Work\Node\main.js:6:13) at EventEmitter.emit (events.js:182:13) at EventEmitter.<anonymous> (C:\Work\Node\main.js:18:11) at EventEmitter.emit (events.js:182:13) at EventEmitter.<anonymous> (C:\Work\Node\main.js:12:11) at EventEmitter.emit (events.js:182:13) at EventEmitter.<anonymous> (C:\Work\Node\main.js:7:11) at EventEmitter.emit (events.js:182:13)
Điều gì sẽ xảy ra khi đoạn mã bên dưới được thực thi?
Đề bài
var EventEmitter = require('events'); var crazy = new EventEmitter(); crazy.on('event1', function () { console.log('event1 fired!'); process.nextTick(function () { crazy.emit('event2'); }); }); crazy.on('event2', function () { console.log('event2 fired!'); process.nextTick(function () { crazy.emit('event3'); }); }) crazy.on('event3', function () { console.log('event3 fired!'); process.nextTick(function () { crazy.emit('event1'); }); }); crazy.emit('event1');
Kết quả thực tế:
Khi chạy đoạn mã, bạn sẽ thấy output tương tự như sau (tiếp tục vô hạn):
event1 fired! event2 fired! event3 fired! event1 fired! event2 fired! event3 fired!
Mô-đun cụm (cluster module) hoạt động như thế nào? Có gì khác so với bộ load balance?
Mô-đun Cluster trong Node.js tạo ra các tiến trình con bằng cách fork từ máy chủ chính. Nó hỗ trợ hai cách phân phối kết nối:
- Phương pháp luân phiên (round-robin): Đây là cách mặc định (trừ trên Windows). Quy trình chính sẽ lắng nghe trên một cổng, nhận các kết nối mới và phân phối chúng lần lượt đến các tiến trình con. Phương pháp này tích hợp một số cơ chế thông minh để đảm bảo không có tiến trình nào bị quá tải.
- Phương pháp lắng nghe trực tiếp: Quy trình chính tạo một socket lắng nghe và gửi socket này đến các tiến trình con. Các tiến trình này sẽ trực tiếp chấp nhận các kết nối đến.
Điểm khác biệt giữa mô-đun Cluster và bộ cân bằng tải (load balancer) là: Cluster phân phối tải giữa các tiến trình, còn bộ cân bằng tải phân phối các yêu cầu đến các máy chủ khác nhau.
Tổng kết câu hỏi phỏng vấn NodeJS
Như vậy, bạn đã hoàn thành bộ câu hỏi phỏng vấn NodeJS rồi đấy. Những bài thực hành và phần lý thuyết hẳn không làm khó bạn đúng không nào? Ngoài những kiến thức trên, đừng quên tiếp tục cập nhật những kiến thức mới về Node.js và thực hành nhiều hơn nhé!