Блог Supercharged с прямыми трансляциями — разделение кода

В нашей последней прямой трансляции Supercharged мы реализовали разделение кода и фрагментацию на основе маршрутов. Благодаря HTTP/2 и собственным модулям ES6 эти методы станут важными для обеспечения эффективной загрузки и кэширования ресурсов сценариев.

Разные советы и подсказки в этом выпуске

  • asyncFunction().catch() с error.stack : 9:55
  • Модули и атрибут nomodule в тегах <script> : 7:30
  • promisify() в узле 8: 17:20

ТЛ;ДР

Как выполнить разделение кода с помощью фрагментов на основе маршрутов:

  1. Получите список точек входа.
  2. Извлеките зависимости модулей всех этих точек входа.
  3. Найдите общие зависимости между всеми точками входа.
  4. Объедините общие зависимости.
  5. Перепишите точки входа.

Разделение кода и разделение на основе маршрутов

Разделение кода и фрагментирование на основе маршрутов тесно связаны между собой и часто используются как взаимозаменяемые. Это вызвало некоторую путаницу. Попробуем прояснить это:

  • Разделение кода . Разделение кода — это процесс разделения вашего кода на несколько пакетов. Если вы не отправляете клиенту один большой пакет со всем своим JavaScript, вы выполняете разделение кода. Одним из конкретных способов разделения вашего кода является использование фрагментов на основе маршрутов.
  • Разбиение на основе маршрутов . Разбиение на основе маршрутов создает пакеты, связанные с маршрутами вашего приложения. Анализируя ваши маршруты и их зависимости, мы можем изменить, какие модули входят в какой пакет.

Зачем нужно разделение кода?

Свободные модули

Благодаря нативным модулям ES6 каждый модуль JavaScript может импортировать свои собственные зависимости. Когда браузер получает модуль, все операторы import запускают дополнительные выборки, чтобы получить модули, необходимые для запуска кода. Однако все эти модули могут иметь собственные зависимости. Опасность заключается в том, что в браузере возникает каскад выборок, который длится несколько циклов, прежде чем код сможет наконец выполниться.

Объединение

Объединение, то есть встраивание всех ваших модулей в один пакет, гарантирует, что браузер получит весь необходимый код после одного прохода, и сможет начать выполнение кода быстрее. Однако это вынуждает пользователя загружать много ненужного кода, в результате чего полоса пропускания и время тратятся впустую. Кроме того, каждое изменение в одном из наших исходных модулей приведет к изменению пакета, что приведет к аннулированию любой кэшированной версии пакета. Пользователям придется заново загружать все это.

Разделение кода

Разделение кода — это золотая середина. Мы готовы инвестировать дополнительные средства туда и обратно, чтобы повысить эффективность сети за счет загрузки только того, что нам нужно, а также повысить эффективность кэширования за счет значительного уменьшения количества модулей в каждом пакете. Если комплектация выполнена правильно, общее количество обращений туда и обратно будет намного меньше, чем при использовании отдельных модулей. Наконец, мы могли бы использовать механизмы предварительной загрузки , такие как 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 .

Увидимся в следующий раз!