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 内联样式(尺寸完全交给父容器,如定高 hero / 固定高广告位),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 隔离层叠上下文,不会盖住消费方叠在图片上的浮层(标题 / 角标等)。