前端打包分析

10,837 阅读2分钟

人话系列

打包:将多个小模块整合成一个大模块

优点是:方便,快捷,易整理

缺点: 体积变大

本质是递归调用

关键词

依赖关系,模块系统,入口文件,引导程序

科学点的叫话:模块捆绑器

入口文件

模块捆绑器具有 入口文件 的这种概念,而不是添加一些脚本标签在浏览器中并让它们运行,我们让 捆绑器 知道哪个文件 是我们应用程序的 主要文件, 该文件能引导我们的整个应用程序

依赖关系

我们的打包程序将从该 入口文件 开始,并尝试理解它依赖于那些文件。 然后, 它会尝试了解哪些文件依赖关系取决于它, 它会继续这样做,直到它发现我们应用程序中的 每个模块,以及它们如何 相互依赖 -- 这种对项目的理解被称为 依赖图

模块系统

每个导入项的code就是一个模块,每一个模块之间的相互依赖,构成依赖图

引导程序

解析依赖图,保证执行顺序跟预期相符

方法

  1. 读取入口文件,解析成 ast
  2. 读取 ast,把import等导入文件,构建成依赖关系,(需要es互转的也可以在这里转换)
  3. 创建依赖图:根据第二步的ast 导入声明的依赖关系,再循环处理下 ast 解析,将其挂载到对应父集中
  4. 创建 bundle: 构建模块,填充解析过后的code, 并填充依赖mapping
  5. 添加引导程序:根据创建的bundle,顺序执行mapping 映射

实际生成的bundle 例子

(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);
})({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';
    // 这里是利用对象key定义方法,将 world 值传递到 1中的 name变量中去
  },
  {}
],})

基础转换函数例子

const fs = require('fs');
const path = require('path');
const babylong = require('babylon');
const traverse = require('babel-traverse').default;
const {transformFromAst} = require('babel-core');
​
let count = 0;
function createAsset(filename) {
  const content = fs.readFileSync(filename, 'utf-8');
  const ast = babylong.parse(content, {
    sourceType: 'module',
  });
​
  const dependencies = [];
  traverse(ast, {
    ImportDeclaration: ({node}) => {
      dependencies.push(node.source.value);
    },
  });
​
  const id = count++;
​
  const {code} = transformFromAst(ast, null, {
    presets: ['env'],
  });
​
  return {
    id,
    filename,
    dependencies,
    code,
  };
}
​
function createGraph(entry) {
  const mainAsset = createAsset(entry)
console.log('mainAsset', mainAsset);
  const queue = [mainAsset];
  for(const asset of queue) {
    asset.mapping = {}
    const dirname = path.dirname(asset.filename)
    asset.dependencies.forEach(relativePath => {
      const absolutePath = path.join(dirname, relativePath)
      const child = createAsset(absolutePath)
      asset.mapping[relativePath] = child.id
      queue.push(child)
    })
  }
  return queue
}
​
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;
}
​
// console.log(createAsset(path.resolve(__dirname, '../example/entry.js')));
const graph = createGraph(path.resolve(__dirname, '../example/entry.js'));
const result = bundle(graph);
​
console.log(result);