Một bài viết chi tiết hơn về ESM và CommonJS modules trong Node.js

Một bài viết chi tiết hơn về ESM và CommonJS modules trong Node.js

Những mẩu tin ngắn hàng ngày dành cho bạn
  • Tin tức sáng sớm, mọi người còn nhớ vụ kiện của Ryan Dahl - hay nói đúng hơn là của nhóm Deno với Oracle về cái tên JavaScript không?

    Oracle đã phản hồi rằng họ không từ bỏ cái tên JavaScript đâu 🫣

    https://x.com/deno_land/status/1876728474666217739

    » Xem thêm
  • Mọi người nghỉ tết sớm rồi hay sao á? Nhiên cái nguyên tuần nay traffic giảm hẳn luôn 😳. Một mình tuôi nói kể cũng buồn, ai đi ngang qua đọc được thì thả một "còm men" cho vui cửa vui nhà nha. Nói gì cũng được vì ẩn danh cả mà 😇🔥

    » Xem thêm
  • Có người hỏi mình là cập nhật tin tức ở đâu mà nhanh thế, hay là kiếm ra được mấy cái tools, mấy cái projects... ở đâu mà nhiều thế? Thì có một nguồn xa tận chân trời mà gần ngay trước mắt đó chính là trang Github Trending này đây.

    Trang này thống kê lại các kho lưu trữ đang có lượt "star" nhiều nhất theo ngày/tuần/tháng. Nó còn xem theo được ngôn ngữ cơ, mà mỗi ngôn ngữ lại kiểu như một chủ đề á. Ví dụ Python thì hót rần rần về AI, LLMs..., Rust thì bao tools siêu mạnh, còn Go thì... đồ chơi liên tục 😁. Trong khi JavaScript 🫣😑

    » Xem thêm

Vấn đề

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.jsTì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ách ESM và CommonJS hoạt động

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ápimportrequire để sử dụng chúng.

Có thể sử dụng CommonJS và ESM đan xen nhau không?

Câu trả lời là có. Bạn có thể làm theo hướng dẫn dưới đây.

Nhập modules CommonJS vào dự án ESM

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

Nhập modules ESM vào CommonJS

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

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 requireimport 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

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.

Tổng kết

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:

Cao cấp
Hello

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!

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!

Xem tất cả

Đăng ký nhận thông báo bài viết mới

hoặc
* Bản tin tổng hợp được gửi mỗi 1-2 tuần, huỷ bất cứ lúc nào.

Bình luận (3)

Nội dung bình luận...
Avatar
Phạm Tiến Đạt8 tháng trước
- Em cám ơn anh. Bài viết của anh rất hay
Trả lời
Avatar
Xuân Hoài Tống8 tháng trước
Cảm ơn e nhé, ghé blog a thường xuyên nha vì a viết hàng tuần á.
Avatar
Phạm Tiến Đạt8 tháng trước
- Hiện tại em viết một NPM package và dùng Typescript để compie ra hai file một là cjs và esm, sau đó em dùng cjs trong main và esm trong module ở package.json như vậy thì nếu em install package này dùng trong Node Server thì mặc định sẽ dùng ở cjs phải không anh và ngươc lại ở các dự án Reacts thì sẽ dùng ở ESM ạ. Mong anh trả lời ạ, em cám ơn anh.
Trả lời
Avatar
Xuân Hoài Tống8 tháng trước
Ồ nếu thế thì e có thể tham khảo phần Universal modules trong bài viết kia, nó dùng để cấu hình cho Node hoặc Trình duyệt biết nên import tệp nào. Hoặc tham khảo 2 thuộc tính main và browser tại https://docs.npmjs.com/cli/v10/configuring-npm/package-json#main Điều quan trọng là phải có cấu hình 2 thuộc tính này trong package.json thì Node/Trình duyệt mới hiểu được em đang muốn dùng tệp nào ở môi trường nào.
Avatar
Phạm Tiến Đạt8 tháng trước
Dạ em chào anh, em có một câu hỏi ạ Theo như anh đề cập thì Commonjs là module trong Nodejs, và Esm là module được sử dụng trong trình duyệt vậy khi import Commonjs vào Esm thì có cần phải qua build tool, webpackage, babel để convert không anh.
Trả lời
Avatar
Xuân Hoài Tống8 tháng trước
Chào em, một câu hỏi thú vị. Khi em muốn import commonjs vào esm thì anh hiểu em đang làm việc với dự án Node.js, Node ở các phiên bản hiện tại hỗ trợ cả commonjs và esm nên em có thể làm như thế mà không cần qua build tools nào cả, Node tự làm. Nhưng như thế nhiều vấn đề có thể xuất hiện, ví dụ như em chạy các phiên bản Node thấp hơn hay các phiên bản Node cao hơn có thể ngừng hỗ trợ commonjs cho nên tốt nhất là vẫn nên dùng thêm các build tools để được hỗ trợ lâu dài và đa phiên bản. Còn trong chiều ngược lại, em đang làm dự án của trình duyệt thì muốn import commonjs vào esm thì phải qua build tools rồi em nhé!
Bấm hoặc cuộn mạnh để sang bài mới