懒人神器:svg-sprite-loader实现自己的Icon组件

2019-12-10 11:11:19

参考地址  懒人神器:svg-sprite-loader实现自己的Icon组件

用 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

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...可能需要你的设计师重新切图,同样是张鑫旭大佬
关于切图的这篇文章

引入所有Icon文件

上面我们的基本功能已经完成了,还有最后一个小小的问题——我每次引用一个文件的时候就得import一下,这肯定也不满足我们偷懒的最终目标。
不过,总会有人比你更懒,或者总会有人比你先懒。在这里,我们可以使用webpack的require.contextAPI来动态引入你所有的Icon.

现在我们是不能动态引入模块,但是webpack为我们提供了相关功能,webpack) 允许我们使用表达式动态引入模块。比如: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.contextAPI 来创建自己的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);

得到的结果是:

// requestwebpackContext(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.resolvewebpackContextResolve(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$";

最后,我们requestcontext 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一次注册组件的命令。对于我现在的简单应用场景来说,并没有自己写的简单方便。
不过在其他的时候,他也可以作为另一个选择。

require.contextAPI.


  • 2018-08-07 20:00:42

    xUtils3.0版本的发送同步网络请求的方式

    对于Android开发来说,基本都是用异步来从网络上请求数据,很少用到同步请求的。近日项目有个地方需要使用到同步请求(以我目前的知识储备来说好像只能用同步请求来解决这个问题了),去网上搜索相关资料,又没有找到什么明确的使用方法。所以记下来,以备不时之需。

  • 2018-08-14 23:35:28

    Retrofit 设置 超时时间

    今天开发的时候遇到一个网络请求超时的问题,后台处理是成功的,但是移动端返回的总是提示请求超时,在设置了retrofit请求超时的时间延长以后,就可以请求成功了,下面是配置的方法:

  • 2018-08-16 16:10:43

    Laravel 跨域解决方案

    我们在用 laravel 进行开发的时候,特别是前后端完全分离的时候,由于前端项目运行在自己机器的指定端口(也可能是其他人的机器) , 例如 localhost:8000 , 而 laravel 程序又运行在另一个端口,这样就跨域了,而由于浏览器的同源策略,跨域请求是非法的。其实这个问题很好解决,只需要添加一个中间件就可以了。

  • 2018-08-18 20:30:12

    laravel5.5 路由分割成不同文件

    routes.php/api.php文件用来放置laravel路由,当项目越来越大,相应的路由文件也会越来越多。如果能够将不同功能的路由分割到不同的文件,那么对以后的维护将很有帮助。

  • 2018-08-20 15:26:19

    关于OnTouch 和OnClick同时调用冲突的解决方案

    大家在搞轮播图的时候会碰到这样的情况,点击进入webview界面,长按轮播图停止轮播,手松开图又开始轮播,这里就涉及到了OnTouch 和OnClick同时调用。两者是有冲突的。这里简单介绍,给大家提供思路。

  • 2018-08-20 15:29:11

    揭开RecyclerView的神秘面纱(二):处理RecyclerView的点击事件

    主要讲述了RecyclerView的基本使用方法,不同的布局管理器而造成的多样化展示方式,展示了数据之后,一般都会与用户进行交互,因此我们需要处理用户的点击事件。在ListView和GridView提供了onItemClickListener这个监听器,然而我们查找RecyclerView的API却没有类似的监听器,因此我们需要自己手动处理它的点击事件。 以下提供两种方法来实现处理RecyclerView点击事件的功能,以下代码均基于上一篇文章的代码做出修改。

  • 2018-08-20 22:58:46

    onInterceptTouchEvent和onTouchEvent调用关系详解 ...

    老实说,这两个小东东实在是太麻烦了,很不好懂,我自己那api文档都头晕,在网上找到很多资料,才知道是怎么回事,这里总结一下,记住这个原则就会很清楚了: