xiaochengzi6 / Blog

个人博客
GNU Lesser General Public License v2.1
0 stars 0 forks source link

webpack 的打包原理的简单实现 #44

Open xiaochengzi6 opened 2 years ago

xiaochengzi6 commented 2 years ago

注意:如果已经生成了./dist/bundle.js就不需要执行 Node bundle.js文件。 初次运行 node bundle.js 然后代开 index.html 可以查看

文件代码在 webpack的打包原理的简单实现

实现原理

1.需要读到入口文件里面的内容。

2.分析入口文件,递归的去读取模块所依赖的文件内容,生成AST语法树。

3.根据AST语法树,生成浏览器能够运行的代码

入口文件为 /src/index.js 它的依赖有 /src/add.js 和 /src/minus.js 去解析 ast.program.body 树发现 大致由这样的形式组成

[
  Node {type: 'ImportDeclaration'},
  Node {type: 'ImportDeclaration'},
  Node {type: 'VariableDeclaration'},
  Node {type: 'VariableDeclaration'},
  Node {type: 'ExpressionStatement'},
  Node {type: 'ExpressionStatement'}
]

/src/index.js 的代码时这样

import add from "./add.js"
import {minus} from "./minus.js";

const sum = add(1,2);
const division = minus(2,1);

console.log(sum);
console.log(division);

ImportDeclaration 代表的是 import 声明语句

VariableDeclaration 代表的是 变量 声明语句

ExpressionStatement 表达式语句

那现在可以分析出 文件的依赖是通过 type: ImportDeclaration 的 import 表达式得到的。

每一个语句 简单的看很有规律是由 [] 数组来组合的似乎每一个都被换分成

  Node {
    type: 'ImportDeclaration',// 这里可以看成类型
    start: 28,
    end: 61,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    specifiers: [ [Node] ],
    source: Node {
      type: 'StringLiteral',
      start: 48,
      end: 60,
      loc: [SourceLocation],
      extra: [Object],
      value: './minus.js' // 这里的value指的就是import的值
    }
  },

我们就利用这样的特性来获取入口文件下的依赖 可以通过 node.source.value 来获得这些文件名并保存

// 依赖
let deps = {};

traverse(ast, {
  ImportDeclaration({node}){
    const dirname = path.dirname(file);
    const abspath = './' + path.join(dirname, node.source.value)
    deps[node.source.value] = abspath;
  }
})

deps = { './add.js': './src\\add.js', './minus.js': './src\\minus.js' }

再将Es6转化成 Es5

 (function (graph) {
        function require(file) {
            (function (code) {
                eval(code)
            })(graph[file].code)
        }
        require(file) // 这里 file 指向的是外层函数的参数 file
    })(depsGraph)

刚开始执行eval(code)的时候就是在运行这个代码

// index.js
"use strict"
var _add = _interopRequireDefault(require("./add.js"));
var _minus = require("./minus.js");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
var sum = (0, _add["default"])(1, 2);
var division = (0, _minus.minus)(2, 1);
console.log(sum); console.log(division);

但是这里是在 depsGraph 这个对象中运行的 我们来看一下这个对象

{
  './src/index.js': {
    deps: { './add.js': './src\\add.js', './minus.js': './src\\minus.js' },
    code: '"use strict";\n' +
      '\n' +
      'var _add = _interopRequireDefault(require("./add.js"));\n' +
      '\n' +
      'var _minus = require("./minus.js");\n' +
      '\n' +
      'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 
"default": obj }; }\n' +
      '\n' +
      'var sum = (0, _add["default"])(1, 2);\n' +
      'var division = (0, _minus.minus)(2, 1);\n' +
      'console.log(sum);\n' +
      'console.log(division);'
  },
  './src\\add.js': {
    deps: {},
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports["default"] = void 0;\n' +
      '\n' +
      'var _default = function _default(a, b) {\n' +
      '  return a + b;\n' +
      '};\n' +
      '\n' +
      'exports["default"] = _default;'
  },
  './src\\minus.js': {
    deps: {},
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports.minus = void 0;\n' +
      '\n' +
      'var minus = function minus(a, b) {\n' +
      '  return a - b;\n' +
      '};\n' +
      '\n' +
      'exports.minus = minus;'
  }
}

