Перейти к содержанию

Формат пакетов Angular

📅 6.03.2023

Этот документ описывает формат пакетов Angular (APF). APF — это специфическая для Angular спецификация структуры и формата пакетов npm, которая используется всеми сторонними пакетами Angular (@angular/core, @angular/material и т.д.) и большинством сторонних библиотек Angular.

APF позволяет пакету бесперебойно работать в большинстве распространенных сценариев, использующих Angular. Пакеты, использующие APF, совместимы с инструментарием, предлагаемым командой Angular, а также с более широкой экосистемой JavaScript.

Разработчикам сторонних библиотек рекомендуется придерживаться того же формата пакетов npm.

APF версионируется вместе с остальной частью Angular, и в каждой основной версии улучшается формат пакета. Вы можете найти версии спецификации до v13 в этом google doc.

Зачем указывать формат пакета?

В современном JavaScript ландшафте разработчики используют пакеты различными способами, используя множество различных инструментальных цепочек (Webpack, rollup, esbuild и т.д.). Эти инструменты могут понимать и требовать различные входные данные — некоторые инструменты могут обрабатывать последнюю версию языка ES, в то время как другие могут выиграть от прямого потребления более старой версии ES.

Формат дистрибутива Angular поддерживает все широко используемые инструменты разработки и рабочие процессы, а также делает упор на оптимизацию, которая приводит либо к уменьшению размера полезной нагрузки приложения, либо к ускорению цикла итераций разработки (build time).

Разработчики могут полагаться на Angular CLI и ng-packagr (инструмент сборки, который использует Angular CLI) для создания пакетов в формате Angular package. Подробнее см. в руководстве Creating Libraries.

Расположение файлов

В следующем примере показана упрощенная версия схемы расположения файлов пакета @angular/core с пояснениями для каждого файла в пакете.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
node_modules/@angular/core
+- README.md
+- package.json
+- index.d.ts
+- esm2022
|  +- core.mjs
|  +- index.mjs
|  +- public_api.mjs
|  +- testing
+- fesm2022
|  +- core.mjs
|  +- core.mjs.map
|  +- testing.mjs
|  +- testing.mjs.map
+- testing
   +- index.d.ts

Эта таблица описывает расположение файлов в node_modules/@angular/core с аннотациями, описывающими назначение файлов и каталогов:

Files Purpose
README.md README пакета, используется веб-интерфейсом npmjs.
package.json Первичный package.json, описывающий сам пакет, а также все доступные точки входа и форматы кода. Этот файл содержит отображение "exports", используемое программами выполнения и инструментами для выполнения разрешения модулей.
index.d.ts Пакет .d.ts для основной точки входа @angular/core.
esm2022/
  ─ core.mjs
  ─ index.mjs
  ─ public_api.mjs
Дерево источников @angular/core в нерасширенном формате ES2022.
esm2022/testing/ Дерево точки входа @angular/core/testing в нерасширенном формате ES2022.
fesm2022/
  ─ core.mjs
  ─ core.mjs.map
  ─ testing.mjs
  ─ testing.mjs.map
Код для всех точек входа в уплощенном (FESM) формате ES2022, вместе с исходными картами.
testing/ Каталог, представляющий точку входа "тестирование".
testing/index.d.ts Пакет .d.ts для точки входа @angular/core/testing.

package.json

Основной package.json содержит важные метаданные пакета, включая следующие:

  • Он декларирует пакет в формате EcmaScript Module (ESM).

  • Он содержит поле экспорт, которое определяет доступные форматы исходного кода всех точек входа

  • Содержит ключи, которые определяют доступные форматы исходного кода основной точки входа @angular/core, для инструментов, которые не понимают "exports".

    Эти ключи считаются устаревшими и могут быть удалены по мере расширения поддержки "exports" в экосистеме.

  • Объявляет, содержит ли пакет побочные эффекты

ESM декларация

Ключ содержится в файле верхнего уровня package.json:

1
2
3
{
  "type": "module"
}

Это сообщает разрешителям, что код в пакете использует модули EcmaScript, а не модули CommonJS.

"exports"

