拋棄iconfont+symbol, svg在vite+vue的最佳使用方式

語言: CN / TW / HK

iconfont 的缺點

iconfont 共有三種使用方式,分別是 unicode 引用font-class 引用symbol 引用

其中 unicodefont-class 是基於字體的,最嚴重的缺點是被瀏覽器最小 12px 限制和不支持多色圖標

而最推薦的 symbol 基於 svg 的 use 標籤 ,原理是節點的深克隆

symbol 缺點也很簡單:不能自動保持寬高比,內部元素樣式不能被修改,比如修改內部的 path

css svg { fill: #fff; & > g:nth-child(1) > path { /* 修改單條 path 的樣式 */ fill: #666; } & > g:nth-child(2) > path { fill: #aaa; } }

另外 iconfont 對 svg 圖標的管理也存在問題,比如

  • 多色圖標樣式擦除,svg 內的 filter 在上傳的時候會被 iconfont 擦除
  • 每上傳一次文件都要修改項目cdn鏈接,沒有刪除文件也沒有修改文件名,只是新增文件都要修改 cdn 鏈接,沒有向後兼容性
  • 內容審查/其他原因,目前(2022年6月26日) iconfont cdn 無法生成,顯示功能不可用

本地 svg symbol 的缺點

本地 svg 不存在 iconfont cdn 的缺點,本地 svg 一般用 symbol, 但是 symbol 方式卻還是有缺點,目前 symbol 是使用最頻繁的方式,缺點也很明顯:

  • 不能自動保持寬高比
  • 內部元素樣式不能被修改

缺點1: 不能保持寬高比

我們知道 svg 有 viewBox 屬性,比如 一個 svg 有 viewBox="0 0 200 100",即

```html

``` 那麼實際渲染下,svg 的 height 就被自動設置為 `150px` 但是 `symbol` 方式就不可行,使用示例如下 ```html ``` 這裏 height 並不會按照 `viewBox` 屬性比例自動變成 `50px` 也就是説 我們想要 svg 像 img 一樣自動改變寬高是不可行的 比如 `掘金` [文章頁面](https://juejin.cn/post/7112032169102409764) 右側的點贊圖標 svg 就沒有按照 `viewBox` 自動改變寬度 這個頁面的點贊圖標的屬性是 ```html ``` 使用的時候設置了 `height: 100%;`,但是 `width` 並不是和 `height` 相等 你可以打開控制枱,選取點贊圖標這個元素,可以看到確實不等 而大多數的關於使用 svg 的文章都沒有提到這個問題和解決方案 解決方法也很簡單,就是給額外當前使用的 svg 加上 `viewBox="0 0 20 20"` 即可 ## 缺點2: 內部元素樣式不能被修改 如果我們想自定義 svg 內部單條 path 的樣式,`symbol` 方式 無法做到 ```html ``` 無法修改成功,`symbol` 方式內部樣式不能修改,這個沒有好的解決的方法,除非新增 svg 比如説某個 svg 圖標,我們想要它被 hover 時,內部元素變一個色,外部元素變另一個色,`symbol` 方式 只能設置單色圖標,對這種情況就黔驢技窮了 # 什麼是 svg 的最佳使用方式 為什麼不直接在 html 裏使用 svg 呢,直接使用顯然不行 svg 一般都是很長的文本,誰樂意在 vue_template/react_tsx 手動把 svg 複製進去呢 不僅複用性為零,而且某些 svg 內部還有 style 標籤,而 vue_template 裏是不能有 style 標籤的 但是優點也很明顯,如果使用 svg原生tag , 那麼將不存在 `symbol` 方式 帶來的上述兩個缺點 ## 誰在使用 svg原生tag 比如 [github](https://github.com/) 的 svg 圖標在渲染的時候就是 svg原生tag,並不是 `symbol` 方式 不信的話,你打開 github.com 後打開控制枱,選取頁面上的任意一個圖標,就能看到它是 svg原生tag 另外 [愛奇藝](https://www.iqiyi.com/),[騰訊視頻](https://v.qq.com/),[知乎](https://www.zhihu.com/),[bilibili](https://www.bilibili.com/),[twitter](https://twitter.com/home[), [npmjs](https://www.npmjs.com/),[figma](https://www.figma.com/),[stackoverflow](https://stackoverflow.com/) 也在使用 接下來就説明如何使用 vue 的組件來封裝 svg原生tag ## 如何封裝 svg原生tag 的 vue組件 在 vite 裏面,我們需要一個插件 [vite-svg-loader](https://github.com/jpkleemans/vite-svg-loader) `vite-svg-loader` 的原理也很簡單 - 在 代碼裏 import svg 的時候讀取 svg - 用 [svgo](https://github.com/svg/svgo) 對 svg 預處理,比如處理 svg 內部的 style 標籤等其他優化 - 然後把這個 svg 轉成 vue 組件 這樣當你在 vue 裏 import 這個 svg 時,就是在使用一個 vue 組件,使用示例如下 ```ts // ./vite.config.ts import vue from '@vitejs/plugin-vue'; import { defineConfig } from 'vite'; import svgLoader from 'vite-svg-loader'; export default defineConfig(({ command, mode }) => { return { plugins: [ vue(), svgLoader(), ], }; }); ``` ```html ``` 這樣實際渲染出來,我們在使用 svg 的地方就是 svg原生tag,因此它和直接把 svg 的代碼直接複製到 vue 組件裏沒有區別,它能自動適應寬高比,也能單獨修改內部單個 path 的樣式 但是這種方式不僅保留了原生 svg 的優點,而且大大提升了 svg 的複用性 ## 進一步的優化 實際上,上述例子還有很多可優化的點 ### 優化1: 按 name 使用 name.svg 我們知道 `symbol` 方式是這樣使用的 ```html ``` 一般我們會對它封裝 vue 全局組件,然後傳入 name=`icon-xxx`,那麼這個 svg原生tag 組件也可以這樣封裝 由於此時的 svg 變成了一個個組件,我們想根據 name 來動態使用組件,於是解決方法就是 vue 的動態使用組件 我們先將所有的 svg 文件放在 `@/assets/svg/` 目錄下,然後封裝一個 `SvgRaw.vue` 的全局組件 ```html ``` 全局註冊該組件後,使用方式如下,我們不再需要對每個 svg 單獨 import ```html ``` ### 優化2: 組件樣式的 scoped 我們知道當設置 `style scoped` 時,`template` 的每個節點會加上 `data-v-hash`,`style` 的選擇器也會加上 `[data-v-hash]`,達到不同組件的樣式隔離的效果 如果我們想在 SvgRaw 組件的 style 給 svg 加一些統一的樣式,比如 ```css svg { width: auto; height: auto; } * { // 顏色漸變效果 transition: fill 250ms; } ``` 顯然,為了不使這個樣式污染全局樣式,我們需要設置 style scoped 但是由於我們的 svg 的動態加載的獨立組件,內部的節點並不會被加上 `data-v-hash`,也就無法被 css 選擇器選中 因此,我們需要手動給 svg 組件加上 `data-v-hash` ```html ``` ### 優化3: 避免不同 svg 內部 filter id 衝突 某些 svg 效果比較複雜,內部一般會有 `filter` 節點,比如 ```svg ``` 可以看到 `filter0_d_359_2426` 應該是一個全局唯一的 id,如果其他 svg 內部也有這個 id ,會造成樣式衝突,我們想要的效果是 每個 svg 內部樣式應該是獨立隔離,互不影響的 因此,我們需要對 svg 內部的 id 做唯一化處理,這個可以直接在 `vite-svg-loader` 配置 svgo 來解決 ```ts // ./vite.config.ts import vue from '@vitejs/plugin-vue'; import { defineConfig } from 'vite'; import svgLoader from 'vite-svg-loader'; export default defineConfig(({ command, mode }) => { return { plugins: [ vue(), svgLoader({ svgoConfig: { plugins: [ { name: 'cleanupIDs', params: { prefix: { // 避免不同 svg 內部的 filter id 相同導致樣式錯亂 // https://github.com/svg/svgo/issues/674#issuecomment-328774019 toString() { let count: number = this.count ?? 0; count++; this.count = count; return `svg-random-${count.toString(36)}-`; }, } as string, }, }, ], }, }), ], }; }); ``` ## 總結 我們闡述了 iconfont 以及 symbol 方式使用 svg 的缺點,同時説明了使用 svg 原生節點可以解決這些缺點 但是 svg 原生節點 使用不便,於是我們使用一些工具封裝了 svg 原生節點的 vue 組件,讓它使用便利性與原來一致 同時針對 vue 組件做了 css scoped 和 id 唯一化 的優化