很明显里面只有以绝对路径命名的参数 例如这些require("./add.js")都不会得到这个对象中的code 我们要对其转化成一个绝对路径

(function (graph) {
    function require(file) {
        function absRequire(relPath) {
          // 实际上调用 require('./add.js')就是在调用 './src/index.js'中的deps的值
            return require(graph[file].deps[relPath])
        }
        (function (require,code) {
            eval(code)
        })(absRequire,graph[file].code)
    }
    require(file)
})(depsGraph)

怎么来理解 require(graph[file].deps[relPath])实际上调用 require('./add.js')就是在调用'./src/index.js' 中的deps的值

depsGraph: {
  './src/index.js': {
    deps: { './add.js': './src\\add.js', './minus.js': './src\\minus.js' },
    // ...
  },
  //...
}

就是加了一层嵌套。目的还是从主文件下的依赖关系 deps 中拿到绝对路径。

但是 export 也没有定义 在执行 add.js 文件的时候会出现问题

// add.js
"use strict";
Object.defineProperty(exports, "__esModule", {  value: true});
exports["default"] = void 0;
var _default = function _default(a, b) {  return a + b;};
exports["default"] = _default;

所以我们创建一个 export = {} 然后再通过 require 函数 return 出去这样包裹着的对象。 使用 _interopRequireDefault 会接收到我们的 exports default function () {....} 的值。

// index.js
"use strict"
var _add = _interopRequireDefault(require("./add.js"));
var _minus = require("./minus.js");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

清晰了然 _interopRequireDefault 查看对象是否有 esModule (ECM格式)对象。如果有就会直接返回 obj 反之则返回一个对象且属性名为 default 的属性。

'./src\\add.js': {
    deps: {},
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports["default"] = void 0;\n' +
      '\n' +
      'var _default = function _default(a, b) {\n' +
      '  return a + b;\n' +
      '};\n' +
      '\n' +
      'exports["default"] = _default;'
  }

我们可以明白 这些 add.js 又或者是 minu.js 的这类文件都会将 __esModule 这样的属性存在来证明是使用了 ESM 格式

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

总结:

1、我们首先去分析了一下入口文件的 ast 树 可以看到文件的依赖关系可以通过 ast 抽象语法树中获得 ImportDeclaration 来去获得然后将其返回 {file, deps, code}

2、然后我们开始去遍历这个 入口的依赖 并将其依赖的依赖也要遍历 返回的结果被存放在 数组中 之后就会对其进行组合

那么 同样的也可以清楚的了解到 webpack 也是这样去获得 依赖文件的 并对每一个依赖文件都进行迭代, 再根据不同的文件后缀名调用 loader 将文件转化 然后 生成 chunk 最后根据入口点 entry 和 分包策略 生成不同的文件。

webpack 的核心构建流程:

1、首先是初始化 去获得 webpack.config.js 中的配置对象 shell 命令行传入的参数 和 webpack 默认参数 得到一些初始化参数

2、根据上述的初始化参数去创建一个 compliter 实例化对象 这个对象是编译管理器 可以通过它 来获得 webpack 的主要环境 并去注册配置的 plugins 插件更像是一种监听函数 会监听 webpack 生命周期的事件节点 当事件触发就会触发相应的回调函数。

3、根据入口文件进行 ast 语法抽象树的解析 然后去获得依赖文件并遍历依赖文件

4、根据 文件的类型和 loader 的配置,调用所有配置的 loader 对文件进行转化,再找到该模块的依赖的 再去转化

5、递归完成之后会得到每个文件的结果和文件的依赖关系,根据入口 entry 和 分包配置生成代码 chunk

6、将这些 chunk 输出到文件系统中