模拟实现webpack的工作原理

23 阅读4分钟

大致流程

  • 读取配置的入口文件
  • 通过@babel/parser转ast
  • 通过@babel/traverse的traverse方法处理ast,将type为 ImportDeclaration 解析出来路径,生成deps【收集相互依赖的文件】
  • @babel/core的transformFromAst方法处理ast,生成code
  • 递归处理deps
  • 通过套用模板,生成出可以执行的文件,内部用eval执行js代码

webpack编译原理

设置初始化环境

创建如下目录

.
├── package.json
├── src
│   ├── count.js
│   ├── index.js
│   └── utils.js
└── webpack.config.js
{
  "name": "wpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack --config webpack.config.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "webpack": "^5.75.0",
    "webpack-cli": "^5.0.1"
  },
  "devDependencies": {
    "@babel/preset-env": "^7.20.2",
    "babel-loader": "^9.1.2"
  }
}
const path = require("path");
module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
  },
  module: {
    //使用 babel-loader 处理es6语法
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          //  预设babel做怎样的兼容性处理
          presets: ["@babel/preset-env"],
        },
      },
    ],
  },
};
import { a, b } from "./count.js";
import { sum } from "./utils.js";
const res = sum(a, b);
console.log(res);
export const sum = (a, b) => {
  return a + b;
};
export let a = 1;
export let b = 2;
export let c = 99;

生成出来打包结果文件dist/bundle.js;

(() => {
  "use strict";
  var __webpack_modules__ = {
    "./src/count.js": (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"a\": () => (/* binding */ a),\n/* harmony export */   \"b\": () => (/* binding */ b),\n/* harmony export */   \"c\": () => (/* binding */ c)\n/* harmony export */ });\nvar a = 1;\nvar b = 2;\nvar c = 99;\n\n//# sourceURL=webpack://wpack/./src/count.js?");
    },
    "./src/index.js": (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _count__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./count */ \"./src/count.js\");\n/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./utils */ \"./src/utils.js\");\n\n\nconsole.log(_count__WEBPACK_IMPORTED_MODULE_0__.a, _count__WEBPACK_IMPORTED_MODULE_0__.b, \"count\");\nvar res = (0,_utils__WEBPACK_IMPORTED_MODULE_1__.sum)(_count__WEBPACK_IMPORTED_MODULE_0__.a, _count__WEBPACK_IMPORTED_MODULE_0__.b);\nconsole.log(res);\n\n//# sourceURL=webpack://wpack/./src/index.js?");
    },
    "./src/utils.js": (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"sum\": () => (/* binding */ sum)\n/* harmony export */ });\nvar sum = function sum(a, b) {\n  console.log(a, b, \"sum\");\n  return a + b;\n};\n\n//# sourceURL=webpack://wpack/./src/utils.js?");
    }
  };
  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;
  }
  (() => {
    __webpack_require__.d = (exports, definition) => {
      for (var key in definition) {
        if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
          Object.defineProperty(exports, key, {
            enumerable: true,
            get: definition[key]
          });
        }
      }
    };
  })();
  (() => {
    __webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
  })();
  (() => {
    __webpack_require__.r = exports => {
      if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
        Object.defineProperty(exports, Symbol.toStringTag, {
          value: 'Module'
        });
      }
      Object.defineProperty(exports, '__esModule', {
        value: true
      });
    };
  })();
  var __webpack_exports__ = __webpack_require__("./src/index.js");
})();

该文件为可以执行的文件. 接下来开始分析webpack的执行原理。

1从入口文件开始,解析为ast

创建自己的打包文件wpack.js。

// 引入编译文件 Compiler
const Compiler = require('./package/Compiler')
// 引入配置文件 webpack.config.js
const options = require('./webpack.config.js')
const compiler = new Compiler(options)
// 执行 compiler 实例的run方法
compiler.run()

在package目录下创建 Compiler.js文件

class Compiler {
  // 构造函数
  constructor(options) {
    this.options = options;
    this.entry = options.entry;
  }
  run() {
    console.log(this.entry);
  }
}
module.exports = Compiler;

在控制台执行命令node wpack.js, 可以看到输出 ./src/index.js 入口文件的路径地址。 完成了上面第一步,接下来开始使用parser.parse对从入口文件引入的的资源进行解析。