Поле "exports" имеет следующую структуру:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
"exports": {
  "./schematics/*": {
    "default": "./schematics/*.js"
  },
  "./package.json": {
    "default": "./package.json"
  },
  ".": {
    "types": "./core.d.ts",
    "esm": "./esm2022/core.mjs",
    "esm2022": "./esm2022/core.mjs",
    "default": "./fesm2022/core.mjs"
  },
  "./testing": {
    "types": "./testing/testing.d.ts",
    "esm": "./esm2022/testing/testing.mjs",
    "esm2022": "./esm2022/testing/testing.mjs",
    "default": "./fesm2022/testing.mjs"
  }
}

Основной интерес представляют ключи "." и "./testing", которые определяют доступные форматы кода для первичной точки входа @angular/core и вторичной точки входа @angular/core/testing соответственно. Для каждой точки входа доступны следующие форматы:

Форматы Детали
Типизация (.d.ts файлы) .d.ts файлы используются TypeScript, когда зависят от данного пакета.
es2022 Код ES2022 сжат в один исходный файл.
esm2022 Код ES2022 в нерасплющенных исходных файлах (этот формат включен для экспериментов — подробности смотрите в этом обсуждении дефолтов).
default Код ES2022, сплющенный в один исходник.

Инструментарий, знающий эти ключи, может предпочтительно выбирать желаемый формат кода из экспорта.

Библиотеки могут захотеть раскрыть дополнительные статические файлы, которые не попадают в экспорт точек входа на основе JavaScript, такие как Sass mixins или предварительно скомпилированный CSS.

Для получения дополнительной информации смотрите Управление активами в библиотеке.

Устаревшие ключи разрешения

В дополнение к "exports", в файле верхнего уровня package.json также определяются ключи разрешения устаревших модулей для распознавателей, которые не поддерживают "exports". Для @angular/core это:

1
2
3
4
{
    "module": "./fesm2022/core.mjs",
    "typings": "./core.d.ts"
}

Как показано в предыдущем фрагменте кода, модуль resolver может использовать эти ключи для загрузки определенного формата кода.

Побочные эффекты

Последняя функция package.json — это объявление о том, имеет ли пакет побочные эффекты.

1
2
3
{
    "sideEffects": false
}

Большинство пакетов Angular не должны зависеть от побочных эффектов верхнего уровня и поэтому должны включать это объявление.

Точки входа и разделение кода

Пакеты в формате пакетов Angular содержат одну первичную точку входа и ноль или более вторичных точек входа (например, @angular/common/http). Точки входа выполняют несколько функций.

  1. Они определяют спецификаторы модулей, из которых пользователи импортируют код (например, @angular/core и @angular/core/testing).

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

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

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

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

    Многие современные инструменты сборки способны "разделять код" (aka lazy loading) только на уровне ES-модуля.

    Формат пакетов Angular Package Format использует в основном один "плоский" ES-модуль для каждой точки входа. Это означает, что большинство инструментов сборки не способны разделить код с одной точкой входа на несколько выходных фрагментов.

Общее правило для пакетов APF — использовать точки входа для минимально возможных наборов логически связанного кода. Например, пакет Angular Material публикует каждый логический компонент или набор компонентов как отдельную точку входа — одну для кнопки, одну для вкладок и т. д.

Это позволяет при желании лениво загружать каждый компонент Material отдельно.

Не все библиотеки требуют такой детализации. Большинство библиотек с единой логической целью должны публиковаться как одна точка входа.

Например, @angular/core использует одну точку входа для среды выполнения, потому что среда выполнения Angular обычно используется как единое целое.

Разрешение вторичных точек входа

Вторичные точки входа могут быть разрешены через поле "exports" в файле package.json для пакета.

README.md

Файл README в формате Markdown, который используется для отображения описания пакета на npm и GitHub.

Пример содержимого README пакета @angular/core:

1
2
3
4
5
# Angular

