# Day 20-21：前端新手技能樹；驗證器流程模組化；Vitest - vi 的 Mock/ Spy 方法

By [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake) · 2025-12-18

---

前端新手技能樹 課程影片 #0-#4, #9, #10
---------------------------

身體狀態欠佳，輕量閱讀也做不到，就把影片當成背景知識，技術筆記抓重點、讓之後有力氣再練功就好。  
整串是給「前端新手技能樹 #0-#4、#9、#10」用的隨手小抄。

* * *

### Compiler / Bundler / CLI

*   Compiler（編譯器）：把一種語言轉成另一種語言，例如 TypeScript 編成瀏覽器看得懂的 JavaScript，或 JSX 編成純 JS。
    
*   Bundler（打包工具）：把一堆 JS / CSS / 圖片等模組分析依賴後打成幾個 bundle，例如 Webpack、Vite、Rollup，讓瀏覽器少下載、好快取。
    
*   CLI（命令列工具）：用指令快速完成重複工作，如 `npm init`, `vite create`, `git commit`，本質就是「用參數呼叫程式的一種介面」。
    

實務情境小抄：

*   開新專案：用框架官方 CLI 一次生出基本檔案結構（例如 `npm create vite@latest my-app`）。
    
*   寫 TS / React：編譯器負責轉譯，Bundler 負責打包壓縮、切檔，瀏覽器只接收最後的 JS / CSS。
    

