← Lập trình JavaScript cơ bản

Bài 11 · Vận dụng · 20 phút· Cập nhật 11/06/2026

Module (ESM) & npm

Biên soạn bởi Nguyễn Anh Tuấn

Module ESM (import/export) & npm: package.json, semver ^ và ~, ESM vs CommonJS, và bundler (Vite) gộp code - nền tảng cho dự án JavaScript thật.

Thời các thẻ <script> rời rạc, mọi biến đều nằm chung global - hai file vô tình đặt trùng tên là ghi đè nhau, sinh bug ngầm. Module sửa điều đó: mỗi file có scope RIÊNG, chỉ những gì bạn export mới lộ ra ngoài.

mỗi file là một module có scope riêng

// file: chao.js
const noiBo = "khong lo ra ngoai";        // rieng tu cua module nay

export function chao(ten) {                // chu dong chia se
  return "Chao " + ten;
}
  • Mỗi file = một module, có scope RIÊNG (không ô nhiễm global).
  • Chỉ thứ được export mới ra ngoài; còn lại là riêng tư.
  • Nền tảng để viết code lớn, tái dùng và dễ bảo trì.

Hai kiểu xuất: named (nhiều thứ theo tên) và default (một “cái chính” mỗi file). Khi nhập, named phải khớp tên (có thể đổi bằng as), còn default đặt tên tuỳ ý.

named + default

// math.js
export const PI = 3.14;                      // named
export function cong(a, b) { return a + b; } // named
export default function tru(a, b) { return a - b; }  // default (mot cai)

// main.js
import tru, { PI, cong } from "./math.js";    // default + named
import { cong as themVao } from "./math.js";  // doi ten bang "as"
import * as math from "./math.js";            // gom het thanh "math"
console.log(cong(2, 3), tru(5, 1), PI);       // 5 4 3.14

Kết quả khi chạy

5 4 3.14
  • named: export theo tên; import { } phải khớp tên (đổi bằng as).
  • default: một export chính mỗi file; import đặt tên tuỳ ý.
  • import * as ns gom cả module thành một namespace.

npm là kho gói lớn nhất cùng công cụ cài. npm install tải gói vào node_modules và ghi vào package.json. Phiên bản ghi theo semver với ký hiệu khoảng - thử xem ký hiệu nào nhận phiên bản nào:

cài & khai báo phụ thuộc

npm init -y            # tao package.json
npm install dayjs      # tai vao node_modules + ghi "dependencies"
Khoảng phiên bản:
^1.2.3 → chấp nhận 1.2.3 ≤ v < 2.0.0
1.2.2
1.2.3
1.2.9
1.3.0
1.5.0
2.0.0
^ cho cập nhật MINOR & PATCH (không đổi MAJOR); ~ chỉ cho PATCH; không ký hiệu = đúng một phiên bản.

package.json (trích) - dấu ^ khai báo khoảng phiên bản (JSON thật không chứa comment)

{
  "dependencies": { "dayjs": "^1.11.0" }
}

dùng gói - import từ TÊN gói, không phải đường dẫn file

import dayjs from "dayjs";
console.log(dayjs().format("YYYY-MM-DD"));
  • npm install: tải vào node_modules + ghi package.json (dependencies).
  • semver MAJOR.MINOR.PATCH; ^ cho minor+patch, ~ chỉ patch.
  • package-lock.json ghim phiên bản chính xác; ĐỪNG commit node_modules.

Cạm bẫy ^0.x

Khi MAJOR là 0 (thư viện coi như chưa ổn định), ^ khoá luôn cả MINOR: ^0.2.3 nghĩa là ≥ 0.2.3 và < 0.3.0 (không nhảy lên 0.3.0). Đừng giả định ^ luôn chỉ giữ nguyên MAJOR.

npm, yarn, pnpm, bun - chọn cái nào?

npm đi kèm Node nên là mặc định. Có vài lựa chọn thay thế, lệnh gần như y hệt (cài / thêm / chạy script): yarn, pnpm (nhanh & tiết kiệm ổ đĩa nhờ chia sẻ gói giữa các dự án), và bun (vừa là package manager, vừa là một runtime - xem Bài 1). Mỗi cái có lockfile riêng (package-lock.json / yarn.lock / pnpm-lock.yaml / bun.lock) - đừng trộn nhiều cái trong cùng một dự án. Khoá này dùng npm; còn chính trang web bạn đang đọc được dựng bằng pnpm.

Có hai hệ module trong thế giới JS. ESM (import/export) là chuẩn hiện đại; CommonJS (require/module.exports) là hệ cũ của Node mà bạn vẫn sẽ gặp.

hai cú pháp

// ESM (chuan - trinh duyet & Node moi)
import fs from "node:fs";
export const x = 1;

// CommonJS (he cu cua Node)
const fs = require("fs");
module.exports = { x: 1 };
  • ESM: chuẩn ngôn ngữ, nạp TĨNH (phân tích khi build → tree-shaking tốt).
  • CommonJS: hệ cũ của Node, nạp ĐỘNG lúc chạy bằng require.
  • Node bật ESM bằng "type": "module" (hoặc đuôi .mjs); code mới ưu tiên ESM.

Trung thực: vẫn còn lẫn lộn

