# Rollup
# Rollup是什么?
Rollup 是一个 JavaScript 模块打包器,一般用于打包库文件。Rollup带有 Tree-shaking 的能力,使得我们的代码更加简洁清爽。
# Tree-shaking
从字面意思理解,就是"摇树", 也被称为 "live code inclusion," 它是清除实际上并没有在给定项目中使用的代码的过程,但是它可以更加高效。对于Rollup来说,它可以静态分析代码中的 import,并排除任何未曾使用的代码。
这里举一个小例子:
在使用CommonJS时候,必须导入(import)完整的工具(tool)或库(library)对象。
// 使用 CommonJS 导入(import)完整的 utils 对象
var utils = require('utils');
var query = 'Rollup';
// 使用 utils 对象的 ajax 方法
utils.ajax('https://api.example.com?search=' + query).then(handleResponse);
 2
3
4
5
但是在ES6模块时,无需导入整个utils对象,我们可以只导入(import)我们所需的 ajax 函数:
// 使用 ES6 import 语句导入(import) ajax 函数
import { ajax } from 'utils';
var query = 'Rollup';
// 调用 ajax 函数
ajax('https://api.example.com?search=' + query).then(handleResponse);
 2
3
4
5
# 使用rollup
首先确保你的电脑安装了node环境,我们使用npm就可以安装rollup。首先创建一个npm的开发环境。
# 安装学习中使用到的依赖
终端中执行下述命令:
# 新建一个文件夹
mkdir rollup-learn
# 初始化 package.json
npm init -y
# 安装rollup 依赖
npm i @babel/core @babel/preset-env  @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-typescript lodash rollup rollup-plugin-babel postcss rollup-plugin-postcss rollup-plugin-terser tslib typescript rollup-plugin-serve rollup-plugin-livereload -D
 2
3
4
5
6
7
8
# 前置知识:
常见的模块化规范:
- Asynchronous Module Definition 异步模块定义
 - ES6 module是es6提出了新的模块化方案
 - IIFE(Immediately Invoked Function Expression) 即立即执行函数表达式,所谓立即执行,就是声明一个函数,声明完了立即执行
 - UMD全称为 Universal Module Definition 也就是通用模块定义
 - cjs是nodejs采用的模块化标准,commonjs使用方法 require 来引入模块,这里 require() 接收的参数是模块名或者是模块文件的路径
 
# rollup的配置文件 rollup.config.js
和webpack的配置文件命名非常类似rollup.config.js 使用ESM的模块规范编写
export default {
  input: 'src/main.js', // 入口文件
  output: {
    file: 'dist/bundle.cjs.js', // 输出文件的路径和名称
    format: 'cjs', // 五种输出格式:amd/es6/iife/umd/cjs
    name: 'rollup-bundleName' 
  }
}
 2
3
4
5
6
7
8
当format为iife和umd时必须提供,将作为全局变量挂在window下。
新建 src/main.js
console.log('hello');
 新建 dist\index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>rollup</title>
</head>
<body>
  <script src="./bundle.cjs.js"></script>
</body>
</html>
 2
3
4
5
6
7
8
9
10
11
12
# 支持babel
为了使用新的语法,可以使用babel来进行编译输出
为了支持,babel,我们需要装babel依赖的包:
- @babel/core babel的核心包, 所有的核心Api都在这个库里
 - @babel/preset-env 这是一个预设的插件集合,包含了一组相关的插件, Bable中是通过各种插件来指导如何进行代码转换。该插件包含所有es6转化为es5的翻译规则。
 - @rollup/plugin-babel babel插件 (opens new window)
 
@rollup/plugin-babel 官方文档中介绍, 是rollup和babel之间一个无缝衔接的插件。
修改main.js 文件:
let sum = (a,b)=>{
  return a+b;
}
let result = sum(1,2);
console.log(result);
 2
3
4
5
添加 .babelrc 文件:
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false
      }
    ]
  ]
}
 2
