rollup - 構(gòu)建原理及簡(jiǎn)易實(shí)現(xiàn)
黃丹丹,微醫(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ā)一下吧。
