想要字型圖示設計師卻給了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="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);
}
}
然後通過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 }) ```
圖示元件的實現部分還是比較簡單的,到這裡圖示部分的詳解就結束了,我們下一篇再見~
- Vue元件庫文件站點的搭建思路
- 想要字型圖示設計師卻給了SVG?沒關係,自己轉
- 這些年我開源的幾個小專案
- 開源的網易雲音樂API專案都是怎麼實現的?
- 關聯線探究,如何連線流程圖的兩個節點
- 我做了一個線上白板!
- 基於Vue2.x的前端架構,我們是這麼做的
- 揭開Vue非同步元件的神祕面紗
- 手把手教你實現在Monaco Editor中使用VSCode主題
- Vue0.11版本原始碼閱讀系列五:批量更新是怎麼做的
- Vue0.11版本原始碼閱讀系列七:補充
- Vue0.11版本原始碼閱讀系列五:批量更新是怎麼做的
- 冬天到了,給你的網站下個雪吧
- 帶你實現一個簡單的多邊形編輯器
- 登入重構小記
- 冬天到了,給你的網站下個雪吧
- 高仿一個echarts餅圖