Skip to content

手搓Rollup ▶️

GitHub 项目地址 modify-rollup

前言

Rollup Github开始照猫画虎 ,手写一个rollup,下面是一些前置知识,也是rollup用到的一些库

magic-string

magic-string是一个操作字符串和生成source-map的工具

js
const MagicString = require('magic-string');
const sourceCode = `export const name = 'magic-string'`;
const ms = new MagicString(sourceCode);

console.log('snip----', ms.snip(0, 6).toString());

console.log('remove----', ms.remove(0, 7).toString());

console.log('update----', ms.update(7, 12, 'let').toString());

const bundle = new MagicString.Bundle();
bundle.addSource({
  content: `const a = 'magic-string'`,
  separator: '\n'
});

bundle.addSource({
  content: `const b = 'magic-string'`,
  separator: '\n'
});

console.log('bundle----\n', bundle.toString());

AST 抽象语法树

js代码编译后会被转成AST树(抽象语法树),这颗树定义了代码的结构,通过操纵这颗树,我们可以精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作

查看AST语法树 ast Explorer

AST工作流

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

acorn

acorn js解析器

  • 使用 acorn 库解析 js代码 import $ from 'jquery'
  • AST 遍历实现
    • 实现了自定义的 AST 遍历函数 walk 和 visit
    • 支持进入节点(enter)和离开节点(leave)两种回调
    • 递归遍历所有子节点
  • 遍历过程
    • 对 AST 中的每个节点执行深度优先遍历
    • 打印每个节点的进入和离开事件及节点类型
js
const acorn = require('acorn');
const sourceCode = `import $ from 'jquery'`;
const ast = acorn.parse(sourceCode, {
  locations: true,
  ranges: true,
  sourceType: 'module',
  ecmaVersion: 8
});


const walk = (astNode, {enter, leave}) => {
  visit(astNode, null, enter, leave);
};

const visit = (astNode, parent, enter, leave) => {
  if (enter) {
    enter(astNode, parent);
  }

  const keys = Object.keys(astNode).filter(key =>
    typeof astNode[key] === 'object'
  );

  keys.forEach(key => {
    const value = astNode[key];
    if (Array.isArray(value)) {
      value.forEach(childNode => {
        if (childNode.type) {
          visit(childNode, astNode, enter, leave);
        }
      });
    } else if (value && value.type && typeof value === 'object') {
      visit(value, astNode, enter, leave);
    }
  });

  if (leave) {
    leave(astNode, parent);
  }
};


ast.body.forEach(node => {
  walk(node, {
    enter(node) {
      console.log('enter:', node.type);
    }, leave(node) {
      console.log('leave:', node.type);
    }
  });
});

作用域

实现作用域的层次结构和变量查找功能

  • constructor(options):初始化作用域
    • name:作用域名称
    • parent:父作用域引用,形成作用域链
    • names:当前作用域内定义的变量名数组
  • add(name):向当前作用域添加变量名
  • findDefiningScope(name):查找定义指定变量的作用域
    • 首先在当前作用域查找
    • 如果未找到且存在父作用域,则递归向上查找
    • 如果找到则返回对应作用域,否则返回null
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;
    }
  }
}

const a = 1;

function one() {
  const b = 1;

  function two() {
    const c = 2;
    console.log(a, b, c);
  }
}

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(
  twoScope.findDefiningScope('a')?.name,
  twoScope.findDefiningScope('b')?.name,
  twoScope.findDefiningScope('c')?.name
);

参考资料

开始手搓 👏

主入口(配置文件 rollup.config.js)

  • entry 入口文件
  • output 输出文件
  • rollup 函数(手搓rollup实现)
js
const path = require('path');
const rollup = require('../lib/rollup');

const entry = path.resolve(__dirname, 'main.js');
const output = path.resolve(__dirname, '../dist/bundle.js');

rollup(entry, output);

主实现(rollup.js)

  • 上一步定义的rollup函数传入的两个参数
  • 同时将读取->转换->生成代码,抽离出去,也就是放在一个bundle类中
js
const Bundle = require('./bundle');

function rollup(entry, output) {
  const bundle = new Bundle({entry});
  bundle.build(output);
}

/* global module */
module.exports = rollup;

打包类(bundle.js)

