Simply Patrick

用 Rust 和 WebAssembly 打造即時 Markdown 編輯器

這個專案是我「52 個 Rust 專案」學習計畫的一部分。在完成了 16 個專案後,我發現 WebAssembly 是一個重要的學習缺口,於是決定用 Rust 來打造一個即時 Markdown 編輯器。

專案原始碼:wasm-markdown-editor

為什麼選擇這個專案?

在分析了之前完成的專案後,我發現幾個學習上的空白:

  • 已掌握的領域:Async/await、網路程式設計(TCP/UDP/HTTP/WebSockets)、解析器、CLI 工具、錯誤處理
  • 待加強的領域:資料庫 ORM、程序宏、FFI、WebAssembly、進階測試、GUI/圖形

選擇 Markdown 編輯器的原因:

  1. 填補關鍵缺口:之前沒有任何 WASM 專案
  2. 善用既有技能:運用之前在計算機、shell、EBML 等專案中學到的解析技巧
  3. 互動性強:能立即看到成果,滿足感高
  4. 實用價值:這是真正能用的工具,不只是 demo
  5. 現代技術棧: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. 釋放記憶體

字串傳遞流程

  1. JS 字串寫入 WASM 線性記憶體
  2. 傳遞指標和長度給 Rust 函式
  3. Rust 處理資料
  4. Rust 將結果寫入記憶體
  5. 回傳結果的指標給 JS
  6. JS 從記憶體讀取結果字串
  7. 釋放不再需要的記憶體

型別轉換對照表

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 特定技能

  1. ✅ 理解 cdylib crate 類型及其作用
  2. ✅ 使用 #[wasm_bindgen] 屬性匯出給 JS
  3. ✅ 管理 Rust/JavaScript 邊界的記憶體
  4. ✅ 型別轉換(簡單型別 vs. 複雜結構)
  5. ✅ WASM 模組初始化模式
  6. ✅ 在瀏覽器 DevTools 中除錯 WASM
  7. ✅ 套件大小優化技術
  8. ✅ 建置工具(wasm-packwasm-bindgen

什麼時候該用 WASM?

適合的場景

  • ✅ 計算密集型操作(解析、圖像處理)
  • ✅ 想重用現有的 Rust 函式庫
  • ✅ 效能關鍵路徑
  • ✅ 套件大小可接受時

不太適合的場景

  • ❌ 大量 DOM 操作(用 JS)
  • ❌ 微小的工具函式(開銷不值得)
  • ❌ 簡單的 CRUD 操作
  • ❌ 套件大小是關鍵考量時

與純 JavaScript 方案的比較

效能

  • Markdown 解析:比 JS 替代方案快約 2-3 倍
  • 打字時沒有 GC 暫停
  • 可預測的記憶體使用

套件大小

  • 與流行的 JS 函式庫相當或更小
  • markdown-it.js:約 320 KB
  • 我們的方案:235 KB(包含解析器 + 統計)

反思

順利的部分

  1. 流暢的建置過程:wasm-pack「直接就能用」
  2. 優秀的文件:Rust WASM book 非常有價值
  3. 型別安全:編譯時就能捕捉錯誤
  4. 效能:解析明顯流暢
  5. 工具鏈:自動產生的 TypeScript 定義很有幫助

克服的挑戰

  1. 路徑解析:404 錯誤需要理解 WASM 服務方式
  2. 記憶體模型:理解線性記憶體花了一些時間
  3. 型別轉換:學習何時用 JsValue vs. 簡單型別
  4. 非同步初始化:理解 init() 的必要性

驚喜的發現

  1. 套件大小:比預期小(222 KB 含完整解析器!)
  2. 建置速度:增量建置很快(8 秒)
  3. 瀏覽器支援:所有現代瀏覽器都能用,不需要 polyfill
  4. 開發體驗:在 DevTools 中除錯 WASM 相當不錯

結論

這個專案成功填補了我 rust-52-projects 學習旅程中的一個主要缺口。它證明了 Rust + WASM 已經可以用於生產環境的互動式 Web 應用程式,尤其是像解析這樣的計算密集型任務。

Rust 的效能和安全性與 JavaScript 的普及性結合,創造了一個強大的開發模式。工具鏈(wasm-packwasm-bindgen)已經成熟到體驗流暢且高效的程度。

這是一個很好的「第一個 WASM 專案」,在學習核心概念的同時建構出真正實用的東西。

參考資源


我的 VSCode Peacock 配色收藏

什麼是 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 實作一個生產級的非同步任務佇列系統。這個專案展示了多個重要的 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&lt;(), String&gt; {
        let message = String::from_utf8_lossy(payload);
        println!("處理任務:{}", message);
        Ok(())
    }
}

3. Newtype 模式的應用場景

討論: 應該使用 Vec<u8> 還是 JobPayload 新類型?

目前實作(Vec<u8>):

pub struct Job {
    pub payload: Vec&lt;u8&gt;,
    // ...
}

Newtype 模式:

pub struct JobPayload(Vec&lt;u8&gt;);

impl JobPayload {
    pub fn new(bytes: Vec&lt;u8&gt;) -> Self {
        Self(bytes)
    }
    
    pub fn as_bytes(&self) -> &[u8] {
        &self.0
    }
}

impl From&lt;String&gt; 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&lt;'_&gt;) -> 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 在專案中的使用:

  1. #[tokio::main] - 設定非同步執行環境
#[tokio::main]
async fn main() -> Result&lt;(), Box&lt;dyn std::error::Error&gt;&gt; {
    // ...
}
  1. 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);
}
  1. 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&lt;Mutex&lt;Connection&gt;&gt;,  // ✅ std::sync::Mutex
}

