Get in touch
or send us a question?
CONTACT

Bài 2. Common JS và ESM modules trong NodeJS

CommonJS và ESM modules trong NodeJS.

Trong quá trình phát triển phần mềm hiện đại, các mô-đun tổ chức code của phần mềm thành các khối độc lập cùng nhau tạo nên một ứng dụng lớn hơn, phức tạp hơn.

Trong hệ sinh thái JavaScript của Browser, việc sử dụng các mô-đun JavaScript phụ thuộc vào các câu lệnh import export các câu lệnh này lần lượt được load và export các mô-đun EMCAScript (hoặc mô-đun ES).

Định dạnh mô-đun ES là định dạng tiêu chuẩn chính thức để đóng gói các đoạn mã JavaScript để tái sử dụng và hầu hết các trình duyệt web hiện đại đều hỗ trợ các mô-đun này.

Tuy nhiên, NodeJS hỗ trợ định dạng mô-đun CommonJS theo mặc định. Các mô-đun CommonJS load bằng cách sử dụng require(), và hàm xuất từ mô-đun CommonJS bằng module.exports.

Định dạng mô-đun ES đã được giới thiệu trong NodeJS v8.5.0 khi hệ thống mô-đun JavaScript được chuẩn hóa. Là một mô-đun thử nghiệm, cần có cờ –experimental-modules để chạy thành công mô-đun ES trong môi trường NodeJS.

Tuy nhiên, bắt đầu từ phiên bản 13.2.0, NodeJS đã hỗ trợ ổn định các mô-đun ES.

Bài viết này sẽ không đề cập nhiều đến việc sử dụng cả hai định dạng mô-đun, mà là cách CommonJS so sánh với các mô-đun ES và lý do tại sao bạn có thể muốn sử dụng cái này hơn cái kia.

So sánh cú pháp mô-đun CommonJS và mô-đun ES.

Theo mặc định, NodeJS coi mã JavaScript là các mô-đun Common JS. Do đó, các mô-đun CommonJS được đặc trưng bởi câu lệnh require() để nhập mô-đun và module.exports để xuất mô-đun.

Ví dụ: đây là mô-đun CommonJS xuất hai chức năng:

module.exports.add = function(a,b){
     return a+b;
}

module.exports.subtract= function(a,b){
     return a-b;
}

chúng ta có thể import các public function vào một tệp NodeJS khác bằng cách sử dụng từ khóa require(), giống như ví dụ dưới đây.

const {add, subtract} = require('./util')

console.log(add(5,5)) //10
console.log(substract(10,5)) //5

Nếu bạn muốn tìm kiếm một hướng dẫn chuyên sâu hơn về các mô-đun CommonJS, hãy xem hướng dẫn này.

Mặt khác, tác giả thư viện cũng có thể chỉ cần kích hoạt các mô-đun ES trong gói NodeJS bằng cách thay đổi phần mở rộng tệp từ .js thành .mjs.

Ví dụ: đây là một mô-đun ES đơn giản (có phần mở rộng .mjs) xuất hai hàm để sử dụng chung:

// util.mjs

export function add(a,b) {
   return a + b;
}

export function subtract(a,b){
   return a - b;
}

Sau đó, chúng ta có thể nhập cả hai hàm này bằng cách sử dụng câu lệnh import:

// app.mjs

import {add, subtract} from './util.mjs'

console.log(add(5,5)) //10
console.log(subtract(10,5)) //5

Có thể thực hiện một cách khác để kích hoạt các mô-đun ES trong dự án của bạn bằng cách thêm trường “type:module” bên trong tệp package.json gần nhất ( cùng thư mục với package bạn đang tạo):

{
  "name": "my-library",

  "version": "1.0.0",

  "type": "module",

  // ...
}

Với sự bao gồm đó, NodeJS coi tất cả các tệp bên trong gói đó là các mô-đun ES và bạn sẽ không phải thay đổi tệp thành phần mở rộng .mjs. Bạn có thể tìm hiểu thêm về các mô-đun ES tại đây.

Ưu và nhược điểm của việc sử dụng mô-đun ES và mô-đun CommonJS trong NodeJS.

