webpack原理

171 阅读6分钟

webpack构建流程分析

webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:首先会从配置文件和shell语句中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需要的参数;初始化完成后会调用Comiler的run来真正启动webpack编译构建过程,webpack的构建流程包括compile,make,build,seal,emit阶段,执行完这些阶段就完成了构建过程

初始化

entry-options启动

从配置文件和Shell语句中读取与合并参数,得出最终的参数

run实例化

compiler:用上一步得到的参数初始化Compiler对象,加载所有配置的插件,执行对象的run方法开始执行编译,执行对象的run方法开始执行编译

编译构建

entry确定入口

根据配置中的entry找出所有的入口文件

make编译模版

从入口文件出发,调用所有配置的Loader对模块进行编译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理

build module完成模块编译

经过上面一步使用Loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系

seal 输出资源

根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这不是可以修改输出内容的最后机会

emit输出完成

在确定好输出内通后,根据配置确定输出的路径和文件名,吧文件内容写到文件系统

实现一个简易的webpack

准备工作

目录结构

首先初始化一个项目,结构如下:

|-- forestpack
    |-- dist(打包目录)
    |   |-- bundle.js
    |   |-- index.html
    |-- lib(核心文件)
    |   |-- compiler.js(编译相关。Compiler为一个类,并且有run方法去开启编译,还有构建module(buildModule)和输出文件(emitFiles))
    |   |-- index.js(实例化Compiler类,并将配置参数(对应forstpack.config.js)传入)
    |   |-- parser.js(解析相关。包含解析AST(getAST)、收集依赖)
    |   |-- test.js(测试文件,用于测试方法函数打console使用)
    |-- src(源代码)
    |   |-- greeting.js
    |   |-- index.js
    |-- forstpack.config.js(配置文件,类似webpack.config.js)
    |-- package.json

完成‘造轮子’前30%的代码

首先完善forstpack.config.js和src

首先forstpack.config.js

const path = require("path");

module.exports = {
  entry: path.join(__dirname, "./src/index.js"),
  output: {
    path: path.join(__dirname, "./dist"),
    filename: "bundle.js",
  },
};

其次是src,在src目录下定义了两个文件

  • greeting.js
// greeting.js
export function greeting(name) {
  return "你好" + name;
}
  • index.js
import { greeting } from "./greeting.js";

document.write(greeting("森林"));

到这里,我们的准备工作都完成了

逻辑梳理

webpack的整个流程是

  • 读取入口文件
  • 分析入口文件,递归的去读取模块所依赖的文件内容,生成AST语法树
  • 根据AST语法树,生成浏览器能够运行的代码

正式开工

compile.js编写

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

module.exports = class Compiler {
  // 接收通过lib/index.js new Compiler(options).run()传入的参数,对应`forestpack.config.js`的配置
  constructor(options) {
    const { entry, output } = options;
    this.entry = entry;
    this.output = output;
    this.modules = [];
  }
  // 开启编译
  run() {}
  // 构建模块相关
  buildModule(filename, isEntry) {
    // filename: 文件名称
    // isEntry: 是否是入口文件
  }
  // 输出文件
  emitFiles() {}
};

compile.js主要做了几个事情:

  • 接收forestpack.config.js配置参数,并初始化entry、output
  • 开启编译run方法。处理构建模块、收集依赖、输出文件等。
  • buildModule方法。主要用于构建模块(被run方法调用)
  • emitFiles方法。输出文件(同样被run方法调用) 到这里,compiler.js的大致结构已经出来了,但是得到模块的源码后, 需要去解析,替换源码和获取模块的依赖项, 也就对应我们下面需要完善的parser.js。

parser.js 编写

const fs = require("fs");
// const babylon = require("babylon");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("babel-core");
module.exports = {
  // 解析我们的代码生成AST抽象语法树
  getAST: (path) => {
    const source = fs.readFileSync(path, "utf-8");

    return parser.parse(source, {
      sourceType: "module", //表示我们要解析的是ES模块
    });
  },
  // 对AST节点进行递归遍历
  getDependencies: (ast) => {
    const dependencies = [];
    traverse(ast, {
      ImportDeclaration: ({ node }) => {
        dependencies.push(node.source.value);
      },
    });
    return dependencies;
  },
  // 将获得的ES6的AST转化成ES5
  transform: (ast) => {
    const { code } = transformFromAst(ast, null, {
      presets: ["env"],
    });
    return code;
  },
};
  1. 首先说一下用到的几个babel包
  • @babel/parser:用于将源码生成AST
  • @babel/traverse:对AST节点进行递归遍历
  • babel-core/@babel/preset-env:将获得的ES6的AST转化成ES5
  1. 完善 compiler.js