新增Compilation.js文件

// 该文件是每次编译生成的资源文件实例
class Compilation {
  // 构造函数传入 compiler实例
  constructor(compiler) {
    const { options, modules } = compiler;
    this.options = options;
    this.entryId = "";
    // 当前命令执行的根路径
    this.root = process.cwd();

    this.modules = modules;
  }
  // 设置要编译文件的路径,以及该文件是否为入口文件
  buildModule(absolutePath, isEntry){
    console.log(absolutePath, this.root, "absolutePath");
    let ast = Parser.ast(absolutePath);
  }
}
module.exports = Compilation;

在Compiler.js中引入

// 引入Compilation文件
const Compilation = require("./Compilation");

// Compiler是配置文件实例的结果,只有一个
class Compiler {
  // 构造函数
  constructor(options) {
    this.options = options;
    this.entry = options.entry;
    // 设置 modules,用来设置loader,
    this.modules = [];
  }
  run() {
    console.log(this.entry);
    // +++新增内容+++
    this.compile();
  }
  // +++新增内容+++
  compile() {
    const compilation = new Compilation(this);
    compilation.buildModule(this.options.entry, true)
  }
}
module.exports = Compiler;

新增Parser.js文件

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
// Parser解析文件
class Parser {
  // 创建静态方法 ast,将指定的路径文件解析为ast对象
  static ast(path) {
    // 1.读取文件
    const content = fs.readFileSync(path, "utf-8"); 
    console.log("读取文件", content);
    // 2.解析读取出来的文件内容,使用module类型及膝
    const _ast = parser.parse(content, {
      sourceType: "module",
    });
    console.log(_ast);
    console.log("我是ast的body内容", _ast.program.body);
    return _ast;
  }
}
module.exports = Parser;

使用@babel/parser进行解析,安装依赖文件yarn add -D @babel/parser

执行命令node wpack.js

读取文件 import { a, b } from "./count";
import { sum } from "./utils";
const res = sum(a, b);
console.log(res);

Node {
  type: 'File',
  start: 0,
  end: 104,
  loc: SourceLocation {
    start: Position { line: 1, column: 0, index: 0 },
    end: Position { line: 5, column: 0, index: 104 },
    filename: undefined,
    identifierName: undefined
  },
  errors: [],
  program: Node {
    type: 'Program',
    start: 0,
    end: 104,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    sourceType: 'module',
    interpreter: null,
    body: [ [Node], [Node], [Node], [Node] ],
    directives: []
  },
  comments: []
}
我是ast的body内容 [
  Node {
    type: 'ImportDeclaration',
    start: 0,
    end: 31,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    specifiers: [ [Node], [Node] ],
    source: Node {
      type: 'StringLiteral',
      start: 21,
      end: 30,
      loc: [SourceLocation],
      extra: [Object],
      value: './count'
    }
  },
  Node {
    type: 'ImportDeclaration',
    start: 32,
    end: 62,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    specifiers: [ [Node] ],
    source: Node {
      type: 'StringLiteral',
      start: 52,
      end: 61,
      loc: [SourceLocation],
      extra: [Object],
      value: './utils'
    }
  },
  Node {
    type: 'VariableDeclaration',
    start: 63,
    end: 85,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    declarations: [ [Node] ],
    kind: 'const'
  },
  Node {
    type: 'ExpressionStatement',
    start: 86,
    end: 103,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    expression: Node {
      type: 'CallExpression',
      start: 86,
      end: 102,
      loc: [SourceLocation],
      callee: [Node],
      arguments: [Array]
    }
  }
]

2用traverse处理ast对象,查找出所有依赖的文件

给Parser类添加getDependecy方法

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

