Bundler源码从0分析

一、什么是Bunlder?

Bundler其实是一个项目依赖管理器。

二:被打包的项目介绍

整个演示项目的目录结构如下所示:

├── bundler.js
├── package.json
└── src
    ├── index.js
    ├── msg.js
    └── word.js

word.js


const word = 'world';
export default word;

msg.js


import word from './word.js'
const msg = `hello ${word}`;
export default msg;

index.js


import msg from './msg.js'
console.log(msg)
export default index;

三、实现bundler.js

要实现bundler,我们需要实现3部分功能:
1. moduleAnalyser:模块分析。分析模块,得到模块的依赖、代码等信息。
2. makeDependenciesGraph:生成依赖图谱。遍历打包项目,得到所有需要的模块的分析结果 。
3. generateCode:生成可执行代码。提供require()函数和exports对象,生成可以在浏览器执行的代码。

1、模块分析

使用fs模块读取module的内容;使用@babel/parser将文件内容转换成抽象语法树AST;使用遍历了@babel/traverseAST ,对每个ImportDeclaration节点(保存的相对于module的路径信息)做映射,把依赖关系拼装在 dependencies对象里;使用@babel/core结合@babel/preset-env预设,将AST转换成了浏览器可以执行的代码。

1.1 获取文件的文本内容
// bundler.js

const fs = require('fs');
const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename, 'utf-8');
  console.log(content);
};
moduleAnalyser('./src/index.js');

打印结果:
在这里插入图片描述
为了更清楚展示,我们对代码进行高亮,需要安装一个依赖 npm i cli-highlight -g
执行命令:
node bunlder.js | highlight

在这里插入图片描述

1.2、利用 babel-parser 将文本转为 ast

我们获取到了文本以后,如果直接就拿来分析依赖当然也可以,但是处理起来非常麻烦,效率也低下,尤其是文件内容复杂的时候。所以我们需要将文本转化为 js 可直接操作的对象 ast。
前面我们讲到了 babel,它可以将 js 源文件根据我们的需要做内容变更,比如将我们的 es6 编写的源文件转成 es5,其实就是将我们的源文件内容先转为 ast 再去实现后续变更的。它有一个专门负责转换的模块,叫做 baben/parser,前身是 babylon。

先安装依赖 npm install @babel/parser --save

// bundler.js

const fs = require('fs');
const parser = require('@babel/parser');

const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename, 'utf-8');
  console.log(parser.parse(content, {
    sourceType: 'module',
  }));
};
moduleAnalyser('./src/index.js');

执行 node bunlder.js | highlight

在这里插入图片描述

1.3、ast 操作和转换成文本

我们要从 ast 获取信息,可以使用 babel-traverse 遍历 ast,这期间会有一些特定的钩子让我们能执行自己的操作。我们在遍历到 import 声明的时候,将 import 的文件名记录到依赖数组。最后我们再利用 做源码的 babel-core es6 => es5 的转换。
先安装这两个依赖:
npm install @babel/core --save
npm install @babel/preset-env --save

此时代码如下:

// bundler.js

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

const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename, 'utf-8');
  const ast = parser.parse(content, {
    sourceType: 'module',
  });
  const dependencies = {};
  const dirPath = path.dirname(filename);
  traverse(ast, {
    ImportDeclaration({ node }) {
      const fileRelativePath = node.source.value;
      const srcRelativePath = `./${path.join(dirPath, fileRelativePath)}`;
      dependencies[fileRelativePath] = srcRelativePath;
    },
  });
  const { code } = babel.transformFromAst(ast, null, {
    presets: ['@babel/preset-env'],
  });
  console.log('======dependencies', dependencies);
  console.log('======code', code);
  return {
    filename,
    dependencies,
    code,
  };
};
moduleAnalyser('./src/index.js');

执行 node bunlder.js | highlight

在这里插入图片描述

2、依赖图谱

前面我们将了如何获取单个文件的依赖和转换成 es5 的代码,这里我们讲一下如何对所有以来的文件做分析,生成一个依赖图谱。

// bundler.js

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

// 模块分析
const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename, 'utf-8');
  const ast = parser.parse(content, {
    sourceType: 'module',
  });
  const dependencies = {};
  const dirPath = path.dirname(filename);
  traverse(ast, {
    ImportDeclaration({ node }) {
      const fileRelativePath = node.source.value;
      const srcRelativePath = `./${path.join(dirPath, fileRelativePath)}`;
      dependencies[fileRelativePath] = srcRelativePath;
    },
  });
  const { code } = babel.transformFromAst(ast, null, {
    presets: ['@babel/preset-env'],
  });
  return {
    filename,
    dependencies,
    code,
  };
};

