为了账号安全,请及时绑定邮箱和手机立即绑定

一文搞懂 webpack 核心原理,手写 webpack 竟如此简单?

image.png

背景

webpack 作为前端开发中的重中之重,却鲜有人对其核心原理进行深入研究,本文完全是跟随其核心方法及流程由浅入深分析讲解,旨在能为你在 webpack 核心原理的理解上带来些许灵感,更上一层楼!

前提

  1. 随手打开过 webpack 的官网
  2. 随手打开过 babel 的官网
  3. 简单了解过 tapable 的功能
  4. 简单了解过 node 是干嘛的
  5. 简单写过点 js
  6. 执行过 npm run build (这一步很关键!!!)
  7. 有手就行

版本

5.73.0

示例

手写 webpack核心原理完整示例代码链接

详情

  1. 目录结构

.
├── main
│   └── index.js
├── package-lock.json
├── package.json
├── src
│   ├── div.js
│   ├── mul.js
│   ├── sub.js
│   └── sum.js
├── webpack.config.js
  1. 文件内容

sum.js

function sum(num1, num2) { 
  return num1 + num2
}
module.exports = { sum }

sub.js

function sub(num1, num2) { 
  return num1 - num2
}
module.exports = { sub }

mul.js

function mul(num1, num2) { 
  return num1 * num2
}
module.exports = { mul }

div.js

function div(num1, num2) { 
  return num1 / num2
}
module.exports = { div }

index.js

const { sum } = require("../src/sum.js");
const { sub } = require("../src/sub.js");
const { mul } = require("../src/mul.js");
const { div } = require("../src/div.js");

const sumResult = sum(50, 50);
const subResult = sub(50, 50);
const mulResult = mul(50, 50);
const divResult = div(50, 50);

console.log({
  sumResult,
  subResult,
  mulResult,
  divResult,
});

webpack.config.js

const webpack = require("webpack");
const path = require("path");

module.exports = {
  entry: "./main/index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].bundle.js",
  },
  mode: "development",
  devtool: "source-map",
  module: {
    rules: [
      {
        test: /.m?js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
          },
        },
      },
    ],
  },
  plugins: [
    new webpack.BannerPlugin({
      banner: "webpack! webpack! webpack! webpack! webpack!",
      footer: true,
    }),
  ],
};
  1. 打包产物

(() => {
  var __webpack_modules__ = {
    "./src/div.js": (module) => {
      function div(num1, num2) {
        return num1 / num2;
      }
      module.exports = {
        div: div,
      };
    },
    "./src/mul.js": (module) => {
      function mul(num1, num2) {
        return num1 * num2;
      }
      module.exports = {
        mul: mul,
      };
    },
    "./src/sub.js": (module) => {
      function sub(num1, num2) {
        return num1 - num2;
      }
      module.exports = {
        sub: sub,
      };
    },
    "./src/sum.js": (module) => {
      function sum(num1, num2) {
        return num1 + num2;
      }
      module.exports = {
        sum: sum,
      };
    },
  };

  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;
  }

  (() => {
    var _require = __webpack_require__("./src/sum.js"),
      sum = _require.sum;
    var _require2 = __webpack_require__("./src/sub.js"),
      sub = _require2.sub;
    var _require3 = __webpack_require__("./src/mul.js"),
      mul = _require3.mul;
    var _require4 = __webpack_require__("./src/div.js"),
      div = _require4.div;
    var sumResult = sum(50, 50);
    var subResult = sub(50, 50);
    var mulResult = mul(50, 50);
    var divResult = div(50, 50);
    console.log({
      sumResult: sumResult,
      subResult: subResult,
      mulResult: mulResult,
      divResult: divResult,
    });
  })();
})();



 /*! webpack! webpack! webpack! webpack! webpack! */
  1. 结果分析

  • 打包之后的代码是一个 IIFE ,主要包含以下内容

    • webpack_moduleskey 为模块路径,value 为模块内容作为执行体的函数
    • webpack_module_cache:缓存
    • webpack_require:加载模块的方法
    • IIFE:这里执行 __webpack_require__ 方法并返回 module.exports
  • babel-loader 已将代码处理为 es5 的语法

  • BannerPlugin 已将内容写入打包后的文件