// Parser解析文件
class Parser {
  // 创建静态方法 ast,将指定的路径文件解析为ast对象
  static ast(path) {
    // 1.读取文件
    const content = fs.readFileSync(path, "utf-8"); 
    console.log("读取文件", content);
    // 2.解析读取出来的文件内容,使用module类型及膝
    const _ast = parser.parse(content, {
      sourceType: "module",
    });
    //console.log(_ast);
    //console.log("我是ast的body内容", _ast.program.body);
    return _ast;
  }
  // +++新增内容+++
  // 收集依赖,通过入口文件src/index.js,把所有的依赖文件进行解析
  static getDependecy(ast, file) {
    // 存放项目所有依赖的资源文件
    const dependencies = {};
    traverse(ast, {
      // 处理ast对象中为ImportDeclaration的值
      ImportDeclaration: ({ node }) => {
        const oldValue = node.source.value;
        const dirname = path.dirname(file);
        const relativepath = "./" + path.join(dirname, oldValue);
        console.log("转换路径", oldValue, relativepath);
        dependencies[oldValue] = relativepath;
        node.source.value = relativepath; // 将 ./data.js 转化成 ./src/data.js
      },
    });
    console.log(dependencies, "dependencies");
    return dependencies;
  }
}
module.exports = Parser;

添加依赖@babel/traverseyarn add -D @babel/traverse

修改 buildModule 方法

const fs = require("fs");
const path = require("path");
const Parser = require("./Parser");
// 该文件是每次编译生成的资源文件实例
class Compilation {
  // 构造函数传入 compiler实例
  constructor(compiler) {
    const { options, modules } = compiler;
    this.options = options;
    this.entryId = "";
    // 当前命令执行的根路径
    this.root = process.cwd();

    this.modules = modules;
  }
  buildModule(absolutePath, isEntry) {
    console.log(absolutePath, this.root, "absolutePath");
    let ast = Parser.ast(absolutePath);
    // +++新增内容+++
    // 将绝对路径转为相对路径
    const relativePath = "./" + path.relative(this.root, absolutePath);
    console.log(
      "路径地址++++++",
      absolutePath,
      path.relative(this.root, absolutePath)
    );
    if (isEntry) {
      this.entryId = relativePath;
    }
    const dependencies = Parser.getDependecy(ast, relativePath);
    console.log("从入口开始,所有的依赖资源文件:", dependencies);
  }
}
module.exports = Compilation;

执行node wpack.js命令后:

路径地址++++++ ./src/index.js src/index.js
转换路径 ./count ./src/count
转换路径 ./utils ./src/utils
{ './count': './src/count', './utils': './src/utils' } dependencies
从入口开始,所有的依赖资源文件: { './count': './src/count', './utils': './src/utils' }

成功解析出来入口文件依赖的2个文件,count.js和utils.js。

3处理过ast后,将ast生成制定版本的code

经过上面的ast对象,查找处理了所有依赖dependencies文件,接下来开始将ast对象转为指定版本的code代码。

Parser新增 transform 方法

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");

// Parser解析文件
class Parser {
  // 创建静态方法 ast,将指定的路径文件解析为ast对象
  static ast(path) {
    // 1.读取文件
    const content = fs.readFileSync(path, "utf-8"); 
    console.log("读取文件", content);
    // 2.解析读取出来的文件内容,使用module类型及膝
    const _ast = parser.parse(content, {
      sourceType: "module",
    });
    // console.log(_ast);
    // console.log("我是ast的body内容", _ast.program.body);
    return _ast;
  }
  // +++新增内容+++
  // 收集依赖,通过入口文件src/index.js,把所有的依赖文件进行解析
  static getDependecy(ast, file) {
    // 存放项目所有依赖的资源文件
    const dependencies = {};
    traverse(ast, {
      // 处理ast对象中为ImportDeclaration的值
      ImportDeclaration: ({ node }) => {
        const oldValue = node.source.value;
        const dirname = path.dirname(file);
        const relativepath = "./" + path.join(dirname, oldValue);
        console.log("转换路径", oldValue, relativepath);
        dependencies[oldValue] = relativepath;
        node.source.value = relativepath; // 将 ./data.js 转化成 ./src/data.js
      },
    });
    console.log(dependencies, "dependencies");
    return dependencies;
  }
  // +++新增内容+++
  // 把ast数据转为制定版本的可以执行代码
  static transform(ast) {
    const { code } = transformFromAst(ast, null, {
      presets: ["@babel/preset-env"],
    });
    return code;
  }
}
module.exports = Parser;

在Compilation的buildModule中引入新方法

新增第34和35行

