Rollup-构建原理及简易实现

一、Rollup 概述

官网地址:https://rollupjs.org/guide/en/

Rollup 是什么

我们先看看 Rollup 的作者 Rich Harris 是怎么讲的?Rollup 是一个模块化的打包工具。本质上,它会合并 JavaScript 文件。而且你不需要去手动指定它们的顺序,或者去担心文件之间的变量名冲突。它的内部实现会比说的复杂一点,但是它就是这么做的 —— 合并。

对比 Webpack

webpack 对前端来说是再熟悉不过的工具了,它提供了强大的功能来构建前端的资源,包括 html/js/ts/css/less/scss ... 等语言脚本,也包括 images/fonts ... 等二进制文件。正是因为 webpack 拥有如此强大的功能,所以 webpack 在进行资源打包的时候,就会产生很多冗余的代码(如果你有查看过 webpack 的 bundle 文件,便会发现)。

而对于一些项目(特别是类库)只有 js,而没有其他的静态资源文件,使用 webpack 就有点大才小用了,因为 webpack bundle 文件的体积略大,运行略慢,可读性略低。这个时候就可以选择 Rollup

Rollup是一个模块打包器,支持 ES6 模块,支持 Tree-shaking,但不支持 webpack 的 code-splitting、模块热更新等,这意味着它更适合用来做类库项目的打包器而不是应用程序项目的打包器。

简单总结

对于应用适用于 webpack,对于类库更适用于 Rollup,react/vue/anngular 都在用Rollup作为打包工具

二、Rollup 前置知识

在阐述 Rollup 的构建原理之前,我们需要了解一些前置知识

magic-string

magic-string 是 Rollup 作者写的一个关于字符串操作的库,这个库主要是对字符串一些常用方法进行了封装

 
 
 
 
  1. var MagicString = require('magic-string')
  2. var magicString = new MagicString('export var name = "zhangsan"')
  3. // 以下所有操作都是基于原生字符串
  4. // 类似于截取字符串
  5. console.log(magicString.snip(0, 6).toString()) // export
  6. // 从开始到结束删除
  7. console.log(magicString.remove(0, 7).toString()) //  var name = "zhangsan"
  8. // 多个模块,把他们打包在一个文件里,需要把很多文件的源代码合并在一起
  9. let bundleString = new MagicString.Bundle();
  10. bundleString.addSource({
  11.     content: 'console.log(hello)',
  12.     separator: '\n'
  13. })
  14. bundleString.addSource({
  15.     content: 'console.log(world)',
  16.     separator: '\n'
  17. })
  18. // // 原理类似
  19. // let str = ''
  20. // str += 'console.log(hello);\n'
  21. // str += 'console.log(world);\n'
  22. console.log(bundleString.toString()) 
  23. // hello
  24. // world

AST

通过 javascript parse 可以把代码转化为一颗抽象语法树 AST,这颗树定义了代码的结构,通过操纵这个树,我们可以精确的定位到声明语句、赋值语句、运算符语句等等,实现对代码的分析、优化、变更等操作 源代码:main.js

 
 
 
 
  1. // main.js
  2. import { a } from './a'
  3. console.log(a)