Rất nhiều thư viện vẫn xuất CommonJS; công cụ build và Node lo phần “bắc cầu” giữa hai hệ, nhưng đôi khi vẫn gặp lỗi tương thích khó hiểu. Biết cả hai cú pháp giúp bạn gỡ rối khi import một gói không như mong đợi.

Trình duyệt không hiểu bare import kiểu import x from "dayjs", và nạp hàng trăm file lẻ thì chậm. Bundler (Vite, esbuild, webpack) gộp & tối ưu trước khi đưa lên web.

vòng đời với Vite

npm create vite@latest my-app   # tao du an
npm install                     # cai phu thuoc
npm run dev                     # dev server: nap nhanh + hot reload
npm run build                   # gop & toi uu ra thu muc dist/
  • Bundler giải import, transpile cú pháp mới, tree-shaking (bỏ code thừa), nén.
  • Dev server (Vite) nạp nhanh và hot-reload khi bạn sửa code.
  • npm run build sinh thư mục dist/ tối ưu cho production.

Tiếp theo

Đủ mảnh ghép rồi! Bài cuối - “Dự án: app web nhỏ” - ráp tất cả (DOM, sự kiện, module, async, mảng/object) thành một ứng dụng web tương tác chạy được.

Câu hỏi thường gặp

Mỗi file JS là một module có scope RIÊNG: biến/hàm không tự lọt ra ngoài (tránh “ô nhiễm” global như kiểu nhiều thẻ script cũ). Bạn chủ động chia sẻ bằng export và dùng lại bằng import → code gọn, tái dùng được, dễ bảo trì.

named: xuất NHIỀU thứ theo TÊN (export function a() {}; rồi import { a }). default: MỘT “cái chính” cho mỗi file (export default …; rồi import tên_tuỳ_ý). Có thể trộn cả hai, nhưng mỗi file chỉ một default. import { } phải khớp đúng tên đã export.

npm là kho gói (package) lớn nhất cùng công cụ cài. npm install <gói> tải gói vào thư mục node_modules và ghi vào package.json. Sau đó bạn import từ tên gói. Đừng commit node_modules - nó tái tạo được từ package.json + lockfile.

Đều là package manager - cài & quản lý gói. npm đi kèm Node nên là mặc định. yarn và pnpm là lựa chọn thay thế phổ biến; pnpm tiết kiệm ổ đĩa nhờ chia sẻ gói giữa các dự án. bun vừa là package manager vừa là một runtime (xem Bài 1). Lệnh gần như giống nhau (install/add/run), nhưng mỗi cái có lockfile riêng - đừng trộn nhiều cái trong cùng một dự án.

Đó là semver (MAJOR.MINOR.PATCH). ^1.2.3 cho phép cập nhật KHÔNG đổi MAJOR (>= 1.2.3< 2.0.0). ~1.2.3 chặt hơn, chỉ PATCH (>= 1.2.3< 1.3.0). package-lock.json ghim phiên bản CHÍNH XÁC để máy khác cài lại y hệt. (Lưu ý: ^0.x đặc biệt - xem Bước 3.)

ESM (import/export) là chuẩn của ngôn ngữ, chạy trên trình duyệt & Node hiện đại, nạp TĨNH (phân tích được khi build → tree-shaking tốt). CommonJS (require/module.exports) là hệ cũ của Node, nạp ĐỘNG lúc chạy. Code mới ưu tiên ESM; vẫn gặp CommonJS ở thư viện/đoạn cũ.

Trình duyệt (nhất là bản cũ) không hiểu “bare import” từ npm, và nạp hàng trăm file lẻ thì chậm. Bundler (Vite, esbuild, webpack) gộp & tối ưu: giải import, transpile cú pháp mới, loại code thừa (tree-shaking), nén. Dev server (Vite) còn nạp nhanh và hot-reload khi sửa code.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Lợi ích chính của việc chia code thành module là gì?

  1. 1

    Tách module

    Viết toan.js export hàm cong(a, b); import vào main.js và dùng.

    Hoàn thành khi: import { cong } chạy đúng; biến nội bộ của toan.js không lọt ra global.

  2. 2

    default export

    Tạo file export default một hàm; import với một tên do bạn tự đặt.

    Hoàn thành khi: import tên bất kỳ vẫn lấy đúng giá trị default.

  3. 3

    npm install

    npm init -y, rồi npm install một gói (vd dayjs), import và dùng thử.

    Hoàn thành khi: node_modulespackage.json xuất hiện gói; import chạy được.

  4. 4

    Đọc semver

    Dùng công cụ ở Bước 3: với ^1.2.3 những phiên bản nào thoả? Với ~1.2.3 thì sao?

    Hoàn thành khi: ^ nhận 1.2.3…1.5.0 (không 2.0.0); ~ nhận ≥1.2.3 và <1.3.0 - tức 1.2.x với x ≥ 3 (1.2.2 bị loại).

  5. 5

    ESM vs CommonJS

    Trong một dự án, tìm file dùng import (ESM) và file dùng require (CommonJS).

    Hoàn thành khi: Nhận ra hai cú pháp; biết "type": "module" bật ESM cho Node.

  6. 6

    Build thử

    Chạy một lệnh build (vd vite build) và mở thư mục dist xem kết quả.

    Hoàn thành khi: Thấy bundler gộp nhiều file nguồn thành ít file đã tối ưu/nén.