跳至主要內容

開發紀錄

靜態 Astro 頁面如何保留 Canvas、GSAP 與局部互動

從本站保留的 Canvas 實作、目前的 GSAP 換頁效果與作品分類,說明靜態 HTML 和瀏覽器程式如何分工,以及為什麼不必為了局部互動接回整個 Vue App。

林予晴
發布
  • Astro
  • JavaScript
  • GSAP
  • 漸進增強

把網站改成靜態輸出後,作品分類仍然可以切換,圖片可以放大,換頁前也能播放動畫。這些行為需要瀏覽器程式,但不因此需要把整個頁面重新包成 Vue App。

我在這次架構裡採用的判準是:先讓文件交付內容,再讓每個互動取得自己需要的 DOM 和資源。判斷要保留什麼時,看的是效果依賴哪些能力,不是它以前寫在哪一種元件裡。

這篇以 2026 年 10 月 20 日讀到的實作為範圍。有一個現況必須先說清楚:本站目前共用背景是 CSS/SVG 工程網格,瀏覽器腳本更新它的 CSS 變數;早先的 Canvas 流線元件仍保留在程式裡,但沒有掛進目前的共用版型。下面用它說明可獨立的繪圖責任,不把保留檔案當成目前畫面。

靜態輸出決定內容何時產生,不禁止瀏覽器程式

Astro 元件可以先產生 HTML,再透過元件內的 script 載入瀏覽器程式,處理事件和動畫;不必為了這些行為先加入 UI framework。[1]

兩者執行的時間與取得的資料不同。本站的作品標題、圖片、分類入口及 Demo 連結,先由資料與元件產生;作品頁的腳本再尋找頁面根節點,處理分類顯示和圖牆排列。瀏覽器不是從一個空容器開始,等腳本跑完才知道有哪些作品。

這也留下較清楚的失敗邊界。JavaScript 沒有執行時,分類不會變成增強後的單一區塊切換,圖牆也不會進行腳本排版;但初始文件裡仍有作品文字、圖片和主要連結。圖片放大等腳本操作則不能因為縮圖已存在,就宣稱沒有 JavaScript 也能使用。

Canvas 的初始化函式,不必認識整個網站

保留的流線實作以 initVectorFlow(background) 接收背景根節點,在裡面尋找 canvas,取得繪圖 context,再建立軌跡與粒子資料。它使用瀏覽器提供的影格、尺寸和可見狀態,不需要從 Vue store 取得作品、文章或目前路由。

這讓繪圖演算法和網站內容可以各自維護。傳進來的根節點決定查找範圍;函式回傳的清理動作,則負責移除它建立的監聽器、取消影格,並清理可選的光暈動畫。外層元件處理何時呼叫初始化和清理。

這裡值得保留的是介面:輸入一個有明確結構的根節點,輸出一個釋放資源的函式。不是把原本的 onMounted 名稱換掉,就算完成遷移。

例如,移走元件生命週期後,仍要確認 canvas 是否存在、取得 context 失敗時怎麼結束,以及非同步載入動畫套件時,原來的頁面是否已離開。這些問題原本就存在,只是現在要直接寫在瀏覽器程式的責任裡。

上述是保留實作的程式碼觀察,不是本輪重新掛載、量測或驗收 Canvas 的結果。流線的歷史量測另見背景流線篇。

GSAP 可以裝飾換頁,不接管目的地

目前的換頁效果走的是另一條路:共用控制先初始化,再非同步載入動態效果模組。這個模組用 GSAP 播放百葉遮罩,最後以 location.assign() 前往連結目的地。

目的地仍然來自真正的 a 元素。程式會避開外部網址、帶 query 或 hash 的連結、下載連結、另開視窗,以及使用修飾鍵的點擊;降低動態效果偏好啟用時,也不走這段遮罩流程。

這個分工很具體:HTML 提供連結,腳本只在符合條件時介入離頁效果。它沒有建立 client router,也沒有把下一頁內容塞回目前的文件。

動態效果模組載入失敗時,外層會清掉換頁揭幕標記;主要控制已先初始化,原生連結也仍在文件中。這是程式裡的降級安排,不代表所有網路失敗、導覽取消及瀏覽器返回路徑都已在本輪逐一操作驗證。

局部狀態放在它真正影響的範圍

作品頁目前有開發、設計與 AIGC 三個分類,入口各有自己的 hash。腳本讀取 hash 決定顯示哪個區塊,點擊分類時更新網址,再同步區塊和入口的狀態。

這個狀態不需要提升成全站 store。它只影響作品頁,網址也能表達選中的分類。圖牆重排則由該頁的觀察器、圖片事件和影格排程處理,不必要求文章頁或首頁知道圖牆高度。

可以用下面這張表檢查邊界,而不是先決定所有東西都要搬進同一種元件:

行為 本站採用的責任範圍 仍需保留的基本內容
作品分類 作品頁根節點與網址 hash 分類入口和各區塊內容
圖牆排列 該頁的格子、卡片及圖片事件 可讀的初始網格
GSAP 換頁 共用動態效果模組 真正的頁面連結
保留的 Canvas 流線 傳入的背景根節點與繪圖資源 不承載正文的裝飾層

這張表是對目前程式責任的整理,不是建議把所有互動都改成手寫 DOM。若需求變成大量欄位連動、跨區即時狀態或複雜編輯器,仍要重新評估狀態模型與框架,而不是堅持每種產品都只用零散事件處理。

清理資源時,也要區分離開和暫存

拆出初始化函式後,很容易只想到「離頁就全部移除」。但瀏覽器可能把文件放進往返快取,返回時沿用原來的 DOM。

目前工程網格的外層會在 pagehide 清理動畫實例,並在符合快取恢復條件的 pageshow 重新初始化。作品頁則在離頁時取消待執行影格;是否斷開觀察器和移除控制監聽器,會再依事件的 persisted 分支判斷。

這些不是同一種清理策略。動畫可以重建,原生控制也可能需要保留,不能用一個全域「停止所有東西」函式取代每個功能的生命週期契約。具體狀態與操作證據另見背景生命週期篇,這裡只交代架構上的所有權,不沿用舊背景的測試結果替現在版本背書。

先確認內容,再確認增強行為

把局部互動接回靜態頁面時,我會先查初始 HTML:讀者要看的文字、圖片和目的地是否已存在;接著才查腳本是否只控制約定的節點,以及它建立的資源由誰釋放。

最後仍要操作實際行為。程式裡有取消影格,不等於所有返回情境都已測過;有原生連結,也不等於動畫介入後的每條路徑都沒有問題。本文沒有重新量測動畫流暢度、套件體積、效能或搜尋結果,也沒有對所有裝置作互動驗收。

路由與責任分工篇回答頁面由誰組成;這篇補的是互動如何取得自己的範圍。只要內容沒有被綁進動畫的啟動條件,保留效果就不必等於保留整個 App。

Sources

[1] https://docs.astro.build/en/guides/client-side-scripts/