转化为 AST 是长这样子的,如下:

 
 
 
 
  1. {
  2.   "type": "Program", // 这个 AST 类型为 Program,表明是一个程序
  3.   "start": 0,
  4.   "end": 40,
  5.   "body": [ // body 是一个数组,每一条语句都对应 body 下的一个语句
  6.     {
  7.       "type": "ImportDeclaration", // 导入声明类型
  8.       "start": 0,
  9.       "end": 23,
  10.       "specifiers": [
  11.         {
  12.           "type": "ImportSpecifier",
  13.           "start": 9,
  14.           "end": 10,
  15.           "imported": {
  16.             "type": "Identifier",
  17.             "start": 9,
  18.             "end": 10,
  19.             "name": "a" // 导入模块命名 name 'a'
  20.           },
  21.           "local": {
  22.             "type": "Identifier",
  23.             "start": 9,
  24.             "end": 10,
  25.             "name": "a" // 本地模块命名,同 imported.name
  26.           }
  27.         }
  28.       ],
  29.       "source": {
  30.         "type": "Literal",
  31.         "start": 18,
  32.         "end": 23,
  33.         "value": "./a", // 导入路径 './a'
  34.         "raw": "'./a'"
  35.       }
  36.     },
  37.     {
  38.       "type": "ExpressionStatement", // 表达式类型
  39.       "start": 24,
  40.       "end": 38,
  41.       "expression": {
  42.         "type": "CallExpression", // 调用表达式类型
  43.         "start": 24,
  44.         "end": 38,
  45.         "callee": {
  46.           "type": "MemberExpression",
  47.           "start": 24,
  48.           "end": 35,
  49.           "object": {
  50.             "type": "Identifier",
  51.             "start": 24,
  52.             "end": 31,
  53.             "name": "console"
  54.           },
  55.           "property": {
  56.             "type": "Identifier",
  57.             "start": 32,
  58.             "end": 35,
  59.             "name": "log"
  60.           },
  61.           "computed": false,
  62.           "optional": false
  63.         },
  64.         "arguments": [
  65.           {
  66.             "type": "Identifier",
  67.             "start": 36,
  68.             "end": 37,
  69.             "name": "a"
  70.           }
  71.         ],
  72.         "optional": false
  73.       }
  74.     }
  75.   ],
  76.   "sourceType": "module"
  77. }

AST 工作流

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

acorn

acorn 是一个 JavaScript 语法解析器,它将 JavaScript 字符串解析成语法抽象树 AST 如果想了解 AST 语法树可以点下这个网址https://astexplorer.net/

作用域/作用域链

在 js 中,作用域是用来规定变量访问范围的规则, 作用域链是由当前执行环境和上层执行环境的一系列变量对象组成的,它保证了当前执行环境对符合访问权限的变量和函数的有序访问

三、Rollup

Rollup 是怎样工作的呢?

你给它一个入口文件 —— 通常是 index.js。Rollup 将使用 Acorn 读取解析文件 —— 将返回给我们一种叫抽象语法树(AST)的东西。一旦有了 AST ,你就可以发现许多关于代码的东西,比如它包含哪些 import 声明。

假设 index.js 文件头部有这样一行:

 
 
 
 
  1. import foo from './foo.js';

这就意味着 Rollup 需要去加载,解析,分析在 index.js 中引入的 ./foo.js。重复解析直到没有更多的模块被加载进来。更重要的是,所有的这些操作都是可插拔的,所以您可以从 node_modules 中导入或者使用 sourcemap-aware 的方式将 ES2015 编译成 ES5 代码。

在 Rollup 中,一个文件就是一个模块,每个模块都会根据文件的代码生成一个 AST 抽象语法树。

分析 AST 节点,就是看这个节点有没有调用函数方法,有没有读到变量,有,就查看是否在当前作用域,如果不在就往上找,直到找到模块顶级作用域为止。如果本模块都没找到,说明这个函数、方法依赖于其他模块,需要从其他模块引入。如果发现其他模块中有方法依赖其他模块,就会递归读取其他模块,如此循环直到没有依赖的模块为止 找到这些变量或着方法是在哪里定义的,把定义语句包含进来即可 其他无关代码一律不要

看如下代码,我们先实际操作一下:

 
 
 
 
  1. // index.js
  2. import { foo } from "./foo";
  3. foo()
  4. var city = 'hangzhou'
  5. function test() {
  6.     console.log('test')
  7. }
  8. console.log(test())
 
 
 
 
  1. // foo.js
  2. import { bar } from "./bar";
  3. export function foo() {
  4.     console.log('foo')
  5. }
 
 
 
 
  1. // bar.js
  2. export function bar() {
  3.     console.log('bar')
  4. }
 
 
 
 
  1. // rollup.config.js
  2. export default {
  3.     input: './src/index.js',
  4.     output: {
  5.         file: './dist/bundle.js', // 打包后的存放文件
  6.         format: 'cjs', //输出格式 amd es6 life umd cjs
  7.         name: 'bundleName', //如果输出格式 life,umd 需要指定一个全局变量
  8.     }
  9. };

执行 npm run build,会得到如下结果:

 
 
 
 
  1. 'use strict';
  2. function foo() {
  3.     console.log('foo');
  4. }
  5. foo();
  6. function test() {
  7.     console.log('test');
  8. }
  9. console.log(test());

