源代码 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
一次引入所有文件。