# Why
公司專案使用的 Icon 是從 figma 下載 SVG Icon 放到專案中
若是不需要更改顏色的 Icon 可以直接用 img 來呈現
<img src=".../xxx.svg">
但總是會有需要改顏色的情境 (ex: disabled 要用灰色之類)
以 img tag 的方式使用無法更改顏色
用 Inline SVG 複製貼上醜又難維護
<template>
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
...可能很長很長...
</g>
</svg>
...其他 html DOM
</div>
</template>
因此使用第三方套件 vite-plugin-vue2-svg
(專案為 vite + vue2)
將指定資料夾中的 SVG 檔案打包成 Vue Component
使用時會 render Inline SVG,便可透過 fill: currentColor
+ text color
更改顏色
<template>
<component :is="svgComponent" :width="width" :height="height" />
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from '@vue/composition-api'
export default defineComponent({
name: 'SvgIcon',
props: {
name: { type: String, required: true },
width: { type: String },
height: { type: String, default: '100%' },
},
setup(props) {
const svgComponent = defineAsyncComponent(() => import(`/icons/${props.name}.svg`))
return {
svgComponent,
}
},
})
</script>
<style lang="scss" scoped>
::v-deep {
svg:not([fill^='url']),
path:not([fill^='url']),
g:not([fill^='url']) {
fill: currentColor;
}
}
</style>
p.s. vite-plugin-vue2-svg 預設會將 viewBox 拿掉
這會使得 icon 沒辦法用 width, height 更改大小
因此要將 removeViewBox
設為 false
// vite.config.ts
import { createSvgPlugin } from 'vite-plugin-vue2-svg' // only for vue2.x
export default defineConfig({
plugins: [
createSvgPlugin({
svgoConfig: {
plugins: [
{
// ref. https://github.com/svg/svgo#default-preset
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
},
},
},
],
},
}),
]
})
但 vite-plugin-vue2-svg 只有 vue 2
& vite 2
能夠使用
考慮到未來或許會升級 vite3 (vue 3),需要捨棄 vite-plugin-vue2-svg
因此尋找能更優雅使用 SVG Icon 的方式
尤其是想要能夠像 Font Icon 一樣透過 css class 簡單好讀
# Approach
# 1. Font Icon
可藉由 IcoMoon (opens new window) 提供的服務把 SVG 打包成 Font Icon
- 優點:
- 不需要安裝額外的第三方套件
- 能使用 font-size, color 調整大小、顏色
- 缺點:
- 由於是 Font,會受到所有跟 Font 有關的 css 影響 ( line-height, letter-space 之類 )
- 佔用偽類別
- 每次有新增 Icon 都要重新生成 Font Icon
- Font Icon 無法透過 VS Code 的插件「SVG Preview」預覽
# 2. SVG Sprite
可藉由 SVG Sprites (opens new window) 提供的服務把所有 SVG 打包成一個 SVG Sprite 檔案
- 優點:
- 不需要安裝額外的第三方套件
- 用 Inline SVG 但寫起來簡短
<svg>
<use xlink:href="#{name}"></use>
</svg>
- 缺點:
- 每次新增 Icon 時都要重新生成 SVG Sprite
- SVG 數量越多 Sprite 檔案越大
- SVG Sprite 無法透過 VS Code 的插件「SVG Preview」預覽
# 3. Pure CSS Icon
Vue, Vite 的核心成員,大神 Anthony Fu 的這篇文 聊聊纯CSS 图标 (opens new window)
使用 css 屬性 mask-image
將 SVG 作為遮罩圖層使用
將 background 設為 currentColor,就能夠用 css 屬性 color 去更改顏色
寬高設為 1em 就能夠讓 SVG Icon 依據 font-size 改變大小
如此便擁有了 Font Icon 的優點
能夠像 Font Icon 一樣以 css class 指定 icon
且又不會受到其他 Font css 的影響
簡直太神啦 🙏
# Mask-Image 效果展示
但為了使用 Pure CSS Icon 額外引入 UnoCSS 與相關套件反而違反原本想盡量減少第三方套件的原意 🤔
何況專案已經有使用 WindiCSS
了
恩?那何不乾脆用 WindiCSS 提供的 plugins 功能
將專案中的 SVG 檔案參考 UnoCSS Pure CSS Icon 的方法生成個別的 class
// windi.config.ts
import { defineConfig } from 'windicss/helpers'
import plugin from 'windicss/plugin'
export default defineConfig({
plugins: [
plugin(({ addComponents }) => {
// TODO:
}),
],
})
首先是從放置 SVG 的 /icons
資料夾中讀取全部 SVG File 的檔名
const svgIcons = fs
.readdirSync(path.resolve(__dirname, '/icons'))
.filter(fileName => fileName.includes('.svg'))
.map(fileName => ({ name: fileName.replace('.svg', ''), url: `/icons/${fileName}` }))
分成
- i-{name}?mask 用 mask-image,可以更換顏色的 Icon
如果遇到不支援 mask-image 的則用 background - i-{name} 用 background-image,不能更換顏色的 Icon
svgIcons.forEach(({ className, url }) => {
const className = `.i-${name}`
addComponents({
[`${className}\\?mask`]: {
'display': 'inline-block',
'background-image': `url("${url}")`,
'background-repeat': 'no-repeat',
'background-size': '100% 100%',
'background-color': 'transparent',
'width': '1em',
'height': '1em',
'@supports (-webkit-mask-image: url(#mask)) or (mask-image: url(#mask))': {
'mask-size': '100% 100%',
'mask-repeat': 'no-repeat',
'mask-image': `url("${url}")`,
'background': 'currentColor',
},
},
[className]: {
'display': 'inline-block',
'background-image': `url("${url}")`,
'background-repeat': 'no-repeat',
'background-size': '100% 100%',
'background-color': 'transparent',
'height': '1em',
'width': '1em',
},
})
})
p.s. url 的部分也可以轉成 base64 字串,將 SVG 內容本身一起打包進 css
並且可以在轉換時判斷該 SVG 是單色還是多色 SVG(可依據 fill 是不是 currentColor),單色的就可以使用 mask-image
這樣就不需要產生 i-{name}?mask
、i-{name}
兩種 class
完成了以 SVG filename 作為命名的 class
優點:
- 不需要安裝額外的第三方套件 ( 除了 WindiCSS
- 能使用 font-size, text color 調整大小、顏色
- 不會受到其他 font css 屬性的影響
- 不需要佔用偽類別
- 可以個別新增/更換 svg 檔案
- 因為是 class,使用 vuetify 這樣的 ui framework 時
可利用xxx-icon
這類的 props 去替換成自己的 icon,就不需要用 slot 另外寫了
缺點:
- mask-image 支援度問題,雖然目前主流瀏覽器的最新版都有支援
# But
本來覺得一切都很順利,可以直接用在專案上
卻發現一個 mask-image + firefox 的已知 render bug
用了 mask-image 的 DOM 有 rotate 時,會跑出預期外的框線 😭
2 年前就有人回報,但還沒修好
https://bugzilla.mozilla.org/show_bug.cgi?id=1671784 (opens new window)
https://bugzilla.mozilla.org/show_bug.cgi?id=1764056 (opens new window)
雖然 Firefox 的使用人數相較之下算少
但已知會有 Bug,就只能放棄直接用在專案上了
反正都研究了,留個紀錄