返回正文更新
在 UmiJS + Vue3 项目中实现 WebP 图片自动转换和优化
前言
WebP 是一种现代图片格式,相比传统的 JPG/PNG 格式,通常可以减少 25-35% 的文件大小,某些图片甚至可以减少 80% 以上。
本文将介绍如何在 UmiJS + 3 项目中实现 WebP 图片的自动转换和智能加载。
功能特性
- ✅ 构建时自动转换:构建时自动将 JPG/PNG 转换为 WebP
- ✅ 智能格式选择:自动检测浏览器支持,优先使用 WebP
- ✅ 自动回退:不支持的浏览器自动使用原始格式
- ✅ 性能优化:使用缓存避免重复检测和重复加载
- ✅ 零配置使用:组件化封装,使用简单
实现步骤
安装依赖
bash
pnpm add -D imagemin imagemin-webp1
创建图片转换脚本
创建 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()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
convert-images.mjs
创建图片工具函数
创建 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
})
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
image.ts
创建 WebP 图片组件
创建 src/components/WebPImage.vue:
Vue SFC
<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>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
WebPImage.vue
配置 类型声明
创建 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
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
images.d.ts
配置 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,
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.umirc.ts
更新 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"
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
package.json
使用方法
基本使用
Vue SFC
<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>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在线图片(使用 CDN 转换)
Vue SFC
<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>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在线图片(使用代理服务)
Vue SFC
<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
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
工作原理
构建时转换
- 运行
pnpm build时,会先执行build:images脚本 - 脚本扫描
src/assets目录下的所有 JPG/PNG 图片 - 使用
imagemin-webp转换为 WebP 格式 - 转换后的文件保存在同一目录
运行时加载
- 组件初始化时,使用
getWebPSupportSync()同步获取 WebP 支持状态 - 如果缓存不存在,默认假设支持(避免重复加载)
- 使用
<picture>标签,浏览器自动选择最佳格式 - 后台异步检测,更新缓存供下次使用
性能优化
- 缓存机制:使用
localStorage缓存检测结果 - 避免重复加载:初始值使用同步方法获取,避免先加载原始图片再加载 WebP
- 智能回退:使用
<picture>标签,浏览器自动处理回退
效果对比
测试结果显示,yay.jpg (177KB) 转换为 yay.webp (23KB) 后,文件大小减少了 87.0%!
注意事项
- 开发环境:开发时不会自动转换,需要手动运行
pnpm build:images - Git 管理:建议将
.webp文件添加到.gitignore,因为它们可以通过构建脚本自动生成 - 质量调整:可在
scripts/convert-images.mjs中调整质量参数(默认 80) - 浏览器兼容性:现代浏览器都支持 WebP,组件会自动检测并回退
总结
通过以上配置,我们实现了:
- ✅ 构建时自动转换图片为 WebP
- ✅ 智能格式选择和自动回退
- ✅ 性能优化,避免重复加载
- ✅ 组件化封装,使用简单
这套方案可以显著提升页面加载速度,特别是在图片较多的场景下效果明显。希望这篇文章对你有帮助!
V 0.12.8 |
基于 MIT Licensed版权所有 © 2009- 2026 CMONO.NET
本站访客数
--次 本站总访问量
--人次 