详细分析Webpack打包流程及原理

1.准备工作

在流程分析过程中我们会简单实现 webpack 的一些功能,部分功能的实现会借助第三方工具:

  • tapable提供Hooks机制来接入插件进行工作;
  • babel相关依赖可用于将源代码解析为AST,进行模块依赖收集和代码改写。
// 创建仓库
mkdir webpack-demo && cd webpack-demo && npm init -y

// 安装 babel 相关依赖
npm install @babel/parser @babel/traverse @babel/types @babel/generator -D

// 安装 tapable(注册/触发事件流)和 fs-extra 文件操作依赖
npm install tapable fs-extra -D

接下来我们在src目录下新建两个入口文件entry1.jsentry2.js和一个公共模块文件module.js
在这里插入图片描述
并分别为文件添加一些内容:

// src/entry1.js
const module = require('./module');
const start = () => 'start';
start();
console.log('entry1 module: ', module);

// src/entry2.js
const module = require('./module');
const end = () => 'end';
end();
console.log('entry2 module: ', module);

// src/module.js
const name = 'cegz';
module.exports = {
  name,
};

有了打包入口,我们再来创建一个webpack.config.js配置文件做一些基础配置:

// ./webpack.config.js
const path = require('path');
const CustomWebpackPlugin = require('./plugins/custom-webpack-plugin.js');

module.exports = {
    entry: {
        entry1: path.resolve(__dirname, './src/entry1.js'),
        entry2: path.resolve(__dirname, './src/entry2.js'),
    },
    context: process.cwd(),
    output: {
        path: path.resolve(__dirname, './build'),
        filename: '[name].js',
    },
    plugins: [new CustomWebpackPlugin()],
    resolve: {
        extensions: ['.js', '.ts'],
    },
    module: {
        rules: [
            {
                test: /\.js/,
                use: [
                    path.resolve(__dirname, './loaders/transformArrowFnLoader.js'), // 转换箭头函数
                ],
            },
        ],
    },
};

以上配置,指定了两个入口文件,以及一个output.build输出目录,同时还指定了一个 plugin 和一个 loader

接下来我们编写webpack的核心入口文件,来实现打包逻辑。这里我们创建webpack核心实现所需的文件:
在这里插入图片描述

  • webpack.js webpack 入口文件
  • compiler.js webpack 核心编译器
  • compilation.js webpack 核心编译对象
  • utils.js 工具函数

这里我们创建了两个比较相似的文件:compiler 和 compilation,在这里做下简要说明:

  • compiler:webpack 的编译器,它提供的run方法可用于创建compilation编译对象来处理代码构建工作;
  • compilation:由compiler.run创建生成,打包编译的工作都由它来完成,并将打包产物移交给 compiler做输出写入操作。

对于入口文件lib/webpack.js,你会看到大致如下结构:

// lib/webpack.js
function webpack(options) {
  ...
}
module.exports = webpack;

对于执行入口文件的测试用例,代码如下:

// 测试用例 webpack-demo/build.js
const webpack = require('./lib/webpack');
const config = require('./webpack.config');

const compiler = webpack(config);

// 调用run方法进行打包
compiler.run((err, stats) => {
  if (err) {
    console.log(err, 'err');
  }
  // console.log('构建完成!', stats.toJSON());
});

接下来,我们从lib/webpack.js入口文件,按照以下步骤开始分析打包流程。
初始化阶段-webpack

  • 合并配置项
  • 创建 compiler
  • 注册插件

编译阶段 - build

  • 读取入口文件
  • 从入口文件开始进行编译
  • 调用 loader 对源代码进行转换
  • 借助 babel 解析为 AST 收集依赖模块
  • 递归对依赖模块进行编译操作

生成阶段 - seal

  • 创建 chunk 对象
  • 生成 assets 对象

写入阶段 - emit

2.初始化阶段

初始化阶段的逻辑集中在调用webpack(config)时候,下面我们来看看webpack()函数体内做了哪些事项。

2.1 读取与合并配置信息

通常,在我们的工程的根目录下,会有一个webpack.config.js作为webpack的配置来源;除此之外,还有一种是通过webpak bin cli命令进行打包时,命令行上携带的参数也会作为webpack的配置。

