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