前言
WebP 是一种现代图片格式,相比传统的 JPG/PNG 格式,通常可以减少 25-35% 的文件大小,某些图片甚至可以减少 80% 以上。本文将介绍如何在 UmiJS + Vue 3 项目中实现 WebP 图片的自动转换和智能加载。
功能特性
- ✅ 构建时自动转换:构建时自动将 JPG/PNG 转换为 WebP
- ✅ 智能格式选择:自动检测浏览器支持,优先使用 WebP
- ✅ 自动回退:不支持的浏览器自动使用原始格式
- ✅ 性能优化:使用缓存避免重复检测和重复加载
- ✅ 零配置使用:组件化封装,使用简单
实现步骤
1. 安装依赖
bash
pnpm add -D imagemin imagemin-webp
2. 创建图片转换脚本
创建 scripts/convert-images.mjs:
javascript
import imagemin from "imagemin"
import imageminWebp from "imagemin-webp"
import path from "path"
import fs from "fs"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
/**
* 图片转 WebP 脚本
* 将 src/assets 目录下的 jpg/jpeg/png 图片转换为 webp 格式
*/
async function convertImages() {
const assetsDir = path.join(__dirname, "../src/assets")
// 检查目录是否存在
if (!fs.existsSync(assetsDir)) {
console.log("⚠️ assets 目录不存在,跳过图片转换")
return
}
console.log("🖼️ 开始转换图片为 WebP 格式...")
try {
const files = await imagemin([`${assetsDir}/*.{jpg,jpeg,png}`], {
destination: assetsDir,
plugins: [
imageminWebp({
quality: 80, // 质量 0-100,80 是质量和文件大小的良好平衡
method: 6, // 压缩方法 0-6,6 是最慢但压缩率最高
}),
],
})
if (files.length === 0) {
console.log("ℹ️ 没有找到需要转换的图片")
} else {
console.log(`✅ 成功转换 ${files.length} 张图片为 WebP 格式:`)
files.forEach((file) => {
const fileName = path.basename(file.destinationPath)
const originalSize = fs.statSync(
file.sourcePath.replace(/\.webp$/, path.extname(file.sourcePath))
).size
const webpSize = fs.statSync(file.destinationPath).size
const reduction = ((1 - webpSize / originalSize) * 100).toFixed(1)
console.log(` - ${fileName} (减少 ${reduction}%)`)
})
}
} catch (error) {
console.error("❌ 图片转换失败:", error.message)
process.exit(1)
}
}
// 执行转换
convertImages()
3. 创建图片工具函数
创建 src/utils/image.ts:
typescript
/**
* 图片工具函数 - 支持 WebP 格式
*/
const WEBP_SUPPORT_CACHE_KEY = "__webp_support__"
/**
* 检测浏览器是否支持 WebP 格式(带缓存)
* 使用 localStorage 缓存检测结果,避免重复检测
*/
export function checkWebPSupport(): Promise<boolean> {
// 先检查缓存
if (typeof window !== "undefined" && window.localStorage) {
const cached = window.localStorage.getItem(WEBP_SUPPORT_CACHE_KEY)
if (cached !== null) {
return Promise.resolve(cached === "true")
}
}
// 如果没有缓存,进行检测
return new Promise((resolve) => {
const webP = new Image()
webP.onload = webP.onerror = () => {
const supported = webP.height === 2
// 缓存结果
if (typeof window !== "undefined" && window.localStorage) {
window.localStorage.setItem(WEBP_SUPPORT_CACHE_KEY, String(supported))
}
resolve(supported)
}
webP.src =
"data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA"
})
}
/**
* 同步获取 WebP 支持状态(从缓存)
* 如果缓存不存在,默认返回 true(现代浏览器都支持)
* 这样可以避免初始加载非 WebP 资源
*/
export function getWebPSupportSync(): boolean {
if (typeof window === "undefined" || !window.localStorage) {
// SSR 环境,默认返回 true
return true
}
const cached = window.localStorage.getItem(WEBP_SUPPORT_CACHE_KEY)
if (cached !== null) {
return cached === "true"
}
// 没有缓存时,默认假设支持(现代浏览器都支持 WebP)
// 如果实际不支持,后续检测会更新缓存,下次就会使用正确的值
return true
}
/**
* 将图片 URL 转换为 WebP 格式
* 支持多种转换方式:
* 1. 内置图片:直接替换扩展名
* 2. 在线图片:使用图片代理服务或 CDN 转换
*
* @param url 原始图片 URL
* @param options 转换选项
* @returns WebP 格式的 URL
*/
export function convertToWebP(
url: string,
options: {
// 是否强制使用 WebP(即使浏览器不支持)
force?: boolean
// 图片代理服务 URL(用于在线图片转换)
proxyUrl?: string
// CDN 转换参数(如腾讯云、阿里云等)
cdnParams?: string
} = {}
): string {
const { force = false, proxyUrl, cdnParams } = options
// 如果是 data URL,直接返回
if (url.startsWith("data:")) {
return url
}
// 如果是内置图片(相对路径或 umi 处理后的路径),替换扩展名
// 支持 umi 处理后的路径格式:/static/yay.7d162f31.jpg -> /static/yay.7d162f31.webp
if (
url.startsWith("./") ||
url.startsWith("../") ||
(!url.startsWith("http") && !url.startsWith("data:"))
) {
// 匹配 .jpg, .jpeg, .png 扩展名(可能包含 hash)
return url.replace(/\.(jpg|jpeg|png)(\?.*)?$/i, ".webp$2")
}
// 在线图片处理
if (url.startsWith("http://") || url.startsWith("https://")) {
// 方式1: 使用图片代理服务
if (proxyUrl) {
return `${proxyUrl}?url=${encodeURIComponent(url)}&format=webp`
}
// 方式2: 使用 CDN 参数转换(如腾讯云、阿里云等)
if (cdnParams) {
const separator = url.includes("?") ? "&" : "?"
return `${url}${separator}${cdnParams}`
}
// 方式3: 使用在线图片转换服务(如 Cloudinary、ImageKit 等)
// 这里提供一个示例,实际使用时需要根据服务商调整
// return `https://your-image-service.com/convert?url=${encodeURIComponent(url)}&format=webp`;
// 方式4: 简单替换扩展名(如果服务器支持)
return url.replace(/\.(jpg|jpeg|png)(\?.*)?$/i, ".webp$2")
}
return url
}
/**
* 获取图片的最佳格式 URL
* 如果浏览器支持 WebP,返回 WebP 格式;否则返回原始格式
*
* @param originalUrl 原始图片 URL
* @param webpUrl WebP 格式的 URL(可选,如果不提供则自动生成)
* @param webpSupported 浏览器是否支持 WebP(可选,如果不提供则自动检测)
* @returns 最佳格式的 URL
*/
export async function getBestImageUrl(
originalUrl: string,
webpUrl?: string,
webpSupported?: boolean
): Promise<string> {
const isSupported =
webpSupported !== undefined ? webpSupported : await checkWebPSupport()
if (isSupported) {
return webpUrl || convertToWebP(originalUrl)
}
return originalUrl
}
/**
* 预加载图片
*/
export function preloadImage(url: string): Promise<void> {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => resolve()
img.onerror = reject
img.src = url
})
}
4. 创建 WebP 图片组件
创建 src/components/WebPImage.vue:
vue
<template>
<picture>
<!-- 如果支持 WebP,优先使用 WebP -->
<source
v-if="webpSupported && webpSrc"
:srcset="webpSrc"
type="image/webp"
/>
<!-- 回退到原始格式 -->
<img
:src="fallbackSrc"
:alt="alt"
:width="width"
:height="height"
:class="imgClass"
:style="imgStyle"
:loading="loading"
@load="handleLoad"
@error="handleError"
/>
</picture>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue"
import {
checkWebPSupport,
getWebPSupportSync,
convertToWebP,
} from "../utils/image"
interface Props {
// 原始图片 URL(必需)
src: string
// WebP 格式的 URL(可选,如果不提供则自动生成)
webpSrc?: string
// 图片描述
alt?: string
// 图片宽度
width?: string | number
// 图片高度
height?: string | number
// CSS 类名
imgClass?: string
// 内联样式
imgStyle?: string | Record<string, any>
// 懒加载
loading?: "lazy" | "eager"
// 图片代理服务 URL(用于在线图片转换)
proxyUrl?: string
// CDN 转换参数
cdnParams?: string
}
const props = withDefaults(defineProps<Props>(), {
alt: "",
loading: "lazy",
})
const emit = defineEmits<{
load: [event: Event]
error: [event: Event]
}>()
// 使用同步方法获取初始值,避免初始加载非 WebP 资源
// 如果缓存不存在,默认假设支持(现代浏览器都支持)
// 后续异步检测会更新这个值
const webpSupported = ref(getWebPSupportSync())
// 计算 WebP 格式的 URL
const webpSrc = computed(() => {
// 如果明确提供了 webpSrc,直接使用
if (props.webpSrc) {
return props.webpSrc
}
// 对于在线图片,只有在提供了转换方式时才生成 WebP URL
const isOnlineImage =
props.src.startsWith("http://") || props.src.startsWith("https://")
if (isOnlineImage) {
if (!props.proxyUrl && !props.cdnParams) {
// 如果没有提供转换方式,返回空字符串,使用原始格式
return ""
}
// 有转换方式,生成 WebP URL
return convertToWebP(props.src, {
proxyUrl: props.proxyUrl,
cdnParams: props.cdnParams,
})
}
// 对于内置图片(通过 import 导入的),自动尝试使用 WebP 版本
// 构建脚本会在同目录下生成 .webp 文件,通过替换扩展名来引用
// 如果 webp 文件不存在,浏览器会自动回退到原始图片
return convertToWebP(props.src, {})
})
// 回退到原始格式
const fallbackSrc = computed(() => props.src)
// 在后台异步检测 WebP 支持(如果缓存不存在)
// 这样可以更新缓存,但不影响初始渲染
onMounted(async () => {
// 如果已经有缓存,就不需要再次检测
if (typeof window !== "undefined" && window.localStorage) {
const cached = window.localStorage.getItem("__webp_support__")
if (cached === null) {
// 没有缓存,进行检测并更新
const supported = await checkWebPSupport()
// 如果检测结果与初始假设不同,更新状态
// 但此时图片可能已经加载,浏览器会使用 <picture> 标签自动选择
if (supported !== webpSupported.value) {
webpSupported.value = supported
}
}
} else {
// 没有 localStorage,直接检测
webpSupported.value = await checkWebPSupport()
}
})
const handleLoad = (event: Event) => {
emit("load", event)
}
const handleError = (event: Event) => {
emit("error", event)
}
</script>
5. 配置 TypeScript 类型声明
创建 src/types/images.d.ts:
typescript
/**
* 图片文件类型声明
*/
declare module "*.jpg" {
const content: string
export default content
}
declare module "*.jpeg" {
const content: string
export default content
}
declare module "*.png" {
const content: string
export default content
}
declare module "*.gif" {
const content: string
export default content
}
declare module "*.webp" {
const content: string
export default content
}
declare module "*.svg" {
const content: string
export default content
}
declare module "*.ico" {
const content: string
export default content
}
declare module "*.bmp" {
const content: string
export default content
}
6. 配置 UmiJS
更新 .umirc.ts,添加 WebP 文件支持:
typescript
import { defineConfig } from "umi"
import { routes } from "./src/router/index"
const isPrd = process.env.NODE_ENV === "production"
export default defineConfig({
npmClient: "pnpm",
presets: [require.resolve("@umijs/preset-vue")],
manifest: {
fileName: "manifest.json",
},
// 开发环境 vite ,线上环境 webpack 打包
vite: isPrd ? false : {},
chainWebpack: function (config, { webpack }) {
// 配置 webp 文件支持(与 jpg/png 一样处理)
// umi 默认已经处理图片,但可能不包含 webp,这里确保 webp 被正确处理
// 使用 asset/resource 类型,让 webpack 将 webp 文件作为静态资源处理
if (!config.module.rules.has("webp")) {
config.module
.rule("webp")
.test(/\.webp$/)
.type("asset/resource")
.generator({
filename: "static/[name].[hash:8][ext]",
})
}
config.optimization.runtimeChunk(true)
return config
},
codeSplitting: {
jsStrategy: "depPerChunk",
},
hash: true,
history: { type: "hash" },
routes,
})
7. 更新 package.json
在 package.json 中添加构建脚本:
json
{
"scripts": {
"dev": "umi dev",
"build": "pnpm build:images && umi build",
"build:prd": "pnpm build:images && UMI_ENV=prd umi build",
"build:images": "node scripts/convert-images.mjs",
"postinstall": "umi setup",
"start": "npm run dev",
"preview": "umi preview",
"analyze": "ANALYZE=1 umi build"
}
}
使用方法
基本使用
vue
<script setup lang="ts">
import WebPImage from "../components/WebPImage.vue"
import yayImage from "../assets/yay.jpg"
import yayWebpImage from "../assets/yay.webp"
</script>
<template>
<div>
<!-- 方式1: 自动检测并使用 WebP -->
<WebPImage :src="yayImage" width="388" alt="图片" />
<!-- 方式2: 手动指定 WebP 文件 -->
<WebPImage
:src="yayImage"
:webp-src="yayWebpImage"
width="388"
alt="图片"
/>
</div>
</template>
在线图片(使用 CDN 转换)
vue
<template>
<!-- 阿里云 OSS -->
<WebPImage
src="https://your-bucket.oss-cn-hangzhou.aliyuncs.com/image.jpg"
cdn-params="x-oss-process=image/format,webp"
width="500"
alt="CDN 图片"
/>
<!-- 腾讯云 COS -->
<WebPImage
src="https://your-bucket.cos.ap-shanghai.myqcloud.com/image.jpg"
cdn-params="imageMogr2/format/webp"
width="500"
alt="CDN 图片"
/>
</template>
在线图片(使用代理服务)
vue
<script setup lang="ts">
const proxyUrl = "https://your-image-proxy.com/convert"
</script>
<template>
<WebPImage
src="https://example.com/image.jpg"
:proxy-url="proxyUrl"
width="500"
alt="在线图片"
/>
</template>
工作原理
1. 构建时转换
- 运行
pnpm build时,会先执行build:images脚本 - 脚本扫描
src/assets目录下的所有 JPG/PNG 图片 - 使用
imagemin-webp转换为 WebP 格式 - 转换后的文件保存在同一目录
2. 运行时加载
- 组件初始化时,使用
getWebPSupportSync()同步获取 WebP 支持状态 - 如果缓存不存在,默认假设支持(避免重复加载)
- 使用
<picture>标签,浏览器自动选择最佳格式 - 后台异步检测,更新缓存供下次使用
3. 性能优化
- 缓存机制 :使用
localStorage缓存检测结果 - 避免重复加载:初始值使用同步方法获取,避免先加载原始图片再加载 WebP
- 智能回退 :使用
<picture>标签,浏览器自动处理回退
效果对比
测试结果显示,yay.jpg (177KB) 转换为 yay.webp (23KB) 后,文件大小减少了 87.0%!
注意事项
- 开发环境 :开发时不会自动转换,需要手动运行
pnpm build:images - Git 管理 :建议将
.webp文件添加到.gitignore,因为它们可以通过构建脚本自动生成 - 质量调整 :可在
scripts/convert-images.mjs中调整质量参数(默认 80) - 浏览器兼容性:现代浏览器都支持 WebP,组件会自动检测并回退
总结
通过以上配置,我们实现了:
- ✅ 构建时自动转换图片为 WebP
- ✅ 智能格式选择和自动回退
- ✅ 性能优化,避免重复加载
- ✅ 组件化封装,使用简单
这套方案可以显著提升页面加载速度,特别是在图片较多的场景下效果明显。希望这篇文章对你有帮助!