以上,我们可以看到Rollup 只是会合并你的代码 —— 没有任何浪费。所产生的包也可以更好的缩小。有人称之为 “作用域提升(scope hoisting)”。其次,它把你导入的模块中的未使用代码移除。这被称为“(摇树优化)treeshaking”。总之,Rollup 就是一个模块化的打包工具。

接下来我们进入源码,具体分析下 Rollup 的构建流程

Rollup 构建流程分析

Rollup 源码结构

 
 
 
 
  1. │  bundle.js // Bundle 打包器,在打包过程中会生成一个 bundle 实例,用于收集其他模块的代码,最后再将收集的代码打包到一起。
  2. │  external-module.js // ExternalModule 外部模块,例如引入了 'path' 模块,就会生成一个 ExternalModule 实例。
  3. │  module.js // Module 模块,module 实例。
  4. │  rollup.js // rollup 函数,一切的开始,调用它进行打包。
  5. ├─ast // ast 目录,包含了和 AST 相关的类和函数
  6. │      analyse.js // 主要用于分析 AST 节点的作用域和依赖项。
  7. │      Scope.js // 在分析 AST 节点时为每一个节点生成对应的 Scope 实例,主要是记录每个 AST 节点对应的作用域。
  8. │      walk.js // walk 就是递归调用 AST 节点进行分析。
  9. ├─finalisers
  10. │      cjs.js
  11. │      index.js
  12. └─utils // 一些帮助函数
  13.         map-helpers.js
  14.         object.js
  15.         promise.js
  16.         replaceIdentifiers.js

Rollup 构建流程

我们以 index.js 入口文件,index 依赖了 foo.js,foo 依赖了 bar.js

 
 
 
 
  1. // index.js
  2. import { foo } from "./foo";
  3. foo()
  4. var city = 'hangzhou'
  5. function test() {
  6.     console.log('test')
  7. }
  8. console.log(test())
 
 
 
 
  1. // foo.js
  2. import { bar } from "./bar";
  3. export function foo() {
  4.     console.log('foo')
  5. }
 
 
 
 
  1. // bar.js
  2. export function bar() {
  3.     console.log('bar')
  4. }

debug 起来!!!

 
 
 
 
  1. // debug.js
  2. const path = require('path')
  3. const rollup = require('./lib/rollup')
  4. // 入口文件的绝对路径
  5. let entry = path.resolve(__dirname, 'src/main.js')
  6. // 和源码有所不同,这里使用的是同步,增加可读性
  7. rollup(entry, 'bundle.js')

1.new Bundle(), build()

首先生成一个 Bundle 实例,也就是打包器。然后执行 build 打包编译

 
 
 
 
  1. // rollup.js
  2. let Bundle = require('./bundle')
  3. function rollup(entry, outputFileName) {
  4.     // Bundle 代表打包对象,里面包含所有的模块信息
  5.     const bundle = new Bundle({ entry })
  6.     // 调用 build 方法开始进行编译
  7.     bundle.build(outputFileName)
  8. }
  9. module.exports = rollup

lib/bundle.js根据入口路径出发(在 bundle 中,我们会首先统一处理下入口文件的后缀),去找到他的模块定义,在 fetchModule 中,会生成一个 module 实例

我们关注红框中的代码,会发现返回了一个 module

2.new Module()

每个文件都是一个模块,每个模块都会有一个 Module 实例。在 Module 实例中,会调用 acorn 库的 parse() 方法将代码解析成 AST。

对生成的 AST 进行分析analyse我们先看一下入口文件 index.js 生成的 AST

