предисловие
Для того, чтобы изучить принцип накопительной упаковки, я клонировал исходный код последней версии (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.
первый шаг
каждыйModule
imports
а также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начальство.
Я обрезал первоначальный исходный код накопительного пакета и добавил множество комментариев, чтобы код было легче читать.