# ES Module import/export 和 設計思維
> 前言:
> 最近在工作中覺得 import/export 的方式很多變與彈性,所以想好好的來理解討論一下:
現代前端開發日趨複雜,但無論技術如何演進,有一個核心概念始終不變:**最終所有的程式碼都會轉錄成瀏覽器能理解的 HTML、CSS、JavaScript**。
現代前端開發的構築趨勢可以從三個面向來理解:
1. **模組化開發**:將程式碼拆分成可重用的模組
2. **打包工具協助編譯**:使用 Webpack、Vite 等工具處理複雜的依賴關係
3. **最終輸出標準格式**:轉換成瀏覽器原生支援的格式
## HTML `<script>` 標籤:JavaScript 的載體
網頁中讓 JavaScript 執行的載體是透過 HTML `<script>` 標籤。這個標籤不只可以引用 JavaScript,也可以引用其他類型的腳本,但最常見的用途就是載入 JavaScript。
HTML 引入模組化 JavaScript 主要有兩種形式:
### 1. CommonJS 模組
```html
<script type='text/javascript' src='./bundle.js'></script>
```
### 2. ES Module 模組
```html
<script type='module' src='./main.js'></script>
```
在前端打包工具中,通常會這樣設置(以 ES Module 為例):
```javascript
const config = {
type: 'module'
}
```
---
## CommonJS:傳統的模組系統
CommonJS 是 ES6 之前主要的模組化解決方案,主要用於 Node.js 環境。
### 匯出語法
```javascript
// utils.js
module.exports.add = function(a, b) {
return a + b;
}
module.exports.subtract = function(a, b) {
return a - b;
}
```
### 引入語法
使用 `require()` 函數來引入模組:
```javascript
// app.js
const { add, subtract } = require('./utils')
console.log(add(5, 5)) // 10
console.log(subtract(10, 5)) // 5
```
## ES Modules:現代標準
ES Modules(ESM)是 ECMAScript 6 引入的官方模組系統,也是現代前端開發的主流選擇。
### 1. 命名匯出(Named Export)
**匯出方式**:
```javascript
// utils.js
export function addition(a, b) {
return a + b
}
export const subtraction = (a, b) => a - b
function multiplication(a, b) {
return a * b
}
const division = (a, b) => a / b
// 批量匯出
export { division, multiplication }
```
**引入方式**:
```javascript
// app.js
import {
addition,
subtraction,
multiplication as multiply, // 使用別名
division as div
} from './utils.js'
addition(5, 3) // 8
subtraction(5, 3) // 2
multiply(4, 2) // 8
div(6, 2) // 3
```
**批量引入**:
當需要引入大量模組時,可以使用 `*` 語法:
```javascript
import * as utils from "./utils.js"
utils.addition(5, 3) // 8
utils.subtraction(5, 3) // 2
utils.multiplication(4, 2) // 8
utils.division(6, 2) // 3
```
### 2. 預設匯出(Default Export)
每個檔案只能有一個預設匯出,引入時可以自由命名:
```javascript
// utils1.js
function isOdd(num) {
return num % 2 !== 0
}
export default isOdd
```
```javascript
// utils2.js
const isEven = (num) => num % 2 === 0
export default isEven
```
```javascript
// app.js
import isOdd from './utils1'
import shouldBeEven from './utils2' // 可以自定義名稱
console.log(isOdd(3)) // true
console.log(shouldBeEven(3)) // false
```
## 實戰案例:useReducer Counter
讓我們透過一個實際的例子來展示如何善用 `index.ts` 作為資料夾的統一入口點。
### 專案結構
```
.
├── Counter.tsx
└── stores
├── actions.ts
├── index.ts
├── reducer.ts
└── state.ts
```
### 1. 建立核心檔案
**state.ts**:
```typescript
export interface State {
count: number
}
export const initialState: State = {
count: 0
}
```
**actions.ts**:
```typescript=
type ActionType = 'decrement' | 'increment'
export interface Action {
type: ActionType
}
export const decrement = (): Action => ({ type: 'decrement' })
export const increment = (): Action => ({ type: 'increment' })
```
**reducer.ts**:
```typescript=
import { type Action } from './actions'
import { type State } from './state'
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'decrement':
return { ...state, count: state.count - 1 }
case 'increment':
return { ...state, count: state.count + 1 }
default:
return state
}
}
```
### 2. 建立統一入口點
**index.ts**:
```typescript=
export * from './actions'
export * from './reducer'
export * from './state'
```
### 3. 在 Recat 元件中使用
```typescript=
import { useReducer } from 'react'
// 透過單一入口點引入所有需要的內容
import { decrement, increment, initialState, reducer } from './stores'
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<div>
<h1>Counter</h1>
<p>Count: {state.count}</p>
<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>
</div>
)
}
export default Counter
```
## 設計模式:Facade Pattern(外觀模式)
透過 `index.ts` 的概念,我們實際上實踐了 **Facade Pattern(外觀模式)**。這個模式讓呼叫端可以透過一個簡潔的介面來使用子系統的功能,而不需要了解內部的複雜細節。
### 優點
- **簡化引入**:只需要從一個地方引入所有相關功能
- **降低耦合**:外部組件不需要知道內部檔案結構
- **易於維護**:內部結構調整時,只需要修改 index 檔案
- **提升可讀性**:程式碼引入部分更加簡潔明瞭
## 為什麼選擇 ES Modules?
### 1. 瀏覽器原生支援
這是最重要的原因。現代瀏覽器原生支援 ES Modules,不需要額外的 polyfill 或轉換工具。
### 2. 性能優化(Tree Shaking)
ES Modules 的**靜態結構**特性讓打包工具(如 Vite、Webpack)能夠在建構時精準地移除未使用的程式碼,大幅縮小最終檔案體積。這對於追求極致載入速度的前端應用來說是無法抗拒的優勢。
### 3. 更好的工具支援
現代開發工具對 ES Modules 有更完善的支援,包括:
- 更準確的靜態分析
- 更好的自動完成功能
- 更精確的重構工具
## TypeScript 模組路徑別名設定
在現代前端專案中設定路徑別名(例如用 `@/` 代表 `src/`),遵循**兩步驟原則**:
### 第一步:設定 TypeScript(開發環境)
修改 `tsconfig.json`:
```json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
```
**參數說明**:
- `"baseUrl": "."` - 設定解析的基準目錄
- `"paths"` - 定義路徑別名的對應規則
### 第二步:設定打包工具(建構環境)
在打包工具的設定檔中加入別名:
```javascript
// 通用概念
resolve: {
alias: {
'@': 'src 資料夾的絕對路徑'
}
}
```
> **重要提醒**:
> - 打包工具需要提供絕對路徑
> - 確保兩個步驟的對應關係一致
> - 不同打包工具有不同的設定語法
## 總結
ES Modules 的設計哲學其實反映了現代前端開發的核心思維:**模組化、可維護性、和開發體驗的最佳化**。
### 從技術角度看
ES Modules 不只是語法糖,它真正解決了前端開發中的幾個痛點:
- **靜態分析**:讓工具能更好地理解我們的程式碼
- **Tree Shaking**:讓打包後的檔案更小更高效
- **開發體驗**:IDE 能提供更精準的提示和重構
### 從設計角度看
透過實際案例,我們看到了如何運用 Facade Pattern 來組織程式碼。這不只是技術問題,更是設計思維的體現:
- **抽象複雜度**:透過 index.ts 隱藏內部實作細節
- **單一職責**:每個模組專注於自己的功能
- **依賴反轉**:外部程式碼不依賴具體的檔案結構
### 從實務角度看
**開發工具的設定需要考慮到整個工作流程**。不是只設定 TypeScript 就夠了,還要考慮打包工具、編輯器支援等等。