Learning Objectives
- 說明模組化的好處,用 ES 模組的 export(具名、預設)與 import 拆分程式
- 用 npm init 建立專案,看懂 package.json、node_modules 與 dependencies/devDependencies
- 說明 Babel 的用途,並完成安裝、設定與轉譯
- 說明打包工具 webpack 的 entry、output、loader,以及現在常見的 Vite 怎麼建立專案
為什麼要把程式拆成模組
程式寫到幾千行,全部放在一個檔案裡很難維護;拆成好幾個 <script> 檔案,所有最外層的變數又都在同一個全域作用域,
兩個檔案都宣告了 add 就互相覆蓋。載入順序錯了(先用到才載入)也會出錯。
模組化是把一個獨立的功能放在一個檔案裡,每個檔案就是一個模組(module):
- 模組裡宣告的變數、函式只在這個模組裡看得到,不會汙染全域作用域
- 要給別人用的東西明確地
export(匯出),要用別人的東西明確地import(匯入),依賴關係一目了然 - 程式碼容易重複使用,也容易分工
ES2015 為 JavaScript 加入了官方的模組語法,稱為 ES 模組(ES Modules,ESM)。這一課先學語法,再介紹寫模組化專案時常用的工具:npm、Babel、webpack 與 Vite。
本課的逐步執行動畫不支援
import/export(它跑的是一般 script),所以模組的程式碼用文字說明,工具的操作用終端機導覽。
export:具名匯出與預設匯出
具名匯出(named export):在宣告前面加 export,一個模組可以有很多個。變數、常數、函式、類別都可以匯出。
// math.js
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
function secret() { … } // 沒有 export:只有 math.js 自己能用
也可以寫在檔案最後一次匯出:export { add, PI };。
預設匯出(default export):加上 default,一個模組只能有一個,通常用在「這個檔案主要就是提供這一個東西」時。
// print.js
export default function print(msg) {
console.log('[訊息] ' + msg);
}
import:匯入模組
// index.js
import print from './print.js'; // 預設匯出:名稱自己取,不用大括號
import { add, PI } from './math.js'; // 具名匯出:放在大括號裡,名稱要和匯出時相同
import { add as plus } from './math.js'; // 用 as 重新命名,避免撞名
import * as math from './math.js'; // 把所有具名匯出收進一個物件:math.add、math.PI
import show, { add as sum } from './print-and-math.js'; // 同時匯入時,預設匯出寫在前面
print(add(1, 2)); // [訊息] 3
在瀏覽器裡使用模組:
<script type="module" src="index.js"></script>
ES 模組的規則:
| 規則 | 說明 |
|---|---|
| 自動是嚴格模式 | 不寫 'use strict' 也一樣,例如不能使用未宣告的變數 |
| 有自己的作用域 | 模組最外層的 let、const、函式不會變成全域變數,也不會掛到 window 上 |
| 只執行一次 | 同一個模組被好幾個檔案 import,也只會載入、執行一次,大家拿到的是同一份 |
| 路徑要寫清楚 | 瀏覽器裡要寫 ./math.js(相對路徑與副檔名);打包工具通常允許省略副檔名 |
| 可以用 top-level await | 模組的最外層可以直接寫 await(一般 script 不行) |
| 要用伺服器開啟 | 用 file:// 開啟會被 CORS 規則擋下,要用第 23 課的本機伺服器 |
想一想:ES 模組
math.js 裡寫了 export function add(a, b) { … } 與 export default function print(msg) { … }。在 index.js 裡同時匯入兩者,下列哪一個寫法正確?
一個模組可以有幾個預設匯出(export default)?
util.js 是用 <script type="module"> 載入的模組,它最外層寫了 let count = 0;。另一個一般的 <script> 能用 count 或 window.count 讀到它嗎?
直接在檔案總管點開一個使用 <script type="module"> 的 HTML(file:// 開頭),模組沒有執行。最可能的原因是什麼?
npm 專案:package.json 與 node_modules
第 23 課用過 npm 執行 http-server。寫一個專案時,npm 還負責記錄「這個專案用了哪些套件」:
| 檔案或資料夾 | 是什麼 |
|---|---|
package.json |
專案的設定檔:名稱、版本、可執行的指令(scripts)、用到的套件 |
node_modules/ |
npm install 下載的套件實際放在這裡,通常很大,不放進 Git(寫在 .gitignore) |
package-lock.json |
記錄每個套件安裝的確切版本,讓每個人裝出來的都一樣,要放進 Git |
package.json 裡有兩種套件清單:
| 清單 | 安裝指令 | 用途 |
|---|---|---|
dependencies |
npm install 套件 |
程式執行時需要的套件 |
devDependencies |
npm install --save-dev 套件(或 -D) |
只有開發時需要的工具,例如 Babel、webpack、Vite |
拿到別人的專案(沒有 node_modules)時,在專案資料夾執行一次 npm install,npm 就會依照 package.json 與 package-lock.json 把套件全部裝回來。
投影片提到的「全域安裝」(-g)是把套件裝在系統上;專案用的工具建議裝在專案裡(局部安裝),每個專案各自管理版本。
用 npm init 建立專案
先確認已經安裝 Node.js(第 23 課),打開終端機跟著做。版本不同時,輸出的欄位與數字會略有差異。
-
1
建立資料夾並初始化
npm init會一題一題問專案名稱、版本、作者等資訊;加上-y(--yes)就全部套用預設值,直接產生package.json。較新版本的 npm 預設不填 license,並加上"type": "commonjs"。 -
2
設定 npm init 的預設值(可選)
每次都要填的作者資訊,可以設成預設值,之後
npm init -y就會自動帶入。npm config ls -l列出所有設定。 -
3
在 scripts 加上自訂指令
用 VS Code 打開
package.json,在scripts裡加上指令,之後用npm run 名稱執行。npm start與npm test是少數不用寫run的特例。後面 Babel、webpack 都會用到這個方法。"scripts": { "hello": "echo 你好" }
Babel:把新語法轉成舊瀏覽器看得懂的程式
JavaScript 的標準叫 ECMAScript,2015 年的大改版(ES2015,也叫 ES6)加入了 let、const、箭頭函式、class、模組、Promise 等,
之後每年都有新版(ES2016、ES2017……)。
Babel 是 JavaScript 的轉譯器(transpiler):把新版的語法改寫成效果相同的舊語法,讓不支援新語法的瀏覽器也能執行。例如:
// 轉譯前
const fn = () => 1;
// 轉譯後(Babel 官方文件的範例)
var fn = function fn() {
return 1;
};
投影片製作時,很多使用者還在用只支援 ES5 的舊瀏覽器(例如 IE),Babel 幾乎是必備工具。現在主流瀏覽器都已支援 ES2015 以後的大部分語法, Babel 主要用在:要支援較舊的瀏覽器、想用瀏覽器還沒全面支援的最新語法,或是轉譯 JSX(React 的語法)等非標準語法。
@babel/core:Babel 的核心@babel/cli:在終端機執行babel指令@babel/preset-env:依「目標瀏覽器」自動決定要轉譯哪些語法
投影片裡的 @babel/polyfill 從 Babel 7.4.0 起已經停止使用,官方建議改為直接引入 core-js/stable。
語法可以轉譯,但舊瀏覽器缺少的內建函式(例如 Array.prototype.includes)要靠這類 polyfill 補上。
安裝與設定 Babel
接著在剛剛建立的 demo 專案裡操作。
-
1
安裝 Babel(開發時才需要,用 --save-dev)
三個套件會出現在
package.json的devDependencies,實際檔案放在node_modules裡。輸出的套件數量依版本而不同。 -
2
建立設定檔 babel.config.json
在專案根目錄新增
babel.config.json,告訴 Babel 使用 preset-env,並設定要支援的瀏覽器版本:{ "presets": [ ["@babel/preset-env", { "targets": { "chrome": "67", "firefox": "60", "safari": "11.1" } }] ] }不寫
targets時,preset-env 會把 ES2015 以後的語法全部轉成 ES5。 -
3
寫程式並加上 build 指令
新增
src/app.js,寫一些新語法,例如const fn = () => 1;。再到package.json的scripts加上:"build": "babel src --out-dir dist"意思是把
src資料夾裡的程式轉譯後放到dist資料夾。 -
4
看轉譯的結果
打開
dist/app.js,箭頭函式與const都被改寫了(Windows 的命令提示字元用type dist\app.js)。檔案開頭可能還會多一行"use strict";。
打包工具:webpack
模組拆得越細,檔案就越多。早期瀏覽器不支援模組,或是一個網頁要下載上百個小檔案很慢,所以有了打包工具(bundler):
從一個入口檔案開始,順著 import 找出所有用到的模組,合併、壓縮成少數幾個檔案。
webpack 是最具代表性的打包工具,目前的主要版本是 webpack 5。它的設定檔 webpack.config.js 有幾個核心概念:
| 概念 | 意思 |
|---|---|
| entry(入口) | 從哪個檔案開始找依賴,預設 ./src/index.js |
| output(輸出) | 打包結果放在哪裡、叫什麼名字,預設 ./dist/main.js |
| loader(載入器) | webpack 本身只看得懂 JavaScript 與 JSON;其他類型的檔案(CSS、需要 Babel 轉譯的 JS)要靠 loader 轉換成模組 |
| mode(模式) | development 方便除錯,production 會壓縮檔案 |
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, use: 'babel-loader' }, // JS 先交給 Babel
{ test: /\.css$/, use: ['style-loader', 'css-loader'] }, // 可以 import './style.css'
{ test: /\.png$/, type: 'asset/resource' }, // 圖片:webpack 5 內建
],
},
};
每一條 rules 是「符合 test 的檔案,交給哪些 loader 處理」。use 是陣列時,從右到左執行:先 css-loader 讀進 CSS,再由 style-loader 放進頁面。
投影片的圖片用 file-loader,webpack 5 起改用內建的 asset modules(type: 'asset/resource'),不必另外安裝。
用 webpack 打包模組
開一個新的資料夾 webpack-demo,照投影片的「功能模組化範例」:src/math.js 匯出 add、src/print.js 預設匯出 print、src/index.js 匯入兩者並執行 print(add(1, 2))。
-
1
建立專案並安裝 webpack
webpack是核心,webpack-cli讓你在終端機執行 webpack 指令。webpack 官方建議在package.json加上"private": true並移除"main",避免不小心把專案發布到 npm。 -
2
建立設定檔與 start 指令
在專案根目錄新增上面那份
webpack.config.js(只用到 JavaScript 時,可以先拿掉module那一段)。在package.json的scripts加上"start": "webpack --config webpack.config.js"。沒有設定檔時,webpack 也會用預設值:入口src/index.js、輸出dist/main.js。 -
3
執行打包
npm start執行 webpack:它從src/index.js找到math.js與print.js,合併成dist/main.js。最後一行看到 compiled successfully 就是成功了。 -
4
在網頁裡使用打包結果
新增
dist/index.html,用一般的<script src="main.js"></script>載入打包好的檔案(不需要type="module",模組已經合併好了),用本機伺服器開啟,主控台會印出[訊息] 3。 -
5
加上 Babel loader
要讓 webpack 打包時順便用 Babel 轉譯,安裝
babel-loader,再把上面設定檔module.rules的第一條加進去(Babel 的設定沿用babel.config.json)。
現在常見的選擇:Vite
webpack 功能完整,但設定檔要自己寫,專案大了啟動也比較慢。現在新專案常用的是 Vite(法文的「快」,念作 /vit/):
- 開發時不先打包:直接利用瀏覽器原生的 ES 模組,存檔後畫面幾乎立刻更新
- 幾乎不用設定:內建開發伺服器、CSS 與圖片的處理,一行指令建立專案
- 正式發布時才打包:
npm run build產生壓縮好的檔案(Vite 8 使用 Rolldown 打包)
2026 年 10 月查詢時,Vite 的最新主要版本是 8,需要 Node.js 20.19 以上或 22.12 以上。
| webpack | Vite | |
|---|---|---|
| 開發時 | 先打包整個專案,再啟動 | 不打包,瀏覽器需要哪個模組才處理哪個 |
| 設定 | 通常要寫 webpack.config.js |
大多不用設定 |
| 常見於 | 已經存在多年的專案 | 新建立的專案,也有 React、Vue 等框架的範本 |
學 webpack 的觀念(入口、依賴、loader、打包)仍然有用:Vite 與其他工具做的是同一件事,只是做法更自動。
用 Vite 建立專案
在任意資料夾開啟終端機,用 Vite 的官方範本建立一個純 JavaScript(vanilla)專案。
-
1
建立專案
npm create vite@latest會下載最新版的建立工具;--template vanilla指定不使用框架的範本。注意--template前面要多一組--,讓參數傳給 Vite 而不是 npm。沒有指定範本時,它會用選單問你要用哪一種。 -
2
安裝套件並啟動開發伺服器
npm run dev啟動 Vite 的開發伺服器,預設網址是http://localhost:5173/。打開專案裡的main.js修改並存檔,瀏覽器會立刻更新。按 Ctrl+C 停止。 -
3
打包成正式版本
npm run build把專案打包到dist資料夾,裡面的檔案可以直接放到任何網頁伺服器上。範本裡的main.js已經使用import,你可以照本課的寫法新增自己的模組。
想一想:npm、Babel 與打包工具
從 GitHub 下載了一個前端專案,裡面有 package.json 但沒有 node_modules 資料夾。第一步要做什麼?
Babel 的主要用途是什麼?
webpack 的 loader 是做什麼的?
下列哪一個套件適合用 npm install --save-dev 安裝(放在 devDependencies)?
重點精華回顧
- 模組化:一個檔案一個模組,有自己的作用域,用
export匯出、import匯入,不汙染全域。 - 具名匯出可以有很多個,匯入時放大括號、名稱要相同(可用
as改名);預設匯出最多一個,匯入時自己取名、寫在前面。 - 瀏覽器用
<script type="module">載入模組,要透過伺服器開啟;模組自動是嚴格模式,只執行一次。 npm init -y建立package.json;npm install把套件裝進node_modules;開發工具用--save-dev放進 devDependencies;npm run 指令執行 scripts。- Babel 把新語法轉譯成等效的舊語法,搭配
@babel/preset-env與babel.config.json;@babel/polyfill已停止使用。 - webpack 從 entry 開始找出所有依賴,打包到 output;loader 處理 JavaScript 以外的檔案。
- Vite 開發時直接使用瀏覽器的 ES 模組、幾乎不用設定,
npm create vite@latest建立專案、npm run dev啟動。