ES Modules(ESM)快速入門指南
===
###### tags: `JavaScript`
###### tags: `JavaScript`, `frontend`, `backend`, `ESM`, `ES Modules`
<br>
[TOC]
<br>
> by gpt-5 (2025/10/07)
## 0. 先懂三件事
* 模組用 `export` 與 `import` 溝通(瀏覽器、Node 都支援)。
* **瀏覽器開發時要用 HTTP/HTTPS 伺服器**(`file://` 直接開會因為安全性導致 `import` 失敗)。
* Node 要嘛:`"type": "module"`(整個專案用 ESM),要嘛檔名 `.mjs`。
---
## 1) 最小可跑範例(瀏覽器)
- ### 結構
```
/esm-hello
├─ index.html
└─ utils.js
```
- ### utils.js
```js=
// 匯出(named + default)
export function add(a, b) { return a + b; }
const VERSION = '1.0.0';
export default VERSION;
```
- ### index.html
```html=
<!doctype html>
<meta charset="utf-8" />
<h1>ESM Hello</h1>
<div id="app"></div>
<script type="module">
// 相對路徑、要副檔名
import VERSION, { add } from './utils.js';
document.getElementById('app').textContent =
`version=${VERSION}, add(2,3)=${add(2,3)}`;
</script>
```
- ### 啟動(務必用伺服器)
```bash
cd esm-hello
python3 -m http.server 5500
# 瀏覽 http://localhost:5500
```
- ### 錯誤訊息(console log)
`Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "text/plain". Strict MIME type checking is enforced for module scripts per HTML spec`
- **原因**
- 這個錯誤是因為伺服器把 `*.js`(或 `*.mjs`)回應成 **`text/plain`**,但 ES Modules 需要「JavaScript MIME type」(如 `text/javascript` 或 `application/javascript`)。修法看你用哪種伺服器:
- Python on Windows(`http.server` 常把 `.js` 當 `text/plain`)(Python on Ubuntu 則無此問題)
- **解法:`serve_js.py`**
```python=
# save as serve_js.py
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
import mimetypes
mimetypes.add_type('application/javascript', '.js')
mimetypes.add_type('application/javascript', '.mjs')
mimetypes.add_type('text/css', '.css')
class Handler(SimpleHTTPRequestHandler):
extensions_map = {
**SimpleHTTPRequestHandler.extensions_map,
'.js': 'application/javascript',
'.mjs': 'application/javascript',
'.json':'application/json',
}
ThreadingHTTPServer(
('127.0.0.1', 5500),
Handler
).serve_forever()
```
**執行**:
```bash
python serve_js.py
# 瀏覽 http://127.0.0.1:5500/index.htm
```
<br>
---
## 2) 最小可跑範例(Node.js)
- ### 方式 A:`package.json` 指定 ESM
```json
{
"name": "esm-node-hello",
"type": "module"
}
```
**main.js**
```js
import { readFile } from 'node:fs/promises'; // Node 內建模組也支援 import
const txt = await readFile(new URL('./note.txt', import.meta.url), 'utf8');
console.log('content:', txt);
```
執行:
```bash
node main.js
```
- ### 方式 B:用 `.mjs`
沒有設定 `"type": "module"` 時,改檔名 `main.mjs` 也可直接 `node main.mjs`。
---
## 3) 常用語法小抄
```js
// 匯出
export const PI = 3.14;
export function fn() {}
export default class Foo {}
export { fn as doWork }; // 更名匯出
export * from './more.js'; // 轉 re-export
export { x, y } from './more.js'; // 選擇性轉出
// 匯入
import v, { fn as work, PI } from './mod.js'; // default + named + 更名
import * as Utils from './utils.js'; // 命名空間匯入
const { sub } = await import('./math.js'); // 動態載入(可條件載入)
// 模組層可用 top-level await(瀏覽器/Node 皆可)
```
---
## 4) 路徑與套件匯入
* **相對匯入**:`'./x.js'`、`'../x.js'` —— 記得**副檔名**。
* **裸字串(bare specifier)**:`import _ from 'lodash'`
* 瀏覽器要靠 **Import Maps** 或 bundler;
* Node 會從 `node_modules` 解析。
* **Import Maps(瀏覽器)簡例**
```html
<script type="importmap">
{
"imports": {
"lodash": "https://esm.sh/lodash-es"
}
}
</script>
<script type="module">
import { chunk } from 'lodash';
console.log(chunk([1,2,3,4], 2));
</script>
```
---
## 5) 與 CommonJS 互通(Node)
* **在 ESM 中使用 CJS**:
```js
// 假設 old-lib 是 CJS(module.exports = ...)
import pkg from 'old-lib';
```
* **在 CJS 中使用 ESM**(動態匯入):
```js
// CJS 檔案(require 語法)
(async () => {
const { add } = await import('./math.mjs');
console.log(add(1,2));
})();
```
* **在 ESM 中需要 require**:
```js
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const cjsPkg = require('old-lib');
```
---
## 6) 開發體驗:Vite 30 秒開工
```bash
npm create vite@latest my-esm-app -- --template vanilla
cd my-esm-app
npm i
npm run dev
# Vite 會自帶本機伺服器與 ESM 解析、HMR
```
> 你也可以用 Rollup/Webpack/esbuild,但 Vite 對原生 ESM 體驗最簡。
---
## 7) 常見錯誤與排查
* **直接用 `file://` 打開** → 改用本機伺服器(`python -m http.server`)。
* **忘了副檔名** → 瀏覽器 `import './utils'` 會 404;加上 `.js`。
* **跨網域/CORS** → 模組是跨來源嚴格模式,請確保正確的 Origin 與 MIME(`text/javascript`)。
* **混用 CJS/ESM** → 在 Node 釐清 `type: module`、`.mjs`、`.cjs`。
* **找不到裸字串套件**(瀏覽器) → 用 Import Maps 或 bundler;或改成 CDN 的 ESM URL(如 esm.sh)。
---
## 8) 建議的專案結構(小型前端)
```
/src
main.js # 入口 (module)
lib/
math.js
dom.js
index.html # <script type="module" src="/src/main.js">
package.json # 可選(build 工具)
```
**index.html**
```html
<script type="module" src="/src/main.js"></script>
```
**src/main.js**
```js
import { mount } from './lib/dom.js';
mount('#app', 'Hello ESM!');
```
---
## 9) 什麼時候仍用 `<script src>`?
* Demo/教學、**離線 file://** 雙擊打開、要「一行就能嵌入」給他人的元件。
這時請提供 IIFE/UMD 版本(我剛為你的 Tree Viewer 也做了 IIFE 版)。
---
## 10) 套用在 Tree Viewer 的對照
* **ESM 用法**(推薦):
```html
<script type="module">
import { TreeViewer } from './tree-viewer-lib.js';
TreeViewer.injectStyles();
const tree = new TreeViewer(document.getElementById('tree'));
tree.setData(myData);
</script>
```
* **IIFE 用法**(file:// 預覽或一行導入):
```html
<script src="./tree-viewer-lib.iife.js"></script>
<script>
TreeViewer.injectStyles();
const tree = new TreeViewer(document.getElementById('tree'));
tree.setData(myData);
</script>
```
<br>
{%hackmd vaaMgNRPS4KGJDSFG0ZE0w %}