为什么不能自己DIY一个iconfont库?

1,144 阅读5分钟

起初

前段时间由于阿里巴巴的字体库升级,也着实带来了不小的麻烦,图标无法上传,反馈没有入口等等,由此我心生一个新的想法,为什么不实现一个自己的iconfont图标库?

但又技术短浅,像是老虎咬刺猬无从下口,于是各种翻资料,偶然间查到思否上有位大佬封装的插件svgtofont,但感觉还不够自定义,就把代码 Clone 下来看一下底层实现,结果发现是 svgicons2svgfont

以下是参考svgtofont实现思路做的融合改变,技术浅薄大佬勿喷,如有更好的方案欢迎留言!

项目地址

为什么要使用 icon?

若使用图片的方式来实现图标的话,一方面操作样式会很繁琐,如果需要修改图标的颜色、大小等等,那么就需要重新生成一个新的图标,另一方面就是图片很容易失真,变得模糊不清或者变形等,而使用icon的方式会让修改样式、调整大小、渐变颜色等等变的轻而易举,因为icon是使用svg转换的,同时svg又是矢量图,众所周知矢量图是不会存在失真现象的,并且使用起来也是简单上手,再也不需要去担心由于项目中的图片过多而导致项目打包的时候打包文件会很多,或者在网络请求图片时造成的加载缓慢等情况了!

项目目录结构

.
├── font
│   ├── iconfont.svg
├── svg
│   ├── test.svg (svg文件)
├── template
├── index.ejs (EJS模板文件)
├── iconfont.js
└── package.json

实现流程

svgicons2svgfont 会根据指定的项目中的svg文件地址将svg文件集合到 iconfont.svg 中,但仅仅生成 iconfont.svg 文件是不利于使用的,那么就需要使用 svg2ttf 生成 iconfont.ttf 文件,TTF,全称(TrueTypeFont)是Apple公司和Microsoft公司共同推出的字体文件格式;而为了兼容浏览去,我们需要将 iconfont.ttf 文件转换为 .woff 格式,WOFF文件是Web开放字体格式(Web Open Font Format)是一种网页所采用的字体格式标准,因为我们要借助于 ttf2woffttf2woff2 两个插件;接着我们要利用 ttf2eot 将字体进行压缩,.eot 文件是一种压缩字库,目的是解决在网页中嵌入特殊字体的难题。

接下来就需要引入字体库,将文件引入到css中

@font-face {
    font-family: "iconfont";
    src: url('生成的iconfont.ttf文件路径') format('truetype'),
    url('生成的iconfont.woff') format('woff'),
    url('生成的iconfont.woff2') format('woff2'),
    url('生成的iconfont.svg文件路径') format('svg');
}

