В нашем последнем Supercharged Livestream мы реализовали разделение кода и фрагментацию на основе маршрутов. С HTTP/2 и собственными модулями ES6 эти методы станут необходимыми для обеспечения эффективной загрузки и кэширования ресурсов скрипта.
Разные советы и рекомендации в этом эпизоде
-
asyncFunction().catch()
сerror.stack
: 9:55 - Модули и атрибут
nomodule
в тегах<script>
: 7:30 -
promisify()
в узле 8: 17:20
TL;DR
Как выполнить разделение кода с помощью фрагментации на основе маршрутов:
- Получите список ваших точек входа.
- Извлеките зависимости модулей всех этих точек входа.
- Найдите общие зависимости между всеми точками входа.
- Объедините общие зависимости.
- Перепишите точки входа.
Разделение кода против фрагментации на основе маршрута
Разделение кода и фрагментация на основе маршрута тесно связаны и часто используются как взаимозаменяемые. Это вызвало некоторую путаницу. Давайте попробуем прояснить это:
- Разделение кода : Разделение кода — это процесс разделения вашего кода на несколько пакетов. Если вы не отправляете клиенту один большой пакет со всем вашим JavaScript, вы делаете разделение кода. Одним из конкретных способов разделения вашего кода является использование фрагментации на основе маршрутов.
- Разбиение на основе маршрутов : Разбиение на основе маршрутов создает пакеты, которые связаны с маршрутами вашего приложения. Анализируя ваши маршруты и их зависимости, мы можем изменить, какие модули попадают в какой пакет.
Зачем нужно разделение кода?
Свободные модули
С собственными модулями ES6 каждый модуль JavaScript может импортировать свои собственные зависимости. Когда браузер получает модуль, все операторы import
будут запускать дополнительные выборки для получения модулей, необходимых для запуска кода. Однако все эти модули могут иметь свои собственные зависимости. Опасность заключается в том, что браузер в конечном итоге получает каскад выборок, которые длятся несколько циклов, прежде чем код сможет быть наконец выполнен.
Комплектация
Объединение, которое встраивает все ваши модули в один пакет, гарантирует, что браузер получит весь необходимый ему код после 1 кругового обхода и сможет быстрее запустить код. Однако это заставляет пользователя загружать много ненужного кода, поэтому пропускная способность и время тратятся впустую. Кроме того, каждое изменение одного из наших исходных модулей приведет к изменению пакета, что сделает недействительной любую кэшированную версию пакета. Пользователям придется заново загружать все это.
Разделение кода
Разделение кода — это золотая середина. Мы готовы вкладывать дополнительные циклы, чтобы повысить эффективность сети, загружая только то, что нам нужно, и повысить эффективность кэширования, значительно уменьшив количество модулей в пакете. Если пакетирование выполнено правильно, общее количество циклов будет намного ниже, чем при свободных модулях. Наконец, мы могли бы использовать механизмы предварительной загрузки, такие как link[rel=preload]
чтобы сэкономить дополнительное время на три раунда, если это необходимо.
Шаг 1: Получите список ваших точек входа
Это только один из многих подходов, но в эпизоде мы проанализировали sitemap.xml
веб-сайта, чтобы получить точки входа на наш веб-сайт. Обычно используется специальный файл JSON, в котором перечислены все точки входа.
Использование Babel для обработки JavaScript
Babel обычно используется для «транспиляции»: использования новейшего кода JavaScript и превращения его в старую версию JavaScript, чтобы больше браузеров могли выполнить этот код. Первым шагом здесь является разбор нового JavaScript с помощью парсера (Babel использует babylon ), который превращает код в так называемое «абстрактное синтаксическое дерево» (AST). После того, как AST сгенерирован, ряд плагинов анализируют и искажают AST.
Мы собираемся активно использовать babel для обнаружения (и последующей манипуляции) импорта модуля JavaScript. У вас может возникнуть соблазн обратиться к регулярным выражениям, но регулярные выражения недостаточно мощны для правильного анализа языка и их трудно поддерживать. Использование проверенных инструментов, таких как Babel, избавит вас от многих головных болей.
Вот простой пример запуска Babel с пользовательским плагином:
const plugin = {
visitor: {
ImportDeclaration(decl) {
/* ... */
}
}
}
const {code} = babel.transform(inputCode, {plugins: [plugin]});
Плагин может предоставить объект visitor
. Посетитель содержит функцию для любого типа узла, который плагин хочет обработать. Когда узел этого типа встречается при прохождении AST, соответствующая функция в объекте visitor
будет вызвана с этим узлом в качестве параметра. В приведенном выше примере метод ImportDeclaration()
будет вызван для каждого объявления import
в файле. Чтобы лучше понять типы узлов и AST, взгляните на astexplorer.net .
Шаг 2: Извлечение зависимостей модуля
Чтобы построить дерево зависимостей модуля, мы проанализируем этот модуль и создадим список всех импортируемых им модулей. Нам также нужно проанализировать эти зависимости, поскольку они, в свою очередь, также могут иметь зависимости. Классический случай рекурсии!
async function buildDependencyTree(file) {
let code = await readFile(file);
code = code.toString('utf-8');
// `dep` will collect all dependencies of `file`
let dep = [];
const plugin = {
visitor: {
ImportDeclaration(decl) {
const importedFile = decl.node.source.value;
// Recursion: Push an array of the dependency’s dependencies onto the list
dep.push((async function() {
return await buildDependencyTree(`./app/${importedFile}`);
})());
// Push the dependency itself onto the list
dep.push(importedFile);
}
}
}
// Run the plugin
babel.transform(code, {plugins: [plugin]});
// Wait for all promises to resolve and then flatten the array
return flatten(await Promise.all(dep));
}
Шаг 3: Найдите общие зависимости между всеми точками входа.
Поскольку у нас есть набор деревьев зависимостей — лес зависимостей, если хотите, — мы можем найти общие зависимости, ища узлы, которые появляются в каждом дереве. Мы выровняем и дедуплицируем наш лес и отфильтруем, чтобы оставить только элементы, которые появляются во всех деревьях.
function findCommonDeps(depTrees) {
const depSet = new Set();
// Flatten
depTrees.forEach(depTree => {
depTree.forEach(dep => depSet.add(dep));
});
// Filter
return Array.from(depSet)
.filter(dep => depTrees.every(depTree => depTree.includes(dep)));
}
Шаг 4: Объединение общих зависимостей
Чтобы связать наш набор общих зависимостей, мы могли бы просто объединить все файлы модулей. При использовании этого подхода возникают две проблемы: первая проблема заключается в том, что связка все еще будет содержать операторы import
, которые заставят браузер попытаться извлечь ресурсы. Вторая проблема заключается в том, что зависимости зависимостей не были связаны. Поскольку мы уже делали это раньше, мы собираемся написать еще один плагин babel.
Код довольно похож на наш первый плагин, но вместо того, чтобы просто извлечь импорты, мы также удалим их и вставим связанную версию импортированного файла:
async function bundle(oldCode) {
// `newCode` will be filled with code fragments that eventually form the bundle.
let newCode = [];
const plugin = {
visitor: {
ImportDeclaration(decl) {
const importedFile = decl.node.source.value;
newCode.push((async function() {
// Bundle the imported file and add it to the output.
return await bundle(await readFile(`./app/${importedFile}`));
})());
// Remove the import declaration from the AST.
decl.remove();
}
}
};
// Save the stringified, transformed AST. This code is the same as `oldCode`
// but without any import statements.
const {code} = babel.transform(oldCode, {plugins: [plugin]});
newCode.push(code);
// `newCode` contains all the bundled dependencies as well as the
// import-less version of the code itself. Concatenate to generate the code
// for the bundle.
return flatten(await Promise.all(newCode)).join('\n');
}
Шаг 5: Перепишите точки входа
Для последнего шага мы напишем еще один плагин Babel. Его задача — удалить все импорты модулей, которые находятся в общем пакете.
async function rewrite(section, sharedBundle) {
let oldCode = await readFile(`./app/static/${section}.js`);
oldCode = oldCode.toString('utf-8');
const plugin = {
visitor: {
ImportDeclaration(decl) {
const importedFile = decl.node.source.value;
// If this import statement imports a file that is in the shared bundle, remove it.
if(sharedBundle.includes(importedFile))
decl.remove();
}
}
};
let {code} = babel.transform(oldCode, {plugins: [plugin]});
// Prepend an import statement for the shared bundle.
code = `import '/static/_shared.js';\n${code}`;
await writeFile(`./app/static/_${section}.js`, code);
}
Конец
Это было довольно увлекательно, не так ли? Пожалуйста, помните, что нашей целью в этом эпизоде было объяснить и демистифицировать разделение кода. Результат работает, но он специфичен для нашего демонстрационного сайта и будет ужасно неудачным в общем случае. Для производства я бы рекомендовал полагаться на проверенные инструменты, такие как WebPack, RollUp и т. д.
Наш код можно найти в репозитории GitHub .
Увидимся в следующий раз!