源码浅析

  1. npm run build

  • 执行 ./node_modules/.bin/webpack 文件
  • webpack-cli 负责处理执行 commandconfig 相关的配置并传入 webpack
  • webpack 根据传入的 config 以一个或多个 js 文件为入口,递归检查每个js 模块的依赖,从而构建一个依赖关系图,然后依据该图将整个应用程序打包成一个或多个 bundle
    图片描述
  1. run

  • 初始化 command 配置并注册相关的 callback
  • 解析 command 执行 callback 加载 webpack
  • 执行 webpack 生成 compiler
  • runWebpack 核心代码如下:
async runWebpack(options, isWatchCommand){
  const callback = (error, stats) => { ... }
  compiler = await this.createCompiler(options, callback);
}
  • createCompiler 核心代码如下:
async createCompiler(options, callback) {
  let config = await this.loadConfig(options);
  config = await this.buildConfig(config, options)
  compiler = this.webpack(config.options, callback)
}

图片描述

webpack-cli 相对于 webpack 主要做了以下几件事情:

  • 解析命令行的指令
  • 解析 webpack.config.js
  • 执行 this.webpack(config, cb) 得到 compiler 对象
  1. webpack

  • 该方法接收 optionscallback 两个参数
  • 根据是否传入了 callback 决定是否执行 compiler.run()
  • create 方法用于创建一个包含 compiler 对象的 object,核心代码如下:
const create = () => {
  let compiler;
  const webpackOptions = options;
  compiler = createCompiler(webpackOptions)
  return { compiler, ...}
}
  • webpack 核心代码如下:
const webpack = (options, callback) => {
  const create = () => {
    let compiler;
    const webpackOptions = options;
    compiler = createCompiler(webpackOptions)
  }
  if (callback) {
    const { compiler, ... } = create();
    compiler.run()
  }
}
  1. createCompiler

  • options 进行 normalized 处理
  • 实例化 compiler
  • 注入 compilerpluginapply 方法中
  • 执行相关的 hooks
  • process() 中将所有的 option 转为相应的 plugin 进行处理
  • 返回 compiler
  • createCompiler 核心代码如下:
const createCompiler = rawOptions => {
  const options = getNormalizedWebpackOptions(rawOptions);
  applyWebpackOptionsBaseDefaults(options);
  const compiler = new Compiler(options.context, options);
  if (Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      if (typeof plugin === "function") {
        plugin.call(compiler, compiler);
      } else {
        plugin.apply(compiler);
      }
    }
  }
  applyWebpackOptionsDefaults(options);
  compiler.hooks.environment.call();
  compiler.hooks.afterEnvironment.call();
  new WebpackOptionsApply().process(options, compiler);
  compiler.hooks.initialize.call();
  return compiler;
}
  • process 核心代码如下:
class WebpackOptionsApply extends OptionsApply {
  process(options, compiler) {
    // 1. 处理 options 转为 plugins
    // 2. 处理 entry 并在 EntryOptionPlugin 中注册 entryOption hook
    new EntryOptionPlugin().apply(compiler);
    // 3. 执行上一步注册的 entryOption hook
    compiler.hooks.entryOption.call(options.context, options.entry);
    return options;
  }
}
  • EntryOptionPlugin 核心代码如下:
class EntryOptionPlugin { 
  apply(compiler) {
    compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
      EntryOptionPlugin.applyEntryOption(compiler, context, entry);
      return true;
    });
  }
  
  static applyEntryOption(compiler, context, entry) {
    new EntryPlugin(context, entry, options).apply(compiler);
  }
}
  • EntryPlugin 核心代码如下:
class EntryPlugin {
  apply(compiler) {
    // 1. 注册关键的 make hook callback 以便后面调用
    compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
      compilation.addEntry(context, dep, options, err => {
        callback(err);
      });
    });
  }
}
  1. Compiler

  • 简单来讲,compiler 是一个包含当前运行 webpack 所有配置的对象,被实例化时会利用 tapable 初始化大量 hook 以便在 webpack 整个生命周期去注册,因此,一旦被创建便存在于 webpack 的整个生命周期中。
  • Compiler 核心代码如下:
class Compiler {
  constructor(context, options) {
    this.hooks = Object.freeze({
      // 初始化大量的 hook
    })
  }

