Menu

ES module trong JavaScript: import, export và tải động

ES module trong JavaScript hoạt động thế nào: export có tên và export mặc định, cú pháp import, import() động và các quy tắc phân biệt module với script thường.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Module là một tệp có phạm vi riêng

Trước khi có ES module, mỗi thẻ <script> đổ các biến của nó vào không gian tên toàn cục, và thứ tự tải quyết định cái gì nhìn thấy cái gì. ES module khắc phục điều đó bằng cách cho mỗi tệp một phạm vi riêng. Không có gì lọt ra ngoài trừ khi bạn export nó một cách rõ ràng. Không có gì đi vào trừ khi bạn import nó một cách rõ ràng.

Hai tệp, một tệp export, một tệp import:

add và multiply nằm trong math.js. Chúng chỉ hiện ra trong main.js nhờ có import. Mọi thứ khác trong math.js (hàm hỗ trợ, hằng số, bất cứ thứ gì) đều không truy cập được từ bên ngoài.

Từ đây có hai quy tắc nên nắm sớm:

  • Module tự động chạy ở strict mode. Không cần 'use strict'.
  • this ở cấp cao nhất là undefined, không phải đối tượng toàn cục.

Export có tên: export ngay khi khai báo

Đây là dạng phổ biến nhất. Đặt export trước bất kỳ function, class, const hay let nào và nó sẽ trở thành một phần giao diện công khai của module:

Các tên trong dấu ngoặc nhọn phải khớp chính xác với tên đã export: import { circlearea } sẽ thất bại. Nếu một tên trùng với thứ bạn đã có, hãy đổi tên khi import bằng as:

Bạn cũng có thể liệt kê các export ở cuối tệp thay vì viết ngay tại chỗ; một số người thích cách này vì có một phần "API công khai" rõ ràng:

Cả hai cách cho cùng một kết quả. Hãy chọn một và dùng nhất quán trong dự án.

Export mặc định: một cái cho mỗi module

Một module cũng có thể có một export mặc định duy nhất. Export mặc định dành cho các tệp thật sự chỉ có một thứ chính: một component, một lớp, một đối tượng cấu hình:

Ba điều cần để ý:

  • Không có dấu ngoặc nhọn quanh log ở phía import.
  • Tên được import là tùy bạn. import shout from './logger.js' cũng chạy y hệt.
  • Mỗi tệp chỉ có một export mặc định. Thêm cái thứ hai và tệp sẽ không phân tích được.

Export có tên và export mặc định có thể cùng tồn tại:

Export mặc định đứng trước, rồi đến các export có tên trong dấu ngoặc nhọn. Thứ tự này là cố định.

Nên dùng loại nào? Export có tên dễ refactor hơn: đổi tên trong cả codebase chỉ cần một lần tìm và thay thế, vì mọi chỗ import đều dùng cùng một tên. Export mặc định linh hoạt nhưng cho phép mỗi nơi gọi chọn một tên khác nhau, khiến việc tìm kiếm khó hơn. Hầu hết hướng dẫn phong cách hiện đại nghiêng về export có tên và chỉ dành export mặc định cho các module thật sự chỉ có một mục đích.

Import tất cả, export lại, hiệu ứng phụ

Vài dạng import khác bạn sẽ gặp.

Gom mọi export có tên vào một đối tượng namespace:

Export lại từ module khác mà không import vào phạm vi hiện tại:

Mẫu này là cách các tệp khởi đầu của thư viện gom các phần từ tệp nội bộ thành một giao diện công khai.

Và cuối cùng, một câu lệnh import không gán gì cả, dành cho các module chỉ có nhiệm vụ tạo hiệu ứng phụ (polyfill, CSS-in-JS, đăng ký trình xử lý):

Tệp chạy một lần; không có gì được import theo tên.

Import là tĩnh và sống

Hai đặc tính của import đôi khi khiến người ta bất ngờ.

Tĩnh. Các khai báo import được phân giải trước khi bất kỳ dòng code nào của bạn chạy. Bạn không thể đặt chúng trong if, trong hàm hay trong try. Đường dẫn phải là một chuỗi literal, không phải biến. Đó là điều cho phép các công cụ phân tích import mà không cần chạy code: bundler, trình kiểm tra kiểu, công cụ tree shaking đều dựa vào nó.

// Not allowed - SyntaxError.
if (userWantsFancy) {
  import { fancy } from './fancy.js';
}

Nếu cần tải theo điều kiện, hãy dùng import() (sẽ nói ngay sau đây).

Sống. Một binding được import là tham chiếu chỉ đọc trỏ về export, không phải một bản chụp. Nếu module export gán lại giá trị, nơi import sẽ thấy giá trị mới:

Bạn cũng không thể gán lại một import ở phía sử dụng: count = 5 trong main.js sẽ ném lỗi. Import là các góc nhìn chỉ đọc.

import() động để tải khi cần

