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>,不走構建期預生成邏輯;預設(noop provider)不生成 srcset,設定 swiftConfig.image provider 後由 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,直接給元件寫 widthratio 模式的預設寬度 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)

名稱類型必填預設值說明
srcstring✓—圖片 URL,支援 .bnc 加密路徑。執行期為空 / undefined 時渲染錯誤佔位(.placeholder.is-error),不拋錯、不拖垮 SSR
altstring✓—無障礙描述
imgKeystring''.bnc 解密金鑰覆寫項;不傳時自動從 useSystemStore().info.img_key 讀取
ratiostring | number—寬高比,如 "16/9" "16:9" 或 420 / 300(數值)。寬度預設撐滿父容器(樣式表零特異度規則,非內聯),直接給元件寫 width 即可定寬
sizesstring自動生成自訂 srcset sizes 媒體查詢
fit'cover' | 'contain' | 'fill' | 'none' | 'scale-down''cover'圖片填充方式,對應 CSS object-fit
loading'lazy' | 'eager''lazy'懶載入策略,首屏圖傳 'eager'
fetchpriority'auto' | 'high' | 'low''auto'資源優先級,首屏大圖傳 'high'
preloadbooleanfalseLCP 優化:啟用後自動注入 <link rel="preload" as="image"> 到 <head>,HTML 解析期就開始 fetch;加密分支(.bnc)不支援,因為實際渲染的是解密後的 blob URL
providerstringundefined單圖覆蓋 swiftConfig.image 路由。傳 'noop' 強制不處理(原 URL 直出);傳具體 provider 名(如 'cf-image')強制走該處理器。僅作用於外連圖分支;加密圖 / 本地圖天然繞過
fillbooleanfalse填滿父容器形態:容器與 <img> 走 100%×100%、不輸出 aspect-ratio 內聯樣式(尺寸完全交給父容器),fit 照常生效。與 ratio 互斥(同傳時 fill 優先 + dev 警告)
decoding'async' | 'sync' | 'auto''async'圖片解碼模式
placeholderstring | { dark, light } | falsefalse佔位/回退圖。字串=單圖;物件走主題切換;false 停用
placeholderClassstring''附加到佔位 div 的 class 鉤子(僅作用於 SwImage 自管佔位 div)
titlestring''原生 title 屬性(hover tooltip / SEO)
radiusnumber | string0圓角,number 自動加 px
blurnumber0高斯模糊半徑(px)
backgroundColorstring'transparent'容器背景色

事件

事件名載荷類型說明
clickMouseEvent外層容器點擊,包括 overlay 區域的點擊
loadEvent<img> 載入完成
errorEvent<img> 載入失敗

插槽

插槽名說明
default覆蓋層內容(視訊播放器、文字遮罩等),z-index 高於圖片和佔位層
placeholder自訂佔位內容(覆寫 placeholder prop 的預設渲染)

CSS 變數

變數名預設值說明
--sw-image-hover-zoom-scale1.1hover-zoom class 啟用時的懸停縮放倍數
--sw-image-hover-zoom-duration0.25s懸停縮放的過渡時長(與 scale 成對,想要慢移/快彈調這個)
--sw-image-placeholder-bgtransparent佔位層底色(loading 態)。消費方覆寫此變數即得自訂佔位底色
--sw-image-placeholder-error-bgcolor-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 隔離層疊上下文,不會蓋住消費方疊在圖片上的浮層(標題 / 角標等)。