iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
Modern Web

用 Astro 打造 Content-first 前端網站:30 天從靜態內容到會員、資料庫與選型(3rd)系列 第 8

把 Vue 元件變成 Astro 島:client:* 開關怎麼挑?

  • 分享至 

  • xImage
  •  

想加一顆「收藏」按鈕,元件寫好、import 進頁面、貼上去、重整。按了沒反應,元件沒壞,是你還沒告訴 Astro 這一塊要在瀏覽器裡能互動。

Astro 預設把元件輸出成不帶 JavaScript 的靜態 HTML(Day 1 量過同一份內容的 JS 傳輸量,這是它 0 JS 的原因)。要讓某一塊能互動,必須在那個元件加上 client:* 指令。把 client:* 當成「要不要引入 Vue」的總開關,容易把引入 JS 和啟動時機混在一起。它只決定這座島什麼時候啟動。加了島會多送 JS;只換啟動時機,通常不會讓那包 JS 變小。

如果把 Vue SFC 當成 Astro 頁面的預設單位,整個 layout、整篇文章、每個區塊都會先寫成 Vue,再掛進 Astro。這樣做一樣能運作,只是沒有用到 Astro 讓內容預設維持靜態的方式。只有真的要互動的那幾塊,才需要做成 Vue 島。

一塊怎麼變成島

島就是「一頁靜態 HTML 裡,少數會動的那幾塊」。把一個 Vue 元件變成島,只有兩步:import 進來、加 client: 指令。

---
// src/pages/blog/[...slug].astro
import ReactionButton from '../../components/ReactionButton.vue';
---
<article>
  <!-- 文章內容:純 HTML,0 JS -->
</article>

<ReactionButton client:load />   <!-- 這一塊才變島、才有 JS -->

差別在 client:load

  • 不加<ReactionButton /> 一樣會出現在畫面上,但只有 HTML、樣式,按了沒反應,因為沒有 JS。
  • 加了:Astro 才把這顆按鈕需要的 JavaScript 送到瀏覽器。

文章那一大段沒有 client:,所以維持 0 JS。只有標上這個指令的元件,Astro 才會把 JS 送到瀏覽器。

client:* 是一份「什麼時候啟動」的選單

client:load 結尾的 load 是在指定「這座島什麼時候啟動」,換個值就是換個時機。常用的有這幾個:

指令 什麼時候啟動 適合
client:load 頁面一載入就啟動 首屏馬上要能用的(主要按鈕、導覽列開關)
client:idle 等瀏覽器忙完、閒下來才啟動 次要的、晚一點動也沒差的
client:visible 捲到看得見它才啟動 在很下面、或很吃資源的元件(圖表、留言區)
client:media 符合某個 CSS media query 才啟動 只有手機(或只有桌機)才需要的
client:only 跳過伺服器端 HTML,整塊只在瀏覽器畫 純前端、本來就沒辦法在伺服器先 render 的

client:only 時,要指定框架名,例如 client:only="vue"。這塊跳過了伺服器端渲染,Astro build 時不會跑它,所以你得告訴 Astro 這塊是用哪個框架畫的。漏了會直接報錯。

啟動越晚,瀏覽器就能先把首屏內容畫出來。首屏一載入就必須能操作的元件用 client:load;可以晚一點啟動的元件則用 idlevisible

這排指令決定「啟動時機」,不會改變送到瀏覽器的 JS 有多少。我把同一顆按鈕從 client:load 改成 client:visible,build 出來的 JS 檔一個位元組都沒變;差別只在它什麼時候被抓下來執行。

實作專案的兩個 bench 頁面載入同一顆按鈕。檔名相同只是第一步,接著看 visible 把哪一段工作往後挪。

指令 首屏載入後 捲到按鈕前 看到按鈕時 JS bundle
client:load 立刻 import 元件並 hydrate 按鈕還沒被看到,也已完成啟動 已可立即點 同一組 Vue island JS
client:visible 先觀察是否進入 viewport 先不啟動這座島 進入 viewport 後才 import 並 hydrate 同一組 Vue island JS

兩頁載入的是同一顆 Vue 按鈕,下載的 JS 檔一樣大。load 一進頁就啟動這座島,visible 則等使用者捲到按鈕才啟動。visible 減少的是首屏那段時間瀏覽器要做的工作:先處理畫面內容,等按鈕進入 viewport 再啟動。

client:load 和 client:visible 使用同一組 Vue island JS 檔,差別只在 hydrate 時機

選指令時,先判斷「這個互動什麼時候必須準備好」。這份選單只列啟動時機;JS 大小要看島裡裝了什麼:

元件情境 建議指令 為什麼
首屏看得到,而且使用者一進頁就可能操作 client:load 互動可用性比延後成本重要,像導覽列選單、theme toggle、首屏 CTA
在文章中段或底部,使用者不一定會看到 client:visible 不讓這座島搶首屏 hydration,像留言區、推薦 carousel、文章底部 feedback
在下方,但出現時最好已經能點 client:visible={{ rootMargin: "200px" }} 提早一點 hydrate,避免使用者捲到後還要等
不急著互動,但通常會用到 client:idle 等初始載入結束後再啟動,適合次要工具或輔助 UI

「按鈕在首屏」和「按鈕在文章底部」會選不同指令。首屏按鈕用 visible,大多只是把事情繞一圈,因為它一開始就可見;文章底部按鈕用 load,會讓一個讀者可能根本不會看到的互動元件提早搶資源。

加一座島,到底多送了哪些 JavaScript 到瀏覽器?

Day 1 把那顆 Vue 按鈕島的 JavaScript 攤開過,可以分成三樣東西:

  1. Vue 本身(讓元件能跑的那套)。
  2. 那顆按鈕的程式碼。
  3. 把按鈕接上 Vue、讓它真的能點的那段接線。

