Skip to content

webpack 打包 bundle 原理分析

js
const compier = webpack(options);
compier.run();

通过 webpack 生命周期概率,可以知道打包大致过程

  • 读取到配置的入口
    • 入口模板的路径
    • 模块分析
      • 模块依赖(依赖的路径)
        • 可以用递归的方式处理依赖模块
      • 内容处理(处理后的内容代码)
      • 依赖图谱对象
    • webpackBootstrap 入口函数
  • 读取到配置的出口
    • 生产文件
      • 文件存放的位置

根据以上的分析,编写一个自定义 webpack 打包文件

编写自定义 webpack 打包项目

新增 webpack.config.js, 跟 webpack 默认配置一样

js
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, './dist'), // 输出的文件目录,必须是绝对路径
    filename: 'main.js', // 输出的文件名称
  },
  mode: 'development',
};

在此之前,先安装 @babel 库

js
npm i -D @babel/parser @babel/traverse @babel/core @babel/preset-env

@babel/parser 来支持解析内容转 ast 树方便解析
@babel/traverse 来可以过滤 ast
@babel/core@babel/preset-envast 转为 js
Ast 抽象语法书:以 node 节点为单位的 tree 树结构

新建 /lib/webpack.js, 打包的核心文件

js
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAst } = require('@babel/core');

module.exports = class webpack {
  constructor(options) {
    // console.log(options)
    const { entry, output } = options;
    this.entry = entry;
    this.output = output;
    this.modules = [];
  }

  run() {
    const info = this.parse(this.entry);

    // 输出依赖图谱
    this.modules.push(info);
    for (let i = 0; i < this.modules.length; i++) {
      const item = this.modules[i];
      const { yilai } = item;

      for (let j in yilai) {
        this.modules.push(this.parse(yilai[j]));
      }
    }

    // 数据结构转换 arr->obj
    const obj = {};
    this.modules.forEach((item) => {
      obj[item.entryFile] = item;
    });

    this.file(obj);
  }

  parse(entryFile) {
    // 读取入口文件
    const content = fs.readFileSync(entryFile, 'utf-8');

    // 解析内容为 ast 树
    const ast = parser.parse(content, { sourceType: 'module' });

    // 过滤 ast 树
    const dirname = path.dirname(entryFile);

    //分析依赖 以及依赖路径
    // 内容处理
    const yilai = {};
    traverse(ast, {
      ImportDeclaration({ node }) {
        const newPathName = './' + path.join(dirname, node.source.value);
        yilai[node.source.value] = newPathName;
      },
    });

    // ast 转 js
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env'],
    });

    return {
      entryFile,
      yilai,
      code,
    };
  }

  file(code) {
    // 生产文件,放入到 dist 的目录
    const filePath = path.join(this.output.path, this.output.filename);

    // 生产 bundle 文件内容
    const newCode = JSON.stringify(code);

    const bundle = `(function (modules) {
      function require(module) {
        function pathRequire(relaPath) {
          // relaPath: ./a.js -> ./src/a.js
          return require(modules[module].yilai[relaPath])
        }
        const exports = {};
        (function (require,exports, code) {
          eval(code)
        })(pathRequire,exports, modules[module].code)
        
        return exports
      }

      require('${this.entry}')
    })(${newCode})`;

    // webpackBootstrap
    fs.writeFileSync(filePath, bundle, 'utf-8');
  }
};

以下测试代码,新建 src/b.js

js
export const strb = 'b.js';

新建 src/a.js

js
import { strb } from './b.js';
export const stra = 'a.js' + strb;

新建入口文件 src/index.js

js
import { stra } from './a.js';
console.log(stra + 'index.js');

最后新建打包文件 bundle.js

js
const webapck = require('./lib/webpack');
const options = require('./webpack.config');

new webapck(options).run();

执行 node bundle.js, 即可生产 ./dist/main.js

js
(function (modules) {
  function require(module) {
    function pathRequire(relaPath) {
      // relaPath: ./a.js -> ./src/a.js
      return require(modules[module].yilai[relaPath]);
    }
    const exports = {};
    (function (require, exports, code) {
      eval(code);
    })(pathRequire, exports, modules[module].code);

    return exports;
  }

  require('./src/index.js');
})({
  './src/index.js': {
    entryFile: './src/index.js',
    yilai: { './a.js': './src\\a.js' },
    code: '"use strict";\n\nvar _a = require("./a.js");\n\nconsole.log(_a.stra + \'index.js\');',
  },
  './src\\a.js': {
    entryFile: './src\\a.js',
    yilai: { './b.js': './src\\b.js' },
    code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.stra = void 0;\n\nvar _b = require("./b.js");\n\nvar stra = \'a.js\' + _b.strb;\nexports.stra = stra;',
  },
  './src\\b.js': {
    entryFile: './src\\b.js',
    yilai: {},
    code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.strb = void 0;\nvar strb = \'b.js\';\nexports.strb = strb;',
  },
});

运行以上代码结果

js
a.jsb.jsindex.js;

基于 MIT 许可发布