webpack4从入门到精通保姆级教程【打包原理】

383 阅读4分钟

开始:从 webpack 命令行说起

通过 npm scripts 运行 webpack

  • 开发环境: npm run dev
  • 生产环境:npm run build

通过 webpack 直接运行

  • webpack entry.js bundle.js

image.png

查找 webpack 入口文件

在命令行运行以上命令后,npm 会让命令行工具进入 node_modules.bin 目录 查找是否存在 webpack.sh 或者 webpack.cmd 文件,如果存在,就执行,不 存在,就抛出错误。

实际的入口文件是:node_modules\webpack\bin\webpack.js

分析 webpack 的入口文件:webpack.js

image.png

启动后的结果

webpack 最终找到 webpack-cli (webpack-command) 这个 npm 包,并且 执行 CLI

webpack-cli 做的事情

引入 yargs,对命令行进行定制

分析命令行参数,对各个参数进行转换,组成编译配置项

引用 webpack,根据配置项进行编译和构建

从 NON_COMPILATION_CMD 分析出不需要编译的命令

webpack-cli 处理不需要经过编译的命令

const { NON_COMPILATION_ARGS } = require("./utils/constants");
const NON_COMPILATION_CMD = process.argv.find(arg => {
  if (arg === "serve") {
    global.process.argv = global.process.argv.filter(a => a !== "serve");
    process.argv = global.process.argv;
  }
  return NON_COMPILATION_ARGS.find(a => a === arg);
});
if (NON_COMPILATION_CMD) {
  return require("./utils/prompt-command")(
    NON_COMPILATION_CMD,
    ...process.argv
  );
}

NON_COMPILATION_ARGS 的内容

webpack-cli 提供的不需要编译的命令

const NON_COMPILATION_ARGS = [
  "init", //创建一份 webpack 配置文件
  "migrate", //进行 webpack 版本迁移
  "add", //往 webpack 配置文件中增加属 性
  "remove", //往 webpack 配置文件中删除属 性
  "serve", //运行 webpack-serve
  "generate-loader", //生成 webpack loader 代码
  "generate-plugin", //生成 webpack plugin 代码
  "info" //返回与本地环境相关的一些信息
];

命令行工具包 yargs 介绍

提供命令和分组参数

动态生成 help 帮助信息

image.png

webpack-cli 使用 args 分析

参数分组 (config/config-args.js),将命令划分为 9 类:

  • Config options: 配置相关参数(文件名称、运行环境等)
  • Basic options: 基础参数(entry 设置、debug 模式设置、watch 监听设置、devtool 设置)
  • Module options: 模块参数,给 loader 设置扩展
  • Output options: 输出参数(输出路径、输出文件名称)
  • Advanced options: 高级用法(记录设置、缓存设置、监听频率、bail 等)
  • Resolving options: 解析参数(alias 和 解析的文件后缀设置)
  • Optimizing options: 优化参数 ·Stats options: 统计参数
  • options: 通用参数(帮助命令、版本信息等)

webpack-cli 执行的结果

webpack-cli 对配置文件和命令行参数进行转换最终生成配置选项参数 options 最终会根据配置参数实例化 webpack 对象,然后执行构建流程

Webpack 的本质

Webpack 可以将其理解是一种基于事件流的编程范例,一系列的插件运行。

先看一段代码

核心对象 Compiler 继承 Tapable