// 生成依赖图谱。这里用动态数组方式实现,也可以用递归实现。
const makeDependenciesGraph = (entry) => {
  const entryModule = moduleAnalyser(entry);
  const graphArr = [entryModule];
  for (let i = 0; i < graphArr.length; i++) {
    const { dependencies } = graphArr[i];
    if (dependencies) {
      Object.keys(dependencies).forEach((name) => {
        graphArr.push(moduleAnalyser(dependencies[name]));
      });
    }
  }
  // 依赖数组转为一个对象,方便操作
  const graph = {};
  graphArr.forEach((item) => {
    const { filename, dependencies, code } = item;
    graph[filename] = {
      dependencies,
      code,
    };
  });
  console.log(graph);
  return graph;
};
makeDependenciesGraph('./src/index.js');

执行 node bunlder.js | highlight

在这里插入图片描述

3、生成代码

// bundler.js

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

// 模块分析
const moduleAnalyser = (filename) => {
  const content = fs.readFileSync(filename, 'utf-8');
  const ast = parser.parse(content, {
    sourceType: 'module',
  });
  const dependencies = {};
  const dirPath = path.dirname(filename);
  traverse(ast, {
    ImportDeclaration({ node }) {
      const fileRelativePath = node.source.value;
      const srcRelativePath = `./${path.join(dirPath, fileRelativePath)}`;
      dependencies[fileRelativePath] = srcRelativePath;
    },
  });
  const { code } = babel.transformFromAst(ast, null, {
    presets: ['@babel/preset-env'],
  });
  return {
    filename,
    dependencies,
    code,
  };
};

// 生成依赖图谱。这里用动态数组方式实现,也可以用递归实现。
const makeDependenciesGraph = (entry) => {
  const entryModule = moduleAnalyser(entry);
  const graphArr = [entryModule];
  for (let i = 0; i < graphArr.length; i++) {
    const { dependencies } = graphArr[i];
    if (dependencies) {
      Object.keys(dependencies).forEach((name) => {
        graphArr.push(moduleAnalyser(dependencies[name]));
      });
    }
  }
  // 依赖数组转为一个对象,方便操作
  const graph = {};
  graphArr.forEach((item) => {
    const { filename, dependencies, code } = item;
    graph[filename] = {
      dependencies,
      code,
    };
  });
  return graph;
};

// 生成代码
const generateCode = (entry) => {
  const graph = JSON.stringify(makeDependenciesGraph(entry));
  return `
    (function(graph){
      function require(module) {
        function localRequire(relativePath) {
          return require(graph[module].dependencies[relativePath]);
        }
        var exports = {};
        (function(require, exports, code) {
          eval(code);
        })(localRequire, exports, graph[module].code)
        return exports;
      }
      require('${entry}');
    })(${graph});
  `;
};
const code = generateCode('./src/index.js');
console.log(code);

执行 node bunlder.js | highlight

在这里插入图片描述
生成的代码:

 (function(graph){
      function require(module) {
        function localRequire(relativePath) {
          return require(graph[module].dependencies[relativePath]);
        }
        var exports = {};
        (function(require, exports, code) {
          eval(code);
        })(localRequire, exports, graph[module].code)
        return exports;
      }
      require('./src/index.js');
    })({"./src/index.js":{"dependencies":{"./msg.js":"./src\\msg.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\nvar _msg = _interopRequireDefault(require(\"./msg.js\"));\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\nconsole.log(_msg[\"default\"]);\nvar _default = index;\nexports[\"default\"] = _default;"},"./src\\msg.js":{"dependencies":{"./word.js":"./src\\word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\nvar _word = _interopRequireDefault(require(\"./word.js\"));\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\nvar msg = \"hello \".concat(_word[\"default\"]);\nvar _default = msg;\nexports[\"default\"] = _default;"},"./src\\word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\nvar word = 'world';\nvar _default = word;\nexports[\"default\"] = _default;"}});

直接复制,在浏览器控制台执行,成功打印出: hello world

在这里插入图片描述

四、小结

这只是简单的基础的打包工具实现,主要技术点在于文件解析,函数回调,递归的使用。仅供参考。

参考链接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值