Webpack5的事件流和插件机制

2,611 阅读5分钟

Webpack的事件流和插件机制

前面2篇我们已经讲了webpack的运行原理和loader的原理,本篇我们来讲一下webpack的事件流和插件的机制。本系列一共分为三篇

  1. Webpack5的打包分析
  2. Webpack的loader的实现
  3. Webpack的事件流和插件的原理

webpack的事件流

什么是webpack的事件流,这里引入深入浅出的webpack里面的一段话。

Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。 Webpack 通过 Tapable 来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 --吴浩麟《深入浅出webpack》

webpack的运行机制

前面第一篇我们自己实现了一个简单的webpack, 我们通过一个图片来了解一下webpack从开始启动到打包资源后的一个整体运行机制。 总体分为这几个关键步骤

  1. 当我们运行webpack的时候,它会读取你的传入的配置文件(webpack.config.js), 然后运行compile.run来初始化本质构建的参数(入口文件等),然后开始分析模块 (environment钩子函数)
  2. 接下来进入了entryOption阶段,webpack开始读取配置的入口entry,然后开始递归的遍历所有的入口文件 (entryOption钩子)
  3. webpack进入入口文件后,开始分析依赖项,进行compilation过程。 (compilation 钩子函数)
    • 使用配置好的loader对文件内容进行编译(buildModule), 我们可以从传入的事件回调的参数module上拿到模块的resource(资源路径) (buildModule 钩子函数)
    compiler.hooks.compilation.tap('compilation', (compilation , compilationParams) => {
      compilation.hooks.buildModule.tap('sourceMap', (module) => {
        console.log(module.resource)  // 资源路径
      })
    })
    
    • 将经过loader处理后的文件,通过acorn解析生成ast语法树(normalModuleLoader)分析文件的依赖关系,然后形成依赖数组,重复上面的模块分析过程 (webpack5 已经废弃了normalModuleLoader 这个钩子)
  4. emit阶段: 所有文件的编译及转化都已经完成,包含了最终输出的资源,我们可以在传入事件回调的compilation.assets 上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息。 下面这张图是整体的运行机制和主要插件的图的对比 还有一张图是基于webpack基于模块的更加详细的运行流程图(虚线代表重复的执行)

webpack的插件开发

我们主要实战2个插件

  1. 使用md模式列出文件打包资源的列表
  2. 通过插件分析文件依赖的组件

首先我们需要知道webpack的插件需要是怎么开发的。它需要2个条件

  1. 必须是一个类
  2. 要暴露一个apply方法,webpack会在初始化的时候执行这个方法, 并传入一个compiler, 源码里面是这样的
if (Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
        if (typeof plugin === "function") {
            plugin.call(compiler, compiler);
        } else {
            plugin.apply(compiler);
        }
    }
}

列表打包资源插件

从上面的图我们可以看出,我们可以在资源打包后对打包后的资源进行获取,然后新增一个文件。

  1. 通过emit 钩子函数,我们可以在回调函数中的的compilation中的参数拿到assets资源列表
  2. 然后新增一项资源,通过webpack进行流程处理
class FileListPlugin {
    constructor({ filename = 'index.md' }) {
        this.filename = filename;
    }

    apply(compiler) {
        compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, cb) => {
            let assets = compilation.assets;
            let content = '## 文件    资源大小\r\n';
            Object.entries(assets).forEach(([key, value]) => {
                content += `${key}    ${value.size()}\r\n`;
            });
            assets[this.filename] = {
                size() {
                    return content.length;
                },
                source() {
                    return content;
                }
            };
            cb();
        });
    }
}

module.exports = FileListPlugin;

组件被使用的列表

在现在一切皆组件的思想下,一个组件可能被多个页面使用,有时候我们修改一个组件,都不知道会不会影响到其他的页面,所有这里开发了一个插件,用来统计组件被使用的页面情况。在webpack4和webpack5中的使用不一样,我们现在是基于webpack5进行分析。

  1. 从上面的流程图中我们可以看到,每一个资源(asset) 都会模块处理阶段NormalModuleFactory, 所有我们可以在模块经过loader处理完成后的afterResolve的钩子来分析模块
  2. 在分析完模块的依赖后,可以通过done钩子等将分析的内容输出
class NormalModuleFactory {
  constructor() {
    this.dependencies = {}
    this.entry = ''
    this.workDictory = ""
  }

  apply(compiler) {
    compiler.hooks.normalModuleFactory.tap('NormalModuleFactory', (nmf) => {
      nmf.hooks.afterResolve.tapAsync('nmf', (result, callback) => {
        let { request, contextInfo, context } = result
        if (!request.includes('node_modules')) {
          if (!contextInfo.issuer) {
            console.log('entry', path.normalize(request))
            this.entry = request
            this.workDictory = context
          } else if(!['react', 'react-dom'].includes(request)){
            // 有依赖项 contextInfo 是请求资源的上下文
            const requestPath = path.relative(this.workDictory, contextInfo.issuer)
            // './component/Hello   requestPath: src/index.js
            if(!requestPath.includes("node_modules")) {
              const resourcePath = path.join(context, request)
              console.log(requestPath, request)
              request = path.relative(this.workDictory, resourcePath)
              if(!this.dependencies[request]) {
                this.dependencies[request] = [requestPath]
              } else {
                this.dependencies[request].push(requestPath)
              }
            }
          }
        }
        callback()
      })
    })
    compiler.hooks.done.tap('NormalModuleFactory', () => {
      console.log(this.dependencies)
    })
  }
}

module.exports = NormalModuleFactory

执行npx webpack后我们可以得到这个内容, key为被使用的组件的路径,value为使用该组件的页面或者组件,可以根据项目内容再进行路径匹配分析具体的样式和组件依赖。

{
  'src\\base\\a.js': [ 'src\\index.js' ],
  'src\\component\\Hcc': [ 'src\\index.js' ],
  'src\\component\\Hello': [ 'src\\index.js', 'src\\component\\Hcc.jsx' ],
  'src\\index.less': [ 'src\\index.js' ],
  'src\\base\\b': [ 'src\\base\\a.js' ],
  'src\\pic\\hcc.jpg': [ 'src\\index.less' ]
}
参考文章
  1. webpack插件机制探索
  2. 揭秘webpack插件工作流程和原理