Khi bạn cần quyết định lúc chạy có tải một module hay không (tính năng nặng, code splitting theo route, polyfill theo điều kiện), hãy dùng import() như một hàm. Nó trả về một promise resolve thành các export của module:

Vì đây là một lời gọi hàm bình thường, bạn có thể:

  • Await nó bên trong một hàm async.
  • Truyền một biến làm đường dẫn.
  • Dùng nó bên trong if hoặc try/catch.

Destructuring đối tượng nhận được cũng giống như với import tĩnh:

default là khóa của export mặc định khi bạn destructuring. Đổi tên nó thành gì tùy bạn.

Các trường hợp dùng thực tế là code splitting (chỉ gửi thư viện biểu đồ khi người dùng nhấp "hiện biểu đồ"), polyfill theo kết quả kiểm tra tính năng, và các plugin được phát hiện lúc chạy.

Chạy ES module: trình duyệt và Node

Cú pháp ở đâu cũng giống nhau; thứ thay đổi là cách môi trường chạy tìm và tải tệp.

Trên trình duyệt, đánh dấu script khởi đầu là module:

<script type="module" src="./main.js"></script>

Với type="module", trình duyệt hiểu import/export, chạy code ở strict mode và hoãn việc thực thi cho đến khi HTML được phân tích xong. Đường dẫn phải là tương đối (./, ../) hoặc URL tuyệt đối: các tên trần như import 'lodash' không chạy được nếu không có import map hoặc bundler.

Trong Node, có hai cách để bật:

  • Đặt tên tệp là .mjs, hoặc
  • Đặt "type": "module" trong package.json gần nhất, khi đó mọi tệp .js đều là module.

Node cũng yêu cầu đường dẫn đầy đủ kèm đuôi tệp: import './utils.js', không phải import './utils'.

// package.json
{
  "type": "module",
  "main": "./index.js"
}

Cả hai môi trường đều yêu cầu ghi rõ đuôi tệp trong ESM gốc. Các bundler (Vite, webpack, esbuild) sẽ tự phân giải đường dẫn không có đuôi trong lúc phát triển: tiện lợi, nhưng dựa vào đó nghĩa là mã nguồn của bạn sẽ không chạy được nếu thiếu bước build.

Những lỗi thường gặp

Vài thứ hay khiến người ta vấp:

  • Quên type="module" trên trình duyệt. Thiếu nó, <script> chạy như script thường và import là lỗi cú pháp.
  • Thiếu đuôi tệp trong Node. import './utils' thất bại; import './utils.js' chạy được. Bundler che giấu điều này, môi trường chạy gốc thì không.
  • Mong có __dirname hoặc require trong ES module. Những thứ đó chỉ có trong CommonJS. Trong ESM, hãy dùng import.meta.url và chuyển đổi nó khi cần đường dẫn.
  • Import vòng tròn chạm vào giá trị trước khi chúng sẵn sàng. Hai module import lẫn nhau là hợp lệ, nhưng đọc một export chưa được gán sẽ cho bạn undefined. Hãy tổ chức code để vòng tròn không bị chạm tới trong lúc khởi tạo, hoặc tách nó ra.
  • Cố import theo điều kiện. Câu lệnh import tĩnh không cho phép điều đó. Hãy dùng import() động cho mọi thứ phụ thuộc vào lúc chạy.

Tiếp theo: CommonJS và ESM

ES module là chuẩn, nhưng rất nhiều code Node ngoài thực tế vẫn dùng CommonJS: require, module.exports và một bộ quy tắc khác về thời điểm code chạy. Hiểu cả hai và cách chúng phối hợp với nhau là nội dung của trang tiếp theo.

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

Làm sao dùng ES module trong JavaScript?

Export giá trị từ một tệp bằng export hoặc export default, rồi đưa chúng vào tệp khác bằng import. Trên trình duyệt, tải tệp khởi đầu bằng <script type="module" src="main.js"></script>. Trong Node, dùng đuôi .mjs hoặc đặt "type": "module" trong package.json.

Export mặc định và export có tên khác nhau thế nào?

Một module có thể có nhiều export có tên (export function foo() {}) nhưng chỉ một export mặc định (export default ...). Export có tên phải được import đúng tên đó trong dấu ngoặc nhọn: import { foo } from './x.js'. Export mặc định có thể được import với bất kỳ tên nào: import whatever from './x.js'.

import() động trong JavaScript là gì?

import() khi được gọi như một hàm sẽ trả về một promise resolve thành các export của module. Khác với câu lệnh import tĩnh, nó chạy lúc được gọi, nên bạn có thể tải code theo điều kiện hoặc khi cần. Đây là cách triển khai code splitting và lazy loading.

Có cần ghi đuôi tệp trong đường dẫn import không?

Với ES module gốc (trình duyệt và trình tải ESM của Node) thì có. Bạn phải viết ./utils.js, không phải ./utils. Các bundler như Vite và webpack dễ dãi hơn và sẽ tự phân giải đường dẫn không có đuôi, nhưng dựa vào điều đó khiến code của bạn khó mang sang môi trường khác.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu