SVG 优化探索

5,056 阅读7分钟

原文:SVG 优化探索 | AlloyTeam
作者:summer

在前端开发中或多或少都有用到 SVG,本篇文章就来总结下如何在前端项目中使用 SVG,每种使用方式的优缺点分析,以及对 SVG 的一些优化探索。

一、认识 SVG

SVG(Scalable Vector Graphics,可缩放矢量图形)是一种基于可扩展标记语言XML),用于描述二维矢量图形的一种图形格式。

这里简要说明下位图与矢量图的区别:位图是由像素点构成的,矢量图则是由一些形状元素构成。下图中放大位图可以看到点,而放大矢量图看到的仍然是形状。SVG 属于矢量图,因此理论上能够无限缩放,而不会导致马赛克。

image-20190721201713555

简单的 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 图片格式在使用上的区别:

image-20190728170717428

二、SVG 使用方法

使用 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 会发生重绘行为。

这几种使用方式的特点:

image-20190728170355429

三、在 React + Webpack 项目中如何引入 SVG?

在 webpack 中有各种 loader 可以加载 SVG:

1、url-loader

官方文档:webpack.docschina.org/loaders/url…

可以把 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 文件被打包后的大小:

image-20190727213907016

此外 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 原文件进行优化的。从打包后的文件大小可以看出来文件有被压缩:

image-20190727214315507

这种方式的缺点:SVG 资源不可被缓存。且会将 svg 资源的处理逻辑与页面的交互逻辑一起打包。

最好的方式是:SVG 资源与 JS 资源分离,图片的加载不影响页面的主要执行逻辑。并且所有的 SVG 资源希望能一次 HTTP 请求就能搞定。

3、svg-inline-loader

官方文档:webpack.js.org/loaders/svg…

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\\\">.....

看下打包大小,相对比,这个体积优化的效果还不错。

image-20190727220013803

此外还有 raw-loader 等都可以处理 SVG 文件。

四、SVG 优化

1. 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 后,我们看下打包大小,确实是有很大幅度的压缩。

image-20190727222022385

2. SVG 雪碧图

当项目需要加载多个 SVG 文件时,上述加载方式就需要优化了。我们考虑以下几种情况:

1)使用 url-loader 加载多个 svg 文件

image-20190727230155985

这种方式会产生多次 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 资源分离。

image-20190728141342715

image-20190728142106996

为了解决第一个缺点:在组件加载前,增加自动 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 资源分离了。

image-20190731212610492

webpack 配置:

optimization: {
        splitChunks: {
          cacheGroups: {
            commons: {
              name: 'commons',
              chunks: 'initial',
              minChunks: 2
            }
          }
        }
     },
第二种方法

第二种方法是使用 SVG 的 viewbox 属性来指定显示 SVG 画布的区域,跟 background-position 原理差不多。待探索~

参考:

svgontheweb.com/

developer.mozilla.org/zh-CN/docs/…

www.sumydesigns.com/difference-…

community.wia.io/d/6-generat…


AlloyTeam 欢迎优秀的小伙伴加入。
简历投递: alloyteam@qq.com
详情可点击 腾讯AlloyTeam招募Web前端工程师(社招)