The Rust Crate
สิ่งที่จะสร้าง
หัวข้อที่มีชื่อว่า “สิ่งที่จะสร้าง”ครึ่ง Rust ของ OfflineNotes และ pipeline ที่พาเข้ามาใน browser ในบทนี้ crates/crdt กลายเป็น crate จริงที่มี exported function ตัวเล็ก ๆ ตัวเดียว ชื่อ crdt_version, wasm-pack compile เป็น WebAssembly module ใน crates/crdt/pkg, และ apps/web import module นั้นแล้วเรียกใช้ function ยังไม่มี CRDT — ประเด็นคือพิสูจน์เส้นทาง Rust → WASM → JavaScript ให้ครบตั้งแต่ต้นจนจบ ก่อนที่ logic จริงจะมาพึ่ง
CRDT engine ต้อง เหมือนกันแบบ byte-for-byte ในทุก client เพราะสองเครื่องที่ merge op ชุดเดียวกันต้องได้ผลลัพธ์เดียวกัน การเขียนครั้งเดียวใน Rust แล้ว compile เป็น WASM ให้แกนกลางที่ portable, เร็ว, และ test ได้ตัวเดียว แทนที่จะ reimplement ให้ต่างกันนิด ๆ ต่อ platform แต่แกนกลางนั้นไร้ค่าจนกว่า toolchain รอบ ๆ จะน่าเบื่อและเชื่อถือได้ — เราจึง build ทั้ง pipeline ตอนนี้ กับ function ที่แค่ return version string ที่ซึ่งความผิดพลาดเห็นชัดและราคาถูก
wasm-bindgengenerate glue ที่ให้ JavaScript เรียก Rust และส่ง type จริงข้าม boundary ได้ (ไม่ใช่แค่ตัวเลข)wasm-packขับทั้ง build: compile crate เป็นwasm32-unknown-unknown, รันwasm-bindgen, แล้วปล่อยแพ็กเกจที่พร้อม import (.wasm+.js+.d.ts)--target webผลิต output แบบ ES-module ที่มี functioninit()ซึ่งคุณawaitก่อนเรียกอะไรก็ตาม — ตรงกับที่ Astro/Vite และ browser ต้องการพอดี โดยไม่ต้องมี loader เฉพาะ bundler
ข้อดีข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีข้อเสีย”Rust → WASM สำหรับ engine เทียบกับเขียน CRDT ด้วย TypeScript
- Pros: มี implementation ที่เชื่อถือได้ตัวเดียวใช้ร่วมกันทุก client; ownership model ของ Rust ทำให้การจดบัญชี tombstone/id ผิดแบบเนียน ๆ ได้ยาก; ความเร็ว merge เกือบเท่า native
- Cons: มี toolchain และภาษาตัวที่สองใน repo; มี boundary JS↔WASM ที่คุณต้อง marshal data ข้าม; มี build step (
wasm-pack) ที่ต้องรันก่อนแอป
wasm-pack เทียบกับการขับ cargo + wasm-bindgen-cli เอง
- Pros: คำสั่งเดียวทำ compile + bindgen + packaging; install target
wasm32ให้คุณ; ปล่อย TypeScript declaration ให้อัตโนมัติ - Cons: มี global tool อีกตัวที่ต้อง install; ซ่อนขั้นตอนที่คุณอาจต้องเข้าใจในที่สุด; และคุณต้องยึดตามความเห็นของ tool เรื่อง layout ของ output
ติดตั้ง
หัวข้อที่มีชื่อว่า “ติดตั้ง”1. crates/crdt/Cargo.toml
หัวข้อที่มีชื่อว่า “1. crates/crdt/Cargo.toml”ประกาศ crate crate-type = ["cdylib"] คือสิ่งที่ทำให้ cargo ปล่อย WebAssembly dynamic library; การเพิ่ม "rlib" ให้เรารัน cargo test ธรรมดาบน host ได้ด้วยภายหลัง (Module 6) ที่ซึ่ง CRDT logic test ได้ง่ายกว่าผ่าน browser มาก
[package]name = "crdt"version = "0.1.0"edition = "2021"
[lib]crate-type = ["cdylib", "rlib"]
[dependencies]wasm-bindgen = "0.2"serde และ serde-wasm-bindgen จะมาเข้าลิสต์นี้เมื่อเราส่ง structured op ข้าม boundary — สำหรับ function ที่ return String แค่ wasm-bindgen ก็พอ
2. crates/crdt/src/lib.rs
หัวข้อที่มีชื่อว่า “2. crates/crdt/src/lib.rs”เรามี exported function หนึ่งตัว #[wasm_bindgen] บอก macro ให้ generate JS binding ให้; env!("CARGO_PKG_VERSION") อ่าน version จาก Cargo.toml ตอน compile ดังนั้นการเรียกสำเร็จพิสูจน์ว่า Rust code จริงรันใน browser
use wasm_bindgen::prelude::*;
/// A trivial exported function — just to prove the Rust → WASM → JS path works./// The real CRDT (`NoteDoc`) arrives in Module 6.#[wasm_bindgen]pub fn crdt_version() -> String { format!("crdt {}", env!("CARGO_PKG_VERSION"))}3. Root package.json — a build:wasm script
หัวข้อที่มีชื่อว่า “3. Root package.json — a build:wasm script”กลับมาที่ root ของ repo เพิ่ม WASM build แล้วทำให้ dev/build ขึ้นกับขั้นนั้น เพื่อให้ pkg มีอยู่เสมอก่อนที่ web app จะพยายาม import:
{ "name": "offlinenotes", "private": true, "version": "0.0.0", "scripts": { "build:wasm": "wasm-pack build crates/crdt --target web", "dev": "pnpm build:wasm && pnpm --filter web dev", "build": "pnpm build:wasm && pnpm --filter web build" }}wasm-pack build crates/crdt --target web compile crate แล้วเขียน output ไปที่ crates/crdt/pkg/ (--out-dir ค่าดีฟอลต์) รอบแรก wasm-pack จะ install target wasm32-unknown-unknown ให้คุณ
4. apps/web/astro.config.mjs — an alias to pkg
หัวข้อที่มีชื่อว่า “4. apps/web/astro.config.mjs — an alias to pkg”module ที่ compile แล้วอยู่นอก apps/web จึงตั้งชื่อ import สะอาด ๆ ให้แทน path ../../../.. ที่เปราะ Vite alias จัดการเรื่องนี้ตอน build:
import { defineConfig } from 'astro/config';import { fileURLToPath } from 'node:url';
export default defineConfig({ vite: { resolve: { alias: { '@crdt': fileURLToPath(new URL('../../crates/crdt/pkg', import.meta.url)), }, }, },});เพื่อให้ editor/type-checker รู้จักชื่อเดียวกัน เพิ่ม entry paths ลงใน apps/web/tsconfig.json (Vite จัดการตอนรัน; TypeScript ต้องมี map ของตัวเอง):
{ "extends": "astro/tsconfigs/strict", "compilerOptions": { "paths": { "@crdt/*": ["../../crates/crdt/pkg/*"] } }}5. apps/web/src/pages/index.astro — call across the boundary
หัวข้อที่มีชื่อว่า “5. apps/web/src/pages/index.astro — call across the boundary”แทนที่หน้า scaffold ด้วยหน้าที่ import module แล้วเรียกใช้งาน ด้วย --target web คุณต้อง await init() (ที่ fetch และ instantiate .wasm) ก่อน exported function ตัวใด ๆ Vite resolve ไฟล์ .wasm ที่อยู่ข้างกันให้อัตโนมัติ
------<h1>OfflineNotes</h1>
<script> import init, { crdt_version } from '@crdt/crdt.js';
await init(); console.log('WASM says:', crdt_version());</script>หน้านี้เป็น smoke test แบบใช้แล้วทิ้ง — module App Shell จะแทนที่ด้วย UI จริง
6. .gitignore — ignore build output
หัวข้อที่มีชื่อว่า “6. .gitignore — ignore build output”ไดเรกทอรี pkg/ และ target/ เป็นของที่ generate ขึ้นมา เพิ่มเข้า ignore เพื่อไม่ให้ commit ตามไป:
crates/crdt/pkgcrates/crdt/targetตรวจสอบผล
หัวข้อที่มีชื่อว่า “ตรวจสอบผล”build WASM package จาก root ของ repo:
pnpm build:wasmที่คาดว่าจะเห็น — wasm-pack compile แล้วรายงานว่าสำเร็จ:
[INFO]: Compiling to Wasm...[INFO]: :-) Done in Xs[INFO]: :-) Your wasm pkg is ready to publish at .../crates/crdt/pkg.ยืนยันว่า output มีอยู่:
ls crates/crdt/pkgที่คาดว่าจะเห็น — glue, binary และ types:
crdt.js crdt.d.ts crdt_bg.wasm crdt_bg.wasm.d.ts package.jsonทีนี้รันแอป (ซึ่ง rebuild WASM ก่อน แล้วค่อยเริ่ม Astro):
pnpm devเปิด http://localhost:4321/, เปิด DevTools → Console แล้วยืนยันบรรทัดที่ print ออกมาจาก Rust:
WASM says: crdt 0.1.0การเห็น string นั้นคือ run check: Rust compile เป็น WASM, module load และ initialize ใน browser และ JavaScript เรียกเข้าไปได้สำเร็จ
Check your understanding:
- ทำไมคุณต้อง
await init()ก่อนเรียกcrdt_version()? จริง ๆ แล้วinit()ทำอะไร? crate-type = ["cdylib", "rlib"]ให้อะไรกับคุณที่["cdylib"]ลำพังให้ไม่ได้?- ทำไมต้อง alias
@crdtทั้งในastro.config.mjsและtsconfig.json— แต่ละตัวแก้ปัญหาอะไร? - บทนี้ export แค่ function ตัวเล็ก ๆ ทำไมถึง build ทั้ง toolchain ตอนนี้ แทนที่จะรอตอนที่ CRDT จริงมีขึ้นมา?
crates/crdt เป็น Rust crate จริงแล้ว, wasm-pack build --target web compile ออกมาเป็น crates/crdt/pkg, และ apps/web import module นั้นผ่าน alias @crdt แล้วรัน Rust code ใน browser pipeline ที่จะพา CRDT ต่อไปได้รับการพิสูจน์แล้วและน่าเบื่อ
นั่นจบ Setup & Tooling ต่อไป สร้าง Astro shell และ custom elements ตัวแรกที่ประกอบเป็น notes UI: App Shell →