const fs = require("fs");
const path = require("path");
const Parser = require("./Parser");
// 该文件是每次编译生成的资源文件实例
class Compilation {
  // 构造函数传入 compiler实例
  constructor(compiler) {
    const { options, modules } = compiler;
    this.options = options;
    this.entryId = "";
    // 当前命令执行的根路径
    this.root = process.cwd();

    this.modules = modules;
  }
  buildModule(absolutePath, isEntry) {
    console.log(absolutePath, this.root, "absolutePath");
    let ast = Parser.ast(absolutePath);
    // +++新增内容+++
    // 将绝对路径转为相对路径
    const relativePath = "./" + path.relative(this.root, absolutePath);
    console.log(
      "路径地址++++++",
      absolutePath,
      path.relative(this.root, absolutePath)
    );
    if (isEntry) {
      this.entryId = relativePath;
    }
    const dependencies = Parser.getDependecy(ast, relativePath);
    //console.log("从入口开始,所有的依赖资源文件:", dependencies);
    // +++新增内容+++
    // 将ast转为低版本代码
    const transformCode = Parser.transform(ast);
    console.log("转换后的代码 ", transformCode);

    // 将资源进行导出
    return {
      dependencies,
      relativePath,
      transformCode,
    };
  }
}
module.exports = Compilation;

安装依赖 yarn add -D @babel/core 执行命令 node wpack.js, 打印出 转换后的代码

转换后的代码  
"use strict";

var _count = require("./src/count");
var _utils = require("./src/utils");
var res = (0, _utils.sum)(_count.a, _count.b);
console.log(res);

4递归处理deps

修改Compilercompile方法

// 引入Compilation文件
const Compilation = require("./Compilation");

// Compiler是配置文件实例的结果,只有一个
class Compiler {
  // 构造函数
  constructor(options) {
    this.options = options;
    this.entry = options.entry;
    // 设置 modules,用来设置loader,
    this.modules = [];
  }
  run() {
    console.log(this.entry);
    this.compile();
  }
  // +++新增内容+++
  compile() {
    const compilation = new Compilation(this);
    const entryModule = compilation.buildModule(this.options.entry, true);

    /* +++新增内容+++ */
    // 将解析处理的module资源添加到 modules中
    this.modules.push(entryModule);
    // 循环遍历多个modules
    this.modules.forEach((_module) => {
      const deps = _module.dependencies;
      console.log(deps);
      for (const dep in deps) {
        if (deps.hasOwnProperty(dep)) {
          // 递归遍历 modules,因为文件有相互之间的引入
          this.modules.push(compilation.buildModule(deps[dep]), false);
        }
      }
    });
  }
}
module.exports = Compiler;

第22行后为新增加内容。

执行命令node wpack.js

可以看到3个被编译转换的代码资源,第4行、第13行、第27行,分别对应index.js、count.js、utils.js

./src/index.js
./src/index.js /Users/shuai/Desktop/code/nodeapp/selfpack/wpack absolutePath
路径地址++++++ ./src/index.js src/index.js
转换后的代码  "use strict";

var _count = require("./src/count.js");
var _utils = require("./src/utils.js");
var res = (0, _utils.sum)(_count.a, _count.b);
console.log(res);
{ './count.js': './src/count.js', './utils.js': './src/utils.js' }
./src/count.js /Users/shuai/Desktop/code/nodeapp/selfpack/wpack absolutePath
路径地址++++++ ./src/count.js src/count.js
转换后的代码  "use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports.c = exports.b = exports.a = void 0;
var a = 1;
exports.a = a;
var b = 2;
exports.b = b;
var c = 99;
exports.c = c;
./src/utils.js /Users/shuai/Desktop/code/nodeapp/selfpack/wpack absolutePath
路径地址++++++ ./src/utils.js src/utils.js
转换后的代码  "use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports.sum = void 0;
var sum = function sum(a, b) {
return a + b;
};
exports.sum = sum;

5生成可执行文件

最后一步将上面3个转换后的代码,生成到一个可以执行的文件,进行代码合并拼接。

Compilation类新增加emitFiles方法