在配置文件中包含了我们要让webpack打包处理的入口模块、输出位置、以及各种loaderplugin 等;在命令行上也同样可以指定相关的配置,且权重高于配置文件。(下面将模拟webpack cli参数合并处理)

所以,我们在webpack入口文件这里将先做一件事情:合并配置文件与命令行的配置。

// lib/webpack.js
function webpack(options) {
  // 1、合并配置项
  const mergeOptions = _mergeOptions(options);
  ...
}

function _mergeOptions(options) {
  const shellOptions = process.argv.slice(2).reduce((option, argv) => {
    // argv -> --mode=production
    const [key, value] = argv.split('=');
    if (key && value) {
      const parseKey = key.slice(2);
      option[parseKey] = value;
    }
    return option;
  }, {});
  return { ...options, ...shellOptions };
}

module.exports = webpack;

2.2 创建编译器(compiler)对象

好的程序结构离不开一个实例对象,webpack同样也不甘示弱,其编译运转是由一个叫做compiler的实例对象来驱动运转。

compiler实例对象上会记录我们传入的配置参数,以及一些串联插件进行工作的hooks API

同时,还提供了run方法启动打包构建,emitAssets对打包产物进行输出磁盘写入。这部分内容后面介绍。

// lib/webpack.js
const Compiler = require('./compiler');

function webpack(options) {
  // 1、合并配置项
  const mergeOptions = _mergeOptions(options);
  // 2、创建 compiler
  const compiler = new Compiler(mergeOptions);
  ...
  return compiler;
}

module.exports = webpack;

Compiler构造函数基础结构如下:

// lib/compiler.js
const fs = require('fs');
const path = require('path');
const { SyncHook } = require('tapable'); // 串联 compiler 打包流程的订阅与通知钩子
const Compilation = require('./compilation'); // 编译构造函数

class Compiler {
  constructor(options) {
    this.options = options;
    this.context = this.options.context || process.cwd().replace(/\\/g, '/');
    this.hooks = {
      // 开始编译时的钩子
      run: new SyncHook(),
      // 模块解析完成,在向磁盘写入输出文件时执行
      emit: new SyncHook(),
      // 在输出文件写入完成后执行
      done: new SyncHook(),
    };
  }

  run(callback) {
    ...
  }

  emitAssets(compilation, callback) {
    ...
  }
}

module.exports = Compiler;

当需要进行编译时,调用compiler.run方法即可:

compiler.run((err, stats) => { ... });

2.3 插件注册

有 compiler 实例对象后,就可以注册配置文件中的一个个插件,在合适的时机来干预打包构建。插件需要接收compiler对象作为参数,以此来对打包过程及产物产生side effect

插件的格式可以是函数或对象,如果为对象,需要自定义提供一个apply方法。关于插件的知识可参考我写的文章。常见的插件结构如下:

class WebpackPlugin {
  apply(compiler) {
    ...
  }
}

注册插件逻辑如下:

// lib/webpack.js
function webpack(options) {
  // 1、合并配置项
  const mergeOptions = _mergeOptions(options);
  // 2、创建 compiler
  const compiler = new Compiler(mergeOptions);
  // 3、注册插件,让插件去影响打包结果
  if (Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      if (typeof plugin === "function") {
        plugin.call(compiler, compiler); // 当插件为函数时
      } else {
        plugin.apply(compiler); // 如果插件是一个对象,需要提供 apply 方法。
      }
    }
  }
  return compiler;
}

到这里,webpack的初始工作已经完成,接下来是调用compiler.run()进入编译构建阶段。

3.编译阶段

编译工作的起点是在compiler.run,它会:

  • 发起构建通知,触发 hooks.run 通知相关插件;
  • 创建 compilation 编译对象;
  • 读取 entry 入口文件;
  • 编译 entry 入口文件;

3.1 创建compilation编译对象

模块的打包(build)和 代码生成(seal)都是由 compilation 来实现。

// lib/compiler.js
class Compiler {
  ...
  run(callback) {
    // 触发 run hook
    this.hooks.run.call();
    // 创建 compilation 编译对象
    const compilation = new Compilation(this);
    ...
  }
}