  compile(callback) {
    const params = this.newCompilationParams();
    // 2. 创建 compilation 对象 
    const compilation = this.newCompilation(params);
    // 3. 执行 make hook callback
    this.hooks.make.callAsync(compilation, err => {
      this.hooks.finishMake.callAsync(compilation, err => {
        compilation.seal()
      })
    })
  }

  // compiler 被创建后执行 run 方法
  run(callback) {
    const onCompiled = (err, compilation) => {}
    const run = () => {
      // 1. 执行 compile 
      this.compile(onCompiled);
    };
    run();
  }
}
  1. Compilation

  • compilation 代表了一次单一的版本构建和资源的生成,即在编译过程中每当检测到某个文件发生变化,就会执行一次新的编译过程,从而生成编译结果。compilation 表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,也就是说它只存在于编译阶段。
class Compilation {
  constructor() {
    this.hooks = Object.freeze({ ... })
    this.entries = new Map(); // 存放所有的入口信息
    this.modules = new Set(); // 存放解析后所有的模块信息
    this.chunks = new Set();  // 存放 chunks 信息
    this.assets = {};  // 存放生成的资源信息
  }
  seal() {} // 对资源进行封存最终输出 assets
}
  1. make

  • 执行 makecallback,从 entry 开始对 module 进行 addbuildparse
  • factorize 会执行 resolver 去处理 moduleloader 的路径相关信息
  • addModule 用于生成模块间的 ModuleGraph
  • buildModule 会通过 runLoader 执行对应的 loaders
  • 执行 parse 方法解析 module 生成 AST
  • 执行 processModuleDependencies 递归处理模块间的依赖并重复执行上述流程
class EntryPlugin {
  apply(compiler) {
    compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
      compilation.addEntry(context, dep, options, err => {
        callback(err);
      });
    })
  }
}

执行 make 的回调开始编译,由于编译涉及大量的引用和回调,此处仅沿着执行线进行简单梳理,流程如下:

图片描述

以上就是处理单个模块的核心流程,拿到本次处理结果后会执行 processModuleDependencies 根据依赖对模块进行递归处理直到 parse 完所有的模块保存在 compilation

_doBuild(options, compilation, resolver, fs, hooks, callback) {
  const processResult = (err, result) => { 
    const source = result[0];
    const sourceMap = result.length >= 1 ? result[1] : null;
    this._source = this.createSource({...})
    return callback();
  }
  runLoaders({ ... }, (err, result) => {
    processResult(err, result.result)
  })
}

callback(err) {
  const handleParseResult = result => {
    return handleBuildDone();
  };
  const source = this._source.source();
  result = this.parser.parse(this._ast || source, {
    source,
    current: this,
    module: this,
    compilation: compilation,
    options: options
  });
  handleParseResult(result)
}
  1. seal

  • 执行 compilation.seal()
  • 处理 Optimization 相关配置
  • 根据处理好的 modules 生成 chunks
  • 生成 assets 挂载到 compilation.assets
  • 根据 output 配置生成文件夹
  • 执行 emitFiles 输出资源到文件夹

图片描述

源码小结

总结

webpack 真正的执行过程远比此处复杂得多,所以本文仅仅只是提取了执行过程中的核心方所做的事情进行详解,使你更轻易的了解到它所做的事情;如果你有时间且有兴趣,不妨可以自己从头撸上一遍,毕竟阅读源码的过程是无聊且枯燥的,能不看就尽量别看了吧,除非你刻意要做那个很卷的人!

至此,我们大概可以明确了手写 webpack 的基本思路,执行过程如下所示:

图片描述

手写思路

  1. package.json

"scripts": {
   "build": "node ./webpack/index.js",
 }

执行该指令模拟去加载那个可执行文件

  1. index

require("./webpack-cli")
  1. webpack-cli

 // 1、获取文件的配置
const webpackConfig = require("../webpack.config.js");

 // 2、获取命令行配置
const getCommandOption = () => {
  const commandOptionList = process.argv.slice(2);
  const option = {};
  commandOptionList.forEach((item) => {
    const [key, value] = item.split("=");
    if (key[0] !== "-" || key[1] !== "-") {
      console.error("options is invaild");
      return false;
    }
    if (key.slice(2) && value) {
      option[key.slice(2)] = value;
    }
  });
  return option;
};

