這個專案是我「52 個 Rust 專案」學習計畫的一部分。在完成了 16 個專案後,我發現 WebAssembly 是一個重要的學習缺口,於是決定用 Rust 來打造一個即時 Markdown 編輯器。
專案原始碼:wasm-markdown-editor
為什麼選擇這個專案?
在分析了之前完成的專案後,我發現幾個學習上的空白:
- 已掌握的領域:Async/await、網路程式設計(TCP/UDP/HTTP/WebSockets)、解析器、CLI 工具、錯誤處理
- 待加強的領域:資料庫 ORM、程序宏、FFI、WebAssembly、進階測試、GUI/圖形
選擇 Markdown 編輯器的原因:
- 填補關鍵缺口:之前沒有任何 WASM 專案
- 善用既有技能:運用之前在計算機、shell、EBML 等專案中學到的解析技巧
- 互動性強:能立即看到成果,滿足感高
- 實用價值:這是真正能用的工具,不只是 demo
- 現代技術棧:WASM 在 Rust 生態系中越來越重要
技術架構
Rust 依賴項
[dependencies]
wasm-bindgen = "0.2" # JavaScript 互操作層
pulldown-cmark = "0.12" # 經過實戰驗證的 Markdown 解析器
web-sys = "0.3" # Web API 綁定
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6" # Rust/JS 之間的資料序列化
console_error_panic_hook = "0.1" # 瀏覽器中更好的錯誤訊息
專案結構
wasm-markdown-editor/
├── Cargo.toml # Rust 專案設定
├── index.html # 主進入點
├── src/
│ ├── lib.rs # WASM 進入點
│ ├── parser.rs # Markdown 解析 + 統計邏輯
│ └── utils.rs # Panic hook 和工具函式
├── www/
│ ├── index.js # JavaScript 應用邏輯
│ └── styles.css # 樣式
└── pkg/ # 建置輸出
├── wasm_markdown_editor.js
└── wasm_markdown_editor_bg.wasm
核心實作
將函式匯出到 JavaScript
use wasm_bindgen::prelude::*;
// WASM 模組載入時自動執行
#[wasm_bindgen(start)]
pub fn init() {
utils::set_panic_hook(); // 更好的錯誤訊息
}
// 簡單的字串轉換
#[wasm_bindgen]
pub fn markdown_to_html(markdown: &str) -> String {
parser::parse_markdown(markdown)
}
// 複雜結構 → JavaScript 物件
#[wasm_bindgen]
pub fn get_statistics(text: &str) -> JsValue {
let stats = parser::calculate_stats(text);
serde_wasm_bindgen::to_value(&stats).unwrap()
}
關鍵模式:
#[wasm_bindgen] 標記要匯出給 JS 的函式
#[wasm_bindgen(start)] 在模組初始化時自動執行
- 簡單型別(str、數字、布林)自動轉換
- 複雜型別需要
serde-wasm-bindgen 進行序列化
Markdown 解析
use pulldown_cmark::{html, Options, Parser};
pub fn parse_markdown(markdown: &str) -> String {
let mut options = Options::empty();
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_FOOTNOTES);
options.insert(Options::ENABLE_TASKLISTS);
let parser = Parser::new_ext(markdown, options);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
html_output
}
啟用的功能:刪除線、表格、註腳、任務清單、標題屬性。
統計資訊計算
#[derive(Serialize)]
pub struct Statistics {
pub characters: usize,
pub characters_no_spaces: usize,
pub words: usize,
pub lines: usize,
pub paragraphs: usize,
pub reading_time_minutes: f64,
}
pub fn calculate_stats(text: &str) -> Statistics {
let words = text.split_whitespace().count();
let paragraphs = text
.split("\n\n")
.filter(|s| !s.trim().is_empty())
.count();
// 平均閱讀速度:每分鐘 200 字
let reading_time_minutes = (words as f64 / 200.0).ceil();
// ...
}
JavaScript 整合
import init, {
markdown_to_html,
get_statistics
} from '../pkg/wasm_markdown_editor.js';
async function run() {
// 初始化 WASM 模組
await init();
// 現在可以使用 Rust 函式了
const html = markdown_to_html(markdown);
const stats = get_statistics(text);
}
// 效能優化:防抖動
let debounceTimer = null;
const DEBOUNCE_DELAY = 300;
function handleInput() {
if (debounceTimer) clearTimeout(debounceTimer);
updateStatistics(); // 立即更新(快速)
debounceTimer = setTimeout(() => {
updatePreview(); // 防抖動(較重)
saveToStorage();
}, DEBOUNCE_DELAY);
}
關鍵重點:
- ES6 模組匯入 WASM
- 呼叫 Rust 函式前必須先執行非同步的
init()
- 防抖動避免過度渲染
- 統計立即更新(便宜),預覽防抖動(昂貴)
WASM 編譯深入解析
編譯流程
1. Rust 原始碼 (lib.rs, parser.rs, utils.rs)
↓
2. rustc --target wasm32-unknown-unknown
↓
3. 原始 .wasm 二進位檔(WebAssembly 位元組碼)
↓
4. wasm-bindgen(產生 JS 膠水程式碼)
↓
5. wasm-opt(Binaryen 優化)
↓
6. 最終輸出:.wasm + .js + .d.ts
記憶體模型
WASM 使用線性記憶體(單一連續區塊),JavaScript 和 Rust 透過這塊共享記憶體交換資料:
sequenceDiagram
participant JS as JavaScript
participant Mem as WASM 線性記憶體
participant Rust as Rust
JS->>Mem: 1. 寫入字串
JS->>Rust: 2. 傳遞指標 + 長度
Rust->>Rust: 3. 處理資料
Rust->>Mem: 4. 寫入結果
Rust->>JS: 5. 回傳結果指標
JS->>Mem: 6. 讀取結果
JS->>Mem: 7. 釋放記憶體
字串傳遞流程:
- JS 字串寫入 WASM 線性記憶體
- 傳遞指標和長度給 Rust 函式
- Rust 處理資料
- Rust 將結果寫入記憶體
- 回傳結果的指標給 JS
- JS 從記憶體讀取結果字串
- 釋放不再需要的記憶體
型別轉換對照表
| Rust 型別 |
WASM 型別 |
JavaScript 型別 |
&str, String |
i32 (ptr) + i32 (len) |
string |
i32, u32 |
i32 |
number |
f64 |
f64 |
number |
bool |
i32 (0 或 1) |
boolean |
JsValue |
externref |
any |
| 可序列化 struct |
externref |
object |
建置與效能
建置指令
# 安裝 wasm-pack(只需一次)
cargo install wasm-pack
# 建置 WASM 模組
wasm-pack build --target web
# 執行 Rust 單元測試
cargo test
# 在瀏覽器中執行 WASM 測試
wasm-pack test --headless --firefox
建置輸出
- WASM 套件:222 KB(已優化)
- JS 膠水程式碼:13 KB
- 總下載量:235 KB
- 建置時間:約 8 秒(增量)、約 13 秒(完整)
Cargo.toml 關鍵設定
[lib]
crate-type = ["cdylib", "rlib"] # WASM 編譯必需
[profile.release]
opt-level = "s" # 優化檔案大小(而非速度)
lto = true # 連結時優化,產生更小的套件
為什麼這些設定很重要:
cdylib = C 動態函式庫類型,WASM 必需
opt-level = "s" 產生的二進位檔比 “3” 小約 30%
- LTO 消除整個依賴樹中的死碼
遇到的問題與解決方案
問題:404 錯誤
從 www/ 目錄提供服務時,瀏覽器無法存取 ../pkg/(在提供的目錄之外)。
初始嘗試:從 www/ 目錄提供服務
http://localhost:8080/ → www/index.html
http://localhost:8080/pkg/... → 404 錯誤
解決方案:在專案根目錄建立 index.html
# 從專案根目錄提供服務,而非 www/
cd wasm-markdown-editor
python -m http.server 8080
# 存取 http://localhost:8080
實作的功能
- ✅ 帶防抖動的即時預覽
- ✅ 即時統計(字數、字元數、閱讀時間)
- ✅ LocalStorage 自動儲存
- ✅ 帶內嵌 CSS 的 HTML 匯出
- ✅ 範例 Markdown 載入器
- ✅ 鍵盤快捷鍵(Ctrl+S、Ctrl+K)
- ✅ 響應式分割視窗佈局
學習成果
WASM 特定技能
- ✅ 理解
cdylib crate 類型及其作用
- ✅ 使用
#[wasm_bindgen] 屬性匯出給 JS
- ✅ 管理 Rust/JavaScript 邊界的記憶體
- ✅ 型別轉換(簡單型別 vs. 複雜結構)
- ✅ WASM 模組初始化模式
- ✅ 在瀏覽器 DevTools 中除錯 WASM
- ✅ 套件大小優化技術
- ✅ 建置工具(
wasm-pack、wasm-bindgen)
什麼時候該用 WASM?
適合的場景:
- ✅ 計算密集型操作(解析、圖像處理)
- ✅ 想重用現有的 Rust 函式庫
- ✅ 效能關鍵路徑
- ✅ 套件大小可接受時
不太適合的場景:
- ❌ 大量 DOM 操作(用 JS)
- ❌ 微小的工具函式(開銷不值得)
- ❌ 簡單的 CRUD 操作
- ❌ 套件大小是關鍵考量時
與純 JavaScript 方案的比較
效能:
- Markdown 解析:比 JS 替代方案快約 2-3 倍
- 打字時沒有 GC 暫停
- 可預測的記憶體使用
套件大小:
- 與流行的 JS 函式庫相當或更小
- markdown-it.js:約 320 KB
- 我們的方案:235 KB(包含解析器 + 統計)
反思
順利的部分
- 流暢的建置過程:wasm-pack「直接就能用」
- 優秀的文件:Rust WASM book 非常有價值
- 型別安全:編譯時就能捕捉錯誤
- 效能:解析明顯流暢
- 工具鏈:自動產生的 TypeScript 定義很有幫助
克服的挑戰
- 路徑解析:404 錯誤需要理解 WASM 服務方式
- 記憶體模型:理解線性記憶體花了一些時間
- 型別轉換:學習何時用 JsValue vs. 簡單型別
- 非同步初始化:理解 init() 的必要性
驚喜的發現
- 套件大小:比預期小(222 KB 含完整解析器!)
- 建置速度:增量建置很快(8 秒)
- 瀏覽器支援:所有現代瀏覽器都能用,不需要 polyfill
- 開發體驗:在 DevTools 中除錯 WASM 相當不錯
結論
這個專案成功填補了我 rust-52-projects 學習旅程中的一個主要缺口。它證明了 Rust + WASM 已經可以用於生產環境的互動式 Web 應用程式,尤其是像解析這樣的計算密集型任務。
Rust 的效能和安全性與 JavaScript 的普及性結合,創造了一個強大的開發模式。工具鏈(wasm-pack、wasm-bindgen)已經成熟到體驗流暢且高效的程度。
這是一個很好的「第一個 WASM 專案」,在學習核心概念的同時建構出真正實用的東西。
參考資源
什麼是 Peacock?
Peacock 是一個非常實用的 VSCode 擴充套件,由 John Papa 開發。它的主要功能是讓你可以為不同的 VSCode 工作區設定不同的顏色主題,透過改變工作區的視窗顏色(包括狀態列、標題列、活動列等),讓你在同時開啟多個專案時能夠快速辨識當前正在工作的專案。
為什麼需要 Peacock?
如果你跟我一樣,經常需要同時處理多個專案,你一定遇過這些困擾:
- 🔀 在多個 VSCode 視窗間切換時,常常搞不清楚哪個視窗對應哪個專案
- 💥 不小心在錯誤的專案中編輯或執行程式碼
- 🎯 想要快速找到特定專案的視窗,但需要逐一檢視
Peacock 透過視覺化的顏色標記,完美解決了這些問題。每個專案都有自己獨特的顏色,一眼就能辨識。
主要功能
- 工作區顏色化:自訂狀態列、標題列、活動列等 UI 元素的顏色
- 預設配色庫:內建多種精心挑選的顏色主題
- 收藏顏色:可以建立自己的配色收藏清單
- 快速切換:透過指令面板快速更改工作區顏色
- 專案記憶:每個工作區的顏色設定會被記錄,重新開啟時自動套用
我的 Peacock 收藏配色
我用 AI 幫我產生一套以品牌和技術堆疊為主題的配色方案。這些顏色不僅視覺上容易辨識,也與對應的技術或品牌有直接關聯,讓我能更直覺地記憶和使用。
品牌主題色系
這些是知名科技品牌的代表色,當我在處理與這些平台相關的專案時特別好用:
| 顏色名稱 |
色碼 |
適用場景 |
| Airbnb Pink |
#ff385c |
Airbnb 相關專案 |
| Amazon Orange |
#ff9900 |
AWS 或 Amazon 服務整合 |
| Azure Blue |
#007fff |
Azure 雲端專案 |
| Facebook Blue |
#1877f2 |
Meta/Facebook 專案 |
| Google Blue |
#4285f4 |
Google Cloud 或 Firebase 專案 |
| LinkedIn Blue |
#0a66c2 |
LinkedIn 整合專案 |
| Netflix Red |
#e50914 |
串流媒體相關專案 |
| Spotify Green |
#1db954 |
音樂或音訊相關專案 |
| Tesla Red |
#e82127 |
IoT 或電動車相關專案 |
| Twitter Blue |
#1da1f2 |
社群媒體專案 |
程式語言與框架色系
這是我最常用的部分!根據專案使用的主要技術堆疊選擇對應顏色:
前端技術
- React Blue (
#61dafb) - React 專案
- Vue Green (
#42b883) - Vue.js 專案
- Angular Red (
#dd0531) - Angular 專案
- Svelte Orange (
#ff3d00) - Svelte 專案
- Nuxt Green (
#00dc82) - Nuxt.js 專案
- Tailwind Cyan (
#06b6d4) - 使用 Tailwind CSS 的專案
後端技術
- Node Green (
#215732) - Node.js 後端專案
- Node.js Green (
#68a063) - Node.js 全端專案
- Deno Green (
#00a853) - Deno 專案
- Python Blue (
#3776ab) - Python 專案
- Go Cyan (
#00add8) - Go 專案
- Rust Orange (
#ce422b) - Rust 專案
- PHP Purple (
#777bb4) - PHP 專案
- Ruby Red (
#cc342d) - Ruby 專案
- Java Orange (
#007396) - Java 專案
- Kotlin Purple (
#7f52ff) - Kotlin 專案
- Elixir Purple (
#6f42be) - Elixir 專案
- Scala Red (
#dc322f) - Scala 專案
- C# Purple (
#239120) - C# 專案
- C++ Blue (
#00599c) - C++ 專案
框架與工具
- Django Green (
#092e20) - Django 專案
- Laravel Red (
#ff2d20) - Laravel 專案
- NestJS Red (
#ea2845) - NestJS 專案
- Spring Green (
#6db33f) - Spring Framework 專案
- Fastify Blue (
#000000) - Fastify 專案
JavaScript 生態系
- JavaScript Yellow (
#f9e64f) - 純 JavaScript 專案
- TypeScript Blue (
#3178c6) - TypeScript 專案
- Babel Yellow (
#f9dc3e) - Babel 設定專案
- Webpack Blue (
#8dd6f9) - Webpack 相關專案
- ESLint Purple (
#4b32c3) - ESLint 設定專案
資料庫與基礎設施
- MongoDB Green (
#13aa52) - MongoDB 專案
- PostgreSQL Blue (
#336791) - PostgreSQL 專案
- Redis Red (
#dc382d) - Redis 快取專案
- Docker Blue (
#2496ed) - Docker 容器化專案
- Kubernetes Blue (
#326ce5) - K8s 部署專案
- Firebase Orange (
#ffa400) - Firebase 專案
- GraphQL Pink (
#e10098) - GraphQL API 專案
其他科技品牌
- GitHub Green (
#08872b) - GitHub 相關專案或 Actions
- npm Red (
#cb3837) - npm 套件開發
- Yarn Blue (
#2c8ebb) - 使用 Yarn 的專案
- AMD Red (
#ed1c24) - AMD 相關專案
- Intel Blue (
#0071c5) - Intel 相關專案
- Nvidia Green (
#76b900) - GPU 運算或機器學習專案
- Electron Blue (
#47848f) - Electron 桌面應用程式
特殊用途
- Jest Red (
#c21325) - 測試專案
- Bootstrap Purple (
#7952b3) - Bootstrap 前端專案
- Mandalorian Blue (
#1857a4) - 星際大戰粉絲的選擇 😄
- Something Different (
#832561) - 當你想要與眾不同的時候
如何使用這些配色
1. 安裝 Peacock
在 VSCode 擴充套件市場搜尋 “Peacock” 並安裝,或直接使用指令:
code --install-extension johnpapa.vscode-peacock
2. 設定收藏配色
將以下完整配色清單加入你的 VSCode 設定檔(settings.json):
{
"peacock.favoriteColors": [
{"name":"Airbnb Pink","value":"#ff385c"},
{"name":"Amazon Orange","value":"#ff9900"},
{"name":"AMD Red","value":"#ed1c24"},
{"name":"Angular Red","value":"#dd0531"},
{"name":"Apple Gray","value":"#555555"},
{"name":"AWS Orange","value":"#ff9900"},
{"name":"Azure Blue","value":"#007fff"},
{"name":"Babel Yellow","value":"#f9dc3e"},
{"name":"Bootstrap Purple","value":"#7952b3"},
{"name":"C++ Blue","value":"#00599c"},
{"name":"C# Purple","value":"#239120"},
{"name":"Clojure Green","value":"#5881d8"},
{"name":"Deno Green","value":"#00a853"},
{"name":"Django Green","value":"#092e20"},
{"name":"Docker Blue","value":"#2496ed"},
{"name":"Electron Blue","value":"#47848f"},
{"name":"Elixir Purple","value":"#6f42be"},
{"name":"ESLint Purple","value":"#4b32c3"},
{"name":"Facebook Blue","value":"#1877f2"},
{"name":"Fastify Blue","value":"#000000"},
{"name":"Firebase Orange","value":"#ffa400"},
{"name":"GitHub Green","value":"#08872b"},
{"name":"Go Cyan","value":"#00add8"},
{"name":"Google Blue","value":"#4285f4"},
{"name":"GraphQL Pink","value":"#e10098"},
{"name":"Haskell Purple","value":"#5e5086"},
{"name":"Instagram Pink","value":"#e4405f"},
{"name":"Intel Blue","value":"#0071c5"},
{"name":"Java Orange","value":"#007396"},
{"name":"JavaScript Yellow","value":"#f9e64f"},
{"name":"Jest Red","value":"#c21325"},
{"name":"Kotlin Purple","value":"#7f52ff"},
{"name":"Kubernetes Blue","value":"#326ce5"},
{"name":"Laravel Red","value":"#ff2d20"},
{"name":"LinkedIn Blue","value":"#0a66c2"},
{"name":"Lua Blue","value":"#000080"},
{"name":"Mandalorian Blue","value":"#1857a4"},
{"name":"MATLAB Orange","value":"#0071c5"},
{"name":"Meta Blue","value":"#1877f2"},
{"name":"Microsoft Blue","value":"#0078d4"},
{"name":"MongoDB Green","value":"#13aa52"},
{"name":"NestJS Red","value":"#ea2845"},
{"name":"Netflix Red","value":"#e50914"},
{"name":"Node Green","value":"#215732"},
{"name":"Node.js Green","value":"#68a063"},
{"name":"npm Red","value":"#cb3837"},
{"name":"Nuxt Green","value":"#00dc82"},
{"name":"Nvidia Green","value":"#76b900"},
{"name":"Perl Blue","value":"#0073a1"},
{"name":"PHP Purple","value":"#777bb4"},
{"name":"PostgreSQL Blue","value":"#336791"},
{"name":"Python Blue","value":"#3776ab"},
{"name":"R Blue","value":"#276dc3"},
{"name":"React Blue","value":"#61dafb"},
{"name":"Redis Red","value":"#dc382d"},
{"name":"Ruby Red","value":"#cc342d"},
{"name":"Rust Orange","value":"#ce422b"},
{"name":"Scala Red","value":"#dc322f"},
{"name":"Slack Purple","value":"#e01e5a"},
{"name":"Something Different","value":"#832561"},
{"name":"Spotify Green","value":"#1db954"},
{"name":"Spring Green","value":"#6db33f"},
{"name":"Svelte Orange","value":"#ff3d00"},
{"name":"Swift Orange","value":"#fa7343"},
{"name":"Tailwind Cyan","value":"#06b6d4"},
{"name":"Tesla Red","value":"#e82127"},
{"name":"Twitter Blue","value":"#1da1f2"},
{"name":"TypeScript Blue","value":"#3178c6"},
{"name":"Uber Black","value":"#000000"},
{"name":"Vue Green","value":"#42b883"},
{"name":"Webpack Blue","value":"#8dd6f9"},
{"name":"WhatsApp Green","value":"#25d366"},
{"name":"Yarn Blue","value":"#2c8ebb"}
]
}
這份清單包含了 74 種配色,涵蓋主流的程式語言、框架、工具和科技品牌。你可以直接複製整段 JSON 貼到你的設定檔中。
3. 使用配色
開啟專案後,按下 Ctrl+Shift+P(Mac: Cmd+Shift+P)開啟指令面板,輸入 “Peacock”,你會看到以下選項:
- Peacock: Change to a Favorite Color - 從收藏清單選擇顏色
- Peacock: Enter a Color - 輸入自訂顏色
- Peacock: Surprise Me with a Random Color - 隨機選擇顏色
- Peacock: Reset Colors - 重設為預設顏色
結語
Peacock 是一個看似簡單,但能大幅提升開發效率的工具。透過視覺化的顏色標記,我再也不會在錯誤的專案視窗中執行不對的指令,也能更快速地在多個專案間切換。
相關連結
專案概述
本文將探討如何使用 Rust 實作一個生產級的非同步任務佇列系統。這個專案展示了多個重要的 Rust 概念和系統設計原則,包括:
- 模組化設計與封裝
- Trait 系統的應用
- 並發安全與資料競爭處理
- 非同步程式設計 (Tokio)
- SQLite 資料持久化
- CLI 工具設計
專案原始碼: https://github.com/p47t/rust-52-projects/tree/master/async-job-queue
系統架構
核心組件
我們的任務佇列系統由三個主要模組組成:
async-job-queue/
├── src/
│ ├── job.rs # 任務定義與狀態管理
│ ├── storage.rs # SQLite 儲存層
│ ├── worker.rs # 工作池與任務處理
│ ├── lib.rs # 公開 API
│ └── bin/
│ ├── producer.rs # 生產者 CLI
│ └── worker.rs # 消費者 CLI
資料流程
Producer (CLI) → SQLite Database ← Worker Pool (多個 workers)
↓
[Pending Jobs]
↓
[Running Jobs]
↓
[Completed/Failed/Dead Letter]
Rust 核心概念解析
1. 模組可見性:mod vs pub mod
問題: 如何設計乾淨的公開 API?
在 lib.rs 中,我們使用私有模組配合公開重匯出:
// 私有模組 - 隱藏內部實作細節
mod job;
mod storage;
mod worker;
// 公開重匯出 - 只暴露需要的類型
pub use job::{Job, JobHandler, JobStatus, Priority};
pub use storage::{Storage, StorageError};
pub use worker::WorkerPool;
優點:
- ✅ 使用者只能透過
use async_job_queue::{Job, Storage} 匯入
- ✅ 內部模組結構可以自由重構而不影響公開 API
- ✅ 更好的封裝性,隱藏實作細節
替代方案(不推薦):
pub mod job; // 暴露整個模組,使用者可存取所有公開項目
2. Trait:定義行為抽象
JobHandler trait 定義了任務處理的介面:
pub trait JobHandler: Send + Sync {
fn handle(&self, payload: &[u8]) -> Result<(), String>;
}
設計考量:
Send + Sync:確保可以在執行緒間安全傳遞和共享
- 接受
&[u8] 而非具體類型:保持通用性,支援任何序列化格式
- 回傳
Result<(), String>:簡單的錯誤處理
使用範例:
struct EchoHandler;
impl JobHandler for EchoHandler {
fn handle(&self, payload: &[u8]) -> Result<(), String> {
let message = String::from_utf8_lossy(payload);
println!("處理任務:{}", message);
Ok(())
}
}
3. Newtype 模式的應用場景
討論: 應該使用 Vec<u8> 還是 JobPayload 新類型?
目前實作(Vec<u8>):
pub struct Job {
pub payload: Vec<u8>,
// ...
}
Newtype 模式:
pub struct JobPayload(Vec<u8>);
impl JobPayload {
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
impl From<String> for JobPayload {
fn from(s: String) -> Self {
Self(s.into_bytes())
}
}
何時使用 Newtype:
- ✅ 提供型別安全(避免混淆不同的
Vec<u8>)
- ✅ 零成本抽象(編譯時展開,無執行時開銷)
- ✅ API 更清晰(
Job::new(JobPayload, ...) vs Job::new(Vec<u8>, ...))
- ✅ 未來擴充性(可加入驗證、壓縮等功能)
目前專案的選擇:
保持 Vec<u8> 是合理的,因為:
- 系統設計為通用型,不關心具體格式
- API 介面簡單,不易混淆
- 遵循 YAGNI 原則(You Aren’t Gonna Need It)
4. 列舉型別與 Display Trait
問題: 如何為列舉實作 Display?
手動實作(目前方式):
impl fmt::Display for Priority {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Priority::Low => write!(f, "low"),
Priority::Normal => write!(f, "normal"),
Priority::High => write!(f, "high"),
Priority::Critical => write!(f, "critical"),
}
}
}
使用第三方 crate(strum):
use strum_macros::Display;
#[derive(Display)]
#[strum(serialize_all = "lowercase")]
pub enum Priority {
Low,
Normal,
High,
Critical,
}
建議: 對於簡單列舉,手動實作更清晰,無需額外依賴。
5. 非同步程式設計:Tokio 的角色
Tokio 在專案中的使用:
#[tokio::main] - 設定非同步執行環境
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// ...
}
tokio::spawn - 並發執行多個 worker
for worker_id in 0..self.num_workers {
let handle = tokio::spawn(async move {
worker_loop(worker_id, storage, handler, poll_interval).await;
});
handles.push(handle);
}
tokio::time::sleep - 非阻塞延遲
sleep(poll_interval).await; // 輪詢間隔
有趣的觀察:
這個專案實際上是「假非同步」!核心操作都是同步的:
- SQLite 操作使用
rusqlite(同步 API)
- 任務處理是同步函式
- 使用
std::sync::Mutex 而非 tokio::sync::Mutex
為什麼還要用 Tokio?
- 方便建立並發 worker(比
std::thread 更輕量)
- 非阻塞的 sleep(不會凍結整個執行緒)
- 為未來的非同步擴充做準備
6. std::sync::Mutex vs tokio::sync::Mutex
關鍵差異:
| 特性 |
std::sync::Mutex |
tokio::sync::Mutex |
| 等待鎖時 |
阻塞整個執行緒 |
讓出執行權給其他 task |
| 使用方式 |
mutex.lock().unwrap() |
mutex.lock().await |
| 適用場景 |
短時間持鎖、同步操作 |
長時間持鎖、跨 await |
| 效能 |
更快(作業系統級) |
稍慢(需協調排程) |
目前專案的正確選擇:
pub struct Storage {
conn: Arc<Mutex<Connection>>, // ✅ std::sync::Mutex
}
pub fn get_next_pending(&self) -> Result<Option<Job>, StorageError> {
let conn = self.conn.lock().unwrap();
// 執行快速的同步資料庫操作
// 沒有在持鎖期間 await
Ok(job)
}
何時必須使用 tokio::sync::Mutex:
// ❌ 錯誤:在持有 std::sync::Mutex 時 await
let guard = std_mutex.lock().unwrap();
async_operation().await; // 會阻塞整個執行緒!
// ✅ 正確:使用 tokio::sync::Mutex
let guard = tokio_mutex.lock().await;
async_operation().await; // OK,會讓出執行權
並發安全與資料競爭
發現的競爭條件問題
原始設計的漏洞:
// ❌ 有競爭條件的程式碼
pub fn get_next_pending(&self) -> Result<Option<Job>, StorageError> {
// 步驟 1:讀取待處理任務
let job = SELECT ... WHERE status = Pending LIMIT 1;
// ⚠️ 問題:Worker 2 可能在這裡也讀取到相同任務!
Ok(job)
}
// Worker 處理邏輯
let job = storage.get_next_pending()?; // 取得任務
job.mark_running(); // 標記為執行中
storage.update(&job)?; // 更新資料庫
時序圖展示問題:
| 時間 |
Worker 1 |
Worker 2 |
資料庫狀態 |
| T1 |
get_next_pending() |
|
Job A: Pending |
| T2 |
讀取 Job A |
|
|
| T3 |
|
get_next_pending() |
Job A: Pending |
| T4 |
|
讀取 Job A (相同!) |
|
| T5 |
mark_running() |
|
|
| T6 |
update(Job A) |
|
Job A: Running |
| T7 |
|
mark_running() |
|
| T8 |
|
update(Job A) |
Job A: Running |
| T9 |
處理 Job A |
處理 Job A |
❌ 重複處理! |
解決方案:原子性操作
修正後的實作:
pub fn get_next_pending(&self) -> Result<Option<Job>, StorageError> {
let conn = self.conn.lock().unwrap();
// ✅ 原子性的 UPDATE + RETURNING(一個 SQL 語句完成)
let mut stmt = conn.prepare(
"UPDATE jobs
SET status = ?1, updated_at = ?2
WHERE id = (
SELECT id FROM jobs
WHERE status = ?3
ORDER BY priority DESC, created_at ASC
LIMIT 1
)
RETURNING id, payload, priority, status, retry_count, max_retries,
created_at, updated_at, error_message",
)?;
let now = Utc::now().to_rfc3339();
let job = stmt
.query_row(
params![JobStatus::Running as i32, now, JobStatus::Pending as i32],
|row| Ok(self.row_to_job(row)?),
)
.optional()?;
Ok(job)
}
為什麼這樣可以解決問題:
- 原子性保證:
UPDATE ... RETURNING 是單一 SQL 語句
- Mutex 保護: 同一時間只有一個 worker 能執行此查詢
- 狀態立即改變: 任務在被回傳前就已標記為 Running
- 資料庫層級鎖定: SQLite 的 EXCLUSIVE 鎖確保寫入安全
修正後的時序:
| 時間 |
Worker 1 |
Worker 2 |
資料庫狀態 |
| T1 |
取得 Mutex |
|
|
| T2 |
UPDATE Job A → Running |
等待 Mutex |
Job A: Running |
| T3 |
釋放 Mutex,回傳 Job A |
|
|
| T4 |
|
取得 Mutex |
|
| T5 |
|
UPDATE (找不到 Pending) |
Job A: Running |
| T6 |
|
回傳 None |
|
| T7 |
處理 Job A |
等待下一輪 |
✅ 無重複處理 |
Worker 程式碼簡化
因為 get_next_pending() 已經原子性地標記任務,worker 程式碼更簡潔:
// ✅ 修正後
match storage.get_next_pending() {
Ok(Some(mut job)) => {
// 任務已經是 Running 狀態,直接處理
match handler.handle(&job.payload) {
Ok(()) => job.mark_completed(),
Err(e) => job.mark_failed(e),
}
storage.update(&job)?;
}
Ok(None) => {
sleep(poll_interval).await; // 無任務,等待
}
Err(e) => { /* 處理錯誤 */ }
}
日誌與除錯
tracing vs println!
何時使用 println!:
// ✅ 使用者導向的輸出
println!("Job ID: {}", job.id);
println!("Status: {}", job.status);
何時使用 tracing:
// ✅ 操作性日誌、除錯資訊
tracing::info!(job_id = %job.id, "Processing job");
tracing::error!("Database error: {}", e);
在 CLI 工具中:
producer 使用 println! 顯示命令結果(使用者期望的輸出)
worker 使用 tracing 記錄操作日誌(方便監控和除錯)
初始化 tracing
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 設定日誌格式與過濾級別
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive(tracing::Level::INFO.into()),
)
.init();
// ...
}
使用方式:
# 預設 INFO 級別
./worker
# 設定為 DEBUG 級別
RUST_LOG=debug ./worker
資料持久化設計
SQLite Schema
CREATE TABLE jobs (
id TEXT PRIMARY KEY,
payload BLOB NOT NULL,
priority INTEGER NOT NULL,
status INTEGER NOT NULL,
retry_count INTEGER NOT NULL,
max_retries INTEGER NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
error_message TEXT
);
CREATE INDEX idx_status_priority
ON jobs(status, priority DESC, created_at ASC);
索引設計考量:
status 在前:快速過濾待處理任務
priority DESC:高優先順序優先
created_at ASC:相同優先順序時,先進先出 (FIFO)
狀態轉換
┌─────────────┐
│ Pending │
└──────┬──────┘
│
↓
┌──────────────┐
│ Running │
└──────┬───────┘
│
┌───────┴────────┐
│ │
↓ (成功) ↓ (失敗)
┌───────────┐ ┌─────────┐
│ Completed │ │ Failed │
└───────────┘ └────┬────┘
│
┌───────┴────────┐
│ │
↓ (可重試) ↓ (超過最大重試)
[Pending] ┌──────────────┐
(retry_count++) │ DeadLetter │
└──────────────┘
失敗處理:
pub fn mark_failed(&mut self, error: String) {
self.status = if self.can_retry() {
self.retry_count += 1;
JobStatus::Pending // 重新排程
} else {
JobStatus::DeadLetter // 移至死信佇列
};
self.error_message = Some(error);
self.updated_at = Utc::now();
}
CLI 設計
Producer(生產者)
# 提交任務
producer submit -p "Hello, World!" -r high -m 3
# 查詢狀態
producer status --job-id <uuid>
# 統計資料
producer stats
使用 Clap 實作:
#[derive(Parser)]
#[command(name = "producer")]
#[command(about = "Job queue producer - submit and manage jobs")]
struct Cli {
#[arg(short, long, default_value = "jobs.db")]
database: String,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Submit {
#[arg(short, long)]
payload: String,
#[arg(short = 'r', long, default_value = "normal")]
priority: String,
#[arg(short, long, default_value = "3")]
max_retries: u32,
},
Status { /* ... */ },
Stats,
}
Worker(消費者)
# 啟動 4 個 workers
worker -w 4
# 使用不同的資料庫
worker --database /path/to/jobs.db --workers 8
效能考量
輪詢 vs 事件驅動
目前實作(輪詢):
loop {
match storage.get_next_pending() {
Ok(Some(job)) => { /* 處理 */ }
Ok(None) => sleep(poll_interval).await, // 等待 1 秒
Err(e) => { /* 錯誤處理 */ }
}
}
優點:
- ✅ 實作簡單
- ✅ 可靠性高(無需額外機制)
- ✅ 易於理解和維護
缺點:
- ❌ 延遲:最多
poll_interval 的延遲
- ❌ 資源浪費:無任務時仍在輪詢
改進方案(事件驅動):
可以使用 tokio::sync::Notify 或資料庫的 NOTIFY/LISTEN:
// 生產者插入任務後通知
notifier.notify_waiters();
// Worker 等待通知
notifier.notified().await;
指數退避(Exponential Backoff)
if job.retry_count > 0 {
let backoff = Duration::from_secs(2_u64.pow(job.retry_count.min(5)));
sleep(backoff).await;
}
退避時間:
- 第 1 次重試:2^1 = 2 秒
- 第 2 次重試:2^2 = 4 秒
- 第 3 次重試:2^3 = 8 秒
- 第 4 次重試:2^4 = 16 秒
- 第 5 次以上:2^5 = 32 秒(最大值)
目的: 避免快速重試造成系統負擔,給下游服務恢復時間。
最佳實踐總結
1. 模組設計
- 使用私有模組 + 公開重匯出控制 API
- 清晰的關注點分離
2. 型別安全
- 考慮使用 newtype 模式增加型別安全
- 平衡抽象與簡潔性
3. 並發處理
- 識別臨界區(critical section)
- 使用原子操作避免競爭條件
- 選擇適當的同步原語(Mutex、RwLock 等)
4. 錯誤處理
- 使用
thiserror 定義清晰的錯誤類型
- 區分可恢復與不可恢復的錯誤
5. 日誌策略
- CLI 輸出用
println!
- 操作日誌用
tracing
- 使用環境變數控制日誌級別
6. 非同步選擇
- 評估是否真的需要 async/await
- 混合使用同步與非同步程式碼時要小心
- 理解
std::sync 與 tokio::sync 的差異
擴充方向
-
真正的非同步化:
- 使用
sqlx 或 tokio-rusqlite
- 非同步的
JobHandler trait
-
分散式支援:
- 使用 PostgreSQL 的
SELECT FOR UPDATE SKIP LOCKED
- Redis 作為訊息佇列
-
監控與觀測:
-
進階功能:
- 延遲任務(scheduled jobs)
- 任務優先順序調整
- 動態 worker 擴縮容
結論
這個專案展示了如何使用 Rust 構建可靠的系統軟體。關鍵要點:
- 型別系統讓我們在編譯期捕獲許多錯誤
- 所有權模型確保記憶體安全與執行緒安全
- Trait 系統提供零成本抽象
- 並發原語幫助我們正確處理競爭條件
Rust 的「如果編譯通過,通常就能正確運行」的特性,讓我們能夠自信地構建複雜的並發系統。
這兩篇文章確實形成了一個有趣且互補的觀點組合:
核心互補性
Jason Gorman 的文章(Codemanship)強調:
- AI 不會取代程式設計師
- 程式設計的難點在於將人類思維轉化為精確的計算思維
- LLM 只是模式匹配,不真正理解代碼
Steve Krenzel 的文章(Bits of Logic)則說明:
- AI 代理需要高品質的代碼環境才能有效工作
- 良好的實踐(測試、類型、文檔)從「可選」變成「必需」
- AI 會「放大代碼庫最糟糕的傾向」
三個層次的互補
1. 戰略層面的一致性
兩位作者都認同:人類程式設計師仍然是核心
- Gorman:「當事情重要時,軟體開發人員會掌舵」
- Krenzel:「代理只有在你為它們創造的環境中才有效」
這不是「AI vs 人類」的零和遊戲,而是人類定義規則,AI 在規則內執行。
2. 技術層面的呼應
Gorman 指出 LLM 的根本限制:
- 不理解代碼,只做統計預測
- 相同提示產生不同結果
- 需要人類驗證輸出
Krenzel 的解決方案正好針對這些限制:
- 100% 代碼覆蓋率:強制 AI 用可執行範例證明每一行
- 強類型系統:縮小 AI 的搜索空間,消除非法狀態
- 快速反饋循環:保持 AI「在短繩上」,小改動→檢查→修復
這實際上是在說:既然 AI 不理解代碼,我們就用機械化的護欄來約束它。
3. 實踐層面的轉化
Gorman 的悲觀預測:
「LLM 實際上讓大多數團隊變慢,軟體更不可靠、更難維護」
Krenzel 提供了避免這種情況的路徑:
- 不是讓 AI 自由發揮,而是建立嚴格的自動化護欄
- 不是期待 AI 理解業務邏輯,而是通過類型和測試外化邏輯
- 不是一次性生成大量代碼,而是快速迭代、持續驗證
深層洞察:AI 作為「放大器」
兩篇文章共同揭示了一個關鍵真理:
AI 不是替代品,而是放大器
- 如果你的代碼庫混亂、測試不足、類型鬆散 → AI 會放大這些問題(Gorman 的警告)
- 如果你的代碼庫結構良好、測試完整、類型嚴格 → AI 會放大生產力(Krenzel 的承諾)
這解釋了為什麼:
- Gorman 看到的失敗案例:團隊試圖用 AI 彌補技術債,結果債務加倍
- Krenzel 看到的成功案例:團隊先支付「技術稅」,然後用 AI 收穫紅利
對實踐的啟示
這兩篇文章合起來給出了一個完整的策略:
短期(現在)
- 不要裁員程式設計師(Gorman 的建議)
- 投資於代碼品質基礎設施(Krenzel 的路線圖)
- 建立 100% 測試覆蓋率
- 遷移到強類型語言
- 自動化所有可自動化的檢查
中期(1-3 年)
- 將 AI 視為「受監督的實習生」
- 可以處理重複性任務
- 需要明確的規則和即時反饋
- 不能做架構決策
長期(3+ 年)
- 重新定義「程式設計師」的角色
- 從「寫代碼」轉向「設計系統和護欄」
- 從「實現功能」轉向「定義不變量和約束」
- 更像「系統架構師 + 品質工程師」的混合體
一個有趣的悖論
Krenzel 的文章標題是「AI 正在迫使我們寫好代碼」,但實際上:
AI 沒有迫使任何人做任何事
真正發生的是:
- 那些已經想寫好代碼的團隊,發現 AI 給了他們一個絕佳的理由去說服管理層投資
- 那些不想投資品質的團隊,會發現 AI 讓情況變得更糟
這與 Gorman 的觀察完全一致:AI 不會改變基本面,只會加速既有趨勢。
結論
這兩篇文章不是矛盾的,而是同一枚硬幣的兩面:
- Gorman 告訴我們「為什麼」:為什麼 AI 不會取代程式設計師(因為難點不在語法)
- Krenzel 告訴我們「如何」:如何讓 AI 成為有效的工具(通過建立嚴格的環境)
合在一起,它們描繪了一個清晰的未來:更多的程式設計師,寫更好的代碼,用 AI 處理繁瑣的部分。
但這個未來不會自動到來——它需要有意識的投資和紀律。那些理解這一點的團隊會繁榮,那些不理解的會掙扎。
您的團隊目前在這個光譜的哪個位置?您認為哪些「技術稅」最值得優先支付?