compilation实例上记录了构建过程中的entriesmodulechunksassets 等编译信息,同时提供 buildseal方法进行代码构建和代码生成。

// lib/compilation.js
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
const t = require('@babel/types');
const { tryExtensions, getSourceCode, toUnixPath } = require('./utils');

class Compilation {
  constructor(compiler) {
    this.compiler = compiler;
    this.context = compiler.context;
    this.options = compiler.options;
    // 记录当前 module code
    this.moduleCode = null;
    // 保存所有依赖模块对象
    this.modules = new Set();
    // 保存所有入口模块对象
    this.entries = new Map();
    // 所有的代码块对象
    this.chunks = new Set();
    // 存放本次产出的文件对象(与 chunks 一一对应)
    this.assets = {};
  }
  build() {}
  seal() {}
}
module.exports = Compilation;

有了compilation对象后,通过执行compilation.build开始模块构建。

// lib/compiler.js
class Compiler {
  ...
  run(callback) {
    // 触发 run hook
    this.hooks.run.call();
    // 创建 compilation 编译对象
    const compilation = new Compilation(this);
    // 编译模块
    compilation.build();
  }
}

3.2 读取entry入口文件

构建模块首先从entry入口模块开始,此时首要工作是根据配置文件拿到入口模块信息。

entry配置的方式多样化,如:可以不传(有默认值)、可以传入string,也可以传入对象指定多个入口。

所以读取入口文件需要考虑并兼容这几种灵活配置方式。

// lib/compilation.js
class Compilation {
  ...
  build() {
    // 1、读取配置入口
    const entry = this.getEntry();
    ...
  }

  getEntry() {
    let entry = Object.create(null);
    const { entry: optionsEntry } = this.options;
    if (!optionsEntry) {
      entry['main'] = 'src/index.js'; // 默认找寻 src 目录进行打包
    } else if (typeof optionsEntry === 'string') {
      entry['main'] = optionsEntry;
    } else {
      entry = optionsEntry; // 视为对象,比如多入口配置
    }
    // 相对于项目启动根目录计算出相对路径
    Object.keys(entry).forEach((key) => {
      entry[key]="./"+path.posix.relative(toUnixPath(this.context), toUnixPath(entry[key]))
    });
    return entry;
  }
}
module.exports = Compilation;

3.3 编译entry入口文件

拿到入口文件后,依次对每个入口进行构建。

// lib/compilation.js
class Compilation {
  ...
  build() {
    // 1、读取配置入口
    const entry = this.getEntry();
    // 2、构建入口模块
    Object.keys(entry).forEach((entryName) => {
      const entryPath = entry[entryName];
      const entryData = this.buildModule(entryName, entryPath);
      this.entries.set(entryName, entryData);
    });
  }
}
module.exports = Compilation;

构建阶段执行如下操作:

  • 通过 fs 模块读取 entry 入口文件内容;
  • 调用 loader 来转换(更改)文件内容;
  • 为模块创建 module 对象,通过 AST 解析源代码收集依赖模块,并改写依赖模块的路径;
  • 如果存在依赖模块,递归进行上述三步操作;

读取文件内容:

// lib/compilation.js
class Compilation {
  ...
  buildModule(moduleName, modulePath) {
    // 1. 读取文件原始代码
    const originSourceCode = fs.readFileSync(modulePath, 'utf-8');
    this.moduleCode = originSourceCode;
    ...
  }
}
module.exports = Compilation;

调用 loader 转换源代码:

// lib/compilation.js
class Compilation {
  ...
  buildModule(moduleName, modulePath) {
    // 1. 读取文件原始代码
    const originSourceCode = fs.readFileSync(modulePath, 'utf-8');
    this.moduleCode = originSourceCode;
    // 2. 调用 loader 进行处理
    this.runLoaders(modulePath);
    ...
  }
}
module.exports = Compilation;

loader本身是一个JS函数,接收模块文件的源代码作为参数,经过加工改造后返回新的代码。

