1.0.0 • Published 2 years ago

espixels v1.0.0

Weekly downloads
-
License
ISC
Repository
-
Last release
2 years ago

rollup-study

一、背景介绍

  • webpack打包非常繁琐,打包体积比较大
  • rollup主要是用来打包JS库的
  • vue/react/angular都在用rollup作为打包工具

二、安装插件

cnpm 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

三、rollup初体验

3.1 创建rollup.config.js文件

配置文件是一个ES6模块,它对外暴露一个对象,这个对象包含了一些Rollup需要的一些选项。通常,我们把这个配置文件叫做rollup.config.js,它通常位于项目的根目录,下面是一些配置选项

3.1.1 基本配置

  1. 首先,我们在根目录下创建rollup.config.js文件,配置如下

    export default {
        input: 'src/main.js',
        output: {
            file: 'dist/bundle.cjs.js', // 输出的文件路径和文件名
            format: 'cjs', // 输出的格式, amd es iife umd cjs system
        }
    }
  2. 接着我们在根目录下创建一个src文件夹,里面放置main.js文件,如下↓↓↓

    console.log('aaa')
  1. 更改package.json文件脚本

    {
        "script": {
        "build": "rollup --config"
    }

3.1.2 rollup的基本配置选项(了解)

// rollup.config.js
export default {
  	// 核心选项
  	input,     // 必须
  	external,
  	plugins,

  	// 额外选项
  	onwarn,

  	// danger zone
  	acorn,
  	context,
  	moduleContext,
  	legacy

  	output: {  // 必须 (如果要输出多个,可以是一个数组)
    	// 核心选项
    	file,    // 必须
    	format,  // 必须
    	name, // 当format是‘iife’的时候,name值必须提供
    	globals,

    	// 额外选项
    	paths,
    	banner,
    	footer,
    	intro,
    	outro,
    	sourcemap,
    	sourcemapFile,
    	interop,

    	// 高危选项
    	exports,
    	amd,
    	indent
    	strict
  	},
};

四、支持babel

  • 为了使用新的语法,可以使用babel来进行编译输出

4.1 安装依赖

  • @babel/core是babel的核心包
  • @babel/preset-env是预设
  • @rollup/plugin-babel是babel插件
cnpm install @rollup/plugin-babel @babel/core @babel/preset-env --save-dev

4.2 配置rollup.config.js

import babel from 'rollup-plugin-babel'

export default {
	input: 'src/main.js',
	output: {
		file: 'dist/bundle.cjs.js', // 输出的文件路径和文件名
		format: 'cjs',
	},
    plugins: [
        babel({
            exclude: "node_modules/**"
        })
    ]
};

4.3 新建.babelrc文件

{
    "presets": [
       [
        "@babel/env",
        {
            "modules":false  // 不要帮忙转换es module写法
        }
       ]
    ]
}

五、tree-shaking

rollup默认支持tree-shaking,会把无用的代码给删除掉

  • Tree-shaking的本质是消除无用的js代码
  • rollup只处理函数和顶层的import/export变量

六、使用第三方模块

rollup.js编译源码中的模块引用默认只支持 ES6+的模块方式import/export

6.1 安装依赖

cnpm install @rollup/plugin-node-resolve @rollup/plugin-commonjs lodash  --save-dev

6.1.1 在main.js中编写如下代码

import _ from 'lodash';
console.log(_);

6.1.2 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:'cjs',//五种输出格式:amd/es6/iife/umd/cjs
        name:'bundleName'//当format为iife和umd时必须提供,将作为全局变量挂在window下
    },
    plugins:[
        babel({
            exclude:"node_modules/**"
        }),
        resolve(),
        commonjs()
    ]
}

七、rollup中使用CDN

7.1.1 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>

7.1.2 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下
+       globals:{
+           lodash:'_', //告诉rollup全局变量_即是lodash
+           jquery:'$' //告诉rollup全局变量$即是jquery
+       }
    },
    plugins:[
        babel({
            exclude:"node_modules/**"
        }),
        resolve(),
        commonjs()
    ],
+   external:['lodash','jquery']
}

八、rollup支持typescript

8.1 安装依赖包

