一个 icon 引发的血案

作者: frank 发表日期:2017-11-19 11:57:17 更新日期:2017-11-19 12:00:33 分类:猿文色

摘要

背景: 最近在使用 VUE + ElementUI + Egg 进行一个项目的重构. VUE 使用的是官方的 CLI 完成初始化, 使用了 scss 做样式管理. 在 utils.js 加入了 sass-resources-loader 以便于引入 scss 全局变量.

正文

上述背景下, 某一天突然发现首页的图标会闪烁. 如下面的视频描述 (配置了 network 为 slow 3g, 实际上只会闪烁一下) :


遇到这个问题的时候看了下 network, 发现每一次加载新的页面 (应该说是有新的组件被渲染的时候) 就会重新加载一次字体文件, 虽然返回的是 304, 但是总会有一次请求的过程. 


为什么会请求多次字体文件呢? 

第一步确认了只有 Chrome 和 Safari 才会有问题, Firefox Quantum 是没有问题的, 不会闪烁, 也不会多次加载字体文件.
第二步确认了在 build 后的项目中是没有问题的.
第三步确认了使用 ElementUI 的自定义图标是没有问题的.

那问题应该就是在开发模式下的某一步配置出现了问题, 而且是和 scss 配置有关的, 进一步是和自定义图标的配置有关.

上面背景也说了项目使用了 sass-resources-loader 引入了 scss 全局变量, 而 _icomoon.scss (使用 icomoon 生成的图标文件) 也被放在了全局变量中. 恰好字体文件也定义在 _icomoon.scss 文件中:

@font-face {
  font-family: 'icomoon';
  src: url('~assets/fonts/icomoon.eot?uzfk7c');
  src: url('~assets/fonts/icomoon.eot?uzfk7c#iefix') format('embedded-opentype'),
    url('~assets/fonts/icomoon.ttf?uzfk7c') format('truetype'),
    url('~assets/fonts/icomoon.woff?uzfk7c') format('woff'),
    url('~assets/fonts/icomoon.svg?uzfk7c#icomoon') format('svg');
  font-weight: normal;
  font-style: normal;
}


随即将字体定义移出全局变量文件 _icomoon.scss, 再刷新就 OK 了.


为什么会这样?
猜测一: 开发模式下只有 HTML 和 JS 文件被加载, 并没有加载 CSS 文件, 那所有的样式肯定是被打包到 JS 文件的, 会不会为了方便, webpack(sass-resources-loader) 会为每一个使用了 scss 全局变量的模块加载一次 scss 全局变量文件?
猜测二: Firefox Quantum 和 使用了 webkit 内核的浏览器在处理这种重复加载的资源文件上处理方式不同.


验证猜测一:

看了下生成的 app.js 文件, 果然 webpack 为每一个模块重新加载了字体文件.


具体是如何做到的呢?
首先看一下 sass-resources-loader 的工作原理:

// loader.js 的关键代码, 最后执行 rewriteImports 方法
...
async.map(
    files,
    (file, cb) => {
      fs.readFile(file, 'utf8', (error, contents) => {
        rewriteImports(error, file, contents, moduleContext, cb);
      });
    },
    (error, resources) => {
      processResources(error, resources, source, moduleContext, callback);
    }
  );
...

进而看一下 rewriteImports 做了什么事?

// 最后生成一系列的 @import ${quote}${newImportPath}${quote}
...
const rewritten = contents.replace(importRegexp, (entire, single, double, unquoted) => {
    const oldImportPath = single || double || unquoted;

    const absoluteImportPath = path.join(path.dirname(file), oldImportPath);
    const relImportPath = getRelativeImportPath(oldImportPath, absoluteImportPath, moduleContext);
    const newImportPath = relImportPath.split(path.sep).join('/');
    logger.debug(`Resources: @import of ${oldImportPath} changed to ${newImportPath}`);

    const lastCharacter = entire[entire.length - 1];
    const quote = lastCharacter === "'" || lastCharacter === '"' ? lastCharacter : '';

    return `@import ${quote}${newImportPath}${quote}`;
  });
...

通过上述代码可以看出, sass-resources-loader 会为每一个 scss 文件 动态导入我们定义的所有 scss 全局变量文件.


验证猜测二, 待续...