const fs = require("fs");
const path = require("path");
const Parser = require("./Parser");
// 该文件是每次编译生成的资源文件实例
class Compilation {
  // 构造函数传入 compiler实例
  constructor(compiler) {
    const { options, modules } = compiler;
    this.options = options;
    this.entryId = "";
    // 当前命令执行的根路径
    this.root = process.cwd();

    this.modules = modules;
  }
  buildModule(absolutePath, isEntry) {
    console.log(absolutePath, this.root, "absolutePath");
    let ast = Parser.ast(absolutePath);
    // +++新增内容+++
    // 将绝对路径转为相对路径
    const relativePath = "./" + path.relative(this.root, absolutePath);
    console.log(
      "路径地址++++++",
      relativePath,
      path.relative(this.root, absolutePath)
    );
    if (isEntry) {
      this.entryId = relativePath;
    }
    const dependencies = Parser.getDependecy(ast, relativePath);
    //console.log("从入口开始,所有的依赖资源文件:", dependencies);
    // +++新增内容+++
    // 将ast转为低版本代码
    const transformCode = Parser.transform(ast);
    console.log("转换后的代码 ", transformCode);
    // 将资源进行导出
    return {
      dependencies,
      relativePath,
      transformCode,
    };
  }
  /* +++新增内容+++ */
  // 生成最后的打包资源
  emitFiles() {
    let _modules = "";
    const outputPath = path.join(
      this.options.output.path,
      this.options.output.filename
    );
    this.modules.map((_module) => {
      if (!_module.relativePath) return;
      // 记得加引号
      _modules += `'${_module.relativePath}': function(module, exports, require){
        ${_module.transformCode}
      },`;
    });
    console.log(_modules, "路径和code拼接的_modules字符串");
  }
}
module.exports = Compilation;

在Compiler类的compile方法中调用

新增第37行

// 引入Compilation文件
const Compilation = require("./Compilation");

// Compiler是配置文件实例的结果,只有一个
class Compiler {
  // 构造函数
  constructor(options) {
    this.options = options;
    this.entry = options.entry;
    // 设置 modules,用来设置loader,
    this.modules = [];
  }
  run() {
    console.log(this.entry);
    this.compile();
  }
  // +++新增内容+++
  compile() {
    const compilation = new Compilation(this);
    const entryModule = compilation.buildModule(this.options.entry, true);

    /* +++新增内容+++ */
    // 将解析处理的module资源添加到 modules中
    this.modules.push(entryModule);
    // 循环遍历多个modules
    this.modules.forEach((_module) => {
      const deps = _module.dependencies;
      console.log(deps);
      for (const dep in deps) {
        if (deps.hasOwnProperty(dep)) {
          // 递归遍历 modules,因为文件有相互之间的引入
          this.modules.push(compilation.buildModule(deps[dep]), false);
        }
      }
    });
    /* +++新增内容+++ */
    compilation.emitFiles();
  }
}
module.exports = Compiler;

执行node wpack.js查看到输出结果

//路径和code拼接的_modules字符串 
  './src/index.js': function(module, exports, require){
        "use strict";

var _count = require("./src/count.js");
var _utils = require("./src/utils.js");
var res = (0, _utils.sum)(_count.a, _count.b);
console.log(res);
      },
'./src/count.js': function(module, exports, require){
        "use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.c = exports.b = exports.a = void 0;
var a = 1;
exports.a = a;
var b = 2;
exports.b = b;
var c = 99;
exports.c = c;
      },
'./src/utils.js': function(module, exports, require){
        "use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.sum = void 0;
var sum = function sum(a, b) {
  return a + b;
};
exports.sum = sum;
      },

Compilation类中设置webpack的处理资源模板