bundle.js负责整个打包流程的管理。接收入口文件路径并生成最终的打包文件

  • constructor(options) 初始化打包器:接收配置选项,解析入口文件的绝对路径
  • build(output) 执行打包构建
    • 调用 fetchModule 获取入口模块
    • 通过 expandAllStatements 展开所有需要的语句
    • 调用 generate 生成最终代码
    • 将结果写入输出文件
  • fetchModule(importer) 加载模块
    • 读取指定路径的文件内容
    • 创建并返回 Module 实例
  • generate() 生成最终代码
    • 使用 MagicString.Bundle 处理代码合并
    • 将所有语句添加到bundle
    • 返回生成的代码字符串
js
const fs = require('fs');
const path = require('path');
const MagicString = require('magic-string');
const Module = require('./module');

class Bundle {
  constructor(options) {
    this.entryPath = path.resolve(options.entry);
  }

  build(output) {
    const entryModule = this.fetchModule(this.entryPath);
    // console.log('entryModule:', entryModule);
    this.statements = entryModule.expandAllStatements();
    // console.log('statements:', this.statements);
    const {code} = this.generate();
    // console.log('code:', code);
    fs.writeFileSync(output, code);
    console.log('modify-rollup bundle success');
  }

  fetchModule(importer) {
    let route = importer;
    if (route) {
      let code = fs.readFileSync(route, 'utf8');
      return new Module({
        code, path: importer, bundle: this
      });
    }
  }

  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()};
  }
}

/* global module */
module.exports = Bundle;
  • 在通过fetchModule加载模块后,会去analyse里面解析,这个解析放到后面看
  • 再通过expandAllStatements去展开这个AST树后取到里面所有的语句,如下图debugger调试可以看到

解析模块类(module.js)

用于负责解析模块代码、分析依赖关系

  • constructor({code, path, bundle}) 初始化模块
    • 使用 MagicString 包装源代码
    • 调用 acorn.parse 生成AST
    • 调用 analyse 函数分析模块
  • expandAllStatements() 展开所有语句
    • 遍历AST的body节点
    • 调用 expandStatement 处理每个语句
    • 返回所有需要包含的语句数组
  • expandStatement(statement) 展开单个语句
    • 标记语句为已包含(_included = true)
    • 返回语句数组
js
const MagicString = require('magic-string');
const analyse = require('./analyse');
const {parse} = require('acorn');

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;
  }
}

/* global module */
module.exports = Module;

分析模块函数(analyse.js)

AST节点属性增强

  • 为抽象语法树(AST)中的每个语句节点添加自定义属性
  • 便于后续的代码处理和打包操作
添加属性类型用途说明
_included布尔值,默认为 false标记该语句是否需要包含在最终打包结果中可写:writable: true,允许后续修改
_module对象引用指向所属的 module 对象便于访问模块相关信息
_source代码片段存储该语句对应的原始代码文本通过 code.snip(statement.start, statement.end) 提取
js
/**
 * 分析抽象语法树,给statement定义属性
 * @param {*} ast 抽象语法树
 * @param {*} code 代码字符串
 * @param {*} module 模块对象
 * */
function analyse(ast, code, module) {
  // 给statement定义属性
  ast.body.forEach(statement => {
    Object.defineProperties(statement, {
      _included: {value: false, writable: true},
      _module: {value: module},
      _source: {value: code.snip(statement.start, statement.end)}
    });
  });
}

/* global module */
module.exports = analyse;

这个就是遍历AST树之后给每条语句添加这三个属性,其中_source属性是通过code.snip(statement.start, statement.end) 提取的语句原始代码文本

测试

到这里完成了第一步,之后运行主入口的测试文件进行测试,这里还没有对代码进行如何处理,只是单纯的先转AST,再从AST取出源代码写入到输出文件。

tree-shaking

Tree-shaking 是一种基于 ES 模块静态结构的死代码消除技术

测试实现原理

以下列代码为例:main.js是主入口,他引用了message.js 模块,message.js模块中定义了两个变量nameage,在打包之后,bundle.js中只保留了nameage 这两个变量,而要去掉这些import和export的语句

js
import {age, name} from './message';

const say = () => {
  console.log('name:', name);
  console.log('age:', age);
};

say();
js
export let name = 'modify';
export let age = 18;
js
let name = 'modify';
let age = 18;
const say = () => {
  console.log('name:', name);
  console.log('age:', age);
};
say();

分析模块函数(analyse.js)

module.js构造函数中添加三个属性

