Teras ialah satu set fungsi, konfigurasi dan utiliti yang digunakan dalam aplikasi.
Biasanya, teras ialah perkara yang anda seret dari projek ke projek. Dalam kes ini, saya cuba mengambil apa yang perlu sahaja.
Secara ringkas tentang segala-galanya teras:
Hanya navigasi patut diberi perhatian khusus.
Mari kita lihat dengan lebih dekat pelaksanaan kawalan navigasi.
Andaikan kami mempunyai laluan untuk keseluruhan aplikasi:
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 - jenis yang akan mewakili set rentetan.
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; }
Pautan Navigasi - antara muka tatasusunan pautan.
getRoute membahagikan rentetan dengan parameter dan menggantikan kekunci yang ditemui dengan nilai yang diluluskan:
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; }
Fungsi terkini untuk "membentuk" laluan dalam laluan aplikasi:
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); }
Contoh penggunaan. Dalam 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), }, ], }, ];
Dalam route/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));
Perkhidmatan untuk menghantar acara ke analisis google dan metrik yandex - metrik:
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 - konfigurasi metrik Google dan Yandex:
Perkhidmatan am untuk menghantar acara:
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); } } }
Kaedah perkhidmatan:
Pelaksanaan perkhidmatan Google Analitis:
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, }); } } } }
Metrik 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); } }
Utiliti untuk menghantar parameter permintaan - 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; }
Menetapkan mata wang - mata wang/mata wang.ts:
import { DEFAULT_CURRENCY_CODE, Provider } from '@angular/core'; export function provideCurrency(currencyCode: string): Provider { return { provide: DEFAULT_CURRENCY_CODE, useValue: currencyCode, }; }
Mendapatkan dan menggunakan pembolehubah persekitaran - 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()], };
Penaipan borang - 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; }
Arahan pengesanan perubahan 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; }
persediaan 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, }, ]; }
Menetapkan Jenis Kandungan untuk permintaan 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); };
Perkhidmatan untuk menambah kelas pada komponen dan arahan - 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(); } }
Jenis tersuai - jenis/jenis.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;
Utiliti untuk bekerja dengan tarikh dan tarikh akhir - kegunaan:
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
Atas ialah kandungan terperinci Pembangunan perkhidmatan teras dan utiliti dalam Angular 18. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!