模組五|建置與部署(Day 20–24)
模組五收尾,來做一個注定失敗的實驗。我手上另一個專案(claude-mobile,不在本系列 repo 裡)做過一個 three.js r128 的捲動式登陸頁:使用者往下捲,3D 場景的相機跟著推軌,文案一段段浮出——典型的 scroll-driven narrative。這種頁面是前端炫技首選,也是檢驗 Flutter Web 邊界的好題目:它整頁都是 canvas 繪製,理論上該是 Flutter 的主場,對吧?
那個 landing page 的骨架很標準:three.js 管 WebGL 場景,捲動進度當作動畫時間軸,DOM 文案層用 CSS 疊在 canvas 上面。核心迴圈十行講完:
// three.js r128 時代的寫法(claude-mobile 專案,此處以敘述重現)
const scrollProgress = () =>
window.scrollY / (document.body.scrollHeight - window.innerHeight);
function animate() {
const t = scrollProgress(); // 0 → 1
camera.position.z = lerp(12, 4, t); // 相機推軌
camera.position.y = lerp(0, 2.5, easeOut(t));
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
但生態才是這套玩法的真正主角:three.js 十幾年累積的範例庫、GLTF 載入器、shader 素材,加上 DOM 文案層天生可選取、可被爬蟲讀(登陸頁通常需要 SEO)。Vue 在這裡只是殼,重活是 three.js 和瀏覽器合力扛的。
搬到 Flutter Web,選項盤點如下,每一條我都讓 agent 探過路:
**路線一:找 three.js 的 Dart 等價物。**社群有 three_dart、flutter_gl 這類移植,但版本落後 three.js 主線一大截、維護時斷時續。agent 很快撞牆:它可以流暢地翻譯相機推軌的數學,但套件缺件時,強型別編譯器只會誠實地告訴你「這個 API 不存在」。Day 1 獨家論點的反面在這裡現形——agent 翻得動 code,翻不動生態。
**路線二:HtmlElementView 開洞,把 three.js 原封不動嵌回來。**技術上可行,但想清楚這是什麼:在 Flutter 的 canvas 裡挖一個洞、塞回一個 DOM 元素,裡面再跑一個 WebGL canvas,兩套渲染各畫各的,捲動事件還要跨界同步。為了用 Flutter 而把頁面主體交還給 JS——那當初為什麼要離開 Vue?
**路線三:放棄 3D,用 Flutter 自己的管線重做捲動敘事。**這條反而有驚喜。捲動驅動動畫在 Flutter 是 first-class 公民:
// ScrollController 驅動動畫進度,體感比監聽 window.scrollY 乾淨
class _LandingState extends State<Landing> {
final _scroll = ScrollController();
double get progress => _scroll.hasClients
? (_scroll.offset / _scroll.position.maxScrollExtent).clamp(0.0, 1.0)
: 0.0;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _scroll,
builder: (_, __) => CustomPaint(
painter: HeroPainter(progress), // 2D 場景照 progress 重繪
child: buildCopyLayer(progress), // 文案層跟著同一個進度走
),
);
}
}
ScrollController、Transform、CustomPaint 這套組合拳,打 2D 視差、漸變、路徑動畫都很順——但就是沒有 3D 場景圖。WebGL 等級的內容,Flutter Web 目前沒有可靠的原生答案。
一句話:Flutter Web 搬不動 three.js 的生態,這個實驗的價值不在成品,在於把「canvas 主場」論點的邊界實際量了出來。模組五到此收工,Day 25 進入最兇的戰場:SEO/SSR——先回顧 Nuxt 的 SSR/SSG 原理,再看 Flutter Web 拿什麼接招。