Trước kia tôi đã viết một số bài nói về các loại modules trong Node.js cũng như trong JavaScript. Đại khái là có nhắc đến CommonJS, AMD rồi cả ESM modules nữa, bạn đọc có thể xem lại tại hai bài viết Tìm hiểu về require trong node.js và Tìm hiểu về modules trong Node.js. Tại sao lại có nhiều loại modules như vậy?. Tuy nhiên chưa đi sâu vào chúng.
Nhiều người thắc mắc không biết lúc nào thì dùng require
, lúc nào thì dùng import
. Hay có thể sử dụng cả hai trong cùng một dự án được hay không? Bài viết ngày hôm nay, chúng ta hãy cùng nhau tìm hiểu về cách hoạt động của hai loại modules này trong Node.js để trả lời những thắc mắc ở bên trên nhé.
Có thể nói rằng, việc ngay từ đầu không có hệ thống modules rõ ràng nào trong JavaScript đã dẫn đến việc phức tạp hóa vấn đề như hiện nay. Cộng đồng sử dụng JavaScript đời đầu đã phải tự tay tạo ra các thể loại modules cho nó, trong đó có thể kể đến như là AMD, UMD... Khi đó, Node.js ra đời và lựa chọn CommonJS làm trình xử lý modules mặc định.
Nhận ra thiếu sót của mình, cuối cùng ECMAScript đã phải giới hiệu hệ thống modules chính thức cho đặc tả này là ESM. Ngay lập tức, JavaScript được cập nhật loại modules này, nhưng có lẽ hơi muộn vì giờ đây đã có quá nhiều package được tạo ra trước đó sử dụng hệ thống modules không chính thức. Nhưng dẫu sao có còn hơn không, vấn đề chỉ còn là thời gian cho đến khi ESM được phổ biến rộng rãi.
Tóm lại, có thể hiểu CommonJS được sinh ra để sử dụng trong Node.js. ESM là hệ thống modules chính thức của JavaScript, nhưng vì sinh sau đẻ muộn cho nên ESM chỉ được hỗ trợ từ Node.js 12+, còn trong trình duyệt hiện đại ngày nay thì ESM hầu như đều có thể chạy được.
Vậy trình duyệt có thể chạy được CommonJS không? Câu trả lời là không. Nhưng chúng ta vẫn có thể tạo ra được package hỗ trợ có thể chạy được trong cả hai môi trường.
Nếu vậy thì trình duyệt có thể chạy được tất cả ESM modules chứ? Câu trả lời vẫn là không. Hiểu đơn giản nếu trong modules có chứa các hàm của Node.js mà trình duyệt không có thì chắc chắc nó sẽ không thể nào chạy được. Ngược lại, ESM cũng chưa chắc đã chạy được trong Node.js vì có thể nó chứa các hàm mà trong Node.js không hỗ trợ. Tóm lại, chạy được hay không còn phụ thuộc vào người phát triển có hỗ trợ nữa hay không.
Ví dụ tạo ra một module bằng CommonJS:
// add.js file
// hoặc add.cjs file
function add(a, b) {
return a + b;
}
module.exports = add;
Đuôi mở rộng .cjs
là hoàn toàn hợp lệ và nó dùng để nhắc cho Node.js biết rằng đây là một module sử dụng CommonJS.
Sau đó chúng ta có thể sử dụng module bằng cú pháp require
:
const add = require('./add.js')
add(1, 2);
Tương tự, một module bằng ESM sẽ trông giống như sau:
// add.js file
// hoặc add.mjs file
function add(a, b) {
return a + b;
}
export default add;
Tương tự, .mjs
cũng là một đuôi hợp lệ và nó nhắc cho Node.js biết rằng đây là module sử dụng ESM.
Sử dụng EMS module bằng cú pháp import
:
import add from './add.js';
add(1, 2);
Như vậy, chúng ta có thể thấy sự khác biệt lớn nhất giữa CommonJS và ESM nằm ở cú pháp xuất/nhập module, cũng như cú phápimport
và require
để sử dụng chúng.
Câu trả lời là có. Bạn có thể làm theo hướng dẫn dưới đây.
Rất đơn giản, sử dụng cú pháp import
như bình thường:
// index.js
import add from './add.cjs';
add(1, 2);
Vì require
là hàm đồng bộ nên không thể sử dụng nó để nhập các modules ESM. Thay vào đó, để nhập ESM vào CommonJS, chúng ta sẽ sử dụng import
. import
trả về một Promise, trong kết quả chứa một trường default
trỏ đến điểm xuất modules mặc định của ESM.
// index.js
(async function () {
const add = (await import('./index.mjs')).default;
add(1, 2);
})();
Vì await
bắt buộc phải được gọi trong một hàm async nên cách viết trên mới hoạt động được. Hoặc bạn có thể sử dụng một phiên bản Node.js mới hơn có hỗ trợ Top-level await.
Đôi khi có thể thấy trong một số dự án sử dụng được cả cú pháp require
và import
song song. Đó nhiều khả năng là do nó có sử dụng một số công cụ hỗ trợ chuyển mã như Typescript, Webpack, Rollup... Về bản chất, chúng ta có thể thoải mái viết cú pháp require
hoặc import
, nhưng sau khi build, công cụ sẽ chuyển mã về thống nhất một cú pháp của CommonJS hoặc ESM theo thiết lập từ trước.
Universal modules là khái niệm chỉ một module có thể hoạt động trên cả Node.js lẫn trình duyệt. Tức là tùy vào trường hợp sử dụng modules trong môi trường nào thì nó sẽ tự động sử dụng cú pháp của CommonJS hoặc ESM.
Để làm được điều đó, rất đơn giản chỉ cần khai báo đường dẫn đến tệp được sử dụng trong từng môi trường trong file package.json
.
{
...
"main": "cjs/index.js",
"module": "es6/index.js",
...
}
Với main
là đường dẫn đến index.js
sử dụng cú pháp của CommonJS, module
thì dẫn đến index.js
sử dụng cú pháp của ESM. Sau này cài đặt và sử dụng package đó trong môi trường Node.js hoặc trình duyệt, chúng sẽ tự hiểu và lựa chọn loại module tương thích. Lúc này bạn có thể viết mã tương thích với trình duyệt hoặc Node.js trong cùng một package.
"Khoan đã, như vậy thì tôi phải viết mã hai lần ở hai nơi sao?" Có thể! Nhưng chẳng ai làm vậy, bởi vì build tools sẽ giúp chúng ta tạo ra Universal module, chỉ bằng một lần viết mã và qua một bước build để sinh ra mã cho cả hai môi trường.
Có rất nhiều build tools mà bạn có thể tìm thấy, ví dụ như là webpack, rollup và một cái tên mới nổi gần đây về tốc độ chính là esbuild.
Bạn đọc có thể tham khảo documents của công cụ muốn sử dụng hoặc tìm kiếm ví dụ cũng như các khung để bắt đầu nhanh trên github.
CommonJS hoạt động trong Node.js nhưng không hoạt động trong trình duyệt. ESM được hỗ trợ bởi tất cả các trình duyệt hiện đại và các phiên bản mới nhất của Node.js 12+.
Rất nhiều công cụ trong hệ sinh thái JavaScript đã được phát triển trong Node.js, và Node chỉ mới bắt đầu hỗ trợ ESM gần đây, do đó, một phần lớn các dự án đều đang sử dụng CommonJS.
Nếu bạn đang bắt đầu với một dự án Node.js mới và phân vân giữa việc có nên hỗ trợ hoàn toàn ESM hay không thì hãy nghĩ đến việc rất nhiều package thông dụng trên npm vẫn đang sử dụng CommonJS. Mặc dù ESM có khả năng hỗ trợ nhập modules bằng CommonJS nhưng vẫn nên cân nhắc khả năng xảy ra sự cố và khắc phục sự cố sau này.
Cuối cùng Universal là một khái niệm chỉ các packages có thể hoạt động trong nhiều môi trường khác nhau như Node và trình duyệt. Bằng cách khai báo đường dẫn nhập tệp trong package.json
để qua đó chỉ dẫn cho môi trường biết cách nhập các loại modules tương thích.
Tham khảo:
Tôi & khao khát "chơi chữ"
Bạn đã thử viết? Và rồi thất bại hoặc chưa ưng ý? Tại 2coffee.dev chúng tôi đã có quãng thời gian chật vật với công việc viết. Đừng nản chí, vì giờ đây chúng tôi đã có cách giúp bạn. Hãy bấm vào để trở thành hội viên ngay!
Đăng ký nhận thông báo bài viết mới
Bình luận (3)