// webpack-cli 包含大量处理 command 相关的逻辑,这里我就不写了,有兴趣可自行阅读源码
// 3、合并配置
const config = { ...webpackConfig, ...commandConfig };

// 4、执行 webpack
webpack(config, (err, stats) => {
  if (err) {
    console.err(error);
  }
  console.log("stats", stats);
});

// 至此,webpack-cli 已经完成了自己的任务
// 关于为什么安装 webpack 也会提示一并安装 webpack-cli ? 我们可以只安装 webpack 吗 ?
// 当然可以。因为 webpack 依赖 webpack-cli 会处理配置相关的功能。
// 如果我们可以自己解析并生成 config 交给 webpack 处理,那就只安装 webpack 就可以了
// 毕竟 vite 之前的 vue 和 react ,二者也都没有安装过 webpack-cli
  1. webpack

const Compiler = require("./Compiler")

// 简单处理下 entry 兼容 string 和 object 的形式传入, 数组自己可以尝试处理下哈
const getNormalizedEntryStatic = (entry) => {
  if (typeof entry === "string") {
    return {
      main: entry,
    };
  }
  if (Object.prototype.toString.call(entry) === "[object Object]") {
    return { ...entry };
  }
};

const createCompiler = (options) => {
  options.entry = getNormalizedEntryStatic(options.entry);
  const compiler = new Compiler(options);
  
  // 将 compiler 传入 plugins 中
  if (Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      if (typeof plugin === "function") {
        plugin.call(compiler, compiler);
      } else {
        plugin.apply(compiler);
      }
    }
  }
  return compiler;
};

const webpack = (config, callback) => {
  // 1、创建 compiler 对象
  const compiler = createCompiler(config);
  if (callback) {
    // 2、调用 run 方法之前我们初始化好的 run hook
    compiler.hooks.run.call();
    // 3、执行 compiler 的 run 方法
    compiler.run((err, stats) => {
      if (err) {
        console.error(err);
      }
      console.log(stats);
    });
  }
};

module.exports = webpack
  1. Compiler

const { SyncHook } = require("tapable");
const Compilation = require("./Compilation")

class Compiler {
  constructor(options) {
    // 1、简单初始化两个 hook 意思一下
    this.hooks = {
      run: new SyncHook(),
      done: new SyncHook(),
    };
    // 2、保存一个 context,这里跳过用户配置简单处理
    this.context = process.cwd();
    // 3、保存一份 options
    this.options = options;
  }

  run() {
    // 4、创建 Compilation 这里我们把这个 compiler 直接传过去
    const compilation = new Compilation(this);
    // 5、从 entry 开始处理
    compilation.addEntry();
  }
}

module.exports = Compiler
  1. Compilation

const fs = require("fs");
const path = require("path");
const parse = require("./parse")
const runLoaders = require("./runLoaders")
const codeGeneration = require("./codeGeneration")

class Compilation {
  constructor(compiler) {
    this.compiler = compiler;
    this.entries = new Set(); // 入口模块
    this.modules = new Set(); // 依赖模块
    this.chunks = new Set(); // chunks
    this.sourceCode = ""; // 源码
    this.assets = {}; // 即将打包的资源
  }

  // 读取模块 => 用 loader 处理模块 => 解析模块
  build(moduleName, modulePath) {
    const sourceCode = fs.readFileSync(modulePath, "utf-8");
    runLoaders(this, modulePath, sourceCode);
    return parse(this, moduleName, modulePath);
  }

  addEntry() {
    // 1、从入口开始依次处理
    const { context, options } = this.compiler;
    for (const entryName in options.entry) {
      if (Object.hasOwnProperty.call(options.entry, entryName)) {
        const entryPath = options.entry[entryName];
        // 2、build 处理
        const buildCompletedModule = this.build(
          entryName,
          path.resolve(context, entryPath)
        );
        // 3、处理完成添加到 entries
        this.entries.add(buildCompletedModule);
        // 4、对资源进行打包输出
        this.seal(entryName, buildCompletedModule);
      }
    }
  }