Các mô-đun ES là tiêu chuẩn cho JavaScript trong khi CommonJS là mặc định trong NodeJS.

Định dạng mô-đun ES được tạo để chuẩn hóa hệ thống mô-đun JavaScript. Nó đã trở thành định dạng tiêu chuẩn để đóng gói mã JavaScript để tái sử dụng.

Mặt khác, hệ thống mô-đun CommonJS được tích hợp vào NodeJS. Trước khi giới thiệu mô-đun ES trong NodeJS, CommonJS là tiêu chuẩn cho các mô-đun NodeJS. Do đó, có rất nhiều thư viện và mô-đun NodeJS được viết bằng CommonJS

Để hỗ trợ trình duyệt, tất cả các trình duyệt đều hỗ trợ cú pháp mô-đun ES và bạn có thể sử dụng import / export trong các framework như React và Vue.js. Các framework này sử dụng một bộ chuyển mã như Babel để biên dịch cú pháp import / export thành require(), mà các phiên bản NodeJS cũ hơn vốn hỗ trợ.

Ngoài việc là tiêu chuẩn cho các mô-đun JavaScript, cú pháp mô-đun ES cũng dễ đọc hơn nhiều so với require(). Các nhà phát triển web chủ yếu viết mã JavaScript trên máy client sẽ không gặp vấn đề gì khi làm việc với các mô-đun NodeJS nhờ cú pháp giống hệt nhau.

NodeJS hỗ trợ mô-đun ES

Các phiên bản NodeJS cũ hơn không hỗ trợ các mô-đun ES

Mặc dù các mô-đun ES đã trở thành định dạng mô-đun chuẩn trong JavaScript, nhưng các nhà phát triển nên xem xét rằng các phiên bản NodeJS cũ hơn thiếu hỗ trợ (cụ thể là NodeJS V9 trở xuống).

Nói cách khác, việc sử dụng các mô-đun ES khiến ứng dụng không tương thích với các phiên bản NodeJS trước đó chỉ hỗ trợ các mô-đun CommonJS (nghĩa là cú pháp require()).

Nhưng với tính năng exports có nhiều điều kiện mới, chúng ta có thể xây dựng các thư viện chế độ kép. Đây là những thư viện bao gồm các mô-đun ES mới hơn, nhưng chúng cũng tương thích ngược với định dạng mô-đun CommonJS được hỗ trợ bởi các phiên bản NodeJS cũ hơn.

Nói cách khác, chúng ta có thể xây dựng một thư viện hỗ trợ cả import() require(), cho phép chúng ta giải quyết vấn đề không tương thích.

Hãy xem xét dự án NodeJS sau:

my-node-library
├── lib/
│   ├── browser-lib.js (iife format)
│   ├── module-a.js  (commonjs format)
│   ├── module-a.mjs  (es6 module format)
│   └── private/
│       ├── module-b.js
│       └── module-b.mjs
├── package.json
└── …

Bên trong package.json, chúng ta có thể sử dụng trường exports để xuất mô-đun chung (module-a) ở hai định dạng mô-đun khác nhau trong khi hạn chế quyền truy cập vào mô-đun riêng(module-b).

// package.json
{
  "name": "my-library",         
  "exports": {
    ".": {
        "browser": {
          "default": "./lib/browser-module.js"
        }
    },
    "module-a": {
        "import": "./lib/module-a.mjs" 
        "require": "./lib/module-a.js"
    }
  }
}

Bằng cách cung cấp các thông tin sau về package my-library, chúng ta có thể sử dụng nó ở bất kỳ đâu được hỗ trợ như sau:

// For CommonJS 
const moduleA = require('my-library/module-a')

// For ES6 Module
import moduleA from 'my-library/module-a'

// Dưới dây sẽ không thể hoạt động.

const moduleA = require('my-library/lib/module-a')

import moduleA from 'my-awesome-lib/lib/public-module-a'

const moduleB = require('my-library/private/module-b')

import moduleB from 'my-library/private/module-b'

Bởi các đường dẫn trong exports chúng ta có thể import và (require()) các mô-đun công khai của mình mà không cần chỉ định đường dẫn tuyệt đối.

