用 svg-sprite-loader 解放你的icon.
好吧,这篇文章的起源就来源于——我懒。
UI小姐姐设计了自己的icon,但是我不想每次引入icon的时候都写一大堆:
<img src="/long/path/to/your/svg/icon.svg" />
很长很长的地址…我觉得最简单的形式还是像饿了么那些UI库一样,直接:
<el-icon name="icon-file-name"></el-icon>
写个文件名就能引入我的icon了。
OK, 以上就是我们的理想模式。So, let’s go!
网上搜寻了一圈,一个简单的解决方案是 —— svg 雪碧图。
它的工作原理是: 利用svg的symbol
元素,将每个icon包括在symbol
中,通过use
元素使用该symbol
.
OK,如果你对此不了解,可以阅读张鑫旭老师的这篇文章.
我们这里简单一点的解释就是,最终你的svg icon会变成下面这个样子的 svg 雪碧图:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="__SVG_SPRITE_NODE__">
<symbol class="icon" viewBox="0 0 1024 1024" id="icon名">{{省略的icon path}}</symbol>
<symbol class="icon" viewBox="0 0 1024 1024" id="icon名">{{省略的icon path}}</symbol>
</svg>
你的每一个icon都对应着一个symbol
元素。然后在你的html中,引入这样的svg, 随后通过use
在任何你需要icon的地方指向symbol:
<use xlink:href="#symbolId"></use>
这个过程中,我们可以把symbol理解为sketch中内置的图形,当你需要使用的时候,把这个形状”拖拽”到你的画板中就行了。而use
就是这个过程中的”拖拽”行为。
要让我们自己生成上面那样的svg雪碧图——肯定是不可能的咯! 恩,你一定想到了,肯定有工具!当然你最常用的应该是webpack的工具吧,这里拿好!
svg-sprite-loader
会把你的icon塞到一个个symbol
中,symbol
的id如果不特别指定,就是你的文件名。它最终会在你的html
中嵌入这样一个svg
,
你就可以像上面这样:
<use xlink:href="#symbolId"></use>
随意使用你的icon咯。
svg-sprite-loader
配置如下:
{
test: /\.svg$/,
loader: 'svg-sprite-loader',
}
有一点需要注意的是,我们并不是所有的svg都要放在我们的雪碧图里,有的也许我就想当做图片用。这时候在我们的webpack
配置中,我们需要对这两种svg区别对待。
首先,我们要把所有要作为icon的svg团结在一起,放在某个文件夹中,例如assets/icons
。其他的svg就随你便啦。
然后对于想要用作图片的:
{
test: /\.svg$/,
loader: 'file-loader',
exclude: path.resolve(__dirname, './src/assets/icons') // 不带icon 玩
}
对于用作icon的:
{
test: /\.svg$/,
loader: 'svg-sprite-loader',
include: path.resolve(__dirname, './src/assets/icons') // 只带自己人玩
}
最后,这俩就分道扬镳啦。
OK, 我们的问题已经解决了一半,不用每次都写路径引入svg
文件了。
但是。。。我们现在要每次都写
<svg>
<use xlink:href="#symbolId"></use>
</svg>
我!不!干!!!而且也没达到我们最初的目的。 所以,我们肯定把上面的那一坨写成一个组件咯:
<template>
<svg :class="svgClass">
<use :xlink:href="`#${name}`"></use>
</svg>
</template>
<script>
export default {
name: 'icon',
props: {
name: {
type: String,
required: true,
},
},
}
</script>
最后,你就达成目标,这样使用:
import 'your-icon.svg';
<icon name="your-icon-name"></icon>
如果你想修改图标的颜色,直接设置该元素的fill
/stroke
属性。如果设置了这些属性没有反应的话,emmm...可能需要你的设计师重新切图,同样是张鑫旭大佬
关于切图的这篇文章
上面我们的基本功能已经完成了,还有最后一个小小的问题——我每次引用一个文件的时候就得import一下,这肯定也不满足我们偷懒的最终目标。 不过,总会有人比你更懒,或者总会有人比你先懒。在这里,我们可以使用webpack的require.contextAPI来动态引入你所有的Icon.
现在我们是不能动态引入模块,但是webpack为我们提供了相关功能,[webpack](Dependency Management) 允许我们使用表达式动态引入模块。比如:require('./template/' + name + '.ejs');
,此时webpack会生成一个context module
A context module is generated. It contains references to all modules in that directory that can be required with a request matching the regular expression. The context module contains a map which translates requests to module ids.
它会被抽象成以下信息:
{
"./table.ejs": 42, // key 是module, value 是module id
"./table-row.ejs": 43,
"./directory/folder.ejs": 44
}
因此,我们可以利用webpack提供的的[require.context
](Dependency ManagementAPI 来创建自己的context module
动态引入icon。它接受三个参数,第一个是文件夹,第二个是是否使用子文件,第三个是文件匹配的正则。
require.context(directory, useSubdirectories = false, regExp = /^\.\//)
对于我们的项目来说,我们需要动态引入的就是require.context('./src/assets/icons', false, /\.svg/)
.
require.context
会返回一个函数,并且该函数有keys()
,id
, resolve()
属性。
keys()
方法返回的该模块可以处理的所有可能请求的模块的数组,简单一点就是满足该参数的模块;resolve()
返回的是请求的module的id;id
是该context module
的id;
总的来说,就是说require.context
帮我们创建一个上下文,比如在这里我们的上下文就是./src/assets/icons
, 随后我们就可以通过request.resolve('./store.svg')
来引入该上下文内的文件了。
我们打印一下:
const request = require.context('./assets/icons', false, /\.svg$/);
console.log(request);
console.log(request.keys());
console.log(request.id);
console.log('request.resolve()', request.resolve('./store.svg'));
console.log(request.resolve);
得到的结果是:
// request
webpackContext(req) {
var id = webpackContextResolve(req);
return __webpack_require__(id);
}
// request.keys()
["./airbloom.svg", "./crown.svg", "./store.svg"]
// request.id
./src/assets/icons sync \.svg$
// request.resolve('./store.svg');
./src/assets/icons/store.svg
// request.resolve
webpackContextResolve(req) {
var id = map[req];
if(!(id + 1)) { // check for number or string
var e = new Error("Cannot find module '" + req + "'");
e.code = 'MODULE_NOT_FOUND';
throw e;
}
有关的源码在这里:
var map = {
"./airbloom.svg": "./src/assets/icons/airbloom.svg",
"./crown.svg": "./src/assets/icons/crown.svg",
"./store.svg": "./src/assets/icons/store.svg"
};
function webpackContext(req) {
var id = webpackContextResolve(req);
return __webpack_require__(id);
}
function webpackContextResolve(req) {
var id = map[req];
if(!(id + 1)) { // check for number or string
var e = new Error("Cannot find module '" + req + "'");
e.code = 'MODULE_NOT_FOUND';
throw e;
}
return id;
}
webpackContext.keys = function webpackContextKeys() {
return Object.keys(map);
};
webpackContext.resolve = webpackContextResolve;
module.exports = webpackContext;
webpackContext.id = "./src/assets/icons sync \\.svg$";
最后,我们request
该context module
下的每一个module
,引入我们所有的icon
// 由于request返回了一个函数,该函数接收req作为参数,在这里其实我们就是把request.keys()中的每一个module传入了request的返回函数中了
request.keys().forEach(request);
-
原理:
symbol
+use:xlink:href
;svg-sprite-loader
生成雪碧图;require.context
动态引入所有文件;
-
优化SVG 有时候,设计师切的icon并不那么geek, 有很多多余的东西,可以使用大名鼎鼎的svgo进行优化, 它提供web在线版,webpack loader等。
-
其他工具 vue-svgicon这款工具相比我们的有更多的feature,比如动画、方向等。它会给每个icon生成一个相对应的js文件, 用来注册这个icon。就我目前的应用场景来说,1. 它会生成很多js文件;2.每次新增一个svg时我就得run一次注册组件的命令。对于我现在的简单应用场景来说,并没有自己写的简单方便。 不过在其他的时候,他也可以作为另一个选择。
-
相关代码已经放在github上啦
-
本来想放在codesandbox上的,结果目前他们还不支持
require.context
API.