使用 svg-sprite-loader、svgo-loader 优化项目中的 Icon

9,303 阅读3分钟

源代码 github.com/Maricaya/h5… 主要查看两个文件 webpack.config.js/src/components/Icon.tsx

背景

又到了愉快的总结时刻,这次我们来看看怎么优化项目中的 Icon。

每次写项目引入 icon 的时候都写一大堆:

   <img src="icons/chart.svg" alt="" />

最好能在项目中直接实现 element-ui 的引入效果:

<Icon name="icon-file-name"/>

直接一句话就可以引入 Icon,这样多方便呀。

网上搜寻了一圈,找到了两个非常好用的 loader:

svg-sprite-loader、svgo-loader

来看看他们是怎么工作的吧!

svg-sprite-loader

原理

svg-sprite-loader 会把你的 svg 塞到一个个 symbol 中,合成一个大的 svg。

最后将这个大的 svg 放入 body 中。

symbol的id如果不特别指定,就是你的文件名。

在页面上形成这样的元素:

<body>
    <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="__SVG_SPRITE_NODE__">
      <symbol xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 1024 1024" id="xxx">// id 是 icon 名
	<!-- 这块是 path -->
      </symbol>
      <symbol xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 1024 1024" id="xxx">// id 是 icon 名
	<!-- 这块是 path -->
      </symbol>
    </svg>
</body>

我们的每一个 icon 都对应着一个 symbol 元素,这个时候我们就可以在页面使用 svg use 啦。

<svg>
  <use xlink:href="#xxx"/> // xxx 为id
</svg>

我们可以把 symbol 理解为 sketch 中内置的图形。

当你需要使用这个图形的时候,把这个形状”拉”到你的画板中就行了。

而 use 就是这个”拉”的行为。

总结一下工作原理:

利用 svg 的 symbol 元素,将每个 icon 包裹在 symbol 中,通过 use 使用该 symbol。

使用方法

安装

yarn add --dev svg-sprite-loader

配置 webpack.config.js,在 module.rules 添加:

{
  test: /\.svg$/,
  use: [
    { loader: 'svg-sprite-loader', options: {} }
  ]
}

icon 写法

<svg>
  <use xlink:href="#xxx"/> // xxx 为id
</svg>

SVGO

背景

设计小姐姐在切图时,可能不会注意某些细节。

比如,PSD 看上去是好的,但是放大个 100 倍,路径的转角和边缘都没对上。

此时如果直接使用 SVG,一个是 SVG 文件太大,二是最终的图像可能不是我们想要的。

这个时候就需要 SVGO 来帮我们处理 svg。

原理

SVGO 将 SVG-as-XML 数据转换为 SVG-as-JS AST 表示形式。

然后在所有AST数据项上运行并执行一些操作,最后,SVGO 再将 AST 转换回 SVG-as-XML 数据字符串。

想深入了解原理的同学,可以看看 官方解释

我就不在这里啰嗦啦。

用法

SVGO 是 svg 优化器,包含很多插件。

它可以删除和修改SVG元素,折叠内容,移动属性等等等等。

安装

yarn add --dev svgo-loader

继续配置 webpack.config.js

{
  test: /\.svg$/,
  use: [
    { loader: 'svg-sprite-loader', options: {} },
+   { loader: 'svgo-loader', options: {} }
  ]
}

引入项目中的 svg 文件会经过 svgo-loader => svg-sprite-loader 的处理。

先处理 svg 图像,然后在页面中生成 svg-symbols。

icon 写法

<svg className="icon" fill="#ccc">
  <use xlinkHref={'#' + props.name} />
</svg>

Icon 组件化

最后,再单独写一个 Icon 组件。

完整代码如下:

Icon.tsx

import React from 'react'
// TreeShaking 不适用于 require
require('icons/money.svg')
require('icons/tag.svg')
require('icons/chart.svg')

type Props = {
  name: String
}

const Icon = (props: Props) => {
  return (
    <svg className="icon">
      <use xlinkHref={'#' + props.name} />
    </svg>
  )
}

export default Icon;

使用时:

<Icon name="money" />

一次性引入所有 Icon

做完了 Icon 组件,又发现一个小小的问题。

在 Icon.tsx 文件中引入 svg 需要一个一个引入,能不能一次性全部引入呢?

可以,不想一直重复引入,我们需要 require 一个目录。

// 不想一直重复引入,需要 require 一个目录
// 因为使用了 TypeScript 需要安装 webpack 的类型文件 @types/webpack-env
// yarn add --dev @types/webpack-env
let importAll = (requireContext: __WebpackModuleApi.RequireContext) => 
	requireContext.keys().forEach(requireContext);

try {importAll(require.context('icons', true, /\.svg$/));} 
	catch (error) {console.log(error);} 

利用 webpack 提供的 require.context API 来创建自己的 context module 动态引入 icon。

require.context(directory, useSubdirectories = false, regExp = /^\.\//)

它接受三个参数

  • 要搜索的文件夹目录
  • 是否还应该搜索它的子目录,
  • 以及一个匹配文件的正则表达式。

对于我们的项目来说,我们需要动态引入的就是

require.context('icons目录', true, /\.svg$/)

require.context 会返回一个函数,并且该函数有keys(),id, resolve() 属性。

一个 context module 会导出一个(require)函数,此函数可以接收一个参数:request。

此导出函数有三个属性:resolve, keys, id。

  • resolve 是一个函数,返回的是请求的 module 的 id
  • keys 也是一个函数,它返回一个数组,是满足该参数的模块。
  • id是该 context module 的id

如果想引入一个文件夹下面的所有文件,或者引入能匹配一个正则表达式的所有文件,这个功能就会很有帮助,例如:

function importAll (r) {
  r.keys().forEach(r);
}

importAll(require.context('../components/', true, /\.js$/));

总的来说,就是说 require.context 帮我们创建一个上下文。

比如在这里我们的上下文就是 ./src/assets/icons

随后我们就可以通过 require.resolve 来引入该上下文内的文件了。

总结

最后,来总结一下 Icon 组件的优化方式。

  • 使用svg-sprite-loader制作 svg-symbol。让我们可以直接使用 svg-use。
  • 使用 svgo-loader 优化 svg。
  • 最后,使用 require.context 一次引入所有文件。