专栏首页coding for love6-4~7 Bundler 源码编写

6-4~7 Bundler 源码编写

1. 简介

学习了前面的内容,我们本节讲一个非常简单的打包工具的实现。

2. 代码准备

我们准备如下三个文件,看看如何将其打包。

// src/index.js
import { sayHello, sayHi } from './say.js';
import message from './message.js';

sayHello(message);
sayHi(message);
// src/say.js
import { hello, hi } from './greeting.js';

export const sayHello = (message) => {
  console.log(`${hello} ${message}`);
};

export const sayHi = (message) => {
  console.log(`${hi} ${message}`);
};
// src/greeting.js
export default 'world';
// src/greeting.js
export const hello = 'hello';
export const hi = 'hi';

3. 模块分析

3.1 获取文件的文本内容

做模块分析,我们首先要获取源码内容。

// bundler.js
const fs = require('fs');

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

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

我们在 cli 运行一下该文件,为了展示更清楚,可以先安装一个包,

npm i cli-highlight -g
node bundler.js | highlight

如下:

可以看到,我们获取到了 src/index.js 中的文件内容。

3.2 利用 babel-parser 将文本转为 ast

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

// 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');

其实,如果大家想方便地查看文本和 ast 对应关系,可以直接访问 astexplorer

3.3 ast 操作和转换成文本

我们要从 ast 获取信息,可以使用 babel-traverse 遍历 ast,这期间会有一些特定的钩子让我们能执行自己的操作。我们在遍历到 import 声明的时候,将 import 的文件名记录到依赖数组。最后我们再利用 babel-core 做源码的 es6 => 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'],
  });
  console.log('======dependencies', dependencies);
  console.log('======code', code);
  return {
    filename,
    dependencies,
    code,
  };
};

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

4. 依赖图谱

前面我们将了如何获取单个文件的依赖和转换成 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');

可以看到这个项目的依赖图谱。

5. 生成代码

// 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);

运行后生成如下代码:

(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":{"./say.js":"./src/say.js","./message.js":"./src/message.js"},"code":"\"use strict\";\n\nvar _say = require(\"./say.js\");\n\nvar _message = _interopRequireDefault(require(\"./message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\n// src/index.js\n(0, _say.sayHello)(_message.default);\n(0, _say.sayHi)(_message.default);"},"./src/say.js":{"dependencies":{"./greeting.js":"./src/greeting.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.sayHi = exports.sayHello = void 0;\n\nvar _greeting = require(\"./greeting.js\");\n\n// src/say.js\nvar sayHello = function sayHello(message) {\n  console.log(\"\".concat(_greeting.hello, \" \").concat(message));\n};\n\nexports.sayHello = sayHello;\n\nvar sayHi = function sayHi(message) {\n  console.log(\"\".concat(_greeting.hi, \" \").concat(message));\n};\n\nexports.sayHi = sayHi;"},"./src/message.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.default = void 0;\n// src/greeting.js\nvar _default = 'world';\nexports.default = _default;"},"./src/greeting.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.hi = exports.hello = void 0;\n// src/greeting.js\nvar hello = 'hello';\nexports.hello = hello;\nvar hi = 'hi';\nexports.hi = hi;"}});

运行上面一段代码:

6. 生成后代码的执行过程分析

这里有些同学可能会对生成后的代码如何执行的过程不太清楚,我们来分析一遍。 step 1 执行 require('./src/index'.js) step 2 (function(require, exports, code) { eval(code); })(localRequire, exports, graph[module].code) 这个闭包函数的执行环境中,require 被定义为 localRequire,而 exports 目前是一个外层定义的空对象 step 3 执行 eval(code),其实就是执行下面这段函数:

‌"use strict";

var _say = require("./say.js");

var _message = _interopRequireDefault(require("./message.js"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

// src/index.js
(0, _say.sayHello)(_message.default);
(0, _say.sayHi)(_message.default);

step4 碰到 require("./say.js") 会执行 localRequire('./say.js'),其实就是重复2,3 步骤执行到:

‌"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.sayHi = exports.sayHello = void 0;

var _greeting = require("./greeting.js");

// src/say.js
var sayHello = function sayHello(message) {
  console.log("".concat(_greeting.hello, " ").concat(message));
};

exports.sayHello = sayHello;

var sayHi = function sayHi(message) {
  console.log("".concat(_greeting.hi, " ").concat(message));
};

exports.sayHi = sayHi;

step5 碰到 require("./greeting.js") 会执行 localRequire('./greeting.js'),重复2,3,如下:

‌"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.hi = exports.hello = void 0;
// src/greeting.js
var hello = 'hello';
exports.hello = hello;
var hi = 'hi';
exports.hi = hi;

这里没有 require 了,会执行到最后,并且在 exports 里面导出模块想要抛出的内容。 step 6 回到 step4 中

var _greeting = require("./greeting.js");

现在 _greeting 就是

{
  hello: 'hello',
  hi: 'hi',
}

继续向下执行到代码结尾。exports 中抛出 sayHello 和 sayHi。 step7 回到 step3 中,_say 就是前面导出的 sayHello 和 sayHi 组成的对象。再往下,遇到 require("./message.js") 是同样的流程。 直到 index 中代码执行完毕。

7. 小结

本节只是演示了一个非常基本的打包器实现,其中很多功能我们都没去实现,比如遇到重复引用,循环引用等该怎么处理。

参考

docs/babel-parser docs/babel-traverse docs/babel-core

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 在线商城项目03-启用mock服务

    对于前后端分离的开发,在后台接口还未就绪时,前端需要使用mock数据进行开发。最容易想到的办法,当然是把mock数据写在页面里,但是这会让我们的页面代码很臃肿,...

    love丁酥酥
  • JS原生引用类型解析6-Boolean类型

    (注1:如果有问题欢迎留言探讨,一起学习!转载请注明出处,喜欢可以点个赞哦!) (注2:更多内容请查看我的目录。)

    love丁酥酥
  • 浏览器加载解析渲染机制的全面解析

    (注1:如果有问题欢迎留言探讨,一起学习!转载请注明出处,喜欢可以点个赞哦!) (注2:更多内容请查看我的目录。)

    love丁酥酥
  • Qt官方示例解析-Address Book-基于单个数据模型在不同视图呈现不同数据

    提要:Qt的这个示例主要讲的是使用代理模型,实现在不同的视图上面显示单个数据模型的数据 这个示例提供了一个地址簿,将联系人按照名称字母{"ABC", "DEF...

    Sky_Mao
  • 用机器学习加速你的网站

    我一生中大约73%的时间都在思考网络性能:如何在慢速手机上能播放60FPS的画面,用完美的顺序加载资源,通过离线缓存能做的一切。等等等等。

    疯狂的技术宅
  • C++之const

    程序手艺人
  • 实现小型打包工具

    hey,各位宝宝,最近的疫情很严重,大家尽量就不要到外面浪了,好好在家做个安静的宝宝吧。不得不出门时也一定要戴口罩哦!照顾好自己,望平安... ...

    用户3258338
  • C#_FindWindow

    landv
  • 我在大厂写React,学到了什么?

    我工作中的技术栈主要是 React + TypeScript,这篇文章我想总结一下如何在项目中运用 React 的一些技巧解决一些实际问题,本文中使用的代码都是...

    ssh1995
  • ES6 你可能不知道的事 – 基础篇

    ES6,或许应该叫 ES2015(2015 年 6 月正式发布),对于大多数前端同学都不陌生。

    用户4962466

扫码关注云+社区

领取腾讯云代金券