Вместо проверки изменений после каждой асинхронной задачи Zoneless-приложения на Angular используют Signals для обнаружения соответствующих изменений состояния. Вот как настроить это с нуля и на что обратить внимание при миграции на Zoneless.
Если вы работали с Angular, то наверняка видели, как компоненты реагируют и обновляют пользовательский интерфейс после клика, таймера или HTTP-запроса без вызова detectChanges(). Angular обеспечивает это удобство уже много лет, поэтому обычно вам не нужно задумываться о том, как это работает.
ZoneJS и NgZone в Angular делают это возможным, и хотя мы можем легко создавать реактивные приложения, NgZone заставляет Angular выполнять проверки чаще, чем необходимо. По мере роста вашего приложения вы можете задаться простым вопросом: может ли Angular обновлять только те представления, которые действительно изменились?
Современный Angular предоставляет такую возможность благодаря бесзонному (Zoneless) обнаружению изменений. Сигналы и другие API Angular могут сообщать Angular, когда представлению требуется обновление. Это позволяет создавать приложения без зависимости от NgZone, которая следит за каждой асинхронной задачей.
Сегодня давайте вместе создадим небольшую панель управления продуктами с помощью Angular 22 и Progress Kendo UI for Angular. Сначала мы узнаем, как работает старая модель. Затем мы используем Signals и httpResource для создания Zoneless-панели управления. Наконец, мы рассмотрим распространенные проблемы, которые могут возникнуть при миграции на Zoneless.
Давайте начнем с того, благодаря чему работала старая модель: NgZone и ZoneJS.
Что такое NgZone и ZoneJS?
Когда мы говорим о NgZone, это сервис, который связывает Angular с библиотекой ZoneJS. ZoneJS патчит множество API браузера, включая таймеры, события и промисы. Когда одна из таких операций завершается, Angular может использовать это уведомление для планирования проверки изменений.
Думайте о ZoneJS как о центральном будильнике. Он не знает, какой именно компонент изменился. В результате после срабатывания будильника Angular может проверить большую часть дерева компонентов. Это удобно, поскольку разработчикам не всегда нужно сообщать Angular об изменении состояния.
Например, этот код изменяет обычное свойство по таймеру:
С помощью ZoneJS таймер может инициировать проверку изменений в Angular, и обновленное свойство значка может появиться в шаблоне, даже если компонент явно не уведомил Angular. У этого удобства есть своя цена, особенно в крупных приложениях:
- Angular может выполнять проверки, которые не приводят к видимым изменениям.
- ZoneJS внедряет код выполнения (runtime) в приложение.
- Трассировки стека могут содержать детали планировщика фреймворка, что затрудняет отладку.
Плата за производительность зависит от конкретного приложения. Zoneless не гарантирует, что любое приложение станет быстрее. Этот подход делает обнаружение изменений более явным и может сократить ненужную работу, когда приложение четко демонстрирует свое состояние.
Мы увидели, почему Angular использовал NgZone и что добавлял ZoneJS. Теперь давайте посмотрим, как Angular узнает, что Zoneless-представлению требуется обновление.
Как работает Zoneless-обнаружение изменений?
Zoneless Angular не нуждается в NgZone для отслеживания задач браузера. Вместо этого Angular планирует проверку изменений, когда один из его API отправляет уведомление. Примеры включают сигнал, прочитанный шаблоном, привязанный обработчик событий, входные данные компонента и . Сигналы — наиболее распространенный вариант. Они хранят состояние и сообщают Angular, какие представления его используют.
Разница проста. ZoneJS может инициировать широкую проверку после таймера, события или промиса, в то время как Zoneless Angular реагирует на уведомление Angular и обновляет представления, связанные с этим состоянием. Не каждое значение должно становиться signal(). Однако значения, используемые шаблоном, должны изменяться посредством механизма, который Angular может наблюдать.
Именно эту идею мы будем использовать в панели управления: пользовательское событие изменяет сигнал, ресурс загружает новые данные, и шаблон обновляется.
Создание Zoneless-панели управления продуктами
Давайте создадим небольшую панель управления продуктами с фильтром по категориям и сеткой Kendo UI for Angular Grid. Небольшой размер примера облегчает отслеживание пути обнаружения изменений.
Шаг 1: Создание проекта Angular
Создайте проект с помощью Angular CLI. Выполните эту команду в терминале:
Первая команда создает проект Angular 22. Она включает автономные компоненты (standalone) и SCSS, а также отключает рендеринг на стороне сервера. Вторая команда осуществляет переход в папку проекта. Теперь у вас должны быть такие файлы, как src/app/app.ts, src/app/app.html и src/app/app.scss.
Запустите новое приложение, чтобы убедиться в работоспособности проекта, прежде чем продолжать:
Откройте http://localhost:4200 в вашем браузере. Вы должны увидеть приложение Angular по умолчанию. Остановите сервер с помощью комбинации Ctrl+C перед продолжением.
Новые проекты Angular используют настройки по умолчанию для данной версии Angular. Прежде чем применять следующий шаг в существующем проекте, проверьте его версию Angular и сгенерированный файл app.config.ts.
Затем проверьте конфигурацию обнаружения изменений, сгенерированную Angular.
Zoneless в Angular 22
В этом примере вы используете Angular 22, в котором по умолчанию применяется бесзонное обнаружение изменений. Сгенерированный файл app.config.ts не требует вызова provideZonelessChangeDetection(). Проект также не включает zone.js в качестве зависимости.
Мы добавляем provideHttpClient(), потому что эта панель управления будет использовать httpResource для запросов к Fake Store API. provideHttpClient() — это HTTP-провайдер, а не Zoneless-провайдер, но если ваше приложение не выполняет HTTP-запросы, вы можете опустить его.
Как насчет старых версий Angular?
Этот раздел предназначен только для старых версий, таких как Angular 20, поскольку не каждый проект использует последнюю версию Angular. Angular 20 и более поздние версии поддерживают переход на Zoneless с помощью provideZonelessChangeDetection(). Новые проекты, созданные с использованием Angular 21 и более поздних версий, используют Zoneless по умолчанию, но существующие приложения все равно требуют анализа перед миграцией.
Для приложения на Angular 20, которое все еще использует ZoneJS, добавьте провайдер в app.config.ts, затем проверьте конфигурацию проекта. Если вы используете более старую версию Angular, обратитесь к руководству по миграции для конкретной версии, так как провайдер и настройка могут отличаться:
После проверки приложения, тестов и сторонних библиотек удалите zone.js и zone.js/testing из angular.json или конфигурации полифиллов. Только после этого удалите пакет:
Теперь вы знаете, как настроить Zoneless для старой версии; а теперь давайте продолжим работу с нашим проектом на Angular 22 и добавим Kendo UI!
Шаг 2: Установка компонентов Kendo UI
Теперь, когда проект Angular готов, добавьте компоненты пользовательского интерфейса, которые будут использовать пользователи. Kendo UI предоставит селектор категорий и сетку продуктов, поэтому установите пакеты из папки проекта:
Ознакомьтесь с документацией по совместимости Kendo UI for Angular при выборе версий пакетов. Компоненты Kendo UI требуют наличия сред выполнения анимации и локализации Angular, поэтому установите @angular/animations и @angular/localize в рамках настройки.
Поскольку Kendo UI использует локализацию Angular внутренне, добавьте среду выполнения локализации в качестве полифилла сборки в angular.json:
Затем загрузите тему Kendo UI в src/styles.scss:
Kendo UI предоставляет готовые к использованию компоненты, такие как Angular DropDownList и Grid. Прежде чем использовать все их возможности, ознакомьтесь с бюджетом бандла Angular. Приложение может успешно компилироваться даже тогда, когда его бандл превышает бюджет по умолчанию, поэтому обновите angular.json после измерения результатов:
Это изменение предоставляет Kendo UI Grid и DropDownList достаточно места в рамках бюджета пакета. Не увеличивайте бюджет в production без предварительного измерения пакета. По мере роста приложения проверяйте импорты, темы и ленивую загрузку (lazy loading). В этом проекте созданный пакет весит около 4.4 МБ, поэтому стандартные лимиты предупреждений в 500 КБ и ошибок в 1 МБ слишком малы для этих компонентов.
Проект готов к созданию пользовательского интерфейса панели управления. Затем свяжите запрос продукта с сигналом, чтобы изменение категории могло загрузить новые данные.
Шаг 3: Получение продуктов с помощью httpResource
Зависимости пользовательского интерфейса на месте, но сетке (grid) все еще нужны данные. Используйте httpResource, чтобы подключить HTTP-запрос к реактивным API Angular. Если сигнал, считываемый функцией запроса, изменяется, ресурс инициирует новый запрос и предоставляет состояния загрузки, ошибки и значения.
Создайте файл src/app/product.service.ts. Выполните эту команду из папки проекта для генерации файла:
Замените сгенерированный сервис следующим кодом:
Сервис не внедряет HttpClient, так как httpResource обрабатывает запрос. Сигнал считывается внутри функции ресурса, поэтому изменение выбранной категории изменяет URL ресурса. В этом примере используется @Service(), поскольку это сервис корневого уровня, который применяет inject().
Теперь сервис отвечает за реактивный запрос продуктов. Это дает компоненту один четкий источник для состояния его сервера.
Шаг 4: Подключение компонентов Kendo UI с помощью сигналов
Сервис готов. Теперь мы подключаем Kendo UI DropDownList (фильтр категорий) и Grid (список продуктов) к сигналам из ProductService. Шаблон считывает сигналы напрямую — [value]="selectedCategory()" и [data]="products()" — поэтому при изменении сигнала Kendo UI обновляется без NgZone.
Kendo UI Grid предоставляет виртуальную прокрутку, сортировку, фильтрацию и изменение размера столбцов «из коробки», и все это реагирует на изменения сигналов. DropDownList обрабатывает навигацию с клавиатуры, фильтрацию и доступность без написания дополнительного кода.
Думайте об этом как о прямом проводе: сигнал — это выключатель, а компонент Kendo UI — это лампада. Когда вы щелкаете выключателем, свет загорается. Централизованная сигнализация (ZoneJS) не требуется.
Текущий Angular CLI создает файлы src/app/app.ts, src/app/app.html и src/app/app.scss. Обновите эти файлы по отдельности. Это соответствует структуре, создаваемой CLI, и показывает, где находится каждая часть компонента.
Теперь замените содержимое файла src/app/app.html шаблоном панели управления:
Давайте посмотрим на HTML-код. DropDownList считывает selectedCategory() с помощью [value]. Grid считывает products() с помощью [data]. А событие (valueChange) записывает значение обратно в сигнал. Вот и весь цикл. Прочитайте сигнал, обновите интерфейс, запишите сигнал, готово!
Функция effect() синхронизирует категорию с заголовком браузера. Это побочный эффект вне шаблона, но он по-прежнему управляется сигналом.
Затем замените содержимое файла src/app/app.scss стилями панели управления:
Блок @if реагирует на состояние ресурса, в то время как @for отображает сводку категорий. Главный момент заключается в том, что шаблон считывает сигналы. Когда selectedCategory изменяется, ресурс и затрагиваемое представление получают уведомления от Angular без использования NgZone.
Запустите приложение, выполнив следующую команду:
Откройте http://localhost:4200. Сначала вы должны увидеть сообщение о загрузке, за которым следует сетка продуктов. Выберите все элементы в выпадающем списке и убедитесь, что сообщение о загрузке появляется снова перед отображением отфильтрованных продуктов.
Готово! Наш поток работает от сигнала к ресурсу, а затем к шаблону. Но у меня возникает следующий вопрос: как протестировать этот поток, не оставляя HTTP-запрос незавершенным? 🤔
Тестирование компонентов без зон (Zoneless)
В тесте без зон необходимо дождаться запланированной работы Angular и управлять асинхронными зависимостями. Незавершенный HTTP-запрос не является стабильной работой, поэтому обработайте его с помощью HttpTestingController. Это позволяет тестировать тот же путь от сигнала к представлению без зависимости от реального API.
Вот целенаправленный тест Vitest для первоначального запроса продуктов в src/app/app.spec.ts. Тест использует provideHttpClientTesting() для управления запросом.
Провайдер без зон включен в TestBed, чтобы сделать настройку явной, даже если Angular 22 включает его по умолчанию в новом приложении:
Скопируйте следующий фрагмент и замените им файл app.spec.ts:
Что делает этот код? Что ж, fixture.detectChanges() запускает первоначальный рендеринг и запрос. flush() завершает контролируемый запрос. А whenStable() ждет, пока Angular завершит работу по рендерингу. Другими словами, вызывайте whenStable() после разрешения HTTP-запроса, а не оставляйте его висеть.
Для запуска нашего теста используйте npm run test:
Ура!! Наш тест теперь охватывает первоначальный запрос и изменение категории. Итак, у нас есть приложение с поддержкой работы без зон и тестированием, но это был проект с нуля. Позвольте мне поделиться с вами распространенными проблемами миграции, с которыми я сталкивался чаще всего.
Распространенные проблемы миграции
Прежде чем мы закончим, позвольте мне рассказать о некоторых наиболее частых проблемах, с которыми я столкнулся при миграции реальных приложений Angular на работу без зон. Это неполный список всех возможных проблем. Это те случаи, с которыми я сталкивался в реальных проектах, и они станут хорошей отправной точкой при проверке вашего собственного приложения.
Эти примеры охватывают различные границы миграции. Практическое исправление зависит от того, куда состояние поступает в приложение.
Таймер изменяет состояние, но интерфейс не обновляется
Обычное свойство, измененное с помощью setInterval(), не уведомляет Angular в приложении без зон. Используйте сигнал для состояния, считываемого шаблоном, и очищайте таймер при уничтожении компонента:
Для потоков RxJS используйте takeUntilDestroyed(), когда ручная подписка должна быть привязана к жизненному циклу компонента.
Пример с таймером — это простейший случай: состояние шаблона требует уведомления от Angular.
Ручная подписка RxJS оставляет устаревшее представление
Эмиссия Observable не делает обычное свойство компонента реактивным автоматически. Предпочитайте toSignal(), когда значение принадлежит шаблону:
toSignal() подписывается на Observable, делает последнее значение доступным в виде сигнала и связывает это значение с жизненным циклом компонента. Если вам нужна ручная подписка для выполнения действия в коде, используйте takeUntilDestroyed() и обновляйте сигнал внутри обратного вызова.
Это поддерживает связь потока с шаблоном без вызова ручного обнаружения изменений. Это полезный паттерн, когда существующее приложение уже использует RxJS.
Сторонний виджет не уведомляет Angular
Карты, графики, редакторы и старые обертки могут выдавать обратные вызовы за пределами реактивной модели Angular. Сначала проверьте, есть ли у пакета актуальная интеграция с Angular. Если ее нет, изолируйте интеграцию и обновите сигнал на границе:
Поскольку шаблон считывает selectedLocation(), обновление сигнала уже уведомляет Angular в приложении без зон. Если внешний обратный вызов обновляет обычное состояние компонента вместо этого, используйте markForCheck() на этой границе. Я использую detectChanges() только для узкой границы интеграции, так как он запускает обнаружение изменений немедленно и не должен становиться решением по умолчанию во всем приложении.
Это те границы, с которыми я сталкивался чаще всего. Это не единственные случаи, но они служат полезной отправной точкой для миграции на работу без зон (Zoneless).
Практический путь миграции
При переносе существующего приложения с ZoneJS на работу без зон (Zoneless) сначала проверьте версию Angular и изучите руководство по миграции для этой версии. Это покажет, нужно ли приложению явным образом вызывать provideZonelessChangeDetection(). Прежде чем что-либо удалять, выполните поиск по проекту на наличие NgZone, zone.js, zone.js/testing, а также кода, зависящего от onStable или onMicrotaskEmpty.
Затем проследите за состоянием, которое каждая функция передает в свой шаблон. Обратите особое внимание на значения, изменяемые таймерами, внешними обратными вызовами (callback) и ручными подписками. Если эту работу выполняет обычное свойство, перенесите его в signal(), computed(), toSignal() или другой механизм уведомлений Angular. Также проверьте сторонние библиотеки, такие как карты, диаграммы, диалоговые окна, выборщики дат и редакторы, поскольку их обратные вызовы могут потребовать явной границы интеграции.
Выполняйте миграцию небольшими шагами, чтобы тестировать изменения по ходу дела. Как только приложение и его зависимости перестанут зависеть от ZoneJS, удалите ее из конфигурации проекта и деинсталлируйте пакет. Наконец, измерьте результат с помощью инструментов производительности браузера. Не стоит предполагать, что отказ от зон сам по себе улучшит каждый показатель.
Итоги
Основная идея заключается в следующем: приложения без зон не зависят от NgZone и ZoneJS для планирования проверки изменений после каждой асинхронной задачи. Вместо этого приложение предоставляет состояние шаблона через Сигналы (Signals) и другие API Angular, которые могут сообщать Angular об изменении соответствующего состояния.
По моему опыту, инкрементальная миграция является наиболее практичным подходом. Перенесите одну функцию, протестируйте ее, проверьте ее интеграции и переходите к следующей функции. Это позволяет легче увидеть реальные зависимости приложения от проверки изменений, не превращая всю миграцию в одну большую сессию отладки.
Теперь ваша очередь опробовать эту модель на небольшом фрагменте клиентского состояния.
Открытая задача
Добавьте поле поиска на панель мониторинга. Сохраняйте поисковый запрос в сигнале (signal()) и используйте computed() для отображения подходящих продуктов. Оставьте запрос категории без изменений, чтобы ресурс управлял состоянием сервера, в то время как Сигналы управляют локальным состоянием пользовательского интерфейса.
Приятного кодинга!
Не стесняйтесь читать дополнительные материалы для получения полного представления. :)
- Руководство по работе без зон в Angular
- Руководство по сигналам Angular
- Руководство по сервисам Angular
- Документация по Kendo UI for Angular Grid
- Исходный код
И помните: Kendo UI for Angular, одна из лучших библиотек для Angular, доступна с бесплатной 30-дневной пробной версией. Так что вперед:
Попробовать сейчас