class Compiler extends Tapable { // ... }

核心对象 Compilation 继承 Tapable

class Compilation extends Tapable { // ... }

Tapable 是什么?

Tapable 是一个类似于 Node.js 的 EventEmitter 的库, 主要是控制钩子函数的发布 与订阅,控制着 webpack 的插件系统。

Tapable 库暴露了很多 Hook(钩子)类,为插件提供挂载的钩子

const {
  SyncHook, //同步钩子
  SyncBailHook, //同步熔断钩子
  SyncWaterfallHook, //同步流水钩子
  SyncLoopHook, //同步循环钩子
  AsyncParallelHook, //异步并发钩子
  AsyncParallelBailHook, //异步并发熔断钩子
  AsyncSeriesHook, //异步串行钩子
  AsyncSeriesBailHook, //异步串行熔断钩子
  AsyncSeriesWaterfallHook //异步串行流水钩子
} = require("tapable");

Tapable hooks 类型

image.png

Tapable 的使用-hook 基本用法示例

const hook1 = new SyncHook(["arg1", "arg2", "arg3"]); //绑定事件到webapck事件流
hook1.tap("hook1", (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)); //1,2,3 //执行绑定的事件 hook1.call(1,2,3)

Tapable 是如何和 webpack 联系起来的?

if (Array.isArray(options)) {
  compiler = new MultiCompiler(options.map(options => webpack(options)));
} else if (typeof options === "object") {
  options = new WebpackOptionsDefaulter().process(options);
  compiler = new Compiler(options.context);
  compiler.options = options;
  new NodeEnvironmentPlugin().apply(compiler);
  if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      if (typeof plugin === "function") {
        plugin.call(compiler, compiler);
      } else {
        plugin.apply(compiler);
      }
    }
  }
  compiler.hooks.environment.call();
  compiler.hooks.afterEnvironment.call();
  compiler.options = new WebpackOptionsApply().process(options, compiler);
}

模拟 Compiler.js

module.exports = class Compiler {
  constructor() {
    this.hooks = {
      accelerate: new SyncHook(["newspeed"]),
      brake: new SyncHook(),
      calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"])
    };
  }
  run() {
    this.accelerate(10);
    this.break();
    this.calculateRoutes("Async", "hook", "demo");
  }
  accelerate(speed) {
    this.hooks.accelerate.call(speed);
  }
  break() {
    this.hooks.brake.call();
  }
  calculateRoutes() {
    this.hooks.calculateRoutes.promise(...arguments).then(
      () => {},
      err => {
        console.error(err);
      }
    );
  }
};

插件 my-plugin.js

const Compiler = require("./Compiler");
class MyPlugin {
  constructor() {}
  apply(compiler) {
    compiler.hooks.brake.tap("WarningLampPlugin", () =>
      console.log("WarningLampPlugin")
    );
    compiler.hooks.accelerate.tap("LoggerPlugin", newSpeed =>
      console.log(`Accelerating to ${newSpeed}`)
    );
    compiler.hooks.calculateRoutes.tapPromise(
      "calculateRoutes tapAsync",
      (source, target, routesList) => {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            console.log(`tapPromise to ${source} ${target} ${routesList}`);
            resolve();
          }, 1000);
        });
      }
    );
  }
}

模拟插件执行

const myPlugin = new MyPlugin();
const options = { plugins: [myPlugin] };
const compiler = new Compiler();
for (const plugin of options.plugins) {
  if (typeof plugin === "function") {
    plugin.call(compiler, compiler);
  } else {
    plugin.apply(compiler);
  }
}
compiler.run();

Webpack 流程篇

webpack的编译都按照下面的钩子调用顺序执行

image.png

WebpackOptionsApply

将所有的配置 options 参数转换成 webpack 内部插件 使用默认插件列表

举例:

  • output.library -> LibraryTemplatePlugin
  • externals -> ExternalsPlugin
  • devtool -> EvalDevtoolModulePlugin, SourceMapDevToolPlugin
  • AMDPlugin, CommonJsPlugin
  • RemoveEmptyChunksPlugin

Compiler hooks

流程相关:

  • (before-)run
  • (before-/after-)compile
  • make
  • (after-)emit
  • done 监听相关:
  • watch-run
  • watch-close

Compilation

Compiler 调用 Compilation 生命周期方法

  • addEntry -> addModuleChain
  • finish (上报模块错误)
  • seal

ModuleFactory

image.png

Module

image.png

NormalModule

Build

  • 使用 loader-runner 运行 loaders
  • 通过 Parser 解析 (内部是 acron)
  • ParserPlugins 添加依赖

Compilation hooks

image.png