6-4~7 Bundler 源码编写

2020-06-08 11:26:31 浏览数 (1)

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

0 人点赞