# 图标生成字体文件

# 背景

公司内部使用字体图标,是在阿里的字体图标矢量库 (opens new window)中找好图标,添加到组里,再下载下来,把文件提交到我们的gitlab仓库维护起来。

这样当然不方便。

先在npm上搜索了下,没找到类似的方案,但把svg图标生成字体与css的库有,也有把图标放在本地某个文件夹就不用再管的loader,后者是webfonts-loader (opens new window)

于是我们讨论了一个方案:

  1. 搭建一台服务器,所有的svg图标都放上面

  2. 解析DOM中用到的图标名称

  3. 请求服务器资源,下载到本地某个目录

  4. 按照webfonts-loader的文档,配置这个目录,之后应该开发与打包都OK

整体思路很简单,实现起来却有些波折。

# 技术验证

首先进行技术验证。

# 新建webpack工程

我用webpack4创建了一个新的工程,里面添加了vue-loader,在其中一个vue文件里这样使用图标(因为我们主要技术栈是vue,先不考虑其它的):

<my-font name="go"></my-font>
<my-font name="search"></my-font>

# 自定义loader,解析DOM,下载图标

自定义一个loader,放在vue-loader的后面,也就是先它执行,对应的webpack的配置如下:

rules: [
    {
        test: /\.vue$/,
        use: ['vue-loader', myfontLoader]
    },
}

我的loader作用就是用来解析vueDOM元素,用正则表达式匹配出my-fontname,再从服务器下载对应的图标到本地目录。

# 使用webfonts-loader

  1. 配置文件 webfonts-loader的配置文件myfont.font.js是这样的:
module.exports = {
  'files': [
    './myfont/*.svg'
  ],
  'fontName': 'myfonticons',
  'classPrefix': 'myfonticon-',
  'baseSelector': '.myfonticon',
  'types': ['eot', 'woff', 'woff2', 'ttf', 'svg'],
  'fileName': 'app.[fontname].[hash].[ext]'
};

其实就是下载到myfont文件夹。

  1. webpack配置

    webfonts-loader除了上述配置文件,还需要有额外的webpack配置:

{
  test: /\.font\.js/,
  use: [
    MiniCssExtractPlugin.loader,
    {
      loader: 'css-loader',
      options: {
        url: false
      }
    },
    'webfonts-loader'
  ]
}
  1. 引入文件 还需要在入口文件中引入一个js
require('./myfont.font');

它的原理是,引用了这个文件后,就可以使用webfonts-loader得到配置文件,生成css提供给下一个loader——css-loader,再使用MiniCssExtractPlugin.loader生成单独的css文件。因为它监听配置的files文件夹,所以当文件夹内文件变更后,会触发webpack重新编译,编译后会进行热更新。

# 测试

经过测试,开发与打包都没有问题。

# 迁移到vue

# 配置

我们项目是使用vue-cli创建的,因为内置了webpack,封装了相当多的功能,所以只能通过它暴露的接口修改webpack配置。

chainWebpack: (config) => {
      config.module
        .rule('kissfont')
        .test(/kiss\.font\.js/)
        // .use(MiniCssExtractPlugin.loader)
        // .loader(MiniCssExtractPlugin.loader)
        // .end()
        .use('css-loader')
        .loader('css-loader')
        .end()
        .use('webfonts-loader')
        .loader('webfonts-loader')
        .end();

      config.module.rule('vue').use(myfontLoader).loader(myfontLoader).end();
  },

# 缺点

问题来了,如果使用MiniCssExtractPlugin.loader会报错。

注释掉后,倒是不报了,但还有另一个问题,首次是因为临时文件夹下面并没有图标,会报错。我的解决方案是每次先加个默认图标。

# 折腾

如果我能在图标下载完成后,再生成css与字体文件,是不是更好点?我们知道,webpack的插件提供了大量的钩子函数,在loader中下载完图标,完全可以在插件中干这件事情。

于是开始折腾。

  1. webfonts-loader代码中,找到用来生成css和字体文件的库——@vusion/webfonts-generator

  2. 使用html-webpack-plugin的钩子函数html-webpack-plugin-before-html-processing,在它期间生成css与字体

  3. 把生成的css注入到页面的css标签

这时,又出现一个问题:无法热更新。主要是开发环境下会有这种需求。

我进行了下列尝试:

  1. css改为用js动态插入到页面,仍使用html-webpack-plugin来注入代码。失败。

  2. 开发环境下,webpack把生成的代码是放内存里了,我找到生成css的地方,并将它放到对应的内存(compiler.outputFileSystem,使用的是memory-fs)中。失败。

  3. 服务启动时代理我存放css的临时目录(config.devServer.contentBase(utils.getTmpDir())),这样目录下文件有更新,页面就会刷新。发现确实是进行刷新,而不是热更新。失败。

怎么办呢?

研究热更新的原理,还是我的css生成的时机不对,不在编译阶段,就没法热更新。可如果在编译阶段,我又无法知道什么时候所有的图标都下载完成。

陷入僵局。

没办法,只能参考webfonts-loader,在入口文件中引入cssimport 'xx.css'),再加个style-loadercss-loader专门处理这个css,这样,当这个css有变化时,也能热更新了。

兜兜转转,没想到还是回归了webfonts-loader的思路,好尴尬。

# 过程中遇到的问题

# 多进程

parallel如果配置为true时,会根据CPU的核心数量开启多进程,如果本地存储变量做为中转记录的话,会失效,只能存在临时文件,或者process.env中,后者有个时机问题,只能在进程开始前存入,否则依然会失效。

# cache-loader缓存

vue文件被默认添加了cache-loader,它在加载一次后,会导致下次跟它相关的loader不会再触发,应该是使用了pitch函数的缘故。

为了打包的图标css能够纯净,需要每次打包都下载需要的图标,这时就得禁止cache-loader

const vueRule = config.module.rule('vue');
vueRule.uses.delete('cache-loader');
vueRule.use(fontLoader).loader(fontLoader).options(options).end();

# require缓存

loader中,添加对配置文件的监听:

this.addDependency(dir);

但配置文件的读取,是用require('xx.js')这样做的,它有个问题,执行过一次后,就加到内存里了,不会再读取这个文件,于是需要在合适的时候清除一遍缓存:

delete require.cache[require.resolve(dir)];

# 总结

通过这个功能,学习了webpack的插件和loader的使用,对cache-loaderthread-loaderhtml-webpack-plugin以及热更新都有了些了解。

最终的插件和使用可以见这里 (opens new window)