Dickzeng 2019-11-03
原文:SVG 优化探索 | AlloyTeam
作者:summer
在前端开发中或多或少都有用到 SVG,本篇文章就来总结下如何在前端项目中使用 SVG,每种使用方式的优缺点分析,以及对 SVG 的一些优化探索。
SVG(Scalable Vector Graphics,可缩放矢量图形)是一种基于可扩展标记语言(XML),用于描述二维矢量图形的一种图形格式。
这里简要说明下位图与矢量图的区别:位图是由像素点构成的,矢量图则是由一些形状元素构成。下图中放大位图可以看到点,而放大矢量图看到的仍然是形状。SVG 属于矢量图,因此理论上能够无限缩放,而不会导致马赛克。
简单的 SVG 样式:
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> <path fill="#000" fill-rule="evenodd" d="M8.667 8L12 11.333l-.667.667L8 8.667 4.667 12 4 11.333 7.333 8 4 4.667 4.667 4 8 7.333 11.333 4l.667.667z" opacity=".64"/> </svg>
SVG 的几大特点:
1)能使用 CSS/JS 进行控制。
2)与分辨率无关,在任何分辨率的设备上都能清晰地展示。就不需要为高清屏准备二倍图、三倍图了。
3)容易编辑
4)高度可压缩
来看下 SVG 格式与 JPG、GIF、PNG 图片格式在使用上的区别:
使用 SVG 图片有多种方式,每种方式都有自己的优缺点,选择合适的方式就好。
1、在 Img 中引入
<img src="logo.svg" alt="Logo" height="65" width="68">
需要注意的是,使用这种方法在交互性上有很多限制,如不能使用 JS 来控制,不支持 CSS3 动画。
2、通过 CSS 中 Background-image 引入
.logo { background-image: url(logo.svg); }
使用背景图片方法需要注意的一点是,最好不要使用 base64 编码来格式化 SVG 图片,因为它在加载完前会阻塞其它资源的下载。使用这种方法在交互性上也有很多限制,如不能使用 JS 来控制,不支持 CSS3 动画。
3、通过 Iframe 引入
<iframe src="logo.svg">Your browser does not support iframes</iframe>
不确定这是不是还是一种好的使用方法。
4、通过 Embed 引入
<embed type="image/svg+xml" src="logo.svg" />
大多数浏览器都支持,但最好还是不要使用这种方法。
5、通过 Object 引入
<object type="image/svg+xml" data="bblogo.svg">Your browser does not support SVGs</object>
如果你想使用 JS 来进行交互控制,<object>
是 SVG 使用方法中很好的一个选择。 只需要把它放到 HTML 中就可以了。
6、将 SVG 元素直接加入到 HTML 中
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68 65"> <path fill="#1A374D" d="M42 27v-20c0-3.7-3.3-7-7-7s-7 3.3-7 7v21l12 15-7 15.7c14.5 13.9 35 2.8 35-13.7 0-13.3-13.4-21.8-26-18zm6 25c-3.9 0-7-3.1-7-7s3.1-7 7-7 7 3.1 7 7-3.1 7-7 7z"/> </svg>
把 SVG直接插入到 HTML 中,可以节省 HTTP 请求,而且很方便使用 JS 来控制。但是这样,SVG资源就不能被浏览器缓存。同时使用 JS 来操控 SVG 会发生重绘行为。
这几种使用方式的特点:
在 webpack 中有各种 loader 可以加载 SVG:
1、url-loader
官方文档:https://webpack.docschina.org...
可以把 svg 当作普通 jpg、png 图片来使用。
安装:
npm install url-loader --save-dev
webpack 配置:
test: /\.(png|jpg|gif|svg)$/i, use: [ { loader: 'url-loader', } ]
在 React 组件中的引入方式:
import test from './test.svg' export default class App extends Component { render() { return ( <img src={test} /> ); } };
经过 url-loader 处理后,test.svg 文件被处理为 "data:image/svg+xml;base64,PHN2ZyBpZ....." 这样的 base64 编码。
module.exports = "data:image/svg+xml;base64,PHN2ZyBpZ..
我们用 webpack-bundle-analyzer 插件测试看看 svg 文件被打包后的大小:
此外 file-loader 也可以。
这种方式的优点: SVG 资源可被缓存,SVG 资源可单独加载。
缺点:加载多个 SVG 文件时,会产生多个 http 请求,影响页面加载性能。
2、svg-react-loader
安装:
npm install svg-react-loader --save-dev
webpack 配置:
{ test: /\.svg$/, loader: 'svg-react-loader', }
在 React 组件中的引入方式:
import Test from "./svg/test.svg"; export default class App extends Component { render() { return <Test className={iconType} /> } }
svg-react-loader 会将 svg 文件处理成 React 组件,最后会以 svg 标签的形式渲染到 html 中。从最后渲染到 html 中的代码来看,svg-react-loader 是有对 svg 原文件进行优化的。从打包后的文件大小可以看出来文件有被压缩:
这种方式的缺点:SVG 资源不可被缓存。且会将 svg 资源的处理逻辑与页面的交互逻辑一起打包。
最好的方式是:SVG 资源与 JS 资源分离,图片的加载不影响页面的主要执行逻辑。并且所有的 SVG 资源希望能一次 HTTP 请求就能搞定。
3、svg-inline-loader
官方文档:https://webpack.js.org/loader...
svg-inline-loader 会根据配置项去除 SVG 中冗余的代码或者不必要的代码,以减少 SVG 的文件大小。
安装:
npm install svg-inline-loader --save-dev
webpack 配置:
{ test: /\.svg$/, loader: 'svg-inline-loader' }
在 React 组件中的引入方式:
import test from "./test.svg"; export default class App extends Component { render() { return ( <span dangerouslySetInnerHTML={{__html: test}} /> ); } };
经过 svg-inline-loader 处理后 svg 文件被处理为这样的字符串:
module.exports = \"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\">.....
看下打包大小,相对比,这个体积优化的效果还不错。
此外还有 raw-loader 等都可以处理 SVG 文件。
一般设计师会使用 Adobe suite 或者 Sketch 等工具导出 SVG 文件,但这样的 SVG 一般是有属性冗余的,所以需要对 SVG 进行一定的压缩。
手动压缩:当然,也不需要手动压缩,但是可以看看哪些属性是有冗余的。
工具压缩:推荐使用 SVGO。
webpack 项目中引入 SVGO:
安装:
npm install svgo svgo-loader --save-dev
webpack 配置:
{ test: /\.svg$/, exclude: /node_modules/, loaders: [ 'url-loader', 'svgo-loader' ], },
在单独使用 url-loader 时,文件 test.svg 打包后的大小是:17.82 KB。在使用 svgo-loader 后,我们看下打包大小,确实是有很大幅度的压缩。
当项目需要加载多个 SVG 文件时,上述加载方式就需要优化了。我们考虑以下几种情况:
1)使用 url-loader 加载多个 svg 文件
这种方式会产生多次 http 请求,而浏览器对并发请求数目是有限制的,请求太多会影响页面加载性能。这种情况就需要引入雪碧图,将多个 svg 文件合成一个 svg 文件。
2)使用 svg-react-loader ,当一个组件需要加载多个 svg 文件时,所有的 svg 文件都会被打包到 index.js 文件中。如果 svg 文件过多,就会增大 index.js 文件体积。而 SVG 作为图片资源,最好是作为一个单独文件异步加载,与页面主要执行逻辑分离。当然这里也同样需要引入雪碧图。
第一种方法是把所有的图标通过<symbol>
元素定义在 SVG 代码中,嵌入在<symbol>
元素中的图标是不会被直接显示的。通过使用<use>
元素的 xlink:href="#id" 来引用单个图标。
也就是说合成后的大 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名1">{{省略的icon path}}</symbol> <symbol class="icon" viewBox="0 0 1024 1024" id="icon名2">{{省略的icon path}}</symbol> </svg>
在需要引入单个图标的时候:
<use xlink:href="#symbolId"></use>
这里我们使用 svg-sprite-loader 来自动生成 svg 雪碧图。
安装:
npm install svg-sprite-loader --save-dev
loader 配置:
{ test: /\.svg$/, exclude: /node_modules/, use: [{ loader: 'svg-sprite-loader', }], }
react 组件中引入:
import "./svg/node.svg"; import "./svg/test.svg"; import "./svg/logo.svg"; export default class App extends Component { render() { return ( <div> <svg><use xlinkHref="#node" /></svg> <svg><use xlinkHref="#test" /></svg> <svg><use xlinkHref="#logo" /></svg> </div> ); } }
这样配置打包后,body 下会自动注入一个大的 SVG 元素,达到了雪碧图的效果。可是这样还是有两个缺点:1)当需要使用这些 svg 的时候,需要在组件中单独 import。2)这个大的 SVG 资源并没有与 JS 资源分离。
为了解决第一个缺点:在组件加载前,增加自动 import 所有 svg 的功能:
// requires and returns all modules that match const requireAll = requireContext => requireContext.keys().map(requireContext); // import all svg const req = require.context('./svg', true, /\.svg$/); requireAll(req);
现在可以自动导入了,不用到每个组件中去单独 import了。可是还需要解决第二个问题:SVG 资源与 JS 资源分离。
从 svg-sprite-loader 文档中看到可以利用 spriteloaderplugin 插件将所有 SVG 文件打包输出为一个大的 SVG:
这里的 loader 配置:
{ test: /\.svg$/, exclude: /node_modules/, loaders: [ { loader: 'svg-sprite-loader', options: { extract: true, spriteFilename: 'sprite.svg', // 生成的 SVG 雪碧图资源路径 } }, 'svgo-loader' ], },
插件配置:
plugins: [ new SpriteLoaderPlugin({ plainSprite: true }) ]
这种方式会在打包目录下生成 sprite.svg 文件,我们可以通过 ajax 请求的方式获取到该 svg 资源,然后将其注入到 html 中去,这样就将 svg 资源与 js 资源分离了。
fetch(`sprite.svg`).then(res => { return res.text(); }).then(data => { document.getElementById('svg-sprite').innerHTML = data; });
这样的方式是我们自己手动去请求的,思考下能否使用 webpack 配置或者一些插件就能达到 svg 与 js 资源分离的目的呢。于是想到了webpack 的 SplitChunksPlugin 插件提供的抽取公共代码的能力。
于是尝试把 import 所有 svg 的逻辑抽离出来成为一个单独的 js 文件。配置好 SplitChunks 后,webpack 就会把这部分逻辑抽离出来,单独打包成一个 js 文件。这样就实现了 svg 与 js 资源分离了。
webpack 配置:
optimization: { splitChunks: { cacheGroups: { commons: { name: 'commons', chunks: 'initial', minChunks: 2 } } } },
第二种方法是使用 SVG 的 viewbox 属性来指定显示 SVG 画布的区域,跟 background-position 原理差不多。待探索~
参考:
https://developer.mozilla.org...
https://www.sumydesigns.com/d...
https://community.wia.io/d/6-...
AlloyTeam 欢迎优秀的小伙伴加入。
简历投递: [email protected]
详情可点击 腾讯AlloyTeam招募Web前端工程师(社招)