Изучите принципы упаковки из исходного кода исходного накопительного пакета.

внешний интерфейс rollup.js

предисловие

Для того, чтобы изучить принцип накопительной упаковки, я клонировал исходный код последней версии (v2.26.5). Потом я обнаружил, что упаковщик оказался не таким, как я думал: кода было слишком много, и просто смотреть на файл d.ts было головной болью. Чтобы посмотреть, сколько строк в исходном коде, я написал скрипт и обнаружил, что там 19650 строк, вылет...

Это удержит меня от изучения накопительного пакета? Невозможно, следующая лучшая вещь, я скачал первую версию накопительного исходного кода, всего около 1000 строк.

Моя цель состоит в том, чтобы узнать, как упаковывался шаблон, как встряхивание дерева. Первое издание исходного кода достигла этих двух функций (полуфабриката), поэтому для того, чтобы увидеть первое издание исходного кода достаточно.

Ну давайте начнем текст.

текст

используется накопительный пакетacornа такжеmagic-stringдве библиотеки. Чтобы лучше читать исходный код накопительного пакета, необходимо в них разобраться.

Теперь я просто объясню роль этих двух библиотек.

acorn

acorn

Например следующий код:

export default function add(a, b) { return a + b }

{
    "type": "Program",
    "start": 0,
    "end": 50,
    "body": [
        {
            "type": "ExportDefaultDeclaration",
            "start": 0,
            "end": 50,
            "declaration": {
                "type": "FunctionDeclaration",
                "start": 15,
                "end": 50,
                "id": {
                    "type": "Identifier",
                    "start": 24,
                    "end": 27,
                    "name": "add"
                },
                "expression": false,
                "generator": false,
                "params": [
                    {
                        "type": "Identifier",
                        "start": 28,
                        "end": 29,
                        "name": "a"
                    },
                    {
                        "type": "Identifier",
                        "start": 31,
                        "end": 32,
                        "name": "b"
                    }
                ],
                "body": {
                    "type": "BlockStatement",
                    "start": 34,
                    "end": 50,
                    "body": [
                        {
                            "type": "ReturnStatement",
                            "start": 36,
                            "end": 48,
                            "argument": {
                                "type": "BinaryExpression",
                                "start": 43,
                                "end": 48,
                                "left": {
                                    "type": "Identifier",
                                    "start": 43,
                                    "end": 44,
                                    "name": "a"
                                },
                                "operator": "+",
                                "right": {
                                    "type": "Identifier",
                                    "start": 47,
                                    "end": 48,
                                    "name": "b"
                                }
                            }
                        }
                    ]
                }
            }
        }
    ],
    "sourceType": "module"
}

Вы можете увидеть тип этого ASTprogramУказание того, что это программа.bodyОн содержит дочерние узлы AST, соответствующие всем операторам ниже программы.

Каждый узел имеетtypeтип, напр.Identifier, указывающий, что этот узел является идентификатором;BlockStatementОн указывает, что узел является блочным оператором;ReturnStatementявляется оператором возврата.

Если вы хотите узнать более подробную информацию об узле AST, можете посмотреть эту статью.«Разбор JavaScript с помощью Acorn».

magic-string

magic-stringЭто также библиотека для работы со строками, написанная автором rollup. Вот пример на гитхабе:

var MagicString = require( 'magic-string' );
var s = new MagicString( 'problems = 99' );

s.overwrite( 0, 8, 'answer' );
s.toString(); // 'answer = 99'

s.overwrite( 11, 13, '42' ); // character indices always refer to the original string
s.toString(); // 'answer = 42'

s.prepend( 'var ' ).append( ';' ); // most methods are chainable
s.toString(); // 'var answer = 42;'

var map = s.generateMap({
  source: 'source.js',
  file: 'converted.js.map',
  includeContent: true
}); // generates a v3 sourcemap

require( 'fs' ).writeFile( 'converted.js', s.toString() );
require( 'fs' ).writeFile( 'converted.js.map', map.toString() );

Как видно из примера, эта библиотека в основном инкапсулирует некоторые распространенные методы работы со строками. Здесь не так много введения.

структура исходного кода свертывания

│  bundle.js // Bundle 打包器,在打包过程中会生成一个 bundle 实例,用于收集其他模块的代码,最后再将收集的代码打包到一起。
│  external-module.js // ExternalModule 外部模块,例如引入了 'path' 模块,就会生成一个 ExternalModule 实例。
│  module.js // Module 模块,开发者自己写的代码文件,都是 module 实例。例如有 'foo.js' 文件,它就对应了一个 module 实例。
│  rollup.js // rollup 函数,一切的开始,调用它进行打包。
│
├─ast // ast 目录,包含了和 AST 相关的类和函数
│      analyse.js // 主要用于分析 AST 节点的作用域和依赖项。
│      Scope.js // 在分析 AST 节点时为每一个节点生成对应的 Scope 实例,主要是记录每个 AST 节点对应的作用域。
│      walk.js // walk 就是递归调用 AST 节点进行分析。
│
├─finalisers
│      cjs.js // 打包模式,目前只支持将代码打包成 common.js 格式
│      index.js
│
└─utils // 一些帮助函数
        map-helpers.js
        object.js
        promise.js
        replaceIdentifiers.js

