Angular 在订阅中通用添加元标记

Angular Universal add meta tag in subscription

我在尝试动态设置元标记时遇到了一些实际问题。 我可以轻松地在 ngOnInit 方法中设置标签,但是如果我使用 Subscription addTag 方法什么都不做。

export class AppComponent implements OnInit, OnDestroy {
    public page: Page;
    public postSlug: string;

    private pages: Page[] = [];
    private url: string = '/';

    private routerSubscription: Subscription;
    private pagesSubscription: Subscription;

    constructor(
        private router: Router,
        private contentfulService: ContentfulService,
        private pageService: PageService,
        private meta: Meta
    ) {}

    ngOnInit(): void {
        this.meta.addTag({ name: 'app', content: 'Set from app component' });
        this.url = this.router.url.split('#')[0]; // For initial page load

        this.contentfulService.getPages();
        this.getPages();
        this.onNavigationEnd();
    }

    ngOnDestroy(): void {
        if (this.routerSubscription) this.routerSubscription.unsubscribe();
        if (this.pagesSubscription) this.pagesSubscription.unsubscribe();
    }

    private onNavigationEnd(): void {
        this.routerSubscription = this.router.events.subscribe((event: any) => {
            if (!(event instanceof NavigationEnd)) return;

            this.url = event.urlAfterRedirects.split('#')[0];

            this.setPage();
            this.setPost();
        });
    }

    private setPost(): void {
        this.postSlug = undefined; // Always reset

        if (!this.page || this.url.indexOf('/live-better/') === -1) return;

        this.meta.addTag({ name: 'post', content: 'Set post' });

        var urlParts = this.url.split('/');
        this.postSlug = urlParts[urlParts.length - 1];
    }

    private setPage(): void {
        if (!this.pages.length || !this.url) return;
        this.page = this.pages.find((page: Page) => page.slug === this.url);

        if (!this.page) {
            this.page = this.pages.find(
                (page: Page) => this.url.indexOf(page.slug) === 0
            );
        }

        this.meta.addTag({ name: 'page', content: 'Set page' });

        console.log(this.page);

        this.pageService.setTitle(this.page.title);
        this.pageService.setMetadata(this.page);
    }

    private getPages(): void {
        this.pagesSubscription = this.contentfulService.pages.subscribe(
            (pages: Page[]) => {
                if (!pages.length) return;
                this.pages = pages;

                this.meta.addTag({ name: 'pages', content: 'Get pages' });

                this.setPage();
                this.setPost();
            }
        );
    }
}

其余代码执行正常。如果我查看源代码,我可以看到 { name: 'app', content: 'Set from app component' } 的标签,但我看不到任何其他标签。

有谁知道我是否遗漏了什么?


我认为这一定是视图后数据加载的问题,所以我创建了一个这样的解析器:

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { ContentfulService } from '../services/contentful.service';
import { Meta } from '@angular/platform-browser';
import { mergeMap, first } from 'rxjs/operators';
import { Observable, of, from } from 'rxjs';

import { Resolve } from '../models/resolve';
import { Page } from '../models/page';

@Injectable({ providedIn: 'root' })
export class PageResolver implements Resolve<{ page: Page; postSlug: string }> {
    constructor(
        private contentfulService: ContentfulService,
        private meta: Meta
    ) {}

    resolve(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
    ): Observable<{ page: Page; postSlug: string }> {
        console.log('about to parse');
        return this.getData(state);
    }

    private getData(
        state: RouterStateSnapshot
    ): Observable<{ page: Page; postSlug: string }> {
        const currentUrl = state.url.split('#')[0];
        console.log(currentUrl);

        this.meta.addTag({ name: 'resolve', content: 'Resolving route' });
        if (!this.contentfulService.current.length) {
            console.log('first load');
            return from(
                this.contentfulService.getPages().then((pages: Page[]) => {
                    this.meta.addTag({
                        name: 'first',
                        content: 'First resolve hit',
                    });
                    return this.parseData(pages, currentUrl);
                })
            );
        } else {
            console.log('after load');
            return this.contentfulService.pages.pipe(
                first(),
                mergeMap((pages: Page[]) => {
                    this.meta.addTag({
                        name: 'second',
                        content: 'Changed page',
                    });
                    return of(this.parseData(pages, currentUrl));
                })
            );
        }
    }