cnpm install tslib typescript @rollup/plugin-typescript --save-dev

8.2 将main.js改名为main.ts

let myName:string = 'zhufeng';
let age:number=12;
console.log(myName,age);

8.3 配置rollup.config.js

import babel from 'rollup-plugin-babel';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
export default {
    input:'src/main.ts',
    output:{
        file:'dist/bundle.cjs.js',//输出文件的路径和名称
        format:'iife',//五种输出格式:amd/es6/iife/umd/cjs
        name:'bundleName',//当format为iife和umd时必须提供,将作为全局变量挂在window下
        globals:{
            lodash:'_', //告诉rollup全局变量_即是lodash
            jquery:'$' //告诉rollup全局变量$即是jquery
        }
    },
    plugins:[
        babel({
            exclude:"node_modules/**"
        }),
        resolve(),
        commonjs(),
        typescript()
    ],
    external:['lodash','jquery']
}

8.4 新建ts.config.json文件

{
  "compilerOptions": {  
    "target": "es5",                          
    "module": "ESNext",                     
    "strict": true,                         
    "skipLibCheck": true,                    
    "forceConsistentCasingInFileNames": true 
  }
}

九、压缩JS

通常我们压缩js使用的是terser,terser是支持ES6 +的JavaScript压缩器工具包

9.1 安装

cnpm install rollup-plugin-terser --save-dev

9.2 配置rollup.config.js

import babel from 'rollup-plugin-babel';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
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:'iife',//五种输出格式:amd/es6/iife/umd/cjs
        name:'bundleName',//当format为iife和umd时必须提供,将作为全局变量挂在window下
        globals:{
            lodash:'_', //告诉rollup全局变量_即是lodash
            jquery:'$' //告诉rollup全局变量$即是jquery
        }
    },
    plugins:[
        babel({
            exclude:"node_modules/**"
        }),
        resolve(),
        commonjs(),
        typescript(),
        terser()
    ],
    external:['lodash','jquery']
}

十、编译CSS

10.1 安装

cnpm install  postcss rollup-plugin-postcss --save-dev

10.2 rollup.config.js

import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import {terser} from 'rollup-plugin-terser';
+import postcss from 'rollup-plugin-postcss';
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下
        globals:{
            lodash:'_', //告诉rollup全局变量_即是lodash
            jquery:'$' //告诉rollup全局变量$即是jquery
        }
    },
    plugins:[
        babel({
            exclude:"node_modules/**"
        }),
        resolve(),
        commonjs(),
        typescript(),
        //terser(),
+       postcss()
    ],
    external:['lodash','jquery']
}

十一、开启本地服务器

11.1 安装

cnpm install rollup-plugin-serve --save-dev

11.2 配置rollup.config.js

import babel from 'rollup-plugin-babel';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import postcss from 'rollup-plugin-postcss';
+import serve from 'rollup-plugin-serve';
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下
        sourcemap:true,
        globals:{
            lodash:'_', //告诉rollup全局变量_即是lodash
            jquery:'$' //告诉rollup全局变量$即是jquery
        }
    },
    plugins:[
        babel({
            exclude:"node_modules/**"
        }),
        resolve(),
        commonjs(),
        typescript(),
        postcss(),
+       serve({
+           open:true,
+           port:8080,
+           contentBase:'./dist'
+       })
    ],
    external:['lodash','jquery']
}

11.3 配置package.json文件

{
  "scripts": {
    "build": "rollup --config rollup.config.build.js",
    "dev": "rollup --config rollup.config.dev.js -w"
  },
}

rollup源码实现前置知识

一、前景说明

​ rollup 使用了 acornmagic-string 两个库。为了更好的阅读 rollup 源码,必须对它们有所了解。

二、安装

cnpm install magic-string acorn --save

三、magic-string

magic-string是一个操作字符串和生成source-map的工具。magic-string 是 rollup 作者写的一个关于字符串操作的库。以下是代码demo↓↓↓

var MagicString = require('magic-string');
var magicString = new MagicString('export var name = "beijing"');
//类似于截取字符串
console.log(magicString.snip(0,6).toString()); // export
//从开始到结束删除字符串(索引永远是基于原始的字符串,而非改变后的)
console.log(magicString.remove(0,7).toString()); // var name = "beijing"