The sources for this package are in the main [Angular](https://github.com/angular/angular) repo.Please file issues and pull requests against that repo.

License: MIT

Частичная компиляция

Библиотеки в формате пакетов Angular должны публиковаться в режиме "частичной компиляции". Это режим компиляции для ngc, который производит скомпилированный код Angular, не привязанный к конкретной версии среды выполнения Angular, в отличие от полной компиляции, используемой для приложений, где версии компилятора и среды выполнения Angular должны точно совпадать.

Для частичной компиляции кода Angular используйте флаг compilationMode в свойстве angularCompilerOptions вашего tsconfig.json:

1
2
3
4
5
6
{
  
  "angularCompilerOptions": {
    "compilationMode": "partial",
  }
}

Частично скомпилированный код библиотеки затем преобразуется в полностью скомпилированный код в процессе сборки приложения с помощью Angular CLI.

Если ваш конвейер сборки не использует Angular CLI, обратитесь к руководству Consuming partial ivy code outside the Angular CLI.

Оптимизации

Сплющивание модулей ES

Формат пакетов Angular Package Format предписывает публиковать код в "сплющенном" формате ES-модулей. Это значительно сокращает время сборки приложений Angular, а также время загрузки и разбора конечного пакета приложения.

Пожалуйста, ознакомьтесь с отличным постом "The cost of small modules" Нолана Лоусона.

Компилятор Angular может генерировать файлы модулей index ES. Такие инструменты, как Rollup, могут использовать эти файлы для создания уплощенных модулей в формате Flattened ES Module (FESM).

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

За сокращенным названием FESM, произносимым как phe-som, может следовать номер, например FESM2020. Номер относится к уровню языка JavaScript внутри модуля.

Соответственно, файл FESM2022 будет иметь формат ESM+ES2022 и включать заявления импорта/экспорта и исходный код ES2022.

Чтобы сгенерировать уплощенный индексный файл модуля ES, используйте следующие параметры конфигурации в файле tsconfig.json:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "compilerOptions": {
    
    "module": "esnext",
    "target": "es2022",
    
  },
  "angularCompilerOptions": {
    
    "flatModuleOutFile": "my-ui-lib.js",
    "flatModuleId": "my-ui-lib"
  }
}

Как только индексный файл (например, my-ui-lib.js) сгенерирован ngc, можно использовать бандлеры и оптимизаторы, такие как Rollup, для создания сглаженного ESM файла.

Примечание о значениях по умолчанию в package.json.

Начиная с webpack v4, оптимизация сплющивания ES-модулей не должна быть необходима пользователям webpack. Должно быть возможно получить лучшее разделение кода без сплющивания модулей в webpack. На практике регрессия размеров все еще может наблюдаться при использовании не уплощенных модулей в качестве входных данных для webpack v4.

Вот почему записи module и es2022 package.json по-прежнему указывают на файлы FESM.

В настоящее время этот вопрос изучается. Ожидается, что после устранения проблемы регрессии размера, точки входа module и es2022 package.json будут переключены на нерасплющенные файлы.

В настоящее время в APF включен неплющенный код ESM2022 для проверки такого будущего изменения.

флаг "sideEffects"

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

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

Инструменты сборки, такие как Webpack, поддерживают флаг, который позволяет пакетам объявлять, что они не зависят от побочного кода на верхнем уровне своих модулей, что дает инструментам больше свободы для древовидного расщепления кода из пакета. Конечным результатом этих оптимизаций должен стать меньший размер пакета и лучшее распределение кода по частям пакета после разбиения кода. Эта оптимизация может сломать ваш код, если он содержит нелокальные побочные эффекты — однако это не часто встречается в приложениях Angular и обычно является признаком плохого дизайна.

Рекомендуется, чтобы все пакеты претендовали на статус свободных от побочных эффектов, устанавливая свойство sideEffects в false, и чтобы разработчики следовали Angular Style Guide, что естественным образом приводит к коду без нелокальных побочных эффектов.

Дополнительная информация: webpack docs on side effects

Языковой уровень ES2022

Языковой уровень ES2022 теперь является языковым уровнем по умолчанию, который используется Angular CLI и другими инструментами. Angular CLI понижает уровень пакета до уровня языка, который поддерживается всеми целевыми браузерами во время сборки приложения.

.d.ts bundling / сглаживание определений типов

Начиная с APF v8, теперь предпочтительно запускать API Extractor для пакетирования определений TypeScript так, чтобы весь API был представлен в одном файле.

