想要字體圖標設計師卻給了SVG?沒關係,自己轉
持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第3天,點擊查看活動詳情
本文為Varlet組件庫源碼主題閲讀系列第三篇,讀完本篇,你可以瞭解到如何將
svg
圖標轉換成字體圖標文件,以及如何設計一個簡潔的Vue圖標組件。
Varlet
提供了一些常用的圖標,圖標都來自 Material Design Icon。
轉換SVG為字體圖標
圖標原文件是svg
格式的,但最後是以字體圖標的方式使用,所以需要進行一個轉換操作。
處理圖標的是一個單獨的包,目錄為/packages/varlet-icons/
,提供了可執行文件:
打包命令為:
接下來詳細看一下lib/index.js
文件都做了哪些事情。
```js // lib/index.js const commander = require('commander')
commander.command('build').description('Build varlet icons from svg').action(build) commander.parse() ```
使用命令行交互工具commander提供了一個build
命令,處理函數是build
:
```js // lib/index.js const webfont = require('webfont').default const { resolve } = require('path') const CWD = process.cwd() const SVG_DIR = resolve(CWD, 'svg')// svg圖標目錄 const config = require(resolve(CWD, 'varlet-icons.config.js'))// 配置文件
async function build() { // 從配置文件裏取出相關配置 const { base64, publicPath, namespace, fontName, fileName, fontWeight = 'normal', fontStyle = 'normal' } = config
const { ttf, woff, woff2 } = await webfont({
files: `${SVG_DIR}/*.svg`,// 要轉換的svg圖標
fontName,// 字體名稱,也就是css的font-family
formats: ['ttf', 'woff', 'woff2'],// 要生成的字體圖標類型
fontHeight: 512,// 輸出的字體高度(默認為最高輸入圖標的高度)
descent: 64,// 修復字體的baseline
})
} ```
varlet-icons
的配置如下:
js
// varlet-icons.config.js
module.exports = {
namespace: 'var-icon',// css類名的命名空間
fileName: 'varlet-icons',// 生成的文件名
fontName: 'varlet-icons',// 字體名
base64: true,
}
核心就是使用webfont包將多個svg
文件轉換成字體文件,webfont
的工作原理可以通過其文檔上的依賴描述大致看出:
使用svgicons2svgfont包將多個svg
文件轉換成一個svg
字體文件,何為svg
字體呢,就是類似下面這樣的:
```svg
```
每個單獨的svg
文件都會轉換成上面的一個glyph
元素,所以上面這段svg
定義了一個名為geniconsfont
的字體,包含兩個字符圖形,我們可以通過glyph
上定義的Unicode
碼來使用該字形,詳細瞭解svg
字體請閲讀SVG_fonts。
同一個Unicode
在前端的html
、css
、js
中使用的格式是有所不同的,在html/svg
中,格式為&#dddd;
或&#xhhhh;
,&#
代表後面是四位10
進制數值,&#x
代表後面是四位16
進制數值;在css
中,格式為\hhhh
,以反斜槓開頭;在js
中,格式為\uhhhh
,以\u
開頭。
轉換成svg
字體後再使用幾個字體轉換庫分別轉換成各種類型的字體文件即可。
到這裏字體文件就生成好了,不過事情並沒有結束。
```js // lib/index.js const { writeFile, ensureDir, removeSync, readdirSync } = require('fs-extra')
const DIST_DIR = resolve(CWD, 'dist')// 打包的輸出目錄 const FONTS_DIR = resolve(DIST_DIR, 'fonts')// 輸出的字體文件目錄 const CSS_DIR = resolve(DIST_DIR, 'css')// 輸出的css文件目錄
// 先刪除輸出目錄 removeSync(DIST_DIR) // 創建輸出目錄 await Promise.all([ensureDir(FONTS_DIR), ensureDir(CSS_DIR)]) ```
清空上次的成果物,創建指定目錄,繼續:
```js // lib/index.js const icons = readdirSync(SVG_DIR).map((svgName) => { const i = svgName.indexOf('-') const extIndex = svgName.lastIndexOf('.')
return {
name: svgName.slice(i + 1, extIndex),// 圖標的名稱
pointCode: svgName.slice(1, i),// 圖標的代碼
}
})
const iconNames = icons.map((iconName) => "${iconName.name}"
)
```
讀取svg
文件目錄,遍歷所有svg
文件,從文件名中取出圖標名稱和圖標代碼。svg
文件的名稱是有固定格式的:
uFxxx
是圖標的Unicode
代碼,後面的是圖標名稱,名稱也就是我們最終使用時候的css
類名,而這個Unicode
實際上映射的就是字體中的某個圖形,字體其實就是一個“編碼-字形(glyph)”映射表,比如最終生成的css
裏的這個css
類名:
css
.var-icon-checkbox-marked-circle::before {
content: "\F000";
}
var-icon
是命名空間,防止衝突,通過偽元素顯示Unicode
為F000
的字符。
這個約定是svgicons2svgfont規定的:
如果我們不自定義圖標的Unicode
,那麼會默認從E001
開始,在Unicode
中,E000-F8FF
的區間沒有定義字符,用於給我們自行使用private-use-area:
接下來就是生成css
文件的內容了:
```js // lib/index.js
// commonjs格式:導出所有圖標的css類名
const indexTemplate = \
module.exports = [
${iconNames.join(',\n')}
]
// esm格式:導出所有圖標的css類名
const indexESMTemplate = \
export default [
${iconNames.join(',\n')}
]
// css文件的內容
const cssTemplate = \
@font-face {
font-family: "${fontName}";
src: url("${
base64
?
data:application/font-woff2;charset=utf-8;base64,${Buffer.from(woff2).toString('base64')}:
${publicPath}${fileName}-webfont.woff2}") format("woff2"),
url("${
base64
?
data:application/font-woff;charset=utf-8;base64,${woff.toString('base64')}:
${publicPath}${fileName}-webfont.woff}") format("woff"),
url("${
base64
?
data:font/truetype;charset=utf-8;base64,${ttf.toString('base64')}:
${publicPath}${fileName}-webfont.ttf`
}") format("truetype");
font-weight: ${fontWeight};
font-style: ${fontStyle};
}
.${namespace}--set, .${namespace}--set::before { position: relative; display: inline-block; font: normal normal normal 14px/1 "${fontName}"; font-size: inherit; text-rendering: auto; -webkit-font-smoothing: antialiased; }
${icons
.map((icon) => {
return .${namespace}-${icon.name}::before {
content: "\\${icon.pointCode}";
}
})
.join('\n\n')}
`
```
很簡單,拼接生成導出js
文件及css
文件的內容,最後寫入文件即可:
js
// lib/index.js
await Promise.all([
writeFile(resolve(FONTS_DIR, `${fileName}-webfont.ttf`), ttf),
writeFile(resolve(FONTS_DIR, `${fileName}-webfont.woff`), woff),
writeFile(resolve(FONTS_DIR, `${fileName}-webfont.woff2`), woff2),
writeFile(resolve(CSS_DIR, `${fileName}.css`), cssTemplate),
writeFile(resolve(CSS_DIR, `${fileName}.less`), cssTemplate),
writeFile(resolve(DIST_DIR, 'index.js'), indexTemplate),
writeFile(resolve(DIST_DIR, 'index.esm.js'), indexESMTemplate),
])
我們只要引入varlet-icons.css
或less
文件即可使用圖標。
圖標組件
字體圖標可以在任何元素上面直接通過對應的類名使用,不過Varlet
也提供了一個圖標組件Icon,支持字體圖標也支持傳入圖片:
html
<var-icon name="checkbox-marked-circle" />
<var-icon name="http://varlet-varletjs.vercel.app/cat.jpg" />
實現也很簡單:
html
<template>
<component
:is="isURL(name) ? 'img' : 'i'"
:class="
classes(
n(),
[isURL(name), n('image'), `${namespace}-${nextName}`],
)
"
:src="isURL(name) ? nextName : null"
/>
</template>
通過component
動態組件,根據傳入的name
屬性判斷是渲染img
標籤還是i
標籤,圖片的話nextName
就是圖片url
,否則nextName
就是圖標類名。
n
方法用來拼接BEM風格的css
類名,classes
方法主要是用來支持三元表達式,所以上面的:
[isURL(name), n('image'), `${namespace}-${nextName}`]
其實是個三元表達式,為什麼不直接使用三元表達式呢,我也不知道,可能是更方便一點吧。
```ts const { n, classes } = createNamespace('icon')
export function createNamespace(name: string) {
const namespace = var-${name}
// 返回BEM風格的類名 const createBEM = (suffix?: string): string => { if (!suffix) return namespace
return suffix.startsWith('--') ? `${namespace}${suffix}` : `${namespace}__${suffix}`
}
// 處理css類數組 const classes = (...classes: Classes): any[] => { return classes.map((className) => { if (isArray(className)) { const [condition, truthy, falsy = null] = className return condition ? truthy : falsy }
return className
})
}
return { n: createBEM, classes, } } ```
支持設置圖標大小:
html
<var-icon name="checkbox-marked-circle" :size="26"/>
如果是圖片則設置寬高,否則設置字號:
html
<template>
<component
:style="{
width: isURL(name) ? toSizeUnit(size) : null,
height: isURL(name) ? toSizeUnit(size) : null,
fontSize: toSizeUnit(size),
}"
/>
</template>
支持設置顏色,當然只支持字體圖標:
html
<var-icon name="checkbox-marked-circle" color="#2979ff" />
html
<template>
<component
:style="{
color,
}"
/>
</template>
支持圖標切換動畫,當設置了 transition(ms)
並通過圖標的 name
切換圖標時,可以觸發切換動畫:
```html
```
具體的實現是監聽name
屬性變化,然後添加一個改變元素屬性的css
類名,具體到這裏是添加了一個設置縮小為0
的類名--shrinking
:
less
.var-icon {
&--shrinking {
transform: scale(0);
}
}
然後通過css
的transition
設置過渡屬性,這樣就會以動畫的方式縮小為0
,動畫結束後再更新nextName
為name
屬性的值,也就是變成新的圖標,再把這個css
類名去掉,則又會以動畫的方式恢復為原來大小。
html
<template>
<component
:class="
classes(
[shrinking, n('--shrinking')]
)
"
:style="{
transition: `transform ${toNumber(transition)}ms`,
}"
/>
</template>
```ts
const nextName: Ref
const handleNameChange = async (newName: string | undefined, oldName: string | undefined) => { const { transition } = props
// 初始情況或沒有傳過渡時間則不沒有動畫
if (oldName == null || toNumber(transition) === 0) {
nextName.value = newName
return
}
// 添加縮小為0的css類名
shrinking.value = true
await nextTick()
// 縮小動畫結束後去掉類名及更新icon
setTimeout(() => {
oldName != null && (nextName.value = newName)
// 恢復為原本大小
shrinking.value = false
}, toNumber(transition))
}
watch(() => props.name, handleNameChange, { immediate: true }) ```
圖標組件的實現部分還是比較簡單的,到這裏圖標部分的詳解就結束了,我們下一篇再見~
- 面試官問我按鈕級別權限怎麼控制,我説v-if,面試官説再見
- Vue組件庫文檔站點的搭建思路
- 想要字體圖標設計師卻給了SVG?沒關係,自己轉
- 這些年我開源的幾個小項目
- 開源的網易雲音樂API項目都是怎麼實現的?
- 關聯線探究,如何連接流程圖的兩個節點
- 我做了一個在線白板!
- 基於Vue2.x的前端架構,我們是這麼做的
- 揭開Vue異步組件的神祕面紗
- 手把手教你實現在Monaco Editor中使用VSCode主題
- Vue0.11版本源碼閲讀系列五:批量更新是怎麼做的
- Vue0.11版本源碼閲讀系列七:補充
- Vue0.11版本源碼閲讀系列五:批量更新是怎麼做的
- 冬天到了,給你的網站下個雪吧
- 帶你實現一個簡單的多邊形編輯器
- 登錄重構小記
- 冬天到了,給你的網站下個雪吧
- 高仿一個echarts餅圖