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 %}