组件库如何实现Icon组件的图标自动按需加载

4,915 阅读3分钟
原文链接: github.com

背景

目前大多数组件库的Icon图标都是使用的font-family加载字体文件,这样存在一个很大的问题就是开发者可能只使用了其中几个图标,但是最终打包的时候会把整个字体文件打包进去,即使打包时开启压缩以及在文件传输时开启gzip,相对于几个图标而引入几百kb以及几m的体积代价依然很大。

但是提供Icon组件,开发者又因这种情况不去使用,那么干脆不如不提供?

既然页面都能实现按需加载,那么为什么Icon组件加载图标时进行按需加载呢?

实现

ionicons 是一个维护了非常久且非常稳定的图标库,近期再次阅读README.md和搜索issue demand load时发现该项目在4.x版本已经支持上述的图标按需加载了。

相关issue:
ionic-team/ionicons#499
ionic-team/ionicons#611

以下是官方描述

If you're using Ionic Framework, Ionicons is packaged by default, so no installation is necessary. Want to use Ionicons without Ionic Framework? Place the following <script> near the end of your page, right before the closing tag, to enable them.

<script src="https://unpkg.com/ionicons@4.2.2/dist/ionicons.js"></script>

大意是如果你不使用Ionic框架,却又要引入Ionicons,引入下列script标签即可
按需加载示例: jsbin.com/rozujasegi/…
或者直接复制下列这段代码到本地html文件然后打开预览效果

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
<ion-icon name="heart"></ion-icon>
</body>
<script src="https://unpkg.com/ionicons@4.2.2/dist/ionicons.js"></script>
</html>

按需加载的原理是加载完ionicons.js会自动去解析html中的ion-icon标签,读取name然后去动态加载对应的.svg文件,然后在对应dom节点中插入svg以及style,如下图
image

应用在React,实现Icon图标按需加载

最开始我是这样使用的

import * as React from 'react';
import { Component } from 'react';
import 'ionicons/dist/ionicons.js';

export default class Icon extends Component {
  render() {
    const html = '<ion-icon name="ios-heart"></ion-icon>';
    return (
        <i dangerouslySetInnerHTML={{__html: html}}/>
    );
  }
}

很显然是错误的,因为这样写一开始ionicons/dist/ionicons.js就打包到了bundle中,icon组件还未插入到dom中,ionicons的解析就已经结束。

经过改进后如下:

import * as React from 'react';
import { Component } from 'react';
import 'ionicons/dist/ionicons.js';

export default class Icon extends Component {
   componentDidMount() {
    const script = document.createElement('script');
    script.src = 'https://unpkg.com/ionicons@4.4.2/dist/ionicons.js';
    script.id = id;
    script.onload = () => {
      document.body.removeChild(script);
    };
    document.body.appendChild(script);
  }

  render() {
    const html = '<ion-icon name="ios-heart"></ion-icon>';
    return (
        <i dangerouslySetInnerHTML={{__html: html}}/>
    );
  }
}

即等待Icon组件渲染完成,已经插入到真实Dom树中后,我们再创建一个script标签去动态加载ionicons.js去解析ion-icon标签,待script标签加载完成后再进行移除。

粗看之下似乎没什么问题了?

我们再来分析一下下面这个场景,Yoshino组件库的文档Icon组件部分会把Icon展示出来,供开发者输入相关关键词查找Icon,大概接近800icons, 地址:yoshino-ui.github.io/#/docs/comp…

image

这个时候上面的Icon按需加载方案就有问题。

我们假设Icon组件文档页有800个图标,也就是说创建了800个Icon实例,那么对应的当这800个Icon组件被插入到dom树后出触发componentDidMount,此时会创建800个script标签去加载ionicons.js,此时浏览器页面会出现假死状态,即无法响应请求。

因此,我们需要判断一下body中是否已经存在了加载ionicons.js的script标签了。

最后改造结果如下:
github.com/Yoshino-UI/…

喜欢Yoshino组件库就点个star吧,欢迎pr! 🚀