手写简单webpack例子

84 阅读1分钟

入口文件cli,利用Commander获取命令行参数

#!/usr/bin/env node

const { Command } = require("commander");
const path = require("path");
const fs = require("fs");
const webpack = require("./webpack.js");
const program = new Command();

program.option("-c, --config <filename>", "config file").action((options) => {
  let option = {};
  if (options.config) {
    option = require(path.join(process.cwd(), options.config));
  }
  webpack(option);
});

program.parse();

webpack 主体

  • 合并默认参数

  • 创建Compiler对象

  • 初始化可用的hooks

  • 遍历plugins注册

  • 执行run方法,触发不同hooks

  • 获取入口文件,获取文件代码

  • 遍历loader根据正则匹配

  • 将模块地址转为相对于根目录,内容是上面loader编译后内容

  • 替换require为__webpack_require

  • esj拼接模板

const path = require("path");
const fs = require("fs");
const { SyncHook } = require("tapable");
const babel = require("@babel/core");
const generate = require("@babel/generator");
const ejs = require("ejs");

class Compiler {
  constructor(config) {
    this.config = config;
    this.entry = config.entry;
    this.root = process.cwd();
    this.modules = {};
    this.hooks = {
      run: new SyncHook(["compiler"]),
      make: new SyncHook(["compiler"]),
      emit: new SyncHook(["compiler"]),
      done: new SyncHook(["compiler"]),
    };
  }

  run() {
    this.hooks.run.call(this);
    this.hooks.make.call(this);
    this.buildModule(path.resolve(this.root, this.entry));
    this.hooks.emit.call(this);
    this.buildTemplate();
    this.hooks.done.call(this);
  }

  buildModule(modulePath) {
    let source = this.getSource(modulePath);

    let moduleName =
      "./" + path.relative(this.root, modulePath).replace("\\", "/");
    if (!this.modules[moduleName]) {
      const parentPath = path.join(modulePath, "../");
      const { sourceCode, dependenes } = this.parse(source, parentPath);
      this.modules[moduleName] = sourceCode
      dependenes.forEach((dependent) => {
        this.buildModule(dependent);
      });
    }
  }
  getSource(modulePath) {
    let source = fs.readFileSync(modulePath, "utf-8");
    this.config.module.rules.forEach((it) => {
      if (it.test.test(modulePath)) {
        it.use.forEach((loader) => {
          source = require(loader)(source);
        });
      }
    });
    return source;
  }
  parse(code, parentPath) {
    const ast = babel.parse(code, {
      sourceType: "module",
      presets: ["@babel/preset-env"],
    });
    const dependenes = [];
    const root = this.root;
    babel.traverse(ast, {
      CallExpression(p) {
        if (p.node.callee.name == "require") {
          p.node.callee.name = "__webpack_require";
          const relativePath = path
            .relative(
              root,
              path.join(parentPath, "./" + p.node.arguments[0].value)
            )
            .replace("\\", "/");
          dependenes.push(path.join(root, relativePath));
          p.node.arguments[0].value = "./" + relativePath;
        }
      },
    });
    const sourceCode = generate.default(ast).code;
    return {
      sourceCode,
      dependenes,
    };
  }
  buildTemplate() {
    const templateText = fs.readFileSync(
      path.resolve(__dirname, "./template.ejs"),
      "utf-8"
    );
    const output = ejs.render(templateText, {
      entry: this.entry,
      modules: this.modules,
    });
    // console.log(output);
    fs.writeFileSync(
      path.join(this.config.output.path, this.config.output.filename),
      output,
      {
        encoding: "utf8",
      }
    );
  }
}


function webpack(option) {
  const config = mergeExports(option);
  const complier = new Compiler(config);
  complier.run();
}

function mergeExports(option) {
  return Object.assign(
    {
      devtool: "source-map",
    },
    option
  );
}

module.exports = webpack;

模板

(function (modules) {
  var installedModules = {};
  function __webpack_require(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[installedModules].exports;
    }

    var module = (installedModules[moduleId] = {
      i: moduleId,
      exports: {},
    });
    modules[moduleId].call(
      module.exports,
      __webpack_require,
      module,
      module.exports
    );
    return module.exports;
  }

  __webpack_require("<%= entry %>");
})({
  <% for(var key in modules) { %>
    "<%= key %>" : function (__webpack_require, module, exports) {
      <%- modules[key] %>
    },
  <% } %>
});