WebP介紹
WebP 是 Google 推出的一種同時提供了有損和無損兩種壓縮方式的圖片格式,優勢體現在其優秀的圖像壓縮算法,能夠帶來更小的圖片體積,同時擁有更高的的圖像質量。根據官方說明,WebP 在無損壓縮的情況下能比 PNG 減少26%的體積,有損壓縮的情況能比 JPEG 減少25%-34%的體積。
下圖可以看出,相對於傳統的圖片格式,WebP 格式存在瀏覽器兼容性方面的問題。本文通過工程化的手段來實現 WebP 格式的自適應加載。
傳統做法
為了在前端項目里用上 WebP 格式,並且兼容不支持該格式的瀏覽器,通常的做法是判斷瀏覽器支持性,引入 WebP 圖片或其他通用格式的圖片。針對HTML、JS、CSS 三種引入圖片的場景,有以下幾種處理方式:
HTML
借助
<picture>
<source srcSet="https://p3-imagex.byteimg.com/imagex-rc/preview.jpg~tplv-19tz3ytenx-147.webp" type="image/webp" />
<img decoding="async" loading="lazy" src="https://p3-imagex.byteimg.com/imagex-rc/preview.jpg~tplv-19tz3ytenx-147.jpeg" />
</picture>
JS
通過 JS 判斷瀏覽器是否支持 WebP,若支持則引入 WebP 格式的圖片,若不支持則引入JPEG、PNG等通用格式。有以下兩種判斷方式:
- canvas 判斷
isSupportWebp = document.createElement("canvas").toDataURL("image/webp").indexOf("data:image/webp") === 0;
- 加載 WebP 圖片判斷
function isSupportWebp(callback) {
var img = new Image();
img.onload = function () {
var result = (img.width > 0) && (img.height > 0);
callback(result);
};
img.onerror = function () {
callback(false);
};
img.src = 'data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA';
}
CSS
首先需要判斷瀏覽器是否支持 WebP 格式,若支持則在 HTML 根節點添加類名標識。
document.documentElement.classList.add('webp')
然后利用選擇器的優先級做到 WebP 的自適應加載,CSS 中引入圖片的方式做如下改動:
.img { background-image: url('https://p3-imagex.byteimg.com/imagex-rc/preview.jpg~tplv-19tz3ytenx-147.jpeg') }
.webp .img { background-image: url('https://p3-imagex.byteimg.com/imagex-rc/preview.jpg~tplv-19tz3ytenx-147.webp') }
自動化處理
當項目中有大量本地圖片引入時,手動處理的方式就顯得比較繁瑣,除了要根據圖片引入的方式分別處理,還需要事先將所有圖片轉為 WebP 格式。
考慮到前端項目大多由 Webpack 構建,因此,嘗試開發一款 Webpack 插件支持將項目里的圖片轉為 WebP 格式並且支持圖片格式自適應。
方案設計
首先需要將項目中的圖片轉為 WebP 格式,考慮到本地轉換的耗時較大,這一步更適合放到雲端來做,在雲端處理圖片的服務也相對成熟,大多數雲服務廠商均有提供,且圖片上傳之后也可以顯著減少打包產物的體積。
上傳圖片
第一步是收集項目中的圖片文件上傳至雲端,file-loader 支持將 import/require() 引入的文件寫入到目標文件夾並將文件解析為 url,所以可在 file-loader 的基礎上進行改造,方案是:
- 獲取 loader 匹配到的圖片文件,將圖片上傳至雲端,在雲端生成 WebP 格式的圖片;
- 將原始圖片文件替換為圖片服務生成的URL;
- 若圖片上傳失敗則降級為 file-loader 的處理流程,將圖片傳入輸出文件夾。
插入標記
處理過程中有多個地方依賴瀏覽器對 WebP 的兼容性,所以需要有一個全局的標記。通過在 標簽里注入判斷代碼,若瀏覽器支持 WebP 格式則在根節點添加"webp"類名標識,可以在頁面渲染前獲取瀏覽器對 WebP 格式的兼容性。
前端項目通常會用到 html-webpack-plugin,該插件可以協助創建 HTML 文件並自動引入 Webpack 生成的 bundle,插件中提供有多個 hook,如下圖所示。為確保已經生成了 head 和 body 標簽,可以選擇 在 alterAssetTagGroup 階段注入相關的判斷代碼。
替換圖片
接下來則根據全局標識將圖片替換為相應的 url。根據圖片被引入的位置,分為以下兩種情況:
- 圖片在 JS 中被引入。將圖片模塊替換為一段 JS 代碼,根據類名標識返回 WebP 或者通用格式的圖片 url ;
- 圖片在 CSS 中被引入。若在 CSS 模塊中引入 JS 代碼,一方面會執行出錯,另一方面 css-loader 會在編譯階段執行這段代碼,達不到在瀏覽器端判斷 WebP 兼容性的目的,因此 CSS 部分需要單獨處理。
處理CSS
處理CSS中引入的圖片有兩種方案:
- 利用 CSS 選擇器優先級。在 CSS 中有圖片引入的類后邊插入帶 webp 類選擇器的樣式,原理同手動替換時處理 CSS 的方式。
- 全量生成 WebP 版 CSS。即文件中引入的圖片皆為 WebP 格式,在鏈接樣式文件時判斷根節點類名標識,引入 WebP 版 CSS 或原始 CSS。
第一種方案的缺點是改變了部分樣式的優先級,可能會影響整體樣式,因此采用第二種方案。
方案實現
本文的方案選擇了火山引擎提供的 veImageX 圖片服務來處理圖片。veImageX 是火山引擎提供的圖片整體解決方案,能夠將圖片轉換為 WebP、HEIF、AVIF等多種格式 ,支持從圖片上傳、存儲、處理到分發的完整流程。
圖片上傳到 veImageX 之后,可以快速接入 veImageX 的各項雲端處理能力,如:裁剪、旋轉、濾鏡以及橡皮擦、內容擦除等多項 AI 處理能力,並且可以方便地通過更換 URL 后綴的方式獲取不同格式的圖片。因此,以下方案實現基於 veImageX 展開。
- 接入 veImageX 圖片服務,獲取 accessKey、secretKey 以及服務ID,具體接入方式請參考說明文檔;
- Webpack 插件分為兩部分,在 loader 里上傳並替換圖片,在 plugin 里生成 webp 類名標記並處理 CSS 中引入的圖片。通過 Webpack loader 獲取項目里的圖片文件,借助火山引擎提供的 SDK 將圖片上傳至 veImageX,並將圖片模塊替換為服務生成的 url。基於 veImageX 改變 URL 后綴獲取相應圖片格式的特性,根據圖片被引入的位置做不同的處理。
如下,JS 中引入的圖片根據瀏覽器兼容性來判斷,CSS 中引入的圖片則返回通用格式。
result = `var ret = '';
if (typeof document === 'object') {
var format = '';
document.documentElement.classList.forEach(item => { if (item.match(/__(\w+)__/)) format = (item.match(/__(\w+)__/))[1]})
if (format) {
ret = "//${formatDomain}${imagexUri}~${options.template}${urlParams}." + format;
} else {
ret = "//${formatDomain}${imagexUri}~${options.template}${urlParams}.image";
}
} else {
ret = "//${formatDomain}${imagexUri}~${options.template}${urlParams}.image";
}
${esModule ? 'export default' : 'module.exports ='} ret`;
該部分單獨封裝成了 veimagex-webpack-loader,支持將項目中的圖片上傳至 veImageX,不需要 WebP 自適應能力的可直接使用該loader。
- 在 html-webpack-plugin 的 alterAssetTagGroup hook里插入瀏覽器 WebP 兼容性判斷的代碼,這部分代碼在瀏覽器端執行,若瀏覽器支持 WebP 則在根節點添加"webp"類名標識,如下:
compiler.hooks.compilation.tap('ImagexWebpackPlugin', function (compilation) {
const hooks = self.htmlWebpackPlugin.getHooks(compilation);
hooks.alterAssetTagGroups.tapAsync(
'ImagexWebpackPlugin',
self.checkSupportWebp.bind(self)
);
});
ImagexWebpackPlugin.prototype.checkSupportFormat = function (
htmlPluginData,
callback
) {
htmlPluginData.headTags.unshift({
tagName: 'script',
closeTag: true,
attributes: {
type: 'text/javascript'
},
innerHTML: `
var isSupportFormat = !![].map && document.createElement('canvas').toDataURL('image/${this.options.format}').indexOf('data:image/${this.options.format}') == 0;
if (isSupportFormat) document.documentElement.classList.add('__${this.options.format}__');
`
});
callback(null, htmlPluginData);
};
- 對於 CSS 文件的處理是全量生成 WebP 版 CSS 的方案,所以在 alterAssetTagGroup hook里還需要對 引入的 CSS 文件做處理,將 轉為