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
  • Hẳn là nhiều người ở đây đã nghe đến kiểu tấn công bảo mật Clickjacking rồi nhỉ. Kẻ tấn công thường nhúng một website (thường là mục tiêu) vào trong một iframe trên website của chúng, sau đó làm mờ hoặc ẩn nó đi rồi đặt vào vị trí các nút bấm trên web, ví dụ "Bấm vào để nhận quà". Đâu ai ngờ rằng phía trên nút bấm đó là một nút bấm khác trong iframe. Khá nguy hiểm!

    Nhưng trình duyệt đã có cách ngăn chặn kiểu tấn công này bằng các quy tắc như tiêu đề X-Frame-Options, frame-ancestors của CSP và SameSite: Lax/Strict của Cookies...

    Mới đây, đã xuất hiện thêm kiểu tấn công mới - "DoubleClickjacking" 😨. Đại ý là "hắn" lợi dụng hành động double click để lừa người dùng bấm vào một nút mà hắn muốn. Chi tiết hơn trong bài viết này: DoubleClickjacking: A New Era of UI Redressing.

    » Xem thêm
  • Mọi người đã nghe nói đến Jujutsu - jj - một dạng quản lý phiên bản cho mã nguồn (version control system) chưa? Có vẻ như nó đang nhận được nhiều sự quan tâm.

    Chờ xíu! Chẳng phải git đã quá tốt rồi sao? Thế thì chế ra thằng jj để làm gì nữa? Cũng hơi khó trả lời nhỉ? Mỗi công cụ sinh ra chắc chắn phải cải thiện hoặc khắc phục được nhược điểm của cái trước. Cho nên jj ắt hẳn phải làm được điều gì đó mà git chưa làm được nên mới nổi lên như vậy.

    Thật ra mình đã nghe nói đến jj từ vài tháng trước rồi, nhưng vào đọc thì toàn kiến thức cao siêu. Hoặc là đang mang nặng cái lối suy nghĩ của git vào trong đầu rồi nên chưa lĩnh hội ra được điều gì cả.

    Mình hay có kiểu cái gì đọc lần 1 mà không hiểu thì đọc tiếp lần 2, lần 2 không hiểu thì đọc tiếp lần 3... đến lần thứ n mà vẫn không hiểu thì bỏ. Cơ mà không phải là từ bỏ mà một thời gian sau đó quay lại đọc tiếp. Đến một lúc nào đó khả năng mình sẽ hiểu ra một ít vấn đề, thế mới tài 😆.

    Thì cái jj này có vẻ như nó đang mở ra được tính linh hoạt trong việc "cam kết" mã. Tưởng tượng bạn đang làm việc trên một dự án, đang ở nhánh này, muốn sang nhánh khác để sửa, nhưng mà lại đang viết dở ở nhánh này, thế là phải stash, rồi checkout, rồi commit, rồi merge hoặc rebase lại vào nhánh cũ... nhìn chung quá trình làm việc với git nghiêm ngặt đến mức cứng nhắc, cần nhiều thao tác để giải quyết một vấn đề, chưa kể cái cây commit (commit-tree) nữa thì ôi thôi, khỏi xem cho đỡ nhức mắt. Thế nên ông jj này đang làm cách nào đó để bạn khỏi cần phải quan tâm đến các nhánh luôn, sửa trực tiếp vào commit. Nghe ảo nhỉ 😂.

    Đấy mới lĩnh hội được đến đấy, hy vọng sau n lần đọc lại nữa mình sẽ viết được một bài chi tiết hơn về công cụ này.

    » Xem thêm
  • Gòi gòi tới công chiện gòi 🤤🤤🤤

    » 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

5 bài học sâu sắc

Mỗi sản phẩm đi kèm với những câu chuyện. Thành công của người khác là nguồn cảm hứng cho nhiều người theo sau. 5 bài học rút ra được đã thay đổi con người tôi mãi mãi. Còn bạn? Hãy bấm vào ngay!

Mỗi sản phẩm đi kèm với những câu chuyện. Thành công của người khác là nguồn cảm hứng cho nhiều người theo sau. 5 bài học rút ra được đã thay đổi con người tôi mãi mãi. Còn bạn? Hãy bấm vào 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