想要字體圖標設計師卻給了SVG?沒關係,自己轉

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 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在前端的htmlcssjs中使用的格式是有所不同的,在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是命名空間,防止衝突,通過偽元素顯示UnicodeF000的字符。

這個約定是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.cssless文件即可使用圖標。

圖標組件

字體圖標可以在任何元素上面直接通過對應的類名使用,不過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); } }

然後通過csstransition設置過渡屬性,這樣就會以動畫的方式縮小為0,動畫結束後再更新nextNamename屬性的值,也就是變成新的圖標,再把這個css類名去掉,則又會以動畫的方式恢復為原來大小。

html <template> <component :class=" classes( [shrinking, n('--shrinking')] ) " :style="{ transition: `transform ${toNumber(transition)}ms`, }" /> </template>

```ts const nextName: Ref = ref('') const shrinking: Ref = ref(false)

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 }) ```

圖標組件的實現部分還是比較簡單的,到這裏圖標部分的詳解就結束了,我們下一篇再見~