# 一、开始

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

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

# 二、AST

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

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

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

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 Explorer (opens new window)esprima (opens new window)

# 三、V0.3.1的打包原理

入口文件是src/rollup.js (opens new window),其实例化了一个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的使用方式举例如下:

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

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

# 1. build

bundle.build (opens new window)方法中,调用了this.fetchModule (opens new window),其主要是读取文件(模块)内容,然后实例化一个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 (opens new window)的构造函数中,对传入的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 (opens new window)方法中对astbody进行了遍历,判断如果存在import或者export语句,将其放到this.importsthis.exports中。

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

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

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

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

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

# 2. generate

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

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

# 四、流程图

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

# 五、总结

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

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

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

# 六、相关资料

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