JavaScript không có khái niệm "gói" cấp ngôn ngữ

Khác với các ngôn ngữ lập trình hiện đại như Python, Java hay Go, JavaScript không định nghĩa khái niệm "gói" (package) ở cấp độ ngôn ngữ. Thay vào đó, hệ thống quản lý gói trong JavaScript hoàn toàn dựa vào công cụ bên ngoài — chủ yếu là npm và cách Node.js xử lý module. Điều này có nghĩa: bạn không thể nói "JavaScript có hệ thống gói", mà phải hiểu rằng "npm và Node.js cùng tạo ra mô hình gói cho JavaScript".

Module là đơn vị cơ bản của JavaScript

Mỗi tệp .js là một module riêng biệt. Từ ES6, JavaScript hỗ trợ cú pháp import/export để chia sẻ mã giữa các module. Đây là nền tảng để xây dựng thư viện và ứng dụng lớn.

Không giống Go, nơi một package có thể gồm nhiều tệp .go trong cùng thư mục, mỗi file JS là một module độc lập — việc tổ chức logic thành nhóm phụ thuộc vào cách bạn cấu trúc importexport.

Gói (package) là khái niệm từ npm

Một "gói" trong JavaScript thực chất là một thư mục chứa tệp package.json. Tệp này mô tả tên, phiên bản, điểm nhập (entry point), kiểu module và các thông tin khác. Ví dụ:

{
  "name": "my-library",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "exports": "./index.js"
}

Trường "type": "module" chỉ định tất cả file .js trong gói này sẽ được xử lý như ES Module thay vì CommonJS (mặc định trước đây).

Tạo và sử dụng một gói cục bộ

Tạo gói greeting:

mkdir greeting && cd greeting
npm init -y

Sửa package.json để bật chế độ module và thêm hàm chào:

// greeting/index.js
export function greet(name) {
  return `Xin chào, ${name}!`;
}

Tạo dự án sử dụng gói này:

cd ..
mkdir hello && cd hello
npm init -y

Cài đặt gói cục bộ:

npm install ../greeting

Kết quả: npm tạo symbolic link trong node_modules/greeting → ../greeting, giúp bạn thử nghiệm gói trước khi công bố lên kho npm chính thức.

Giới hạn API công khai bằng trường exports

Khi phát triển thư viện, bạn không muốn người dùng truy cập trực tiếp vào các file nội bộ — vì chúng có thể thay đổi bất cứ lúc nào. Trường "exports" trong package.json giúp kiểm soát điều đó.

Ví dụ: di chuyển hàm greet vào file riêng:

// greeting/hello-util.js
export function greet(name) {
  return `Xin chào, ${name}!`;
}
// greeting/index.js
export { greet } from './hello-util.js';

Cập nhật package.json:

{
  "name": "greeting",
  "type": "module",
  "exports": "./index.js"
}

Bây giờ, nếu ai đó cố import từ greeting/hello-util.js, họ sẽ nhận lỗi:

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: 
Package subpath './hello-util.js' is not defined by "exports"

Điều này đảm bảo tính ổn định cho API công khai — bạn có thể đổi tên, di chuyển hoặc xóa file nội bộ mà không làm hỏng code của người dùng.

So sánh với Rust

Giống như Rust, nơi Cargo quản lý crate (library hoặc binary), npm cũng coi mỗi package là một đơn vị phân phối. Một package có thể chứa:

  • Một thư viện duy nhất (qua "main" hoặc "exports")
  • Nhiều script thực thi (được khai báo trong "bin")

Cả hai hệ sinh thái đều tách biệt rõ ràng giữa ngôn ngữ (Rust/JavaScript) và công cụ xây dựng (Cargo/npm).

ES Module trong Node.js hoạt động thế nào?

Node.js xác định một file là ES Module nếu một trong các điều kiện sau đúng:

  1. Tệp có phần mở rộng .mjs
  2. Tệp có .js và nằm trong gói có "type": "module" trong package.json
  3. File sử dụng cú pháp import/export và Node.js tự động suy luận (kèm cảnh báo hiệu năng)

Do đó, luôn nên thiết lập rõ "type": "module" để tránh cảnh báo và hành vi không mong muốn.

Thẻ: JavaScript ES Module npm package.json exports

Đăng vào ngày 26 tháng 5 lúc 13:58