Выше приведена структура каталогов оригинального исходного кода Прежде чем двигаться дальше, внимательно прочитайте приведенные выше комментарии, чтобы понять функцию каждого файла.

Как упакован накопительный пакет?

В сводке файл представляет собой модуль. Каждый модуль будет генерировать абстрактное дерево синтаксиса AST на основе кода файла, а свертки должны анализировать каждый узел AST.

Анализ узла AST заключается в том, чтобы увидеть, вызывает ли этот узел функцию или метод. Если это так, проверьте, находится ли вызываемая функция или метод в текущей области, если нет, ищите, пока не найдете область верхнего уровня модуля.

Если этот модуль не найден, это означает, что эта функция и метод зависят от других модулей и должны быть импортированы из других модулей.

Напримерimport foo from './foo.js'foo()от./foo.jsфайл найти.

во введенииfoo()Процедурная функция, если она найденаfoo()Если функция зависит от других модулей, она будет рекурсивно читать другие модули и так далее, пока не останется зависимых модулей.

Наконец, упакуйте весь импортированный код вместе.

Далее мы начнем с конкретного примера и пошагово разберем, как упаковывается накопительный пакет..

// main.js
import { foo1, foo2 } from './foo'

foo1()

function test() {
    const a = 1
}

console.log(test())
// foo.js
export function foo1() {}
export function foo2() {}

const rollup = require('../dist/rollup')

rollup(__dirname + '/main.js').then(res => {
    res.wirte('bundle.js')
})

main.js

rollup()BundleЭкземпляры, также известные как упаковщики. Затем прочитайте файл в соответствии с путем к файлу записи и, наконец, сгенерируйте файл в соответствии с содержимым файла.Moduleпример.

fs.readFile(path, 'utf-8', (err, code) => {
    if (err) reject(err)
    const module = new Module({
        code,
        path,
        bundle: this, // bundle 实例
    })
})

2. новая процедура Moudle()

в новомModuleнапример, он вызоветacornбиблиотекаparse()метод анализа кода в AST.

this.ast = parse(code, {
    ecmaVersion: 6, // 要解析的 JavaScript 的 ECMA 版本,这里按 ES6 解析
    sourceType: 'module', // sourceType值为 module 和 script。module 模式,可以使用 import/export 语法
})

Далее вам нужно проанализировать сгенерированный AST.

первый шаг

каждыйModuleimportsа такжеexports

Приведенный выше пример соответствуетimportsа такжеexportsдля:

// key 为要引入的具体对象,value 为对应的 AST 节点内容。
imports = {
  foo1: { source: './foo', name: 'foo1', localName: 'foo1' },
  foo2: { source: './foo', name: 'foo2', localName: 'foo2' }
}
// 由于没有导出的对象,所以为空
exports = {}

второй шаг, проанализируйте область между каждым узлом AST и найдите переменные, определенные каждым узлом AST.

Каждый раз, когда проходится узел AST, для него будет генерироваться узел AST.Scopeпример.

// 作用域
class Scope {
	constructor(options = {}) {
		this.parent = options.parent // 父作用域
		this.depth = this.parent ? this.parent.depth + 1 : 0 // 作用域层级
		this.names = options.params || [] // 作用域内的变量
		this.isBlockScope = !!options.block // 是否块作用域
	}

	add(name, isBlockDeclaration) {
		if (!isBlockDeclaration && this.isBlockScope) {
			// it's a `var` or function declaration, and this
			// is a block scope, so we need to go up
			this.parent.add(name, isBlockDeclaration)
		} else {
			this.names.push(name)
		}
	}

	contains(name) {
		return !!this.findDefiningScope(name)
	}

	findDefiningScope(name) {
		if (this.names.includes(name)) {
			return this
		}

		if (this.parent) {
			return this.parent.findDefiningScope(name)
		}

		return null
	}
}

ScopeРоль простая, в ней естьnamesМассив свойств для хранения переменных в этом узле AST. Например следующий код:

function test() {
    const a = 1
}

Из точки останова видно, что объект области, который он генерирует,namesСвойства будут содержатьa. И поскольку это функция модуля, область действия уровня 1 (область действия модуля верхнего уровня равна 0).

третий шаг, анализировать идентификаторы и находить их зависимости.

Moduleиз_dependsOnОбъект.