Astro 內容站的 demo:文章是靜態 HTML,只有結尾那顆按鈕是 Vue 島,點了會累加次數

圖中的按鈕只占頁面結構的一小塊:標題、日期、正文都由 Astro 和 Content Collections 先輸出成 HTML,只有最下面那顆「有幫助」按鈕是 Vue island。「頁面內容」與「互動邊界」可以直接分開看。

這三樣只跟著那顆按鈕來。文章、標題、圖片那些沒加 client: 的地方,一個位元組的 JS 都沒有。所以一頁送到瀏覽器的 JS 有多少,取決於你開了幾座島、島裡裝了什麼,跟頁面多大無關。

多數內容,不用變成島

學會島之後,很容易把整篇、甚至整個 layout 都包成一座大島,理由是「這樣全部都能互動」。開頭提到的「整站都用 Vue 寫、再整包掛成島」,就是這種做法的極端版。

把整頁包成島,等於把整頁的內容都變成 JavaScript 送出去,頁面會回到前端框架 SPA 的做法。Day 1 講的「內容留在 HTML」的好處也會跟著消失。

Astro 預設讓所有內容維持 0 JS 的靜態 HTML,只有真的要互動的那幾小塊才開成島。一個內容站,多數頁面一座島都不用;有島的頁面,通常也只有一兩顆。

真實專案的另一種走法:整份選單只用一個值

上面那份選單有五個值。真實專案可能只用其中一個。

我找了一個上線中的多語系品牌官網來看,它用 Astro 搭 Vue。全站出現九次 client:*,九次都是 client:loadidlevisiblemediaonly 一次都沒出現。每條路由長同一個形狀:

---
// src/pages/[...lang]/index.astro
import Layout from '@/layouts/Layout.astro';
import SiteHeader from '@/components/SiteHeader.vue';
import HomeView from '@/views/home/index.vue';
---
<Layout>
  <SiteHeader client:load />
  <HomeView client:load />
</Layout>

.astro 檔只剩一層殼,標題、文案、圖片全寫在那些 Vue 元件裡面。

這個團隊搬進 Astro 時,沿用了原本的 Vue 習慣。團隊本來寫 Vue,src/views/home/index.vue 這種目錄結構已經用慣了,搬進 Astro 時最小的改動就是把整個 view 掛上來。client:load 是官方文件第一個出現的指令,也是唯一「一定會啟動」的那個。換 visible 要判斷這塊在不在首屏;only 還得多寫框架名。趕上線時挑一個不會出錯的值,是合理的決定。

代價可以量。我把那個專案複製一份跑 astro build,追首頁實際要載哪些檔:

量測 結果
首頁 HTML 7.1 KB
首頁要載的 JS 210 KB(6 支檔)
其中最大一支 193 KB:Vue 執行環境加四個語系的翻譯訊息
整個 build 的 JS 262 KB(57 支檔)

7.1 KB 的 HTML 配 210 KB 的 JS。而那 7.1 KB 裡幾乎沒有內容,因為文案都在 Vue 元件裡,要等 JS 下載、執行完才會出現在畫面上。

client:load 全部換成 client:visible,解決不了 JS 大小的問題。前面量過,換指令不會讓那包 JS 變小,這裡換完還是 210 KB,只是晚一點載。要縮小這包 JS,得先把不需要互動的內容從 Vue 元件搬回 .astro。等標題、文案、圖片在 build 時就印成 HTML,島才會縮到只剩真的要互動的那幾塊。島的邊界縮小後,選單上的其他值才開始有意義。首屏那顆按鈕留 load,頁面底部的 feedback 用 visible

先決定哪些內容不必是島,再挑指令。如果先把內容包進 Vue,選單有五個值也只會用到一個。

client:only 和多座島的邊界

client:only 那一塊不會出現在伺服器送的 HTML 裡(它跳過了 server render)。所以爬蟲和「還沒跑 JS 的瀏覽器」看不到它。能不能 only,要看那塊是不是需要被看見、被收錄。文章標題、摘要、正文、商品資訊這類內容,不該放進 client:only

每座島彼此獨立,不會自動共享狀態。兩顆島想互通資料,得另外處理,放在同一頁也不會直接相通。會員功能就是一個例子:server 的登入狀態、client 的收藏按鈕、feedback widget,要靠清楚的資料流接起來。

常見錯誤

  • 忘了加 client::最常見。元件畫得出來、就是按了沒反應,因為它還是靜態 HTML,沒 JS。
  • client:only 卻檢查不到內容:檢視原始碼發現那塊是空的,是正常的,only 本來就不在伺服器 HTML 裡。需要 SEO 的內容別放進 only 的島。
  • 以為兩座島會互通:在 A 島改了狀態,B 島不會跟著變。島之間要共享得用額外手段,同頁不會讓它們自動相通。

下一步

互動交給島,但只交給真的需要的那幾塊。內容保持靜態,收藏、feedback、搜尋這類需要狀態的元件才變成 Vue island。下一篇接著處理內容格式:純 Markdown 和可以內嵌元件的 MDX 該怎麼選。

查證基準:Astro client directivesislands 官方文件(Astro v6)。

本日程式碼:step-08|只看這天的改動:step-07...step-08


上一篇
只想加一顆按鈕、一點互動,需要動用框架嗎?Astro 頁面裡的原生 script
下一篇
內容要用純 Markdown 還是 MDX?差在哪、怎麼選?
系列文
用 Astro 打造 Content-first 前端網站:30 天從靜態內容到會員、資料庫與選型(3rd)9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言