線上課程觀課進度管理小工具開發日誌
Day 6:JavaScript 程式碼執行排序:遞迴函數、Call Stack、Task Queue
Call Stack;遞迴函數與 call stack 的關係;Task Queue(非同步的概念再釐清)
Day 9:「修煉圈圈 Practice Rings」Web app 開發日誌 - 2025 六角 AI Vibe Coding 體驗營期末大魔王作業
這份筆記旨在記錄 2025 六角 Vibe Coding 體驗營每日作業 Day 21 的期末專案成果,從一個簡單的個人痛點出發,透過與 AI(在我這個情境中是 Perplexity)協作,在一天內完成了一個包含前後端的全端網頁小工具:「修煉圈圈 Practice Rings」。
<100 subscribers
線上課程觀課進度管理小工具開發日誌
Day 6:JavaScript 程式碼執行排序:遞迴函數、Call Stack、Task Queue
Call Stack;遞迴函數與 call stack 的關係;Task Queue(非同步的概念再釐清)
Day 9:「修煉圈圈 Practice Rings」Web app 開發日誌 - 2025 六角 AI Vibe Coding 體驗營期末大魔王作業
這份筆記旨在記錄 2025 六角 Vibe Coding 體驗營每日作業 Day 21 的期末專案成果,從一個簡單的個人痛點出發,透過與 AI(在我這個情境中是 Perplexity)協作,在一天內完成了一個包含前後端的全端網頁小工具:「修煉圈圈 Practice Rings」。
Share Dialog
Share Dialog
主題:在 Node.js 環境下,如何為 CLI 程式設計「可讀、好維護」的輸入驗證流程。
在 JavaScript 中,函式是一級公民,可以:
存在變數、陣列、物件中
當作參數傳入其他函式
當作回傳值傳出
擁有屬性與方法[1]
這非常適合拿來實作「驗證器」:
// 單一職責的驗證器
export function validatePositiveInt(raw, min = 1, max = Infinity) {
const trimmed = String(raw).trim();
if (!/^[+-]?[0-9]+$/.test(trimmed)) {
throw new Error("請輸入整數");
}
const num = parseInt(trimmed, 10);
if (num < min || num > max) {
if (max === Infinity) {
throw new Error(`請輸入大於等於 ${min} 的正整數`);
}
throw new Error(`請輸入 ${min} 到 ${max} 之間的正整數`);
}
return num;
}
設計重點:
驗證器是「普通函式」,接 string、丟錯誤或回傳轉型後的值。
名稱直接表意(validatePositiveInt),不需要額外文件。[1]
曾經的設計:為了「高可重用性」,先做一個驗證器組合器 composeValidators,然後所有驗證流程都要經過這個組合器。
// 抽象過度的範例(不推薦)
import { composeValidators } from "./composeValidators.js";
import {
validateIntegerString,
toInt,
validatePositiveInt,
} from "./validators.js";
export const config = {
validator: composeValidators([
validateIntegerString, // 格式檢查
toInt, // 型別轉換
validatePositiveInt, // 範圍檢查
]),
};
問題:
需要先理解 composeValidators 的執行順序與錯誤傳遞規則,認知負擔高。
每層可能拋出不同錯誤訊息,很難維持一致性。
回傳值若被包成 { value } 之類的物件,只是多一層樣板。
(「為了統一而統一」,但沒有實質收益。)
對單純的 CLI 練習題或小工具來說,這種抽象層級明顯超過實際需求。
重構後的目標:
一個驗證需求,一個函式。
直接回傳值,不包殼。
所有錯誤訊息在函式內統一管理。
// util/validators.js
function validateFormat(input, pattern, errorMsg) {
const trimmed = String(input).trim();
if (!pattern.test(trimmed)) {
throw new Error(errorMsg);
}
return trimmed;
}
function validateRange(num, min, max, errorMsg) {
if (num < min || (max !== undefined && num > max)) {
throw new Error(errorMsg);
}
return num;
}
export function validateNonNegativeInt(raw) {
const trimmed = validateFormat(raw, /^[+-]?[0-9]+$/, "請輸入整數");
const num = parseInt(trimmed, 10);
return validateRange(num, 0, undefined, "請輸入非負整數");
}
export function validateEvenInt(raw, min = 2) {
const trimmed = validateFormat(raw, /^[+-]?[0-9]+$/, "請輸入整數");
const num = parseInt(trimmed, 10);
if (num < 0) {
throw new Error("請輸入非負整數");
}
if (num % 2 !== 0) {
throw new Error("請輸入偶數");
}
return validateRange(num, min, undefined, `請輸入大於等於 ${min} 的偶數`);
}
使用方式:
import { validateNonNegativeInt } from "./util/validators.js";
const config = {
name: "visitNum",
prompt: "請輸入人數:",
validator: validateNonNegativeInt,
};
優點:
不需要閱讀 composeValidators 就能理解驗證邏輯。
錯誤訊息路徑清楚:「格式」→「範圍」→「業務規則」。
型別由 JS 自己承擔(直接回傳 number),不多包一層物件。[1]
情境:寫一個 CLI 工具,重複詢問使用者輸入,直到通過驗證。
// 反例:while(true) + return
import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
function createRl() {
return createInterface({ input, output });
}
export async function askWithValidator(prompt, validator) {
let rl = createRl();
while (true) {
const raw = await rl.question(prompt);
try {
const value = validator(raw);
rl.close();
return value; // 同時結束迴圈與函式
} catch (error) {
console.log("輸入錯誤,請重新輸入");
rl.close();
rl = createRl();
}
}
}
問題:
return 同時負責「跳出迴圈」與「結束函式」,職責混在一起。
除錯時不容易判斷是迴圈控制有誤,還是函式邏輯有誤。
while(true) 需要讀者在腦中模擬可能的結束條件,本身認知負擔就高。[2][3]
即使改成 break 也一樣會有「必須在腦中跑迴圈」的問題,只是職責稍微清楚一些:
// 仍然偏難讀:需要外部變數 + 模擬迴圈狀態
export async function askWithValidator(prompt, validator) {
let rl = createRl();
let result;
while (true) {
const raw = await rl.question(prompt);
try {
result = validator(raw);
break;
} catch (error) {
console.log("輸入錯誤,請重新輸入");
rl.close();
rl = createRl();
}
}
rl.close();
return result;
}
更直覺的方式是改成「錯了就再問一次」的遞迴寫法:
// 版本 1:每次呼叫都建立/關閉一次 rl
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
export function createRl() {
return readline.createInterface({ input, output });
}
export async function askWithValidatorUntilPass(prompt, validator) {
const rl = createRl();
const raw = await rl.question(prompt);
try {
const value = validator(raw);
rl.close();
return value;
} catch (error) {
console.log(error.message);
rl.close();
return askWithValidatorUntilPass(prompt, validator);
}
}
閱讀時的心智模型變成:
問一次。
驗證成功 → 回傳。
驗證失敗 → 顯示錯誤 → 再呼叫自己一次。
幾乎不需要在腦中模擬狀態。對「重試直到成功」這種流程,遞迴比 while 更自然。[3][2]
為了避免每次遞迴都重新建立 readline.Interface,可以把資源管理抽到外層,內層只管重試:
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
export function createRl() {
return readline.createInterface({ input, output });
}
export async function askWithValidatorUntilPass(prompt, validator) {
const rl = createRl();
async function attempt() {
const raw = await rl.question(prompt);
try {
return validator(raw);
} catch (error) {
console.log(error.message || "輸入錯誤,請重新輸入");
return attempt();
}
}
try {
return await attempt();
} finally {
rl.close();
}
}
結構上的分工:
外層:建立與關閉 rl(資源管理)。
內層 attempt():只負責「問 → 驗證 → 成功或重試」。
這樣既避免了 while,也讓職責非常清晰。Node 官方文件也建議為 readline/promises 建立 interface 後,以 async/await 形式操作問題流程。[4][5]
在 CLI 主程式中,常見兩種寫法:
// 寫法 A:async 函式 + 外層 .catch()
async function main() {
// ...
}
main().catch((err) => {
console.error("程式發生未預期錯誤:", err);
process.exit(1);
});
// 寫法 B:async 函式 + 內部 try/catch
async function main() {
try {
// ...
} catch (err) {
console.error("程式發生未預期錯誤:", err);
process.exit(1);
}
}
main();
為了風格一致與可讀性,多數情況下選擇「函式內使用 async/await,就同時用 try/catch 在函式內處理錯誤」會比較好;這與 MDN 對 async function + try...catch 的示範風格相符。[6][7]
範例:
import { askWithValidatorUntilPass } from "./util/askWithValidatorUntilPass.js";
import { validatePositiveInt } from "./util/validators.js";
async function main() {
console.log("=== 細菌分裂總數量計算機 ===\n");
try {
const hours = await askWithValidatorUntilPass(
"請輸入經過的小時數:",
(raw) => validatePositiveInt(raw, 1, 48),
);
const result = 2 ** hours;
console.log("\n結果:", result);
} catch (err) {
console.error("程式發生未預期錯誤:", err);
process.exit(1);
}
}
main();
函式命名即文件validatePositiveInt、validateEvenInt 比 validate、check 清楚許多。[1]
錯誤訊息分層
格式錯誤:「請輸入整數」
範圍錯誤:「請輸入非負整數」或「請輸入 1–10 之間的整數」
業務規則錯誤:「請輸入偶數」
直接回傳,不過度包裝
讓錯誤靠 throw,成功靠回傳值,不需要額外 { value, errors } 外殼。
避免不必要的抽象
沒有實際需求時,不要先設計通用的 validator pipe/chain;一個清楚的函式往往已足夠。
降低認知負擔優先
在迭代流程上,優先順序大致可以是:
認知負擔:遞迴 < 條件明確的迴圈 < while(true) + break < while(true) + return
風格一致性
若用 async/await,就搭配 try/catch。
一致的錯誤處理方式讓程式碼更好維護。[7][6]
主題:在 Node.js 環境下,如何為 CLI 程式設計「可讀、好維護」的輸入驗證流程。
在 JavaScript 中,函式是一級公民,可以:
存在變數、陣列、物件中
當作參數傳入其他函式
當作回傳值傳出
擁有屬性與方法[1]
這非常適合拿來實作「驗證器」:
// 單一職責的驗證器
export function validatePositiveInt(raw, min = 1, max = Infinity) {
const trimmed = String(raw).trim();
if (!/^[+-]?[0-9]+$/.test(trimmed)) {
throw new Error("請輸入整數");
}
const num = parseInt(trimmed, 10);
if (num < min || num > max) {
if (max === Infinity) {
throw new Error(`請輸入大於等於 ${min} 的正整數`);
}
throw new Error(`請輸入 ${min} 到 ${max} 之間的正整數`);
}
return num;
}
設計重點:
驗證器是「普通函式」,接 string、丟錯誤或回傳轉型後的值。
名稱直接表意(validatePositiveInt),不需要額外文件。[1]
曾經的設計:為了「高可重用性」,先做一個驗證器組合器 composeValidators,然後所有驗證流程都要經過這個組合器。
// 抽象過度的範例(不推薦)
import { composeValidators } from "./composeValidators.js";
import {
validateIntegerString,
toInt,
validatePositiveInt,
} from "./validators.js";
export const config = {
validator: composeValidators([
validateIntegerString, // 格式檢查
toInt, // 型別轉換
validatePositiveInt, // 範圍檢查
]),
};
問題:
需要先理解 composeValidators 的執行順序與錯誤傳遞規則,認知負擔高。
每層可能拋出不同錯誤訊息,很難維持一致性。
回傳值若被包成 { value } 之類的物件,只是多一層樣板。
(「為了統一而統一」,但沒有實質收益。)
對單純的 CLI 練習題或小工具來說,這種抽象層級明顯超過實際需求。
重構後的目標:
一個驗證需求,一個函式。
直接回傳值,不包殼。
所有錯誤訊息在函式內統一管理。
// util/validators.js
function validateFormat(input, pattern, errorMsg) {
const trimmed = String(input).trim();
if (!pattern.test(trimmed)) {
throw new Error(errorMsg);
}
return trimmed;
}
function validateRange(num, min, max, errorMsg) {
if (num < min || (max !== undefined && num > max)) {
throw new Error(errorMsg);
}
return num;
}
export function validateNonNegativeInt(raw) {
const trimmed = validateFormat(raw, /^[+-]?[0-9]+$/, "請輸入整數");
const num = parseInt(trimmed, 10);
return validateRange(num, 0, undefined, "請輸入非負整數");
}
export function validateEvenInt(raw, min = 2) {
const trimmed = validateFormat(raw, /^[+-]?[0-9]+$/, "請輸入整數");
const num = parseInt(trimmed, 10);
if (num < 0) {
throw new Error("請輸入非負整數");
}
if (num % 2 !== 0) {
throw new Error("請輸入偶數");
}
return validateRange(num, min, undefined, `請輸入大於等於 ${min} 的偶數`);
}
使用方式:
import { validateNonNegativeInt } from "./util/validators.js";
const config = {
name: "visitNum",
prompt: "請輸入人數:",
validator: validateNonNegativeInt,
};
優點:
不需要閱讀 composeValidators 就能理解驗證邏輯。
錯誤訊息路徑清楚:「格式」→「範圍」→「業務規則」。
型別由 JS 自己承擔(直接回傳 number),不多包一層物件。[1]
情境:寫一個 CLI 工具,重複詢問使用者輸入,直到通過驗證。
// 反例:while(true) + return
import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
function createRl() {
return createInterface({ input, output });
}
export async function askWithValidator(prompt, validator) {
let rl = createRl();
while (true) {
const raw = await rl.question(prompt);
try {
const value = validator(raw);
rl.close();
return value; // 同時結束迴圈與函式
} catch (error) {
console.log("輸入錯誤,請重新輸入");
rl.close();
rl = createRl();
}
}
}
問題:
return 同時負責「跳出迴圈」與「結束函式」,職責混在一起。
除錯時不容易判斷是迴圈控制有誤,還是函式邏輯有誤。
while(true) 需要讀者在腦中模擬可能的結束條件,本身認知負擔就高。[2][3]
即使改成 break 也一樣會有「必須在腦中跑迴圈」的問題,只是職責稍微清楚一些:
// 仍然偏難讀:需要外部變數 + 模擬迴圈狀態
export async function askWithValidator(prompt, validator) {
let rl = createRl();
let result;
while (true) {
const raw = await rl.question(prompt);
try {
result = validator(raw);
break;
} catch (error) {
console.log("輸入錯誤,請重新輸入");
rl.close();
rl = createRl();
}
}
rl.close();
return result;
}
更直覺的方式是改成「錯了就再問一次」的遞迴寫法:
// 版本 1:每次呼叫都建立/關閉一次 rl
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
export function createRl() {
return readline.createInterface({ input, output });
}
export async function askWithValidatorUntilPass(prompt, validator) {
const rl = createRl();
const raw = await rl.question(prompt);
try {
const value = validator(raw);
rl.close();
return value;
} catch (error) {
console.log(error.message);
rl.close();
return askWithValidatorUntilPass(prompt, validator);
}
}
閱讀時的心智模型變成:
問一次。
驗證成功 → 回傳。
驗證失敗 → 顯示錯誤 → 再呼叫自己一次。
幾乎不需要在腦中模擬狀態。對「重試直到成功」這種流程,遞迴比 while 更自然。[3][2]
為了避免每次遞迴都重新建立 readline.Interface,可以把資源管理抽到外層,內層只管重試:
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
export function createRl() {
return readline.createInterface({ input, output });
}
export async function askWithValidatorUntilPass(prompt, validator) {
const rl = createRl();
async function attempt() {
const raw = await rl.question(prompt);
try {
return validator(raw);
} catch (error) {
console.log(error.message || "輸入錯誤,請重新輸入");
return attempt();
}
}
try {
return await attempt();
} finally {
rl.close();
}
}
結構上的分工:
外層:建立與關閉 rl(資源管理)。
內層 attempt():只負責「問 → 驗證 → 成功或重試」。
這樣既避免了 while,也讓職責非常清晰。Node 官方文件也建議為 readline/promises 建立 interface 後,以 async/await 形式操作問題流程。[4][5]
在 CLI 主程式中,常見兩種寫法:
// 寫法 A:async 函式 + 外層 .catch()
async function main() {
// ...
}
main().catch((err) => {
console.error("程式發生未預期錯誤:", err);
process.exit(1);
});
// 寫法 B:async 函式 + 內部 try/catch
async function main() {
try {
// ...
} catch (err) {
console.error("程式發生未預期錯誤:", err);
process.exit(1);
}
}
main();
為了風格一致與可讀性,多數情況下選擇「函式內使用 async/await,就同時用 try/catch 在函式內處理錯誤」會比較好;這與 MDN 對 async function + try...catch 的示範風格相符。[6][7]
範例:
import { askWithValidatorUntilPass } from "./util/askWithValidatorUntilPass.js";
import { validatePositiveInt } from "./util/validators.js";
async function main() {
console.log("=== 細菌分裂總數量計算機 ===\n");
try {
const hours = await askWithValidatorUntilPass(
"請輸入經過的小時數:",
(raw) => validatePositiveInt(raw, 1, 48),
);
const result = 2 ** hours;
console.log("\n結果:", result);
} catch (err) {
console.error("程式發生未預期錯誤:", err);
process.exit(1);
}
}
main();
函式命名即文件validatePositiveInt、validateEvenInt 比 validate、check 清楚許多。[1]
錯誤訊息分層
格式錯誤:「請輸入整數」
範圍錯誤:「請輸入非負整數」或「請輸入 1–10 之間的整數」
業務規則錯誤:「請輸入偶數」
直接回傳,不過度包裝
讓錯誤靠 throw,成功靠回傳值,不需要額外 { value, errors } 外殼。
避免不必要的抽象
沒有實際需求時,不要先設計通用的 validator pipe/chain;一個清楚的函式往往已足夠。
降低認知負擔優先
在迭代流程上,優先順序大致可以是:
認知負擔:遞迴 < 條件明確的迴圈 < while(true) + break < while(true) + return
風格一致性
若用 async/await,就搭配 try/catch。
一致的錯誤處理方式讓程式碼更好維護。[7][6]
No comments yet