js
this.imports = {};
this.exports = {};
// 存放本模块的定义变量的语句
this.definitions = {};

接下来就是重头戏了,修改analyse.js函数,在遍历AST树的过程中(下面贴上了AST语法树的json格式,帮助理解代码) ,记录下每个模块的importsexports,以及每个语句定义的变量

  • 添加_dependOn_defines属性,分别存放本模块的依赖变量(即import导入的)和定义的变量
  • 判断import语句,即type==='ImportDeclaration'
    • 此时去取值source.value,即模块的路径./message
    • 同时去specifiers里面找imported.name也就是导入来的变量,和local.name也就是本模块使用的变量名
    • 记录到imports[local.name],表示在主入口main.js中使用了该变量(作为键),值为source即对应来源模块,importedName即为来源模块导出的名称
  • 判断export语句,即type==='ExportNamedDeclaration'
    • 此时去取值specifiers里面的exported.name,即导出的变量名
    • 记录到exports[exported.name],表示在message.js模块中导出了该变量(作为键),值为exported.name即变量名
  • 处理变量的作用域,也就是说如果我导入了一个name但是在一个方法内部我也使用了一个局部的name ,那么在打包的时候我们需要知道要去替换哪个name
    • 定义一个全局作用域链,这个全局是针对于入口文件的
    • 开始判断,如果是函数声明就加在全局作用域链中,函数内部定义的变量加到函数作用域链中
    • 变量声明加到全局作用域链中
