手写 Webpack Bundler,深入浅出理解打包流程

628 阅读1分钟

话不多说,直接上代码吧!

准备工作

bundler项目目录结构如下:

  • src
    • index.js
    • message.js
    • word.js
  • bundler.js

src/index.js

import message from "./message.js";

console.log(message);

src/message.js

import { world } from './word.js'

const message = `say ${world}`

export default message

src/word.js

export const world = 'hello';

以上是 src 目录下对应文件中的代码。

解析入口文件

项目进行打包,首先要读取项目入口文件,并且对入口文件分析。
分析文件,则需要先拿到文件中的内容以及依赖。

const fs = require("fs"); 
const paser = require("@babel/parser");

const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename, "utf-8"); // 读取出的文件设置为utf-8
  // 生成抽象语法树 AST
  const ast = paser.parse(content, {
    sourceType: "module",
  });
  // 通过抽象语法树分析代码,查看对应的依赖关系
  console.log(ast.program.body);
};

moduleAnalyser("./src/index.js");

运行 node bundler.js 命令查看 index.js 相关代码 image.png

接下来对 AST 抽象语法树进行遍历,找出 “引入” 的语法,同时转换为浏览器可识别的代码。

const fs = require("fs");
const path = require("path");
const paser = require("@babel/parser");
// 默认导出是ES Module的导出,使用 export/import,需要.default
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");

const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename, "utf-8"); // 读取出的文件设置为utf-8
  // 生成抽象语法树 AST
  const ast = paser.parse(content, {
    sourceType: "module",
  });
  // 对抽象语法树进行遍历,需要找出 type: 'ImportDeclaration' 这样的元素
  const dependencies = {};
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename);
      // 转换成基于bundler相对路径,打包时才不会出错
      const newFile = "./" + path.join(dirname, node.source.value); 
      dependencies[node.source.value] = newFile;
    },
  });

  // 将抽象语法树转换为浏览器可识别的代码
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
  });

  return {
    filename,
    dependencies,
    code,
  };
};

const moduleInfo = moduleAnalyser("./src/index.js");
console.log(moduleInfo);

运行 node bundler.js 命令查看相关输出,理解更加直观!

image.png 以上是对 index.js 入口文件进行的分析

生成依赖图谱

接下来要对项目中所有的模块进行分析。
创建 "依赖图谱",其作用是分析项目中除入口文件外的其它文件信息(模块信息)

const makeDependenciesGraph = (entry) => {
  const entryModule = moduleAnalyser(entry);
  const graphArry = [entryModule];

  // 递归其它模块
  for (let i = 0; i < graphArry.length; i++) {
    const item = graphArry[i];
    const { dependencies } = item;

   if (dependencies) {
      for (let j in dependencies) {
        graphArry.push(moduleAnalyser(dependencies[j]));
      }
    }
  }

  const graph = {};
  graphArry.forEach(({ filename, dependencies, code } = item) => {
    graph[filename] = { dependencies, code };
  });

  return graph;
};

const graghInfo = makeDependenciesGraph("./src/index.js");
console.log(graghInfo);

运行 node bundler.js 命令查看依赖图谱,可以直观看到每个模块中对应的依赖关系。

image.png

生成可执行 JS

其实就是要把依赖图谱中的code放到一起,返回一个可执行的js,其实也就是返回了一个js字符串。
注意:在code中有一个require方法和一个exports对象,如果没定义的话,js执行的时候一定会报错的。 在闭包内以require作入口,再定义一个闭包把各个模块划分开防止内部变量污染。同时我们注意到code中使用的是相对路径,所以定义了一个localRequire基于bundler文件的相对路径的转化,才能找到依赖的模块。

const generateCode = (entry) => {
  const graph = JSON.stringify(makeDependenciesGraph(entry));

  // 避免全局污染 使用闭包
  return `
    (function(graph) {
      function require (module) {
        function localRequire(relativePath) {
          // relativePath 等于 ./message.js 相对路径
          // 根据相对路径找到基于 bundler 文件的相对路径(./src/message.js)
          return require(graph[module].dependencies[relativePath])
        }
        var exports = {};
        // 找到对应的code代码
        (function(require, exports, code) {
          eval(code)
        })(localRequire, exports, graph[module].code)
        return exports;
      };
      require('${entry}')
    })(${graph});
  `;
};

运行 node bundler.js 命令查看相关代码 image.png

image.png