// lib/compilation.js
class Compilation {
  ...
  runLoaders(modulePath) {
    const matchLoaders = [];
    // 1、找到与模块相匹配的 loader
    const rules = this.options.module.rules;
    rules.forEach((loader) => {
      const testRule = loader.test;
      if (testRule.test(modulePath)) {
        // 如:{ test:/\.js$/g, use:['babel-loader'] }, { test:/\.js$/, loader:'babel-loader' }
        loader.loader ? matchLoaders.push(loader.loader) : matchLoaders.push(...loader.use);
      }
    });
    // 2. 倒序执行 loader
    for (let i = matchLoaders.length - 1; i >= 0; i--) {
      const loaderFn = require(matchLoaders[i]);
      // 调用 loader 处理源代码
      this.moduleCode = loaderFn(this.moduleCode);
    }
  }
}
module.exports = Compilation;

执行 webpack 模块编译逻辑:

// lib/compilation.js
class Compilation {
  ...
  buildModule(moduleName, modulePath) {
    // 1. 读取文件原始代码
    const originSourceCode = fs.readFileSync(modulePath, 'utf-8');
    this.moduleCode = originSourceCode;
    // 2. 调用 loader 进行处理
    this.runLoaders(modulePath);
    // 3. 调用 webpack 进行模块编译 为模块创建 module 对象
    const module = this.handleWebpackCompiler(moduleName, modulePath);
    return module; // 返回模块
  }
}
module.exports = Compilation;
  • 创建 module 对象;
  • 对 module code 解析为 AST 语法树;
  • 遍历 AST 去识别 require 模块语法,将模块收集在module.dependencies之中,并改写 require 语法为 __webpack_require__
  • 将修改后的 AST 转换为源代码;
  • 若存在依赖模块,深度递归构建依赖模块。
// lib/compilation.js
class Compilation {
  ...
  handleWebpackCompiler(moduleName, modulePath) {
    // 1、创建 module
    const moduleId=modulePath
    const module = {
      id: moduleId, // 将当前模块相对于项目启动根目录计算出相对路径 作为模块ID
      dependencies: new Set(), // 存储该模块所依赖的子模块
      entryPoint: [moduleName], // 该模块所属的入口文件
    };

    // 2、对模块内容解析为 AST,收集依赖模块,并改写模块导入语法为 __webpack_require__
    const ast = parser.parse(this.moduleCode, {
      sourceType: 'module',
    });

    // 遍历 ast,识别 require 语法
    traverse(ast, {
      CallExpression: (nodePath) => {
        const node = nodePath.node;
        if (node.callee.name === 'require') {
          const requirePath = node.arguments[0].value;
          // 寻找模块绝对路径
          const moduleDirName = path.posix.dirname(modulePath);
          const absolutePath = tryExtensions(
            path.posix.join(moduleDirName, requirePath),
            this.options.resolve.extensions,
            requirePath,
            moduleDirName
          );
          // 创建 moduleId
          const moduleId='./'+absolutePath
          // 将 require 变成 __webpack_require__ 语句
          node.callee = t.identifier('__webpack_require__');
          // 修改模块路径(参考 this.context 的相对路径)
          node.arguments = [t.stringLiteral(moduleId)];

          if (!Array.from(this.modules).find(module => module.id === moduleId)) {
            // 在模块的依赖集合中记录子依赖
            module.dependencies.add(moduleId);
          } else {
            // 已经存在模块集合中。虽然不添加进入模块编译 但是仍要在这个模块上记录被依赖的入口模块
            this.modules.forEach((module) => {
              if (module.id === moduleId) {
                module.entryPoint.push(moduleName);
              }
            });
          }
        }
      },
    });

    // 3、将 ast 生成新代码
    const { code } = generator(ast);
    module._source = code;

    // 4、深度递归构建依赖模块
    module.dependencies.forEach((dependency) => {
      const depModule = this.buildModule(moduleName, dependency);
      // 将编译后的任何依赖模块对象加入到 modules 对象中去
      this.modules.add(depModule);
    });

    return module;
  }
}
module.exports = Compilation;

通常我们require一个模块文件时习惯不去指定文件后缀,默认会查找.js文件。

这跟我们在配置文件中指定的resolve.extensions配置有关,在tryExtensions方法中会尝试为每个未填写后缀的Path应用resolve.extensions