//很多模块,把它们打包在一个文件里,需要把很多文件的源代码合并在一起
let bundleString = new MagicString.Bundle();
bundleString.addSource({
    content:'var a = 1;',
    separator:'\n'
});
bundleString.addSource({
    content:'var b = 2;',
    separator:'\n'
});
/* let str = '';
str += 'var a = 1;\n'
str += 'var b = 2;\n'
console.log(str); */
console.log(bundleString.toString());
// var a = 1;
//var b = 2;

四、AST语法树

  • 介绍

    • 抽象语法树(Abstract Syntax Tree,AST)是源代码语法结构的一种抽象表示
    • 它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构
  • 用途

    • 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
    • 优化变更代码,改变代码结构使达到想要的结构
  • 定义

    • 这些工具的原理都是通过JavaScript Parser把代码转化为一颗抽象语法树(AST),这颗树定义了代码的结构,通过操纵这颗树,我们可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作

    npm.io

  • AST工作流

    • Parse(解析) 将源代码转换成抽象语法树,树上有很多的estree节点
    • Transform(转换) 对抽象语法树进行转换
    • Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码

npm.io

五、acorn

acorn的作用比较单一,只负责把源代码变成AST语法树这一件事情。

  • astexplorer可以把代码转成语法树

  • acorn 解析结果符合The Estree Spec规范

  • 拓展内容

    esprima           转AST
    estraverse        AST遍历和转换
    escodegen         代码生成
    
    @babel/parser     转AST
    @babel/traverse   AST遍历
    @babel/generator  代码生成
  • acorn的使用

    这里我们以import $ from 'jquery'为例进行分析,代码如下

    const acorn = require('acorn');
    const sourceCode = 'import $ from "jquery"';
    const ast = acorn.parse(sourceCode, {
    	locations: true,             // 是否显示位置
    	ranges: true,                // 是否显示范围
    	sourceType: 'module',        // 模块类型
    	ecmaVersion: 8,              // ecma版本号
    });
    console.log(ast);

    这时,我们拿到的ast的值为:

    {
      type: 'Program',
      start: 0,
      end: 22,
      loc: SourceLocation {
        start: Position { line: 1, column: 0 },
        end: Position { line: 1, column: 22 }
      },
      range: [ 0, 22 ],
      body: [
        Node {
          type: 'ImportDeclaration',
          start: 0,
          end: 22,
          loc: [SourceLocation],
          range: [Array],
          specifiers: [Array],  // 导入标识符
          source: [Node]
        }
      ],
      sourceType: 'module'
    }

    可以看到这个 AST 的类型为 program,表明这是一个程序。body 则包含了这个程序下面所有语句对应的 AST 子节点。

    每个节点都有一个 type 类型,例如 Identifier,说明这个节点是一个标识符;

    如果想了解更多详情 AST 节点的信息可以看一下这篇文章《使用 Acorn 来解析 JavaScript》

    npm.io

六、rollup打包分析

在 rollup 中,一个文件就是一个模块。每一个模块都会根据文件的代码生成一个 AST 语法抽象树,rollup 需要对每一个 AST 节点进行分析。分析 AST 节点,就是看看这个节点有没有调用函数或方法。如果有,就查看所调用的函数或方法是否在当前作用域,如果不在就往上找,直到找到模块顶级作用域为止。如果本模块都没找到,说明这个函数、方法依赖于其他模块,需要从其他模块引入。

简易版rollup源码实现

一、安装依赖

cnpm install magic-string acorn --save

二、文件目录

├── 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

三、rollup流程以及各个文件分析

3.1 debugger.js文件

​ 此文件使我们在运行自定义rollup的执行文件,它做的事情很简单,就是调用一个rollup方法,进行文件打包

/*
 * @Descripttion: 自定义rollup执行文件
 * @Author: lukasavage
 * @Date: 2022-06-05 16:09:43
 * @LastEditors: lukasavage
 * @LastEditTime: 2022-06-05 17:47:50
 * @FilePath: \rollup-study\debugger.js
 */
const rollup = require('./rollup-demo/rollup');