Напримерtest()переменная в функцииaМожно найти в текущем объеме, это не зависимость.foo1()Он не находится в текущей области модуля, это зависимость.

Точки останова также могут быть найденыModuleиз_dependsOnсобственность имеетfoo1.

Это принцип свертывания, встряхивающий дерево.

rollup смотрит не на то, какие функции вы вводите, а на то, какие функции вы вызываете. Если вызываемой функции нет в этом модуле, она импортируется из других модулей.

Другими словами, если вы вручную импортируете функцию вверху модуля, но она не вызывается. роллап не вводится. Как видно из нашего примера, всегоfoo1() foo2()две функции,_dependsOnно толькоfoo1(), потому что введеноfoo2()Не звонил.

_dependsOnкакая функция? При последующей генерации кода он будет основываться на_dependsOnЗначение для импорта файла.

3. Прочитайте соответствующий файл согласно зависимостям.

от_dependsOnЗначение можно найти, нужно ввестиfoo1()функция.

На этом первом шаге генерируетсяimportsЭто сработало:

imports = {
  foo1: { source: './foo', name: 'foo1', localName: 'foo1' },
  foo2: { source: './foo', name: 'foo2', localName: 'foo2' }
}

Сводкаfoo1В качестве ключа найдите соответствующий ему файл. Затем прочитайте этот файл, чтобы сгенерировать новыйModuleпример. из-заfoo.jsФайл экспортирует две функции, поэтому этот новыйModuleпримерexportsСвойства подобны этому:

exports = {
  foo1: {
    node: Node {
      type: 'ExportNamedDeclaration',
      start: 0,
      end: 25,
      declaration: [Node],
      specifiers: [],
      source: null
    },
    localName: 'foo1',
    expression: Node {
      type: 'FunctionDeclaration',
      start: 7,
      end: 25,
      id: [Node],
      expression: false,
      generator: false,
      params: [],
      body: [Node]
    }
  },
  foo2: {
    node: Node {
      type: 'ExportNamedDeclaration',
      start: 27,
      end: 52,
      declaration: [Node],
      specifiers: [],
      source: null
    },
    localName: 'foo2',
    expression: Node {
      type: 'FunctionDeclaration',
      start: 34,
      end: 52,
      id: [Node],
      expression: false,
      generator: false,
      params: [],
      body: [Node]
    }
  }
}

В это время вы будете использоватьmain.jsИмпортироватьfoo1Как ключ к матчуfoo.jsизexportsобъект. Если совпадение прошло успешно, поставьтеfoo1()Узел AST, соответствующий функции, извлекается и помещается вBundleсередина. Если совпадение не удается, будет сообщено об ошибке, предлагающейfoo.jsЭта функция не экспортируется.

4. Сгенерируйте код.

Так как все функции были введены. Тогда вам нужно позвонитьBundleизgenerate()Метод генерирования кода.

В то же время в процессе упаковки над импортированными функциями необходимо выполнить некоторые дополнительные операции.

удалить лишний код

например изfoo.jsЗарегистрированоfoo1()export function foo1() {}. rollup 会移除掉export ,сталиfunction foo1() {}.因为它们就要打包在一起了,所以就不需要export.

Переименовать

foo(), при объединении одна из функций будет переименована в_foo(), чтобы избежать конфликтов.

Хорошо, вернемся к тексту.

Помните, что я упоминал в начале статьиmagic-stringбиблиотека? существуетgenerate(), исходный код, соответствующий каждому узлу AST, будет добавлен вmagic-stringВ примере:

magicString.addSource({
    content: source,
    separator: newLines
})

Эта операция по сути эквивалентна написанию строк:

str += '这个操作相当于将每个 AST 的源代码当成字符串拼在一起,就像现在这样'

Наконец, верните собранный код.

return { code: magicString.toString() }

Это конец, если вы хотите сгенерировать код в файл, вы можете вызватьwrite()Метод генерирует файл:

rollup(__dirname + '/main.js').then(res => {
    res.wirte('dist.js')
})

Этот метод написанrollup()в функции.

function rollup(entry, options = {}) {
    const bundle = new Bundle({ entry, ...options })
    return bundle.build().then(() => {
        return {
            generate: options => bundle.generate(options),
            wirte(dest, options = {}) {
                const { code } = bundle.generate({
					dest,
					format: options.format,
				})

				return fs.writeFile(dest, code, err => {
                    if (err) throw err
                })
            }
        }
    })
}

конец

Эта статья абстрагирует исходный код, поэтому многие детали реализации не упоминаются. Если вас интересуют детали реализации, вы можете взглянуть на исходный код. код в моемgithubначальство.

Я обрезал первоначальный исходный код накопительного пакета и добавил множество комментариев, чтобы код было легче читать.