# 基于 webp 的 H5 项目
# 什么是 webp
webp 是 Google 推出的一种同时提供了有损压缩与无损压缩(可逆压缩)的图片文件格式。
# webp 的优点
提供了有损压缩与无损压缩(可逆压缩)
具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量
具备了无损和有损的压缩模式、Alpha 透明以及动画的特性
转化效果都相当优秀、稳定和统一
# webp 的兼容性
整体上看 webp 的兼容性的表现还是不错的;谷歌全面支持、安卓浏览器从 4.2 开始支持,safari 再 14 版本之后全部支持。

兼容性具体信息请看这里 (opens new window)
# 为什么要用 webp
移动端网页的加载速度对用户体验极为重要的,虽然网络速度越来越快了,但是很多项目中为了更丰富的视觉体验,图片资源越来越多,体积越来越大;为了更快的呈现页面,必须提升页面加载速度;提升加载速度一般从两个方面考虑,一方面请求的响应速度要足够快,另一方面要尽量减小传输的数据量。
那么为什么是 webp 呢?因为相比于其他相同大小、不同格式的压缩图像,webp 格式的图片拥有更小的体积以及更高的质量,优势十分明显。
# webp 格式转换
webp 格式的图片如何得到呢?google 官方推出了用于将 PNG、JPEG、TIFF 或原始 Y'CbCr 格式的文件压缩转换为 webp 格式的命令行编码工具 -- cwebp;
使用方法如下:
cwebp [options] fromPath -o toPath.webp
其中 options是配置参数,常用配置如下:
- -o [String]: 制定输出文件名称。如果省略,cwebp 将进行压缩,输出静态报告,使用 “-” 作为一个输出名称,将直接输入到文件
- -lossless: 无损压缩
- -q [float]: 质量进度,取值范围 0 ~ 100, 默认 75
cwebp -lossless fromPath -o toPath.webp // 无损压缩转换
cwep -q 90 fromPath -o toPath.webp // 质量90%的压缩转化
formPath 是源文件路径,toPath 是转换后的文件存放路径加文件名称,.webp 转换的文件类型;当然,cwebp 不仅仅支持转成 webp 格式。
此方案通过命令来进行进行图片压缩转换,操作便捷,大大的提升效率,但是如果需要批量压缩,需要使用脚本读取文件夹下所有文件,并遍历执行以上命令,这对一般的前端开发同学来说不太友好;且仅能通过静态资源服务的方式被使用;即将压缩后的图片放在静态资源服务器上,前端项目中所有图片资源链接都为静态资源服务链接;方案可行,但是代价挺高;
# webp 转换方案
# 本地压缩上传
本地压缩上传指的是再本地将图片压缩转换成 webp 图片,然上传至服务至上;当然本地压缩肯定不能是手动一张一张的去压缩转换的;况且,即使是本地压缩完了上传到服务器上之后,你的项目里对图片的应用地址也是不正确的,所以根本无法达到我们要做的优化的效果;
# Node.js 服务使用 cwebp
首先,我们在我们的项目中安装cwebp-bin模块; cwebp-bin模块提供了 Node.js 使用 cwebp 能力进行图片压缩转码的接口,我们的图片压缩服务引入该模块模块实现常见格式图片到 webp 的转码;
npm install -g cwebp-bin
再 node.js 中使用(创建一个 webp-loader.js 放在项目的根目录下):
// webp-loader.js
const { execFileSync } = require('child_process')
const webp = require('cwebp-bin')
const path = require('path')
// 开启子进程,执行webp
execFileSync(webp, [
'-q', // 开启压缩
'90', // 压缩率
path.join(__dirname, 'assets/logo.png'), // 源文件路径
path.join(__dirname, 'assets/logo.webp') // 生成文件的保存路径
])
这里的我们将项目 assets 文件夹下的 logo.png 转换成了 webp; 如果需要批量转换,我们就需要用到 node 的 fs 模块,先使用fs.readdirSyn函数;将读到的文件全部遍历出来一个个执行转换操作;
# 如何在项目中集成 cwebp
vue-cli 项目中,安装 cwebp-bin 模块即可
npm install cwebp-bin -D
# 简单的 webp-loader 实现
直接上代码:
const { execFileSync, exec } = require('child_process')
const webp = require('cwebp-bin')
const path = require('path')
const loaderUtils = require('loader-utils')
module.exports = function (content) {
// loader的配置和内容生成一个文件名,该文件名即打包出来之后的图片文件名以及项目运行中使用的文件名
const url = loaderUtils.interpolateName(this, this.query.name, {
content: content,
regExp: this.query.regExp
})
// 生成的文件存放路径
// 放在项目根目录下的webp目录中
const webpUrl = '/webp/' + url.substring(0, url.lastIndexOf('.')) + '.webp'
// 判断webp目录存在不存在,不存在就创建webp目录
if (!fs.existsSync(path.join(__dirname, 'webp'))) {
fs.mkdirSync(path.join(__dirname, 'webp'))
}
// 执行图片转换操作
execFileSync(webp, [
'-q',
this.query.quality || '75',
this.resourcePath,
'-o',
path.join(__dirname, webpUrl)
])
return content
}
module.exports.raw = true
对 loader 还不是很熟悉的同学请到 webpack 官网看一下loader (opens new window)的 api;
module.exports.raw = true 默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置 raw 为 true,loader 可以接收原始的 Buffer。每一个 loader 都可以用 String 或者 Buffer 的形式传递它的处理结果。complier 将会把它们在 loader 之间相互转换。
我们要读取的是图片类型的文件,loader 函数接收的 content 是 buffer 类型的,所以我们需要在我们的 loader 底部添加 module.exports.raw = true;
图片转换完成之后,我们要做的就是将转换后的图片放到项目打包之后的 dist目录下的 img 图片目录下;所以接下来还需要利用 node.js 将 webp 目录下的 webp 格式的图片移到 dist/img 目录下;
也很简单,直接上代码:
// webp-loader.js
......
// 判断dist/img目录是否生成
fs.readdir(path.join(__dirname, 'dist/img'), (err, paths) => {
if (err) return
const toPath = path.join(__dirname, 'dist/img')
const fromPath = path.join(__dirname, 'webp')
// 执行shell脚本,将webp目录下所有的文件全部复制到dist/img下
exec(`cp -rf ${fromPath}/* ${toPath}`, (err) => {
if (err) {
console.log(err)
} else {
// 复制完成之后删除webp目录,避免每次构建产生的文件重复,且webp目录不能被提交到git上,所以删除webp目录
exec(`rm -rf ${fromPath}`)
}
})
})
到这里,我们的 webp 格式转换的 loader 的基本功能代码已经全部实现了,接下来看看如何在 vue-cli3.0 创建的项目中挂载 webp-loader 加载器。
# 挂载 webp-loader
module.exports = {
chainwebpack(config) {
config.module
.rule('webp')
.test(/\.(gif|png|jpe?g)$/i)
.use()
.loader('./utils/webp-loader.js')
.options({
include: /src/, // 只针对src目录下的图片做转换
name: '[name].[hash:8].[ext]', // 文件名称的生成规则
quality: 80 // 图片生成的压缩率
})
}
}
# 页面图片引用逻辑控制
上面,图片的转换我们已经搞定了,接下来就是在页面上处理图片应用的问题了;到底什么时候该使用 webp 什么使时候使用源文件呢?首先考虑几个问题:
宿主浏览器是否支持 webp 格式
对应的 webp 格式的图片文件是否存在
# 宿主浏览器是否支持 webp
// 判断浏览器是否支持webp
const supportwebp = (function () {
const canvas = typeof document === 'object' ? document.createElement('canvas') : {}
canvas.width = canvas.height = 1
return canvas.toDataURL ? canvas.toDataURL('image/webp').indexOf('image/webp') === 5 : false
})()
// 将是否支持webp的标识挂在到vue原型上,任何vue组件中通过this._iswebp_来获取
Vue.prototype._iswebp_ = supportwebp
# 对应的 webp 图片是否存在
// 直接通过img对象来加载webp图片的路径看是否能加载成功,如果成功,这表示对应的图片存在
let webpSrc
let img = new Image()
img.src = webpSrc
img.onload = () => {
// 执行图片加载成功的操作
}
所以,第一步我们判断完了浏览器是否支持 webp 后将 supportwebp 挂载到全局上(window/Vue.prototype);然后我们可以在我们的项目中创建一个全局的自定义指令;取什么名字没所谓,这里我们就用v-webp;
对 Vue 指令还不太熟悉的可以看看官方文档-自定义指令 (opens new window)。
# v-webp 指令的实现
代码如下:
Vue.directive('webp', {
bind(el, { context }) {
// 判断浏览器是否支持webp格式图片,不支持直接什么都不做return掉
if (!context._iswebp_) return
const imgSrc = el.attributes.src.nodeValue
const webpSrc = `${imgSrc.substr(0, imgSrc.lastIndexOf('.'))}.webp`
let img = new Image()
img.src = webpSrc
// webp格式图片资源存在,加载成功之后替换element的原有src,
// 不存在什么都不用做,直接加载源文件
img.onload = function () {
el.src = webpSrc
}
}
})
定义一个全局的自定义指令;取名 webp;在需要做类型转换的 image 标签上添加 v-webp 指令即可;该指令在初次绑定到 image 上时读取 image 组件的 src 属性;通过 context 读取 image 的上下文,通过上述绑定的isWebp字段判断是否需要加载 webp 图片替换原图片;然后再利用 new Image 对象生成一个图片对象,利用这个图片对象的 onload 方法判断该图片对应的 webp 格式图片是否存在;存在,则将 webp 格式图片地址替换元图片地址赋值个 image;不存在,不做替换,依旧使用原图片;通过指令,项目可以灵活的根据宿主浏览器的兼容性来做格式的切换。
# 在组件中使用 v-webp
<template>
<img src="./assets/logn.png" v-webp />
</template>