前言
大家好,我是jay。在项目开发的过程中,很多时候设计师会给你一个SVG
图标,让你用到项目里当作字体icon
。可能你现有的项目中已经有前辈搭建好这套体系,你直接把这个文件放入某个文件夹中,然后run
一条命令,就可以十分方便的用例如<span class="icon icon-search"></span>
之类的写法把一个icon
渲染出来。那假如现在什么都没有,让你自己去搭建这么一套体系,你会怎么做呢?我们一起往下看吧。
以下图标素材都是我在网上找的,在这里仅当作学习用途,侵删~
实战操作
首先使用vite
搭建一个工程:
npm init
生成package.json
文件,内容如下:
{ "name": "font", "version": "1.0.0", "description": "font project", "main": "index.js", "scripts": { "dev": "vite" }, "author": "jayliang", "license": "ISC", }
npm i vite -D
安装vite
- 根目录下新建
index.html
和index.js
,并在index.html
中如下引入index.js
<script type="module" src="./index.js"></script>
首先我们在根目录新建一个assets
文件夹,用来存放SVG
文件。然后来看如下代码:
//index.js const app = document.querySelector('#app') app.innerHTML = render() function render() { return ` <div class="container"> <h1>Hello SVG</h1> <span>${renderIcon('search', { color: 'red', fontSize: 30 })}</span> </div> ` } function renderIcon(name, options = { color: 'black', fontSize: 16 }) { return '' }
在主渲染逻辑中,我们实际上要实现的是renderIcon
方法。看上面这架势,renderIcon
看来是要直接返回SVG
对应的HTML
片段了。我们现在手里只有一批SVG
文件,怎么让他返回代码片段呢?在浏览器环境开发的时候虽说可以动态引入文件,但是读取文件的原文还是比较难搞的事情毕竟没有fs.readFile
之类的API
。
这里我们可以写一个简单的脚本预处理一下,先把SVG
文件的内容读出来,根目录新建一个icons
文件夹来存放脚本处理的结果。这个预处理脚本要处理的事情如下:
- 读取所有的
SVG
文件内容,改造成字符串导出 - 把
SVG
文件中的width
、height
、fill
字符串提取出来,后续作为参数传入。 - 生成一个入口文件暴露所有
SVG
文件。
icons
文件夹大概长成这个样子:
index.js // 入口文件 script.js //生成文件脚本 home.js //home.svg生成的文件 search.js //search.svg生成的文件
脚本实现
下面一起来看一下script.js
脚本的实现
const path = require('path') const fs = require('fs') const jsdom = require("jsdom"); const { JSDOM } = jsdom; const assetsDirPath = path.resolve(__dirname, '../assets') //存放SVG文件的目录 const assets = fs.readdirSync(assetsDirPath) const currentPath = path.resolve(__dirname, './') //当前目录,即icons目录 assets.forEach(asset => { const assetPath = `${assetsDirPath}/${asset}` let res = fs.readFileSync(assetPath, { encoding: 'utf8' }) const reg = /<svg.*>[\s\S]*<\/svg>/ //将SVG标签过滤出来 let svg = reg.exec(res)[0] const dom = new JSDOM(`<div id="container">${svg}</div>`) //方便操作节点对象 const document = dom.window.document; const container = document.querySelector('#container'); const svgDom = container.querySelector('svg') svgDom.setAttribute('width', '${fontSize}') // width与height属性处理 svgDom.setAttribute('height', '${fontSize}') const paths = svgDom.querySelectorAll('path') for (let i = 0; i < paths.length; i++) { const path = paths[i] path.setAttribute('fill', '${color}') //path属性处理 } svg = container.innerHTML const fileName = asset.split('.')[0] + '.js' //导出函数实现 const string = ` export default function({color,fontSize}){ return \`${svg}\` } ` fs.writeFileSync(currentPath + '/' + fileName, string) }) //入口文件拼接 let importStr = `` let exportStr = `` assets.forEach(asset => { const fileName = asset.split('.')[0] importStr += `import ${fileName} from './${fileName}';\n` exportStr += `${fileName},\n` }) const content = ` ${importStr} export default { ${exportStr} } ` fs.writeFileSync(currentPath + '/index.js',content)
任意一个SVG
文件经过处理后转成的JS
文件内容是这样子的:
//home.js export default function({color,fontSize}){ return `<svg t="1648047237899" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1683" xmlns:xlink="http://www.w3.org/1999/xlink" width="${fontSize}" height="${fontSize}"> <path d="....." fill="${color}"></path> </svg>` }
最后生成的入口文件index.js
内容是这样子的:
import home from './home'; import search from './search'; import set from './set'; export default { home, search, set, }
renderIcon
实现
在上述预处理好icon
文件与入口脚本之后,icon
的render
函数就十分简单了,实现如下:
import icon from './icons' function renderIcon(name, options = { color: 'black', fontSize: 16 }) { const iconRenderFunc = icon[name] if (!iconRenderFunc || typeof iconRenderFunc !== 'function') { throw new Error(`icon:${name} is not found`) } const res = iconRenderFunc(options) return res }
来试一下渲染效果是否符合预期:
` <span>${renderIcon('search', { color: 'red', fontSize: 30 })}</span> <span>${renderIcon('home', { color: 'pink', fontSize: 50 })}</span> <span>${renderIcon('set', { color: 'black', fontSize: 16 })}</span> `
由上图可以看出基本上渲染是没问题的,我们还需要做的一个事情是给渲染出来的SVG
标签暴露一个选择器以及对其加上鼠标移上效果处理。改造renderIcon
方法如下:
function renderIcon(name, options = { color: 'black', fontSize: 16 }, mouseEnterOptions = {}) { // ...... const id = genId() svg.id = id svg.classList += ` icon-${name}` if (Object.keys(mouseEnterOptions).length > 0) { setTimeout(() => { const dom = document.querySelector(`#${id}`) const { color, fontSize } = mouseEnterOptions const { color: originColor, fontSize: originFontsize } = options let resetPathColor = null let resetFontSize = null dom.addEventListener('mouseenter', () => { if (color) { setPathColor(dom, color) resetPathColor = setPathColor } if (fontSize) { setSvgFontsize(dom, fontSize) resetFontSize = setSvgFontsize } }) dom.addEventListener('mouseleave', () => { resetPathColor && resetPathColor(dom, originColor) resetFontSize && resetFontSize(dom, originFontsize) }) }, 0) } } function setSvgFontsize(svg, fontSize) { svg.setAttribute('width', fontSize) svg.setAttribute('height', fontSize) } function setPathColor(svg, color) { const paths = svg.querySelectorAll('path'); [...paths].forEach(path => { path.setAttribute('fill', color) }) }
加多一个mouseEnterOptions
参数定义鼠标移入的参数,然后监听mouseenter
和mouseleave
事件即可。
当然你用框架可以封装成<Icon name="search" options={color:'black',fontSize:30}/>
这样的使用形式,会更加的优雅。
字体图标库
我们上面利用了node.js
预处理SVG
文件+渲染逻辑基本实现了一个能满足大多数业务场景的图标库。那么业界更普遍的做法其实是把SVG
当成字体来用,也就是我最一开始说的也许你只要<span class="icon icon-search"><span>
就能渲染一个图标,下面我们一起来看一下是如何实现的。
我们会用到一个十分牛逼的字体操作库————font-carrier,是在GitHub
上star
有1.5k
的明星第三方包,可以利用它很方便地使用SVG
生成字体。先来安装一下npm i font-carrier -D
,然后在根目录新建一个fonts
目录,在这个目录下新建一个script.js
,内容编写如下:
const fontCarrier = require('font-carrier') const path = require('path') const fs = require('fs') const assetsDirPath = path.resolve(__dirname, '../assets') const assets = fs.readdirSync(assetsDirPath) const font = fontCarrier.create() let initValue = 0xe000 for (let i = 0; i < assets.length; i++) { const assetPath = `${assetsDirPath}/${assets[i]}` const res = fs.readFileSync(assetPath).toString() initValue += 1 const char = String.fromCharCode(initValue) font.setSvg(char, res) } font.output({ path: './iconfonts' })
默认会输出.eot
、.svg
、.ttf
、.woff
、.woff2
,默认会输出这几个字体文件,究其原因是各个浏览器对字体的实现不一样,所以这是为了兼容大多数的浏览器。然后我们再定义一个iconfonts.css
文件,主要为了定义字体,内容如下:
@font-face { font-family: 'iconfont'; src: url('iconfonts.eot'); /* IE9*/ src: url('iconfonts.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('iconfonts.woff') format('woff'), /* chrome、firefox */ url('iconfonts.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/ url('iconfonts.svg#uxiconfont') format('svg'); /* iOS 4.1- */ } .iconfont { font-family: "iconfont"; font-size: 16px; font-style: normal; }
定义完之后,引入这个CSS
文件,然后就可以如下使用了:
<span class="iconfont search"></span> <span class="iconfont home"></span> <span class="iconfont setting"></span>
伪类
上面我们是直接使用了字体所对应的unicode
编码,其实也可以使用CSS
伪类的形式,这也是业界用的最多的形式。在上面的基础上,只要生成多一个icon.css
记录这些伪类信息就行。代码如下:
const iconMap = {} for (let i = 0; i < assets.length; i++) { //...... iconMap[assets[i]] = '\\' + initValue.toString(16).toUpperCase() } let content = `` Object.keys(iconMap).forEach(key => { const name = key.replace('.svg','') const value = iconMap[key] content += ` .icon-${name}::before { content:'${value}' } ` }) fs.writeFileSync('./icon.css',content)
生成的icon.css
内容如下:
.icon-home::before { content: '\E001' } .icon-search::before { content: '\E002' } .icon-set::before { content: '\E003' }
我们就能通过<span class="iconfont icon-home"></span>
这样的方式来使用图标了。
最后
以上就是本篇文章的全部内容,你平时项目开发过程中是如何使用这样的图标库的呢?欢迎留言讨论。如果觉得有趣或者对你有帮助的话,留下一个赞吧~