В предыдущих версиях APF каждая точка входа имела каталог src рядом с точкой входа .d.ts, и этот каталог содержал отдельные файлы .d.ts, соответствующие структуре исходного кода. Хотя этот формат распространения все еще разрешен и поддерживается, он крайне не рекомендуется, поскольку сбивает с толку такие инструменты, как IDE, которые затем предлагают неправильное автозаполнение, и позволяет пользователям зависеть от путей глубокого импорта, которые обычно не считаются публичным API библиотеки или пакета.

Tslib

Начиная с APF v10, рекомендуется добавлять tslib в качестве прямой зависимости от вашей основной точки входа. Это связано с тем, что версия tslib привязана к версии TypeScript, используемой для компиляции вашей библиотеки.

Примеры

Определение терминов

Следующие термины используются во всем этом документе намеренно. В этом разделе приведены определения всех этих терминов для обеспечения дополнительной ясности.

Пакет

Наименьший набор файлов, которые публикуются в NPM и устанавливаются вместе, например @angular/core. Пакет включает манифест под названием package.json, скомпилированный исходный код, файлы определения typescript, карты исходников, метаданные и т.д.

Пакет устанавливается с помощью npm install @angular/core.

Символ

Класс, функция, константа или переменная, содержащаяся в модуле и по желанию видимая внешнему миру через экспорт модуля.

Модуль

Сокращение от ECMAScript Modules. Файл, содержащий операторы, которые импортируют и экспортируют символы.

Это идентично определению модулей в спецификации ECMAScript.

ESM

Сокращение от ECMAScript Modules (см. выше).

FESM

Сокращение от Flattened ES Modules и представляет собой формат файла, созданный путем сглаживания всех ES-модулей, доступных из точки входа, в один ES-модуль.

Идентификатор модуля

Идентификатор модуля, используемый в операторах импорта (например, @angular/core). Часто идентификатор напрямую связан с путем в файловой системе, но это не всегда так из-за различных стратегий разрешения модулей.

Спецификатор модуля

Идентификатор модуля (см. выше).

Стратегия разрешения модулей

Алгоритм, используемый для преобразования идентификаторов модулей в пути в файловой системе. В Node.js есть один, который хорошо специфицирован и широко используется, TypeScript поддерживает несколько стратегий разрешения модулей, Closure Compiler имеет еще одну стратегию.

Формат модуля

Спецификация синтаксиса модуля, которая охватывает, как минимум, синтаксис для импорта и экспорта из файла. Общими форматами модулей являются CommonJS (CJS, обычно используется для приложений Node.js) или ECMAScript Modules (ESM).

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

В связи с этим команда Angular часто использует спецификатор уровня языка в качестве суффикса к формату модуля, (например, ESM+ES2022 указывает, что модуль имеет формат ESM и содержит код ES2022).

Пакет

Артефакт в виде одного JS файла, созданный инструментом сборки (например, Webpack или Rollup), который содержит символы из одного или нескольких модулей. Пакеты — это специфическое для браузера обходное решение, снижающее нагрузку на сеть, которая была бы вызвана, если бы браузеры начали загружать сотни, а то и десятки тысяч файлов.

Node.js обычно не использует пакеты.

Распространенными форматами пакетов являются UMD и System.register.

Языковой уровень

Язык кода (ES2022). Не зависит от формата модуля.

Точка входа

Модуль, предназначенный для импорта пользователем. На него ссылается уникальный идентификатор модуля, и он экспортирует публичный API, на который ссылается этот идентификатор модуля.

Примером может служить @angular/core или @angular/core/testing.

Обе точки входа существуют в пакете @angular/core, но они экспортируют разные символы.

Пакет может иметь много точек входа.

Глубокий импорт

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

Импорт верхнего уровня

Импорт, исходящий из точки входа. Доступные импорты верхнего уровня определяют публичный API и раскрываются в модулях "@angular/name", таких как @angular/core или @angular/common.

Древовидная встряска

Процесс выявления и удаления кода, не используемого приложением — также известный как устранение мертвого кода. Это глобальная оптимизация, выполняемая на уровне приложения с помощью таких инструментов, как Rollup, Closure Compiler или Terser.

AOT-компилятор

Компилятор Ahead of Time для Angular.

Плоские определения типов

Пакетные определения TypeScript, созданные с помощью API Extractor.

Комментарии