3
4
5
6
7
8
9
10
更新配置文件:
import babel from "@rollup/plugin-babel"
export default {
  input: "src/main.js",
  output: {
    file: "dist/bundle.cjs.js", //输出文件的路径和名称
    format: "cjs", //五种输出格式:amd/es6/iife/umd/cjs
    name: "rollup-bundleName", //当format为iife和umd时必须提供,将作为全局变量挂在window下
  },
  plugins: [
    babel({
      babelHelpers: 'bundled', // 这个配置babel希望被显示的配置上去
      exclude: "node_modules/**",
    }),
  ],
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
重新编译, 变成了普通函数。
'use strict';
// 最简单的打包配置
// console.log("hello rollup");
// 使用新的语法,用babel来转义
var sum = function sum(a, b) {
  return a + b;
};
var result = sum(1, 2);
console.log(result);
 2
3
4
5
6
7
8
9
10
# 使用第三方模块
rollup编译源码中的模块引用,默认只支持ESM的模块加载方式 import/export
# 安装依赖
npm install @rollup/plugin-node-resolve @rollup/plugin-commonjs  lodash  --save-dev
 更新配置文件:
import babel from "@rollup/plugin-babel"
import resolve from "@rollup/plugin-node-resolve"
import commonjs from "@rollup/plugin-commonjs"
export default {
  input: "src/main.js",
  output: {
    file: "dist/bundle.cjs.js", //输出文件的路径和名称
    format: "cjs", //五种输出格式:amd/es6/iife/umd/cjs
    name: "rollup-bundleName", //当format为iife和umd时必须提供,将作为全局变量挂在window下
  },
  plugins: [
    babel({
      babelHelpers: "bundled", // 这个配置babel希望被显示的配置上去
      exclude: "node_modules/**",
    }),
    resolve(),
    commonjs(),
  ],
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
安装完毕之后,src\main.js中引用 lodash 看看效果。
# 如何使用CDN
一些类库会使用到CDN这种形式,而不是本地安装的包,这种场景下rollup也是支持的。
src\main.js
import _ from 'lodash';
import $ from 'jquery';
console.log(_.concat([1,2,3],4,5));
console.log($);
export default 'main';
 2
3
4
5
dist\index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>rollup</title>
</head>
<body>
  <script src="https://cdn.jsdelivr.net/npm/lodash/lodash.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/jquery/jquery.min.js"></script>
  <script src="bundle.cjs.js"></script>
</body>
</html>
 2
3
4
5
6
7
8
9
10
11
12
13
14
rollup.config.js
import babel from "@rollup/plugin-babel"
import resolve from "@rollup/plugin-node-resolve"
import commonjs from "@rollup/plugin-commonjs"
export default {
  input: "src/main.js",
  output: {
    file: "dist/bundle.cjs.js", //输出文件的路径和名称
    format: "iife", //五种输出格式:amd/es6/iife/umd/cjs
    name: "bundleName", //当format为iife和umd时必须提供,将作为全局变量挂在window下
    global: {
      lodash: "_",
      jquery: "$",
    },
  },
  external: ["lodash", "jquery"],
  plugins: [
    babel({
      babelHelpers: "bundled", // 这个配置babel希望被显示的配置上去
      exclude: "node_modules/**",
    }),
    resolve(),
    commonjs(),
  ],
}
 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
重新编译打印出来的 budle.cjs.js
var bundleName = (function (_, $) {
	'use strict';
	function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
	var ___default = /*#__PURE__*/_interopDefaultLegacy(_);
	var $__default = /*#__PURE__*/_interopDefaultLegacy($);
	// 最简单的打包配置
	console.log(___default["default"].concat([1, 2, 3], 4, 5));
	console.log($__default["default"]);
	var main = 'main';
	return main;
})(_, $);
 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 如何支持typescript 并进行代码压缩
首先也是需要安装支持的插件:
npm install tslib typescript @rollup/plugin-typescript --save-dev
 如果想要实现代码压缩,还需要使用一个插件 terser
npm install rollup-plugin-terser --save-dev
 添加 tsconfig.json 文件
{
  "compilerOptions": {  
    "target": "es5",                          
    "module": "ESNext",                     
    "strict": true,                         
    "skipLibCheck": true,                    
    "forceConsistentCasingInFileNames": true 
  }
}
 2
3
4
5
6
7
8
9
src\main.ts
let myName:string = 'name';
let age:number=12;
console.log(myName,age);
 2
3
rollup.config.js
import babel from "@rollup/plugin-babel";
import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser';
export default {
  input: "src/main.ts",
  output: {
    file: "dist/bundle.cjs.js", //输出文件的路径和名称
    format: "cjs", //五种输出格式:amd/es6/iife/umd/cjs
    name: "bundleName", //当format为iife和umd时必须提供,将作为全局变量挂在window下
  },
  plugins: [
    babel({
      babelHelpers: "bundled", // 这个配置babel希望被显示的配置上去
      exclude: "node_modules/**",
    }),
    typescript(),
    terser()
  ],
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
编译出来的文件:只有一行,连变量声明都没有了,只有打印的语句
"use strict";console.log("zhufeng",12);
 # 支持启动本地服务器
安装依赖
npm install rollup-plugin-serve --save-dev
 修改配置文件:
import typescript from "@rollup/plugin-typescript"
import serve from "rollup-plugin-serve"
export default {
  input: "src/main.ts",
  output: {
    file: "dist/bundle.cjs.js", //输出文件的路径和名称
    format: "cjs", //五种输出格式:amd/es6/iife/umd/cjs
    name: "bundleName", //当format为iife和umd时必须提供,将作为全局变量挂在window下
  },
  plugins: [
    typescript(),
    serve({
      open: true,
      port: 8081,
      contentBase: "./dist",
    }),
  ],
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# rollup源码实现
在源码实现之前, 来准备一些前置知识。
# 1.1 初始化项目
npm install rollup magic-string acorn --save
 # 1.2 magic-string
magic-string是一个操作字符串和生成source-map的工具
let MagicString = require('magic-string');
let sourceCode = `export var name = "学习rollup"`;
let ms = new MagicString(sourceCode);
console.log(ms); // 打印的是实例
// 裁剪出原始字符串开始和结束之间所有的内容
// 返回一个克隆后的MagicString的实例
console.log(ms.snip(0, 6).toString());//sourceCode.slice(0,6);
// 删除0, 7之间的内容
console.log(ms.remove(0, 7).toString());//sourceCode.slice(7);
// 还可以用用来合并代码 
let bundle = new MagicString.Bundle();
bundle.addSource({
  content: 'var a = 1;',
  separator: '\n'
});
bundle.addSource({
  content: 'var b = 2;',
  separator: '\n'
});
console.log(bundle.toString());
 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
这段代码使用了 magic-string 库,这是一个用于处理字符串的库,主要用于在字符串中进行插入、删除、替换等操作,特别适用于处理源代码字符串。下面对你提供的代码进行解释:
创建
MagicString实例:let MagicString = require('magic-string'); let sourceCode = `export var name = "学习rollup"`; let ms = new MagicString(sourceCode); console.log(ms);1
2
3
4- 引入 
magic-string库并创建了一个MagicString实例ms。 sourceCode是原始的源代码字符串。
- 引入 
 裁剪出原始字符串开始和结束之间所有的内容:
console.log(ms.snip(0, 6).toString());1- 使用 
snip(start, end)方法裁剪字符串,裁剪的范围是从索引start到索引end之间的字符。 - 这里裁剪的范围是从索引 0 到索引 6,即裁剪 "export"。
 toString()方法用于获取裁剪后的字符串。
- 使用 
 删除 0 到 7 之间的内容:
console.log(ms.remove(0, 7).toString());1- 使用 
remove(start, end)方法删除字符串,删除的范围是从索引start到索引end之间的字符。 - 这里删除的范围是从索引 0 到索引 7,即删除 "export "。
 
- 使用 
 用于合并代码的
MagicString.Bundle:let bundle = new MagicString.Bundle(); bundle.addSource({ content: 'var a = 1;', separator: '\n' }); bundle.addSource({ content: 'var b = 2;', separator: '\n' }); console.log(bundle.toString());1
2
3
4
5
6
7
8
9
10- 创建了一个 
MagicString.Bundle实例bundle,用于合并多个源代码字符串。 - 使用 
addSource({ content, separator })方法向bundle中添加源代码片段,content是源代码字符串,separator是每个源代码片段之间的分隔符。 - 在这个例子中,向 
bundle添加了两个源代码片段,分别是'var a = 1;'和'var b = 2;',它们由换行符\n分隔。 bundle.toString()方法用于获取合并后的字符串。
- 创建了一个 
 
# 1.3 AST
- 通过JavaScript Parser可以把代码转化为一颗抽象语法树AST, 这颗树定义了代码的结构,通过操纵这颗树,我们可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作。
 
AST的工作流:
- Parse(解析) 将源代码转换成抽象语法树,树上有很多的estree节点
 - Transform(转换) 对抽象语法树进行转换
 - Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码
 
# 1.4 acorn
acorn 解析结果符合The Estree Spec规范,在webpack和rollup中使用的都是这个库去处理的。
acorn 是一个 JavaScript 解析器(Parser)库,用于将 JavaScript 代码解析为抽象语法树(AST)。这个库是由 Marijn Haverbeke 编写的,被广泛用于各种与 JavaScript 代码分析和处理相关的工具和项目中。以下是一个简单的示例,演示了如何使用 acorn 库来解析 JavaScript 代码:
首先,确保你已经安装了 acorn 库:
npm install acorn
 然后,你可以使用以下代码来解析 JavaScript 代码:
const acorn = require('acorn');
// 要解析的 JavaScript 代码
const code = 'const message = "Hello, Acorn!";';
// 使用 acorn 解析代码,得到 AST
const ast = acorn.parse(code, {
  ecmaVersion: 'latest', // 使用最新的 ECMAScript 版本
  sourceType: 'module',  // 解析模块代码
});
// 打印解析得到的 AST
console.log(JSON.stringify(ast, null, 2));
 2
3
4
5
6
7
8
9
10
11
12
13
下面是使用工具生成的json结构的AST语法树。
{
  "type": "Program",
  "start": 0,
  "end": 32,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 32,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 31,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 13,
            "name": "message"
          },
          "init": {
            "type": "Literal",
            "start": 16,
            "end": 31,
            "value": "Hello, Acorn!",
            "raw": "\"Hello, Acorn!\""
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}
 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
上述代码中:
acorn.parse(code, options)方法用于将传入的 JavaScript 代码code解析成一个抽象语法树(AST)。options对象是可选的,用于指定解析的选项。在上面的例子中,使用了ecmaVersion设置为'latest',表示使用最新的 ECMAScript 版本,以及sourceType设置为'module',表示解析模块代码。JSON.stringify(ast, null, 2)用于将得到的 AST 对象格式化为漂亮的 JSON 字符串,并打印出来。
# 2. 实现rollup
# 2.1 目录结构
├── package.json
├── README.md
├── src
    ├── ast
    │   ├── analyse.js //分析AST节点的作用域和依赖项
    │   ├── Scope.js //有些语句会创建新的作用域实例
    │   └── walk.js //提供了递归遍历AST语法树的功能
    ├── Bundle//打包工具,在打包的时候会生成一个Bundle实例,并收集其它模块,最后把所有代码打包在一起输出
    │   └── index.js 
    ├── Module//每个文件都是一个模块
    │   └── index.js
    ├── rollup.js //打包的入口模块
    └── utils
        ├── map-helpers.js
        ├── object.js
        └── promise.js
 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2.2 src\main.js
console.log('hello');