关于webpack:面试官webpack原理都不会

4次阅读

共计 8021 个字符,预计需要花费 21 分钟才能阅读完成。

引言

前一段时间我把 webpack 源码大略读了一遍,webpack4.x 版本后,其源码曾经比拟宏大,对各种开发场景进行了高度形象,浏览老本也愈发低廉。

适度剖析源码对于大家并没有太大的帮忙。本文次要是想通过剖析 webpack 的构建流程以及实现一个简略的 webpack 来让大家对 webpack 的外部原理有一个大略的理解。(保障能看懂,不懂你打我 ????)

webpack 构建流程剖析

首先,毋庸多言,上图~

webpack 的运行流程是一个串行的过程,从启动到完结会顺次执行以下流程:首先会从配置文件和 Shell 语句中读取与合并参数,并初始化须要应用的插件和配置插件等执行环境所须要的参数;初始化实现后会调用 Compilerrun来真正启动 webpack 编译构建过程,webpack的构建流程包含 compilemakebuildsealemit 阶段,执行完这些阶段就实现了构建过程。

初始化

entry-options 启动

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

run 实例化

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

编译构建

entry 确定入口

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

make 编译模块

从入口文件登程,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都通过了本步骤的解决

build module 实现模块编译

通过下面一步应用 Loader 翻译完所有模块后,失去了每个模块被翻译后的最终内容以及它们之间的依赖关系

seal 输入资源

依据入口和模块之间的依赖关系,组装成一个个蕴含多个模块的 Chunk,再把每个 Chunk 转换成一个独自的文件退出到输入列表,这步是能够批改输入内容的最初机会

emit 输入实现

在确定好输入内容后,依据配置确定输入的门路和文件名,把文件内容写入到文件系统

剖析完构建流程,上面让咱们本人入手实现一个繁难的 webpack 吧~

实现一个繁难的 webpack

筹备工作

目录构造

咱们先来初始化一个我的项目,构造如下:

|-- forestpack
    |-- dist
    |   |-- bundle.js
    |   |-- index.html
    |-- lib
    |   |-- compiler.js
    |   |-- index.js
    |   |-- parser.js
    |   |-- test.js
    |-- src
    |   |-- greeting.js
    |   |-- index.js
    |-- forstpack.config.js
    |-- package.json

这里我先解释下每个文件 / 文件夹对应的含意:

  • dist:打包目录
  • lib:外围文件,次要包含 compilerparser

    • compiler.js:编译相干。Compiler为一个类, 并且有 run 办法去开启编译,还有构建modulebuildModule)和输入文件(emitFiles
    • parser.js:解析相干。蕴含解析ASTgetAST)、收集依赖(getDependencies)、转换(es6 转 es5
    • index.js:实例化 Compiler 类,并将配置参数(对应forstpack.config.js)传入
    • test.js:测试文件,用于测试方法函数打 console 应用
  • src:源代码。也就对应咱们的业务代码
  • forstpack.config.js:配置文件。相似webpack.config.js
  • package.json:这个就不必我多说了~~~(什么,你不晓得??)

先实现“造轮子”前 30% 的代码

我的项目搞起来了,但仿佛还少点货色~~

对了!根底的文件咱们须要先欠缺下:forstpack.config.jssrc

首先是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("森林"));

ok,到这里咱们曾经把须要筹备的工作都实现了。(问:为什么这么根底?答:当然要根底了,咱们的外围是“造轮子”!!)

梳理下逻辑

短暂的停留一下,咱们梳理下逻辑:

Q: 咱们要做什么?

A: 做一个比 webpack 更强的super webpack(不好意思,失态了,一不小心说出了我的心声)。还是低调点(避免一会被疯狂打脸)

Q: 怎么去做?

A: 看下文(23333)

Q: 整个的流程是什么?

A: 哎嘿,大略流程就是:

  • 读取入口文件
  • 剖析入口文件,递归的去读取模块所依赖的文件内容,生成 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 配置参数,并初始化entryoutput
  • 开启编译 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;
  },
};

看完这代码是不是有点懵(说好的保障让看懂的 ????)

别着急,你听我辩解!!????

这里要先着重说下用到的几个 babel 包:

  • @babel/parser:用于将源码生成AST
  • @babel/traverse:对 AST 节点进行递归遍历
  • babel-core/@babel/preset-env:将取得的 ES6AST转化成ES5

parser.js中次要就三个办法:

  • getAST:将获取到的模块内容 解析成 AST 语法树
  • getDependencies:遍历AST,将用到的依赖收集起来
  • transform:把取得的 ES6AST转化成ES5

欠缺 compiler.js

在下面咱们曾经将 compiler.js 中会用到的函数占好地位,上面咱们须要欠缺一下 compiler.js,当然会用到parser.js 中的一些办法(废话,不然我下面干嘛要先把 parser.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");
  }
};

对于 compiler.js 的外部函数,下面我说过一遍,这里次要来看下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");
  }

这里的 bundle 一大坨,什么鬼?

咱们先来理解下 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 打包进去的是一个匿名闭包函数(IIFE
  • modules是一个数组,每一项是一个模块初始化函数
  • __webpack_require__用来加载模块,返回module.exports
  • 通过 WEBPACK_REQUIRE_METHOD(0) 启动程序

(小声 bb:怎么样,这样听懂了吧)

lib/index.js 入口文件编写

到这里,就剩最初一步了(仿佛见到了胜利的曙光)。在 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;}
  },
});

和下面用 webpack 打包生成的 js 文件作下比照,是不是很类似呢?

来吧!展现

咱们在 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>

此时关上浏览器:

如你所愿,失去了咱们预期的后果~

总结

通过对 webpack 构建流程的剖析以及实现了一个繁难的 forestpack,置信你对webpack 的构建原理曾经有了一个清晰的认知!(当然,这里的 forestpackwebpack相比还很弱很弱,,,,)

正文完
 0