Rollup

官网: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

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

1

1.3.2 acor
  • astexplorer可以把代码转成语法树
  • acorn 解析结果符合The Estree Spec规范

1

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 + "离开");
      }
    }
  });
});

1

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里收集importsexportsdefinitions
    • 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插件

  • 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) => InputOptionsnull
Kindasync, sequential
Previous Hook这是构建阶段的第一个钩子
Next HookbuildStart
  • 替换或操作传递给rollup的选项对象
  • 返回null的话rollup不会替换任何内容
  • 如果只需要阅读options,建议使用buildStart钩子,因为在考虑了所有选项钩子的转换后,该钩子可以访问选项
  • 这是唯一一个无法访问大多数插件上下文实用程序功能的钩子,因为它是在完全配置汇总之前运行的
2.2.5 buildStart
字段
Type(options: InputOptions) => void
Kindasync, parallel
Previous Hookoptions
Next HookresolveId并行解析每个入口点
  • 每次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) => stringfalsenull
Kindasync, first
Previous HookbuildStart(如果我们正在解析入口点),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) => stringnull
Kindasync, first
Previous Hook解析加载id的resolveIdresolveDynamicImport。此外,这个钩子可以在任何时候从插件钩子中通过调用this.load来触发预加载与id对应的模块
Next Hooktransform可在未使用缓存或没有使用相同代码的缓存副本时转换加载的文件,否则应使用TransformCachedModule
  • 定义自定义加载程序
  • 返回null会推迟到其他加载函数(最终是从文件系统加载的默认行为)
  • 为了防止额外的解析开销,例如这个钩子已经使用了这个。parse出于某种原因,为了生成AST,这个钩子可以选择性地返回{code,AST,map}对象。ast必须是标准的ESTree ast,每个节点都有开始和结束属性。如果转换不移动代码,可以通过将map设置为null来保留现有的sourcemaps。否则,您可能需要生成源映射。请参阅关于源代码转换的部分
2.2.8 transform
字段
Type(code, id) => string
Kindasync, sequential
Previous Hookload 当前处理的文件的位置。如果使用了缓存,并且有该模块的缓存副本,那么如果插件为该钩子返回true,则应shouldTransformCachedModule
Next HookmoduleParsed 一旦文件被处理和解析,模块就会被解析
  • 可用于转换单个模块
  • 为了防止额外的解析开销,例如这个钩子已经使用了this.parse出于某种原因,为了生成AST
  • 这个钩子可以选择性地返回{code,AST,map}对象
  • ast必须是标准的ESTree ast,每个节点都有startend属性
  • 如果转换不移动代码,可以通过将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 Hookload 加载缓存文件以将其代码与缓存版本进行比较的位置
Next HookmoduleParsed 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 Hooktransform 转换当前处理的文件的位置
Next HookresolveIdresolveDynamicImport 并行解析所有发现的静态和动态导入(如果存在),否则buildEnd
  • 每当模块被Rollup完全解析时,就会调用这个钩子。看看this.getModuleInfo了解传递给这个钩子的信息
  • transform钩子不同,这个钩子从不缓存,可以用来获取缓存模块和其他模块的信息,包括元属性的最终形状、代码和ast
2.2.10 resolveDynamicImport
字段
Type(specifier, importer) => string
Kindasync, first
Previous HookmoduleParsed 已为导入文件分配模块
Next Hookload 如果钩子使用尚未加载的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
Kindasync, parallel
Previous HookmoduleParsed, resolveId or resolveDynamicImport.
Next HookoutputOptions 输出生成阶段的输出,因为这是构建阶段的最后一个挂钩
  • rollup完成打包时调用,但在调用generatewrite之前调用;你也可以返回一个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
Kindasync, parallel
Previous HookbuildEnd如果这是第一次生成输出,否则为generateBundlewriteBundlerenderError取决于先前生成的输出。这是输出生成阶段的第一个钩子
Next HookoutputOptions 输出生成阶段的输出,因为这是构建阶段的最后一个挂钩
  • 替换或操作传递给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()调用
  • 要在生成完成时收到通知,请使用generateBundlerenderError挂钩
  • 当您需要访问传递给的输出选项时,建议使用此挂钩.bundle.generate()或者bundle.write()因为它考虑了所有outputOptions挂钩的转换,并且还包含未设置选项的正确默认值。它还接收传递给的输入选项
