Pembangunan perkhidmatan teras dan utiliti dalam Angular 18

DDD
Lepaskan: 2024-09-13 22:19:07
asal
962 orang telah melayarinya

Разработка core сервисов и утилит в Angular 18

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:

  • api/params.ts - utiliti untuk menetapkan parameter. Contohnya, dalam query params terdapat baggage=true, tetapi true mestilah bukan rentetan, tetapi boolean;
  • currency/currency.ts — konfigurasi mata wang;
  • env/environment.ts - menerima dan menggunakan .env;
  • form/form.ts - sedikit menaip FormGroup;
  • form/extract-changes.directive.ts - tukar arahan pengesanan dalam FormControl;
  • tukul/hammer.ts - tetapan hammerjs;
  • interceptors/content-type.interceptor.ts - menentukan Content-Type untuk permintaan HTTP;
  • metrik - perkhidmatan untuk menghantar acara ke analisis google dan metrik yandex;
  • navigation/mavigation.ts - pelaksanaan laluan dalam aplikasi;
  • styles/extra-class.service.ts - basikal saya untuk kelas gantung;
  • jenis/jenis.ts - jenis tersuai;
  • utils - utiliti untuk bekerja dengan tarikh dan tarikh akhir.

Hanya navigasi patut diberi perhatian khusus.

Urus navigasi apl

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;
Salin selepas log masuk

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;
}
Salin selepas log masuk

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;
}
Salin selepas log masuk

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);
}
Salin selepas log masuk

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),
      },
    ],
  },
];
Salin selepas log masuk

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));
Salin selepas log masuk

Analitis

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');
Salin selepas log masuk

MetricConfig - konfigurasi metrik Google dan Yandex:

  • id - senarai Google IDS;
  • kaunter - kaunter yandex metrika;
  • domain - senarai domain untuk menggabungkan sesi analitik;
  • laluan - satu set laluan apabila perujuk harus ditetapkan semula.

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);
    }
  }
}
Salin selepas log masuk

Kaedah perkhidmatan:

  • init - permulaan pembilang;
  • navigasi - menghantar acara peralihan halaman;
  • hantar - acara pendaftaran;
  • tetapkan - menghantar parameter kepada analitis.

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,
        });
      }
    }
  }
}
Salin selepas log masuk

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);
  }
}
Salin selepas log masuk

teras

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;
}
Salin selepas log masuk

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,
  };
}
Salin selepas log masuk

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()],
};
Salin selepas log masuk

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;
}
Salin selepas log masuk
Salin selepas log masuk

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;
}
Salin selepas log masuk
Salin selepas log masuk

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,
    },
  ];
}
Salin selepas log masuk

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);
};
Salin selepas log masuk

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();
  }
}
Salin selepas log masuk

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;
Salin selepas log masuk

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);
}
Salin selepas log masuk

Короткая реализация для 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);
  });
}
Salin selepas log masuk

И функция для гуманизации:

export function camelCaseToHumanize(str: string): string {
  return str.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
}
Salin selepas log masuk

В следующей статье будем разрабатывать свой 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!

sumber:dev.to
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan