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

Компиляция с опережением времени (AOT)

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

Компилятор Angular ahead-of-time (AOT) compiler преобразует ваш код Angular HTML и TypeScript в эффективный код JavaScript на этапе сборки до того, как браузер загрузит и запустит этот код. Компиляция вашего приложения в процессе сборки обеспечивает более быстрый рендеринг в браузере.

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

Вот несколько причин, по которым вы можете захотеть использовать AOT.

Причины Подробности
Более быстрый рендеринг При использовании AOT браузер загружает предварительно скомпилированную версию приложения. Браузер загружает исполняемый код и может сразу же отрисовать приложение, не дожидаясь его компиляции.
Меньше асинхронных запросов Компилятор включает внешние HTML-шаблоны и таблицы стилей CSS в JavaScript приложения, устраняя отдельные ajax-запросы к этим исходным файлам.
Меньший размер загрузки фреймворка Angular Нет необходимости загружать компилятор Angular, если приложение уже скомпилировано. Компилятор составляет примерно половину самого Angular, поэтому его отказ от загрузки значительно уменьшает полезную нагрузку на приложение.
Более раннее обнаружение ошибок шаблонов Компилятор AOT обнаруживает и сообщает об ошибках привязки шаблонов на этапе сборки до того, как их увидят пользователи.
Повышение безопасности AOT компилирует HTML-шаблоны и компоненты в файлы JavaScript задолго до того, как они будут переданы клиенту. Отсутствие необходимости читать шаблоны и рискованной оценки HTML или JavaScript на стороне клиента снижает вероятность инъекционных атак.

Выбор компилятора

Angular предлагает два способа компиляции вашего приложения:

Angular compile Details
Just-in-Time (JIT) Компилирует ваше приложение в браузере во время выполнения. Это было по умолчанию до Angular 8.
Ahead-of-Time (AOT) Компилирует ваше приложение и библиотеки во время сборки. Этот вариант используется по умолчанию, начиная с Angular 9.

Когда вы выполняете CLI-команды ng build (build only) или ng serve (build and serve locally), тип компиляции (JIT или AOT) зависит от значения свойства aot в вашей конфигурации сборки, указанной в angular.json. По умолчанию aot установлено в true для новых приложений CLI.

Дополнительную информацию см. в CLI command reference и Building and serving Angular apps.

Как работает AOT

Компилятор Angular AOT извлекает метаданные для интерпретации частей приложения, которыми должен управлять Angular. Вы можете указать метаданные явно в декораторах, таких как @Component() и @Input(), или неявно в объявлениях конструкторов декорированных классов.

Метаданные указывают Angular, как создавать экземпляры классов вашего приложения и взаимодействовать с ними во время выполнения.

В следующем примере объект метаданных @Component() и конструктор класса указывают Angular, как создать и отобразить экземпляр TypicalComponent.

1
2
3
4
5
6
7
8
@Component({
  selector: 'app-typical',
  template: '<div>A typical component for {{data.name}}</div>'
})
export class TypicalComponent {
  @Input() data: TypicalData;
  constructor(private someService: SomeService) {  }
}

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

Фазы компиляции

Существует три фазы компиляции AOT.

Фаза Детали
1 анализ кода На этой фазе компилятор TypeScript и AOT-сборщик создают представление исходного текста. Сборщик не пытается интерпретировать метаданные, которые он собирает. Он представляет метаданные как можно лучше и записывает ошибки, когда обнаруживает нарушение синтаксиса метаданных.
2 Генерация кода На этой фазе StaticReflector компилятора интерпретирует метаданные, собранные на фазе 1, выполняет дополнительную проверку метаданных и выдает ошибку, если обнаруживает нарушение ограничений метаданных.
3 Проверка типов шаблонов На этом необязательном этапе компилятор шаблонов Angular template compiler использует компилятор TypeScript для проверки выражений привязки в шаблонах. Вы можете включить эту фазу явно, установив параметр конфигурации strictTemplates; см. Angular compiler options.

Ограничения метаданных

Вы пишете метаданные в подмножестве TypeScript, которое должно соответствовать следующим общим ограничениям:

Дополнительные рекомендации и инструкции по подготовке приложения к компиляции AOT см. в Angular: Writing AOT-friendly applications.

Ошибки при компиляции AOT обычно возникают из-за метаданных, которые не соответствуют требованиям компилятора (более подробно описано ниже). Для помощи в понимании и решении этих проблем смотрите AOT Metadata Errors.

Настройка компиляции AOT

В файле TypeScript configuration file можно указать опции, управляющие процессом компиляции. Полный список доступных опций см. в Angular compiler options.

Фаза 1: Анализ кода

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

В то же время AOT коллектор анализирует метаданные, записанные в декораторах Angular, и выводит информацию о метаданных в файлы .metadata.json, по одному на файл .d.ts.

Вы можете думать о .metadata.json как о диаграмме общей структуры метаданных декоратора, представленной в виде абстрактного синтаксического дерева (AST).

В schema.ts Angular формат JSON описывается как набор интерфейсов TypeScript.

Ограничения синтаксиса выражений

Коллектор AOT понимает только подмножество JavaScript. Определяйте объекты метаданных с помощью следующего ограниченного синтаксиса:

Syntax Example
Literal object {cherry: true, apple: true, mincemeat: false}
Literal array ['cherries', 'flour', 'sugar']
Spread in literal array ['apples', 'flour', ...]
Calls bake(ingredients)
New new Oven()
Property access pie.slice
Array index ingredients[0]
Identity reference Component
A template string `pie is ${multiplier} times better than cake`
Literal string 'pi'
Literal number 3.14153265
Literal boolean true
Literal null null
Supported prefix operator !cake
Supported binary operator a+b
Conditional operator a ? b : c
Parentheses (a+b)

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

Если вы хотите, чтобы ngc немедленно сообщал о синтаксических ошибках, а не создавал файл .metadata.json с ошибками, установите опцию strictMetadataEmit в конфигурационном файле TypeScript.

1
2
3
4
"angularCompilerOptions": {

"strictMetadataEmit" : true
}

В библиотеках Angular есть эта опция для обеспечения чистоты всех файлов Angular .metadata.json, и это лучшая практика — делать то же самое при создании собственных библиотек.

Нет стрелочных функций

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

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

1
2
3
4
@Component({
  
  providers: [{provide: server, useFactory: () => new Server()}]
})

Коллектор AOT не поддерживает функцию стрелки, () => new Server(), в выражении метаданных. Вместо функции он генерирует узел ошибки.

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

Вы можете исправить ошибку, преобразовав ее в это выражение:

1
2
3
4
5
6
7
8
export function serverFactory() {
  return new Server();
}

@Component({
  
  providers: [{provide: server, useFactory: serverFactory}]
})

В версии 5 и более поздних компилятор автоматически выполняет эту перезапись при эмуляции файла .js.

Сворачивание кода

Компилятор может разрешать только ссылки на экспортированные символы. Однако сборщик может оценить выражение во время сбора и записать результат в .metadata.json, а не исходное выражение.

Это позволяет ограниченно использовать неэкспортированные символы в выражениях.

Например, сборщик может оценить выражение 1 + 2 + 3 + 4 и заменить его результатом 10. Этот процесс называется сворачиванием.

Выражение, которое может быть сокращено таким образом, является сворачиваемым.

Коллектор может оценивать ссылки на локальные для модуля объявления const и инициализированные объявления var и let, эффективно удаляя их из файла .metadata.json.

Рассмотрим следующее определение компонента:

1
2
3
4
5
6
7
8
9
const template = '<div>{{hero.name}}</div>';

@Component({
    selector: 'app-hero',
    template: template,
})
export class HeroComponent {
    @Input() hero: Hero;
}

Компилятор не может сослаться на константу template, поскольку она не экспортируется. Однако сборщик может включить константу template в определение метаданных, вписав ее содержимое. Эффект будет таким же, как если бы вы написали:

1
2
3
4
5
6
7
@Component({
    selector: 'app-hero',
    template: '<div>{{hero.name}}</div>',
})
export class HeroComponent {
    @Input() hero: Hero;
}

Больше нет ссылки на template и, следовательно, компилятору нечего беспокоить, когда он позже интерпретирует вывод коллектора в .metadata.json.

Вы можете пойти дальше, включив константу template в другое выражение:

1
2
3
4
5
6
7
8
9
const template = '<div>{{hero.name}}</div>';

@Component({
    selector: 'app-hero',
    template: template + '<div>{{hero.title}}</div>',
})
export class HeroComponent {
    @Input() hero: Hero;
}

Коллектор сводит это выражение к эквивалентной сёрнутой строке:

1
'<div>{{hero.name}}</div><div>{{hero.title}}</div>';

Свёртываемый синтаксис

В следующей таблице описано, какие выражения коллектор может сворачивать, а какие нет:

Синтаксис Сворачивается
Буквальный объект да
Буквальный массив да
Spread в литеральном массиве нет
Вызовы нет
Новые нет
Доступ к свойствам да, если объект является сворачиваемым
Индекс массива да, если цель и индекс сворачиваемые
Ссылка на идентичность да, если это ссылка на локаль
Шаблон без подстановок да
Шаблон с подстановками да, если подстановки сворачиваемые
Буквальная строка да
Буквальное число да
Буквальное булево да
Буквальный null да
Поддерживается префиксный оператор да, если операнд является сворачиваемым
Поддерживается бинарный оператор да, если и левый, и правый операнды являются сворачиваемыми
Условный оператор да, если условие является сворачиваемым
Родительские скобки да, если выражение является сворачиваемм

Если выражение не сворачивается, сборщик записывает его в .metadata.json в виде AST, чтобы компилятор мог его разрешить.

Фаза 2: генерация кода

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

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

Публичные или защищенные символы

Компилятор может ссылаться только на экспортируемые символы.

  • Декорарованные члены класса компонента должны быть публичными или защищенными.

    Нельзя сделать свойство @Input() приватным.

  • Свойства, связанные с данными, также должны быть публичными или защищенными

Поддерживаемые классы и функции

Сборщик может представлять вызов функции или создание объекта с помощью new до тех пор, пока синтаксис корректен. Компилятор, однако, может впоследствии отказаться генерировать вызов определенной функции или создание определенного объекта.

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

Действия компилятора Подробности
Новые экземпляры Компилятор разрешает метаданные, создающие экземпляры класса InjectionToken из @angular/core.
Поддерживаемые декораторы Компилятор поддерживает метаданные только для декораторов Angular в модуле @angular/core.
Вызовы функций Фабричные функции должны быть экспортируемыми, именованными функциями. Компилятор AOT не поддерживает лямбда-выражения ("стрелочные функции") для фабричных функций.

Вызовы функций и статических методов

Коллектор принимает любую функцию или статический метод, содержащий единственный оператор return. Однако компилятор поддерживает только макросы в виде функций или статических методов, которые возвращают выражение.

Например, рассмотрим следующую функцию:

1
2
3
export function wrapInArray<T>(value: T): T[] {
    return [value];
}

Вы можете вызвать wrapInArray в определении метаданных, потому что он возвращает значение выражения, которое соответствует ограничительному подмножеству JavaScript компилятора.

Вы можете использовать wrapInArray() следующим образом:

1
2
@NgModule({ declarations: wrapInArray(TypicalComponent) })
export class TypicalModule {}

Компилятор воспринимает это использование так, как если бы вы написали:

1
2
@NgModule({ declarations: [TypicalComponent] })
export class TypicalModule {}

Модуль Angular RouterModule экспортирует два макростатических метода, forRoot и forChild, чтобы помочь объявить корневые и дочерние маршруты. Просмотрите исходный код для этих методов, чтобы увидеть, как макросы могут упростить настройку сложных NgModules.

Переписывание метаданных

Компилятор специально обрабатывает объектные литералы, содержащие поля useClass, useValue, useFactory и data, преобразуя выражение, инициализирующее одно из этих полей, в экспортируемую переменную, которая заменяет выражение. Этот процесс переписывания этих выражений снимает все ограничения на то, что может быть в них, потому что

компилятору не нужно знать значение выражения — ему просто нужно уметь генерировать ссылку на это значение.

Вы можете написать что-то вроде:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class TypicalServer {}

@NgModule({
    providers: [
        {
            provide: SERVER,
            useFactory: () => TypicalServer,
        },
    ],
})
export class TypicalModule {}

Без переписывания это было бы некорректно, поскольку ламбды не поддерживаются, а TypicalServer не экспортируется. Чтобы разрешить это, компилятор автоматически переписывает это на что-то вроде:

1
2
3
4
5
6
7
8
class TypicalServer {}

export const θ0 = () => new TypicalServer();

@NgModule({
    providers: [{ provide: SERVER, useFactory: θ0 }],
})
export class TypicalModule {}

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

Компилятор переписывает файл .js во время эмитата. Однако он не переписывает файл .d.ts, поэтому TypeScript не распознает его как экспорт.

И он не вмешивается в экспортируемый API модуля ES.

Фаза 3: Проверка типов шаблонов

Одной из наиболее полезных функций компилятора Angular является возможность проверки типов выражений в шаблонах и выявления любых ошибок до того, как они приведут к сбоям во время выполнения. На этапе проверки типов шаблонов компилятор шаблонов Angular использует компилятор TypeScript для проверки выражений привязки в шаблонах.

Включите эту фазу явно, добавив опцию компилятора "fullTemplateTypeCheck" в "angularCompilerOptions" файла конфигурации TypeScript проекта (см. Angular Compiler Options).

Проверка шаблонов выдает сообщения об ошибках, когда в выражении привязки шаблона обнаруживается ошибка типа, подобно тому, как компилятор TypeScript сообщает об ошибках типа в коде в файле .ts.

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

1
2
3
4
5
6
7
@Component({
    selector: 'my-component',
    template: '{{person.addresss.street}}',
})
class MyComponent {
    person?: Person;
}

При этом возникает следующая ошибка:

1
my.component.ts.MyComponent.html(1,1): : Property 'addresss' does not exist on type 'Person'. Did you mean 'address'?

Имя файла, указанное в сообщении об ошибке, my.component.ts.MyComponent.html, является синтетическим файлом, созданным компилятором шаблонов, который содержит содержимое шаблона класса MyComponent. Компилятор никогда не записывает этот файл на диск.

Номера строк и столбцов относятся к строке шаблона в аннотации @Component класса, в данном случае MyComponent.

Если компонент использует templateUrl вместо template, то ошибки сообщаются в HTML-файл, на который ссылается templateUrl, а не в синтетический файл.

Местонахождение ошибки — это начало текстового узла, содержащего интерполяционное выражение с ошибкой. Если ошибка в привязке атрибута, например [value]="person.address.street", то местоположение ошибки — это местоположение атрибута, содержащего ошибку.

При проверке используется средство проверки типов TypeScript и опции, предоставляемые компилятору TypeScript, чтобы контролировать степень детализации проверки типов. Например, если указано strictTypeChecks, то ошибка

1
my.component.ts.MyComponent.html(1,1): : Object is possibly 'undefined'

сообщается, как и вышеприведенное сообщение об ошибке.

Сужение типов

Выражение, используемое в директиве ngIf, используется для сужения объединений типов в компиляторе шаблонов Angular, точно так же, как это делает выражение if в TypeScript.

Например, чтобы избежать ошибки Object is possibly 'undefined' в шаблоне выше, измените его так, чтобы он выдавал интерполяцию, только если значение person инициализировано, как показано ниже:

1
2
3
4
5
6
7
@Component({
    selector: 'my-component',
    template: ' {{person.address.street}} ',
})
class MyComponent {
    person?: Person;
}

Использование *ngIf позволяет компилятору TypeScript сделать вывод, что person, используемый в выражении привязки, никогда не будет undefined.

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

Оператор утверждения типа non-null

Используйте оператор утверждения типа non-null для подавления ошибки Object is possibly 'undefined', когда неудобно использовать *ngIf или когда некоторые ограничения в компоненте гарантируют, что выражение всегда будет non-null при интерполяции выражения связывания.

В следующем примере свойства person и address всегда установлены вместе, что подразумевает, что address всегда будет non-null, если person будет non-null. Не существует удобного способа описать это ограничение для TypeScript и компилятора шаблонов, но в примере ошибка подавлена за счет использования address!.street.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Component({
    selector: 'my-component',
    template:
        '<span *ngIf="person"> {{person.name}} lives on {{address!.street}} </span>',
})
class MyComponent {
    person?: Person;
    address?: Address;

    setData(person: Person, address: Address) {
        this.person = person;
        this.address = address;
    }
}

Оператор утверждения non-null следует использовать осторожно, так как рефакторинг компонента может нарушить это ограничение.

В данном примере рекомендуется включить проверку address в *ngIf, как показано ниже:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Component({
    selector: 'my-component',
    template:
        '<span *ngIf="person && address"> {{person.name}} lives on {{address.street}} </span>',
})
class MyComponent {
    person?: Person;
    address?: Address;

    setData(person: Person, address: Address) {
        this.person = person;
        this.address = address;
    }
}

Ссылки

Комментарии