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 檔案做處理,將 轉為