Maison > Article > interface Web > Développement de services et utilitaires de base dans Angular 18
Core est un ensemble de fonctions, de configurations et d'utilitaires utilisés dans l'application.
Habituellement, le noyau est ce que vous faites glisser d'un projet à l'autre. Dans ce cas, j'ai essayé de prendre uniquement ce qui était nécessaire.
En bref sur tout ce qui est essentiel :
Seule la navigation mérite une attention particulière.
Examinons de plus près la mise en œuvre des contrôles de navigation.
Supposons que nous ayons des itinéraires pour l'ensemble de l'application :
export const PATHS = { home: '', homeAvia: '', homeHotels: 'hotels', homeTours: 'tours', homeRailways: 'railways', rules: 'rules', terms: 'terms', documents: 'documents', faq: 'faq', cards: 'cards', login: 'login', registration: 'registration', notFound: 'not-found', serverError: 'server-error', permissionDenied: 'permission-denied', search: 'search', searchAvia: 'search/avia', searchHotel: 'search/hotels', searchTour: 'search/tours', searchRailway: 'search/railways', } as const;
PathValues - un type qui représentera un ensemble de chaînes.
export type PathValues = (typeof PATHS)[keyof typeof PATHS]; type Filter<T extends string> = T extends `:${infer Param}` ? Param : never; type Split<Value extends string> = Value extends `${infer LValue}/${infer RValue}` ? Filter<LValue> | Split<RValue> : Filter<Value>; export type GetPathParams<T extends string> = { [key in Split<T>]: string | number; }; export interface NavigationLink<T extends PathValues = PathValues> { readonly label: string; readonly route: T; readonly params?: GetPathParams<T>; readonly suffix?: string; }
NavigationLink - interface de tableau de liens.
getRoute divise la chaîne avec des paramètres et remplace les clés trouvées par les valeurs transmises :
export function getRoute<T extends PathValues>(path: T, params: Record<string, string | number> = {}): (string | number)[] { const segments = path.split('/').filter((value) => value?.length); const routeWithParams: (string | number)[] = ['/']; for (const segment of segments) { if (segment.charAt(0) === ':') { const paramName = segment.slice(1); if (params && params[paramName]) { routeWithParams.push(params[paramName]); } else { routeWithParams.push(paramName); } } else { routeWithParams.push(segment); } } return routeWithParams; }
Dernières fonctions pour « façonner » les chemins dans les routes d'application :
export function getChildPath(path: PathValues, parent: PathValues): string { return path.substring(parent.length + 1); } export function childNavigation(route: NavigationChild, parent: PathValues): Route { const redirectTo = route.redirectTo ? `/${route.redirectTo}` : undefined; if (!route.path.length || route.path.length < parent.length + 1) { return { ...route, redirectTo }; } return { ...route, redirectTo, path: getChildPath(route.path, parent), }; } export function withChildNavigation(parent: PathValues): (route: NavigationChild) => Route { return (route: NavigationChild) => childNavigation(route, parent); }
Exemple d'utilisation. Dans app.routes :
export const routes: Routes = [ { path: '', loadComponent: () => import('@baf/ui/layout').then((m) => m.LayoutComponent), children: [ { path: PATHS.search, loadChildren: () => import('./routes/search.routes').then((m) => m.searchRoutes), }, ], }, ];
Dans routes/search.routes.ts :
import { Routes } from '@angular/router'; import { PATHS, withChildNavigation } from '@baf/core'; export const searchRoutes: Routes = [ { path: PATHS.searchAvia, title: $localize`:Search Page:Search for cheap flights`, loadComponent: () => import('@baf/search/page').then((m) => m.SearchPageComponent), }, ].map(withChildNavigation(PATHS.search));
Service d'envoi d'événements à Google Analytics et Yandex Metrika - métriques :
import { InjectionToken } from '@angular/core'; export interface MetricConfig { readonly ids?: string[]; readonly counter?: number; readonly domains: string[]; readonly paths: string[]; } export const METRIC_CONFIG = new InjectionToken<MetricConfig>('MetricConfig');
MetricConfig - configuration des métriques Google et Yandex :
Service général d'envoi d'événements :
import { inject, Injectable } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { NavigationEnd, Router } from '@angular/router'; import { filter } from 'rxjs'; import { tap } from 'rxjs/operators'; import { GoogleAnalytics } from './google-analytics'; import { YandexMetrika } from './yandex.metrika'; export interface MetricOptions { readonly [key: string]: unknown; readonly ym?: Record<string, unknown>; readonly ga?: Record<string, unknown>; } export function extractOptions(options?: MetricOptions): { readonly ym?: Record<string, unknown>; readonly ga?: Record<string, unknown> } { let { ym, ga } = options ?? {}; if (options) { if (!ym && !ga) { ym = options; ga = options; } } return { ym, ga }; } @Injectable() export class MetricService { private readonly router = inject(Router); private readonly yandexMetrika = inject(YandexMetrika); private readonly googleAnalytics = inject(GoogleAnalytics); constructor() { this.router.events .pipe( filter((event): event is NavigationEnd => event instanceof NavigationEnd), tap((event) => this.navigation(event.urlAfterRedirects)), takeUntilDestroyed(), ) .subscribe(); } init(customerId?: string | number) { this.set({ ga: customerId ? { user_id: customerId } : undefined, ym: customerId ? { UserID: customerId } : undefined, }); } navigation(url: string): void { this.googleAnalytics.sendNavigation(url); this.yandexMetrika.hit(url); } send(action: string, options?: MetricOptions): void { const params = extractOptions(options); this.googleAnalytics.sendEvent(action, params.ga); this.yandexMetrika.reachGoal(action, params.ym); } set(options: MetricOptions): void { const params = extractOptions(options); if (params.ga) { this.googleAnalytics.set(params.ga); } if (params.ym) { this.yandexMetrika.set(params.ym); } } }
Méthodes de service :
Mise en place du service Google Analytics :
import { DOCUMENT, isPlatformBrowser } from '@angular/common'; import { inject, Injectable, PLATFORM_ID } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { METRIC_CONFIG } from './metrica.interface'; declare global { interface Window { readonly gtag?: (...params: unknown[]) => void; } } @Injectable() export class GoogleAnalytics { private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); private readonly document = inject(DOCUMENT); private readonly config = inject(METRIC_CONFIG); private readonly title = inject(Title); readonly gtag: (...params: unknown[]) => void; constructor() { if (this.isBrowser && typeof this.document.defaultView?.gtag !== 'undefined' && this.config.ids && this.config.ids?.length > 0) { this.gtag = this.document.defaultView.gtag; } else { this.gtag = () => {}; } } set(payload: Record<string, unknown>): void { this.gtag('set', payload); } sendEvent(action: string, payload?: Record<string, unknown>): void { this.gtag('event', action, { ...payload, event_category: payload?.['eventCategory'], event_label: payload?.['eventLabel'], value: payload?.['eventValue'], }); } sendNavigation(url: string): void { if ( (this.isBrowser && !this.config.domains.every((domain) => this.document.referrer.indexOf(domain) < 0)) || !this.config.paths.every((path) => this.document.location.pathname.indexOf(path) < 0) ) { this.set({ page_referrer: this.document.defaultView?.location.origin ?? '' }); } if (this.config.ids) { for (const key of this.config.ids) { this.gtag('config', key, { page_title: this.title.getTitle(), page_path: url, }); } } } }
Mesures Yandex :
import { DOCUMENT, isPlatformBrowser } from '@angular/common'; import { inject, Injectable, PLATFORM_ID } from '@angular/core'; import { METRIC_CONFIG } from './metrica.interface'; declare global { interface Window { readonly ym?: (...params: unknown[]) => void; } } @Injectable() export class YandexMetrika { private readonly document = inject(DOCUMENT); private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); private readonly config = inject(METRIC_CONFIG); private readonly counter: (...params: unknown[]) => void; constructor() { if (this.isBrowser && typeof this.document.defaultView?.ym !== 'undefined' && !!this.config.counter) { this.counter = this.document.defaultView.ym; } else { this.counter = () => {}; } } hit(url: string, options?: Record<string, unknown>): void { let clearReferrer = false; if ( (this.isBrowser && !this.config.domains.every((domain) => this.document.referrer.indexOf(domain) < 0)) || !this.config.paths.every((path) => this.document.location.pathname.indexOf(path) < 0) ) { clearReferrer = true; } const optionsAll: { referer?: string } = { ...options }; if (clearReferrer) { optionsAll.referer = ''; } this.counter(this.config.counter, 'hit', url, optionsAll); } reachGoal(target: string, options?: Record<string, unknown>): void { this.counter(this.config.counter, 'reachGoal', target, options); } set(params: Record<string, unknown>, options?: Record<string, unknown>): void { this.counter(this.config.counter, 'userParams', params, options); } }
Utilitaire de diffusion des paramètres de demande - api/params.ts :
export type HttpParams = Record<string, string | number | boolean | ReadonlyArray<string | number | boolean>>; export function castParams(all: Record<string, unknown>): HttpParams { const params: HttpParams = {}; for (const [key, value] of Object.entries(all)) { if (Array.isArray(value) && value.length > 0) { params[key] = value.filter((val) => { return typeof val === 'string' || typeof val === 'boolean' || typeof val === 'number'; }); } else if (typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number') { params[key] = value; } } return params; }
Définition de la devise - devise/currency.ts :
import { DEFAULT_CURRENCY_CODE, Provider } from '@angular/core'; export function provideCurrency(currencyCode: string): Provider { return { provide: DEFAULT_CURRENCY_CODE, useValue: currencyCode, }; }
Obtenir et utiliser des variables d'environnement - env/environment.ts :
import type { ApplicationConfig} from '@angular/core'; import { APP_INITIALIZER, makeStateKey, TransferState } from '@angular/core'; export const ENV_KEY = makeStateKey<Environment>('Environment'); export interface Environment { readonly aviasalesToken: string; readonly hotellookToken: string; } export const ENV_DEFAULT: Environment = { aviasalesToken: '', hotellookToken: '', }; export function provideEnv() { return [ { provide: APP_INITIALIZER, useFactory: (transferState: TransferState) => { return () => { transferState.set<Environment>(ENV_KEY, { aviasalesToken: process.env['AVIASALES_TOKEN'] ?? ENV_DEFAULT.aviasalesToken, hotellookToken: process.env['HOTELLOOK_TOKEN'] ?? ENV_DEFAULT.hotellookToken, }); }; }, deps: [TransferState], multi: true, }, ]; } export const envConfig: ApplicationConfig = { providers: [provideEnv()], };
Saisie de formulaire - form/form.ts :
import type { FormControl, FormGroup } from '@angular/forms'; export type FormFor<T> = { [P in keyof T]: FormControl<T[P]>; }; export type FormWithSubFor<T> = { [P in keyof T]: T[P] extends Record<string, unknown> ? FormGroup<FormFor<T[P]>> : FormControl<T[P]>; }; export function castQueryParams(queryParams: Record<string, unknown>, props?: string[]): Record<string, unknown> { const mapped: Record<string, unknown> = {}; const keys = props ?? Object.keys(queryParams); for (const key of keys) { const value = queryParams[key]; if (typeof value === 'string' && value.length > 0) { if (['true', 'false'].includes(value)) { mapped[key] = value === 'true'; } else if (!isNaN(Number(value))) { mapped[key] = Number(value); } else { mapped[key] = value; } } else if (typeof value === 'boolean') { mapped[key] = value; } else if (typeof value === 'number' && value > 0) { mapped[key] = value; } } return mapped; }
Directive de détection de changement FormControl :
import type { FormControl, FormGroup } from '@angular/forms'; export type FormFor<T> = { [P in keyof T]: FormControl<T[P]>; }; export type FormWithSubFor<T> = { [P in keyof T]: T[P] extends Record<string, unknown> ? FormGroup<FormFor<T[P]>> : FormControl<T[P]>; }; export function castQueryParams(queryParams: Record<string, unknown>, props?: string[]): Record<string, unknown> { const mapped: Record<string, unknown> = {}; const keys = props ?? Object.keys(queryParams); for (const key of keys) { const value = queryParams[key]; if (typeof value === 'string' && value.length > 0) { if (['true', 'false'].includes(value)) { mapped[key] = value === 'true'; } else if (!isNaN(Number(value))) { mapped[key] = Number(value); } else { mapped[key] = value; } } else if (typeof value === 'boolean') { mapped[key] = value; } else if (typeof value === 'number' && value > 0) { mapped[key] = value; } } return mapped; }
configuration de Hammerjs :
import type { EnvironmentProviders, Provider } from '@angular/core'; import { importProvidersFrom, Injectable } from '@angular/core'; import { HAMMER_GESTURE_CONFIG, HammerGestureConfig, HammerModule } from '@angular/platform-browser'; @Injectable() export class HammerConfig extends HammerGestureConfig { override overrides = { swipe: { velocity: 0.4, threshold: 20 }, pinch: { enable: false }, rotate: { enable: false }, }; } export function provideHammer(): (Provider | EnvironmentProviders)[] { return [ importProvidersFrom(HammerModule), { provide: HAMMER_GESTURE_CONFIG, useClass: HammerConfig, }, ]; }
Définition du type de contenu pour les requêtes HTTP -interceptors/content-type.interceptor.ts :
import type { HttpInterceptorFn } from '@angular/common/http'; export const contentTypeInterceptor: HttpInterceptorFn = (req, next) => { if (!req.headers.has('Content-Type') && req.headers.get('enctype') !== 'multipart/form-data') { req = req.clone({ headers: req.headers.set('Content-Type', 'application/json') }); } return next(req); };
Service d'ajout de classes aux composants et directives - styles/extra-class.service.ts :
import { DestroyRef, ElementRef, inject, Injectable, Renderer2 } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import type { Observable, Subscription } from 'rxjs'; import { tap } from 'rxjs'; type Styles = string | string[] | undefined | null; export function toClass(value: unknown | undefined | null, prefix = 'is'): Styles { return value ? `${prefix}-${value}` : undefined; } @Injectable() export class ExtraClassService { private readonly destroyRef = inject(DestroyRef); private readonly render = inject(Renderer2); private readonly elementRef: ElementRef<HTMLElement> = inject(ElementRef); private styles: Record<string, Styles> = {}; private subscriptions: Record<string, Subscription> = {}; update(key: string, styles: Styles): void { if (this.styles[key]) { const lastStyles = this.styles[key]; if (Array.isArray(lastStyles)) { lastStyles.map((style) => this.render.removeClass(this.elementRef.nativeElement, style)); } else if (lastStyles) { this.render.removeClass(this.elementRef.nativeElement, lastStyles); } } if (Array.isArray(styles)) { styles.map((style) => this.render.addClass(this.elementRef.nativeElement, style)); } else if (styles) { this.render.addClass(this.elementRef.nativeElement, styles); } this.styles[key] = styles; } patch(style: string, active: boolean): void { if (active) { this.render.addClass(this.elementRef.nativeElement, style); } else { this.render.removeClass(this.elementRef.nativeElement, style); } } register(key: string, observable: Observable<unknown>, callback: () => void, start = true): void { if (this.subscriptions[key]) { this.unregister(key); } if (start) { callback(); } this.subscriptions[key] = observable .pipe( tap(() => callback()), takeUntilDestroyed(this.destroyRef), ) .subscribe(); } unregister(key: string): void { this.subscriptions[key].unsubscribe(); } }
Types personnalisés - types/type.ts :
export type ChangeFn = (value: any) => void; export type TouchedFn = () => void; export type DisplayFn = (value: any, index?: number) => string; export type MaskFn = (value: any) => string; export type StyleFn = (value?: any) => string | string[]; export type CoerceBoolean = boolean | string | undefined | null;
Utilitaires pour travailler avec des dates et des délais - utils :
export function getDaysBetweenDates(startDate: Date | string, endDate: Date | string): number { return Math.round((new Date(endDate).getTime() - new Date(startDate).getTime()) / 86400000); }
Короткая реализация для uuid:
export function uuidv4(): string { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; const v = c == 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); }
И функция для гуманизации:
export function camelCaseToHumanize(str: string): string { return str.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()); }
В следующей статье будем разрабатывать свой UI KIT.
Все исходники находятся на github, в репозитории - github.com/Fafnur/buy-and-fly
Демо можно посмотреть здесь - buy-and-fly.fafn.ru/
Мои группы: telegram, medium, vk, x.com, linkedin, site
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!