pub fn get_next_pending(&self) -&gt; Result&lt;Option&lt;Job&gt;, StorageError&gt; {
    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) -&gt; Result&lt;Option&lt;Job&gt;, StorageError&gt; {
    // 步驟 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) -&gt; Result&lt;Option&lt;Job&gt;, StorageError&gt; {
    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)
}

為什麼這樣可以解決問題:

  1. 原子性保證: UPDATE ... RETURNING 是單一 SQL 語句
  2. Mutex 保護: 同一時間只有一個 worker 能執行此查詢
  3. 狀態立即改變: 任務在被回傳前就已標記為 Running
  4. 資料庫層級鎖定: 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() -&gt; Result&lt;(), Box&lt;dyn std::error::Error&gt;&gt; {
    // 設定日誌格式與過濾級別
    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 &lt;uuid&gt;

# 統計資料
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::synctokio::sync 的差異

擴充方向

  1. 真正的非同步化:

    • 使用 sqlxtokio-rusqlite
    • 非同步的 JobHandler trait
  2. 分散式支援:

    • 使用 PostgreSQL 的 SELECT FOR UPDATE SKIP LOCKED
    • Redis 作為訊息佇列
  3. 監控與觀測:

    • Prometheus metrics
    • 分散式追蹤
  4. 進階功能:

    • 延遲任務(scheduled jobs)
    • 任務優先順序調整
    • 動態 worker 擴縮容

結論

這個專案展示了如何使用 Rust 構建可靠的系統軟體。關鍵要點:

  • 型別系統讓我們在編譯期捕獲許多錯誤
  • 所有權模型確保記憶體安全與執行緒安全
  • Trait 系統提供零成本抽象
  • 並發原語幫助我們正確處理競爭條件

Rust 的「如果編譯通過,通常就能正確運行」的特性,讓我們能夠自信地構建複雜的並發系統。


The future of AI coding

這兩篇文章確實形成了一個有趣且互補的觀點組合:

核心互補性

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 的承諾)

這解釋了為什麼:

  1. Gorman 看到的失敗案例:團隊試圖用 AI 彌補技術債,結果債務加倍
  2. Krenzel 看到的成功案例:團隊先支付「技術稅」,然後用 AI 收穫紅利

對實踐的啟示

這兩篇文章合起來給出了一個完整的策略:

短期(現在)

  1. 不要裁員程式設計師(Gorman 的建議)
  2. 投資於代碼品質基礎設施(Krenzel 的路線圖)
    • 建立 100% 測試覆蓋率
    • 遷移到強類型語言
    • 自動化所有可自動化的檢查

中期(1-3 年)

  1. 將 AI 視為「受監督的實習生」
    • 可以處理重複性任務
    • 需要明確的規則和即時反饋
    • 不能做架構決策

長期(3+ 年)

  1. 重新定義「程式設計師」的角色
    • 從「寫代碼」轉向「設計系統和護欄」
    • 從「實現功能」轉向「定義不變量和約束」
    • 更像「系統架構師 + 品質工程師」的混合體

一個有趣的悖論

Krenzel 的文章標題是「AI 正在迫使我們寫好代碼」,但實際上:

AI 沒有迫使任何人做任何事

真正發生的是:

  • 那些已經想寫好代碼的團隊,發現 AI 給了他們一個絕佳的理由去說服管理層投資
  • 那些不想投資品質的團隊,會發現 AI 讓情況變得更糟

這與 Gorman 的觀察完全一致:AI 不會改變基本面,只會加速既有趨勢

結論

這兩篇文章不是矛盾的,而是同一枚硬幣的兩面:

  • Gorman 告訴我們「為什麼」:為什麼 AI 不會取代程式設計師(因為難點不在語法)
  • Krenzel 告訴我們「如何」:如何讓 AI 成為有效的工具(通過建立嚴格的環境)

合在一起,它們描繪了一個清晰的未來:更多的程式設計師,寫更好的代碼,用 AI 處理繁瑣的部分

但這個未來不會自動到來——它需要有意識的投資和紀律。那些理解這一點的團隊會繁榮,那些不理解的會掙扎。

您的團隊目前在這個光譜的哪個位置?您認為哪些「技術稅」最值得優先支付?


試用 Gemini 3 Pro

我使用 Gemini 3 Pro 和 Antigravity IDE 建立了 https://github.com/p47t/rec.2020。你可以在 https://p47t.github.io/rec.2020/ 查看已建立的應用程式。

rec.2020.png

以下是建構 https://github.com/p47t/rec.2020 的過程:

  1. 專案初始化:我首先要求 Gemini「建立一個視覺化 Rec.2020 色域的 web 應用程式」。它立即使用基本的 canvas 實作建立了專案檔案。
  2. 改進:接著我要求更多改進來優化使用者介面。
  3. 互動功能:為了讓它更實用,我請求「在滑鼠移動時顯示座標」,Gemini 完美地實作了這個功能,並正確處理了座標轉換。
  4. 色彩準確度:我還要求修正不正確的 sRGB 色域檢查,並繪製 sRGB 三角形以便於驗證。Gemini 理解色彩科學的需求,並實作了正確的數學轉換,以便在 Rec.2020 空間內顯示 sRGB 三角形。
  5. 部署:最後,我要求一份「部署指南」,它便生成了一份包含詳細說明的 DEPLOY.md
  6. 錯誤修復:我發現了另一個由畫布縮放顯示引起的問題,導致顏色選擇不準確。Gemini 識別出了問題,並修復了座標計算以支援調整視窗大小。

整個過程非常順暢。我幾乎不需要寫任何樣板程式碼。Gemini 3 Pro 同樣出色地理解高層次的意圖和複雜的領域知識(如色彩空間)。