1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 44119字,需瀏覽 89分鐘

         ·

        2021-06-24 13:53


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

        一、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ì)比說的復(fù)雜一點(diǎn),但是它就是這么做的 —— 合并。

        對(duì)比 Webpack

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

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

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

        簡單總結(jié)

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

        二、Rollup 前置知識(shí)

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

        magic-string

        magic-string 是 Rollup 作者寫的一個(gè)關(guān)于字符串操作的庫,這個(gè)庫主要是對(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

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

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

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

        {
          "type""Program", // 這個(gè) AST 類型為 Program,表明是一個(gè)程序
          "start": 0,
          "end": 40,
          "body": [ // body 是一個(gè)數(shù)組,每一條語句都對(duì)應(yīng) body 下的一個(gè)語句
            {
              "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)化成抽象語法樹,樹上有很多的 estree 節(jié)點(diǎn) Transform(轉(zhuǎn)換) 對(duì)抽象語法樹進(jìn)行轉(zhuǎn)換 Generate(代碼生成) 將上一步經(jīng)過轉(zhuǎn)換過的抽象語法樹生成新的代碼

        acorn

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

        作用域/作用域鏈

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

        三、Rollup

        Rollup 是怎樣工作的呢?

        你給它一個(gè)入口文件 —— 通常是 index.js。Rollup 將使用 Acorn 讀取解析文件 —— 將返回給我們一種叫抽象語法樹(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)來。更重要的是,所有的這些操作都是可插拔的,所以您可以從 node_modules 中導(dǎo)入或者使用 sourcemap-aware 的方式將 ES2015 編譯成 ES5 代碼。

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

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

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

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

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


        │  bundle.js // Bundle 打包器,在打包過程中會(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 起來!!!

        // 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 庫的 parse() 方法將代碼解析成 AST。對(duì)生成的 AST 進(jìn)行分析analyse我們先看一下入口文件 index.js 生成的 AST可以看到 ast.body 是一個(gè)數(shù)組,分別對(duì)應(yīng) index.js 的五條語句 展開這個(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"
        }

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

        第一步:分析當(dāng)前模塊導(dǎo)入【import】和導(dǎo)出【exports】模塊,將引入的模塊和導(dǎo)出的模塊存儲(chǔ)起來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') {// 說明這是一個(gè) import 語句
                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') { // 說明這是一個(gè) exports 語句
                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)出語句

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

        找出當(dāng)前模塊使用到了哪些變量 標(biāo)記哪些變量時(shí)當(dāng)前模塊聲明的,哪些變量是導(dǎo)入別的模塊的變量 我們定義以下字段用來存放:_defines: { value: {} },//存放當(dāng)前模塊定義的所有的全局變量
        _dependsOn: { value: {} },//當(dāng)前模塊沒有定義但是使用到的變量,也就是依賴的外部變量_included: { value: false, writable: true },//此語句是否已經(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)前的所有的語法樹的所有的頂級(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 },//此語句是否已經(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 屬性,說明這個(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 = {};把全局變量的定義語句存放到 definitions 里

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

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

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

        //展開這個(gè)模塊里的語句,把些語句中定義的變量的語句都放到結(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)依賴的變量,找到這些變量的聲明語句。這些語句可能是在當(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)入模塊變量的聲明語句
              return module.define(exportData.localName);
            } else {
              //definitions 是對(duì)象,key 當(dāng)前模塊的變量名,值是定義這個(gè)變量的語句
              let statement = this.definitions[name];
              if (statement && !statement._included) {
                return this.expandStatement(statement);
              } else {
                return [];
              }
            }
          }

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

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

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

        簡單來說,Rollup 構(gòu)建其實(shí)就是做了以下幾件事:

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

        四、總結(jié)

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

        五、參考資料

        https://zhuanlan.zhihu.com/p/372808332

        https://github.com/xitu/gold-miner/blob/master/TODO/rollup-interview.md

        https://blog.csdn.net/q411020382/article/details/108302906

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

        手機(jī)掃一掃分享

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

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            麻豆国产网站入口 | 学生妹一级大片 | 男女男网站 | 在线毛片基地 | 久久性生活视频 | 男男啪啪动漫 | 91无码人妻精品一区二区三区男 | 97超碰人人摸 | 漫蛙manwa防走失站漫画软件测评 | 俄罗斯2一3sex性hd |