Bằng cách bao gồm các đường dẫn cho .js .mjs, chúng ta có thể giải quyết vấn đề không tương thích, chúng ta có thể ánh xạ các package modules cho các môi trường khác nhau như trình duyệt và NodeJS trong khi hạn chế quyền truy cập vào các mô-đun private.

Các phiên bản NodeJS mới hơn hỗ trợ đầy đủ các mô-đun ES

Trong hầu hết các phiên bản NodeJS thấp hơn, mô-đun ES được đánh dấu là thử nghiệm. Điều này có nghĩa là mô-đun thiếu một số tính năng và nằm sau cờ –experimental-modules. Các phiên bản mới hơn của NodeJS có hỗ trợ ổn định cho các mô-đun ES.

Tuy nhiên, điều quan trọng cần nhớ là để NodeJS coi một mô-đun là mô-đun ES, một trong những điều sau đây phải xảy ra:

  • Phần mở rông tệp của mô-đun phải chuyển đổi từ .js (đối với CommonJS) thành .mjs (đối với mô-đun ES)
  • Hoặc chúng ta phải đặt trường {“type”:”module”} trong tệp package.json gần nhất.

Trong trường hợp này, tất cả mã trong gói đó sẽ được coi là mô-đun ES và nên sử dụng câu lệnh import/export thay vì require().

CommonJS cung cấp tính linh hoạt với module imports

Trong mô-đun ES, câu lệnh import chỉ có thể được gọi ở đầu tệp. Gọi nó ở bất kỳ nơi nào khác sẽ tự động chuyển biểu thức sang đầu tệp hoặc thậm chí có thể gây ra lỗi.

Mặt khác, với hàm require(), sẽ được phân tích cú pháp khi chạy. Kết quả là, require() có thể được gọi ở bất kỳ đâu trong mã. Bạn có thể sử dụng nó để tải các mô-đun theo điều kiện hoặc động từ câu lệnh if, vòng lặp điều kiện và hàm.

Ví dụ: bạn có thể gọi hàm require() bên trong một câu lệnh có điều kiện như sau:

if(user.length > 0){

   const userDetails = require(‘./userDetails.js’);

  // Do something ..
}

Ở đây, chúng ta tải một mô-đun là userDetails chỉ khi có người dùng.

CommonJS tải các mô-đun một cách đồng bộ, các mô-đun ES không đồng bộ.

Một trong những hạn chế của việc sử dụng require() là nó tải các mô-đun một cách đồng bộ. Điều này có nghĩa là các mô-đun được tải và xử lý từng cái một.

Như bạn có thể đoán, điều này có thể gây ra một số vấn đề về hiệu suất cho các ứng dụng quy mô lớn có hàng trăm mô-đun. Trong trường hợp như vậy, import có thể hoạt động tốt hơn require() dựa trên sự không đồng của nó.

Tuy nhiên, bản chất đồng bộ của require() có thể không phải là vấn đề lớn đối với ứng dụng quy mô nhỏ sử dụng một vài mô-đun.

Kết luận: CommonJS hay ES modules?

Đối với các nhà phát triển vẫn sử dụng phiên bản NodeJS cũ hơn, việc sử dụng mô-đun ES mới sẽ không thực tế.

Bởi vì hỗ trợ sơ sài, việc chuyển đổi một dự án hiện có sang các mô-đun ES sẽ khiến ứng dụng không tương thích với các phiên bản NodeJS trước đó chỉ hỗ trợ các mô-đun CommonJS (nghĩa là cú pháp require()).

Do đó, việc di chuyển dự án của bạn sang sử dụng các mô-đun ES có thể không mang lại nhiều lợi ích. Là người mới bắt đâu, việc tìm hiểu về các mô-đun ES có thể hữu ích và thuận tiện vì chúng đang trở thành định dạng chuẩn để xác định các mô-đun trong JavaScript cho cả phía Client (Browser) và phía Server (NodeJS).

Đối với các dự án NodeJS mới, các mô-đun ES cung cấp giải pháp thay thế cho CommonJS. Định dạng mô-đun ES cung cấp một lộ trình dễ dàng hơn để viết JavaScript, có thể chạy trong trình duyệt hoặc trên máy chủ.

Nói chung, các mô-đun EMCAScript là tương lai của JavaScript.