官网:https://www.rollupjs.com/
Rollup 基础
概览
Rollup 是一个用于 JavaScript 的模块打包工具,它将小的代码片段编译成更大、更复杂的代码,例如库或应用程序。它使用 JavaScript 的 ES6 版本中包含的新标准化代码模块格式,而不是以前的 CommonJS 和 AMD 等特殊解决方案。ES 模块允许你自由无缝地组合你最喜欢的库中最有用的个别函数。这在未来将在所有场景原生支持,但 Rollup 让你今天就可以开始这样做。
安装
npm install --global rollup
这将使 Rollup 可以作为全局命令行工具使用。你也可以在本地安装它,请参阅 在本地安装 Rollup。
背景
- 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.config.js
Asynchronous Module Definition
异步模块定义- ES6 module是es6提出了新的模块化方案
IIFE(Immediately Invoked Function Expression)
即立即执行函数表达式,所谓立即执行,就是声明一个函数,声明完了立即执行- UMD全称为
Universal Module Definition
,也就是通用模块定义 cjs
是nodejs采用的模块化标准,commonjs使用方法require
来引入模块,这里require()
接收的参数是模块名或者是模块文件的路径
rollup.config.js
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下
}
}
src\main.js
src\main.js
console.log('hello');
package.json
package.json
{
"scripts": {
"build": "rollup --config"
},
}
dist\index.html
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>
支持babel
- 为了使用新的语法,可以使用babel来进行编译输出
安装依赖
- @babel/core是babel的核心包
- @babel/preset-env是预设
- @rollup/plugin-babel是babel插件
cnpm install @rollup/plugin-babel @babel/core @babel/preset-env --save-dev
src\main.js
let sum = (a,b)=>{
return a+b;
}
let result = sum(1,2);
console.log(result);
.babelrc
.babelrc
{
"presets": [
[
"@babel/env",
{
"modules":false
}
]
]
}
rollup.config.js
rollup.config.js
+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:'bundleName'//当format为iife和umd时必须提供,将作为全局变量挂在window下
},
+ plugins:[
+ babel({
+ exclude:"node_modules/**"
+ })
+ ]
}
tree-shaking
- Tree-shaking的本质是消除无用的js代码
- rollup只处理函数和顶层的import/export变量
src\main.js
src\main.js
import {name,age} from './msg';
console.log(name);
src\msg.js
src\msg.js
export var name = 'zhufeng';
export var age = 12;
使用第三方模块
- rollup.js编译源码中的模块引用默认只支持 ES6+的模块方式
import/export
安装依赖
cnpm install @rollup/plugin-node-resolve @rollup/plugin-commonjs lodash --save-dev
src\main.js
src\main.js
import _ from 'lodash';
console.log(_);
rollup.config.js
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()
]
}
使用CDN
src\main.js
src\main.js
import _ from 'lodash';
import $ from 'jquery';
console.log(_.concat([1,2,3],4,5));
console.log($);
export default 'main';
dist\index.html
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>
rollup.config.js
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']
}
使用typescript
安装
cnpm install tslib typescript @rollup/plugin-typescript --save-dev
src\main.ts
src\main.ts
let myName:string = 'zhufeng';
let age:number=12;
console.log(myName,age);
rollup.config.js
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']
}
tsconfig.json
tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "ESNext",
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
压缩JS
- terser是支持ES6 +的JavaScript压缩器工具包
安装
cnpm install rollup-plugin-terser --save-dev
rollup.config.js
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
- terser是支持ES6 +的JavaScript压缩器工具包
安装
cnpm install postcss rollup-plugin-postcss --save-dev
rollup.config.js
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';
+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']
}
src\main.js
src\main.js
import './main.css';
src\main.css
src\main.css
body{
background-color: green;
}
本地服务器
安装
cnpm install rollup-plugin-serve --save-dev
rollup.config.dev.js
rollup.config.dev.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']
}
package.json
package.json
{
"scripts": {
"build": "rollup --config rollup.config.build.js",
"dev": "rollup --config rollup.config.dev.js -w"
},
}
AST流和Tree-Shaking
1.前置知识
1.1. 初始化项目
npm install rollup magic-string acorn --save
1.2. magic-string
- magic-string是一个操作字符串和生成source-map的工具
var MagicString = require('magic-string');
let sourceCode = `export var name = "zhufeng"`;
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);
//还可以用用来合并代码 //TODO
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.3. AST
- 通过
JavaScript Parser
可以把代码转化为一颗抽象语法树AST,这颗树定义了代码的结构,通过操纵这颗树,我们可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作
1.3.1 AST工作流
- Parse(解析) 将源代码转换成抽象语法树,树上有很多的estree节点
- Transform(转换) 对抽象语法树进行转换
- Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码
1.3.2 acor
- astexplorer可以把代码转成语法树
- acorn 解析结果符合
The Estree Spec
规范
1.3.2.1 walk.js
function walk(astNode, { enter, leave }) {
visit(astNode, null, enter, leave);
}
function visit(node, parent, enter, leave) {
if (enter) {
enter.call(null, node, parent);
}
let keys = Object.keys(node).filter(key => typeof node[key] === 'object')
keys.forEach(key => {
let value = node[key];
if (Array.isArray(value)) {
value.forEach(val => visit(val, node, enter, leave));
} else if (value && value.type) {
visit(value, node, enter, leave)
}
});
if (leave) {
leave.call(null, node, parent);
}
}
module.exports = walk;
1.3.2.2 use.js
const acorn = require('acorn');
const walk = require('./walk');
const sourceCode = 'import $ from "jquery"'
const ast = acorn.parse(sourceCode, {
sourceType: 'module', ecmaVersion: 8
});
let indent = 0;
const padding = () => ' '.repeat(indent)
ast.body.forEach((statement) => {
walk(statement, {
enter(node) {
if (node.type) {
console.log(padding() + node.type + "进入");
indent += 2;
}
},
leave(node) {
if (node.type) {
indent -= 2;
console.log(padding() + node.type + "离开");
}
}
});
});
ImportDeclaration进入
ImportDefaultSpecifier进入
Identifier进入
Identifier离开
ImportDefaultSpecifier离开
Literal进入
Literal离开
ImportDeclaration离开
1.4 作用域
1.4.1 作用域
-
在JS中,作用域是用来规定变量访问范围的规则
function one() { var a = 1; } console.log(a);
1.4.2 作用域链
- 作用域链是由当前执行环境与上层执行环境的一系列变量对象组成的,它保证了当前执行环境对符合访问权限的变量和函数的有序访问
1.4.2.1 scope.js
scope.js
class Scope {
constructor(options = {}) {
//作用域的名称
this.name = options.name;
//父作用域
this.parent = options.parent;
//此作用域内定义的变量
this.names = options.names || [];
}
add(name) {
this.names.push(name);
}
findDefiningScope(name) {
if (this.names.includes(name)) {
return this;
} else if (this.parent) {
return this.parent.findDefiningScope(name);
} else {
return null;
}
}
}
module.exports = Scope;
1.4.2.2 useScope.js
useScope.js
var a = 1;
function one() {
var b = 1;
function two() {
var c = 2;
console.log(a, b, c);
}
}
let Scope = require('./scope');
let globalScope = new Scope({ name: 'global', names: [], parent: null });
let oneScope = new Scope({ name: 'one', names: ['b'], parent: globalScope });
let twoScope = new Scope({ name: 'two', names: ['c'], parent: oneScope });
console.log(
threeScope.findDefiningScope('a').name,
threeScope.findDefiningScope('b').name,
threeScope.findDefiningScope('c').name
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.2 src\main.js
src\main.js
console.log('hello');
2.3 debugger.js
const path = require('path');
const rollup = require('./lib/rollup');
let entry = path.resolve(__dirname, 'src/main.js');
rollup(entry, 'bundle.js');
2.4 rollup.js
lib\rollup.js
const Bundle = require('./bundle')
function rollup(entry, filename) {
const bundle = new Bundle({ entry });
bundle.build(filename);
}
module.exports = rollup;
2.5 bundle.js
lib\bundle.js
let fs = require('fs');
let path = require('path');
let Module = require('./module');
let MagicString = require('magic-string');
class Bundle {
constructor(options) {
//入口文件数据
this.entryPath = path.resolve(options.entry.replace(/\.js$/, '') + '.js');
//存放所有的模块
this.modules = {};
}
build(filename) {
const entryModule = this.fetchModule(this.entryPath);//获取模块代码
this.statements = entryModule.expandAllStatements(true);//展开所有的语句
const { code } = this.generate();//生成打包后的代码
fs.writeFileSync(filename, code);//写入文件系统
}
fetchModule(importee) {
let route = importee;
if (route) {
let code = fs.readFileSync(route, 'utf8');
const module = new Module({
code,
path: importee,
bundle: this
})
return module;
}
}
generate() {
let magicString = new MagicString.Bundle();
this.statements.forEach(statement => {
const source = statement._source.clone();
magicString.addSource({
content: source,
separator: '\n'
})
})
return { code: magicString.toString() }
}
}
module.exports = Bundle;
2.6 module.js
lib\module.js
const MagicString = require('magic-string');
const { parse } = require('acorn');
let 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);
}
expandAllStatements() {
let allStatements = [];
this.ast.body.forEach(statement => {
let statements = this.expandStatement(statement);
allStatements.push(...statements);
});
return allStatements;
}
expandStatement(statement) {
statement._included = true;
let result = [];
result.push(statement);
return result;
}
}
module.exports = Module;
2.7 analyse.js
lib\ast\analyse.js
function analyse(ast, code,module) {
//给statement定义属性
ast.body.forEach(statement => {
Object.defineProperties(statement, {
_module: { value: module },
_source: { value: code.snip(statement.start, statement.end) }
})
});
}
module.exports = analyse;
3. 实现tree-shaking
3.1 基本原理
- 第一步
- 在
module
里收集imports
、exports
和definitions
- 在
analyse
收集_defines
和_dependsOn
- 在
- 第二步
- 重构
expandAllStatements
- 重构
3.2 基本语句
//存放本模块导入了哪些变量
this.imports = {};
// 存放本模块导出了哪些变量
this.exports = {};
//存放本模块的定义变量的语句
this.definitions = {};
//此变量存放所有的变量修改语句,key是变量名,值是一个数组
this.modifications = {};//{name:[name+='jiagou'],age:'age++'}
//记得重命名的变量{key老的变量名:value新的变量名}
this.canonicalNames = {};//{age:'age$1'}
//本模块从哪个模块导入了什么变量,在当前模块内叫什么名字
//this.imports.name = {'./msg','name'};
this.imports[localName] = { source, importName }
//本模块导出了哪个变量,对应哪个本地变量
//this.exports.name = {localName:'name'};
this.exports[exportName] = { localName };
//本顶级语句定义的变量
statement._defines[name] = true;
//定义变量的语句
this.definitions[name] = statement;
//本语句用到的变量
statement._dependsOn[name] = true;
//从模块中获取定义变量的语句
module.define(name);
3.3 main.js
src\main.js
import { name, age } from './msg';
function say() {
console.log('hello', name);
}
say();
3.4 msg.js
src\msg.js
export var name = 'zhufeng';
export var age = 12;
3.5 bundle.js
lib\bundle.js
let fs = require('fs');
let path = require('path');
let Module = require('./module');
let MagicString = require('magic-string');
class Bundle {
constructor(options) {
//入口文件数据
this.entryPath = path.resolve(options.entry.replace(/\.js$/, '') + '.js');
//存放所有的模块
this.modules = {};
}
build(filename) {
const entryModule = this.fetchModule(this.entryPath);//获取模块代码
this.statements = entryModule.expandAllStatements(true);//展开所有的语句
const { code } = this.generate();//生成打包后的代码
fs.writeFileSync(filename, code);//写入文件系统
}
fetchModule(importee, importer) {
+ let route;
+ if (!importer) {
+ route = importee;
+ } else {
+ if (path.isAbsolute(importee)) {
+ route = importee;
+ } else {
+ route = path.resolve(path.dirname(importer), importee.replace(/\.js$/, '') + '.js');
+ }
+ }
if (route) {
let code = fs.readFileSync(route, 'utf8');
const module = new Module({
code,
path: importee,
bundle: this
})
return module;
}
}
generate() {
let magicString = new MagicString.Bundle();
this.statements.forEach(statement => {
const source = statement._source.clone();
+ if (statement.type === 'ExportNamedDeclaration') {
+ source.remove(statement.start, statement.declaration.start);
+ }
magicString.addSource({
content: source,
separator: '\n'
})
})
return { code: magicString.toString() }
}
}
module.exports = Bundle;
3.6 utils.js
lib\utils.js
function hasOwnProperty(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop)
}
exports.hasOwnProperty = hasOwnProperty;
3.7 module.js
lib\module.js
const MagicString = require('magic-string');
const { parse } = require('acorn');
+const { hasOwnProperty } = require('./utils');
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 = {};
+ //存放本模块的定义变量的语句 a=>var a = 1;b =var b =2;
+ this.definitions = {};
analyse(this.ast, this.code, this);
}
expandAllStatements() {
let allStatements = [];
this.ast.body.forEach(statement => {
+ //导入的语句默认全部过滤掉
+ if (statement.type === 'ImportDeclaration') return;
let statements = this.expandStatement(statement);
allStatements.push(...statements);
});
return allStatements;
}
expandStatement(statement) {
statement._included = true;
let result = [];
+ //获取此语句依赖的变量
+ let _dependsOn = Object.keys(statement._dependsOn);
+ _dependsOn.forEach(name => {
+ //找到此变量定义的语句,添加到输出数组里
+ let definitions = this.define(name);
+ result.push(...definitions);
+ });
//把此语句添加到输出列表中
result.push(statement);
return result;
}
+ define(name) {
+ //先判断此变量是外部导入的还是模块内声明的
+ if (hasOwnProperty(this.imports, name)) {
+ //说明此变量不是模块内声明的,而是外部导入的,获取从哪个模块内导入了哪个变量
+ const { source, importName } = this.imports[name];
+ //获取这个模块
+ const importModule = this.bundle.fetchModule(source, this.path);
+ //从这个模块的导出变量量获得本地变量的名称
+ const { localName } = importModule.exports[importName];
+ //获取本地变量的定义语句
+ return importModule.define(localName);//name
+ } else {//如果是模块的变量的话
+ let statement = this.definitions[name];//name
+ if (statement && !statement._included) {
+ //如果本地变量的话还需要继续展开
+ return this.expandStatement(statement);
+ } else {
+ return []
+ }
+ }
+ }
}
module.exports = Module;
3.8 analyse.js
- 第1个循环 找出导入导出的变量
- 第2个循环 找出定义和依赖的变量
lib\ast\analyse.js
+const Scope = require('./scope');
+const walk = require('./walk');
+const { hasOwnProperty } = require('../utils');
+function analyse(ast, code, module) {
+ //第1个循环,找出导入导出的变量
+ ast.body.forEach(statement => {
+ Object.defineProperties(statement, {
_module: { value: module }
_source: { value: code.snip(statement.start, statement.end) },
+ _defines: { value: {} },//此节点上定义的变量say
+ _dependsOn: { value: {} }//此此节点读取了哪些变量
+ })
+ //import { name, age } from './msg';
+ if (statement.type === 'ImportDeclaration') {
+ let source = statement.source.value;// ./msg
+ statement.specifiers.forEach(specifier => {
+ let importName = specifier.imported.name;//导入的变量名
+ let localName = specifier.local.name;//本地的变量名
+ //imports.name = {source:'./msg',importName:'name'};
+ module.imports[localName] = { source, importName }
+ });
+ } else if (statement.type === 'ExportNamedDeclaration') {
+ const declaration = statement.declaration;
+ if (declaration && declaration.type === 'VariableDeclaration') {
+ const declarations = declaration.declarations;
+ declarations.forEach(variableDeclarator => {
+ const localName = variableDeclarator.id.name;//name
+ const exportName = localName;
+ //exports.name = {localName:'name'};
+ module.exports[exportName] = { localName };
+ });
+ }
+ }
+ });
+ //第2次循环创建作用域链
+ let currentScope = new Scope({ name: '全局作用域' });
+ //创建作用域链,为了知道我在此模块中声明哪些变量,这些变量的声明节点是哪个 var name = 1;
+ ast.body.forEach(statement => {
+ function addToScope(name) {
+ currentScope.add(name);//把name变量放入当前的作用域
+ //如果没有父亲,相当 于就是根作用域或者 当前的作用域是一个块级作用域的话
+ if (!currentScope.parent) {//如果没有父作用域,说明这是一个顶级作用域
+ statement._defines[name] = true;//在一级节点定义一个变量name _defines.say=true
+ module.definitions[name] = statement;
+ }
+ }
+ walk(statement, {
+ enter(node) {
+ //收集本节点上使用的变量
+ if (node.type === 'Identifier') {
+ statement._dependsOn[node.name] = true;
+ }
+ let newScope;
+ switch (node.type) {
+ case 'FunctionDeclaration':
+ addToScope(node.id.name);//say
+ const names = node.params.map(param => param.name);
+ newScope = new Scope({ name: node.id.name, parent: currentScope, names });
+ break;
+ case 'VariableDeclaration':
+ node.declarations.forEach(declaration => {
+ addToScope(declaration.id.name);//var
+ });
+ break;
+ default:
+ break;
+ }
+ if (newScope) {
+ Object.defineProperty(node, '_scope', { value: newScope });
+ currentScope = newScope;
+ }
+ },
+ leave(node) {
+ if (hasOwnProperty(node, '_scope')) {
+ currentScope = currentScope.parent;
+ }
+ }
+ });
+ });
+}
module.exports = analyse;
3.9 scope.js
lib\ast\scope.js
class Scope {
constructor(options = {}) {
//作用域的名称
this.name = options.name;
//父作用域
this.parent = options.parent;
//此作用域内定义的变量
this.names = options.names || [];
}
add(name) {
this.names.push(name);
}
findDefiningScope(name) {
if (this.names.includes(name)) {
return this;
} else if (this.parent) {
return this.parent.findDefiningScope(name);
} else {
return null;
}
}
}
module.exports = Scope;
3.10 lib\ast\walk.js
lib\ast\walk.js
function walk(astNode, { enter, leave }) {
visit(astNode, null, enter, leave);
}
function visit(node, parent, enter, leave) {
if (enter) {
enter.call(null, node, parent);
}
let keys = Object.keys(node).filter(key => typeof node[key] === 'object')
keys.forEach(key => {
let value = node[key];
if (Array.isArray(value)) {
value.forEach(val => visit(val, node, enter, leave));
} else if (value && value.type) {
visit(value, node, enter, leave)
}
});
if (leave) {
leave.call(null, node, parent);
}
}
module.exports = walk;
4.包含修改语句
4.1 main.js
src\main.js
import { name, age } from './msg';
console.log(name);
4.2 msg.js
src\msg.js
export var name = 'zhufeng';
name += 'jiagou';
export var age = 12;
4.3 module.js
lib\module.js
const MagicString = require('magic-string');
const { parse } = require('acorn');
const { hasOwnProperty } = require('./utils');
let 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 = {};
//存放本模块的定义变量的语句 a=>var a = 1;b =var b =2;
this.definitions = {};
//存放变量修改语句
+ this.modifications = {};
analyse(this.ast, this.code, this);
}
expandAllStatements() {
let allStatements = [];
this.ast.body.forEach(statement => {
if (statement.type === 'ImportDeclaration') return;
let statements = this.expandStatement(statement);
allStatements.push(...statements);
});
return allStatements;
}
expandStatement(statement) {
statement._included = true;
let result = [];
//获取此语句依赖的变量
let _dependsOn = Object.keys(statement._dependsOn);
_dependsOn.forEach(name => {
//找到此变量定义的语句,添加到输出数组里
let definitions = this.define(name);
result.push(...definitions);
});
//把此语句添加到输出列表中
result.push(statement);
+ //找到此语句定义的变量,把定义的变量和修改语句也包括进来
+ //注意要先定义再修改,所以要把这行放在push(statement)的下面
+ const defines = Object.keys(statement._defines);
+ defines.forEach(name => {
+ //找到定义的变量依赖的修改的语句
+ const modifications = hasOwnProperty(this.modifications, name) && this.modifications[name];
+ if (modifications) {
+ //把修改语句也展开放到结果里
+ modifications.forEach(statement => {
+ if (!statement._included) {
+ let statements = this.expandStatement(statement);
+ result.push(...statements);
+ }
+ });
+ }
+ });
return result;
}
define(name) {
//先判断此变量是外部导入的还是模块内声明的
if (hasOwnProperty(this.imports, name)) {
//说明此变量不是模块内声明的,而是外部导入的,获取从哪个模块内导入了哪个变量
const { source, importName } = this.imports[name];
//获取这个模块
const importModule = this.bundle.fetchModule(source, this.path);
//从这个模块的导出变量量获得本地变量的名称
const { localName } = importModule.exports[importName];
//获取本地变量的定义语句
return importModule.define(localName);//name
} else {//如果是模块的变量的话
let statement = this.definitions[name];//name
if (statement && !statement._included) {
//如果本地变量的话还需要继续展开
return this.expandStatement(statement);
} else {
return []
}
}
}
}
module.exports = Module;
4.4 analyse.js
lib\ast\analyse.js
const Scope = require('./scope');
const walk = require('./walk');
const { hasOwnProperty } = require('../utils');
function analyse(ast, code, module) {
//第1个循环,找出导入导出的变量
ast.body.forEach(statement => {
Object.defineProperties(statement, {
_module: { value: module },
_source: { value: code.snip(statement.start, statement.end) },
_defines: { value: {} },//此节点上定义的变量say
_dependsOn: { value: {} },//此此节点读取了哪些变量
+ _modifies: { value: {} },//本语句修改的变量
})
//import { name, age } from './msg';
if (statement.type === 'ImportDeclaration') {
let source = statement.source.value;// ./msg
statement.specifiers.forEach(specifier => {
let importName = specifier.imported.name;//导入的变量名
let localName = specifier.local.name;//本地的变量名
//imports.name = {source:'./msg',importName:'name'};
module.imports[localName] = { source, importName }
});
} else if (statement.type === 'ExportNamedDeclaration') {
const declaration = statement.declaration;
if (declaration && declaration.type === 'VariableDeclaration') {
const declarations = declaration.declarations;
declarations.forEach(variableDeclarator => {
const localName = variableDeclarator.id.name;//name
const exportName = localName;
//exports.name = {localName:'name'};
module.exports[exportName] = { localName };
});
}
}
});
//第2次循环创建作用域链
let currentScope = new Scope({ name: '全局作用域' });
//创建作用域链,为了知道我在此模块中声明哪些变量,这些变量的声明节点是哪个 var name = 1;
ast.body.forEach(statement => {
+ function checkForReads(node) {
+ //如果此节点类型是一个标识符话
+ if (node.type === 'Identifier') {
+ statement._dependsOn[node.name] = true;
+ }
+ }
+ function checkForWrites(node) {
+ function addNode(node) {
+ const name = node.name;
+ statement._modifies[name] = true;//statement._modifies.age = true;
+ if (!hasOwnProperty(module.modifications, name)) {
+ module.modifications[name] = [];
+ }
+ module.modifications[name].push(statement);
+ }
+ if (node.type === 'AssignmentExpression') {
+ addNode(node.left, true);
+ } else if (node.type === 'UpdateExpression') {
+ addNode(node.argument);
+ }
+ }
function addToScope(name) {
currentScope.add(name);//把name变量放入当前的作用域
//如果没有父亲,相当 于就是根作用域或者 当前的作用域是一个块级作用域的话
if (!currentScope.parent) {//如果没有父作用域,说明这是一个顶级作用域
statement._defines[name] = true;//在一级节点定义一个变量name _defines.say=true
module.definitions[name] = statement;
}
}
walk(statement, {
enter(node) {
- if (node.type === 'Identifier') {
- statement._dependsOn[node.name] = true;
- }
+ //收集本节点上使用的变量
+ checkForReads(node);
+ checkForWrites(node);
let newScope;
switch (node.type) {
case 'FunctionDeclaration':
addToScope(node.id.name);//say
const names = node.params.map(param => param.name);
newScope = new Scope({ name: node.id.name, parent: currentScope, names });
break;
case 'VariableDeclaration':
node.declarations.forEach(declaration => {
addToScope(declaration.id.name);//var
});
break;
default:
break;
}
if (newScope) {
Object.defineProperty(node, '_scope', { value: newScope });
currentScope = newScope;
}
},
leave(node) {
if (hasOwnProperty(node, '_scope')) {
currentScope = currentScope.parent;
}
}
});
});
}
module.exports = analyse;
5.支持块级作用域
5.1 src\main.js
src\main.js
var name = 'zhufeng';
if (true) {
var age = 12;
}
console.log(age);
5.2 lib\ast\scope.js
lib\ast\scope.js
class Scope {
constructor(options = {}) {
//作用域的名称
this.name = options.name;
//父作用域
this.parent = options.parent;
//此作用域内定义的变量
this.names = options.names || [];
// 是否块作用域
+ this.isBlock = !!options.isBlock
}
+ add(name, isBlockDeclaration) {
+ if (!isBlockDeclaration && this.isBlock) {
//这是一个var或者函数声明,并且这是一个块级作用域,所以我们需要向上提升
+ this.parent.add(name, isBlockDeclaration)
} else {
this.names.push(name);
}
}
findDefiningScope(name) {
if (this.names.includes(name)) {
return this;
} else if (this.parent) {
return this.parent.findDefiningScope(name);
} else {
return null;
}
}
}
module.exports = Scope;
5.3 analyse.js
lib\ast\analyse.js
const Scope = require('./scope');
const walk = require('./walk');
const { hasOwnProperty } = require('../utils');
function analyse(ast, code, module) {
//第1个循环,找出导入导出的变量
ast.body.forEach(statement => {
Object.defineProperties(statement, {
_module: { value: module },
_source: { value: code.snip(statement.start, statement.end) },
_defines: { value: {} },//此节点上定义的变量say
_dependsOn: { value: {} },//此此节点读取了哪些变量
_modifies: { value: {} },//本语句修改的变量
})
//import { name, age } from './msg';
if (statement.type === 'ImportDeclaration') {
let source = statement.source.value;// ./msg
statement.specifiers.forEach(specifier => {
let importName = specifier.imported.name;//导入的变量名
let localName = specifier.local.name;//本地的变量名
//imports.name = {source:'./msg',importName:'name'};
module.imports[localName] = { source, importName }
});
} else if (statement.type === 'ExportNamedDeclaration') {
const declaration = statement.declaration;
if (declaration && declaration.type === 'VariableDeclaration') {
const declarations = declaration.declarations;
declarations.forEach(variableDeclarator => {
const localName = variableDeclarator.id.name;//name
const exportName = localName;
//exports.name = {localName:'name'};
module.exports[exportName] = { localName };
});
}
}
});
//第2次循环创建作用域链
let currentScope = new Scope({ name: '全局作用域' });
//创建作用域链,为了知道我在此模块中声明哪些变量,这些变量的声明节点是哪个 var name = 1;
ast.body.forEach(statement => {
function checkForReads(node) {
//如果此节点类型是一个标识符话
if (node.type === 'Identifier') {
statement._dependsOn[node.name] = true;
}
}
function checkForWrites(node) {
function addNode(node) {
const name = node.name;
statement._modifies[name] = true;//statement._modifies.age = true;
if (!hasOwnProperty(module.modifications, name)) {
module.modifications[name] = [];
}
module.modifications[name].push(statement);
}
if (node.type === 'AssignmentExpression') {
addNode(node.left, true);
} else if (node.type === 'UpdateExpression') {
addNode(node.argument);
}
}
+ function addToScope(name, isBlockDeclaration) {
+ currentScope.add(name, isBlockDeclaration);//把name变量放入当前的作用域
//如果没有父亲,相当 于就是根作用域或者 当前的作用域是一个块级作用域的话
+ if (!currentScope.parent || (currentScope.isBlock && !isBlockDeclaration)) {//如果没有父作用域,说明这是一个顶级作用域
statement._defines[name] = true;//在一级节点定义一个变量name _defines.say=true
module.definitions[name] = statement;
}
}
walk(statement, {
enter(node) {
//收集本节点上使用的变量
checkForReads(node);
checkForWrites(node);
let newScope;
switch (node.type) {
case 'FunctionDeclaration':
addToScope(node.id.name);//say
const names = node.params.map(param => param.name);
+ newScope = new Scope({ name: node.id.name, parent: currentScope, names, isBlock: false });
break;
case 'VariableDeclaration':
node.declarations.forEach(declaration => {
+ if (node.kind === 'let' || node.kind === 'const') {
+ addToScope(declaration.id.name, true);//这是是一个块级变量
+ } else {
+ addToScope(declaration.id.name);//var
+ }
});
break;
+ case 'BlockStatement':
+ newScope = new Scope({ parent: currentScope, isBlock: true });
+ break;
default:
break;
}
if (newScope) {
Object.defineProperty(node, '_scope', { value: newScope });
currentScope = newScope;
}
},
leave(node) {
if (hasOwnProperty(node, '_scope')) {
currentScope = currentScope.parent;
}
}
});
});
}
module.exports = analyse;
5.4 module.js
lib\module.js
const MagicString = require('magic-string');
const { parse } = require('acorn');
const { hasOwnProperty } = require('./utils');
const analyse = require('./ast/analyse');
+const SYSTEMS = ['console', 'log'];
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 = {};
//存放本模块的定义变量的语句 a=>var a = 1;b =var b =2;
this.definitions = {};
//存放变量修改语句
this.modifications = {};
analyse(this.ast, this.code, this);
}
expandAllStatements() {
let allStatements = [];
this.ast.body.forEach(statement => {
if (statement.type === 'ImportDeclaration') return;
+ //默认不包含所有的变量声明语句
+ if (statement.type === 'VariableDeclaration') return;
let statements = this.expandStatement(statement);
allStatements.push(...statements);
});
return allStatements;
}
expandStatement(statement) {
statement._included = true;
let result = [];
//获取此语句依赖的变量
let _dependsOn = Object.keys(statement._dependsOn);
_dependsOn.forEach(name => {
//找到此变量定义的语句,添加到输出数组里
let definitions = this.define(name);
result.push(...definitions);
});
//把此语句添加到输出列表中
result.push(statement);
//找到此语句定义的变量,把定义的变量和修改语句也包括进来
//注意要先定义再修改,所以要把这行放在push(statement)的下面
const defines = Object.keys(statement._defines);
defines.forEach(name => {
//找到定义的变量依赖的修改的语句
const modifications = hasOwnProperty(this.modifications, name) && this.modifications[name];
if (modifications) {
//把修改语句也展开放到结果里
modifications.forEach(statement => {
if (!statement._included) {
let statements = this.expandStatement(statement);
result.push(...statements);
}
});
}
});
return result;
}
define(name) {
//先判断此变量是外部导入的还是模块内声明的
if (hasOwnProperty(this.imports, name)) {
//说明此变量不是模块内声明的,而是外部导入的,获取从哪个模块内导入了哪个变量
const { source, importName } = this.imports[name];
//获取这个模块
const importModule = this.bundle.fetchModule(source, this.path);
//从这个模块的导出变量量获得本地变量的名称
const { localName } = importModule.exports[importName];
//获取本地变量的定义语句
return importModule.define(localName);//name
} else {//如果是模块的变量的话
let statement = this.definitions[name];//name
+ if (statement) {
+ if (statement._included) {
+ return [];
+ } else {
+ return this.expandStatement(statement);
+ }
+ } else {
+ if (SYSTEMS.includes(name)) {
+ return [];
+ } else { //如果找不到定义的变量就报错
+ throw new Error(`变量${name}既没有从外部导入,也没有在当前的模块声明`);
+ }
}
}
}
}
module.exports = Module;
6.实现变量名重命名
6.1 src\main.js
src\main.js
import { age1 } from './age1.js';
import { age2 } from './age2.js';
import { age3 } from './age3.js';
console.log(age1, age2, age3);
/**
const age$2 = '年龄';
const age1 = age$2 + '1';
const age$1 = '年龄';
const age2 = age$1 + '2';
const age = '年龄';
const age3 = age + '3';
console.log(age1, age2, age3);
*/
6.2 src\age1.js
src\age1.js
const age = '年龄';
export const age1 = age + '1';
6.3 src\age2.js
src\age2.js
const age = '年龄';
export const age2 = age + '2';
6.4 src\age3.js
src\age3.js
const age = '年龄';
export const age3 = age + '3';
6.5 utils.js
lib\utils.js
+const walk = require('./ast/walk');
function hasOwnProperty(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop)
}
exports.hasOwnProperty = hasOwnProperty;
+function replaceIdentifiers(statement, source, replacements) {
+ walk(statement, {
+ enter(node) {
+ if (node.type === 'Identifier') {
+ if (node.name && replacements[node.name]) {
+ source.overwrite(node.start, node.end, replacements[node.name]);
+ }
+ }
+ }
+ })
+}
+exports.replaceIdentifiers = replaceIdentifiers;
6.6 bundle.js
lib\bundle.js
let fs = require('fs');
let path = require('path');
let Module = require('./module');
let MagicString = require('magic-string');
+const { hasOwnProperty, replaceIdentifiers } = require('./utils');
class Bundle {
constructor(options) {
//入口文件数据
this.entryPath = path.resolve(options.entry.replace(/\.js$/, '') + '.js');
//存放所有的模块
this.modules = {};
}
build(filename) {
const entryModule = this.fetchModule(this.entryPath);//获取模块代码
this.statements = entryModule.expandAllStatements(true);//展开所有的语句
+ this.deconflict();
const { code } = this.generate();//生成打包后的代码
fs.writeFileSync(filename, code);//写入文件系统
console.log('done');
}
+ deconflict() {
+ const defines = {};//定义的变量
+ const conflicts = {};//变量名冲突的变量
+ this.statements.forEach(statement => {
+ Object.keys(statement._defines).forEach(name => {
+ if (hasOwnProperty(defines, name)) {
+ conflicts[name] = true;
+ } else {
+ defines[name] = [];//defines.age = [];
+ }
+ //把此声明变量的语句,对应的模块添加到数组里
+ defines[name].push(statement._module);
+ });
+ });
+ Object.keys(conflicts).forEach(name => {
+ const modules = defines[name];//获取定义此变量名的模块的数组
+ modules.pop();//最后一个模块不需要重命名,保留 原来的名称即可 [age1,age2]
+ modules.forEach((module, index) => {
+ let replacement = `${name}$${modules.length - index}`;
+ debugger
+ module.rename(name, replacement);//module age=>age$2
+ });
+ });
+ }
fetchModule(importee, importer) {
let route;
if (!importer) {
route = importee;
} else {
if (path.isAbsolute(importee)) {
route = importee;
} else {
route = path.resolve(path.dirname(importer), importee.replace(/\.js$/, '') + '.js');
}
}
if (route) {
let code = fs.readFileSync(route, 'utf8');
const module = new Module({
code,
path: importee,
bundle: this
})
return module;
}
}
generate() {
let magicString = new MagicString.Bundle();
this.statements.forEach(statement => {
+ let replacements = {};
+ Object.keys(statement._dependsOn)
+ .concat(Object.keys(statement._defines))
+ .forEach(name => {
+ const canonicalName = statement._module.getCanonicalName(name);
+ if (name !== canonicalName)
+ replacements[name] = canonicalName;
+ });
const source = statement._source.clone();
if (statement.type === 'ExportNamedDeclaration') {
source.remove(statement.start, statement.declaration.start);
}
+ replaceIdentifiers(statement, source, replacements);
magicString.addSource({
content: source,
separator: '\n'
})
})
return { code: magicString.toString() }
}
}
module.exports = Bundle;
6.7 module.js
lib\module.js
const MagicString = require('magic-string');
const { parse } = require('acorn');
const { hasOwnProperty } = require('./utils');
const analyse = require('./ast/analyse');
const SYSTEMS = ['console', 'log'];
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 = {};
//存放本模块的定义变量的语句 a=>var a = 1;b =var b =2;
this.definitions = {};
//存放变量修改语句
this.modifications = {};
+ this.canonicalNames = {};
analyse(this.ast, this.code, this);
}
expandAllStatements() {
let allStatements = [];
this.ast.body.forEach(statement => {
if (statement.type === 'ImportDeclaration') return;
if (statement.type === 'VariableDeclaration') return;
let statements = this.expandStatement(statement);
allStatements.push(...statements);
});
return allStatements;
}
expandStatement(statement) {
statement._included = true;
let result = [];
//获取此语句依赖的变量
let _dependsOn = Object.keys(statement._dependsOn);
_dependsOn.forEach(name => {
//找到此变量定义的语句,添加到输出数组里
let definitions = this.define(name);
result.push(...definitions);
});
//把此语句添加到输出列表中
result.push(statement);
//找到此语句定义的变量,把定义的变量和修改语句也包括进来
//注意要先定义再修改,所以要把这行放在push(statement)的下面
const defines = Object.keys(statement._defines);
defines.forEach(name => {
//找到定义的变量依赖的修改的语句
const modifications = hasOwnProperty(this.modifications, name) && this.modifications[name];
if (modifications) {
//把修改语句也展开放到结果里
modifications.forEach(statement => {
if (!statement._included) {
let statements = this.expandStatement(statement);
result.push(...statements);
}
});
}
});
return result;
}
define(name) {
//先判断此变量是外部导入的还是模块内声明的
if (hasOwnProperty(this.imports, name)) {
//说明此变量不是模块内声明的,而是外部导入的,获取从哪个模块内导入了哪个变量
const { source, importName } = this.imports[name];
//获取这个模块
const importModule = this.bundle.fetchModule(source, this.path);
//从这个模块的导出变量量获得本地变量的名称
const { localName } = importModule.exports[importName];
//获取本地变量的定义语句
return importModule.define(localName);//name
} else {//如果是模块的变量的话
let statement = this.definitions[name];//name
if (statement) {
if (statement._included) {
return [];
} else {
return this.expandStatement(statement);
}
} else {
if (SYSTEMS.includes(name)) {
return [];
} else {
throw new Error(`变量${name}既没有从外部导入,也没有在当前的模块声明`);
}
}
}
}
+ rename(name, replacement) {
+ this.canonicalNames[name] = replacement;
+ }
+ getCanonicalName(name) {
+ return this.canonicalNames[name] || name;
+ }
}
module.exports = Module;
手写实现
1.调试
1.1 debugger.js
import { rollup, watch } from 'rollup';
import inputOptions from './rollup.config.js'
; (async function () {
//打包阶段
const bundle = await rollup(inputOptions);
//生成阶段
await bundle.generate(inputOptions.output);
//写入阶段
await bundle.write(inputOptions.output);
/*
const watcher = watch(inputOptions);
watcher.on('event', event => {
console.log(event);
});
setTimeout(() => {
watcher.close();
}, 1000); */
//关闭阶段
await bundle.close();
})();
1.2 rollup.config.js
rollup.config.js
export default {
input: "./src/index.js",
output: {
dir: 'dist',
}
}
1.3 package.json
package.json
{
"type": "module",
"scripts": {
"build": "rollup -c"
},
}
2. rollup插件
2.1 插件规范
- 插件应该有一个清晰的名称,带有
rollup-plugin-prefix
- 在package.json中包含插件关键字
- 插件应该经过测试。我们推荐mocha或ava,它们支持开箱即用的Promise
- 尽可能使用异步方法。
- 编写英文文档
- 如果合适的话,确保你的插件输出正确的
sourcemap
- 如果您的插件使用“虚拟模块”(例如,用于辅助功能),请在模块ID前面加上
\0
。这会阻止其他插件尝试处理它
2.2.插件属性
2.2.1 name
- 插件的名称,用于错误消息和警告
- Type: 字符串
2.2 Build Hooks
- 为了与构建过程交互,你的插件对象包括“钩子”
- 钩子是在构建的不同阶段调用的函数
- 钩子可以影响构建的运行方式,提供关于构建的信息,或者在构建完成后修改构建
- 有不同种类的钩子
async
钩子还可以返回解析为相同类型值的Promise
;否则,钩子将被标记为sync
first
如果有几个插件实现了这个钩子,钩子会按顺序运行,直到钩子返回一个非null
或未定义的值sequential
如果几个插件实现了这个钩子,那么它们都将按照指定的插件顺序运行。如果一个钩子是异步的,那么这种类型的后续钩子将等待当前钩子被解析parallel
如果多个插件实现了这个钩子,那么它们都将按照指定的插件顺序运行。如果一个钩子是异步的,那么这类后续钩子将并行运行,而不是等待当前钩子
Build Hooks
在构建阶段运行,该阶段由rollup.rollup(inputOptions)
触发- 它们主要负责在
rollup
处理输入文件之前定位、提供和转换输入文件 - 构建阶段的第一个钩子是
options
,最后一个钩子总是buildEnd
- 如果出现生成错误,将在此之后调用
closeBundle
2.2.1 rollup-plugin-build.js
plugins\rollup-plugin-build.js
import fs from 'fs';
function build() {
return {
name: 'build',
async watchChange(id, change) {
console.log('watchChange', id, change);
},
async closeWatcher() {
console.log('closeWatcher');
},
async options(inputOptions) {
console.log('options');
//inputOptions.input = './src/main.js';
},
async buildStart(inputOptions) {
console.log('buildStart');
},
async resolveId(source, importer) {
if (source === 'virtual') {
console.log('resolveId', source);
//如果resolveId钩子有返回值了,那么就会跳过后面的查找逻辑,以此返回值作为最终的模块ID
return source;
}
},
//加载此模块ID对应的内容
async load(id) {
if (id === 'virtual') {
console.log('load', id);
return `export default "virtual"`;
}
},
async shouldTransformCachedModule({ id, code, ast }) {
console.log('shouldTransformCachedModule');
//不使用缓存,再次进行转换
return true;
},
async transform(code, id) {
console.log('transform');
},
async moduleParsed(moduleInfo) {
console.log('moduleParsed');
},
async resolveDynamicImport(specifier, importer) {
console.log('resolveDynamicImport', specifier, importer);
},
async buildEnd() {
console.log('buildEnd');
}
}
}
export default build;
2.2.2 rollup.config.js
rollup.config.js
+import build from './plugins/rollup-plugin-build.js';
export default {
input: "./src/index.js",
output: [{
dir: 'dist',
}],
plugins: [
+ build()
]
}
2.2.4 options
字段 | 值 | |
---|---|---|
Type | (options: InputOptions) => InputOptions | null |
Kind | async, sequential | |
Previous Hook | 这是构建阶段的第一个钩子 | |
Next Hook | buildStart |
- 替换或操作传递给
rollup
的选项对象 - 返回
null
的话rollup不会替换任何内容 - 如果只需要阅读
options
,建议使用buildStart
钩子,因为在考虑了所有选项钩子的转换后,该钩子可以访问选项 - 这是唯一一个无法访问大多数插件上下文实用程序功能的钩子,因为它是在完全配置汇总之前运行的
2.2.5 buildStart
字段 | 值 |
---|---|
Type | (options: InputOptions) => void |
Kind | async, parallel |
Previous Hook | options |
Next Hook | resolveId并行解析每个入口点 |
- 每次
rollup.rollup build
都要调用此钩子 - 当您需要访问传递给rollup的选项时,建议使用这个钩子
- 因为它考虑了所有
options
钩子的转换,还包含未设置选项的正确默认值
build\plugin-buildStart.js
export default function buildStart() {
return {
name: 'buildStart',
buildStart(InputOptions) {
console.log('buildStart', InputOptions);
}
};
}
2.2.6 resolveId
字段 | 值 | ||
---|---|---|---|
Type | (source, importer) => string | false | null |
Kind | async, first | ||
Previous Hook | buildStart (如果我们正在解析入口点),moduleParsed (如果我们正在解析导入),或者作为resolveDynamicImport 的后备方案。此外,这个钩子可以在构建阶段通过调用插件钩子触发。emitFile 发出一个入口点,或在任何时候通过调用此。resolve 可手动解析id | ||
Next Hook | 如果解析的id 尚未加载,则load ,否则buildEnd |
-
定义自定义解析器
-
解析程序可用于定位第三方依赖关系等。这里
source
是导入语句中所写的导入对象,即 -
来源就是
"../bar.js"
import { foo } from '../bar.js';
-
importer
是导入模块的完全解析id -
在解析入口点时,
importer
通常是undefined -
这里的一个例外是通过
this.emitFile
生成的入口点。在这里,您可以提供一个importer
参数 -
对于这些情况,
isEntry
选项将告诉您,我们是否正在解析用户定义的入口点、发出的块,或者是否为此提供了isEntry
参数。解析上下文函数 -
例如,您可以将其用作为入口点定义自定义代理模块的机制。以下插件将代理所有入口点以注入
polyfill
导入 -
返回
null
将遵循其他resolveId
函数,最终遵循默认的解析行 -
返回
false
信号,表示源应被视为外部模块
,不包括在bundle
中 `
build\plugin-polyfill.js
//我们在polyfill id前面加上\0,告诉其他插件不要尝试加载或转换它
const POLYFILL_ID = '\0polyfill';
const PROXY_SUFFIX = '?inject-polyfill-proxy';
export default function injectPolyfillPlugin() {
return {
name: 'inject-polyfill',
async resolveId(source, importer, options) {
if (source === POLYFILL_ID) {
//重要的是,对于polyfills,应始终考虑副作用
//否则,使用`treeshake.moduleSideEffects:false`可能会阻止包含polyfill
return { id: POLYFILL_ID, moduleSideEffects: true };
}
if (options.isEntry) {
//确定实际的入口是什么。我们需要skipSelf来避免无限循环。
const resolution = await this.resolve(source, importer, { skipSelf: true, ...options });
//如果它无法解决或是外部的,只需返回它,这样Rollup就可以显示错误
if (!resolution || resolution.external) return resolution;
//在代理的加载钩子中,我们需要知道入口是否有默认导出
//然而,在那里,我们不再有完整的“解析”对象,它可能包含来自其他插件的元数据,这些插件只在第一次加载时添加
//仅在第一次加载时添加。因此我们在这里触发加载。
const moduleInfo = await this.load(resolution);
//我们需要确保即使对于treeshake来说,原始入口点的副作用也得到了考虑。moduleSideEffects:false。
//moduleSideEffects是ModuleInfo上的一个可写属性
moduleInfo.moduleSideEffects = true;
//重要的是,新入口不能以\0开头,并且与原始入口具有相同的目录,以免扰乱相对外部导入的生成
//此外,保留名称并在末尾添加一个“?查询”可以确保preserveModules将为该条目生成原始条目名称
return `${resolution.id}${PROXY_SUFFIX}`;
}
return null;
},
load(id) {
if (id === POLYFILL_ID) {
// 替换为实际的polyfill import '@babel/polyfill'
return "console.log('polyfill');";
}
if (id.endsWith(PROXY_SUFFIX)) {
const entryId = id.slice(0, -PROXY_SUFFIX.length);
//我们知道ModuleInfo.hasDefaultExport是可靠的,因为我们在等待在resolveId中的this.load
// We know ModuleInfo.hasDefaultExport is reliable because we awaited this.load in resolveId
const { hasDefaultExport } = this.getModuleInfo(entryId);
let code =
`import ${JSON.stringify(POLYFILL_ID)};` + `export * from ${JSON.stringify(entryId)};`;
//命名空间重新导出不会重新导出默认值,因此我们需要在这里进行特殊处理
if (hasDefaultExport) {
code += `export { default } from ${JSON.stringify(entryId)};`;
}
return code;
}
return null;
}
};
}
2.2.7 load
字段 | 值 | |
---|---|---|
(id) => string | null | |
Kind | async, first | |
Previous Hook | 解析加载id的resolveId 或resolveDynamicImport 。此外,这个钩子可以在任何时候从插件钩子中通过调用this.load 来触发预加载与id对应的模块 | |
Next Hook | transform 可在未使用缓存或没有使用相同代码的缓存副本时转换加载的文件,否则应使用TransformCachedModule |
- 定义自定义加载程序
- 返回
null
会推迟到其他加载函数(最终是从文件系统加载的默认行为) - 为了防止额外的解析开销,例如这个钩子已经使用了这个。parse出于某种原因,为了生成AST,这个钩子可以选择性地返回
{code,AST,map}
对象。ast
必须是标准的ESTree ast
,每个节点都有开始和结束属性。如果转换不移动代码,可以通过将map设置为null来保留现有的sourcemaps。否则,您可能需要生成源映射。请参阅关于源代码转换的部分
2.2.8 transform
字段 | 值 |
---|---|
Type | (code, id) => string |
Kind | async, sequential |
Previous Hook | load 当前处理的文件的位置。如果使用了缓存,并且有该模块的缓存副本,那么如果插件为该钩子返回true,则应shouldTransformCachedModule |
Next Hook | moduleParsed 一旦文件被处理和解析,模块就会被解析 |
- 可用于转换单个模块
- 为了防止额外的解析开销,例如这个钩子已经使用了
this.parse
出于某种原因,为了生成AST - 这个钩子可以选择性地返回
{code,AST,map}
对象 - ast必须是标准的ESTree ast,每个节点都有
start
和end
属性 - 如果转换不移动代码,可以通过将map设置为null来保留现有的sourcemaps。否则,您可能需要生成源映射。请参阅关于源代码转换的部分
npm install rollup-pluginutils @rollup/plugin-babel @babel/core @babel/preset-env -D
plugins\rollup-plugin-babel.js
```js
import { createFilter } from 'rollup-pluginutils'
import babel from '@babel/core'
function plugin(pluginOptions = {}) {
const defaultExtensions = ['.js', '.jsx']
const { exclude, include, extensions = defaultExtensions } = pluginOptions;
const extensionRegExp = new RegExp(`(${extensions.join('|')})$`)
const userDefinedFilter = createFilter(include, exclude);
const filter = id => extensionRegExp.test(id) && userDefinedFilter(id);
return {
name: 'babel',
async transform(code, filename) {
if (!filter(filename)) return null;
let result = await babel.transformAsync(code);
return result
}
}
}
export default plugin
2.2.9 shouldTransformCachedModule
字段 | 值 |
---|---|
Type | ({id, code, ast, resoledSources, moduleSideEffects, syntheticNamedExports) => boolean |
Kind: async, first | |
Previous Hook | load 加载缓存文件以将其代码与缓存版本进行比较的位置 |
Next Hook | moduleParsed if no plugin returns true, otherwise transform . |
- 如果使用了
Rollup
缓存(例如,在监视模式下或通过JavaScript API显式使用),如果在加载钩子之后,加载的代码与缓存副本的代码相同,则Rollup将跳过模块的转换钩子 - 为了防止这种情况,丢弃缓存的副本,而是转换一个模块,插件可以实现这个钩子并返回true。
- 这个钩子还可以用来找出缓存了哪些模块,并访问它们缓存的元信息
- 如果一个插件没有返回true,Rollup将触发其他插件的这个钩子,否则将跳过所有剩余的插件。
npx rollup -c -w
shouldTransformCachedModule
transform
moduleParsed
shouldTransformCachedModule
moduleParsed
2.2.10 moduleParsed
字段 | 值 |
---|---|
Type | (moduleInfo: ModuleInfo) => void |
Kind: async, parallel | |
Previous Hook | transform 转换当前处理的文件的位置 |
Next Hook | resolveId 和 resolveDynamicImport 并行解析所有发现的静态和动态导入(如果存在),否则buildEnd |
- 每当模块被
Rollup
完全解析时,就会调用这个钩子。看看this.getModuleInfo
了解传递给这个钩子的信息 - 与
transform
钩子不同,这个钩子从不缓存,可以用来获取缓存模块和其他模块的信息,包括元属性的最终形状、代码和ast
2.2.10 resolveDynamicImport
字段 | 值 |
---|---|
Type | (specifier, importer) => string |
Kind | async, first |
Previous Hook | moduleParsed 已为导入文件分配模块 |
Next Hook | load 如果钩子使用尚未加载的id ,如果动态导入包含字符串且钩子未解析,请加载resolveId ,否则为buildEnd |
- 为动态导入定义自定义解析程序
- 返回
false
表明导入应该保持原样,而不是传递给其他解析程序,从而使其成为外部的 - 与
resolveId
钩子类似,还可以返回一个对象,将导入解析为不同的id,同时将其标记为外部 - 如果动态导入被传递一个字符串作为参数,那么从这个钩子返回的字符串将被解释为一个现有的模块id,而返回null将推迟到其它解析器resolveId
index.js
import('./msg.js').then(res => console.log(res))
2.2.11 buildEnd
字段 | 值 |
---|---|
Type | (error) => void |
Kind | async, parallel |
Previous Hook | moduleParsed, resolveId or resolveDynamicImport. |
Next Hook | outputOptions 输出生成阶段的输出,因为这是构建阶段的最后一个挂钩 |
- 在
rollup
完成打包时调用,但在调用generate
或write
之前调用;你也可以返回一个Promise
- 如果在构建过程中发生错误,则会将其传递给此钩子
2.3 Output Generation Hooks
- 输出生成钩子可以提供有关生成的包的信息,并在完成后修改构建
- 输出生成阶段的第一个钩子是
outputOptions
,最后一个钩子要么generateBundle
是通过成功生成输出 - 或者在输出生成过程中的任何时候发生错误
renderError
- 此外,
closeBundle
可以作为最后一个钩子调用,但用户有责任手动调用bundle.close()
以触发此钩子
2.3.1 rollup-plugin-generation.js
plugins\rollup-plugin-generation.js
function generation() {
return {
name: 'rollup-plugin-generation',
//这个钩子是同步的,不能加async
outputOptions(outputOptions) {
console.log('outputOptions');
},
renderStart() {
console.log('renderStart');
},
banner() {
console.log('banner');
},
footer() {
console.log('footer');
},
intro() {
console.log('intro');
},
outro() {
console.log('outro');
},
renderDynamicImport() {
console.log('renderDynamicImport');
},
augmentChunkHash() {
console.log('augmentChunkHash');
},
resolveFileUrl() {
console.log('resolveFileUrl');
},
resolveImportMeta() {
console.log('resolveImportMeta');
},
renderChunk() {
console.log('renderChunk');
},
generateBundle() {
console.log('generateBundle');
},
writeBundle() {
console.log('writeBundle');
},
renderError() {
console.log('renderError');
},
closeBundle() {
console.log('closeBundle');
}
}
}
export default generation;
2.3.2 rollup.config.js
rollup.config.js
import build from './plugins/rollup-plugin-build.js';
+import generation from './plugins/rollup-plugin-generation.js';
export default {
input: "./src/index.js",
output: [{
dir: 'dist',
}],
plugins: [
build(),
+ generation()
]
}
2.3.3 outputOptions
字段 | 值 |
---|---|
Type | (outputOptions) => null |
Kind | async, parallel |
Previous Hook | buildEnd 如果这是第一次生成输出,否则为generateBundle ,writeBundle 或renderError 取决于先前生成的输出。这是输出生成阶段的第一个钩子 |
Next Hook | outputOptions 输出生成阶段的输出,因为这是构建阶段的最后一个挂钩 |
- 替换或操作传递给
bundle.generate()
的输出选项对象bundle.write()
- 返回
null
并不能代替任何东西 - 如果您只需要读取输出选项,建议使用
renderStart
钩子,因为在考虑renderStart
所有钩子的转换后,此钩子可以访问输出选项
2.3.4 renderStart
字段 | 值 |
---|---|
Type | (outputOptions, inputOptions) => void |
种类 | async, parallel |
上一个钩子 | outputOptions |
下一个钩子 | banner, footer, intro and outro 并行运行 |
- 每次初始调用
bundle.generate()
或被bundle.write()
调用 - 要在生成完成时收到通知,请使用
generateBundle
和renderError
挂钩 - 当您需要访问传递给的输出选项时,建议使用此挂钩.
bundle.generate()
或者bundle.write()
因为它考虑了所有outputOptions
挂钩的转换,并且还包含未设置选项的正确默认值。它还接收传递给的输入选项
2.3.5 banner
字段 | 值 | |
---|---|---|
Type | string | (() => string) |
Kind | async, parallel | |
Previous Hook | renderStart | |
Next Hook | 针对每个动态导入表达式 renderDynamicImport |
2.3.6 footer
字段 | 值 | |
---|---|---|
Type | string | (() => string) |
Kind | async, parallel | |
Previous Hook | renderStart | |
Next Hook | 针对每个动态导入表达式 renderDynamicImport |
2.3.7 intro
字段 | 值 | |
---|---|---|
Type | string | (() => string) |
Kind | async, parallel | |
Previous Hook | renderStart | |
Next Hook | 针对每个动态导入表达式 renderDynamicImport |
2.3.8 outro
字段 | 值 | |
---|---|---|
Type | string | (() => string) |
Kind | async, parallel | |
Previous Hook | renderStart | |
Next Hook | 针对每个动态导入表达式 renderDynamicImport |
2.3.9 renderDynamicImport
字段 | 值 |
---|---|
Type | ({format, moduleId, targetModuleId, customResolution}) => {left: string, right: string} |
Kind | async, parallel |
Previous Hook | banner , footer, intro, outro |
Next Hook | augmentChunkHash 对于每个在文件名中包含哈希的块 |
- 这个钩子提供了对如何呈现动态导入的细粒度控制
- 方法是替换导入表达式参数的左侧 ( import() 和右侧 ( ) 的代码。)
- 返回null延迟到此类型的其他钩子并最终呈现特定于格式的默认值
- format是渲染的输出格式
- moduleId执行动态导入的模块的 id
- 如果导入可以解析为内部或外部 id,targetModuleId则将设置为此 id,否则将为null
plugins\rollup-plugin-renderDynamicImport.js
export default function dynamicImportPolyfillPlugin() {
return {
name: 'dynamic-import-polyfill',
renderDynamicImport() {
return {
left: 'dynamicImportPolyfill(',
right: ', import.meta.url)'
};
}
};
}
dynamicImportPolyfill('./msg-ca034dda.js', import.meta.url).then(res => console.log(res.default));
function dynamicImportPolyfill(filename, url) {
return new Promise((resolve) => {
const script = document.createElement("script");
script.type = "module";
script.onload = () => {
resolve(window.mod);
};
const absURL = new URL(filename, url).href;
console.log(absURL);
const blob = new Blob([
`import * as mod from "${absURL}";`,
` window.mod = mod;`], { type: "text/javascript" });
script.src = URL.createObjectURL(blob);
document.head.appendChild(script);
});
}
2.3.9 augmentChunkHash
字段 | 值 |
---|---|
Type | (chunkInfo: ChunkInfo) => string |
Kind | sync, sequential |
Previous Hook | renderDynamicImport针对每个动态导入表达式 |
Next Hook | resolveFileUrl 对于每次使用import.meta.ROLLUP_FILE_URL_referenceId 和resolveImportMeta 所有其他访问import.meta |
- 可用于增加单个块的散列
- 为每个
Rollup
输出块调用 - 返回一个
false
值不会修改散列 - 真实值将传递给
hash.update
. - 这
chunkInfo
是generateBundle
不依赖文件名的属性的简化版本
2.3.10 resolveFileUrl
- import.meta是一个给JavaScript模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的URL。
字段 | 值 |
---|---|
Type | ({chunkId, fileName, format, moduleId, referenceId, relativePath}) => string |
Kind | sync, first |
Previous Hook | augmentChunkHash 对于在文件名中包含哈希的每个块 |
Next Hook | renderChunk 对于每个块 |
- 允许自定义Rollup如何解析插件通过此链接发出的文件的URL
this.emitFile
- 默认情况下,Rollup 将生成代码
import.meta.ROLLUP_FILE_URL_referenceId
该代码应正确生成发出文件的绝对URL,而与输出格式和部署代码的主机系统无关
src\index.js
import logger from 'logger'
console.log(logger);
plugins\rollup-plugin-resolveFileUrl.js
export default function resolveFileUrl() {
return {
name: 'resolveFileUrl',
resolveId(source) {
if (source === 'logger') {
return source;
}
},
load(importee) {
if (importee === 'logger') {
let referenceId = this.emitFile({ type: 'asset', source: 'console.log("logger")', fileName: "logger.js" });
return `export default import.meta.ROLLUP_FILE_URL_${referenceId}`;
}
},
resolveFileUrl({ chunkId, fileName, format, moduleId, referenceId, relativePath }) {//import.meta.url
return `new URL('${fileName}', document.baseURI).href`;
}
};
}
2.3.11 resolveImportMeta
字段 | 值 |
---|---|
Type | (property, {chunkId, moduleId, format}) => string |
Kind | sync, first |
Previous Hook | augmentChunkHash 对于在文件名中包含哈希的每个块 |
Next Hook | renderChunk 对于每个块 |
- 允许自定义 Rollup 如何处理
import.meta
,import.meta.someProperty
特别是import.meta.url
- 在 ES 模块中,
import.meta
是一个对象,import.meta.url
包含当前模块的 URL
2.3.12 renderChunk
字段 | 值 |
---|---|
Type | (code, chunk, options) => string |
Kind | async, sequential |
Previous Hook | resolveFileUrl 对于 . 的每次使用import.meta.ROLLUP_FILE_URL_referenceId 和resolveImportMeta 所有其他访问import.meta |
Next Hook | generateBundle |
- 可用于转换单个块
- 为每个
rollup
输出块文件调用。返回null将不应用任何转换
2.3.13 generateBundle
字段 | 值 |
---|---|
Type | (options, bundle, isWrite) => void |
Kind | async, sequential |
Previous Hook | renderChunk对于每个块 |
Next Hook | writeBundle 如果输出是通过生成的,否则这是输出生成阶段的最后一个钩子,如果生成另一个输出bundle.write() ,可能会再次跟随outputOptions |
- 在
bundle.generate()
之后调用 - 或者在
bundle.write()
把文件写入之前调用 - 要在写入文件后修改文件,请使用
writeBundle
挂钩 writeBundle
提供正在写入或生成的文件的完整列表及其详细信息- 您可以通过从该钩子中的捆绑对象中删除文件来防止发出文件。要发出其他文件,请使用
this.emitFile
插件上下文功能
npm i dedent
plugins\rollup-plugin-html.js
import dedent from 'dedent';
export default function html() {
return {
name: 'html',
generateBundle(options, bundle) {
let entryName;
for (let fileName in bundle) {
let assetOrChunkInfo = bundle[fileName];
//console.log(fileName, assetOrChunkInfo);
if (assetOrChunkInfo.isEntry) {
entryName = fileName;
}
}
this.emitFile({
type: 'asset',
fileName: 'index.html',
source: dedent`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>rollup</title>
</head>
<body>
<script src="${entryName}" type="module"></script>
</body>
</html>`
});
}
};
}
2.3.14 writeBundle
字段 | 值 |
---|---|
Type | (options,bundle) => void |
Kind | async, parallel |
Previous Hook | generateBundle |
Next Hook | 如果被调用,这是输出生成阶段的最后一个钩子,如果生成另一个输出,可能会再次跟随outputOptions |
bundle.write()
仅在写入所有文件后才调用- 与
generateBundle
钩子类似,bundle
提供正在写入的文件的完整列表及其详细信息
2.3.15 renderError
字段 | 值 |
---|---|
Type | (error: Error) => void |
Kind | async, parallel |
Previous Hook | renderStart从到 的任何钩子renderChunk |
Next Hook | outputOptions 如果它被调用,这是输出生成阶段的最后一个钩子,如果生成另一个输出,可能会再次跟随 |
bundle.generate()
当 rollup 在或期间遇到错误时调用bundle.write()
- 错误被传递给这个钩子。要在生成成功完成时收到通知,请使用
generateBundle
钩子
2.3.16 closeBundle
字段 | 值 | |
---|---|---|
Type | closeBundle: () => Promise | void |
Kind | async, parallel | |
Previous Hook | buildEnd 如果有构建错误.否则何时bundle.close()被调用,在这种情况下,这将是最后一个被触发的钩子。 |
- 可用于清理可能正在运行的任何外部服务
Rollup
的CLI
将确保在每次运行后调用此钩子- 但
JavaScript API
的用户有责任在bundle.close()
他们完成生成包后手动调用
3.Plugin Context
3.1 this.emitFile
- thisemitfile
- Type: (emittedFile: EmittedChunk | EmittedAsset) => string
- 发出一个包含在生成输出中的新文件,并返回一个referenceId,该ID可在不同位置用于引用发出的文件
- emittedFile 可以有两种形式之一
type EmittedChunk = {
type: 'chunk';
id: string;
name?: string;
fileName?: string;
};
type EmittedAsset = {
type: 'asset';
name?: string;
fileName?: string;
source?: string | Uint8Array;
};
3.2 this.load
- thisgetmoduleinfo
- Type: (moduleId: string) => (ModuleInfo | null)
- 返回有关相关模块的其他信息
3.3 this.load
- thisload
- Type: ({id: string, moduleSideEffects?: boolean | ‘no-treeshake’ | null, syntheticNamedExports?: boolean | string | null, meta?: {[plugin: string]: any} | null, resolveDependencies?: boolean}) => Promise
- 加载并解析与给定id对应的模块,并将附加的元信息附加到模块(如果提供)
- 这将触发与另一个模块导入该模块时相同的加载、转换和模块授权挂钩
3.4 this.resolve
- thisresolve
- 使用Rollup使用的相同插件将导入解析为模块ID(即文件名),并确定导入是否应该是外部的
- 如果返回null,则无法通过Rollup或任何插件解析导入,但用户未明确将其标记为外部
4.实战案例
- astexplorer可以把代码转成语法树
4.1 @rollup/plugin-commonjs
4.1.1 安装
npm install @rollup/plugin-commonjs --save
4.1.2 src\index.js
src\index.js
import catValue from './cat.js';
console.log(catValue);
4.1.3 src\cat.js
src\cat.js
module.exports = 'catValue';
4.1.4 rollup-plugin-commonjs.js
plugins\rollup-plugin-commonjs.js
import { createFilter } from 'rollup-pluginutils'
import MagicString from 'magic-string';
import { walk } from 'estree-walker';
import path from 'path';
export default function (pluginOptions = {}) {
const defaultExtensions = ['.js', '.jsx']
const { exclude, include, extensions = defaultExtensions } = pluginOptions;
const extensionRegExp = new RegExp(
`(${extensions.join('|')})$`
)
const userDefinedFilter = createFilter(include, exclude);
const filter = id => extensionRegExp.test(id) && userDefinedFilter(id);
return {
name: 'commonjs',
transform(code, id) {
if (!filter(id)) return null;
const result = transformAndCheckExports(this.parse, code, id)
return result;
}
}
}
function transformAndCheckExports(parse, code, id) {
const { isEsModule, ast } = analyzeTopLevelStatements(parse, code, id);
if (isEsModule) {
return null;
}
return transformCommonjs(code, id, ast)
}
function getKeypath(node) {
const parts = [];
while (node.type === 'MemberExpression') {
parts.unshift(node.property.name);
node = node.object;
}
if (node.type !== 'Identifier') return null;
const { name } = node;
parts.unshift(name);
return { name, keypath: parts.join('.') };
}
function analyzeTopLevelStatements(parse, code) {
const ast = parse(code);
let isEsModule = false;
for (const node of ast.body) {
switch (node.type) {
case 'ExportDefaultDeclaration':
isEsModule = true;
break;
case 'ExportNamedDeclaration':
isEsModule = true;
break;
case 'ImportDeclaration':
isEsModule = true;
break;
default:
}
}
return { isEsModule, ast };
}
function transformCommonjs(code, id, ast) {
const magicString = new MagicString(code);
const exportDeclarations = [];
let moduleExportsAssignment;
walk(ast, {
enter(node) {
switch (node.type) {
case 'AssignmentExpression':
if (node.left.type === 'MemberExpression') {
const flattened = getKeypath(node.left);
if (flattened.keypath === 'module.exports') {
moduleExportsAssignment = node;
}
}
break;
default:
break;
}
}
});
const { left } = moduleExportsAssignment;
const exportsName = path.basename(id, path.extname(id));
magicString.overwrite(left.start, left.end, exportsName);
magicString.prependRight(left.start, 'var ');
exportDeclarations.push(`export default ${exportsName};`);
const exportBlock = `\n\n${exportDeclarations.join('\n')}`;
magicString.trim().append(exportBlock);
return {
code: magicString.toString()
}
}
4.1.5 rollup.config.js
rollup.config.js
//import babel from '@rollup/plugin-babel'
//import babel from './plugins/rollup-plugin-babel.js'
//import commonjs from '@rollup/plugin-commonjs'
+import commonjs from './plugins/rollup-plugin-commonjs'
export default {
input: "./src/index.js",
output: {
dir: 'dist'
},
plugins: [
//babel(),
+ commonjs()
]
}
4.2 @rollup/plugin-node-resolve
4.2.1 安装
npm install @rollup/plugin-node-resolve check-is-array -D
(!) Unresolved dependencies
https://rollupjs.org/guide/en/#warning-treating-module-as-external-dependency
isarray (imported by src/index.js)
4.2.2 src\index.js
src\index.js
import isArray from 'check-is-array';
console.log(isArray);
4.2.3 rollup-plugin-node-resolve.js
plugins\rollup-plugin-node-resolve.js
import path from 'path';
import Module from 'module';
function resolve() {
return {
name: 'resolve',
//因为我们要改造根据模块的名称查找模所路径的逻辑
async resolveId(importee, importer) {
//如果是相对路径,则走默认逻辑
if (importee[0] === '.' || path.isAbsolute(importee)) {
return null;
}
let location = Module.createRequire(path.dirname(importer)).resolve(importee);
console.log(location);
return location;
}
}
}
export default resolve;
4.3 @rollup/plugin-alias
4.3.1 rollup.config.js
rollup.config.js
//import build from './plugins/rollup-plugin-build.js';
//import polyfill from './plugins/rollup-plugin-inject-polyfill.js';
//import babel from './plugins/rollup-plugin-babel.js';
//import generation from './plugins/rollup-plugin-generation.js';
//import importPolyFill from './plugins/rollup-plugin-import-polyfill.js';
//import commonjs from '@rollup/plugin-commonjs';
//import commonjs from './plugins/rollup-plugin-commonjs';
//import resolve from '@rollup/plugin-node-resolve';
import resolve from './plugins/rollup-plugin-node-resolve.js';
//import alias from '@rollup/plugin-alias';
import alias from './plugins/rollup-plugin-alias.js';
export default {
input: './src/index.js',
//watch: true,
output: {
//file: 'dist/main.js',
dir: 'dist'
},
plugins: [
resolve(),
alias({
entries: [
{ find: './xx.js', replacement: 'check-is-array' }
]
}),
],
watch: {
clearScreen: false
}
}
4.3.2 rollup-plugin-alias.js
plugins\rollup-plugin-alias.js
function matches(pattern, importee) {
if (pattern instanceof RegExp) {
return pattern.test(importee);
}
if (importee.length < pattern.length) {
return false;
}
if (importee === pattern) {
return true;
}
return importee.startsWith(pattern + '/');
}
function alias(options = {}) {
const { entries } = options;
if (entries.length === 0) {
return {
name: 'alias',
resolveId: () => null
};
}
return {
name: 'alias',
resolveId(importee, importer) {
if (!importer) {
return null;
}
const matchedEntry = entries.find((entry) => matches(entry.find, importee));
if (!matchedEntry) {
return null;
}
const updatedId = importee.replace(matchedEntry.find, matchedEntry.replacement);
//调用this.resolve意味着重新解析
return this.resolve(updatedId, importer, Object.assign({ skipSelf: true }))
.then((resolved) => resolved || { id: updatedId });
}
};
}
export default alias;