2.3.5 banner
字段
Typestring(() => string)
Kindasync, parallel
Previous HookrenderStart
Next Hook针对每个动态导入表达式 renderDynamicImport
2.3.6 footer
字段
Typestring(() => string)
Kindasync, parallel
Previous HookrenderStart
Next Hook针对每个动态导入表达式 renderDynamicImport
2.3.7 intro
字段
Typestring(() => string)
Kindasync, parallel
Previous HookrenderStart
Next Hook针对每个动态导入表达式 renderDynamicImport
2.3.8 outro
字段
Typestring(() => string)
Kindasync, parallel
Previous HookrenderStart
Next Hook针对每个动态导入表达式 renderDynamicImport
2.3.9 renderDynamicImport
字段
Type({format, moduleId, targetModuleId, customResolution}) => {left: string, right: string}
Kindasync, parallel
Previous Hookbanner , footer, intro, outro
Next HookaugmentChunkHash对于每个在文件名中包含哈希的块
  • 这个钩子提供了对如何呈现动态导入的细粒度控制
  • 方法是替换导入表达式参数的左侧 ( 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
Kindsync, sequential
Previous HookrenderDynamicImport针对每个动态导入表达式
Next HookresolveFileUrl对于每次使用import.meta.ROLLUP_FILE_URL_referenceIdresolveImportMeta所有其他访问import.meta
  • 可用于增加单个块的散列
  • 为每个Rollup输出块调用
  • 返回一个false值不会修改散列
  • 真实值将传递给hash.update.
  • chunkInfogenerateBundle不依赖文件名的属性的简化版本
2.3.10 resolveFileUrl
  • import.meta是一个给JavaScript模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的URL。
字段
Type({chunkId, fileName, format, moduleId, referenceId, relativePath}) => string
Kindsync, first
Previous HookaugmentChunkHash对于在文件名中包含哈希的每个块
Next HookrenderChunk对于每个块
  • 允许自定义Rollup如何解析插件通过此链接发出的文件的URLthis.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
Kindsync, first
Previous HookaugmentChunkHash对于在文件名中包含哈希的每个块
Next HookrenderChunk对于每个块
  • 允许自定义 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
Kindasync, sequential
Previous HookresolveFileUrl对于 . 的每次使用import.meta.ROLLUP_FILE_URL_referenceIdresolveImportMeta所有其他访问import.meta
Next HookgenerateBundle
  • 可用于转换单个块
  • 为每个rollup输出块文件调用。返回null将不应用任何转换
2.3.13 generateBundle
字段
Type(options, bundle, isWrite) => void
Kindasync, sequential
Previous HookrenderChunk对于每个块
Next HookwriteBundle如果输出是通过生成的,否则这是输出生成阶段的最后一个钩子,如果生成另一个输出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
Kindasync, parallel
Previous HookgenerateBundle
Next Hook如果被调用,这是输出生成阶段的最后一个钩子,如果生成另一个输出,可能会再次跟随outputOptions
  • bundle.write()仅在写入所有文件后才调用
  • generateBundle钩子类似,bundle提供正在写入的文件的完整列表及其详细信息
2.3.15 renderError
字段
Type(error: Error) => void
Kindasync, parallel
Previous HookrenderStart从到 的任何钩子renderChunk
Next HookoutputOptions如果它被调用,这是输出生成阶段的最后一个钩子,如果生成另一个输出,可能会再次跟随
  • bundle.generate()当 rollup 在或期间遇到错误时调用bundle.write()
  • 错误被传递给这个钩子。要在生成成功完成时收到通知,请使用generateBundle钩子
2.3.16 closeBundle
字段
TypecloseBundle: () => Promisevoid
Kindasync, parallel
Previous HookbuildEnd 如果有构建错误.否则何时bundle.close()被调用,在这种情况下,这将是最后一个被触发的钩子。
  • 可用于清理可能正在运行的任何外部服务
  • RollupCLI 将确保在每次运行后调用此钩子
  • 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.实战案例

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;
  • 22
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Oracle的rollup函数主要用于分组汇总数据。当使用rollup函数时,如果有n个字段,则会按照不同的字段组合进行分组统计。具体来说,rollup函数会从左到右依次增加一个字段,进行分组统计,同时也会去掉所有分组字段进行一次总体统计。因此,使用rollup函数可以得到从0列到n列的分组维度。 举个例子,如果有一个名为T_STUDENT_GRADES的表,包含如下字段:CLASS(班级)、STUDENT(学生姓名)、SUBJECT(科目)和GRADES(成绩)。可以使用rollup函数进行分组汇总操作。 - 如果只按照CLASS字段进行分组汇总,可以使用以下语句:select CLASS, sum(GRADES) from T_STUDENT_GRADES group by rollup(CLASS)。这将得到每个班级的总成绩,以及所有班级的总成绩。 - 如果同时按照CLASS和STUDENT字段进行分组汇总,可以使用以下语句:select CLASS, STUDENT, sum(GRADES) from T_STUDENT_GRADES group by rollup(CLASS, STUDENT)。这将得到每个班级每个学生的总成绩,以及每个班级的总成绩和所有班级的总成绩。 - 如果按照CLASS、STUDENT和SUBJECT字段进行分组汇总,可以使用以下语句:select CLASS, STUDENT, SUBJECT, sum(GRADES) from T_STUDENT_GRADES group by rollup(CLASS, STUDENT, SUBJECT)。这将得到每个班级每个学生每个科目的总成绩,以及每个班级每个学生的总成绩、每个班级的总成绩和所有班级的总成绩。 通过使用rollup函数,可以方便地进行多层次的分组汇总,从而得到更详细的统计结果。<span class="em">1</span><span class="em">2</span><span class="em">3</span><span class="em">4</span>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值