可以看到 ast.body 是一个数组,分别对应 index.js 的五条语句 展开这个 ast 树如下:

 
 
 
 
  1. {
  2.   "type": "Program",
  3.   "start": 0,
  4.   "end": 128,
  5.   "body": [
  6.     {
  7.       "type": "ImportDeclaration", // 导入声明
  8.       "start": 0,
  9.       "end": 31,
  10.       "specifiers": [
  11.         {
  12.           "type": "ImportSpecifier",
  13.           "start": 9,
  14.           "end": 12,
  15.           "imported": {
  16.             "type": "Identifier",
  17.             "start": 9,
  18.             "end": 12,
  19.             "name": "foo"
  20.           },
  21.           "local": {
  22.             "type": "Identifier",
  23.             "start": 9,
  24.             "end": 12,
  25.             "name": "foo"
  26.           }
  27.         }
  28.       ],
  29.       "source": {
  30.         "type": "Literal",
  31.         "start": 20,
  32.         "end": 30,
  33.         "value": "./foo.js",
  34.         "raw": "\"./foo.js\""
  35.       }
  36.     },
  37.     {
  38.       "type": "ExpressionStatement",
  39.       "start": 32,
  40.       "end": 37,
  41.       "expression": {
  42.         "type": "CallExpression",
  43.         "start": 32,
  44.         "end": 37,
  45.         "callee": {
  46.           "type": "Identifier",
  47.           "start": 32,
  48.           "end": 35,
  49.           "name": "foo"
  50.         },
  51.         "arguments": [],
  52.         "optional": false
  53.       }
  54.     },
  55.     {
  56.       "type": "VariableDeclaration",
  57.       "start": 38,
  58.       "end": 59,
  59.       "declarations": [
  60.         {
  61.           "type": "VariableDeclarator",
  62.           "start": 42,
  63.           "end": 59,
  64.           "id": {
  65.             "type": "Identifier",
  66.             "start": 42,
  67.             "end": 46,
  68.             "name": "city"
  69.           },
  70.           "init": {
  71.             "type": "Literal",
  72.             "start": 49,
  73.             "end": 59,
  74.             "value": "hangzhou",
  75.             "raw": "'hangzhou'"
  76.           }
  77.         }
  78.       ],
  79.       "kind": "var"
  80.     },
  81.     {
  82.       "type": "FunctionDeclaration",
  83.       "start": 61,
  84.       "end": 104,
  85.       "id": {
  86.         "type": "Identifier",
  87.         "start": 70,
  88.         "end": 74,
  89.         "name": "test"
  90.       },
  91.       "expression": false,
  92.       "generator": false,
  93.       "async": false,
  94.       "params": [],
  95.       "body": {
  96.         "type": "BlockStatement",
  97.         "start": 77,
  98.         "end": 104,
  99.         "body": [
  100.           {
  101.             "type": "ExpressionStatement",
  102.             "start": 83,
  103.             "end": 102,
  104.             "expression": {
  105.               "type": "CallExpression",
  106.               "start": 83,
  107.               "end": 102,
  108.               "callee": {
  109.                 "type": "MemberExpression",
  110.                 "start": 83,
  111.                 "end": 94,
  112.                 "object": {
  113.                   "type": "Identifier",
  114.                   "start": 83,
  115.                   "end": 90,
  116.                   "name": "console"
  117.                 },
  118.                 "property": {
  119.                   "type": "Identifier",
  120.                   "start": 91,
  121.                   "end": 94,
  122.                   "name": "log"
  123.                 },
  124.                 "computed": false,
  125.                 "optional": false
  126.               },
  127.               "arguments": [
  128.                 {
  129.                   "type": "Literal",
  130.                   "start": 95,
  131.                   "end": 101,
  132.                   "value": "test",
  133.                   "raw": "'test'"
  134.                 }
  135.               ],
  136.               "optional": false
  137.             }
  138.           }
  139.         ]
  140.       }
  141.     },
  142.     {
  143.       "type": "ExpressionStatement",
  144.       "start": 106,
  145.       "end": 125,
  146.       "expression": {
  147.         "type": "CallExpression",
  148.         "start": 106,
  149.         "end": 125,
  150.         "callee": {
  151.           "type": "MemberExpression",
  152.           "start": 106,
  153.           "end": 117,
  154.           "object": {
  155.             "type": "Identifier",
  156.             "start": 106,
  157.             "end": 113,
  158.             "name": "console"
  159.           },
  160.           "property": {
  161.             "type": "Identifier",
  162.             "start": 114,
  163.             "end": 117,
  164.             "name": "log"
  165.           },
  166.           "computed": false,
  167.           "optional": false
  168.         },
  169.         "arguments": [
  170.           {
  171.             "type": "CallExpression",
  172.             "start": 118,
  173.             "end": 124,
  174.             "callee": {
  175.               "type": "Identifier",
  176.               "start": 118,
  177.               "end": 122,
  178.               "name": "test"
  179.             },
  180.             "arguments": [],
  181.             "optional": false
  182.           }
  183.         ],
  184.         "optional": false
  185.       }
  186.     }
  187.   ],
  188.   "sourceType": "module"
  189. }

