跳转至

浅析Rollup打包原理

一、开始

本次Rollup打包原理分析是v0.3.1版本,因其代码精简,易读性高,所以用它来初探打包器的工作原理。

Rollup中,一个文件就是一个Module(模块),每一个模块都会生成AST语法抽象树,Rollup会对每个AST节点进行分析。

二、AST

Rollup打包的核心是对AST的解析和处理,要想深入理解打包原理,必须深入理解AST

Rollup是通过acorn这个库进行AST的生成的,acorn的生成结果遵循estree规范。

下面是一个通过acorn生成的AST例子,来直观感受下。

1
2
3
4
5
6
7
const { parse } = require('acorn');
const code = `import { a } from './a.js'`

console.log(parse(code, {
  ecmaVersion: 7,
  sourceType: 'module',
}))

生成的AST结果:

{
  type: 'Program',
  start: 0,
  end: 26,
  body: [
    {
      type: 'ImportDeclaration',
      start: 1,
      end: 26,
      specifiers: [
        {
          type: 'ImportSpecifier',
          start: 9,
          end: 10,
          imported: {
            type: 'Identifier',
            start: 9,
            end: 10,
            name: 'a',
          },
          local: {
            type: 'Identifier',
            start: 9,
            end: 10,
            name: 'a',
          },
        },
      ],
      source: {
        type: 'Literal',
        start: 18,
        end: 26,
        value: './a.js',
        raw: '\'./a.js\'',
      },
    },
  ],
  sourceType: 'module',
};

AST的基本结构是一个个Node节点,其必包含typeloc(start/end)信息。typeProgramNode节点表明这是段程序,通常是脚本的最外层,body是其属性。

这里有几个网站可以在线查看AST结构:AST Exploreresprima

三、V0.3.1的打包原理

入口文件是src/rollup.js,其实例化了一个Bundle,调用其build方法后,返回了一个对象,包含generatewrite方法。

export function rollup(entry, options = {}) {
  const bundle = new Bundle({
    entry,
    esolvePath: options.resolvePath
  }) 
  return bundle.build().then(() => {
    return {
      generate: options => bundle.generate(options),
      write: (dest, options = {}) => {
        let { code, map } = bundle.generate({
                    dest,
                    format: options.format,
                    globalName: options.globalName
                });

                return Promise.all([
                    writeFile( dest, code ),
                    writeFile( dest + '.map', map.toString() )
                ]);
      }
    }
  })
}

注意这个版本的Rollup的使用方式举例如下:

1
2
3
rollup('entry.js').then((res) => {
  res.wirte('bundle.js')
})

可以看出其主要分为两步,第一步是通过build构建,第二步是generate或者write进行输出。genenrate是把code返回,write是把code写入文件中。

1. build

bundle.build方法中,调用了this.fetchModule,其主要是读取文件(模块)内容,然后实例化一个Module

class Bundle {
  build() {
    return this.fetchModule(this.entryPath)
      .then(entryPath => {
        this.entryModule = entryModule;
        return entryModule.expandAllStatements(true);
      })
      .then((statements) => {
        this.statements = statements;
        this.deconflict();
      });
  }

  fetchModule() {
    // ...

    const module = new Module({
      code,
      path: route,
      bundle: this,
    });

    // ...
  }
}

Module的构造函数中,对传入的code(文件内容)通过acorn.parse进行了解析,获取了AST,然后调用了this.analyse

class Module {
  constructor({ code, path, bundle }) {
    this.code = new MagicString(code, {
      filename: path
    })
    this.ast = acorn.parse(code, {
      ecmaVersion: 6,
      sourceType: 'module',
    })
  }
  this.analyse();
}

moduleanalyse方法中对astbody进行了遍历,判断如果存在import或者export语句,将其放到this.importsthis.exports中。

然后调用了ast目录下的analyse方法,为每个node节点增加了_scope(作用域)、_defines(定义变量)、_modifies(修改的)、_dependsOn(依赖)等变量。

回到bundle.build方法中,在调用fetchModule获取到entryModule(入口模块)后,然后调用了entryModule.expandAllStatements方法。其遍历了ast.body,调用expandStatement去收集所有的语句。

expandStatement根据节点的_dependsOn属性,找到其依赖,然后依次调用define方法。define方法中根据导入模块的路径去调用fetchModule,实现了递归引入依赖,构建了一个大的模块树。

这样所有的声明都被收集到了bundle.statements中,然后调用this.deconflict解决命名冲突。

bundle.build的整体逻辑就是这样,在其过程中会有许多对AST的判断,以区分不同的类型的节点,来进行不同的处理。

2. generate

bundle.generate的方法是相对独立的,其核心思想是对this.statements的拼接。因为所有的语句都已经收集到了statement中,那么拼接起来返回就行了。

在拼接过程中过滤了export语句,并处理了一些import语句,然后返回了code

四、流程图

画了一张流程图,来加深理解:

五、总结

上面就是Rollup基本的打包原理,简单的总结下就是从入口文件开始构建AST、收集依赖,最后拼接所有语句,输出bundle

现在的最新版本V2.59.0比其多出的功能包括:watchplugin、命令行参数等。

Rollup的插件系统其实就是在buildgenerate的不同生命周期阶段额外做的一些事情,其英文文档非常详细,建议直接阅读文档。

六、相关资料

  1. Rollup文档
  2. 文章中的例子
  3. estree
  4. 使用Acorn来解析JavaScript
  5. 从rollup初版源码学习打包原理
  6. 原来rollup这么简单之rollup.rollup篇