想要字型圖示設計師卻給了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="https://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 }) ```

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