导读:在产品中适当使用图标,可以让产品更生动,也更简洁。在前端项目中,处理和引入图标都是必不可少的环节。在 Web 产品中引入图标,大致经历过如下几个阶段:使用独立的图片来引入图标、使用 CSS sprites 技术、使用字体图标(font icons)、使用 SVG(inline SVG/SVG sprites)、在前端视图层框架中封装组件。本文将简单梳理一下图标相关的工作流程的演进,以及我们在百度设计语言系统推进过程中相关的一些尝试。
全文7006字,预计阅读时间 14分钟。
在过去有很长一段时间,前端是通过引入图片来承载图标。在没有 CSS 支持的时代,用 <img>
标签引入图标图片是唯一的可能。
<a href="/contact.html"> <img src="mail.jpg" alt="email"></a>
到了 CSS 支持背景图以后,人们开始使用 background-image
来引入一个个小图片,但本质上没有改变每个图标都使用单独图片的问题。
显然,这样的方式在有很多图标的网页中将发起很多 HTTP 请求,占用浏览器的并行请求数量,导致整体加载时间缓慢,体验很差。对于有些鼠标悬浮后切换图标的设计,这种方式还会出现第一次切换时需要等待图标加载的问题。(但是令人沮丧的是,直到现在还有网站依然保留着这样的方式。)
后来在大约本世纪初的头几年,人们找到了一种新的技巧:通过将图片合并技术(image sprite)引入前端,将数量众多的图标图片进行巧妙拼合,并且在样式中通过 background-position
来通过不同位置匹配不同的图标进行显示。例如:
.toolbtn { background: url(icons.png); display: inline-block; height: 20px; width: 20px;}#btn1 { background-position: -20px 0px;}#btn2 { background-position: -40px 0px;}
虽然这种方式相较于每个小图标一个图片文件,只会发起一次 HTTP 请求,对性能更加友好,但是依然有着如下问题:
background-position
这种方式的限制,生成逻辑无法保证灵活适应各种可能的使用场景。
图片来自https://www.smashingmagazine.com/2012/04/css-sprites-revisited/
在这个时代,设计师和工程师协作的模式一般来说都是设计师将设计好的图标文件交付给工程师,由工程师来通过图片编辑工具或者一些雪碧图生成器来维护拼合后的图片,效率和可维护性都非常堪忧。
由于图标从某种程度上来看可以被视为“象形文字”,所以当 CSS 开始支持 @font-face
引入 web font,人们立刻想到了用它来载入、显示图标。从 2012 年至今,提供大量免费图标的 FontAwesome 就取得了很大的成功(后来开始商业化的 FontAwesome 5 的甚至为他们在 Kickstarter 上筹集到了一百万美金),各种字体图标平台也层出不穷。阿里的 iconfont.cn 平台从多年前开始就已经成为国内最受欢迎的图标托管、共享、管理平台。可以说字体图标时至今日还是最热门的 web 图标方案之一。
字体图标的原理非常简单,通过占用一些 Unicode 字符编码(通常是私人使用区,U+E000-U+F8FF
、U+F0000-U+FFFFD
以及 U+100000-U+10FFFD
范围内)并为其绘制字形,同时生成好一堆预定义的图标名 class name,通过 web font 的方式加载资源,通过对应的 class name 来引用图标。由于各个浏览器对 web font 支持的字体格式兼容性有差异,往往需要生成多个格式的字体供浏览器进行选择性加载:
/* iconfont.cn 生成的样式文件大致如下: */@font-face { font-family: "iconfont"; src: url('iconfont.eot'); /* IE9 */ src: url('iconfont.eot#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('iconfont.woff2') format('woff2'), url('iconfont.woff') format('woff'), url('iconfont.ttf') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ url('iconfont.svg#iconfont') format('svg'); /* iOS 4.1- */}.iconfont { font-family: "iconfont" !important;}.icon-flag:before { content: "\e233";}
在 HTML 中使用:
<i class="icon-flag"></i>
字体图标虽然也很难维护,但是相比“雪碧图”还是有不少明显的优势:
但我们可以看出,这个方案对使用者的工程能力已经有所要求。虽然在这个时代,多数业内前端团队已经都有了初步的工程化能力,开始使用诸如 Grunt/Gulp 甚至 webpack 等工具,基于 Node + npm 去定制各自团队的工程化方案了,但是编排每个图标的 Unicode 编码、生成对应的 CSS 代码就已经有比较大的工作量,更别说生成这么多格式的字体文件,普通工程师根本无从下手。这也是 iconfont.cn 吸引大量用户的重要原因。重度依赖第三方平台,自己建设成本又比较高,使得图标的可维护性依然存在一定的痛点。
另外,虽然字体图标解决了一些“雪碧图”的体验问题,它也带来了一些新问题:
图片来自https://github.blog/2016-02-22-delivering-octicons-with-svg/ 这一点实际上和“雪碧图”有着很大的共同点。虽然我们可以使用 data URI 来将资源内联,事实上有很长时间我们也的确使用过将图片或者字体通过 data URI 编码后内联到 HTML 的方式来避免这个加载的时间差,但是编码本身会增加内容 1/3 左右的尺寸,实际上只能算是一种取舍和妥协。更别说字体图标需要生成如此多格式的字体,内联到 HTML 网页性能将大打折扣。
aria-hidden
这样的语义标记,会对读屏器用户产生多大的困惑。SVG 天生就带有可伸缩(SVG 中的 S)特性,非常适合用来实现图标。同时,SVG 是文本文件,同时诸多支持矢量编辑的设计工具都支持通过 SVG 导出,设计师可以直接交付给工程师使用,也不再需要生成字体文件,大大缓解了可维护性上的痛点。但如果将它当成图片,通过 <img>
或 CSS background-image
来引入,仅仅有这些优势还不足以撼动图标字体的地位。
SVG 的真正强大之处在于,当将其内联入 HTML 内容,那么它的文档模型将可以被该页面的 JS/CSS 访问和操作。这为 web 图标开启了新的篇章:
<title>
元素标记内容,对读屏器友好。相比于通过图片资源加载或者图标字体,只有一个劣势:
虽然内联 SVG 有很多优势,但是在这个阶段,在开发时使用它们却不像字体图标那么简单直接(引入一个 CSS,前端就能任意使用),需要对工程有一定侵入性的处理。GitHub 在 2016 年全面启用了内联 SVG 的方案,他们的技术栈是 Ruby 的后端渲染,通过服务端脚本定义的 helper 函数来进行图标字体的调用:
<%= octicon(:symbol => "plus") %>
输出:
<svg aria-hidden="true" class="octicon octicon-plus" width="12" height="16" role="img" version="1.1" viewBox="0 0 12 16"> <path d="M12 9H7v5H5V9H0V7h5V2h2v5h5v2z"></path></svg>
由于 SVG 支持一个 <use>
元素,可以从内联的 SVG 中选取特定内容出来作为独立的 SVG 进行显示,所以人们受 CSS sprite 的启发,也设计了一个 SVG sprite 方案。引入整个 SVG sprite 的资源仅需要内联一个 <svg>
元素:
<svg> <defs> <symbol id="shape-icon-1"> <!-- icon paths and shapes --> <symbol> <symbol id="shape-icon-2"> <!-- icon paths and shapes --> <symbol> <!-- etc --> </defs></svg>
使用时:
<svg viewBox="0 0 16 16" class="icon"> <use xlink:href="#shape-icon-1"></use></svg>
同时,也有不少基于 Grunt/Gulp/webpack 的构建方案,来快速生成 SVG sprite。
这种方式主要的问题在于:
终于到了我们现在所处的时代,这是一个 web 端渲染逻辑被移到前端,前端工程方向被组件化框架主导的时代。在使用 React/Vue/Angular/Svelte/…… 等各种框架的过程中,我们已经习惯于将视图逻辑通过组件进行拆解和复用。那么我们很自然地就可以通过设计图标组件来对底层方案进行一层封装,暴露给前端更简单直接的 API 来使用图标。要注意的是,这并没有在根本上改变 web 图标渲染的方式,底层依然是基于前文提到的各种方案。在不使用这些视图层框架的项目中,我们依然仰赖使用上述 low-level 的实现来进行开发。
当然,从各方面综合比较,封装内联 SVG 应该是当前最佳的选择。上文 GitHub 后端 helper 的方案对应当前前端的技术方案,实际上就是基于内联 SVG 的图标组件。npm 上目前也有很多基于各个组件框架开发的图标组件,包括 FontAwesome 都已经内置了 SVG、React/Vue 组件等更现代化的方案。
既然体验问题已经由内联 SVG 得到了比较好的解决,那么在这个阶段我们就有更多的精力去更多地考虑研发效能、一致性、开发体验的问题了。从我们在百度内部以往的实践中来看,存在这如下的一些问题:
svg-icon-loader
的方案将图标引入项目,但方案往往各不相同。一旦引入这样的流程,相当于给图标在特定项目中新增了一个 fork 版本,日后想做设计风格的统一调整就需要业务跟进修改,成本很高。理想情况下,我们希望达成如下目标:
目前我们在推进百度设计语言系统的过程中,和工程效能团队一起,设计了如下整体方案:
图标平台整体流程
这个平台可以视为是一个简单的图标 CMS,可以创建/管理图标库,图标设计师负责来在其中添加、管理图标。在完成数据的更新后,可以选择发布当前图标输出到 API。这个 API 返回图标库中图标的图形数据(SVG 源文件)和元数据,在整个流程中主要有两个消费者:给设计团队使用的 Sketch 插件,以及前端的编译/发布服务。我们允许图标库发布时通过 webhook 配置需要通知的编译服务,所以有必要的话,不同的使用方也可以选择自己自定义整套编译发布的流程。
我们给设计团队提供了联通图标管理平台的 Sketch 插件,设计师可以在插件中快速搜索需要的图标进行使用。通过我们的插件导出在线标注稿后,标注稿上就会自动标注图标在图标平台中的唯一标识符,这也是我们用来生成图标组件时用的标识符,前端工程师通过它就能直接从图标组件包中引入对应的图标组件。
这个服务在图标库 API 触发更新时主要做了三件事:
currentColor
。在这一步我们通过 svgson
遍历 SVG 元素处理相关逻辑。编译服务对包模板(boilerplate)仅有的约定是:
模板提供者需要提供图标组件的具体实现,以及将图标数据转换为前端代码的构建脚本。如果没有特殊的需求,直接使用我们提供的 React/Vue 等框架下的组件模板,就可以获得高质量的前端图标组件实现了。
通过编译服务发布完成以后,前端工程师只需要知道:1. 使用的图标来自哪个 npm 包 2. 这个图标叫什么名字,即可快速在前端项目中引入图标。同时,整个流程保证了设计师产出的设计稿、前端实现的一致,并且可以从图标平台中心化地控制升级。
在 Web 产品中引入图标我们前端工程师做过很多探索,也产出过很多相关的辅助工具来完善整个协作流程。在目前组件化开发的大背景下,我们通过分析各个方案的优缺点,建立起一套当下的“最佳实践”,减少了流程中的沟通和容易出错的人工操作,高效地达成了设计和实现的一致性。最后,希望本文的内容能给大家带来收获,谢谢。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。