<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          rollup - 構(gòu)建原理及簡(jiǎn)易實(shí)現(xiàn)

          共 44047字,需瀏覽 89分鐘

           ·

          2021-07-05 03:08

          黃丹丹,微醫(yī)前端技術(shù)部醫(yī)療支撐組。一個(gè)資深貓奴,愛靜亦愛動(dòng)無(wú)痕切換的精分程序媛。

          一、Rollup 概述

          官網(wǎng)地址:https://rollupjs.org/guide/en/

          Rollup 是什么

          我們先看看 Rollup 的作者 Rich Harris 是怎么講的?Rollup 是一個(gè)模塊化的打包工具。本質(zhì)上,它會(huì)合并 JavaScript 文件。而且你不需要去手動(dòng)指定它們的順序,或者去擔(dān)心文件之間的變量名沖突。它的內(nèi)部實(shí)現(xiàn)會(huì)比說(shuō)的復(fù)雜一點(diǎn),但是它就是這么做的 —— 合并。

          對(duì)比 Webpack

          webpack 對(duì)前端來(lái)說(shuō)是再熟悉不過(guò)的工具了,它提供了強(qiáng)大的功能來(lái)構(gòu)建前端的資源,包括 html/js/ts/css/less/scss ... 等語(yǔ)言腳本,也包括 images/fonts ... 等二進(jìn)制文件。正是因?yàn)?webpack 擁有如此強(qiáng)大的功能,所以 webpack 在進(jìn)行資源打包的時(shí)候,就會(huì)產(chǎn)生很多冗余的代碼(如果你有查看過(guò) webpack 的 bundle 文件,便會(huì)發(fā)現(xiàn))。

          而對(duì)于一些項(xiàng)目(特別是類庫(kù))只有 js,而沒有其他的靜態(tài)資源文件,使用 webpack 就有點(diǎn)大才小用了,因?yàn)?webpack bundle 文件的體積略大,運(yùn)行略慢,可讀性略低。這個(gè)時(shí)候就可以選擇 Rollup

          Rollup是一個(gè)模塊打包器,支持 ES6 模塊,支持 Tree-shaking,但不支持 webpack 的 code-splitting、模塊熱更新等,這意味著它更適合用來(lái)做類庫(kù)項(xiàng)目的打包器而不是應(yīng)用程序項(xiàng)目的打包器。

          簡(jiǎn)單總結(jié)

          對(duì)于應(yīng)用適用于 webpack,對(duì)于類庫(kù)更適用于 Rollup,react/vue/anngular 都在用Rollup作為打包工具

          二、Rollup 前置知識(shí)

          在闡述 Rollup 的構(gòu)建原理之前,我們需要了解一些前置知識(shí)

          magic-string

          magic-string 是 Rollup 作者寫的一個(gè)關(guān)于字符串操作的庫(kù),這個(gè)庫(kù)主要是對(duì)字符串一些常用方法進(jìn)行了封裝

          var MagicString = require('magic-string')
          var magicString = new MagicString('export var name = "zhangsan"')
          // 以下所有操作都是基于原生字符串
          // 類似于截取字符串
          console.log(magicString.snip(0, 6).toString()) // export
          // 從開始到結(jié)束刪除
          console.log(magicString.remove(0, 7).toString()) //  var name = "zhangsan"

          // 多個(gè)模塊,把他們打包在一個(gè)文件里,需要把很多文件的源代碼合并在一起
          let bundleString = new MagicString.Bundle();
          bundleString.addSource({
              content: 'console.log(hello)',
              separator: '\n'
          })
          bundleString.addSource({
              content: 'console.log(world)',
              separator: '\n'
          })
          // // 原理類似
          // let str = ''
          // str += 'console.log(hello);\n'
          // str += 'console.log(world);\n'
          console.log(bundleString.toString()) 
          // hello
          // world

          AST

          通過(guò) javascript parse 可以把代碼轉(zhuǎn)化為一顆抽象語(yǔ)法樹 AST,這顆樹定義了代碼的結(jié)構(gòu),通過(guò)操縱這個(gè)樹,我們可以精確的定位到聲明語(yǔ)句、賦值語(yǔ)句、運(yùn)算符語(yǔ)句等等,實(shí)現(xiàn)對(duì)代碼的分析、優(yōu)化、變更等操作 源代碼:main.js

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

          轉(zhuǎn)化為 AST 是長(zhǎng)這樣子的,如下:

          {
            "type""Program", // 這個(gè) AST 類型為 Program,表明是一個(gè)程序
            "start": 0,
            "end": 40,
            "body": [ // body 是一個(gè)數(shù)組,每一條語(yǔ)句都對(duì)應(yīng) body 下的一個(gè)語(yǔ)句
              {
                "type""ImportDeclaration", // 導(dǎo)入聲明類型
                "start": 0,
                "end": 23,
                "specifiers": [
                  {
                    "type""ImportSpecifier",
                    "start": 9,
                    "end": 10,
                    "imported": {
                      "type""Identifier",
                      "start": 9,
                      "end": 10,
                      "name""a" // 導(dǎo)入模塊命名 name 'a'
                    },
                    "local": {
                      "type""Identifier",
                      "start": 9,
                      "end": 10,
                      "name""a" // 本地模塊命名,同 imported.name
                    }
                  }
                ],
                "source": {
                  "type""Literal",
                  "start": 18,
                  "end": 23,
                  "value""./a", // 導(dǎo)入路徑 './a'
                  "raw""'./a'"
                }
              },
              {
                "type""ExpressionStatement", // 表達(dá)式類型
                "start": 24,
                "end": 38,
                "expression": {
                  "type""CallExpression", // 調(diào)用表達(dá)式類型
                  "start": 24,
                  "end": 38,
                  "callee": {
                    "type""MemberExpression",
                    "start": 24,
                    "end": 35,
                    "object": {
                      "type""Identifier",
                      "start": 24,
                      "end": 31,
                      "name""console"
                    },
                    "property": {
                      "type""Identifier",
                      "start": 32,
                      "end": 35,
                      "name""log"
                    },
                    "computed"false,
                    "optional"false
                  },
                  "arguments": [
                    {
                      "type""Identifier",
                      "start": 36,
                      "end": 37,
                      "name""a"
                    }
                  ],
                  "optional"false
                }
              }
            ],
            "sourceType""module"
          }


          AST 工作流

          Parse(解析)將代碼轉(zhuǎn)化成抽象語(yǔ)法樹,樹上有很多的 estree 節(jié)點(diǎn) Transform(轉(zhuǎn)換) 對(duì)抽象語(yǔ)法樹進(jìn)行轉(zhuǎn)換 Generate(代碼生成) 將上一步經(jīng)過(guò)轉(zhuǎn)換過(guò)的抽象語(yǔ)法樹生成新的代碼

          acorn

          acorn 是一個(gè) JavaScript 語(yǔ)法解析器,它將 JavaScript 字符串解析成語(yǔ)法抽象樹 AST 如果想了解 AST 語(yǔ)法樹可以點(diǎn)下這個(gè)網(wǎng)址https://astexplorer.net/

          作用域/作用域鏈

          在 js 中,作用域是用來(lái)規(guī)定變量訪問(wèn)范圍的規(guī)則, 作用域鏈?zhǔn)怯僧?dāng)前執(zhí)行環(huán)境和上層執(zhí)行環(huán)境的一系列變量對(duì)象組成的,它保證了當(dāng)前執(zhí)行環(huán)境對(duì)符合訪問(wèn)權(quán)限的變量和函數(shù)的有序訪問(wèn)

          三、Rollup

          Rollup 是怎樣工作的呢?

          你給它一個(gè)入口文件 —— 通常是 index.js。Rollup 將使用 Acorn 讀取解析文件 —— 將返回給我們一種叫抽象語(yǔ)法樹(AST)的東西。一旦有了 AST ,你就可以發(fā)現(xiàn)許多關(guān)于代碼的東西,比如它包含哪些 import 聲明。

          假設(shè) index.js 文件頭部有這樣一行:

          import foo from './foo.js';

          這就意味著 Rollup 需要去加載,解析,分析在 index.js 中引入的 ./foo.js。重復(fù)解析直到?jīng)]有更多的模塊被加載進(jìn)來(lái)。更重要的是,所有的這些操作都是可插拔的,所以您可以從 node_modules 中導(dǎo)入或者使用 sourcemap-aware 的方式將 ES2015 編譯成 ES5 代碼。

          在 Rollup 中,一個(gè)文件就是一個(gè)模塊,每個(gè)模塊都會(huì)根據(jù)文件的代碼生成一個(gè) AST 抽象語(yǔ)法樹。

          分析 AST 節(jié)點(diǎn),就是看這個(gè)節(jié)點(diǎn)有沒有調(diào)用函數(shù)方法,有沒有讀到變量,有,就查看是否在當(dāng)前作用域,如果不在就往上找,直到找到模塊頂級(jí)作用域?yàn)橹埂H绻灸K都沒找到,說(shuō)明這個(gè)函數(shù)、方法依賴于其他模塊,需要從其他模塊引入。如果發(fā)現(xiàn)其他模塊中有方法依賴其他模塊,就會(huì)遞歸讀取其他模塊,如此循環(huán)直到?jīng)]有依賴的模塊為止 找到這些變量或著方法是在哪里定義的,把定義語(yǔ)句包含進(jìn)來(lái)即可 其他無(wú)關(guān)代碼一律不要

          看如下代碼,我們先實(shí)際操作一下:

          // index.js
          import { foo } from "./foo";
          foo()
          var city = 'hangzhou'

          function test() {
              console.log('test')
          }

          console.log(test())
          // foo.js
          import { bar } from "./bar";
          export function foo() {
              console.log('foo')
          }
          // bar.js
          export function bar() {
              console.log('bar')
          }
          // rollup.config.js
          export default {
              input: './src/index.js',
              output: {
                  file: './dist/bundle.js', // 打包后的存放文件
                  format: 'cjs', //輸出格式 amd es6 life umd cjs
                  name: 'bundleName', //如果輸出格式 life,umd 需要指定一個(gè)全局變量
              }
          };

          執(zhí)行 npm run build,會(huì)得到如下結(jié)果:

          'use strict';

          function foo() {
              console.log('foo');
          }

          foo();

          function test() {
              console.log('test');
          }

          console.log(test());

          以上,我們可以看到Rollup 只是會(huì)合并你的代碼 —— 沒有任何浪費(fèi)。所產(chǎn)生的包也可以更好的縮小。有人稱之為 “作用域提升(scope hoisting)”。其次,它把你導(dǎo)入的模塊中的未使用代碼移除。這被稱為“(搖樹優(yōu)化)treeshaking”。總之,Rollup 就是一個(gè)模塊化的打包工具。

          接下來(lái)我們進(jìn)入源碼,具體分析下 Rollup 的構(gòu)建流程

          Rollup 構(gòu)建流程分析

          Rollup 源碼結(jié)構(gòu)


          │  bundle.js // Bundle 打包器,在打包過(guò)程中會(huì)生成一個(gè) bundle 實(shí)例,用于收集其他模塊的代碼,最后再將收集的代碼打包到一起。
          │  external-module.js // ExternalModule 外部模塊,例如引入了 'path' 模塊,就會(huì)生成一個(gè) ExternalModule 實(shí)例。
          │  module.js // Module 模塊,module 實(shí)例。
          │  rollup.js // rollup 函數(shù),一切的開始,調(diào)用它進(jìn)行打包。

          ├─ast // ast 目錄,包含了和 AST 相關(guān)的類和函數(shù)
          │      analyse.js // 主要用于分析 AST 節(jié)點(diǎn)的作用域和依賴項(xiàng)。
          │      Scope.js // 在分析 AST 節(jié)點(diǎn)時(shí)為每一個(gè)節(jié)點(diǎn)生成對(duì)應(yīng)的 Scope 實(shí)例,主要是記錄每個(gè) AST 節(jié)點(diǎn)對(duì)應(yīng)的作用域。
          │      walk.js // walk 就是遞歸調(diào)用 AST 節(jié)點(diǎn)進(jìn)行分析。

          ├─finalisers
          │      cjs.js
          │      index.js

          └─utils // 一些幫助函數(shù)
                  map-helpers.js
                  object.js
                  promise.js
                  replaceIdentifiers.js

          Rollup 構(gòu)建流程

          我們以 index.js 入口文件,index 依賴了 foo.js,foo 依賴了 bar.js

          // index.js
          import { foo } from "./foo";
          foo()
          var city = 'hangzhou'

          function test() {
              console.log('test')
          }

          console.log(test())
          // foo.js
          import { bar } from "./bar";
          export function foo() {
              console.log('foo')
          }
          // bar.js
          export function bar() {
              console.log('bar')
          }

          debug 起來(lái)!!!

          // debug.js
          const path = require('path')
          const rollup = require('./lib/rollup')
          // 入口文件的絕對(duì)路徑
          let entry = path.resolve(__dirname, 'src/main.js')
          // 和源碼有所不同,這里使用的是同步,增加可讀性
          rollup(entry, 'bundle.js')

          1.new Bundle(), build()

          首先生成一個(gè) Bundle 實(shí)例,也就是打包器。然后執(zhí)行 build 打包編譯

          // rollup.js
          let Bundle = require('./bundle')
          function rollup(entry, outputFileName) {
              // Bundle 代表打包對(duì)象,里面包含所有的模塊信息
              const bundle = new Bundle({ entry })
              // 調(diào)用 build 方法開始進(jìn)行編譯
              bundle.build(outputFileName)
          }
          module.exports = rollup

          lib/bundle.js根據(jù)入口路徑出發(fā)(在 bundle 中,我們會(huì)首先統(tǒng)一處理下入口文件的后綴),去找到他的模塊定義,在 fetchModule 中,會(huì)生成一個(gè) module 實(shí)例我們關(guān)注紅框中的代碼,會(huì)發(fā)現(xiàn)返回了一個(gè) module

          2.new Module()

          每個(gè)文件都是一個(gè)模塊,每個(gè)模塊都會(huì)有一個(gè) Module 實(shí)例。在 Module 實(shí)例中,會(huì)調(diào)用 acorn 庫(kù)的 parse() 方法將代碼解析成 AST。對(duì)生成的 AST 進(jìn)行分析analyse我們先看一下入口文件 index.js 生成的 AST可以看到 ast.body 是一個(gè)數(shù)組,分別對(duì)應(yīng) index.js 的五條語(yǔ)句 展開這個(gè) ast 樹如下:

          {
            "type""Program",
            "start": 0,
            "end": 128,
            "body": [
              {
                "type""ImportDeclaration", // 導(dǎo)入聲明
                "start": 0,
                "end": 31,
                "specifiers": [
                  {
                    "type""ImportSpecifier",
                    "start": 9,
                    "end": 12,
                    "imported": {
                      "type""Identifier",
                      "start": 9,
                      "end": 12,
                      "name""foo"
                    },
                    "local": {
                      "type""Identifier",
                      "start": 9,
                      "end": 12,
                      "name""foo"
                    }
                  }
                ],
                "source": {
                  "type""Literal",
                  "start": 20,
                  "end": 30,
                  "value""./foo.js",
                  "raw""\"./foo.js\""
                }
              },
              {
                "type""ExpressionStatement",
                "start": 32,
                "end": 37,
                "expression": {
                  "type""CallExpression",
                  "start": 32,
                  "end": 37,
                  "callee": {
                    "type""Identifier",
                    "start": 32,
                    "end": 35,
                    "name""foo"
                  },
                  "arguments": [],
                  "optional"false
                }
              },
              {
                "type""VariableDeclaration",
                "start": 38,
                "end": 59,
                "declarations": [
                  {
                    "type""VariableDeclarator",
                    "start": 42,
                    "end": 59,
                    "id": {
                      "type""Identifier",
                      "start": 42,
                      "end": 46,
                      "name""city"
                    },
                    "init": {
                      "type""Literal",
                      "start": 49,
                      "end": 59,
                      "value""hangzhou",
                      "raw""'hangzhou'"
                    }
                  }
                ],
                "kind""var"
              },
              {
                "type""FunctionDeclaration",
                "start": 61,
                "end": 104,
                "id": {
                  "type""Identifier",
                  "start": 70,
                  "end": 74,
                  "name""test"
                },
                "expression"false,
                "generator"false,
                "async"false,
                "params": [],
                "body": {
                  "type""BlockStatement",
                  "start": 77,
                  "end": 104,
                  "body": [
                    {
                      "type""ExpressionStatement",
                      "start": 83,
                      "end": 102,
                      "expression": {
                        "type""CallExpression",
                        "start": 83,
                        "end": 102,
                        "callee": {
                          "type""MemberExpression",
                          "start": 83,
                          "end": 94,
                          "object": {
                            "type""Identifier",
                            "start": 83,
                            "end": 90,
                            "name""console"
                          },
                          "property": {
                            "type""Identifier",
                            "start": 91,
                            "end": 94,
                            "name""log"
                          },
                          "computed"false,
                          "optional"false
                        },
                        "arguments": [
                          {
                            "type""Literal",
                            "start": 95,
                            "end": 101,
                            "value""test",
                            "raw""'test'"
                          }
                        ],
                        "optional"false
                      }
                    }
                  ]
                }
              },
              {
                "type""ExpressionStatement",
                "start": 106,
                "end": 125,
                "expression": {
                  "type""CallExpression",
                  "start": 106,
                  "end": 125,
                  "callee": {
                    "type""MemberExpression",
                    "start": 106,
                    "end": 117,
                    "object": {
                      "type""Identifier",
                      "start": 106,
                      "end": 113,
                      "name""console"
                    },
                    "property": {
                      "type""Identifier",
                      "start": 114,
                      "end": 117,
                      "name""log"
                    },
                    "computed"false,
                    "optional"false
                  },
                  "arguments": [
                    {
                      "type""CallExpression",
                      "start": 118,
                      "end": 124,
                      "callee": {
                        "type""Identifier",
                        "start": 118,
                        "end": 122,
                        "name""test"
                      },
                      "arguments": [],
                      "optional"false
                    }
                  ],
                  "optional"false
                }
              }
            ],
            "sourceType""module"
          }

          我們通過(guò)這個(gè) AST 樹,分析 **analyse **具體做了什么???

          第一步:分析當(dāng)前模塊導(dǎo)入【import】和導(dǎo)出【exports】模塊,將引入的模塊和導(dǎo)出的模塊存儲(chǔ)起來(lái)this.imports = {};//存放著當(dāng)前模塊所有的導(dǎo)入
          this.exports = {};//存放著當(dāng)前模塊所有的導(dǎo)出

              this.imports = {};//存放著當(dāng)前模塊所有的導(dǎo)入
              this.exports = {};//存放著當(dāng)前模塊所有的導(dǎo)出
              this.ast.body.forEach(node => {
                if (node.type === 'ImportDeclaration') {// 說(shuō)明這是一個(gè) import 語(yǔ)句
                  let source = node.source.value; // 從哪個(gè)模塊導(dǎo)入的
                  let specifiers = node.specifiers; // 導(dǎo)入標(biāo)識(shí)符
                  specifiers.forEach(specifier => {
                    const name = specifier.imported.name; //name
                    const localName = specifier.local.name; //name
                    //本地的哪個(gè)變量,是從哪個(gè)模塊的的哪個(gè)變量導(dǎo)出的
                    this.imports[localName] = { name, localName, source }
                  });
                  //}else if(/^Export/.test(node.type)){ // 導(dǎo)出方法有很多
                } else if (node.type === 'ExportNamedDeclaration') { // 說(shuō)明這是一個(gè) exports 語(yǔ)句
                  let declaration = node.declaration;//VariableDeclaration
                  if (declaration.type === 'VariableDeclaration') {
                    let name = declaration.declarations[0].id.name;
                    this.exports[name] = {
                      node, localName: name, expression: declaration
                    }
                  }
                }
              });
              analyse(this.ast, this.code, this);//找到了_defines 和 _dependsOn

          打斷點(diǎn)可以看到,foo 已經(jīng)被存入 imports  =》** import { foo } from "./foo";  ** exports:{} 表示沒有導(dǎo)出語(yǔ)句

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

          找出當(dāng)前模塊使用到了哪些變量 標(biāo)記哪些變量時(shí)當(dāng)前模塊聲明的,哪些變量是導(dǎo)入別的模塊的變量 我們定義以下字段用來(lái)存放:_defines: { value: {} },//存放當(dāng)前模塊定義的所有的全局變量
          _dependsOn: { value: {} },//當(dāng)前模塊沒有定義但是使用到的變量,也就是依賴的外部變量_included: { value: false, writable: true },//此語(yǔ)句是否已經(jīng)被包含到打包結(jié)果中,防止重復(fù)打包_source: { value: magicString.snip(statement.start, statement.end) } //magicString.snip 返回的還是 magicString 實(shí)例 clone

          分析每個(gè) AST 節(jié)點(diǎn)之間的作用域,構(gòu)建 scope tree,

          function analyse(ast, magicString, module) {
              let scope = new Scope();//先創(chuàng)建一個(gè)模塊內(nèi)的全局作用域
              //遍歷當(dāng)前的所有的語(yǔ)法樹的所有的頂級(jí)節(jié)點(diǎn)
              ast.body.forEach(statement => {
              
                  //給作用域添加變量 var function const let 變量聲明
                  function addToScope(declaration) {
                      var name = declaration.id.name;//獲得這個(gè)聲明的變量
                      scope.add(name);
                      if (!scope.parent) {//如果當(dāng)前是全局作用域的話
                          statement._defines[name] = true;
                      }
                  }

                  Object.defineProperties(statement, {
                      _defines: { value: {} },//存放當(dāng)前模塊定義的所有的全局變量
                      _dependsOn: { value: {} },//當(dāng)前模塊沒有定義但是使用到的變量,也就是依賴的外部變量
                      _included: { value: false, writable: true },//此語(yǔ)句是否已經(jīng) 被包含到打包結(jié)果中了
                      //start 指的是此節(jié)點(diǎn)在源代碼中的起始索引,end 就是結(jié)束索引
                      //magicString.snip 返回的還是 magicString 實(shí)例 clone
                      _source: { value: magicString.snip(statement.start, statement.end) }
                  });
                  
                  //這一步在構(gòu)建我們的作用域鏈
                  walk(statement, {
                      enter(node) {
                          let newScope;
                          if (!node) return
                          switch (node.type) {
                              case 'FunctionDeclaration':
                                  const params = node.params.map(x => x.name);
                                  if (node.type === 'FunctionDeclaration') {
                                      addToScope(node);
                                  }
                                  //如果遍歷到的是一個(gè)函數(shù)聲明,我會(huì)創(chuàng)建一個(gè)新的作用域?qū)ο?br>                        newScope = new Scope({
                                      parent: scope,//父作用域就是當(dāng)前的作用域
                                      params
                                  });
                                  break;
                              case 'VariableDeclaration': //并不會(huì)生成一個(gè)新的作用域
                                  node.declarations.forEach(addToScope);
                                  break;
                          }
                          if (newScope) {//當(dāng)前節(jié)點(diǎn)聲明一個(gè)新的作用域
                              //如果此節(jié)點(diǎn)生成一個(gè)新的作用域,那么會(huì)在這個(gè)節(jié)點(diǎn)放一個(gè)_scope,指向新的作用域
                              Object.defineProperty(node, '_scope', { value: newScope });
                              scope = newScope;
                          }
                      },
                      leave(node) {
                          if (node._scope) {//如果此節(jié)點(diǎn)產(chǎn)出了一個(gè)新的作用域,那等離開這個(gè)節(jié)點(diǎn),scope 回到父作用法域
                              scope = scope.parent;
                          }
                      }
                  });
              });
              ast._scope = scope;
              //找出外部依賴_dependsOn
              ast.body.forEach(statement => {
                  walk(statement, {
                      enter(node) {
                          if (node._scope) {
                              scope = node._scope;
                          } //如果這個(gè)節(jié)點(diǎn)放有一個(gè) scope 屬性,說(shuō)明這個(gè)節(jié)點(diǎn)產(chǎn)生了一個(gè)新的作用域
                          if (node.type === 'Identifier') {
                              //從當(dāng)前的作用域向上遞歸,找這個(gè)變量在哪個(gè)作用域中定義
                              const definingScope = scope.findDefiningScope(node.name);
                              if (!definingScope) {
                                  statement._dependsOn[node.name] = true;//表示這是一個(gè)外部依賴的變量
                              }
                          }

                      },
                      leave(node) {
                          if (node._scope) {
                              scope = scope.parent;
                          }

                      }
                  });
              });
          }

          斷點(diǎn)可以看到**_defines 和 _dependsOn **分別存入了當(dāng)前變量和引入的變量第三步:this.definitions = {};把全局變量的定義語(yǔ)句存放到 definitions 里

          // module.js
          this.definitions = {};//存放著所有的全局變量的定義語(yǔ)句
              this.ast.body.forEach(statement => {
                Object.keys(statement._defines).forEach(name => {
                  //key 是全局變量名,值是定義這個(gè)全局變量的語(yǔ)句
                  this.definitions[name] = statement;
                });
              });

          第四步:展開語(yǔ)句,展開當(dāng)前模塊的所有語(yǔ)句,把這些語(yǔ)句中定義的變量的語(yǔ)句都放到結(jié)果里

          if (statement.type === 'ImportDeclaration') {return} 如果是導(dǎo)入聲明語(yǔ)句,既 import { foo } from "./foo";這條語(yǔ)句我們是不需要的,return 掉

          //展開這個(gè)模塊里的語(yǔ)句,把些語(yǔ)句中定義的變量的語(yǔ)句都放到結(jié)果里
            expandAllStatements() {
              let allStatements = [];
              this.ast.body.forEach(statement => {
                if (statement.type === 'ImportDeclaration') {return}
                let statements = this.expandStatement(statement);
                allStatements.push(...statements);
              });
              return allStatements;
            }

          **expandStatement:**找到當(dāng)前節(jié)點(diǎn)依賴的變量,找到這些變量的聲明語(yǔ)句。這些語(yǔ)句可能是在當(dāng)前模塊聲明的,也也可能是在導(dǎo)入的模塊的聲明的 然后放入結(jié)果里


            expandStatement(statement) {
              let result = [];
              const dependencies = Object.keys(statement._dependsOn);//外部依賴 [name]
              dependencies.forEach(name => {
                //找到定義這個(gè)變量的聲明節(jié)點(diǎn),這個(gè)節(jié)點(diǎn)可以有在當(dāng)前模塊內(nèi),也可能在依賴的模塊里
                let definition = this.define(name);
                result.push(...definition);
              });
              if (!statement._included) {
                statement._included = true;//表示這個(gè)節(jié)點(diǎn)已經(jīng)確定被納入結(jié)果里了,以后就不需要重復(fù)添加了
                result.push(statement);
              }
              return result;
            }
            

          define: 找到定義這個(gè)變量的聲明節(jié)點(diǎn),這個(gè)節(jié)點(diǎn)可以有在當(dāng)前模塊內(nèi),也可能在依賴的模塊里const module = this.bundle.fetchModule(importData.source, this.path);獲取導(dǎo)入變量的模塊

             define(name) {
              //查找一下導(dǎo)入變量里有沒有 name
              if (hasOwnProperty(this.imports, name)) {
                const importData = this.imports[name];
                // 獲取導(dǎo)入變量的模塊
                const module = this.bundle.fetchModule(importData.source, this.path);
                // 這個(gè) module 模塊也有導(dǎo)入導(dǎo)出
                const exportData = module.exports[importData.name];
                // 返回這個(gè)導(dǎo)入模塊變量的聲明語(yǔ)句
                return module.define(exportData.localName);
              } else {
                //definitions 是對(duì)象,key 當(dāng)前模塊的變量名,值是定義這個(gè)變量的語(yǔ)句
                let statement = this.definitions[name];
                if (statement && !statement._included) {
                  return this.expandStatement(statement);
                } else {
                  return [];
                }
              }
            }

          this.statements 里就是所有我們分析標(biāo)記之后返回的數(shù)組

          以上分析了很多,但總結(jié)來(lái)就是做了以下事情:

          • 收集導(dǎo)入和導(dǎo)出變量
          • 建立映射關(guān)系,方便后續(xù)使用
          • 收集所有語(yǔ)句定義的變量
          • 建立變量和聲明語(yǔ)句之間的對(duì)應(yīng)關(guān)系,方便后續(xù)使用
          • 過(guò)濾 import 語(yǔ)句
          • 刪除關(guān)鍵詞
          • 輸出語(yǔ)句時(shí),判斷變量是否為 import
          • 如是需要遞歸再次收集依賴文件的變量
          • 否則直接輸出
          • 構(gòu)建依賴關(guān)系,創(chuàng)建作用域鏈,交由./src/ast/analyse.js 文件處理
          • 在抽象語(yǔ)法樹的每一條語(yǔ)句上掛載_source(源代碼)、_defines(當(dāng)前模塊定義的變量)、_dependsOn(外部依賴的變量)、_included(是否已經(jīng)包含在輸出語(yǔ)句中)
          • 收集每個(gè)語(yǔ)句上定義的變量,創(chuàng)建作用域鏈

          3.generate()

          第一步:移除額外代碼 例如從 foo.js 中引入的 foo() 函數(shù)代碼是這樣的:export function foo() {}。rollup 會(huì)移除掉 export,變    成 function foo() {}。因?yàn)樗鼈兙鸵虬谝黄鹆耍跃筒恍枰?export 了。

          第二步:把 AST 節(jié)點(diǎn)的源碼 addSource 到 magicString,這個(gè)操作本質(zhì)上相當(dāng)于拼字符串,第三步:**return magicString.toString() **。返回合并后源代碼

          generate() {
              let magicString = new MagicString.Bundle();
              this.statements.forEach(statement => {
                const source = statement._source;
                if (statement.type === 'ExportNamedDeclaration') {
                  source.remove(statement.start, statement.declaration.start);
                } 
                magicString.addSource({
                  content: source,
                  separator: '\n'
                });
              });
              return { code: magicString.toString() };
            }

          最后輸出到'dist/bundle.js'中

          小結(jié)

          簡(jiǎn)單來(lái)說(shuō),Rollup 構(gòu)建其實(shí)就是做了以下幾件事:

          • 獲取入口文件的內(nèi)容,包裝成 module,生成抽象語(yǔ)法樹
          • 對(duì)入口文件抽象語(yǔ)法樹進(jìn)行依賴解析
          • 生成最終代碼
          • 寫入目標(biāo)文件

          四、總結(jié)

          以上代碼實(shí)現(xiàn)過(guò)程可以幫你簡(jiǎn)單的實(shí)現(xiàn)一個(gè) rollup 打包流程,但也僅僅是對(duì) rollup 源碼進(jìn)行了抽象,方便大家理解 rollup 打包的原理,其中很多細(xì)節(jié)沒有寫出來(lái),如果感興趣的話,可以深入閱讀一下源碼。

          愛心三連擊

          1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的在看是我創(chuàng)作的動(dòng)力。

          2.關(guān)注公眾號(hào)腦洞前端,獲取更多前端硬核文章!加個(gè)星標(biāo),不錯(cuò)過(guò)每一條成長(zhǎng)的機(jī)會(huì)。

          3.如果你覺得本文的內(nèi)容對(duì)你有幫助,就幫我轉(zhuǎn)發(fā)一下吧。

          瀏覽 97
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲中文字幕网 | 黄色一级视频免费网站 | 美国十次欧美日韩在线 | 操逼www | 久久9999 |