// 执行打包,并且把打包后的结果写入bundle.js中
rollup('./src/demo.js', 'bundle.js');

3.1.1 demo.js

​ 此文件是用户写的js文件,也是我们需要打包的文件

console.log('hello');
console.log('rollup');

3.1.2 rollup.js

​ 此文件使我们自己实现rollup的入口文件

/*
 * @Descripttion: 自定义rollup的入口文件
 * @Author: lukasavage
 * @Date: 2022-06-06 09:24:45
 * @LastEditors: lukasavage
 * @LastEditTime: 2022-06-06 09:30:59
 * @FilePath: \rollup-study\rollup-demo\rollup.js
 */
const Bundle = require('./Bundle');

/**
 * rollup打包方法,核心原理是:先通过acorn编译拿到ast语法树,通过fs.writeFileSync方法写出文件
 * @param {string} entry 打包路径
 * @param {string} outputFile 要打包后的文件名
 */
function rollup(entry, outputFile) {
    // 打包文件的实例bundle,包含打包文件的所有信息
	const bundle = new Bundle({ entry });
	bundle.build(outputFile);
}

module.exports = rollup;

3.1.3 bundle.js

​ 此文件是实现rollup的核心包,包括读写操作文件、生成字符串包等功能

/*
 * @Descripttion: rollup的打包暴露出的方法
 * @Author: lukasavage
 * @Date: 2022-06-05 16:03:53
 * @LastEditors: lukasavage
 * @LastEditTime: 2022-06-06 09:36:26
 * @FilePath: \rollup-study\rollup-demo\Bundle\index.js
 */

const { default: MagicString } = require('magic-string');
const path = require('path');
const fs = require('fs');

const Module = require('../Module');

class Bundle {
	constructor({ entry }) {
		// 可能传过来的是相对路径,统一转成绝对路径
		this.entryPath = path.resolve(entry);
		// 存放着本次打包的所有模块
		this.modules = {};
	}

	// 负责编译入口文件,然后把结果写入outputFile
	build(outputFile) {
		// 1.先获取模块
		const entryModule = (this.entryModule = this.fetchModule(
			this.entryPath
		));
		// 2.将代码展开(展开的意思是:将import、require('xxx')获取到的变量放入到当前文件中,并同时删除import、require)
		this.statements = entryModule.expandAllStatement();
		const code = this.generate();
		fs.writeFileSync(outputFile, code);
	}

	/**
	 * 根据模块的绝对路径返回模块的实例
	 * @param {string} entryPath 模块的绝对路径
	 */
	fetchModule(entryPath) {
		const route = entryPath;
		if (route) {
			const code = fs.readFileSync(route, 'utf8');
			const module = new Module({
				code,
				path: route,
				bundle: this,
			});
			return module;
		}
	}

	generate() {
		// 生成字符串包
		const bundleString = new MagicString.Bundle();
		// this.statements只有入口模块里所有的顶层节点
		this.statements.forEach(statement => {
			const content = statement._source.clone();
			bundleString.addSource({
				content,
				separator: '\n',
			});
		});
		return bundleString.toString();
	}
}
module.exports = Bundle;

// 该模块有点类似于webpack中的Compile

3.1.4 module.js

​ 模块文件信息的汇总,包括code、path、bundle、ast等

/*
 * @Descripttion: 模块文件信息的汇总,包括code、path、bundle、ast等
 * @Author: lukasavage
 * @Date: 2022-06-05 16:21:20
 * @LastEditors: lukasavage
 * @LastEditTime: 2022-06-06 20:33:39
 * @FilePath: \rollup-study\rollup-demo\Module\index.js
 */
const { default: MagicString } = require('magic-string');
const { parse } = require('acorn');

const analyse = require('../ast/analyse');

class Module {
	constructor({ code, path, bundle }) {
		this.code = new MagicString(code, { filename: path });
		this.path = path;
		this.bundle = bundle;
		this.ast = parse(code, {
			ecmaVersion: 8,
			sourceType: 'module',
		});
		analyse(this.ast, this.code, this );
	}
	// 展开代码的方法
	expandAllStatement() {
		const allStatements = [];
		this.ast.body.forEach(statement => {
			// todo: 我们可能要把statement进行拓展,有可能一行变成多行var name = '张三'; console.log('name');
			allStatements.push(statement);
		});
		return allStatements;
	}
}
module.exports = Module;

