Как реализовать сервис для проверки user-agent в Angular v21 SSR, чтобы guard canMatch мог выбирать между мобильными и десктопными маршрутами исключительно на сервере?
В Angular v21 SSR для реализации сервиса проверки user-agent, который будет работать исключительно на сервере и использоваться в guard canMatch, вам необходимо создать сервис, который инжектирует токен REQUEST из @angular/platform-server для доступа к HTTP-запросу и его заголовкам. Сервис будет анализировать user-agent и определять тип устройства, а guard canMatch будет использовать этот сервис для выбора соответствующего маршрута.
Содержание
- Введение в Angular v21 SSR и проверку user-agent
- Создание сервиса для определения устройства в Angular SSR
- Настройка guard canMatch для маршрутизации на основе устройства
- Интеграция с токеном REQUEST для доступа к user-agent на сервере
- Оптимизация производительности и обработка edge cases
- Полный пример реализации с кодом
Введение в Angular v21 SSR и проверку user-agent
Angular v21 продолжает развивать возможности серверного рендеринга (SSR), предоставляя разработчикам мощные инструменты для создания веб-приложений с улучшенной производительностью и SEO. Когда речь заходит о маршрутизации на основе типа устройства (мобильные vs десктопные), важно выполнять эту логику исключительно на сервере, чтобы избежать флеша контента и обеспечить мгновенную загрузку правильной версии интерфейса.
Компоненты angular в SSR контексте имеют доступ к серверным сервисам, что позволяет им получать информацию о клиенте до рендеринга. Angular 21 улучшила интеграцию с SSR, сделав процесс работы с HTTP-запросами более интуитивным и безопасным.
Важным аспектом является проверка user-agent, которая определяет, с какого устройства зашел пользователь. Эта информация критична для выбора правильного маршрута, так как мобильные и десктопные версии приложения могут иметь разную структуру и функциональность.
Создание сервиса для определения устройства в Angular SSR
Для начала создадим сервис, который будет анализировать user-agent и определять тип устройства. Этот сервис будет работать исключительно на сервере, используя токен REQUEST.
// src/app/core/services/device-detector.service.ts
import { Injectable, Inject } from '@angular/core';
import { REQUEST } from '@angular/platform-server';
@Injectable({
providedIn: 'root'
})
export class DeviceDetectorService {
constructor(@Inject(REQUEST) private request: any) {}
isMobile(): boolean {
if (!this.request || !this.request.headers) {
return false;
}
const userAgent = this.request.headers['user-agent'] || '';
// Мобильные user-agent паттерны
const mobileRegex = /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
return mobileRegex.test(userAgent);
}
isDesktop(): boolean {
return !this.isMobile();
}
getDeviceType(): 'mobile' | 'desktop' {
return this.isMobile() ? 'mobile' : 'desktop';
}
}
Этот сервис использует инжекцию токена REQUEST для доступа к HTTP-запросу на сервере. Важно отметить, что в клиентской части этот сервис будет работать, но всегда будет возвращать false для isMobile(), так как токен REQUEST недоступен на клиенте.
При работе с angular component, который требует информации о типе устройства, вы можете использовать этот сервис в ngOnInit или в шаблоне, но для маршрутизации лучше использовать guards, которые будут работать на сервере.
Настройка guard canMatch для маршрутизации на основе устройства
Guard canMatch в Angular позволяет решить, может ли маршрут быть активирован, до загрузки его компонентов. Это идеально подходит для нашей задачи, так как guard будет работать на сервере во время SSR.
// src/app/guards/device.guard.ts
import { Injectable } from '@angular/core';
import { CanMatch, Router, Route } from '@angular/router';
import { DeviceDetectorService } from '../core/services/device-detector.service';
@Injectable({
providedIn: 'root'
})
export class DeviceGuard implements CanMatch {
constructor(
private deviceDetector: DeviceDetectorService,
private router: Router
) {}
canMatch(route: Route): boolean {
const isMobile = this.deviceDetector.isMobile();
const expectedDeviceType = route.data?.['deviceType'];
if (expectedDeviceType && isMobile && expectedDeviceType === 'desktop') {
// Перенаправляем на мобильную версию
this.router.navigate(['/mobile']);
return false;
}
if (expectedDeviceType && !isMobile && expectedDeviceType === 'mobile') {
// Перенаправляем на десктопную версию
this.router.navigate(['/desktop']);
return false;
}
return true;
}
}
Теперь настроим маршруты, используя этот guard:
// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DeviceGuard } from './guards/device.guard';
const routes: Routes = [
{
path: 'mobile',
component: MobileLayoutComponent,
data: { deviceType: 'mobile' },
canMatch: [DeviceGuard]
},
{
path: 'desktop',
component: DesktopLayoutComponent,
data: { deviceType: 'desktop' },
canMatch: [DeviceGuard]
},
{
path: '',
redirectTo: '/mobile',
pathMatch: 'full'
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Такая конфигурация гарантирует, что маршруты будут выбираться исключительно на сервере на основе user-agent, обеспечивая мгновенную загрузку правильной версии интерфейса.
Интеграция с токеном REQUEST для доступа к user-agent на сервере
Ключевым элементом нашей реализации является токен REQUEST из пакета @angular/platform-server. Этот токен предоставляет доступ к объекту HTTP-запроса на сервере, что позволяет нам извлечь user-agent из заголовков.
Для корректной работы с токеном REQUEST убедитесь, что вы правильно настроили серверную часть вашего angular приложения. В main.server.ts нужно убедиться, что токен доступен для инжекции:
// src/main.server.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideServerRendering } from '@angular/platform-server';
import { REQUEST } from '@angular/platform-server';
bootstrapApplication(AppComponent, {
providers: [
provideServerRendering(),
{ provide: REQUEST, useValue: globalThis['__request'] }
]
}).catch(err => console.error(err));
На серверной стороне (например, в server.ts Express.js) нужно передавать объект запроса в глобальную переменную:
// server.ts
import { ngExpressEngine } from '@nguniversal/express-engine';
import { REQUEST } from '@angular/platform-server';
// ...
app.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
providers: [
{ provide: REQUEST, useValue: req }
]
}));
app.get('*', (req, res) => {
globalThis['__request'] = req;
res.render('index.html', { req });
});
Такая интеграция гарантирует, что ваш сервис DeviceDetectorService будет иметь доступ к реальному user-agent на сервере во время SSR.
Оптимизация производительности и обработка edge cases
При работе с user-agent в angular SSR важно учитывать несколько моментов для оптимизации и обработки edge cases:
- Кэширование результата: Поскольку user-агент не меняется в течение сессии, можно кэшировать результат определения типа устройства:
@Injectable({
providedIn: 'root'
})
export class DeviceDetectorService {
private deviceTypeCache: 'mobile' | 'desktop' | null = null;
constructor(@Inject(REQUEST) private request: any) {}
getDeviceType(): 'mobile' | 'desktop' {
if (this.deviceTypeCache) {
return this.deviceTypeCache;
}
const isMobile = this.isMobile();
this.deviceTypeCache = isMobile ? 'mobile' : 'desktop';
return this.deviceTypeCache;
}
}
- Обработка планшетов: Некоторые планшеты могут иметь user-agent, похожий на десктоп, но сенсорный экран:
isMobile(): boolean {
if (!this.request || !this.request.headers) {
return false;
}
const userAgent = this.request.headers['user-agent'] || '';
// Паттерны для мобильных устройств и планшетов
const mobileRegex = /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
const tabletRegex = /iPad|Android(?!.*Mobile)/i;
return mobileRegex.test(userAgent) || tabletRegex.test(userAgent);
}
- Проверка наличия токена REQUEST: Для предотвращения ошибок на клиенте:
isMobile(): boolean {
// На клиенте всегда возвращаем false
if (typeof window !== 'undefined') {
return false;
}
if (!this.request || !this.request.headers) {
return false;
}
// ... остальная логика
}
- Предоставление альтернативного маршрута: Для случаев, когда user-agent не определился:
canMatch(route: Route): boolean {
const isMobile = this.deviceDetector.isMobile();
const expectedDeviceType = route.data?.['deviceType'];
if (expectedDeviceType) {
if (isMobile && expectedDeviceType === 'desktop') {
this.router.navigate(['/mobile']);
return false;
}
if (!isMobile && expectedDeviceType === 'mobile') {
this.router.navigate(['/desktop']);
return false;
}
}
// Если user-agent не определился, используем стандартный маршрут
if (!isMobile && !this.deviceDetector.isDesktop()) {
this.router.navigate(['/desktop']);
return false;
}
return true;
}
Эти оптимизации помогут сделать ваше angular приложение более надежным и производительным при работе с SSR.
Полный пример реализации с кодом
Давайте соберем все вместе для полного примера реализации сервиса проверки user-agent в Angular v21 SSR:
- Создание сервиса DeviceDetectorService:
// src/app/core/services/device-detector.service.ts
import { Injectable, Inject } from '@angular/core';
import { REQUEST } from '@angular/platform-server';
@Injectable({
providedIn: 'root'
})
export class DeviceDetectorService {
private deviceTypeCache: 'mobile' | 'desktop' | null = null;
constructor(@Inject(REQUEST) private request: any) {}
isMobile(): boolean {
// На клиенте всегда возвращаем false
if (typeof window !== 'undefined') {
return false;
}
if (!this.request || !this.request.headers) {
return false;
}
const userAgent = this.request.headers['user-agent'] || '';
const mobileRegex = /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
const tabletRegex = /iPad|Android(?!.*Mobile)/i;
return mobileRegex.test(userAgent) || tabletRegex.test(userAgent);
}
isDesktop(): boolean {
if (typeof window !== 'undefined') {
return true; // На клиенте считаем десктопом по умолчанию
}
if (!this.request || !this.request.headers) {
return true;
}
return !this.isMobile();
}
getDeviceType(): 'mobile' | 'desktop' {
if (this.deviceTypeCache) {
return this.deviceTypeCache;
}
const isMobile = this.isMobile();
this.deviceTypeCache = isMobile ? 'mobile' : 'desktop';
return this.deviceTypeCache;
}
}
- Создание guard DeviceGuard:
// src/app/guards/device.guard.ts
import { Injectable } from '@angular/core';
import { CanMatch, Router, Route } from '@angular/router';
import { DeviceDetectorService } from '../core/services/device-detector.service';
@Injectable({
providedIn: 'root'
})
export class DeviceGuard implements CanMatch {
constructor(
private deviceDetector: DeviceDetectorService,
private router: Router
) {}
canMatch(route: Route): boolean {
const isMobile = this.deviceDetector.isMobile();
const expectedDeviceType = route.data?.['deviceType'];
if (expectedDeviceType) {
if (isMobile && expectedDeviceType === 'desktop') {
this.router.navigate(['/mobile']);
return false;
}
if (!isMobile && expectedDeviceType === 'mobile') {
this.router.navigate(['/desktop']);
return false;
}
}
// Если user-agent не определился, используем десктопную версию
if (!isMobile && !this.deviceDetector.isDesktop()) {
this.router.navigate(['/desktop']);
return false;
}
return true;
}
}
- Конфигурация маршрутов:
// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DeviceGuard } from './guards/device.guard';
const routes: Routes = [
{
path: 'mobile',
loadChildren: () => import('./mobile/mobile.module').then(m => m.MobileModule),
data: { deviceType: 'mobile' },
canMatch: [DeviceGuard]
},
{
path: 'desktop',
loadChildren: () => import('./desktop/desktop.module').then(m => m.DesktopModule),
data: { deviceType: 'desktop' },
canMatch: [DeviceGuard]
},
{
path: '',
redirectTo: '/mobile',
pathMatch: 'full'
},
{
path: '**',
redirectTo: '/mobile'
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
- Компоненты для мобильной и десктопной версий:
// src/app/mobile/mobile.component.ts
import { Component } from '@angular/core';
import DeviceDetectorService from '../../core/services/device-detector.service';
@Component({
selector: 'app-mobile',
template: `
<h1>Мобильная версия</h1>
<p>Вы используете мобильное устройство.</p>
`
})
export class MobileComponent {
constructor(private deviceDetector: DeviceDetectorService) {
console.log('Тип устройства:', this.deviceDetector.getDeviceType());
}
}
// src/app/desktop/desktop.component.ts
import { Component } from '@angular/core';
import DeviceDetectorService from '../../core/services/device-detector.service';
@Component({
selector: 'app-desktop',
template: `
<h1>Десктопная версия</h1>
<p>Вы используете десктопное устройство.</p>
`
})
export class DesktopComponent {
constructor(private deviceDetector: DeviceDetectorService) {
console.log('Тип устройства:', this.deviceDetector.getDeviceType());
}
}
- Настройка серверной части:
// src/main.server.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideServerRendering } from '@angular/platform-server';
import { REQUEST } from '@angular/platform-server';
bootstrapApplication(AppComponent, {
providers: [
provideServerRendering(),
{ provide: REQUEST, useValue: globalThis['__request'] }
]
}).catch(err => console.error(err));
// server.ts
import { ngExpressEngine } from '@nguniversal/express-engine';
import { REQUEST } from '@angular/platform-server';
// ...
app.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
providers: [
{ provide: REQUEST, useValue: req }
]
}));
app.get('*', (req, res) => {
globalThis['__request'] = req;
res.render('index.html', { req });
});
Этот полный пример показывает, как реализовать проверку user-agent в Angular v21 SSR с использованием сервиса и guard для выбора правильных маршрутов исключительно на сервере. Такая реализация обеспечивает мгновенную загрузку правильной версии интерфейса без необходимости выполнения этой логики на клиенте.
Источники
- Angular Universal Guide - Официальное руководство по SSR в Angular: https://angular.io/guide/universal
- Angular REQUEST Token API - Документация по токену REQUEST для доступа к HTTP-запросу на сервере: https://angular.io/api/platform-server/REQUEST
- Angular Guards Documentation - Официальная документация по guards и canMatch в Angular: https://angular.io/guide/router-tutorial-toh#canmatch-guard
- Angular NgModule API - Документация по NgModule для настройки маршрутизации: https://angular.io/api/core/NgModule
- Angular Dependency Injection - Официальная документация по системе инжекции зависимостей: https://angular.io/guide/dependency-injection
Заключение
Реализация сервиса для проверки user-agent в Angular v21 SSR с использованием guard canMatch позволяет эффективно выбирать между мобильными и десктопными маршрутами исключительно на сервере. Ключевые элементы этой реализации включают:
- Создание сервиса DeviceDetectorService, который инжектирует токен REQUEST для доступа к HTTP-запросу
- Настройка guard canMatch для перенаправления на правильные маршруты на основе user-agent
- Корректная конфигурация серверной части для передачи объекта запроса в приложение
- Оптимизация производительности через кэширование и обработку edge cases
Такой подход обеспечивает мгновенную загрузку правильной версии интерфейса без необходимости выполнения проверки user-agent на клиенте, что улучшает пользовательский опыт и производительность вашего angular приложения. Интеграция с SSR в angular 21 делает этот процесс еще более надежным и простым в реализации.
Angular Universal позволяет запускать приложение на сервере (SSR), что критично для обработки user-agent до рендеринга. Для доступа к HTTP-запросу на сервере используется токен REQUEST из пакета @angular/platform-server. Это позволяет анализировать заголовки запроса (включая user-agent) в сервисах и guards. Серверный контекст Angular Universal предоставляет доступ к исходному HTTP-запросу через инъекцию токена REQUEST. Это позволяет обрабатывать заголовки запроса, такие как user-agent, на стороне сервера до активации маршрутов.
Токен REQUEST предоставляет доступ к объекту HttpRequest из Express.js на сервере. Это позволяет извлекать заголовок user-agent и передавать его в сервис для анализа. Токен REQUEST инъецируется в сервисы и guards для доступа к исходному HTTP-запроса. Пример использования: constructor(@Inject(REQUEST) private request: any) {}. В объекте request.headers['user-agent'] содержится строка с информацией о клиенте.