const fs = require("fs");
const path = require("path");
const Parser = require("./Parser");
const rmdir = require("rmdir");
// 该文件是每次编译生成的资源文件实例
class Compilation {
  // 构造函数传入 compiler实例
  constructor(compiler) {
    const { options, modules } = compiler;
    this.options = options;
    this.entryId = "";
    // 当前命令执行的根路径
    this.root = process.cwd();

    this.modules = modules;
  }
  buildModule(absolutePath, isEntry) {
    console.log(absolutePath, this.root, "absolutePath");
    let ast = Parser.ast(absolutePath);
    // +++新增内容+++
    // 将绝对路径转为相对路径
    const relativePath = "./" + path.relative(this.root, absolutePath);
    console.log("路径地址++++++",
      relativePath,
      path.relative(this.root, absolutePath)
    );
    if (isEntry) {
      this.entryId = relativePath;
    }
    const dependencies = Parser.getDependecy(ast, relativePath);
    //console.log("从入口开始,所有的依赖资源文件:", dependencies);
    // +++新增内容+++
    // 将ast转为低版本代码
    const transformCode = Parser.transform(ast);
    console.log("转换后的代码 ", transformCode);
    // 将资源进行导出
    return {
      dependencies,
      relativePath,
      transformCode,
    };
  }
  /* +++新增内容+++ */
  // 生成最后的打包资源
  emitFiles() {
    let _modules = "";
    const outputPath = path.join(
      this.options.output.path,
      this.options.output.filename
    );
    this.modules.map((_module) => {
      if (!_module.relativePath) return;
      // 记得加引号
      _modules += `'${_module.relativePath}': function(module, exports, require){
        ${_module.transformCode}
      },`;
    });
    // console.log("路径和code拼接的_modules字符串", _modules);
    
    /* +++新增内容+++ */
    // template模板文件,是使用webpack编译处理的文件,将引入资源的入口和处理的资源,替换为我们处理的数据
    const template = `(function (modules) {
        var installedModules = {};
        function __webpack_require__(moduleId) {
            if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
            }
            var module = (installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {},
            });
            modules[moduleId].call(
            module.exports,
            module,
            module.exports,
            __webpack_require__
            );
            module.l = true;
            return module.exports;
        }
        __webpack_require__.m = modules;
        __webpack_require__.c = installedModules;
        __webpack_require__.d = function (exports, name, getter) {
            if (!__webpack_require__.o(exports, name)) {
            Object.defineProperty(exports, name, {
                configurable: false,
                enumerable: true,
                get: getter,
            });
            }
        };
        __webpack_require__.r = function (exports) {
            Object.defineProperty(exports, "__esModule", {
            value: true,
            });
        };
        __webpack_require__.n = function (module) {
            var getter =
            module && module.__esModule
                ? function getDefault() {
                    return module["default"];
                }
                : function getModuleExports() {
                    return module;
                };
            __webpack_require__.d(getter, "a", getter);
            return getter;
        };
        __webpack_require__.o = function (object, property) {
            return Object.prototype.hasOwnProperty.call(object, property);
        };
        __webpack_require__.p = "";
        return __webpack_require__((__webpack_require__.s = "${this.entryId}"));
        })({
            ${_modules}
        });
        `;
    console.log("加上webpack的模板代码", template);
    const dist = path.dirname(outputPath);
    if (fs.existsSync(dist)) {
      //rmdir移除已经存在的目录
      rmdir(dist, function (err, dirs, files) {
        if (!err) {
          fs.mkdirSync(dist);
          fs.writeFileSync(outputPath, template, "utf-8");
        }
      });
    } else {
      fs.mkdirSync(dist);
      fs.writeFileSync(outputPath, template, "utf-8");
    }
  }
}
module.exports = Compilation;

第62行到118行为webpack编译文件生成的模板文件,我们可以直接拿来使用,把入口文件和需要处理的资源替换为我们的数据即可。

执行node wpack.js命令

最后就可以在指定的输出路径中,看到输出的文件资源。

(function (modules) {
        var installedModules = {};
        function __webpack_require__(moduleId) {
            if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
            }
            var module = (installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {},
            });
            modules[moduleId].call(
            module.exports,
            module,
            module.exports,
            __webpack_require__
            );
            module.l = true;
            return module.exports;
        }
        __webpack_require__.m = modules;
        __webpack_require__.c = installedModules;
        __webpack_require__.d = function (exports, name, getter) {
            if (!__webpack_require__.o(exports, name)) {
            Object.defineProperty(exports, name, {
                configurable: false,
                enumerable: true,
                get: getter,
            });
            }
        };
        __webpack_require__.r = function (exports) {
            Object.defineProperty(exports, "__esModule", {
            value: true,
            });
        };
        __webpack_require__.n = function (module) {
            var getter =
            module && module.__esModule
                ? function getDefault() {
                    return module["default"];
                }
                : function getModuleExports() {
                    return module;
                };
            __webpack_require__.d(getter, "a", getter);
            return getter;
        };
        __webpack_require__.o = function (object, property) {
            return Object.prototype.hasOwnProperty.call(object, property);
        };
        __webpack_require__.p = "";
        return __webpack_require__((__webpack_require__.s = "./src/index.js"));
        })({
            './src/index.js': function(module, exports, require){
        "use strict";

var _count = require("./src/count.js");
var _utils = require("./src/utils.js");
var res = (0, _utils.sum)(_count.a, _count.b);
console.log(res);
      },'./src/count.js': function(module, exports, require){
        "use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.c = exports.b = exports.a = void 0;
var a = 1;
exports.a = a;
var b = 2;
exports.b = b;
var c = 99;
exports.c = c;
      },'./src/utils.js': function(module, exports, require){
        "use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.sum = void 0;
var sum = function sum(a, b) {
  return a + b;
};
exports.sum = sum;
      },
        });

