webpack打包原理

1,040 阅读5分钟

一、分析打包生成的文件

打包生成的文件内容分为两大部分:固定模板和依赖图谱(模块路径和相应的chunk) image.png

**固定模板(打补丁)**是定义一些webpack中用到的方法,基本所有打包文件都会有,内容也一样。

重点在于依赖图谱,依赖图谱包括模块路径和相应的chunk。

我们现在要模拟webpack根据文件和配置,生成一个打包文件(bundle文件),所以我们首先要解决如何获取模块路径生成相应的chunk这两个问题。

我们可以根据配置中的entry配置项知道从哪个模块开始执行打包任务,打包任务最核心的工作就是编译模块

获取模块后要做的事情就是查看是否有依赖?获取依赖的路径模块的编译,输出chunk

二、创建基础模板

整体项目目录:

image.png

  1. 新建项目,项目中新建src文件夹。添加index.js和other.js。

index.js:

import { str } from "./other.js";
console.log(str);

other.js:

export const str = "bundle";
import { a } from "./a.js";
export const str = "bundle" + a;

a.js:

export const a = "a";
  1. 项目根目录创建webpack.config.js:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "[name].js",
    path: path.resolve(__dirname, "./dist")
  },
  mode: "development",
  plugins: [new HtmlWebpackPlugin()]
};
  1. 项目根目录创建lib文件夹,新建webpack.js:
module.exports = class Webapck {
  constructor(options) {
    // 读取配置文件信息
    console.log(options);
    this.entry = options.entry;
    this.output = options.output;
  }
  // 入口函数:实现编译
  run() {
    this.parser(this.entry);
  }
  /** 编译函数:实现具体编译
   * 分析是否有依赖,获取依赖路径
   * 编译模块生成chunk
   */
  parser() {}
};
  1. 项目根目录创建bundle.js:
const webpack = require("./lib/webpack.js");
const config = require("./webpack.config.js");
// 创建webpack实例并将配置传入实例,执行编译
new webpack(config).run();

三、实现具体编译

  1. 使用编译函数来实现具体编译。
  2. 编译函数的实现:
  • 分析是否有依赖,获取依赖路径。
  • 编译模块生成chunk
  1. 返回:模块路径、模块依赖、chunk
  2. 思路:
  • 分析是否有依赖,要找到文件中的引入依赖的语句,用@babel/parser将文件内容解析成AST树,当语句的节点类型为“ImportDeclaration”时说明是import依赖,依赖路径就存在于节点的source.value字段中。使用@babel/traverse 对 节点类型为“ImportDeclaration”的语句 进行操作,获取到依赖路径并存储起来。
  • 编译模块生成chunk:使用@babel/core插件中的transformFromAst 将ast树转化为js。
  1. 整体代码:
const fs = require("fs");
const path = require("path");
const BabelParser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");

module.exports = class Webapck {
  constructor(options) {
    // 读取配置文件信息
    // console.log(options);
    this.entry = options.entry;
    this.output = options.output;
  }
  // 入口函数:实现编译
  run() {
    this.parser(this.entry);
  }
  /** 编译函数:实现具体编译
   * 分析是否有依赖,获取依赖路径
   * 编译模块生成chunk
   */
  parser(modulePath) {
    // 获取文件内容
    const content = fs.readFileSync(modulePath, "utf-8");
    // 将内容转化成AST树,生成module形式的内容类型
    const ast = BabelParser.parse(content, { sourceType: "module" });

    // 保存路径依赖
    const dependencies = {};
    traverse(ast, {
      ImportDeclaration({ node }) {
        const newPath =
          "." +
          path.sep +
          path.join(path.dirname(modulePath), node.source.value);
        console.log(newPath);
        dependencies[node.source.value] = newPath;
      }
    });
    // 将ast编译成js
    const { code } = transformFromAst(ast, null, {
      presets: ["@babel/preset-env"] //插件
    });
    console.log(code);
    // 返回模块路径、模块依赖、chunk
    return {
      modulePath,
      dependencies,
      code
    };
  }
};

四、收集所有依赖,整理成关系图谱

上面我们由parser方法获取传入的文件的路径、模块依赖和chunk,但我们还需要获取文件的依赖文件的信息,

  1. 在run方法中收集所有依赖,并转化成需要的关系图谱:路径:{依赖, 代码}

  2. 实现:

  • 循环获取所有入口的依赖,对依赖进行编译,获取其路径、依赖、chunk,将其加到依赖数组中;
  • 获取到所有依赖后,进行遍历修改成需要的关系图谱。

3.整体代码:

// 入口函数:实现编译
run() {
    const moduleParserInfo = this.parser(this.entry);
    console.log(moduleParserInfo);
    // 收集所有依赖
    this.modulesInfo.push(moduleParserInfo);

    // 循环获取所有入口的依赖,对依赖进行编译,获取其路径、依赖、chunk,将其加到modulesInfo数组中
    for (let i = 0; i < this.modulesInfo.length; i++) {
      const dependencies = this.modulesInfo[i].dependencies;
      if (dependencies) {
        for (let j in dependencies) {
          this.modulesInfo.push(this.parser(dependencies[j]));
        }
      }
    }
    // console.log(this.modulesInfo);
    // 数据类型转换:转换成  路径:{依赖, 代码}
    const obj = {};
    this.modulesInfo.forEach(item => {
      obj[item.modulePath] = {
        dependencies: item.dependencies,
        code: item.code
      };
    });
    // 生成bundle文件
    this.bundleFile(obj);
}
  1. 关系图谱的内容

image.png

五、生成bundle文件

  1. 在bundleFile方法中生成bundle文件。

  2. 实现:

  • 生成文件是使用node.js的fs.writeFileSync(bundlePath, content, "utf-8")方法实现的,需要提供打包路径和打包内容。
  • 打包路径是根据配置文件webpack.config.js中的output配置项获得的。
  • 打包内容包括打补丁内容和run方法中生成的关系图谱。关系图谱中的代码无法正常运行,因为缺失了requireexports方法,所以打补丁这两个方法。
  • parser方法中分析出的coderequire引入的依赖路径是相对入口模块的路径,此时我们的代码在dist文件夹下,所以依赖路径要改成相对项目根节点下的路径。所以在检测到代码中有require时写一个newRequire方法来替换它,再执行代码。代码执行过程中会有exports模块,所以我们先设定一个exports对象,执行代码过程中会逐渐将exports模块添加到exports对象中,最后返回exports对象

3.整体代码:

// 生成bundle文件
bundleFile(obj) {
    // 根据配置中的output配置项合成打包文件的地址
    const bundlePath = path.join(this.output.path, this.output.filename);
    // 序列化关系图谱
    const dependenciesInfo = JSON.stringify(obj);
    // 打包的内容
    const content = `(function(modulesInfo) {
        // 打补丁,补上缺失的require和exports方法
        function require(modulePath) {
            // 定义一个新的require:将相对入口模块的路径转换成相对项目根节点下的路径
            function newRequire(relativePath) {
                return require(modulesInfo[modulePath].dependencies[relativePath])
            }
            const exports = {};
            (function(require, code){
                eval(code)
            })(newRequire, modulesInfo[modulePath].code)
            console.log(exports)
            return exports;
        }
        require('${this.entry}')
    })(${dependenciesInfo})`;
    // 生成文件
    fs.writeFileSync(bundlePath, content, "utf-8");
}