3.1.5 ast/analysis.js

​ 通过acorn编译语法树,再给语法树的statement添加_source属性返回

/*
 * @Descripttion:
 * @Author: lukasavage
 * @Date: 2022-06-05 16:56:49
 * @LastEditors: lukasavage
 * @LastEditTime: 2022-06-05 17:03:05
 * @FilePath: \rollup-study\rollup-demo\ast\analyse.js
 */

function analyse(ast, magicStringOfAst, module) {
	ast.body.forEach(statement => {
		Object.defineProperties(statement, {
			// key是_source, 值是这个语法树节点在源码中的源代码
             //start指的是此节点在源代码中的起始索引,end就是结束索引
      		//magicString.snip返回的还是magicString 实例clone
			_source: {
				value: magicStringOfAst.snip(statement.start, statement.end),
			},
		});
	});
}
module.exports = analyse;

3.1.6 总结

Bundle的实例在build的时候,会从入口出发,每一个文件会生成一个module实例,包含模块的源代码,模块的路径,模块的抽象语法树ast,然后将语法树语句进行展开,返回所有的语句组成的数组,最后调用generate生成最终的代码。

3.1.7 原理图总结

npm.io

tree-shaking的实现原理

​ 我们知道,在rollup通过build方法打包的时候,实际上会调用Bundle实例上的build方法,在build方法里面会去分析语法树,tree shaking的原理正是分析了语法树里面的导入、导出变量才实现的。

一、ast导入和导出的解析

1.1 导入与导出的实现

1.1.1 导入语句

首先我们打开https://astexplorer.net/网址来分析下的ast树长什么样

import { name as a } from './msg'

对应的ast如下↓

npm.io

1.1.2 导出语句

export const name = '张三'

对应的ast如下↓

npm.io

1.1.3完整代码如下

以下代码的主要作用:收集imports、exports

/*
 * @Descripttion: 模块文件信息的汇总,包括code、path、bundle、ast等
 * @Author: lukasavage
 * @Date: 2022-06-05 16:21:20
 * @LastEditors: lukasavage
 * @LastEditTime: 2022-06-10 21:35:43
 * @FilePath: \rollup-study\rollup-demo\Module\index.js
 */
const { default: MagicString } = require('magic-string');
const { parse } = require('acorn');

const analyse = require('../ast/analyse');

class Module {
	constructor({ code, path, bundle }) {
		this.code = new MagicString(code, { filename: path });
		this.path = path;
		this.bundle = bundle;
		this.ast = parse(code, {
			ecmaVersion: 8,
			sourceType: 'module',
		});
		this.imports = {}; // 存放着当前模块所有的导入
		this.exports = {}; // 存放着当前模块所有的导出
		this.analyse();
	}
	analyse() {
		this.ast.body.forEach(statement => {
			if (statement.type === 'ImportDeclaration') {
				// 如果是导入语句
				const source = statement.source.value; // ./msg  代表从哪个模块来的
				statement.specifiers.forEach(specifier => {
					const importName = specifier.imported.name; // name
					const localName = specifier.local.name; // a
					// 将上面拿到的本地名、来源、来源名统一记录到this.imports中
					this.imports[localName] = { localName, source, importName };
				});
			} else if (statement.type === 'ExportNamedDeclaration') {
				const declaration = statement.declaration;
				if (declaration.type === 'VariableDeclaration') {
					const declarations = declaration.declarations;
					this.exports[localName] = {
						localName,
						exportName: localName,
						expression: declaration,
					};
				}
			}
		});
		// 1.给import和export赋值
		analyse(this.ast, this.code, this);
	}
	// 展开代码的方法
	expandAllStatement() {
		const allStatements = [];
		this.ast.body.forEach(statement => {
			// todo: 我们可能要把statement进行拓展,有可能一行变成多行var name = '张三'; console.log('name');
			allStatements.push(statement);
		});
		return allStatements;
	}
}
module.exports = Module;

1.1.4 挂载imports、exports

​ 当我们收集到imports、exports之后,我们需要把它们挂载上去了