你还不会手写 Webpack 吗?

375 阅读1分钟

image.png

背景

近来身边人写作风气极其浓厚,感觉不写点东西就好像我一直在“摸鱼”,代码这东西能少写就不要多写,毕竟 “CV” 能解决日常开发中至少 90% 的问题,实在是不知道写点啥,肚子就这么点东西,说得好听点就是才疏学浅,其实就是菜;简单看过一点 webpack 的官方文档,总结一下希望能给你们带来一些小小的帮助,谢谢大家!

前提

  1. 随手打开过 webpack 的官网
  2. 随手打开过 babel 的官网
  3. 简单了解过 tapable 的功能
  4. 简单了解过 node 是干嘛的
  5. 简单写过点 js
  6. 执行过 npm run build (这一步很关键,所以我直接加粗 + 标红)
  7. 有手就行

版本

5.73.0

示例

链接

stackblitz.com/edit/node-a…

详情

  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

image.png

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

image.png

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 的回调开始编译,由于编译涉及大量的引用和回调,此处仅沿着执行线进行简单梳理,流程如下:

image.png

以上就是处理单个模块的核心流程,拿到本次处理结果后会执行 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 输出资源到文件夹

image.png

源码小结

总结

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

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

image.png

手写思路

  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;