前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从minipack看打包原理

从minipack看打包原理

作者头像
不作声
发布2020-07-06 11:37:42
5020
发布2020-07-06 11:37:42
举报
文章被收录于专栏:M不作声M不作声

从minipack看打包原理

前端有很多的打包工具如webpack等,但是打包工具的原理是什么呢?

minipack是一个小型的打包工具,作者ronami,用来解析打包工具的基本原理。代码中有相当多的注释,理解起来也非常容易。

先放其中测试的代码文件,代码文件只有三个,每个都只有一两行代码:

代码语言:javascript
复制
// entry.js
import message from './message.js';

console.log(message);

// message.js
import {name} from './name.js';

export default `hello ${name}!`;

// name.js
export const name = 'world';

在模块化编程中,开发人员将程序分解为离散的功能块,称为模块

三个代码文件就是三个模块,它们之间存在依赖关系。

然后是正文,这是实现打包功能用到的库:

代码语言:javascript
复制
// fs读取文件
const fs = require('fs');
// path库解析文件路径
const path = require('path');
// babylon用于AST解析,构造AST语法树
const babylon = require('babylon');
// travers对AST语法树进行遍历
const traverse = require('babel-traverse').default;
// transformFromAst将语法树转换为代码
const {transformFromAst} = require('babel-core');

createAsset获取模块信息

第一个函数是createAsset()函数,获取模块的信息。主要包括四个部分:

  • id:每个模块的唯一标识符;
  • filename:模块的文件名;
  • dependencies:模块的依赖列表,数据结构为数组;
  • code:模块中的代码。
代码语言:javascript
复制
let ID = 0;

// 接受一个文件参数,为模块创建一个抽象语法树,
// 遍历该树,得到模块的信息对象,属性包括id,文件名,依赖,代码
function createAsset(filename) {
  // 获取文件内容,编码格式utf-8
  const content = fs.readFileSync(filename, 'utf-8');

  // JavaScript解析器会生成抽象语法树
  const ast = babylon.parse(content, {
    sourceType: 'module',
  });

  const dependencies = [];
  // 遍历抽象语法树,从导入声明中获取依赖列表
  traverse(ast, {
    ImportDeclaration: ({node}) => {
      dependencies.push(node.source.value);
    },
  });

  // 获取id
  const id = ID++;

  // 从AST解析获取代码
  const {code} = transformFromAst(ast, null, {
    presets: ['env'],
  });

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

如entry.js文件经过该函数处理后,会得到以下数据:

代码语言:javascript
复制
{
  id: 0,
  filename: './example/entry.js',
  dependencies: ['./message.js'],
  code: '"use strict";\n' +
    '\n' +
    'var _message = require("./message.js");\n' +
    '\n' +
    'var _message2 = _interopRequireDefault(_message);\n' +
    '\n' +
    'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n' +
    '\n' +
    'console.log(_message2.default);'
}

构建依赖图

第二个函数createGraph()接受一个文件,从该文件开始向前遍历,直到处理完所有的模块。

代码语言:javascript
复制
// 函数接受一个入口文件,从入口文件开始,向前递归寻找依赖文件,最后返回一个包含所有模块的数组
function createGraph(entry) {
  // 从入口文件开始获取依赖项
  const mainAsset = createAsset(entry);

  // 创建一个数组类型的队列,起始队列中只有入口文件一个元素
  const queue = [mainAsset];
  // 使用for..of...循环遍历队列,添加一个mapping对象,将依赖项的相对地址改为绝对地址
  for (const asset of queue) {
    asset.mapping = {};

    const dirname = path.dirname(asset.filename);
    // 将每个依赖项的相对地址转为绝对地址,获取到依赖项的全部信息后,
    asset.dependencises.forEach(relativePath => {
      const absolutePath = path.join(dirname, relativePath);

      const child = createAsset(absolutePath);

      // 为mapping添加relativePath属性,属性值为依赖项的id
      asset.mapping[relativePath] = child.id;
      // 将child推到依赖项队列中
      queue.push(child);
    });
  }

  return queue;
}

在第二个函数中,为模块对象添加了一个mapping属性,用于保存依赖的模块的相对路径和模块id的映射。如{‘./message.js’: 1}。遍历完成后,函数返回一个依赖图数组。返回结果大致如下:

代码语言:javascript
复制
[
  {
    id: 0,
    filename: './example/entry.js',
    dependencies: [ './message.js' ],
    code: '',
    mapping: { './message.js': 1 }
  },
  {
    id: 1,
    filename: 'example/message.js',
    dependencies: [ './name.js' ],
    code: '',
    mapping: { './name.js': 2 }
  },
  {
    id: 2,
    filename: 'example/name.js',
    dependencies: [],
    code: '',
    mapping: {}
  }
]

mapping属性解决的问题是,当依赖列表中出现相同的文件时,可以使用唯一标识符id进行区分。

bundle函数

bundle()函数首先对参数进行处理,对每一个模块进行处理,将所有的模块转换成key:value形式,key为模块的唯一标识符id,value是一个二值数组,第一个值是模块的代码,第二个值是mapping。代码如下:

代码语言:javascript
复制
function bundle(graph) {

  let modules = '';

  graph.forEach(mod => {

    modules += `${mod.id}: [
      function (require, module, exports) {
        ${mod.code}
      },
      ${JSON.stringify(mod.mapping)},
    ],`;
  });

  const result = `
    (function(modules) {
        function require(id) {
          const [fn, mapping] = modules[id];

          function localRequire(name) {
            return require(mapping[name]);
          }

          const module = { exports : {} };

          fn(localRequire, module, module.exports);

          return module.exports;
        }

		require(0);
      })({${modules}})
    `;
  return result;
}

const graph = createGraph('./example/entry.js');
const result = bundle(graph);

console.log(result);

中间这段代码与webpack的runtime函数很像。Runtime函数帮助模块顺利地执行模块的导入、导出和执行。

这里的runtime函数,接受依赖图作为参数,但是数据结构已经不同。runtime定义了一个require函数,运行require(0),表示从入口文件开始解析,由于id具有唯一性,所以将id作为参数。

为了实现模块化,runtime构造了函数作用域,模块内的代码被包裹在函数内。minipack项目运行结果中modules为:

代码语言:javascript
复制
{0: [
      function (require, module, exports) {
        "use strict";

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

		var _message2 = _interopRequireDefault(_message);

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

		console.log(_message2.default);
      },
      {"./message.js":1},
    ],
 1: [
      function (require, module, exports) {
        "use strict";

		Object.defineProperty(exports, "__esModule", {
  			value: true
		});

		var _name = require("./name.js");

		exports.default = "hello " + _name.name + "!";
      },
      {"./name.js":2},
    ],
 2: [
      function (require, module, exports) {
        "use strict";

		Object.defineProperty(exports, "__esModule", {
  		value: true
		});
		var name = exports.name = 'world';
      },
      {},
    ],}

总结

打包的基本过程为:

  • 从entry开始生成AST,从导入声明中获取依赖列表
  • 获取entry模块的全部信息
  • 对entry的依赖文件重复上述操作,直到遍历完成
  • 生成依赖图数组
  • 构建runtime函数
  • 将依赖图传递给runtime函数,生成代码
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020.06.23,如有侵权请联系 cloudcommunity@tencent.com 删除

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 从minipack看打包原理
    • createAsset获取模块信息
      • 构建依赖图
        • bundle函数
          • 总结
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档