自定义loader

loader的实现本质就是一个函数方法。是对代码或静态资源进行处理。

新建loader目录

创建test-loader.js自定义loader

const loader = (content, map, meta) => {
  console.log("执行了test-loader++++++++++++++++、", content, map, meta);
  return content + '\n console.log("last line")';
};
module.exports = loader;

创建一个自定义loader,给每个文件代码最后添加一行注释。

在webpack.config.js中添加的test-loader

const path = require("path");

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [path.resolve(__dirname, "loader", "test-loader")],
        options: {
          //  预设babel做怎样的兼容性处理
          presets: ["@babel/preset-env"],
        },
      },
    ],
  }
};

修改Parser的ast方法

// 创建静态方法 ast,将指定的路径文件解析为ast对象
static ast(path, options) {
  // 0,读取配置文件中的rules
  let rules = options.module.rules;
  // 1.读取文件
  const content = fs.readFileSync(path, "utf-8");

  
  /* 处理loader start */
  let loaders = [];
  rules.forEach((rule) => {
    if (rule.test.test(path)) {
      loaders = [...loaders, ...rule.use];
    }
  });
  // 经过loader处理后的code
  let targetCode = "";
  for (let i = loaders.length - 1; i >= 0; i--) {
    let loader = loaders[i];
      targetCode = require(loader)(content);
  }
  /* 处理loader end */

  
  console.log("读取文件", content);
  // 2.解析读取出来的文件内容,使用module类型及膝
  const _ast = parser.parse(targetCode, {
    sourceType: "module",
  });
  // console.log(_ast);
  // console.log("我是ast的body内容", _ast.program.body);
  return _ast;
}

再次运行编译后的文件

执行node wpack.js进行编译打包,输出bundle.js结果

(function (modules) {
  var installedModules = {};
  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {},
    });
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );
    module.l = true;
    return module.exports;
  }
  __webpack_require__.m = modules;
  __webpack_require__.c = installedModules;
  __webpack_require__.d = function (exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, {
        configurable: false,
        enumerable: true,
        get: getter,
      });
    }
  };
  __webpack_require__.r = function (exports) {
    Object.defineProperty(exports, "__esModule", {
      value: true,
    });
  };
  __webpack_require__.n = function (module) {
    var getter =
      module && module.__esModule
        ? function getDefault() {
            return module["default"];
          }
        : function getModuleExports() {
            return module;
          };
    __webpack_require__.d(getter, "a", getter);
    return getter;
  };
  __webpack_require__.o = function (object, property) {
    return Object.prototype.hasOwnProperty.call(object, property);
  };
  __webpack_require__.p = "";
  return __webpack_require__((__webpack_require__.s = "./src/index.js"));
})({
  "./src/index.js": function (module, exports, __webpack_require__) {
    "use strict";

    var _count = __webpack_require__("./src/count.js");
    var _utils = __webpack_require__("./src/utils.js");
    var res = (0, _utils.sum)(_count.a, _count.b);
    console.log(res);
    console.log("last line");
  },
  "./src/count.js": function (module, exports, __webpack_require__) {
    "use strict";

    Object.defineProperty(exports, "__esModule", {
      value: true,
    });
    exports.c = exports.b = exports.a = void 0;
    var a = 1;
    exports.a = a;
    var b = 2;
    exports.b = b;
    var c = 99;
    exports.c = c;
    console.log("last line");
  },
  "./src/utils.js": function (module, exports, __webpack_require__) {
    "use strict";

    Object.defineProperty(exports, "__esModule", {
      value: true,
    });
    exports.sum = void 0;
    var sum = function sum(a, b) {
      return a + b;
    };
    exports.sum = sum;
    console.log("last line");
  },
});