    private parseData(
        pages: Page[],
        currentUrl: string
    ): { page: Page; postSlug: string } {
        let page = this.setPage(pages, currentUrl);
        let postSlug = this.setPost(page, currentUrl);
        let data: { page: Page; postSlug: string } = {
            page: page,
            postSlug: postSlug,
        };

        console.log(data);
        return data;
    }

    private setPage(pages: Page[], currentUrl: string): Page {
        if (!pages.length || !currentUrl) throw 'No pages have been loaded';
        let page = pages.find((page: Page) => page.slug === currentUrl);

        if (!page) {
            page = pages.find(
                (page: Page) => currentUrl.indexOf(page.slug) === 0
            );
        }

        return page;
    }

    private setPost(page: Page, currentUrl: string): string {
        if (!page || currentUrl.indexOf('/live-better/') === -1) return;

        let urlParts = currentUrl.split('/');
        let postSlug = urlParts[urlParts.length - 1];

        let queryIndex = postSlug.indexOf('?');
        if (queryIndex > -1) postSlug = postSlug.substring(0, queryIndex);

        return postSlug;
    }
}

并像这样将其添加到我的路由中:

const routes: Routes = [
    {
        path: '**',
        component: HomeComponent,
        resolve: { content: PageResolver },
    },
];

现在在我的 HomeComponent 中,我得到的数据是这样的:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import { Page } from '@models';
import { Meta } from '@angular/platform-browser';

@Component({
    selector: 'sxp-home',
    templateUrl: './home.component.html',
    styleUrls: ['./home.component.scss'],
})
export class HomeComponent implements OnInit {
    public page: Page;
    public postSlug: string;

    constructor(private route: ActivatedRoute, private meta: Meta) {}

    ngOnInit(): void {
        this.meta.addTag({ name: 'home', content: 'Home component loaded' });
        this.route.data.subscribe(
            (data: { content: { page: Page; postSlug: string } }) => {
                let content = data.content;
                this.meta.addTag({ name: 'meta', content: 'Subscription hit' });
                this.page = content.page;
                this.postSlug = content.postSlug;
                console.log(content);
            }
        );
    }
}

如您所见,我在解析器中使用了 3 次 addTag,在 HomeComponent 中使用了 2 次,但是当我查看源代码时,实际上只有一个已添加:

this.meta.addTag({ name: 'resolve', content: 'Resolving route' });

我似乎无法在任何类型的订阅后设置任何元标记。我希望解析器会延迟查看页面源实际尝试获取数据,直到所有内容都首先加载到解析器中。

我在每次 addTag 调用后添加了控制台日志。 您可以在这里查看它们:

https://sxp-develop-marketing.azurewebsites.net/

页面源是静态的html,当它在浏览器上呈现时将被再次评估。元标记会在那时插入。

要使它们在页面源代码中可见,您必须启用服务器端呈现 - https://angular.io/guide/universal

因此,未正确设置元数据的原因是未使用 TransferState。我创建了一个如下所示的新服务 (TransferHttpService):