main.js转成的AST树
json
{
  "type": "Program",
  "start": 0,
  "end": 127,
  "body": [
    {
      "type": "ImportDeclaration",
      "start": 0,
      "end": 36,
      "specifiers": [
        {
          "type": "ImportSpecifier",
          "start": 8,
          "end": 11,
          "imported": {
            "type": "Identifier",
            "start": 8,
            "end": 11,
            "name": "age"
          },
          "local": {
            "type": "Identifier",
            "start": 8,
            "end": 11,
            "name": "age"
          }
        },
        {
          "type": "ImportSpecifier",
          "start": 13,
          "end": 17,
          "imported": {
            "type": "Identifier",
            "start": 13,
            "end": 17,
            "name": "name"
          },
          "local": {
            "type": "Identifier",
            "start": 13,
            "end": 17,
            "name": "name"
          }
        }
      ],
      "source": {
        "type": "Literal",
        "start": 24,
        "end": 35,
        "value": "./message",
        "raw": "'./message'"
      }
    },
    {
      "type": "VariableDeclaration",
      "start": 38,
      "end": 118,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 44,
          "end": 117,
          "id": {
            "type": "Identifier",
            "start": 44,
            "end": 47,
            "name": "say"
          },
          "init": {
            "type": "ArrowFunctionExpression",
            "start": 50,
            "end": 117,
            "id": null,
            "expression": false,
            "generator": false,
            "async": false,
            "params": [],
            "body": {
              "type": "BlockStatement",
              "start": 56,
              "end": 117,
              "body": [
                {
                  "type": "ExpressionStatement",
                  "start": 60,
                  "end": 87,
                  "expression": {
                    "type": "CallExpression",
                    "start": 60,
                    "end": 86,
                    "callee": {
                      "type": "MemberExpression",
                      "start": 60,
                      "end": 71,
                      "object": {
                        "type": "Identifier",
                        "start": 60,
                        "end": 67,
                        "name": "console"
                      },
                      "property": {
                        "type": "Identifier",
                        "start": 68,
                        "end": 71,
                        "name": "log"
                      },
                      "computed": false,
                      "optional": false
                    },
                    "arguments": [
                      {
                        "type": "Literal",
                        "start": 72,
                        "end": 79,
                        "value": "name:",
                        "raw": "'name:'"
                      },
                      {
                        "type": "Identifier",
                        "start": 81,
                        "end": 85,
                        "name": "name"
                      }
                    ],
                    "optional": false
                  }
                },
                {
                  "type": "ExpressionStatement",
                  "start": 90,
                  "end": 115,
                  "expression": {
                    "type": "CallExpression",
                    "start": 90,
                    "end": 114,
                    "callee": {
                      "type": "MemberExpression",
                      "start": 90,
                      "end": 101,
                      "object": {
                        "type": "Identifier",
                        "start": 90,
                        "end": 97,
                        "name": "console"
                      },
                      "property": {
                        "type": "Identifier",
                        "start": 98,
                        "end": 101,
                        "name": "log"
                      },
                      "computed": false,
                      "optional": false
                    },
                    "arguments": [
                      {
                        "type": "Literal",
                        "start": 102,
                        "end": 108,
                        "value": "age:",
                        "raw": "'age:'"
                      },
                      {
                        "type": "Identifier",
                        "start": 110,
                        "end": 113,
                        "name": "age"
                      }
                    ],
                    "optional": false
                  }
                }
              ]
            }
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "ExpressionStatement",
      "start": 120,
      "end": 126,
      "expression": {
        "type": "CallExpression",
        "start": 120,
        "end": 125,
        "callee": {
          "type": "Identifier",
          "start": 120,
          "end": 123,
          "name": "say"
        },
        "arguments": [],
        "optional": false
      }
    }
  ],
  "sourceType": "module"
}
analyse.js
js
const walk = require('./walk');
const Scope = require('./scope');
const {hasOwnProperty} = require('./utils');

/**
 * 分析抽象语法树,给statement定义属性
 * @param {*} ast 抽象语法树
 * @param {*} code 代码字符串
 * @param {*} module 模块对象
 * */
function analyse(ast, code, module) {
  // 开始找import和export
  ast.body.forEach(statement => {
    Object.defineProperties(statement, {
      _included: {value: false, writable: true},
      _module: {value: module},
      _source: {value: code.snip(statement.start, statement.end)},
      // 依赖的变量
      _dependOn: {value: {}},
      // 存放本语句定义了哪些变量
      _defines: {value: {}}
    });
    // 判断类型是否为ImportDeclaration,即import语句
    if (statement.type === 'ImportDeclaration') {
      // 找到其来源,即./message
      let source = statement.source.value;
      // 找到其导入的变量
      statement.specifiers.forEach(specifier => {
        let importedName = specifier.imported.name;
        let localName = specifier.local.name;
        // 表示import导入的变量
        module.imports[localName] = {
          source, importedName
        };
      });
    }
    // 判断类型是否为ExportNamedDeclaration,即export语句
    else if (statement.type === 'ExportNamedDeclaration') {
      const {declaration} = statement;
      if (declaration && declaration.type === 'VariableDeclaration') {
        const {declarations} = declaration;
        declarations.forEach(item => {
          const {name} = item.id;
          module.exports[name] = {name};
        });
      }
    }
  });

  // 模块全局作用域
  let globalScope = new Scope({name: 'global', names: [], parent: null});
  // 创建作用域链
  ast.body.forEach(statement => {

    // 把模块下的变量等加到全局作用域下
    function addToScope(name) {
      globalScope.add(name);
      if (!globalScope.parent) {
        statement._defines[name] = true;
        module.definitions[name] = statement;
      }
    }

    function checkForRead(node) {
      if (node.type === 'Identifier') {
        // 表示当前语句依赖了node.name
        statement._dependOn[node.name] = true;
      }
    }

    function checkForWriter(node) {
      function addNode(node) {
        const {name} = node;
        statement._modifies[name] = true;
        if (!hasOwnProperty(module.modifications, name)) {
          module.modifications[name] = [];
        }
        module.modifications[name].push(statement);
      }

      if (node.type === 'AssignmentExpression') {
        addNode(node.left);
      } else if (node.type === 'UpdateExpression') {
        addNode(node.argument);
      }
    }

    walk(statement, {
      enter(node) {
        checkForRead(node);
        checkForWriter(node);
        let newScope;
        switch (node.type) {
          case 'FunctionDeclaration':
            // case 'ArrowFunctionExpression':
            // 先把函数名添加到当前作用域
            addToScope(node.id.name);
            // 把参数添加到当前作用域
            const names = node.params.map(param => param.name);
            // 创建函数的作用域
            newScope = new Scope({name: node.id.name, names, parent: globalScope});
            break;
          case 'VariableDeclaration':
            // 变量声明
            node.declarations.forEach(declaration => {
              addToScope(declaration.id.name);
            });
            break;
          default:
            break;
        }
        if (newScope) {
          Object.defineProperty(node, '_scope', {value: newScope});
          globalScope = newScope;
        }
      }, leave(node) {
        if (hasOwnProperty(node, '_scope')) {
          globalScope = globalScope.parent;
        }
      }
    });
  });
}


/* global module */
module.exports = analyse;

解析模块类(module.js)

这里只需要调整一下AST树再展开还原成源码,先做第一遍展开,对import语句进行过滤

js
function expandAllStatements() {
  let allStatements = [];
  this.ast.body.forEach(statement => {
    if (statement.type === 'ImportDeclaration') {
      return;
    }
    let statements = this.expandStatement(statement);
    allStatements.push(...statements);
  });
  return allStatements;
}

再添加一个方法,用来区分变量是导入还是内部的,直接通过变量名去imports里面找,因为在analyse 中,把所有导入的变量都加到imports里面了,并且在每次找到一个导入的变量时,去遍历加载他导入的模块,然后进行递归,直到找到

js
function define(name) {
  // 区分变量是导入还是内部的
  if (hasOwnProperty(this.imports, name)) {
    // 找到是哪个模块导入的
    const {source, importedName} = this.imports[name];
    const importModule = this.bundle.fetchModule(source, this.path);
    const exportName = importModule.exports[importedName].name;
    return importModule.define(exportName);
  } else {
    // 如果非导入模块,是本地模块的话,获取此变量的变量定义语句
    let statement = this.definitions[name];
    if (statement && !statement._included) {
      return this.expandStatement(statement);
    } else {
      return [];
    }
  }
}
  • 标记已处理:首先将当前语句标记为已包含(避免重复处理)
  • 收集依赖定义
    • statement._dependOn 存储当前语句依赖的变量列表(例如使用了其他地方定义的变量)
    • 对每个依赖变量,通过 this.define(name) 递归获取其定义语句(可能来自当前模块或导入模块),并添加到结果中
    • 作用:确保执行当前语句前,所有依赖的变量已被定义
  • 将当前语句本身添加到结果数组(依赖已收集完毕,现在可以执行该语句)
  • 收集变量修改语句
    • statement._defines 存储当前语句定义的变量(例如 let a = 1 中的 a)
    • 对每个定义的变量,查找其后续修改语句(this.modifications),并递归展开这些修改语句(确保修改语句的依赖也被处理)
    • 作用:确保变量定义后,所有对该变量的修改都被包含在执行序列中
js
function expandStatement(statement) {
  statement._included = true;
  let result = [];
  const _dependOn = Object.keys(statement._dependOn);
  _dependOn.forEach(name => {
    let definitions = this.define(name);
    result.push(...definitions);
  });
  result.push(statement);
  // 找此语句定义的变量以及其修改的变量语句
  const defines = Object.keys(statement._defines);
  defines.forEach(name => {
    // 找其修改语句
    const modifications = hasOwnProperty(this.modifications, name) && this.modifications[name];
    if (modifications) {
      modifications.forEach(modification => {
        if (!modification._included) {
          const expanded = this.expandStatement(modification);
          result.push(...expanded);
        }
      });
    }
  });
  return result;
}

打包类(bundle.js)

generate方法中,添加判断排除导出的语句

js
if (statement.type === 'ExportNamedDeclaration') {
  source.remove(statement.start, statement.declaration.start);
}

自此也就完成了对导入导出的处理,回到了这一步测试实现原理

块级作用域

案例说明

使用以下代码段,当有使用var的时候需要进行变量提升,同时当使用let的时候是块级作用域,应该直接在执行打包的时候直接抛出异常

js
if (true) {
  var flag = true;
}
console.log('flag:', flag);

作用域链(scope.js)

添加一个block属性,用于判断当前作用域是否为块级作用域,同时在add方法中添加变量的时候,判断当前作用域是否为块级作用域,如果是,则添加到父作用域中(表示如果在会计作用于当中用到的是var ,要对这个var进行变量提升)

js
/**
 * 添加变量到作用域
 * @param {string} name 变量名
 * @param {boolean} isBlockDeclaration 是否为块级声明
 * */
function add(name, isBlockDeclaration) {
  // 不是块级声明,且为块级作用域,进行变量提升
  if (!isBlockDeclaration && this.block) {
    this.parent.add(name, isBlockDeclaration);
  } else {
    this.names.push(name);
  }
}

分析模块函数(analyse.js)

先对函数声明(全局作用域)、变量声明(判断他是不是var)、块级作用域(直接提升)进行判断,并创建对应的作用域,下面代码是省略版

js
switch (node.type) {
  case 'FunctionDeclaration':
    newScope = new Scope({name: node.id.name, names, parent: globalScope, block: false});
    break;
  case 'VariableDeclaration':
    node.declarations.forEach(declaration => {
      const flag = node.kind === 'const' || node.kind === 'let';
      addToScope(declaration.id.name, flag);
    });
    break;
  case 'BlockStatement':
    newScope = new Scope({name: 'block', names: [], parent: globalScope, block: true});
    break;
  default:
    break;
}

同时将作用域进行加到全局作用域(上级作用域)的方法也进行调整

js
function addToScope(name, isBlockDeclaration) {
  globalScope.add(name, isBlockDeclaration);
  if (!globalScope.parent || (!isBlockDeclaration && globalScope.block)) {
    statement._defines[name] = true;
    module.definitions[name] = statement;
  }
}

解析模块类(module.js)

define方法中,对第一个else进行处理,判断是否是全局变量,不是的话抛出异常(因为前面排除了变量定义,所以这里是当使用这个变量的时候,此时还没有这个变量即表示该变量未定义,抛出异常)

js
// 如果非导入模块,是本地模块的话,获取此变量的变量定义语句
let statement = this.definitions[name];
if (statement) {
  if (!statement._included) {
    return this.expandStatement(statement);
  } else {
    return [];
  }
} else {
  // 排除掉系统变量
  if (SYSTEM_VAR.includes(name)) {
    return [];
  } else {
    throw new Error(`${name} is not defined`);
  }
}

变量重命名

存在问题&打包

首先假设现在有三个person.js,然后在main.js中引入,然后打包,打包结果如下:【bundel.js 】,这个结果显然不太合理,因为person变量被重复定义了,那么应该如何解决这个问题呢?

再看一下用rollup进行打包,结果如下【rollup-bundle.js】,这个结果的变量被重命名了

js
const person = 'person';
export const person1 = person + '1';
js
import {person1} from './person1';
import {person2} from './person2';
import {person3} from './person3';

console.log('person1:', person1);
console.log('person2:', person2);
console.log('person3:', person3);
js
const person = 'person';
const person1 = person + '1';
console.log('person1:', person1);
const person = 'person';
const person2 = person + '2';
console.log('person2:', person2);
const person = 'person';
const person3 = person + '3';
console.log('person3:', person3);
js
const person$2 = 'person';
const person1 = person$2 + '1';

const person$1 = 'person';
const person2 = person$1 + '2';

const person = 'person';
const person3 = person + '3';

console.log('person1:', person1);
console.log('person2:', person2);
console.log('person3:', person3);

打包类(bundle.js)

在进行构建打包结果之前添加deConflict方法,用于处理变量名冲突的问题

  • 定义两个变量来记录全部定义的变量,同时记录下有变量名冲突的变量
  • 遍历定义过的变量,判断是否和已定义的变量冲突,如果冲突,则将变量名进行重命名
  • module.reName方法用于对变量进行重命名,也就是在module.js类中添加了一个属性(对象),键是旧名字、值是新名字
js
function 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[name].push(statement._module);
    });
  });

  Object.keys(conflicts).forEach(name => {
    const modules = defines[name];
    // 最后一个用这个名字可以不做处理
    modules.pop();
    modules.forEach((module, index) => {
      const replaceName = `${name}${modules.length - index}`;
      module.reName(name, replaceName);
    });
  });
}

随后在generate方法遍历语句中对变量进行重命名,对所有的定义的变量&所有引用依赖的变量合并(这里所有的变量进行重命名)

js
let replaceNames = {};
// 定义的变量、依赖的变量都要进行重命名
Object.keys(statement._defines).concat(Object.keys(statement._dependOn)).forEach(name => {
  const canonicalName = statement._module.getCanonicalName(name);
  if (canonicalName !== name) {
    replaceNames[name] = canonicalName;
  }
});

在构建源码之前添加replaceIdentifier方法,用于对变量进行重命名,直接用magicString进行替换

js
this.replaceIdentifier(statement, source, replaceNames);

magicString.addSource({
  content: source, separator: '\n'
});

function replaceIdentifier(statement, source, replaceNames) {
  walk(statement, {
    enter(node) {
      // 表示是一个标识符,且是需要重命名的
      if (node.type === 'Identifier' && node.name && replaceNames[node.name]) {
        source.overwrite(node.start, node.end, replaceNames[node.name]);
      }
    }
  });
}

By Modify.