SwImage
執行 pnpm optimize-images(或 pnpm build 前自動觸發)後,腳本遞迴處理 public/images/ 下所有 jpg/jpeg/png/gif/webp 檔案,在 public/images/optimized/ 目錄生成四種寬度(640w、1024w、1280w、1920w)的 avif 和 webp 版本。
元件根據 src 路徑自動拼接 srcset,瀏覽器按 avif → webp → 原圖 優先級選擇最佳版本,實現近乎無感的圖片效能最佳化。
何時用 SwImage
SwImage 不是所有圖像的統一出口——先在「用哪個元件」這層選定,再看下面用法:
| 資產類型 | 用哪個 |
|---|---|
| 點陣內容圖(jpg/png/webp:封面 / banner / 商品 / 頭像 / hero) | SwImage |
業務 SVG 圖示(assets/icons/,多處複用) | AppIcon |
| UI 功能圖示(導覽 / 按鈕 / 箭頭 / 狀態) | 圖示庫(@phosphor-icons/vue 等)/ SwIcon |
| 單個 SVG logo / 插畫(需佔位或載入失敗回退) | SwImage fit="contain" |
| 純裝飾小圖 / 內聯 dataURI / sprite / 1px 分隔 | 原生 <img>(帶 alt) |
完整理由與邊緣情形見框架開發規範「圖像資產選型」。
本地圖片 vs 外部圖片
- 本地圖片(
/images/開頭):使用<picture>元素引用預生成的 avif/webp srcset,瀏覽器自動選擇最佳格式。 - SVG 圖片:向量格式,直接以
<img>輸出,不走預生成邏輯。 - 外部 URL(
http/https開頭):直接使用<img>,不走構建期預生成邏輯;預設(noopprovider)不生成 srcset,設定swiftConfig.imageprovider 後由 CDN 按 WIDTHS 生成 srcset(見下方provider)。
<!-- 本地圖片:自動使用預生成的 avif/webp srcset -->
<SwImage src="/images/cover.jpg" alt="封面" ratio="16/9" />
<!-- SVG:直接輸出,不走最佳化 -->
<SwImage src="/images/logo.svg" alt="Logo" fit="contain" />
<!-- 外部圖片:無 srcset,直接 img -->
<SwImage src="https://picsum.photos/400/300" alt="示例" ratio="4/3" />
<!-- 外部圖片單圖覆蓋 provider 路由:已簽名 URL 強制 'noop' 原樣直出(不走 CDN 處理) -->
<SwImage :src="signedUrl" alt="示例" ratio="4/3" provider="noop" />
基本示例
基本用法
對比無 ratio 與設定 ratio='16/9' 時容器高度的差異。
寬高比(ratio)
設定 ratio prop 後,容器會提前按比例佔據高度,圖片載入前不會產生版面跳動(CLS)。建議所有內容圖都設定 ratio。
設定 ratio 時,組件還會按比例在 <img> 上輸出 width / height 屬性(如 ratio="16/9" → width="16" height="9"),滿足 web-vitals / Lighthouse 對圖片尺寸的檢查。這兩個屬性只表達比例、與顯示像素無關——實際尺寸仍由 CSS(width:100%)控制,圖片照常隨視窗響應式縮放,attr 僅供瀏覽器算 aspect-ratio 預留盒子。
常用寬高比對比
1/1(頭像)、4/3(傳統照片)、16/9(視訊/橫幅)三種比例的視覺對比。
fit 三態對比(cover/contain/fill)
載入失敗回退(placeholder vs 停用)
佔位層自訂樣式(placeholderClass)
@load / @error 事件
尺寸控制心智模型(三場景速判)
SwImage 的尺寸哲學是**「尺寸交給外層」**:傳 ratio 時預設撐滿父容器寬度、按比例推高度;傳 fill 時完全填滿父容器。遇到版面彆扭時先對號入座,別用 :deep 穿透打補丁(applier 多次踩坑的經驗換來的表,anibay#8 / bay-admin#40 / cinebay#7 / bay-admin#66):
| 場景 | 正解 | 為什麼 |
|---|---|---|
| 流式內容圖(卡片流/網格,隨欄寬縮放) | 傳 ratio | 容器輸出內聯 aspect-ratio,寬度預設 100%,佔滿格子並預留高度(防 CLS) |
| 填滿父容器(定高 hero / 策展帶頭圖 / 固定高廣告位,父容器自己定位或定高) | 傳 fill | 容器與 <img> 都走 100%×100%、不輸出 aspect-ratio,尺寸完全交給父容器;fit 照常生效 |
| 定寬小圖(頭像 / 迷你封面 / 表格縮圖,圖要服從一個固定尺寸) | 傳 ratio,直接給元件寫 width | ratio 模式的預設寬度 100% 走樣式表裡的零特異度規則 :where(.sw-image.is-ratio),不是內聯——你寫在元件上的任何 class(含 Tailwind w-12)都能直接贏,不必包 wrapper、不必 !important |
<!-- ✅ 定寬 class 直接掛 SwImage 即可;在 flex 行裡記得 flex-shrink: 0 -->
<SwImage class="avatar" src="/a.jpg" alt="" ratio="1/1" />
<style>.avatar { width: 3rem; flex-shrink: 0; }</style>
一條反模式(有真實翻車案例):
<!-- ❌ 想鋪滿定高父容器,:deep 穿透給容器打絕對定位——脆弱且 fill 已內建此能力 -->
<style>.hero :deep(.sw-image) { position: absolute; inset: 0; }</style>
<!-- ✅ 父容器定位/定高,SwImage 傳 fill -->
<div class="hero"><SwImage src="/h.jpg" alt="" fit="cover" fill /></div>
<style>.hero { position: relative; height: 24rem; }</style>
常用場景
<!-- 首屏 Hero 大圖(LCP 三件套):eager 立即載入 + preload 注入 <link rel="preload">
+ fetchpriority 網路優先級拉滿(加密 .bnc 圖不支援 preload,見注意事項) -->
<SwImage src="/images/hero.jpg" alt="首頁主圖" ratio="21/9" loading="eager" preload fetchpriority="high" :placeholder="false" />
<!-- 頭像圓圖:contain 完整顯示,1:1 比例 -->
<SwImage src="/images/avatar.png" alt="使用者頭像" ratio="1/1" fit="contain" />
<!-- 封面模糊底圖:blur 高斯模糊(詳情頁背景常用 pattern) -->
<SwImage src="/images/cover.jpg" alt="" :blur="12" ratio="16/9" />
<!-- 帶佔位圖:載入中顯示品牌 logo -->
<SwImage src="/images/product.jpg" alt="商品圖" ratio="1/1" placeholder="/images/logo.svg" />
<!-- 監聽載入事件 -->
<SwImage src="/images/cover.jpg" alt="封面" @load="onLoad" @error="onError" />
卡片整體點擊 + Hover 縮放
外層容器自帶 click 事件(覆蓋 overlay 區點擊),加 hover-zoom class 啟用懸停放大。兩者組合是封面卡的標準用法:
<SwImage
src="/images/cover.jpg"
alt="影片封面"
ratio="3/4"
class="hover-zoom"
@click="goDetail"
>
<div class="overlay">
<span class="play-icon">▶</span>
<span class="title">{{ item.title }}</span>
</div>
</SwImage>
hover-zoom 預設放大 1.1 倍,業務可透過 CSS 變數調整:
:root {
--sw-image-hover-zoom-scale: 1.05; /* 收斂一點 */
--sw-image-hover-zoom-duration: 0.6s; /* 慢一點 */
}
主題自適應佔位圖
placeholder 支援 { dark, light } 物件形式,元件內透過 CSS(<html data-theme> + prefers-color-scheme)自動依主題切換佔位圖,無執行時負擔:
::alert{type="info"}
**`prefers-reduced-motion: reduce` 下自動不放大**,元件已內建兜底,業務無需自己補。縮放屬「非必要動效」,對前庭功能障礙使用者會引發暈眩(standards UX-004 / WCAG 2.3.3)。
另外,放大**只作用於真實圖**:佔位圖不參與縮放(佔位期 hover 放大佔位 logo 沒有意義)。插槽內的業務圖也不受影響 —— 那部分動效由業務自己決定。
::
vue
<!-- placeholder-class:給佔位 div 附加自訂類(如骨架屏脈衝動畫) -->
<SwImage
src="/images/cover.jpg"
alt="..."
placeholder-class="skeleton-pulse"
:placeholder="{
dark: '/images/placeholder-dark.png',
light: '/images/placeholder-light.png',
}"
/>
自訂佔位內容
#placeholder slot 優先於 placeholder prop,可放任意自訂內容(skeleton 動畫、品牌動效等):
<SwImage src="/images/cover.jpg" alt="...">
<template #placeholder>
<div class="my-skeleton" />
</template>
</SwImage>
暴露方法(defineExpose)
| 成員 | 簽名 | 說明 |
|---|---|---|
getElement | () => HTMLImageElement | null | 取當前渲染的真實 <img>(加密 / <picture> / 一般三個分支通用)。SSR 期與加密圖解密完成前回傳 null —— @load 之後讀最穩 |
<script setup lang="ts">
const imgRef = ref()
function measure() {
const el = imgRef.value?.getElement()
if (el) console.log(el.naturalWidth, el.naturalHeight) // 原圖真實尺寸
}
</script>
<template>
<SwImage ref="imgRef" src="/images/cover.jpg" alt="封面" @load="measure" />
</template>
🔴 別用 querySelector 猜內部 DOM。元件內同時存在多個 <img>(佔位圖最多 3 個,且在 DOM 順序上先於真實圖),querySelector('img') 命中的是佔位圖 —— 不報錯,只是量到佔位 SVG 的尺寸。內部 class(.img / .placeholder-img)是實作細節、不是契約,隨時可能變;測量、decode()、掛 observer 一律走 getElement()。
屬性(Props)
| 名稱 | 類型 | 必填 | 預設值 | 說明 |
|---|---|---|---|---|
src | string | ✓ | — | 圖片 URL,支援 .bnc 加密路徑。執行期為空 / undefined 時渲染錯誤佔位(.placeholder.is-error),不拋錯、不拖垮 SSR |
alt | string | ✓ | — | 無障礙描述 |
imgKey | string | '' | .bnc 解密金鑰覆寫項;不傳時自動從 useSystemStore().info.img_key 讀取 | |
ratio | string | number | — | 寬高比,如 "16/9" "16:9" 或 420 / 300(數值)。寬度預設撐滿父容器(樣式表零特異度規則,非內聯),直接給元件寫 width 即可定寬 | |
sizes | string | 自動生成 | 自訂 srcset sizes 媒體查詢 | |
fit | 'cover' | 'contain' | 'fill' | 'none' | 'scale-down' | 'cover' | 圖片填充方式,對應 CSS object-fit | |
loading | 'lazy' | 'eager' | 'lazy' | 懶載入策略,首屏圖傳 'eager' | |
fetchpriority | 'auto' | 'high' | 'low' | 'auto' | 資源優先級,首屏大圖傳 'high' | |
preload | boolean | false | LCP 優化:啟用後自動注入 <link rel="preload" as="image"> 到 <head>,HTML 解析期就開始 fetch;加密分支(.bnc)不支援,因為實際渲染的是解密後的 blob URL | |
provider | string | undefined | 單圖覆蓋 swiftConfig.image 路由。傳 'noop' 強制不處理(原 URL 直出);傳具體 provider 名(如 'cf-image')強制走該處理器。僅作用於外連圖分支;加密圖 / 本地圖天然繞過 | |
fill | boolean | false | 填滿父容器形態:容器與 <img> 走 100%×100%、不輸出 aspect-ratio 內聯樣式(尺寸完全交給父容器),fit 照常生效。與 ratio 互斥(同傳時 fill 優先 + dev 警告) | |
decoding | 'async' | 'sync' | 'auto' | 'async' | 圖片解碼模式 | |
placeholder | string | { dark, light } | false | false | 佔位/回退圖。字串=單圖;物件走主題切換;false 停用 | |
placeholderClass | string | '' | 附加到佔位 div 的 class 鉤子(僅作用於 SwImage 自管佔位 div) | |
title | string | '' | 原生 title 屬性(hover tooltip / SEO) | |
radius | number | string | 0 | 圓角,number 自動加 px | |
blur | number | 0 | 高斯模糊半徑(px) | |
backgroundColor | string | 'transparent' | 容器背景色 |
事件
| 事件名 | 載荷類型 | 說明 |
|---|---|---|
click | MouseEvent | 外層容器點擊,包括 overlay 區域的點擊 |
load | Event | <img> 載入完成 |
error | Event | <img> 載入失敗 |
插槽
| 插槽名 | 說明 |
|---|---|
default | 覆蓋層內容(視訊播放器、文字遮罩等),z-index 高於圖片和佔位層 |
placeholder | 自訂佔位內容(覆寫 placeholder prop 的預設渲染) |
CSS 變數
| 變數名 | 預設值 | 說明 |
|---|---|---|
--sw-image-hover-zoom-scale | 1.1 | hover-zoom class 啟用時的懸停縮放倍數 |
--sw-image-hover-zoom-duration | 0.25s | 懸停縮放的過渡時長(與 scale 成對,想要慢移/快彈調這個) |
--sw-image-placeholder-bg | transparent | 佔位層底色(loading 態)。消費方覆寫此變數即得自訂佔位底色 |
--sw-image-placeholder-error-bg | color-mix(in srgb, var(--destructive) 20%, transparent) | 載入失敗佔位底色(預設帶 --destructive 紅調)。佔位層在 error 態自動掛 .is-error 切換到此色 |
載入失敗佔位
圖片載入失敗時,佔位層會保持顯示並掛上 .is-error,底色切換為 --sw-image-placeholder-error-bg(預設帶紅調),與 loading 態區分:
<!-- 自訂 loading / error 佔位底色 -->
<style>
.sw-image .placeholder { --sw-image-placeholder-bg: #16110f; }
.sw-image .placeholder.is-error { --sw-image-placeholder-error-bg: #3c0001; }
</style>
SSR 直出的圖若在 hydration 前就載入失敗,
@error事件會早於監聽器觸發而被錯過;組件在onMounted主動補判(img.complete && naturalWidth === 0→ error),確保 error 佔位穩定顯示。
注意事項
src含.bnc後綴時走內部 AES 解密 + IndexedDB 快取(與SwAsyncText共用useDecryptedAsset底層),僅客戶端渲染。- 首屏 LCP 大圖建議同時設定
loading="eager"和fetchpriority="high"。 - 佔位層(
.placeholder)以isolation: isolate隔離層疊上下文,不會蓋住消費方疊在圖片上的浮層(標題 / 角標等)。