import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import {
  TransferState,
  StateKey,
  makeStateKey,
} from '@angular/platform-browser';
import { Observable, from } from 'rxjs';
import { tap } from 'rxjs/operators';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class TransferHttpService {
  constructor(
    protected transferState: TransferState,
    private httpClient: HttpClient,
    @Inject(PLATFORM_ID) private platformId: Object
  ) {}

  request<T>(
    method: string,
    uri: string | Request,
    options?: {
      body?: any;
      headers?:
        | HttpHeaders
        | {
            [header: string]: string | string[];
          };
      reportProgress?: boolean;
      observe?: 'response';
      params?:
        | HttpParams
        | {
            [param: string]: string | string[];
          };
      responseType?: 'json';
      withCredentials?: boolean;
    }
  ): Observable<T> {
    // tslint:disable-next-line:no-shadowed-variable
    return this.getData<T>(
      method,
      uri,
      options,
      (method: string, url: string, options: any) => {
        return this.httpClient.request<T>(method, url, options);
      }
    );
  }

  /**
   * Performs a request with `get` http method.
   */
  get<T>(
    url: string,
    options?: {
      headers?:
        | HttpHeaders
        | {
            [header: string]: string | string[];
          };
      observe?: 'response';
      params?:
        | HttpParams
        | {
            [param: string]: string | string[];
          };
      reportProgress?: boolean;
      responseType?: 'json';
      withCredentials?: boolean;
    }
  ): Observable<T> {
    // tslint:disable-next-line:no-shadowed-variable
    return this.getData<T>(
      'get',
      url,
      options,
      (_method: string, url: string, options: any) => {
        return this.httpClient.get<T>(url, options);
      }
    );
  }

  /**
   * Performs a request with `post` http method.
   */
  post<T>(
    url: string,
    body: any,
    options?: {
      headers?:
        | HttpHeaders
        | {
            [header: string]: string | string[];
          };
      observe?: 'response';
      params?:
        | HttpParams
        | {
            [param: string]: string | string[];
          };
      reportProgress?: boolean;
      responseType?: 'json';
      withCredentials?: boolean;
    }
  ): Observable<T> {
    // tslint:disable-next-line:no-shadowed-variable
    return this.getPostData<T>(
      'post',
      url,
      body,
      options,
      // tslint:disable-next-line:no-shadowed-variable
      (_method: string, url: string, body: any, options: any) => {
        return this.httpClient.post<T>(url, body, options);
      }
    );
  }

  /**
   * Performs a request with `put` http method.
   */
  put<T>(
    url: string,
    _body: any,
    options?: {
      headers?:
        | HttpHeaders
        | {
            [header: string]: string | string[];
          };
      observe?: 'body';
      params?:
        | HttpParams
        | {
            [param: string]: string | string[];
          };
      reportProgress?: boolean;
      responseType?: 'json';
      withCredentials?: boolean;
    }
  ): Observable<T> {
    // tslint:disable-next-line:no-shadowed-variable
    return this.getPostData<T>(
      'put',
      url,
      _body,
      options,
      (_method: string, url: string, _body: any, options: any) => {
        return this.httpClient.put<T>(url, _body, options);
      }
    );
  }

  /**
   * Performs a request with `delete` http method.
   */
  delete<T>(
    url: string,
    options?: {
      headers?:
        | HttpHeaders
        | {
            [header: string]: string | string[];
          };
      observe?: 'response';
      params?:
        | HttpParams
        | {
            [param: string]: string | string[];
          };
      reportProgress?: boolean;
      responseType?: 'json';
      withCredentials?: boolean;
    }
  ): Observable<T> {
    // tslint:disable-next-line:no-shadowed-variable
    return this.getData<T>(
      'delete',
      url,
      options,
      (_method: string, url: string, options: any) => {
        return this.httpClient.delete<T>(url, options);
      }
    );
  }

  /**
   * Performs a request with `patch` http method.
   */
  patch<T>(
    url: string,
    body: any,
    options?: {
      headers?:
        | HttpHeaders
        | {
            [header: string]: string | string[];
          };
      observe?: 'response';
      params?:
        | HttpParams
        | {
            [param: string]: string | string[];
          };
      reportProgress?: boolean;
      responseType?: 'json';
      withCredentials?: boolean;
    }
  ): Observable<T> {
    // tslint:disable-next-line:no-shadowed-variable
    return this.getPostData<T>(
      'patch',
      url,
      body,
      options,
      // tslint:disable-next-line:no-shadowed-variable
      (
        _method: string,
        url: string,
        body: any,
        options: any
      ): Observable<any> => {
        return this.httpClient.patch<T>(url, body, options);
      }
    );
  }

  /**
   * Performs a request with `head` http method.
   */
  head<T>(
    url: string,
    options?: {
      headers?:
        | HttpHeaders
        | {
            [header: string]: string | string[];
          };
      observe?: 'response';
      params?:
        | HttpParams
        | {
            [param: string]: string | string[];
          };
      reportProgress?: boolean;
      responseType?: 'json';
      withCredentials?: boolean;
    }
  ): Observable<T> {
    // tslint:disable-next-line:no-shadowed-variable
    return this.getData<T>(
      'head',
      url,
      options,
      (_method: string, url: string, options: any) => {
        return this.httpClient.head<T>(url, options);
      }
    );
  }

  /**
   * Performs a request with `options` http method.
   */
  options<T>(
    url: string,
    options?: {
      headers?:
        | HttpHeaders
        | {
            [header: string]: string | string[];
          };
      observe?: 'response';
      params?:
        | HttpParams
        | {
            [param: string]: string | string[];
          };
      reportProgress?: boolean;
      responseType?: 'json';
      withCredentials?: boolean;
    }
  ): Observable<T> {
    // tslint:disable-next-line:no-shadowed-variable
    return this.getData<T>(
      'options',
      url,
      options,
      // tslint:disable-next-line:no-shadowed-variable
      (_method: string, url: string, options: any) => {
        return this.httpClient.options<T>(url, options);
      }
    );
  }

  // tslint:disable-next-line:max-line-length
  getData<T>(
    method: string,
    uri: string | Request,
    options: any,
    callback: (
      method: string,
      uri: string | Request,
      options: any
    ) => Observable<any>
  ): Observable<T> {
    let url = uri;

    if (typeof uri !== 'string') {
      url = uri.url;
    }

    const tempKey = url + (options ? JSON.stringify(options) : '');
    const key = makeStateKey<T>(tempKey);
    try {
      return this.resolveData<T>(key);
    } catch (e) {
      //console.log('in catch', key);
      return callback(method, uri, options).pipe(
        tap((data: T) => {
          if (isPlatformBrowser(this.platformId)) {
            // Client only code.
            // nothing;
          }
          if (isPlatformServer(this.platformId)) {
            //console.log('set cache', key);
            this.setCache<T>(key, data);
          }
        })
      );
    }
  }

  private getPostData<T>(
    _method: string,
    uri: string | Request,
    body: any,
    options: any,
    callback: (
      method: string,
      uri: string | Request,
      body: any,
      options: any
    ) => Observable<any>
  ): Observable<T> {
    let url = uri;

    if (typeof uri !== 'string') {
      url = uri.url;
    }

    const tempKey =
      url +
      (body ? JSON.stringify(body) : '') +
      (options ? JSON.stringify(options) : '');
    const key = makeStateKey<T>(tempKey);

    try {
      return this.resolveData<T>(key);
    } catch (e) {
      return callback(_method, uri, body, options).pipe(
        tap((data: T) => {
          if (isPlatformBrowser(this.platformId)) {
            // Client only code.
            // nothing;
          }
          if (isPlatformServer(this.platformId)) {
            this.setCache<T>(key, data);
          }
        })
      );
    }
  }

  private resolveData<T>(key: StateKey<T>): Observable<T> {
    const data = this.getFromCache<T>(key);

    if (!data) {
      throw new Error();
    }

    if (isPlatformBrowser(this.platformId)) {
      //console.log('get cache', key);
      // Client only code.
      this.transferState.remove(key);
    }
    if (isPlatformServer(this.platformId)) {
      //console.log('we are the server');
      // Server only code.
    }

    return from(Promise.resolve<T>(data));
  }

  private setCache<T>(key: StateKey<T>, data: T): void {
    return this.transferState.set<T>(key, data);
  }

  private getFromCache<T>(key: StateKey<T>): T {
    return this.transferState.get<T>(key, null);
  }
}

然后我替换了任何看起来像这样的构造函数:

constructor(private http: HttpClient) {}

对此:

constructor(private http: TransferHttpService) {}

然后一切正常