// lib/utils.js
const fs = require('fs');

function tryExtensions(
  modulePath,  extensions,  originModulePath,  moduleContext
) {
  // 优先尝试不需要扩展名选项(用户如果已经传入了后缀,那就使用用户填入的,无需再应用 extensions)
  extensions.unshift('');
  for (let extension of extensions) {
    if (fs.existsSync(modulePath + extension)) {
      return modulePath + extension;
    }
  }
  // 未匹配对应文件
  throw new Error(
    `No module, Error: Can't resolve ${originModulePath} in  ${moduleContext}`
  );
}

module.exports = {
  tryExtensions,
  ...
}

至此,「编译阶段」到此结束,接下来是「生成阶段」 seal。

3.4 生成阶段

在「编译阶段」会将一个个文件构建成module存储在this.modules之中。

在「生成阶段」,会根据entry创建对应chunk并从this.modules中查找被entry所依赖的module集合。

最后,结合runtime webpack模块机制运行代码,经过拼接生成最终的assets产物。

// lib/compiler.js
class Compiler {
  ...
  run(callback) {
    // 触发 run hook
    this.hooks.run.call();
    // 创建 compilation 编译对象
    const compilation = new Compilation(this);
    // 编译模块
    compilation.build();
    // 生成产物
    compilation.seal();
    ...
  }
}

entry + module --> chunk --> assets 过程如下:

// lib/compilation.js
class Compilation {
  ...
  seal() {
    // 1、根据 entry 创建 chunk
    this.entries.forEach((entryData, entryName) => {
      // 根据当前入口文件和模块的相互依赖关系,组装成为一个个包含当前入口所有依赖模块的 chunk
      this.createChunk(entryName, entryData);
    });
    // 2、根据 chunk 创建 assets
    this.createAssets();
  }

  // 根据入口文件和依赖模块组装chunks
  createChunk(entryName, entryData) {
    const chunk = {
      // 每一个入口文件作为一个 chunk
      name: entryName,
      // entry build 后的数据信息
      entryModule: entryData,
      // entry 的所依赖模块
      modules: Array.from(this.modules).filter((i) =>
        i.entryPoint.includes(entryName)
      ),
    };
    // add chunk
    this.chunks.add(chunk);
  }

  createAssets() {
    const output = this.options.output;
    // 根据 chunks 生成 assets
    this.chunks.forEach((chunk) => {
      const parseFileName = output.filename.replace('[name]', chunk.name);
      // 为每一个 chunk 文件代码拼接 runtime 运行时语法
      this.assets[parseFileName] = getSourceCode(chunk);
    });
  }
}
module.exports = Compilation;

getSourceCode是将entrymodules组合而成的chunk,接入到runtime代码模板之中。

// lib/utils.js
function getSourceCode(chunk) {
  const { entryModule, modules } = chunk;
  return `  (() => {    var __webpack_modules__ = {      ${modules        .map((module) => {          return `          '${module.id}': (module) => {            ${module._source}
      }        `;        })        .join(',')}
    };    var __webpack_module_cache__ = {};    function __webpack_require__(moduleId) {      var cachedModule = __webpack_module_cache__[moduleId];      if (cachedModule !== undefined) {        return cachedModule.exports;      }      var module = (__webpack_module_cache__[moduleId] = {        exports: {},      });      __webpack_modules__[moduleId](module, module.exports, __webpack_require__);      return module.exports;    }    (() => {      ${entryModule._source}
    })();  })();  `;
}

到这里,「生成阶段」处理完成,这也意味着compilation编译工作的完成,接下来我们回到compiler 进行最后的「产物输出」

5.写入阶段

「写入阶段」比较容易理解,assets上已经拥有了最终打包后的代码内容,最后要做的就是将代码内容写入到本地磁盘之中。

// lib/compiler.js
class Compiler {
  ...
  run(callback) {
    // 触发 run hook
    this.hooks.run.call();
    // 创建 compilation 编译对象
    const compilation = new Compilation(this);
    // 编译模块
    compilation.build();
    // 生成产物
    compilation.seal();
    // 输出产物
    this.emitAssets(compilation, callback);
  }

