SVGで独自のアイコンライブラリを構築する

序文

みなさん、こんにちは。私の名前はジェイです。プロジェクト開発の過程で、多くの場合、デザイナーはSVGプロジェクトでフォントとして使用できるアイコンを提供しますicon既存のプロジェクトの一部の先輩がすでにこのシステムを構築している可能性があります。このファイルを特定のフォルダーに直接配置してから、コマンドを使用すると、たとえばなどの書き込み方法でファイルをrun簡単にレンダリングできます。では、今何もないのに、自分でそのようなシステムを構築するように言われたら、どうしますか?一緒に見下ろしましょう。<span class="icon icon-search"></span>icon

以下のアイコン資料はすべてインターネットで見つけたものであり、ここでは学習目的でのみ使用されています。

実際の戦闘作戦

viteプロジェクトを構築するための最初の使用:

  1. npm initpackage.json次の内容のファイルを生成します。
    {
        "name": "font",
        "version": "1.0.0",
        "description": "font project",
        "main": "index.js",
        "scripts": {
            "dev": "vite"
        },
        "author": "jayliang",
        "license": "ISC",
    }
    
    复制代码
  2. npm i vite -Dインストールvite
  3. ルートディレクトリに新しいindex.htmlandindex.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に戻っているようです。現在、手元にあるのはファイルのバッチだけですが、どうすれば彼にコードスニペットを返させることができますか?ブラウザ環境の開発中にファイルを動的にインポートすることは可能ですが、それでもファイルの元のテキストを読み取ることは困難です。結局のところ、そのようなことはありませんSVGHTMLSVGfs.readFileAPI

ここでは、前処理用の簡単なスクリプトを記述し、最初にファイルの内容を読み取りSVG、ルートディレクトリに新しいiconsフォルダーを作成してスクリプト処理の結果を保存できます。この前処理スクリプトが処理するものは次のとおりです。

  • すべてのファイルの内容を読み取りSVG、エクスポート用の文字列に変換します
  • SVG文件中的widthheightfill字符串提取出来,后续作为参数传入。
  • 生成一个入口文件暴露所有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文件与入口脚本之后,iconrender函数就十分简单了,实现如下:

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>
`
复制代码

0w29t6fmke.png

由上图可以看出基本上渲染是没问题的,我们还需要做的一个事情是给渲染出来的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参数定义鼠标移入的参数,然后监听mouseentermouseleave事件即可。

pfnjr5olyc.gif

当然你用框架可以封装成<Icon name="search" options={color:'black',fontSize:30}/>这样的使用形式,会更加的优雅。

字体图标库

我们上面利用了node.js预处理SVG文件+渲染逻辑基本实现了一个能满足大多数业务场景的图标库。那么业界更普遍的做法其实是把SVG当成字体来用,也就是我最一开始说的也许你只要<span class="icon icon-search"><span>就能渲染一个图标,下面我们一起来看一下是如何实现的。

我们会用到一个十分牛逼的字体操作库————font-carrier,是在GitHubstar1.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">&#xE001</span>
<span class="iconfont home">&#xE002</span>
<span class="iconfont setting">&#xE003</span>
复制代码

hscpajyttc.gif

伪类

上面我们是直接使用了字体所对应的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>这样的方式来使用图标了。

最后

以上就是本篇文章的全部内容,你平时项目开发过程中是如何使用这样的图标库的呢?欢迎留言讨论。如果觉得有趣或者对你有帮助的话,留下一个赞吧~

おすすめ

転載: juejin.im/post/7079080370590711822