我们通过这个 AST 树,分析 **analyse **具体做了什么???

第一步:分析当前模块导入【import】和导出【exports】模块,将引入的模块和导出的模块存储起来this.imports = {};//存放着当前模块所有的导入

this.exports = {};//存放着当前模块所有的导出

 
 
 
 
  1. this.imports = {};//存放着当前模块所有的导入
  2.   this.exports = {};//存放着当前模块所有的导出
  3.   this.ast.body.forEach(node => {
  4.     if (node.type === 'ImportDeclaration') {// 说明这是一个 import 语句
  5.       let source = node.source.value; // 从哪个模块导入的
  6.       let specifiers = node.specifiers; // 导入标识符
  7.       specifiers.forEach(specifier => {
  8.         const name = specifier.imported.name; //name
  9.         const localName = specifier.local.name; //name
  10.         //本地的哪个变量,是从哪个模块的的哪个变量导出的
  11.         this.imports[localName] = { name, localName, source }
  12.       });
  13.       //}else if(/^Export/.test(node.type)){ // 导出方法有很多
  14.     } else if (node.type === 'ExportNamedDeclaration') { // 说明这是一个 exports 语句
  15.       let declaration = node.declaration;//VariableDeclaration
  16.       if (declaration.type === 'VariableDeclaration') {
  17.         let name = declaration.declarations[0].id.name;
  18.         this.exports[name] = {
  19.           node, localName: name, expression: declaration
  20.         }
  21.       }
  22.     }
  23.   });
  24.   analyse(this.ast, this.code, this);//找到了_defines 和 _dependsOn

打断点可以看到,foo 已经被存入 imports =》** import { foo } from "./foo"; ** exports:{} 表示没有导出语句

第二步:analyse(this.ast, this.code, this); //找到_defines 和 _dependsOn

找出当前模块使用到了哪些变量 标记哪些变量时当前模块声明的,哪些变量是导入别的模块的变量 我们定义以下字段用来存放:_defines: { value: {} },//存放当前模块定义的所有的全局变量

_dependsOn: { value: {} },//当前模块没有定义但是使用到的变量,也就是依赖的外部变量_included: { value: false, writable: true },//此语句是否已经被包含到打包结果中,防止重复打包_source: { value: magicString.snip(statement.start, statement.end) } //magicString.snip 返回的还是 magicString 实例 clone

分析每个 AST 节点之间的作用域,构建 scope tree,

 
 
 
 
  1. function analyse(ast, magicString, module) {
  2.     let scope = new Scope();//先创建一个模块内的全局作用域
  3.     //遍历当前的所有的语法树的所有的顶级节点
  4.     ast.body.forEach(statement => {
  5.     
  6.         //给作用域添加变量 var function const let 变量声明
  7.         function addToScope(declaration) {
  8.             var name = declaration.id.name;//获得这个声明的变量
  9.             scope.add(name);
  10.             if (!scope.parent) {//如果当前是全局作用域的话
  11.                 statement._defines[name] = true;
  12.             }
  13.         }
  14.         Object.defineProperties(statement, {
  15.             _defines: { value: {} },//存放当前模块定义的所有的全局变量
  16.             _dependsOn: { value: {} },//当前模块没有定义但是使用到的变量,也就是依赖的外部变量
  17.             _included: { value: false, writable: true },//此语句是否已经 被包含到打包结果中了
  18.             //start 指的是此节点在源代码中的起始索引,end 就是结束索引
  19.             //magicString.snip 返回的还是 magicString 实例 clone
  20.             _source: { value: magicString.snip(statement.start, statement.end) }
  21.         });
  22.         
  23.         //这一步在构建我们的作用域链
  24.         walk(statement, {
  25.             enter(node) {
  26.                 let newScope;
  27.   &nb

    分享题目:Rollup-构建原理及简易实现
    URL网址:http://www.hantingmc.com/qtweb/news27/441427.html

    网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

    广告

    声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联