• 初始化参数:从配置文件和shell语句中读取于合并参数,得出最终的参数。
  • 开始编译:用上一步得到的参数初始化compiler对象,加载所有配置的插件,执行对象的run方法开始执行编译,确定入口,根据配置中的entry找出所有的入口文件。
  • 编译模块儿:从入口文件出发,调用所有配置的loader对模块进行编译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过本步骤的处理。
  • 完成模块儿编译:在经过第四步使用loader翻译完所有模块后,得到了每个模块儿被翻译后的最终内容以及它们之间的依赖关系输出资源。根据入口和模块之间的依赖关系,组装成一个个包含了多个模块的chunk。再把每个chunk转换成一个单独的文件,加入到输出列表。这步是可以修改输出内容的最后机会。
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,webpack会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用Webpack提供的API改变Webpack的运行结果。

下面我们来模拟一下webpack的运行流程。在模拟这个运行流程前,首先我们需要先简要介绍一下webpack的一个核心包。tapable。下面是tapable的一个简单使用。我们调用new SyncHook()创建了一个hook实例。hook.tap相当于我们平常所写的bnt.addEventListener,用于绑定时间。hook.call相当于trigger,用于执行事件。

const { SyncHook } = require('tapable')
const hook = new SyncHook()
hook.tap('do something', () => {
  console.log('do something')
})
hook.call()

运行后,控制台会输出

do something

现在我们来简要模拟下webpack的运行流程。新建一个工程,将以下下代码拷贝到对应的文件夹。

plugins/DonePlugin.js

module.exports = class DonePlugin {
  apply(compiler) {
    compiler.hooks.done.tap('DonePlugin', () => {
      console.log('DonePlugin')
    })
  }
}

plugins/RunPlugin.js

module.exports = class RunPlugin {
  apply(compiler) {
    compiler.hooks.run.tap('RunPlugin', () => {
      console.log('RunPlugin')
    })
  }
}

webpack.config.js

const path = require('path')
const RunPlugin = require('./plugins/RunPlugin')
const DonePlugin = require('./plugins/DonePlugin') 
module.exports = {
  context: process.cwd(),
  mode: 'development',
  devtool: false,
  entry: './src/app.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        },
        include: path.join(__dirname, 'src'),
        exclude: /node_modules/
      }
    ]
  },
  plugins:[new RunPlugin(), new DonePlugin()],
  devServer: {}
}

模拟webpack的工作流程

webpack-flow.js

/* 
webpack工作流程
*/
const { SyncHook } = require('tapable')
const fs = require('fs')
const path = require('path')
class Compiler {
  constructor(options) {
    this.options = options
    this.hooks = {
      run: new SyncHook(),
      done: new SyncHook()
    }
  }
  run() {
    const modules = []
    const chunks = []
    const files = []
    this.hooks.run.call() // 触发run钩子执行
    // 根据配置中的entry找出所有的入口文件
    const entry = path.join(this.options.context, this.options.entry)
    console.log('entry', entry)
    //从入口文件出发,调用所有配置的Loader对模块进行编译,再找出该模块依赖的模块,
    //再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
    // 1.读取模块内容
    let entryContent = fs.readFileSync(entry, "utf8");
    let entrySource = babelLoader(entryContent);
    //模块module  chunk代码块 file bundle 文件的关系
    let entryModule = { id: entry, source: entrySource };
    modules.push(entryModule)
    //根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
    let chunk = { name: "main", modules };
    chunks.push(chunk);
    //再把每个Chunk转换成一个单独的文件加入到输出列表
    let file = {
      file: this.options.output.filename,
      source: `  
           (function (modules) {
           function __webpack_require__(moduleId) {
               var module = { exports: {} };
               modules[moduleId].call(
               module.exports,
               module,
               module.exports,
               __webpack_require__
               );
               return module.exports;
           }
           return __webpack_require__("./src/app.js");
           })(
           {
           "./src/app.js": function (module, exports, __webpack_require__) {
               var title = __webpack_require__("./src/title.js");
               console.log(title);
           },
           "./src/title.js": function (module) {
               module.exports = "title";
           },
           });

        `,
    };
    files.push(file);
    //在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
    let outputPath = path.join(
      this.options.output.path,
      this.options.output.filename
    );
    fs.writeFileSync(outputPath, file.source, "utf8");
    //在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,
    //并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果
    this.hooks.done.call();
  }
}

// 初始化参数:从配置文件和shell语句中读取于合并参数,得出最终的参数。
const options = require('./webpack.config')
// 开始编译:用上一步得到的参数初始化compiler对象
const compiler = new Compiler(options)
// 加载所有配置的插件,执行对象的run方法开始执行编译
if (options.plugins && Array.isArray(options.plugins)) {
  for (const plugin of options.plugins) {
    plugin.apply(compiler)
  }
}
// 确定入口:根据配置中的entry找出所有的入口文件
compiler.run()

function babelLoader() {
  return 'babel loader'
}

当我们在wepbpack-flow.js文件中右键单击选择run code运行webpack-flow.js文件后,打开dist目录里的的bundle.js文件就会看到原先空白的bundle.js文件中写入了我们执行后的结果,也就是上述文件中source里面的的内容。

module、chunk、bundle?

module:模块,在 Webpack ⾥⼀切皆模块,⼀个模块对应着⼀个⽂件。Webpack 会从配置的 Entry 开始递归找 出所有依赖的模块。当 webpack 处理到不认识的模块时,需要在 webpack 中的 module 处进⾏配置,当检测到是什么格式的模块,使⽤什么 loader 来处理。

chunk:当使用 webpack 将我们编写的源代码进行打包时,webpack 会根据文件引用关系生成 chunk 文件,webpack 会对这些 chunk 文件进行一些操作

Bundle:webpack 处理完 chunk 文件之后,最终会输出 bundle 文件,这个 bundle 文件包含了经过加载和编译的最终产物。

module、chunk 和 bundle 其实就是同一份代码在不同转换场景取的三个名称。我们编写的是 module,webpack 处理时是 chunk,最终生成供浏览器允许的是 bundle。

参考:

webpack术语之module、chunk和bundle (opens new window)