49. PWA с Angular
📱 Progressive Web App с Angular
Заголовок раздела «📱 Progressive Web App с Angular»PWA (Progressive Web App) — это веб-приложение, которое работает как нативное: устанавливается на устройство, работает офлайн, получает push-уведомления и загружается мгновенно. Angular PWA — это встроенная поддержка через Service Worker 🚀
Установка
Заголовок раздела «Установка»ng add @angular/pwaКоманда автоматически:
- Устанавливает
@angular/service-worker - Создаёт
ngsw-config.json - Добавляет
manifest.webmanifest - Регистрирует Service Worker в
app.config.ts - Добавляет метатеги в
index.html
ngsw-config.json — конфигурация Service Worker
Заголовок раздела «ngsw-config.json — конфигурация Service Worker»{ "$schema": "./node_modules/@angular/service-worker/config/schema.json", "index": "/index.html", "assetGroups": [ { "name": "app", "installMode": "prefetch", "updateMode": "prefetch", "resources": { "files": [ "/favicon.ico", "/index.html", "/manifest.webmanifest", "/*.css", "/*.js" ] } }, { "name": "assets", "installMode": "lazy", "updateMode": "prefetch", "resources": { "files": [ "/assets/**", "/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)" ] } } ], "dataGroups": [ { "name": "api-freshness", "urls": ["/api/news", "/api/products"], "cacheConfig": { "strategy": "freshness", "maxSize": 100, "maxAge": "3d", "timeout": "10s" } }, { "name": "api-performance", "urls": ["/api/categories", "/api/config"], "cacheConfig": { "strategy": "performance", "maxSize": 10, "maxAge": "1d" } } ], "navigationUrls": [ "/**", "!/**/*.*", "!/**/*__*", "!/**/*__*/**" ]}Стратегии кеширования
Заголовок раздела «Стратегии кеширования»prefetch — заранее
Заголовок раздела «prefetch — заранее»Все ресурсы загружаются сразу при установке Service Worker. Идеально для критического JS/CSS:
{ "installMode": "prefetch" }lazy — по требованию
Заголовок раздела «lazy — по требованию»Ресурсы кешируются при первом обращении. Для изображений и второстепенных файлов:
{ "installMode": "lazy" }performance — сначала кеш
Заголовок раздела «performance — сначала кеш»Запросы к API отдаются из кеша немедленно, фоново обновляются:
{ "strategy": "performance", "maxSize": 50, "maxAge": "1h"}freshness — сначала сеть
Заголовок раздела «freshness — сначала сеть»Сначала идём в сеть, если нет ответа за timeout — берём из кеша:
{ "strategy": "freshness", "maxSize": 100, "maxAge": "3d", "timeout": "5s"}manifest.webmanifest
Заголовок раздела «manifest.webmanifest»{ "name": "Мой Angular App", "short_name": "MyApp", "theme_color": "#dd0031", "background_color": "#0f172a", "display": "standalone", "scope": "/", "start_url": "/", "orientation": "portrait", "icons": [ { "src": "assets/icons/icon-72x72.png", "sizes": "72x72", "type": "image/png", "purpose": "maskable any" }, { "src": "assets/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable any" }, { "src": "assets/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable any" } ], "shortcuts": [ { "name": "Добавить товар", "url": "/products/new", "icons": [{ "src": "assets/icons/add.png", "sizes": "96x96" }] } ]}Service Worker — обновления приложения
Заголовок раздела «Service Worker — обновления приложения»import { Injectable } from '@angular/core';import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';import { filter } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })export class SwUpdateService { constructor(private swUpdate: SwUpdate) {}
initialize() { if (!this.swUpdate.isEnabled) { console.log('Service Worker не активен (dev mode)'); return; }
// Подписываемся на события обновления this.swUpdate.versionUpdates .pipe(filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY')) .subscribe(evt => { console.log(`Текущая версия: ${evt.currentVersion.hash}`); console.log(`Новая версия: ${evt.latestVersion.hash}`);
// Спрашиваем пользователя const shouldUpdate = confirm( '🚀 Доступна новая версия приложения. Обновить сейчас?' );
if (shouldUpdate) { this.swUpdate.activateUpdate().then(() => { window.location.reload(); }); } });
// Периодическая проверка обновлений (каждые 6 часов) setInterval(() => { this.swUpdate.checkForUpdate(); }, 6 * 60 * 60 * 1000); }}
// app.component.ts@Component({...})export class AppComponent { constructor(swUpdateService: SwUpdateService) { swUpdateService.initialize(); }}Push Notifications
Заголовок раздела «Push Notifications»import { Injectable } from '@angular/core';import { SwPush } from '@angular/service-worker';
@Injectable({ providedIn: 'root' })export class PushNotificationService { readonly VAPID_PUBLIC_KEY = 'YOUR_VAPID_PUBLIC_KEY';
constructor(private swPush: SwPush) {}
async subscribeToNotifications() { if (!this.swPush.isEnabled) { throw new Error('Service Worker Push не поддерживается'); }
try { const subscription = await this.swPush.requestSubscription({ serverPublicKey: this.VAPID_PUBLIC_KEY, });
// Отправляем подписку на сервер await this.sendSubscriptionToServer(subscription);
console.log('✅ Push-уведомления подключены'); return subscription; } catch (error) { console.error('❌ Ошибка подписки:', error); throw error; } }
listenToNotifications() { return this.swPush.messages; }
listenToClicks() { return this.swPush.notificationClicks; }
private async sendSubscriptionToServer(sub: PushSubscription) { await fetch('/api/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(sub), }); }}Add to Home Screen (A2HS)
Заголовок раздела «Add to Home Screen (A2HS)»import { Injectable } from '@angular/core';import { BehaviorSubject } from 'rxjs';
@Injectable({ providedIn: 'root' })export class InstallPromptService { private deferredPrompt: any = null; canInstall$ = new BehaviorSubject<boolean>(false);
constructor() { window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); // Не показываем браузерный промпт this.deferredPrompt = e; this.canInstall$.next(true); });
window.addEventListener('appinstalled', () => { this.deferredPrompt = null; this.canInstall$.next(false); console.log('✅ Приложение установлено!'); }); }
async install() { if (!this.deferredPrompt) return;
this.deferredPrompt.prompt(); const { outcome } = await this.deferredPrompt.userChoice;
if (outcome === 'accepted') { console.log('Пользователь установил приложение'); } else { console.log('Пользователь отклонил установку'); }
this.deferredPrompt = null; this.canInstall$.next(false); }}