Node.js: Khám Phá Bí Mật Của Mô-đun CommonJS
Trước khi Node.js ra đời, việc phát triển JavaScript phía máy chủ hầu như không tồn tại, và tiêu chuẩn module ES6 chưa được đề xuất. Do đó, Node.js đã lựa chọn tiêu chuẩn mô-đun tiên tiến lúc bấy giờ là CommonJS, thường được viết tắt là CJS.
Thông số Kỹ thuật của CommonJS
1. Vấn Đề Trước CommonJS
Trước khi áp dụng tiêu chuẩn CommonJS, Node.js đã phải đối mặt với nhiều khó khăn:
- Thiếu hệ thống module.
- Hạn chế của thư viện chuẩn.
- Thiếu giao diện chuẩn.
- Không có hệ thống quản lý gói.
Những vấn đề này đã gây ra khó khăn trong việc xây dựng các dự án lớn bằng Node.js, dẫn đến một hệ sinh thái phát triển kém cần cải thiện. CommonJS đã ra đời nhằm khắc phục vấn đề thiếu tiêu chuẩn module trong JavaScript, cho phép việc xây dựng các ứng dụng lớn một cách hiệu quả, tương tự như các ngôn ngữ khác như Java, Python và Ruby. Hệ sinh thái phát triển mạnh mẽ của Node.js ngày nay có nhiều thành tựu đến từ CommonJS.
1.1 Thông số Kỹ thuật Module CommonJS
CommonJS định nghĩa module một cách rõ ràng, chú trọng vào sự tham chiếu, định nghĩa và xác định module.
1.1.1 Tham Chiếu Module
Ví dụ:
const fs = require('fs');
Trong CommonJS, hàm require
là hàm toàn cục nhận định danh module và nhập API tương ứng của module vào phạm vi hiện tại.
1.1.2 Định Nghĩa Module
Node.js cung cấp một đối tượng module
và exports
. module
đại diện cho module hiện tại, trong khi exports
cho phép xác định các API sẽ được xuất. Từng tệp sẽ trở thành một module, và các phương thức hoặc biến có thể được gán vào đối tượng exports
để làm cho chúng trở thành một phần của module.
javascript
// add.js
exports.add = function(a, b) {
return a + b;
};
Trong một tệp khác, bạn có thể nhập module này bằng cách sử dụng require
như sau:
const { add } = require('./add.js');
add(1, 2); // Xuất 3
1.1.3 Xác Định Module
ID module là tham số của hàm require
, nó có thể là một chuỗi theo kiểu camelCase, hoặc đường dẫn tương đối bắt đầu bằng .
hoặc ..
, hoặc đường dẫn tuyệt đối và có thể không có phần mở rộng tệp. Cách xác định này rất đơn giản và giao diện rõ ràng, giúp hạn chế các phương thức và biến trong phạm vi riêng và hỗ trợ việc nhập và xuất một cách mạch lạc mà không lo ô nhiễm biến.
2. Triển Khai Module Trong Node.js
Node.js không hoàn toàn tuân thủ tiêu chuẩn CommonJS mà thực hiện một số điều chỉnh và bổ sung các tính năng cần thiết. Hãy cùng tìm hiểu về cách Node.js triển khai CommonJS.
Khi nhập module trong Node.js, quá trình này trải qua ba bước:
- Phân tích đường dẫn
- Định vị tệp
- Biên dịch và thực thi
Khái Niệm Cốt Lõi
- Module Cốt Lõi: Các module tích hợp sẵn như
fs
,url
,http
, v.v. - Module Tệp: Các module do người dùng tạo như Koa, Express.
Các module cốt lõi được biên dịch thành tệp nhị phân và tải trực tiếp vào bộ nhớ khi khởi động, do đó, việc nhập các module này nhanh chóng hơn so với các module tệp, vốn cần trải qua quy trình phân tích đường dẫn, định vị tệp, biên dịch và thực thi.
2.1 Tải Từ Bộ Nhớ Cache
Node.js lưu trữ các module đã được tải vào bộ nhớ cache. Khi cùng một module được tải lại, nó sẽ được lấy từ bộ nhớ, bỏ qua quá trình tìm kiếm và biên dịch, giúp tăng tốc độ tải đáng kể. Cả module cốt lõi và module tệp đều có thể được tải từ bộ nhớ cache, nhưng module cốt lõi được ưu tiên hơn.
Hàm require
trong Node.js được định nghĩa trong lib/internal/modules/cjs/loader.js
như là Module.prototype.require
, với một lớp bao bọc bổ sung trong hàm makeRequireFunction
.
2.2 Phân Tích Đường Dẫn và Định Vị Tệp
Node.js xử lý phân tích đường dẫn và định vị tệp thông qua phương thức Module._resolveFilename
, xác định đường dẫn đầy đủ của tệp module dựa vào ID module.
- Đường dẫn tương đối: Nếu ID bắt đầu bằng
./
hoặc../
, Node.js sẽ coi đó là một đường dẫn tương đối. - Đường dẫn tuyệt đối: Nếu ID bắt đầu bằng
/
, đây là đường dẫn tuyệt đối. - Đường dẫn module: Nếu không bắt đầu với
.
hoặc/
, Node.js sẽ tìm kiếm trong các thư mụcnode_modules
và thử thêm các phần mở rộng.js
,.json
, và.node
.
2.3 Biên Soạn và Thực Thi
Khi việc định vị tệp hoàn tất, Node.js sẽ chọn một chiến lược biên dịch và thực thi dựa trên phần mở rộng tệp như sau:
- Tệp JavaScript: Được đọc bằng module
fs
và thực thi bằng cách bọc nội dung trong một hàm sử dụng modulevm
. - Tệp JSON: Được đọc và phân tích bằng
JSON.parse
. - Mở rộng C/C++: Được tải thông qua
process.dlopen
.
3. Tối Ưu Hóa và Mở Rộng Tải Module
3.1 Lưu Trữ Module
Node.js sử dụng Module._cache
để lưu trữ các module đã được tải, điều này giúp cải thiện tốc độ tải. Cơ chế này đảm bảo mỗi tệp module, khi đã tải, có thể được truy cập nhanh chóng trong những lần kế tiếp.
3.2 Mở Rộng Tải Module
Node.js cho phép người dùng tùy chỉnh hành vi tải module thông qua require.extensions
. Mặc dù không khuyến nghị cho sản xuất, nó có thể hữu ích trong các tình huống cụ thể.
javascript
require.extensions['.txt'] = function(module, filename) {
const content = fs.readFileSync(filename, 'utf8');
module.exports = content;
};
3.3 Bọc Và Phạm Vi
Node.js bọc mã của mỗi module trong một hàm, giúp cách ly phạm vi module và ngăn ô nhiễm phạm vi toàn cục.
4. Hỗ Trợ Nhập Khẩu Động và ES Module
4.1 Nhập Khẩu Động
Node.js hỗ trợ nhập khẩu động qua hàm import()
, cho phép bạn tải module tại thời điểm chạy, rất thuận tiện cho việc tối ưu hóa hiệu suất.
javascript
async function loadModule() {
const module = await import('./myModule.js');
module.doSomething();
}
Hàm import()
trả về một Promise, giúp xử lý module nhập khẩu bằng cách sử dụng async/await
.
4.2 Hỗ Trợ ES Module
Node.js đã tích hợp hỗ trợ ES Module (ESM) kể từ phiên bản 12, cho phép bạn sử dụng cú pháp import
và export
. Để sử dụng ES Module, bạn có thể chuyển đổi phần mở rộng tệp thành .mjs
hoặc thêm "type": "module" vào tệp package.json
.
5. Kết Luận
Node.js cung cấp một hệ thống module mạnh mẽ và linh hoạt để tổ chức mã nguồn một cách hiệu quả. Với sự hỗ trợ cho cả CommonJS và ES Module, bạn có thể lựa chọn phương pháp phù hợp nhất với dự án của mình. Sự phát triển không ngừng của Node.js giúp bạn luôn có thể tận dụng các tính năng mới tối ưu hóa hiệu suất ứng dụng của mình.
source: viblo