Angular SSR 元标记未在承诺中加载

Angular9 SSR meta tags not loading inside of promises

我正在为我的 SSR 使用 Angular Universal 和 NestJS。当我 运行 npm run build:ssr 并部署它时,我没有得到任何错误,但是在链接网站时不会显示 promises 内的元标记,但是我在根级别设置元标记的路由链接ngOnInit 会在链接网站时按预期加载元标记。

设置元标记的代码。

generateTags({ title = '', description = '', image = '' }) {

 this.title.setTitle(title);
 this.meta.addTags([
   // Open Graph
   { name: 'og:url', content: `https://firestarter.fireship.io${this.router.url}` },
   { name: 'og:title', content: title },
   { name: 'og:description', content: description },
   { name: 'og:image', content: image },
   // Twitter Card
   { name: 'twitter:card', content: 'summary' },
   { name: 'twitter:site', content: '@fireship_dev' },
 ]);
} 

Ek 不加载元标记的代码示例

this.db.firestore.collection('users').doc(this.customerId).get().then((userRef) => {
  this.seo.generateTags({
    title: userRef.data().displayName,
    description: userRef.data().about,
    image: userRef.data().photoURL,
  })
})

不加载元标记的代码示例

this.seo.generateTags({
  title: userRef.data().displayName,
  description: userRef.data().about,
  image: userRef.data().photoURL,
})

包含完整组件的示例:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { AngularFirestore } from '@angular/fire/firestore';
import { SeoService } from 'src/app/services/seo.service';

@Component({
  selector: 'app-profileviewer',
  templateUrl: './profileviewer.component.html',
  styleUrls: ['./profileviewer.component.css']
})
export class ProfileviewerComponent implements OnInit {

  customerId: string;

  constructor(
    private route: ActivatedRoute,
    private db: AngularFirestore,
    private seo: SeoService) { }

  ngOnInit() {
    this.customerId = this.route.snapshot.paramMap.get('id');

    this.db.firestore.collection('users').doc(this.customerId).get().then((userRef) => {
      this.seo.generateTags({
        title: userRef.data().displayName,
        description: userRef.data().about,
        image: userRef.data().photoURL,
      })
    })
  }
}

谁可以使用 Angular9 和 NestJS 在我的元标记中显示来自例如 firebase 的内容?

我在 GitHub 上创建了一个 dummy project,但出现了这个错误。要在环境文件和 运行 npm run serve:ssr 中重现附加 firebase 项目(如果出现错误,可能 npm run build:ssr)并在 chrome 中的源代码中看到元标记是未渲​​染。

编辑: 我已经尝试使用 resolve 来解决这个问题,但它仍然不能用于 promises。这是我使用的解析脚本:

import { Injectable } from '@angular/core';
import { AngularFirestore } from '@angular/fire/firestore';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Http } from '@angular/http';

@Injectable({
  providedIn: 'root'
})
export class LoadSeoService implements Resolve<any> {
  constructor (private http: Http, private db: AngularFirestore) {

  }

  resolve (route: ActivatedRouteSnapshot, rstate: RouterStateSnapshot) {
    // Works
    return "test"

    // Does not work
    // return this.db.firestore.collection("articles").doc(route.params['id'].substr(route.params['id'].length - 20)).get();
  }
}

我在组件中使用的代码:

this.route.data.subscribe(data => {
  console.log(data.cres)

  this.seo.generateTags({
    title: data.cres,
    description: data.cres,
    image: data.cres,
  })
});

您对 firestore 的调用是异步的,因此 SSR 在 promise 解析之前完成。所有异步功能都需要在路由器级别解决,因此 angular 等待它们完成。

尝试将 firestore 请求添加到路由解析器,并在 ngOnInit() 函数中使用来自解析器的数据。

在此处查看更多信息:https://angular.io/api/router/Resolve

最后,经过大量研究和调试,我发现为什么您在 index.html 中看不到元标记。

  1. 确保添加了元标记。添加标签后,我在控制台中打印了元对象。你也可以检查一下。打印它:- 在输出中进入 _doc>documentChild>innerHtml

在上图中的 innerHTML 中,您可以注意到您的元标记。

  1. 为什么它们不是 index.html 的一部分,当它显示时,是因为 firebase promise 在连接到 firebase 并接收数据时会自动解析。但因为它是你的一部分。您的 ssr 不会生成标签。当您的承诺是 resolved.Any 页面加载后不会显示在页面视图中的正在更改的内容时,这些标签将稍后动态添加 source.If 您想要检查您需要检查的更新元标记元素并检查那里。

为了证明我的第二点,这里是 index.html 网络选项卡中文件的屏幕截图。

元素检查器的屏幕截图显示您的元标记已成功添加,但在运行时。

如果您将创建一个普通的 promise 并在其中调用您的生成标签方法,它将显示在源本身中,因为您将只在那里解决该 promise。但是在 firebase 的情况下,该承诺在获取数据之前不会得到解决,因此您的 ssr 无法预渲染它。

要使用 ssr 启用 firebase,您可以在此处找到信息:-

https://medium.com/@antonybudianto/server-side-rendering-with-react-and-firebase-functions-cd67fdb2b605

Angular SSR 在将响应发送给用户之前会等待 ngInit() 中的所有承诺完成。但是我发现 ngInit 并没有使用 firestore 的 get() 方法等待。所以我绑定了 valueChanges() 并且它确实按预期工作,另一个好处是它 returns 一个直接带有数据的 Observable,所以你的 ngInit 看起来像:

ngOnInit() {
    this.customerId = this.route.snapshot.paramMap.get('id');

    this.db.collection('users').doc(this.customerId).valueChanges().subscribe(user => {
      this.seo.generateTags({
        title: user.displayName,
        description: user.about,
        image: user.photoURL,
      });
    });
  }

另一件您可能会发现有用的事情是使用 TransferState 将服务器已解析的内容传输到浏览器,这样它就不会在浏览器完全加载页面时再次尝试获取信息。所以你的组件最终会是这样的:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { AngularFirestore } from '@angular/fire/firestore';
import { SeoService } from 'src/app/services/seo.service';
import { TransferState, makeStateKey } from '@angular/platform-browser';


const USER_TAGS_KEY = makeStateKey('ProfileviewerComponent_UserTagsState');

@Component({
  selector: 'app-profileviewer',
  templateUrl: './profileviewer.component.html',
  styleUrls: ['./profileviewer.component.css']
})
export class ProfileviewerComponent implements OnInit {

  customerId: string;
  tags = [];

  constructor(
    private route: ActivatedRoute,
    private db: AngularFirestore,
    private state: TransferState,
    private seo: SeoService) { }

  ngOnInit() {
    this.customerId = this.route.snapshot.paramMap.get('id');

    this.tags = this.state.get(USER_TAGS_KEY, null);
    if (!this.tags) {
      this.db.collection('users').doc(this.customerId).valueChanges().subscribe(user => {
        this.tags = {
          title: user.displayName,
          description: user.about,
          image: user.photoURL,
        };

        this.state.set(USER_TAGS_KEY, this.tags);
        this.seo.generateTags(this.tags);
      });
    }
  }
}