小程序的字体图标方案

Dec 31, 2022Front End

在 WEB 前端项目中,我们有成熟的方案来显示图标,如:

  1. 制作图标字体;
  2. 直接显示图像(使用 background-img css属性或 img 标签)。

前者将图标转化为 Unicode 码点的字体图形打包进字体文件中,通过不同的字符可以显示不同的图标。这种方案的图标可以通过 font-sizecolor 修改图标的大小和颜色,与正常字体表现一致,体验非常好。

后者简单粗暴,颜色难以修改(往往不同的颜色的同一图标需要重复出图),如果使用 svg 图案,则能通过 fill: currentColorstroke: currentColor 来设置颜色,同时它是矢量的,具有更好的灵活性,基本可以做到与字体图标相同的体验,但如果每个图标都通过 svg 标签嵌入,HTML 页面的大小则会受到直接的影响。

新颖的图标方案

@antfu这篇文章 介绍了一种新的 css 图标方案,通过使用 css 的 mask-image 属性将元素按给定图像的形状进行裁剪,并可以使用 background-color 设定图标颜色,下面是一个实际实现:

其中,我们将 svg 图案作为 mask-image 的底图,同时为了实现图标的颜色和大小跟随字符的效果,我们还需要额外设置一些样式,因此,最简单的实现看起来是这样的:

span {
  width: 1em;
  height: 1em;
  mask-image: url(url/to/icon.svg);
  mask-size: 100%;
  background-color: currentColor;
}
span {
  width: 1em;
  height: 1em;
  mask-image: url(url/to/icon.svg);
  mask-size: 100%;
  background-color: currentColor;
}

这样我们就实现了一个使用 svg 图片来作为图标,同时又具有和字体图标一样的体验性的图标方案。

而对于彩色图标,与纯色图标唯一的区别是,它的颜色是固定不变的,因此我们可以用更简单的处理方法,直接使用 background-image 属性引入即可:

span {
  width: 1em;
  height: 1em;
  background-size: 100%;
  background-image: url(url/to/icon.svg);
}
span {
  width: 1em;
  height: 1em;
  background-size: 100%;
  background-image: url(url/to/icon.svg);
}

小程序实现

如果你直接在小程序中使用上面的代码,或许在开发者工具上体验良好,但一换到真实设备,Dangdang~,出问题了,图标显示不出来。在微信开放社区这一篇来自 @savokiss回答中 可以找到答案。

我们需要给 mask-image 属性加上 -webkit 前缀,同时图片需要使用 base64 格式。于是我们得到下面的解决方案:

view {
  width: 1em;
  height: 1em;
  display: inline-block;
  background-color: currentColor;
  mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMWVtIiBoZWlnaHQ9IjFlbSIgdmlld0JveD0iMCAwIDQwIDQwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTMwLjI0MTcgMzVDMzAuMjI2NiAzNSAzMC4yMTE1IDM1IDMwLjIwMzkgMzVDMjUuOTYxOCAzNC44NzE1IDIwLjY4MzcgMzEuNjczOCAxNC41MDU3IDI1LjQ5NzVDOC4zMzUzMyAxOS4zMjEzIDUuMTM2NzEgMTQuMDUyMiA1LjAwMDU5IDkuODE4NzlDNC45NDAxIDcuODQ1NzIgOS41MTQ5NyA0LjU2NDgzIDkuNTYwMzQgNC41MjcwM0MxMC44MzgzIDMuNjQyNTUgMTIuMjU5OSAzLjk2NzYyIDEyLjg0OTcgNC43OTE2MkMxMy4yMTI3IDUuMjk4MTIgMTYuNjA3OSAxMC40Mzg3IDE2Ljk3MDkgMTEuMDIwOEMxNy40MDE5IDExLjY5MzYgMTcuMzQ4OSAxMi42ODM5IDE2LjgyNzIgMTMuNjU5MUMxNi41ODUyIDE0LjEyMDIgMTUuNzgzNyAxNS41MzM5IDE1LjM1MjYgMTYuMjg5OUMxNS44NDQyIDE2Ljk3MDIgMTcuMDA4NyAxOC40NTk1IDE5LjI2OTYgMjAuNzI3NEMyMS41MzgyIDIyLjk5NTMgMjMuMDI3OCAyNC4xNTk1IDIzLjcwODQgMjQuNjUwOEMyNC40NjQ2IDI0LjIxOTkgMjUuODc4NiAyMy40MTg2IDI2LjM0NzQgMjMuMTY5MUMyNy4zMDc4IDIyLjY2MjYgMjguMjkwOCAyMi42MDIyIDI4Ljk2MzggMjMuMDE3OUMyOS42MjE3IDIzLjQxODYgMzQuODI0MiAyNi44ODA5IDM1LjE5NDcgMjcuMTM4QzM2LjAyNjUgMjcuNzI3NiAzNi4zNjY4IDI5LjE1NjQgMzUuNDY2OSAzMC40NDE1QzM1LjQzNjcgMzAuNDg2OSAzMi4yMDAyIDM1IDMwLjI0MTcgMzVaIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIzIi8+Cjwvc3ZnPgo=');
  -webkit-mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMWVtIiBoZWlnaHQ9IjFlbSIgdmlld0JveD0iMCAwIDQwIDQwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTMwLjI0MTcgMzVDMzAuMjI2NiAzNSAzMC4yMTE1IDM1IDMwLjIwMzkgMzVDMjUuOTYxOCAzNC44NzE1IDIwLjY4MzcgMzEuNjczOCAxNC41MDU3IDI1LjQ5NzVDOC4zMzUzMyAxOS4zMjEzIDUuMTM2NzEgMTQuMDUyMiA1LjAwMDU5IDkuODE4NzlDNC45NDAxIDcuODQ1NzIgOS41MTQ5NyA0LjU2NDgzIDkuNTYwMzQgNC41MjcwM0MxMC44MzgzIDMuNjQyNTUgMTIuMjU5OSAzLjk2NzYyIDEyLjg0OTcgNC43OTE2MkMxMy4yMTI3IDUuMjk4MTIgMTYuNjA3OSAxMC40Mzg3IDE2Ljk3MDkgMTEuMDIwOEMxNy40MDE5IDExLjY5MzYgMTcuMzQ4OSAxMi42ODM5IDE2LjgyNzIgMTMuNjU5MUMxNi41ODUyIDE0LjEyMDIgMTUuNzgzNyAxNS41MzM5IDE1LjM1MjYgMTYuMjg5OUMxNS44NDQyIDE2Ljk3MDIgMTcuMDA4NyAxOC40NTk1IDE5LjI2OTYgMjAuNzI3NEMyMS41MzgyIDIyLjk5NTMgMjMuMDI3OCAyNC4xNTk1IDIzLjcwODQgMjQuNjUwOEMyNC40NjQ2IDI0LjIxOTkgMjUuODc4NiAyMy40MTg2IDI2LjM0NzQgMjMuMTY5MUMyNy4zMDc4IDIyLjY2MjYgMjguMjkwOCAyMi42MDIyIDI4Ljk2MzggMjMuMDE3OUMyOS42MjE3IDIzLjQxODYgMzQuODI0MiAyNi44ODA5IDM1LjE5NDcgMjcuMTM4QzM2LjAyNjUgMjcuNzI3NiAzNi4zNjY4IDI5LjE1NjQgMzUuNDY2OSAzMC40NDE1QzM1LjQzNjcgMzAuNDg2OSAzMi4yMDAyIDM1IDMwLjI0MTcgMzVaIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIzIi8+Cjwvc3ZnPgo=');
  mask-size: 100%;
  -webkit-mask-size: 100%;
}
view {
  width: 1em;
  height: 1em;
  display: inline-block;
  background-color: currentColor;
  mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMWVtIiBoZWlnaHQ9IjFlbSIgdmlld0JveD0iMCAwIDQwIDQwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTMwLjI0MTcgMzVDMzAuMjI2NiAzNSAzMC4yMTE1IDM1IDMwLjIwMzkgMzVDMjUuOTYxOCAzNC44NzE1IDIwLjY4MzcgMzEuNjczOCAxNC41MDU3IDI1LjQ5NzVDOC4zMzUzMyAxOS4zMjEzIDUuMTM2NzEgMTQuMDUyMiA1LjAwMDU5IDkuODE4NzlDNC45NDAxIDcuODQ1NzIgOS41MTQ5NyA0LjU2NDgzIDkuNTYwMzQgNC41MjcwM0MxMC44MzgzIDMuNjQyNTUgMTIuMjU5OSAzLjk2NzYyIDEyLjg0OTcgNC43OTE2MkMxMy4yMTI3IDUuMjk4MTIgMTYuNjA3OSAxMC40Mzg3IDE2Ljk3MDkgMTEuMDIwOEMxNy40MDE5IDExLjY5MzYgMTcuMzQ4OSAxMi42ODM5IDE2LjgyNzIgMTMuNjU5MUMxNi41ODUyIDE0LjEyMDIgMTUuNzgzNyAxNS41MzM5IDE1LjM1MjYgMTYuMjg5OUMxNS44NDQyIDE2Ljk3MDIgMTcuMDA4NyAxOC40NTk1IDE5LjI2OTYgMjAuNzI3NEMyMS41MzgyIDIyLjk5NTMgMjMuMDI3OCAyNC4xNTk1IDIzLjcwODQgMjQuNjUwOEMyNC40NjQ2IDI0LjIxOTkgMjUuODc4NiAyMy40MTg2IDI2LjM0NzQgMjMuMTY5MUMyNy4zMDc4IDIyLjY2MjYgMjguMjkwOCAyMi42MDIyIDI4Ljk2MzggMjMuMDE3OUMyOS42MjE3IDIzLjQxODYgMzQuODI0MiAyNi44ODA5IDM1LjE5NDcgMjcuMTM4QzM2LjAyNjUgMjcuNzI3NiAzNi4zNjY4IDI5LjE1NjQgMzUuNDY2OSAzMC40NDE1QzM1LjQzNjcgMzAuNDg2OSAzMi4yMDAyIDM1IDMwLjI0MTcgMzVaIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIzIi8+Cjwvc3ZnPgo=');
  -webkit-mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMWVtIiBoZWlnaHQ9IjFlbSIgdmlld0JveD0iMCAwIDQwIDQwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTMwLjI0MTcgMzVDMzAuMjI2NiAzNSAzMC4yMTE1IDM1IDMwLjIwMzkgMzVDMjUuOTYxOCAzNC44NzE1IDIwLjY4MzcgMzEuNjczOCAxNC41MDU3IDI1LjQ5NzVDOC4zMzUzMyAxOS4zMjEzIDUuMTM2NzEgMTQuMDUyMiA1LjAwMDU5IDkuODE4NzlDNC45NDAxIDcuODQ1NzIgOS41MTQ5NyA0LjU2NDgzIDkuNTYwMzQgNC41MjcwM0MxMC44MzgzIDMuNjQyNTUgMTIuMjU5OSAzLjk2NzYyIDEyLjg0OTcgNC43OTE2MkMxMy4yMTI3IDUuMjk4MTIgMTYuNjA3OSAxMC40Mzg3IDE2Ljk3MDkgMTEuMDIwOEMxNy40MDE5IDExLjY5MzYgMTcuMzQ4OSAxMi42ODM5IDE2LjgyNzIgMTMuNjU5MUMxNi41ODUyIDE0LjEyMDIgMTUuNzgzNyAxNS41MzM5IDE1LjM1MjYgMTYuMjg5OUMxNS44NDQyIDE2Ljk3MDIgMTcuMDA4NyAxOC40NTk1IDE5LjI2OTYgMjAuNzI3NEMyMS41MzgyIDIyLjk5NTMgMjMuMDI3OCAyNC4xNTk1IDIzLjcwODQgMjQuNjUwOEMyNC40NjQ2IDI0LjIxOTkgMjUuODc4NiAyMy40MTg2IDI2LjM0NzQgMjMuMTY5MUMyNy4zMDc4IDIyLjY2MjYgMjguMjkwOCAyMi42MDIyIDI4Ljk2MzggMjMuMDE3OUMyOS42MjE3IDIzLjQxODYgMzQuODI0MiAyNi44ODA5IDM1LjE5NDcgMjcuMTM4QzM2LjAyNjUgMjcuNzI3NiAzNi4zNjY4IDI5LjE1NjQgMzUuNDY2OSAzMC40NDE1QzM1LjQzNjcgMzAuNDg2OSAzMi4yMDAyIDM1IDMwLjI0MTcgMzVaIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIzIi8+Cjwvc3ZnPgo=');
  mask-size: 100%;
  -webkit-mask-size: 100%;
}

