如何在项目中使用图标 一节中,我们介绍了开发时引入自定义 svg 图标的方法,例如:

以上修改甚至都不需要重启开发服务器,这一切都是通过模版项目中的 build/loaders/svg-loader 完成的,本文将介绍其实现原理。

loader 是什么

在 webpack 中,loader 用于对模块的源代码进行转换。例如我们熟悉的 babel-loader 使用 Babel 转译源代码,style-loader 通过向 html 代码中注入 style 标签添加样式。

在我们的场景中,需要向源代码中注入 svg 的注册代码,此时使用 loader 再合适不过了。

我们使用 webpack 推荐的 配置方式

// build/webpack.base.conf.js
module: {
    rules: [
        {
            resource: resolve('src/app.js'), // 应用loader的文件
            loader: 'svg-loader',
            enforce: 'pre' // 声明svg-loader最先执行
        }
    ]
},
resolveLoader: {
    alias: {
        'svg-loader': path.join(__dirname, './loaders/svg-loader')
    }
}

module.rules规则中,使用了enforce: 'pre',这是为了保证 svg-loader 的执行时机在所有 loader 之前。例如待修改的src/app.js,也满足下面 babel-loader 的规则,将在 svg-loader 处理(注入了使用ES6语法的代码)之后执行。

处理流程

现在我们已经完成了 svg-loader 的注册,下面将涉及内部具体的处理流程。

首先要明确我们需要注入的代码内容。之前在 如何在项目中使用图标 一节中提到过,我们使用 vue-awesome注册自定义 svg 以及使用 svg 格式的 fontawesome 图标。所以以下两类代码就是我们需要注入的:

// 使用 svg 格式的 fontawesome 图标
import 'vue-awesome/icons/envelope';

// 注册自定义 svg 图标
Icon.register({
    myCustomSvg: {
        width: 100,
        height: 100,
        d: 'M...'
    }
});

这样 loader 中的逻辑就很清晰了:

module.exports = function (source) {

    // 从vue-awesome中导入
    if (icons) {
        source += icons.map(name => `import 'vue-awesome/icons/${name}';`).join('');
    }

    // 从svg文件夹中取
    fs.readdirSync(svgDir).forEach(file => {
        let svgName = prefix + path.basename(file, path.extname(file));

        // 注册使用到的svg
        source += `Icon.register(
            {
                '${svgName}': {
                    width: ${parseInt(sizeMatch[1], 10)},
                    height: ${parseInt(sizeMatch[2], 10)},
                    d: '${dMatch[1]}'
                }
            });`;
    });
    return source;
};

至此 svg-loader 中的主要流程已经介绍完了,下面我们将关注开发中的文件更新问题。

监听文件更新

在开发模式下使用 svg 图标的场景中,会出现两种文件更新情况:

  1. 向自定义 svg 文件夹中放入新文件,此时文件夹内容发生更新
  2. 添加 fontawesome 图标,此时config/icon.js文件内容发生更新

在发生以上两类文件更新时,如果能够自动触发 webpack 重新编译,不需要手动重启服务器,将节省宝贵的开发时间。

webpack 的文件监听机制比较复杂,简单来说就是在 Compiler 中通过 Watchpack (底层依赖 chokidar) 监听了compilation.fileDependencies(单个文件) 和 compilation.contextDependencies(文件夹),两者发生变化均会触发重新编译。

在开发模式中,webpack-dev-middleware 已经默认 开启了监听模式

在 loader 执行方法中,this 指向 loader 上下文,其中包含了许多重要的属性和方法,这里只关心两个:

直接使用这两个方法,就能实现文件更新时触发重新编译了。另外有一点需要注意,config/icon.js 中的 icons 数组内容发生变动后,需要删除 require 之前的缓存,否则取到的还是旧数据。

const iconConfigPath = require.resolve('../../config/icon');

// 删除require缓存
delete require.cache[iconConfigPath];
const iconConfig = require(iconConfigPath);
const svgDir = iconConfig.svgDir;

// 监听`svg`文件夹变化
this.addContextDependency(svgDir);

// 监听`config/icon.js`文件变化
this.addDependency(iconConfigPath);