const { getAST, getDependencies, transform } = require("./parser");
const path = require("path");
const fs = require("fs");

module.exports = class Compiler {
  constructor(options) {
    const { entry, output } = options;
    this.entry = entry;
    this.output = output;
    this.modules = [];
  }
  // 开启编译
  run() {
    const entryModule = this.buildModule(this.entry, true);
    this.modules.push(entryModule);
    this.modules.map((_module) => {
      _module.dependencies.map((dependency) => {
        this.modules.push(this.buildModule(dependency));
      });
    });
    // console.log(this.modules);
    this.emitFiles();
  }
  // 构建模块相关
  buildModule(filename, isEntry) {
    let ast;
    if (isEntry) {
      ast = getAST(filename);
    } else {
      const absolutePath = path.join(process.cwd(), "./src", filename);
      ast = getAST(absolutePath);
    }

    return {
      filename, // 文件名称
      dependencies: getDependencies(ast), // 依赖列表
      transformCode: transform(ast), // 转化后的代码
    };
  }
  // 输出文件
  emitFiles() {
    const outputPath = path.join(this.output.path, this.output.filename);
    let modules = "";
    this.modules.map((_module) => {
      modules += `'${_module.filename}' : function(require, module, exports) {${_module.transformCode}},`;
    });

    const bundle = `
        (function(modules) {
          function require(fileName) {
            const fn = modules[fileName];
            const module = { exports:{}};
            fn(require, module, module.exports)
            return module.exports
          }
          require('${this.entry}')
        })({${modules}})
    `;

    fs.writeFileSync(outputPath, bundle, "utf-8");
  }
};

主要说一下emitFiles

emitFiles() {
    const outputPath = path.join(this.output.path, this.output.filename);
    let modules = "";
    this.modules.map((_module) => {
      modules += `'${_module.filename}' : function(require, module, exports) {${_module.transformCode}},`;
    });

    const bundle = `
        (function(modules) {
          function require(fileName) {
            const fn = modules[fileName];
            const module = { exports:{}};
            fn(require, module, module.exports)
            return module.exports
          }
          require('${this.entry}')
        })({${modules}})
    `;

    fs.writeFileSync(outputPath, bundle, "utf-8");
  }

首先我们需要先了解一下webpack文件机制,下面一段代码时经过webpack打包精简后的代码

// dist/index.xxxx.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__(0);
})([
/* 0 module */
(function(module, exports, __webpack_require__) {
  ...
}),
/* 1 module */
(function(module, exports, __webpack_require__) {
  ...
}),
/* n module */
(function(module, exports, __webpack_require__) {
  ...
})]);

分析:

  • webpack将所有模块(可以简单理解为文件)包裹于一个函数,并传入默认参数,将所有模块放入一个数组中,取名为modules,并通过数组的下标来作为moduleId
  • 将modules传入一个自执行函数,自执行函数中包含一个installedModules已经加载过的模块和一个模块加载函数,最后加载入口模块并返回
  • __webpack_require__模块加载,先判断installedModules是否已经加载,加载过了就直接返回exports数据,没有加载过该模块就通过 modules[moduleId].call(module.exports, module, module.exports, webpack_require) 执行模块并且将 module.exports 给返回。

简单来说:

  • 通过webpack打包出来的是一个匿名函数
  • modules是一个数组,每一项是一个模块初始化函数
  • _webpack_require_用来加载模块,返回module.exports
  • 通过WEBPACK_REQUIRE_METHOD(0)启动函数

lib/index.js 入口文件编写

const Compiler = require("./compiler");
const options = require("../forestpack.config");

new Compiler(options).run();

这里逻辑就比较简单了:实例化Compiler类,并将配置参数(对应forstpack.config.js)传入。

运行node lib/index.js就会在dist目录下生成bundle.js文件。

(function (modules) {
  function require(fileName) {
    const fn = modules[fileName];
    const module = { exports: {} };
    fn(require, module, module.exports);
    return module.exports;
  }
  require("/Users/fengshuan/Desktop/workspace/forestpack/src/index.js");
})({
  "/Users/fengshuan/Desktop/workspace/forestpack/src/index.js": function (
    require,
    module,
    exports
  ) {
    "use strict";

    var _greeting = require("./greeting.js");

    document.write((0, _greeting.greeting)("森林"));
  },
  "./greeting.js": function (require, module, exports) {
    "use strict";

    Object.defineProperty(exports, "__esModule", {
      value: true,
    });
    exports.greeting = greeting;

    function greeting(name) {
      return "你好" + name;
    }
  },
});

结果展示

我们在dist目录下创建index.html文件,引入打包生成的bundle.js文件:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="./bundle.js"></script>
  </body>
</html>

打开浏览器

代码逻辑梳理

转载地址

juejin.cn/post/685953…