.iconfont {
    display: inline-block;
    font-family: "iconfont" !important;
    font-size: 16px;
    font-style: normal;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

安装 svgicons2svgfont

  • 新建文件
mkdir svg-font && npm init
cd svg-font
  • 新建 iconfont.js,将svg文件存放在 svg 目录文件下
  • 安装
npm i svgicons2svgfont fs-extra ejs -D

至于文档目前也就只能依赖 NPM 官网的先看看了svgicons2svgfont

  • 使用
const ejs = require('ejs')   // # ejs 可以是通过EJS模板生成文件
const path = require('path')
const fs = require('fs')
const fextra = require('fs-extra')  // 用于文件读写,如果不需要ejs也可以使用 fs 或者 fs-extra 写入
const SVGTOFONT = require("svgicons2svgfont")
  • 使用 svgicons2svgfont 以流的方式生成 svg 目标文件
let unicodeObj = {}
const generateIconfont = () => {
    return new Promise((resolve, reject) => {
        const iconFontStream = new SVGTOFONT({
            fontName: 'iconfont',
            fontHeight: 1024,
            metadata: 'TOM',
            normalize: true     //  元数据标签的内容,很重要,不设置则会导致图标大小不一
        })

        //  设置字体目标
        iconFontStream.pipe(fextra.createWriteStream('./font/iconfont.svg'), { flags: 'a' })
            .on('finish', () => { console.log('font create success'); })
            .on('error', (error) => { console.log('error',error); })

        //  获取图标文件路径
        const svgDir = path.resolve(process.cwd(), "./svg/");
        //  读取.svg文件
        setTimeout(() => {
            const svgFiles = fextra.readdirSync(svgDir, 'utf-8')
            console.log('svgFiles',svgFiles)
            //  格式化svg文件对象
            let svgFileList = []
            //  设置默认Unicode码
            let startUnicode = 0xea01
            svgFiles.forEach(item => {
                svgFileList.push({ file: item })
            })
            svgFileList = [...new Set(svgFileList)]
            //  写入glyphs
            svgFileList.map((item, index) => {
                const [svgFileName] = item['file'].split('.')
                item.iconName = svgFileName
                item[svgFileName] = fextra.createReadStream(svgDir + '/' + item['file'])
                item[svgFileName].metadata = {
                    unicode: [String.fromCharCode(startUnicode++)],
                    name: svgFileName,
                };
                
                // 记录icon数据
                const [iconUnicode] = item[svgFileName]['metadata']['unicode']
                unicodeObj[svgFileName] = iconUnicode
                unicodeObj['name'] = svgFileName
                unicodeObj['unicode'] = `&#${iconUnicode.charCodeAt(0)};`
                return item
            })

            //  写入流
            svgFileList.forEach(item => {
                iconFontStream.write(item[item['iconName']])
            })
            //  关闭流
            iconFontStream.end()
            resolve()
        }, 1000)
    })
}

generateIconfont()

执行命令

node ./iconfont.js

执行成功

这样就可以在 font 文件夹中看到有一个 iconfont.svg 文件了;

安装转化插件

npm i svg2ttf ttf2woff ttf2woff2 ttf2eot -D

从字面意思上应该可以知道这些是做什么的吧?

  • svg2ttf 将 svg 文件转化为 ttf 文件
  • ttf2woff 将 ttf 文件转化为 woff 文件
  • ttf2woff2 将 ttf 文件转化为 woff2 文件
  • ttf2eot 将 ttf 文件转化为 eot 文件

看到这是不是感觉生成的文件和阿里上生成的文件很相似了?

声明文件存储常量

这样做的目的就是便于有效的管理文件存储的路径,当然,也可以用单独的文件进行封装

const SVGTOTTF = require('svg2ttf')
const SVGTOWOFF = require('ttf2woff')
const SVGTOWOFFTWO = require('ttf2woff2')
const SVGTOEOT = require('ttf2eot')

const DIST_TTF = path.resolve(process.cwd(), "./fonts/iconfont.ttf")
const DIST_WOFF = path.resolve(process.cwd(), "./fonts/iconfont.woff")
const DIST_WOFF_TWO = path.resolve(process.cwd(), "./fonts/iconfont.woff2")
const DIST_EOT = path.resolve(process.cwd(), "./fonts/iconfont.eot")

生成 ttf 文件

/** 生成TTF文件 **/
const generateIconTTF = async () => {
    return new Promise(async resolve => {
        let iconttf = SVGTOTTF(await fextra.readFileSync(path.join(__dirname, "font/iconfont.svg"), 'utf-8'), {})
        fextra.writeFileSync(DIST_TTF, new Buffer.from(iconttf.buffer), error => {
            if (error) return
            console.log('is create TTF success')
        })
    })
}

生成 woff 文件

/** 生成WOFF文件 **/
const generateIconWoff = () => {
    return new Promise(async (resolve, reject) => {
        let iconwoff = SVGTOWOFF(await fextra.readFileSync(DIST_TTF))
        fextra.writeFileSync(DIST_WOFF, new Buffer.from(iconwoff.buffer), (error, res) => {
            if (error) {
                reject(error)
                return
            }
            console.log('is create ttf success')
            resolve(true)
        })
    })
}

生成 woff2 文件

/** 生成WOFF2文件 **/
const generateIconWoffTwo = () => {
    return new Promise(resolve => {
        let iconwoff2 = SVGTOWOFFTWO(fextra.readFileSync(DIST_TTF))
        fextra.writeFileSync(DIST_WOFF_TWO, new Buffer.from(iconwoff2.buffer), (error, res) => {
            if (error) return
            console.log('is create ttf success')
        })
        resolve()
    })
}

生成 eot 文件

/** 生成EOT文件 **/
const generateIconEot = () => {
    // const DIST_EOT = path.resolve(process.cwd(), "./font/iconfont.eot")
    return new Promise(resolve => {
        let iconeot = SVGTOEOT(fextra.readFileSync(DIST_TTF))
        fextra.writeFileSync(DIST_EOT, new Buffer.from(iconeot.buffer), (error, res) => {
            if (error) return
            console.log('is create eot success')
        })
        resolve()
    })
}

如果想生成一个图标列表的话,那么就需要借助 ejs

/** 生成可视文件 **/
const generatePage = () => {
    return new Promise(async resolve => {
        const tempPath = path.join(__dirname, "/template/index.ejs")
        const savePath = path.join(__dirname, "/font/index.html")
        const temp = await generateHtml(tempPath, { icons: JSON.stringify(unicodeObj) })
        fextra.outputFileSync(savePath, temp)
    })
}

/** 生成HTML文本 **/
const generateHtml = (savePath, options) => {
    return new Promise((resolve, reject) => {
        ejs.renderFile(savePath, options, (error, str) => {
            if (error) reject(error);
            resolve(str);
        });
    })
}

执行生成

const generateInit = async () => {
    const is_ok = await generateIconfont()
    if (is_ok) {
        setTimeout(() => {
            generateIconTTF()
            generateIconWoff()
            generateIconWoffTwo()
            generateIconEot()
            generatePage()
        }, 500)
    }
}

generateInit()

完整代码

const ejs = require('ejs')
const path = require('path')
const fs = require('fs')
const fextra = require('fs-extra')
const SVGTOFONT = require("svgicons2svgfont");
const SVGTOTTF = require('svg2ttf')
const SVGTOWOFF = require('ttf2woff')
const SVGTOWOFFTWO = require('ttf2woff2')
const SVGTOEOT = require('ttf2eot')

const DIST_TTF = path.resolve(process.cwd(), "./font/iconfont.ttf")
const DIST_WOFF = path.resolve(process.cwd(), "./font/iconfont.woff")
const DIST_WOFF_TWO = path.resolve(process.cwd(), "./font/iconfont.woff2")
const DIST_EOT = path.resolve(process.cwd(), "./font/iconfont.eot")
let unicodeObj = {}

const generateIconfont = () => {
    return new Promise(resolve => {
        const iconFontStream = new SVGTOFONT({
            fontName: 'iconfont',
            fontHeight: 1024,
            metadata: 'TOM',
            normalize: true     //  元数据标签的内容,很重要,不设置则会导致图标大小不一
        })

        //  设置字体目标
        iconFontStream.pipe(fs.createWriteStream('./font/iconfont.svg'))
            .on('finish', () => { console.log('font create success'); })
            .on('error', (error) => { console.log('error',error); })

        //  获取图标文件路径
        const svgDir = path.resolve(process.cwd(), "./svg/");
        //  读取.svg文件
        setTimeout(() => {
            const svgFiles = fs.readdirSync(svgDir, 'utf-8')
            console.log('svgFiles',svgFiles)
            //  格式化svg文件对象
            let svgFileList = []
            //  设置默认Unicode码
            let startUnicode = 0xea01
            svgFiles.forEach(item => {
                svgFileList.push({ file: item })
            })
            svgFileList = [...new Set(svgFileList)]
            //  写入glyphs
            svgFileList.map((item, index) => {
                const [svgFileName] = item['file'].split('.')
                item.iconName = svgFileName
                item[svgFileName] = fs.createReadStream(svgDir + '/' + item['file'])
                item[svgFileName].metadata = {
                    unicode: [String.fromCharCode(startUnicode++)],
                    name: svgFileName,
                };
                const [iconUnicode] = item[svgFileName]['metadata']['unicode']
                unicodeObj[svgFileName] = iconUnicode
                unicodeObj['name'] = svgFileName
                unicodeObj['unicode'] = `&#${iconUnicode.charCodeAt(0)};`
                return item
            })

            //  写入流
            svgFileList.forEach(item => {
                iconFontStream.write(item[item['iconName']])
            })
            //  关闭流
            iconFontStream.end()
            resolve(true)
        }, 1000)
    })
}

/** 生成TTF文件 **/
const generateIconTTF = async () => {
    return new Promise(async (resolve, reject) => {
        let iconttf = SVGTOTTF(await fextra.readFileSync(path.join(__dirname, "font/iconfont.svg"), 'utf-8'), {})
        fextra.writeFileSync(DIST_TTF, new Buffer.from(iconttf.buffer), error => {
            if (error) {
                reject(error)
                return
            }
            console.log('is create TTF success')
            resolve(true)
        })
    })
}

/** 生成WOFF文件 **/
const generateIconWoff = () => {
    return new Promise(async (resolve, reject) => {
        let iconwoff = SVGTOWOFF(await fextra.readFileSync(DIST_TTF))
        fextra.writeFileSync(DIST_WOFF, new Buffer.from(iconwoff.buffer), (error, res) => {
            if (error) {
                reject(error)
                return
            }
            console.log('is create ttf success')
            resolve(true)
        })
    })
}

/** 生成WOFF2文件 **/
const generateIconWoffTwo = () => {
    return new Promise(resolve => {
        let iconwoff2 = SVGTOWOFFTWO(fextra.readFileSync(DIST_TTF))
        fextra.writeFileSync(DIST_WOFF_TWO, new Buffer.from(iconwoff2.buffer), (error, res) => {
            if (error) return
            console.log('is create ttf success')
        })
        resolve()
    })
}

/** 生成EOT文件 **/
const generateIconEot = () => {
    // const DIST_EOT = path.resolve(process.cwd(), "./font/iconfont.eot")
    return new Promise(resolve => {
        let iconeot = SVGTOEOT(fextra.readFileSync(DIST_TTF))
        fextra.writeFileSync(DIST_EOT, new Buffer.from(iconeot.buffer), (error, res) => {
            if (error) return
            console.log('is create eot success')
        })
        resolve()
    })
}

/** 生成可视文件 **/
const generatePage = () => {
    return new Promise(async resolve => {
        const tempPath = path.join(__dirname, "/template/index.ejs")
        const savePath = path.join(__dirname, "/font/index.html")
        const temp = await generateHtml(tempPath, { icons: JSON.stringify(unicodeObj) })
        fextra.outputFileSync(savePath, temp)
    })
}

/** 生成HTML文本 **/
const generateHtml = (savePath, options) => {
    return new Promise((resolve, reject) => {
        ejs.renderFile(savePath, options, (error, str) => {
            if (error) reject(error);
            resolve(str);
        });
    })
}

const generateInit = async () => {
    const is_ok = await generateIconfont()
    if (is_ok) {
        setTimeout(() => {
            generateIconTTF()
            generateIconWoff()
            generateIconWoffTwo()
            generateIconEot()
            generatePage()
        }, 500)
    }
}

generateInit()

如果需要做图标上传并生成的话,需要将代码修改一下,将执行代码改为导出的方式;

export default {
    generateIconfont,
    generateUnicodeHtml,
    generateIconTTF,
    generateIconWoff,
    generateIconWoffTwo,
    generateIconEot
}

然后将上传的文件名称传如到 generateIconfont 方法中,意思就是原来生成字体文件是全部生成的,而上传后就改为单独生成

const generateIconfont = (file) => { ... }

模板DEMO

模板的作用在于将所有的图标展示在页面上,这样在使用图标时,就可以一目了然的看到每个图标所展示的类名、unicode编码,从而很方便的管理图标。

<!-- index.ejs -->
<div id="app">
    <div class="main-icons">
        <div class="main-icon-list">
            <div class="main-icon-tabs s-flex">
                <div class="tab-item" v-for="(item, index) in tabList" :key="index"
                     :class="{ active: activeIndex == index }" @click="activeIndex = index">{{ item }}
                </div>
            </div>
            <ul class="icon-list s-flex flex-wrap">
                <li class="s-flex flex-dir ai-ct jc-ct" v-for="(item, index) in iconList" :key="index"
                    :title="item.name">
                    <em class="iconfont" v-html="item.unicode"></em>
                    <p class="ellipsis-1" v-if="activeIndex == 0">{{ item.unicode }}</p>
                    <p class="ellipsis-1" v-if="activeIndex == 1">{{ item.name }}</p>
                    <p class="ellipsis-1" v-if="activeIndex == 2">icon-{{ item.name }}</p>
                </li>
            </ul>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    const vm = new Vue({
        el: '#app',
        data() {
            return {
                tabList: ['Unicode', 'Font Class', 'Symbol'],
                activeIndex: 0,
                iconList: []
            }
        },
        methods: {},
        mounted() {
            this.iconList = [<%-icons%>]
            console.log(this.iconList)
        }
    })
</script>

<style scoped>
    @font-face {
        font-family: "iconfont";
        src: url('./iconfont.ttf') format('truetype'),
        url('./iconfont.woff') format('woff'),
        url('./iconfont.woff2') format('woff2'),
        url('./iconfont.svg#iconfont') format('svg');
    }

    .iconfont {
        display: inline-block;
        font-family: "iconfont" !important;
        font-size: 16px;
        font-style: normal;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
    }
    /*flex布局*/
    .s-flex { display: -webkit-box; display: -moz-box; display: -webkit-flex; display: -moz-flex; display: -ms-flexbox; display: flex; }
    .flex-1 { -prefix-box-flex: 1; -webkit-box-flex: 1; -webkit-flex: 1; -moz-box-flex: 1; -ms-flex: 1; flex: 1; }
    .flex-dir { flex-direction: column; }
    .flex-wrap { flex-wrap: wrap; }
    .jc-ct { justify-content: center; }
    .ai-ct { align-items: center; }
    .jc-bt { justify-content: space-between; }
    .jc-ad { justify-content: space-around; }
    .jc-fe { justify-content: flex-end; }
    .ai-fe { align-items: flex-end; }
    .ai-fs { align-items: flex-start; }

    /*文字截断*/
    .ellipsis-1 { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
    .ellipsis-2 { display: -webkit-box; overflow: hidden; text-overflow: ellipsis; -webkit-line-clamp: 2; word-break: break-all; -webkit-box-orient: vertical; }


    .main-icon-list {}
    .main-icon-tabs { margin: 20px 0; }
    .main-icon-tabs .tab-item { padding: 10px; margin: 0 10px; border-radius: 30px; background-color: #999999; font-size: 14px; color: #333333; cursor: pointer; }
    .main-icon-tabs .tab-item:hover,
    .main-icon-tabs .tab-item.active { background-color: #0b6666; color: #ffffff; }

    .icon-list { max-height: calc(100vh - 200px); padding: 0; overflow-y: auto; }
    .icon-list li { width: calc((100% / 6) - 20px); padding: 20px 10px 5px; margin: 10px; list-style: none; text-align: center; background-color: #e5e5e6; border-radius: 6px; cursor: pointer; box-sizing: border-box; }
    .icon-list li em { font-size: 26px; }
    .icon-list li p,
    .icon-list li span { line-height: 1.4; width: 100%; text-align: center; margin: 5px 0; font-size: 14px; }
    .icon-list li span { font-size: 12px; }
</style>

成果展示

image.png