如何在 Angular Universal 中支持 cross-fetch/node-fetch?

How to support cross-fetch/node-fetch in Angular Universal?

我似乎无法让 Angular Universal 在 PokemonListComponent 中等待 this.pokemonDataService.getList(),然后再提供页面。 PokemonDataService 使用 pokeapi-typescript 包,后者使用 cross-fetchnode-fetch 包。直接使用 cross-fetchnode-fetch 中的 fetch 函数也不起作用,所以我认为问题是 Angular 没有意识到它需要等待它。

使用旧的 HttpClient 直接访问 API 我需要正常工作(例如 this.pokemonList$ = this.http.get("https://pokeapi.co/api/v2/pokemon?limit=5"))。但是,这是否意味着我需要完全放弃使用 pokeapi-typescript 并实现我自己需要的东西?或者有没有办法让Angular“认识到”它应该等待cross-fetch/node-fetch?也许创建某种包装器(将其直接包装在 PromiseObservable 中似乎不起作用。将其包装在 setTimeout 中确实......但在许多层面上感觉如此错误)?

感谢您的帮助。

src/app/pokemon-list/pokemon-list.component.ts:

import { Component, OnInit } from '@angular/core';
import { INamedApiResourceList, IPokemon } from 'pokeapi-typescript';
import { Observable } from 'rxjs';
import { PokemonDataService } from "../pokemon-data.service"

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

  pokemonList$: Observable<INamedApiResourceList<IPokemon>>

  constructor(private pokemonDataService: PokemonDataService) {
  }

  ngOnInit(): void {
    this.pokemonList$ = this.pokemonDataService.getList()
  }

}

如有需要,额外上下文的文件:

src/app/poke-api.注射-token.ts:

import {InjectionToken} from "@angular/core";
import PokeAPI, { IPokemon } from "pokeapi-typescript";
import { NamedEndpoint } from 'pokeapi-typescript/dist/classes/NamedEndpoint';

export type PokemonEndpoint = NamedEndpoint<IPokemon>;

export const PokeApiToken = new InjectionToken<PokemonEndpoint>('PokeApi', {
  providedIn: 'root',
  factory: () => PokeAPI.Pokemon,
});

src/app/pokemon-data.service.ts:

import { Inject, Injectable } from '@angular/core';
import { from } from 'rxjs';
import {PokeApiToken, PokemonEndpoint} from "./poke-api.injection-token";

@Injectable({
  providedIn: 'root'
})
export class PokemonDataService {

  constructor(@Inject(PokeApiToken) private pokeApi: PokemonEndpoint) { }

  getList() {
    return from(this.pokeApi.list(5))
  }

}

src/app/pokemon-list/pokemon-list.component.html:

<ul>
  <li *ngFor="let pokemon of (pokemonList$ | async)?.results">
    {{pokemon.name}}
  </li>
</ul>

20 年 12 月 12 日更新:

如果它能帮助其他人提出更好的答案(或者如果没有其他答案),我会post我正在做的和正在做的事情。

简而言之,通过 Zone 创建一个宏任务,Angular 能够识别它应该等待它完成。 Angular 自己的 HttpClient 在其他模块中,将它用于相同的目的(以及更多)。 Zone 是由 Angular 的 zone.js 提供的全局 polyfill,所以我认为使用起来应该没问题,不用担心会出现问题。

为了解决这个问题,我们可以创建一个包装器并在其中创建宏任务以确保 Angular 会等待。组件本身不需要更新。

/src/util/wrapPromiseMethodAsMacroTask.ts:

import { Observable } from 'rxjs';

export interface WrapPromiseMethodAsMacroTaskFunction {
  <T>(
    context: any,
    method: (...args: any[]) => Promise<T>,
    ...args: any[]
  ): Observable<T>;
}

export const wrapPromiseMethodAsMacroTask: WrapPromiseMethodAsMacroTaskFunction = <
  T
>(
  context,
  method,
  ...args
) => {
  return new Observable<T>((subscriber) => {
    const task = Zone.current.scheduleMacroTask(
      'wrapPromiseMethod',
      () => null,
      {},
      () => null,
      () => null
    );

    method
      .bind(context)(...args)
      .then((data) => {
        subscriber.next(data);
        subscriber.complete();
      })
      .catch((err) => {
        console.log(err);
        subscriber.error(err);
      })
      .finally(() => {
        task.invoke();
      });
  });
};

/src/app/wrapPromiseMethodAsMacroTask.注射-token.ts:

import { InjectionToken } from '@angular/core';
import {
  WrapPromiseMethodAsMacroTaskFunction,
  wrapPromiseMethodAsMacroTask,
} from '../util/wrapPromiseMethodAsMacroTask';

export const WrapPromiseMethodAsMacroTaskToken = new InjectionToken<WrapPromiseMethodAsMacroTaskFunction>(
  'WrapPromiseMethodAsMacroTask',
  {
    providedIn: 'root',
    factory: () => wrapPromiseMethodAsMacroTask,
  }
);

/src/app/pokemon-data.service.ts

import { Inject, Injectable } from '@angular/core';
import { INamedApiResourceList, IPokemon } from 'pokeapi-typescript';
import { WrapPromiseMethodAsMacroTaskFunction } from 'src/util/wrapPromiseMethodAsMacroTask';
import { PokeApiToken, PokemonEndpoint } from './poke-api.injection-token';
import { WrapPromiseMethodAsMacroTaskToken } from './wrapPromiseMethodAsMacroTask.injection-token';

@Injectable({
  providedIn: 'root',
})
export class PokemonDataService {
  constructor(
    @Inject(WrapPromiseMethodAsMacroTaskToken)
    private wrapPromiseMethodAsMacroTask: WrapPromiseMethodAsMacroTaskFunction,
    @Inject(PokeApiToken) private pokeApi: PokemonEndpoint
  ) {}

  getList() {
    return this.wrapPromiseMethodAsMacroTask<INamedApiResourceList<IPokemon>>(
      this.pokeApi,
      this.pokeApi.list,
      5
    );
  }
}

还要确保在 tsconfig.jstsconfig.app.jsontsconfig.server.jsoncompilerOptions.types 中包含 "zone.js",让 TypeScript 将 Zone 识别为全局并键入它。

仅供参考,zone.js 类型给我带来了很多麻烦,经过一些研究后我发现我必须更新到 Angular 11 以及 zone.js 版本 0.11.3 (https://github.com/angular/angular/issues/37531)。如果你不这样做,你可能会遇到 NodeJS 类型冲突,并且可能需要求助于复制所需的类型并创建你自己的 *.d.ts 或使用 any.

对此提供线索的来源:https://github.com/angular/angular/issues/15278#issuecomment-326034463

我会等一个星期左右才能得到更好的答案,否则就只接受这个。