前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >6-4~7 Bundler 源码编写

6-4~7 Bundler 源码编写

作者头像
love丁酥酥
发布2020-06-08 11:26:31
4770
发布2020-06-08 11:26:31
举报
文章被收录于专栏:coding for lovecoding for love

1. 简介

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

2. 代码准备

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

代码语言:javascript
复制
// src/index.js
import { sayHello, sayHi } from './say.js';
import message from './message.js';

sayHello(message);
sayHi(message);
代码语言:javascript
复制
// 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}`);
};
代码语言:javascript
复制
// src/greeting.js
export default 'world';
代码语言:javascript
复制
// src/greeting.js
export const hello = 'hello';
export const hi = 'hi';

3. 模块分析

3.1 获取文件的文本内容

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

代码语言:javascript
复制
// bundler.js
const fs = require('fs');

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

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

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

代码语言:javascript
复制
npm i cli-highlight -g
代码语言:javascript
复制
node bundler.js | highlight

如下:

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

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

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

代码语言:javascript
复制
// 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 的转换。

代码语言:javascript
复制
// 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 的代码,这里我们讲一下如何对所有以来的文件做分析,生成一个依赖图谱。

代码语言:javascript
复制
// 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. 生成代码

代码语言:javascript
复制
// 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);

运行后生成如下代码:

代码语言:javascript
复制
(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),其实就是执行下面这段函数:

代码语言:javascript
复制
‌"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 步骤执行到:

代码语言:javascript
复制
‌"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,如下:

代码语言:javascript
复制
‌"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 中

代码语言:javascript
复制
var _greeting = require("./greeting.js");

现在 _greeting 就是

代码语言:javascript
复制
{
  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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 简介
  • 2. 代码准备
  • 3. 模块分析
    • 3.1 获取文件的文本内容
      • 3.2 利用 babel-parser 将文本转为 ast
      • 3.3 ast 操作和转换成文本
      • 4. 依赖图谱
      • 5. 生成代码
      • 6. 生成后代码的执行过程分析
      • 7. 小结
      • 参考
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档