  emitAssets(compilation, callback) {
    const { entries, modules, chunks, assets } = compilation;
    const output = this.options.output;

    // 调用 Plugin emit 钩子
    this.hooks.emit.call();

    // 若 output.path 不存在,进行创建
    if (!fs.existsSync(output.path)) {
      fs.mkdirSync(output.path);
    }

    // 将 assets 中的内容写入文件系统中
    Object.keys(assets).forEach((fileName) => {
      const filePath = path.join(output.path, fileName);
      fs.writeFileSync(filePath, assets[fileName]);
    });

    // 结束之后触发钩子
    this.hooks.done.call();

    callback(null, {
      toJSON: () => {
        return {
          entries,
          modules,
          chunks,
          assets,
        };
      },
    });
  }
}

至此,webpack 的打包流程就以完成。接下来我们完善配置文件中未实现的 loader 和 plugin,然后调用测试用例,测试一下上面的实现。

6.编写loader

在 webpack.config.js 中我们为 .js 文件类型配置了一个自定义 loader 来转换文件内容:

// webpack.config.js
module: {
  rules: [
    {
      test: /\.js/,
      use: [
        path.resolve(__dirname, './loaders/transformArrowFnLoader.js'),
      ],
    },
  ],
},

loader 本身是一个函数,接收文件模块内容作为参数,经过改造处理返回新的文件内容。

下面我们在loaders/transformArrowFnLoader.js中,对文件中使用到的箭头函数,转换为普通函数,来理解webpack loader的作用。

// loaders/transformArrowFnLoader.js
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
const t = require('@babel/types');

function transformArrowLoader(sourceCode) {
  const ast = parser.parse(sourceCode, {
    sourceType: 'module'
  });
  traverse(ast, {
    ArrowFunctionExpression(path, state) {
      const node = path.node;
      const body = path.get('body');
      const bodyNode = body.node;
      if (bodyNode.type !== 'BlockStatement') {
        const statements = [];
        statements.push(t.returnStatement(bodyNode));
        node.body = t.blockStatement(statements);
      }
      node.type = "FunctionExpression";
    }
  });
  const { code } = generator(ast);

  return code;
}
module.exports = transformArrowLoader;

执行node build.js
在这里插入图片描述
会发现箭头函数已经打包成了普通函数。

7.编写plugin

从上面介绍我们了解到,每个插件都需要提供一个apply方法,此方法接收compiler作为参数。

通过compiler可以去订阅webpack 工作期间不同阶段的hooks,以此来影响打包结果或者做一些定制操作。

下面我们编写自定义插件,绑定两个不同时机的 compiler.hooks 来扩展 webpack 打包功能:

  • hooks.emit.tap 绑定一个函数,在 webpack 编译资源完成,输出写入磁盘前执行(可以做清除 output.path目录操作);
  • hooks.done.tap 绑定一个函数,在webpack写入磁盘完成之后执行(可以做一些静态资源copy操作)。
// plugins/custom-webpack-plugin
const fs = require('fs-extra');
const path = require('path');

class CustomWebpackPlugin {
  apply(compiler) {
    const outputPath = compiler.options.output.path;
    const hooks = compiler.hooks;

    // 清除 build 目录
    hooks.emit.tap('custom-webpack-plugin', (compilation) => {
      fs.removeSync(outputPath);
    });

    // copy 静态资源
    const otherFilesPath = path.resolve(__dirname, '../src/otherfiles');
    hooks.done.tap('custom-webpack-plugin', (compilation) => {
      fs.copySync(otherFilesPath, path.resolve(outputPath, 'otherfiles'));
    });
  }
}

module.exports = CustomWebpackPlugin;

修改dist目录下的文件名:
在这里插入图片描述

执行node build.js,最终会在 webpack-demo 下生成 build 目录以及入口打包资源。

8.总结

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

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

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

简单说

  • 初始化:启动构建,读取与合并配置参数,加载Plugin,实例化Compiler
  • 编译:从Entry出发,针对每个Module串行调用对应的Loader去翻译文件的内容,再找到该Module 依赖的Module,递归地进行编译处理
  • 输出:将编译后的Module组合成Chunk,将Chunk转换成文件,输出到文件系统中
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值