  addChunk(entryName, entryModule, callback) {
    const chunk = {
      name: entryName,
      entryModule: entryModule,
      modules: Array.from(this.modules).filter((i) =>
        i.name.includes(entryName)
      ),
    };
    this.chunks.add(chunk);
    callback();
  }

  seal(entryName, entryModule) {
    this.addChunk(entryName, entryModule, () => {
      this.emitAsset();
    });
  }

  emitAsset() {
    const output = this.compiler.options.output;
    this.chunks.forEach((chunk) => {
      const outputFileName = output.filename.replace("[name]", chunk.name);
      this.assets[outputFileName] = codeGeneration(chunk);
    });
    if (!fs.existsSync(output.path)) {
      fs.mkdirSync(output.path);
    }
    Object.keys(this.assets).forEach((fileName) => {
      const filePath = path.join(output.path, fileName);
      fs.writeFileSync(filePath, this.assets[fileName]);
    });
    this.compiler.hooks.done.call();
  }
}

module.exports = Compilation
  1. runLoaders

// 遍历加载所有的 loader 作用在源码模块上
const runLoaders =  (compilation, modulePath, sourceCode) => {
  const usedLoaders = [];
  const rules = compilation.compiler.options.module.rules;
  for (const rule of rules) {
    if (rule.test.test(modulePath)) {
      if (rule.loader) {
        usedLoaders.push(rule.loader);
      }
      if (rule.use) {
        usedLoaders.push(...rule.use);
      }
    }
  }
  // 倒序处理
  for (let i = usedLoaders.length - 1; i >= 0; i--) {
    compilation.sourceCode = require(usedLoaders[i])(sourceCode);
  }
};

module.exports = runLoaders
  1. parse

const path = require("path");
const parser = require("@babel/parser");
const { genAbsPath, genModuleId } = require("./utils");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;
const types = require("@babel/types");

const parse = (compilation, moduleName, modulePath) => {
  // 1. 创建模块
  const module = {
    id: genModuleId(compilation.compiler.context, modulePath),
    name: [moduleName],
    __source: "",
    dependencies: new Set(),
  };
  // 2. 生成 ast
  const ast = parser.parse(compilation.sourceCode, {
    sourceType: "module",
  });
  // 3. 对源码进行转换 这里仅针对 require 引入
  traverse(ast, {
    CallExpression: ({ node }) => {
      if (node.callee.name === "require") {
        const nodeName = node.arguments[0].value;
        const nodeDirName = path.posix.dirname(modulePath);
        const nodeAbsPath = genAbsPath(
          path.posix.join(nodeDirName, nodeName),
          compilation.compiler.options.resolve.extensions,
          nodeName,
          nodeDirName
        );
        const moduleId = genModuleId(compilation.compiler.context, nodeAbsPath);
        node.callee = types.identifier("__webpack_require__");
        node.arguments = [types.stringLiteral(moduleId)];
        // 判断模块是否重复引入处理
        const loadedModules = Array.from(compilation.modules).map((i) => i.id);
        if (!loadedModules.includes(moduleId)) {
          module.dependencies.add(moduleId);
        } else {
          compilation.modules.forEach((i) => {
            if (i.id === moduleId) {
              i.name.push(nodeName);
            }
          });
        }
      }
    },
  });

  // 4. 代码生成
  const { code } = generator(ast);
  // 5. 在 module 对象上挂载一份生成好的源码
  module.__source = code;
  // 6. 处理模块依赖
  module.dependencies.forEach((dep) => {
    const depModule = compilation.build(moduleName, dep);
    compilation.modules.add(depModule);
  });
  return module;
};

module.exports = parse;
  1. utils

const fs = require("fs");
const path = require("path");

const genAbsPath = (modulePath, extensions, nodeName, nodeDirName) => {
  if (fs.existsSync(modulePath)) return modulePath;
  for (const extension of extensions) {
    if (fs.existsSync(modulePath + extension)) {
      return modulePath + extension;
    }
  }
};

const genModuleId = (context, modulePath) => {
  return `./${path.posix.relative(context, modulePath)}`;
};

module.exports = {
  genAbsPath,
  genModuleId,
};
  1. codeGeneration

// 简单模拟下代码生成的过程
const codeGeneration = (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}
    })();
  })();
  `;
};
module.exports = codeGeneration;
点击查看更多内容
2人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消