而问题在于,项目的图标往往很多,如果我们将所有图标都转换为 base64 字符串硬编码在代码中,最终的构建产物体积可能会让你大吃一惊,因此我们需要换一种方式。

我们考虑将所有 svg 资源统一上传到服务器或 CDN 托管上,利用请求获取 svg 内容,将其转换为 base64 字符串后动态设置元素的 mask-image 属性,简单的实现可能是这样的:

// the code bases on uni framework
<template>
  <view class="icon" :style="style" />
</template>

<script>
import { encode } from 'js-base64'
export default {
  props: {
    icon: string
  },
  data() {
    return {
      style: '',
    }
  },
  watch: {
    icon: {
      immediate: true,
      handler(icon) {
        uni.request({
          url: icon,
          success: ({ data }) => {
            // avoid data race
            if (icon !== this.icon) return
            if (data.includes('currentColor')) {
              // it means the color of icon is syncing with font color when svg content contains `currentColor`
              const url = 'data:image/svg+xml;base64,' + encode(data)
              this.style = `mask-image: url('${url}'); -webkit-mask-image: url('${url}'); background-color: currentColor;`
              return
            }
            this.style = `background-image: url('${url}');`
          }
        })
      }
    }
  }
}
</script>

<style>
.icon {
  /* basic style */
  width: 1em;
  height: 1em;
  display: inline-block;
  background-size: 100%;
  mask-size: 100%;
  -webkit-mask-size: 100%;
}
</style>
// the code bases on uni framework
<template>
  <view class="icon" :style="style" />
</template>

<script>
import { encode } from 'js-base64'
export default {
  props: {
    icon: string
  },
  data() {
    return {
      style: '',
    }
  },
  watch: {
    icon: {
      immediate: true,
      handler(icon) {
        uni.request({
          url: icon,
          success: ({ data }) => {
            // avoid data race
            if (icon !== this.icon) return
            if (data.includes('currentColor')) {
              // it means the color of icon is syncing with font color when svg content contains `currentColor`
              const url = 'data:image/svg+xml;base64,' + encode(data)
              this.style = `mask-image: url('${url}'); -webkit-mask-image: url('${url}'); background-color: currentColor;`
              return
            }
            this.style = `background-image: url('${url}');`
          }
        })
      }
    }
  }
}
</script>

<style>
.icon {
  /* basic style */
  width: 1em;
  height: 1em;
  display: inline-block;
  background-size: 100%;
  mask-size: 100%;
  -webkit-mask-size: 100%;
}
</style>

而为了减少项目依赖,让整个架构看起来干净一点,可以将 svg 图标事先转化为 base64 进行存储,这样小程序请求到的资源无需进行 base64 编码,减少运行时损耗。当然,这是另外的事情了。

至此,我们已经实现了一个在小程序显示字体图标的可行方案了。