运行下该文件,可以看到

last line
last line
3
last line

自定义plugin

插件的实现依赖第三方库tapable,在不同的生命周期触发不同的方法执行。发生在打包编译时,不同阶段调用插件的apply方法内的不同钩子执行。 安装依赖yarn add tapable

先修改wpack.js入口文件

// 引入编译文件 Compiler
const Compiler = require("./package/Compiler");
// 引入配置文件 webpack.config.js
const options = require("./webpack.config.js");
const compiler = new Compiler(options);
/* 新增内容 */
//添加插件的执行
const plugins = options.plugins;
for (let plugin of plugins) {
  plugin.apply(compiler);
}
// 执行 compiler 实例的run方法
compiler.run();

首先添加对插件的处理,插件的处理过程应该在实例对象run方法执行之前,这样才能把插件安装到comiler实例对象内部。

Compiler类中添加hooks

hooks表示插件的不同执行时机,在构造函数中定义几个测试使用。 yarn add tapable,新增run、emit、done三个测试钩子。正式版本中有很多个钩子的使用调用。

// 引入Compilation文件
const Compilation = require("./Compilation");
const { SyncHook } = require("tapable");

// Compiler是配置文件实例的结果,只有一个
class Compiler {
  // 构造函数
  constructor(options) {
    this.options = options;
    this.entry = options.entry;
    // 设置 modules,用来设置loader,
    this.modules = [];
    /* 新增 处理插件的钩子 */
    this.hooks = {
      run: new SyncHook(),
      // 增加
      emit: new SyncHook(),
      done: new SyncHook(),
    };
  }
  run() {
    console.log(this.entry);
    this.compile();
  }
  // +++新增内容+++
  compile() {
    const compilation = new Compilation(this);
    // 增加
    this.hooks.run.call();
    const entryModule = compilation.buildModule(this.options.entry, true);

    /* +++新增内容+++ */
    // 将解析处理的module资源添加到 modules中
    this.modules.push(entryModule);
    // 循环遍历多个modules
    this.modules.forEach((_module) => {
      const deps = _module.dependencies;
      //   console.log(deps);
      for (const dep in deps) {
        if (deps.hasOwnProperty(dep)) {
          // 递归遍历 modules,因为文件有相互之间的引入
          this.modules.push(compilation.buildModule(deps[dep]), false);
        }
      }
    });
    /* +++新增内容+++ */
    compilation.emitFiles();
    // 增加
    this.hooks.emit.call();
    this.hooks.done.call();
  }
}
module.exports = Compiler;

新建plugins目录

创建consolelog测试插件

// ClgOnBuildWebpackPlugin.js
const pluginName = 'ClgOnBuildWebpackPlugin';

class ClgOnBuildWebpackPlugin {
  apply(compiler) {
    compiler.hooks.run.tap(pluginName, compilation => {
      console.log('wpack开始执行打包!!!');
    });
    // 在文件打包结束后执行
    compiler.hooks.done.tap(pluginName,(compilation)=> {
      console.log("整个webpack打包结束")
    })
    // 在webpack输出文件的时候执行
    compiler.hooks.emit.tap(pluginName,(compilation)=> {
        console.log("文件开始发射")
    })
  }
}
module.exports = ClgOnBuildWebpackPlugin;

在webpack.config.js中引入插件

引入插件,并在plugins中创建实例

const path = require("path");
const babelLoader = require("babel-loader");
const lessLoader = require("./loader/less-loader.js");
const styleLoader = require("./loader/style-loader.js");
/* 新增插件 */
const ClgOnBuildWebpackPlugin = require("./plugins/ClgOnBuildWebpackPlugin.js");

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: ["babel-loader", path.resolve(__dirname, "loader", "test-loader")],
        options: {
          //  预设babel做怎样的兼容性处理
          presets: ["@babel/preset-env"],
        },
      },
    ],
  },
  /* 新增插件 */
  plugins: [new ClgOnBuildWebpackPlugin()],
};

执行node wpack.js命令,可以看到插件的执行情况。 项目代码:gitee.com/shenshuai89…