[![](https://paragraph.com/editor/youtube/play.png)](https://www.youtube.com/watch?v=K525LeqG1-A)

* * *

### HTML / CSS / JS 前端鐵三角

*   HTML：語意與結構，是「骨架」，負責定義頁面上有什麼元素，例如標題、段落、按鈕、表單。
    
*   CSS：長相與排版，是「外觀肌肉 / 皮膚」，控制顏色、大小、位置、字體、RWD 等視覺表現。
    
*   JS：行為與互動，是「大腦與神經」，負責處理事件（點擊、輸入）、更新畫面、跟後端 API 溝通。
    

實務情境：做一個「送出表單」按鈕

*   HTML：`<button type="submit">送出</button>` → 有一個可以點的按鈕。
    
*   CSS：把按鈕變成你想要的顏色、圓角、hover 效果。
    
*   JS：決定按下去要驗證什麼、傳去哪裡、成功時怎麼提示使用者。
    

建議：先用 HTML 把骨架打出來，再慢慢補 CSS、最後再加 JS 行為，避免一開始就糊在一起。

[![](https://paragraph.com/editor/youtube/play.png)](https://www.youtube.com/watch?v=wtconUX03J0)

* * *

### Git / GitHub 與 Learn Git Branching

核心觀念：

*   Git：分散式版本控制系統，幫你記錄每一次修改，可以開分支、多線開發，壞掉可以「穿越時空」回到之前狀態。
    
*   GitHub：拿來放 Git repository 的雲端服務，搭配 issue / PR / 權限，變成大家協作的基地。
    

為什麼重要：

*   不用再有 `final_v3_ok_really_final.html` 這種檔名；用 commit 訊息記錄每一次變更。
    
*   團隊開發時，每個人開自己的 branch，測完再合併進 main，降低互相蓋掉程式碼的機率。
    

練習建議：

*   先用 Learn Git Branching 玩遊戲，把 `commit`、`branch`、`checkout`、`merge`、`rebase` 變成視覺化概念。
    
*   之後再回頭看類似《猴子都能懂的 Git 入門》這種文字教學，把名詞補齊。
    

[![](https://paragraph.com/editor/youtube/play.png)](https://www.youtube.com/watch?v=0y-xgoyebLg)

* * *

### 用 Who / When / What 拆 JS 功能（100 行挑戰思維）

想一個小功能時，可以用這三個問題來拆：

*   Who：誰在操作？對應到「哪個使用者 / 哪個 DOM 元素在觸發事件」。
    
    *   例：使用者點擊「加入購物車」按鈕。
        
*   When：什麼時候要發生？對應到「事件類型或時機」。
    
    *   例：按下按鈕（`click`）、輸入文字（`input`）、頁面載入完成（`DOMContentLoaded`）。
        
*   What：要做什麼？就是事件 handler 裡面的邏輯。
    
    *   例：把商品 id 加進陣列、更新畫面上的數字、存到 `localStorage`。
        

100 行挑戰精神：

*   用原生 JS + HTML + CSS 寫一個你有興趣的小功能（倒數計時器、todo list、抽獎轉盤、打怪計算器）。
    
*   限制自己程式碼控制在大約 100 行內，逼自己做到：
    
    *   拆小 function，命名清楚。
        
    *   把 Who / When / What 寫成乾淨的事件綁定 +邏輯。
        
    *   避免一次塞一大坨寫不完、看不懂的 code。
        

[![](https://paragraph.com/editor/youtube/play.png)](https://www.youtube.com/watch?v=Uy5DAFnd6ms)

* * *

### 框架與撰寫規範（含 airbnb style）

框架做的事：

*   React / Vue / Svelte… 提供一套共通模式：
    
    *   怎麼拆元件、怎麼管理狀態、怎麼做路由。
        
    *   減少你每天直接操作 DOM 的次數，讓 UI 狀態跟資料同步。
        

撰寫規範為什麼要先建立：

*   目標是「多人協作時，每個人寫出來的程式碼看起來像同一個人寫的」。
    
*   規範涵蓋：檔案命名、資料夾結構、JS / TS 寫法、註解風格、commit message 格式等等。
    
*   常見做法：
    
    *   選好語言與框架（例如：TypeScript + React）。
        
    *   設定 ESLint（airbnb 規範）、Prettier 自動排版。
        
    *   配合 Git hook 或 CI，在 `commit` / PR 時自動檢查。
        

這樣未來你在任何框架（React / Vue / Next / Nuxt…）中，都可以沿用同一套「乾淨、可維護」的寫法習慣。

[![](https://paragraph.com/editor/youtube/play.png)](https://www.youtube.com/watch?v=IkVaPpojwA8)

* * *

驗證器流程模組化
--------

目標：把 Node.js CLI 的「使用者輸入＋驗證」流程拆成多個職責單一的小模組，用 Promise / async / await 取代 while loop，讓控制流程線性、可讀性更好，也方便 TDD。驗證不通過視為「可預期錯誤」，在互動流程中處理；只有「整個 CLI 掛掉」才冒泡到 `main`。

### 為什麼要拆模組？

原本的 CLI 驗證流程：在一個 async 函式裡開 `readline`，用 `while (true)` 反覆提問與驗證，輸入錯就 `continue`，正確才 `break`。雖然可以運作，但有幾個問題：

*   **責任混在一起**：  
    \-「建立 / 關閉 CLI」  
    \-「問問題」  
    \-「驗證邏輯」  
    全部集中在同一個函式。
    
*   **控制流程不好讀**：
    
    *   `while` 搭配 async/await 容易產生巢狀，錯誤處理分散。
        
*   **不利 TDD / 重構**：
    
    *   難以單獨測試某一段邏輯（例如 validator），也難以隔離 readline I/O。
        

重構目標：  
**把「CLI 生命週期、提問流程、驗證邏輯」拆成多個責任單一的小模組，以 Promise / async / await 串接。**

* * *

### 各模組的責任與核心概念

模組

責任

`createRl`

建立 readline 介面（stdin / stdout）

`runCliSession`

CLI 生命週期（init / close）

`questionOnce`

單次提問（`rl.question` → Promise）

`askWithValidator`

單欄位：反覆提問＋單一 validator

`composeValidators`

多個 validator → 一個 validator

`questionConfigs`

宣告欄位、提示、驗證器組合

`runCliQuestions`

一次跑完整個互動流程、回傳結果

`main`

真正的業務邏輯怎麼用這些輸入

  

### 各模組函式的依賴關係

    [main]
      │
      │ (1) 呼叫 runCliQuestions
      │ (2) catch 未預期錯誤
      ▼
    [runCliQuestions]
      │
      ├───▶ (1) 呼叫 [runCliSession]
      │         │
      │         ├───▶ 呼叫 [createRl] (建立 readline)
      │         │
      │         ├───▶ 執行傳入的 async callback (處理題目迴圈)
      │         │     │
      │         │     ▼
      │         │    for loop (每一題 q of questionConfigs)
      │         │     │
      │         │     └───▶ 呼叫 [askWithValidator] (rl, prompt, validator)
      │         │             │
      │         │             ├───▶ 呼叫 [questionOnce] (rl, prompt)
      │         │             │       │
      │         │             │       └───▶ Promise wrapper around rl.question
      │         │             │
      │         │             └───▶ 呼叫 validator(raw)
      │         │                     │
      │         │                     └───▶ 實際上是呼叫 [composeValidators] 產生的總驗證器
      │         │                             │
      │         │                             ├───▶ [ensureIntegerString]
      │         │                             ├───▶ [toInt]
      │         │                             └───▶ [parseEvenNum] 等...
      │         │
      │         └───▶ finally: rl.close()
      │
      ▼
    回傳 { [name]: value } 結果給 main
    
    -----------------------------------------------------------
    
    資料結構層 (Config Layer):
    
    [questionConfigs.js]
      │
      ├───▶ 定義 questionConfigs 陣列
      │     │
      │     └───▶ { validator: [composeValidators]([v1, v2, v3]) }
      │                           │
      │                           ├───▶ [v1] (ensureIntegerString)
      │                           ├───▶ [v2] (toInt)
      │                           └───▶ [v3] (parseEvenNum)
      │
      └───▶ 被 [main] 或 [runCliQuestions] 使用 (作為輸入資料)
    
    

* * *

#### 1\. `createRl`：與 Node API 對接的最底層包裝

    export function createRl() {
      return readline.createInterface({ input, output });
    }
    

核心概念：

*   將 `readline/promises` 的建立動作集中於一處，形成統一的工廠函式。
    
*   若未來想替換輸入來源（例如測試用 fake stream），或切換實作，只需修改此模組。
    
*   其它模組可以透過 spy/mock 取代這個函式，避免實際啟動 readline。
    

* * *

#### 2\. `runCliSession`：CLI 的「安全使用器」

    export async function runCliSession(run) {
      const rl = createRl();
    
      try {
        return await run(rl);
      } finally {
        rl.close();
      }
    }
    

核心概念：**「不論** `run` **成功或失敗，離開此函式前一定會關閉** `rl`**。」**

*   成功情境：
    
    *   `run(rl)` 正常完成並回傳結果。
        
    *   `finally` 中呼叫 `rl.close()`，CLI 乾淨收尾，游標狀態正常。
        
*   失敗情境：
    
    *   `run(rl)` 執行過程丟出錯誤。
        
    *   `finally` 仍會呼叫 `rl.close()`，避免 CLI 卡在中途。
        
    *   錯誤繼續往外拋，交由 `main` 處理。
        

主程式只要：

    runCliSession(async (rl) => {
      // 在這裡放心寫 CLI 流程
    }).catch((err) => {
      console.error('未預期錯誤：', err);
    });
    
    

此設計確保 `readline.Interface` 作為「需釋放資源」的物件，無論流程如何結束都能被妥善關閉。

* * *

#### 3\. `questionOnce`：一次提問的最小單元

    export function questionOnce(rl, prompt) {
      return new Promise((resolve) => {
        rl.question(prompt, (answer) => {
          resolve(answer);
        });
      });
    }
    

核心概念：

*   把 callback 型 `rl.question(prompt, cb)` 封裝成 Promise。
    
*   只負責「顯示一個 prompt，取得一行輸入字串」。
    
*   後續互動邏輯（例如重複提問）皆以此為基礎。
    

* * *

#### 4\. `askWithValidator`：單欄位反覆提問直到驗證通過

    export async function askWithValidator(rl, prompt, validator) {
      while (true) {
        const raw = await questionOnce(rl, prompt);
    
        try {
          const value = validator(raw);
          return value; // 通過 → 結束，回傳驗證後的值
        } catch (error) {
          console.log('✗', error.message);
          // 不 return、不 throw → 迴圈繼續 → 再問一次
        }
      }
    }
    
    

從使用者角度：

1.  顯示問題。
    
2.  輸入錯 → CLI 顯示錯誤訊息 → 自動再跳出同一題，讓你重新輸入。
    
3.  輸入對 → 跳出這一題，回傳最後的值。
    

核心概念：

*   控制「反覆提問直到驗證通過」的流程封裝在這一層。
    
*   顯示 validator 拋出的錯誤訊息也在這一層處理。
    
*   主程式看不到「驗證錯誤」，只會看到「最後驗證成功的值」或「程式真的壞掉的錯誤」。
    
*   第三個參數 `validator` 可以是：
    
    *   原子驗證器（單一小函式），或
        
    *   經由 `composeValidators` 組合後的「總驗證器」。
        

* * *

#### 5\. `composeValidators`：多步驟驗證的組合器（高階函式 HOF）

    export function composeValidators(validators) {
      return (value) => {
        let current = value;
        for (const validate of validators) {
          current = validate(current);
        }
        return current;
      };
    }
    
    

核心概念：

*   輸入：一組 validator 陣列，每一顆型別約為 `(v) => v' | throws`。
    
*   輸出：一個「總 validator」函式：
    
    *   依陣列順序依次執行：
        
        *   `current = v1(value)`
            
        *   `current = v2(current)`
            
        *   …
            
    *   任一 validator 丟錯 → 後續 validator 不再執行，錯誤原封不動往外拋出。
        

目的：

*   讓「多步驟驗證」從程式碼結構（巢狀 if / 巢狀函式）抽象成「資料結構＋通用組合器」。
    
*   在設定檔中以陣列呈現規則，例如：
    
    *   `[ensureIntegerString, toInt, parseNonNegativeInt, parseEvenNum]`  
        可以一眼看出完整的驗證流程。
        

* * *

#### 6\. `validators`：單一職責的細顆粒驗證器

整數路線例子：

    export function ensureIntegerString(input) {
      const str = String(input).trim();
      const isIntLiteral = /^[+-]?[0-9]+$/.test(str);
      if (!isIntLiteral) {
        throw new Error('輸入錯誤，請輸入整數');
      }
      return str; // 整數字串（已去空白）
    }
    
    export function toInt(str) {
      return parseInt(str, 10);
    }
    
    export function parseNonNegativeInt(num) {
      if (!Number.isInteger(num)) {
        throw new Error('輸入錯誤，請輸入整數');
      }
      if (num < 0) {
        throw new Error('輸入錯誤，請輸入0或正整數');
      }
      return num;
    }
    
    

小數路線例子：

    export function ensureDecimalString(input) {
      const str = String(input).trim();
      const isDecimal =
        /^[+-]?(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)$/.test(str);
      if (!isDecimal) {
        throw new Error('輸入錯誤，請輸入數字');
      }
      return str;
    }
    
    export function toFloat(str) {
      return Number(str);
    }
    
    

核心概念：

*   每一個 validator 都是「單一職責」：
    
    *   `ensureIntegerString`：檢查是否為整數字面量字串。
        
    *   `toInt`：純數字字串轉為整數。
        
    *   `parseNonNegativeInt`：檢查是否為非負整數值。
        
    *   `parseEvenNum`：檢查是否為大於等於某最小值的偶數。
        
    *   `ensureDecimalString`：檢查是否為十進位數字字串（可含小數）。
        
    *   `toFloat`：數字字串轉為 `number`。
        
*   任一 validator 不滿足條件就丟出 `Error`，由外層互動邏輯統一處理訊息與重試。
    

* * *

#### 7\. `questionConfigs`：用設定描述每一題的「提問內容＋驗證方法」

    import { composeValidators } from './composeValidators.js';
    import {
      ensureIntegerString,
      toInt,
      parseNonNegativeInt,
    } from './validators.js';
    
    export const questionConfigs = [
      {
        name: 'itemCount',
        prompt: '請輸入要購買的數量（0 或正整數）：',
        validator: composeValidators([
          ensureIntegerString,
          toInt,
          parseNonNegativeInt,
        ]),
      },
    ];
    
    

核心概念：

*   把「每一題要問什麼、要怎麼驗證」變成一個純資料結構：
    
    *   題目名稱 `name`：在結果物件中的 key。
        
    *   `prompt`： CLI 顯示給使用者的文字。
        
    *   `validator`：一條已經 compose 好的驗證 pipeline。
        
*   這一層不做任何 IO，只負責宣告「問什麼題＋怎麼驗證」。
    

* * *

#### 8\. `runCliQuestions`：跑完所有互動流程

    import { runCliSession } from './runCliSession.js';
    import { askWithValidator } from './askWithValidator.js';
    
    export async function runCliQuestions(questionConfigs) {
      return runCliSession(async (rl) => {
        const result = {};
    
        for (const q of questionConfigs) {
          const value = await askWithValidator(rl, q.prompt, q.validator);
          result[q.name] = value;
        }
    
        return result;
      });
    }
    
    

核心概念：

*   對外代表「一整段 CLI 問答流程」。
    
*   內部：
    
    *   使用 `runCliSession` 管理 `rl` 的建立與關閉。
        
    *   用 `for...of` 明確表示「依序處理每一題」。
        
    *   每一題透過 `askWithValidator` 完成「提問＋重試＋驗證」。
        
    *   最後組成結果物件 `{ [name]: value }` 回傳。
        

* * *

### `main` 的角色

`main` 只處理兩件事：

1.  呼叫 `runCliQuestions(configs)` 取得完整的回答物件。
    
2.  統一處理「未預期錯誤」（例如程式 bug、IO 例外）。
    

    import { questionConfigs } from './q6config.js';
    import { runCliQuestions } from '../util/runCliQuestions.js';
    
    async function main() {
      const answers = await runCliQuestions(questionConfigs);
      console.log('第六題回答：', answers.evenNum);
    }
    
    main().catch((err) => {
      console.error('程式發生未預期錯誤：', err);
    });
    
    

*   驗證錯誤：由 `askWithValidator` 顯示錯誤並重問。
    
*   系統錯誤：冒泡到 `main().catch(...)`，集中處理。
    

* * *

Vitest - vi 的 Mock / Spy 方法
---------------------------

### 為什麼需要 Mock / Spy？

在 TDD 過程中，希望針對上述每個模組「單獨」驗證行為：

*   不希望測試真的開啟 readline、等待人工輸入。
    
*   希望控制輸入序列（例如第一次輸入錯、第二次輸入對）。
    
*   希望確認：
    
    *   `runCliSession` 是否有呼叫 `createRl` 和 `rl.close`。
        
    *   `runCliQuestions` 是否有依序對每題呼叫 `askWithValidator`。
        

這些需求可以透過 mock function 與 spy 建立「測試替身 test double」達成。

* * *

### Mock function：`vi.fn()`、`vi.mock(...)`

**mock：用假的實作取代真實依賴，並記錄呼叫資訊。**

    const fn = vi.fn();            // 空函式，只記錄呼叫
    const fn2 = vi.fn((x) => x*2); // 有行為的 mock function
    
    

`fn.mock.calls` 是一個二維陣列：

*   `fn.mock.calls[0]`：第一次呼叫時的「參數陣列」。
    
*   `fn.mock.calls[0][0]`：第一次呼叫的第 0 個參數。
    

建立 fake readline：

    function createFakeRl(answers) {
      let callCount = 0;
    
      return {
        question: vi.fn((prompt, callback) => {
          const answer = answers[callCount];
          callCount += 1;
          setTimeout(() => callback(answer), 0);
        }),
        close: vi.fn(),
      };
    }
    
    

用途：

*   `question`：
    
    *   模擬非同步提問行為（透過 `setTimeout`）。
        
    *   可檢查被呼叫次數與 prompt。
        
*   `close`：
    
    *   檢查整個流程是否在最後確實關閉 interface。
        

* * *

### Spy：`vi.spyOn(...)`

**spy：在既有物件的方法外掛監聽器，可以選擇是否改變行為。**

    // util/createRl.js
    export function createRl() { /* ... */ }
    
    

測試中：

    import * as createRlModule from './createRl.js';
    import { vi } from 'vitest';
    
    const fakeRl = { close: vi.fn() };
    
    const createRlSpy = vi
      .spyOn(createRlModule, 'createRl')
      .mockReturnValue(fakeRl);
    
    

效果：

*   測試執行期間呼叫 `createRlModule.createRl()`：
    
    *   不會使用真實實作，而是回傳 `fakeRl`。
        
*   可以透過 `expect(createRlSpy).toHaveBeenCalledTimes(1)` 驗證是否有建立 `rl`。
    
*   結合 `expect(fakeRl.close).toHaveBeenCalledTimes(1)` 可驗證 `runCliSession` 是否在 finally 中確實關閉資源。
    

* * *

### Mock vs Spy：概念對照

*   **mock（**`vi.fn` **/** `vi.mock`**）**：
    
    *   常用於自建「假函式／假物件」，完全不依賴原本的實作。
        
    *   範例：
        
        *   `createFakeRl` 中的 `question`、`close`。
            
*   **spy（**`vi.spyOn`**）**：
    
    *   以「既有物件的方法」為目標加上監聽。
        
    *   預設只觀察呼叫紀錄；可額外用 `mockReturnValue` 改變回傳值。
        
    *   範例：
        
        *   對 `createRlModule.createRl` 掛 spy，用於測試 `runCliSession` 是否有呼叫它，以及是否正確關閉 `rl`。
            

實務 TDD 流程建議：

1.  先使用 `vi.fn()` 建立 mock function，熟悉 `mock.calls` 的結構與常用 matcher。
    
2.  再使用 `vi.spyOn()` 對已有模組方法加上監聽，學習如何在「只觀察」與「觀察＋改行為」之間做取捨。
    

透過這些工具，可以逐層以 TDD 驗證：

*   單一 validator 的行為（純函式）。
    
*   validator pipeline 的行為（`composeValidators`）。
    
*   單題互動與重試行為（`askWithValidator`）。
    
*   多題 CLI 流程（`runCliQuestions`）。
    
*   CLI 生命週期管理（`runCliSession`）。
    

在重構或調整邏輯時，只要觀察測試是否維持通過，即可確認「核心概念」與使用者互動行為仍然一致。

* * *

### 一次讀完整流程各模組程式碼

    // cli/createRl.js
    import readline from 'node:readline/promises';
    import { stdin as input, stdout as output } from 'node:process';
    
    export function createRl() {
      return readline.createInterface({ input, output });
    }
    
    // cli/runCliSession.js
    import { createRl } from './createRl.js';
    
    export async function runCliSession(run) {
      const rl = createRl();
    
      try {
        return await run(rl);
      } finally {
        rl.close();
      }
    }
    
    // cli/questionOnce.js
    export function questionOnce(rl, prompt) {
      return rl.question(prompt);
    }
    
    // cli/askWithValidator.js
    import { questionOnce } from './questionOnce.js';
    
    export async function askWithValidator(rl, prompt, validator) {
      while (true) {
        const raw = await questionOnce(rl, prompt);
    
        try {
          const value = validator(raw);
          return value;
        } catch (error) {
          console.log('✗', error.message);
        }
      }
    }
    
    // cli/composeValidators.js
    export function composeValidators(validators) {
      return (value) => {
        let current = value;
        for (const validate of validators) {
          current = validate(current);
        }
        return current;
      };
    }
    
    // cli/validators.js
    export function ensureIntegerString(input) {
      const str = String(input).trim();
      const isIntLiteral = /^[+-]?[0-9]+$/.test(str);
      if (!isIntLiteral) {
        throw new Error('輸入錯誤，請輸入整數');
      }
      return str;
    }
    
    export function toInt(str) {
      return parseInt(str, 10);
    }
    
    export function parseNonNegativeInt(num) {
      if (!Number.isInteger(num)) {
        throw new Error('輸入錯誤，請輸入整數');
      }
      if (num < 0) {
        throw new Error('輸入錯誤，請輸入 0 或正整數');
      }
      return num;
    }
    
    // cli/questionConfigs.js
    import { composeValidators } from './composeValidators.js';
    import {
      ensureIntegerString,
      toInt,
      parseNonNegativeInt,
    } from './validators.js';
    
    export const questionConfigs = [
      {
        name: 'itemCount',
        prompt: '請輸入要購買的數量（0 或正整數）：',
        validator: composeValidators([
          ensureIntegerString,
          toInt,
          parseNonNegativeInt,
        ]),
      },
    ];
    
    // cli/runCliQuestions.js
    import { runCliSession } from './runCliSession.js';
    import { askWithValidator } from './askWithValidator.js';
    
    export async function runCliQuestions(configs) {
      return runCliSession(async (rl) => {
        const result = {};
    
        for (const q of configs) {
          const value = await askWithValidator(rl, q.prompt, q.validator);
          result[q.name] = value;
        }
    
        return result;
      });
    }
    
    // cli/main.js
    import { questionConfigs } from './questionConfigs.js';
    import { runCliQuestions } from './runCliQuestions.js';
    
    async function main() {
      const answers = await runCliQuestions(questionConfigs);
      console.log('輸入結果：', answers);
    }
    
    main().catch((err) => {
      console.error('程式發生未預期錯誤：', err);
    });

---

*Originally published on [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/day-20-21)*
