如何在 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-fetch
和 node-fetch
包。直接使用 cross-fetch
和 node-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
?也许创建某种包装器(将其直接包装在 Promise
或 Observable
中似乎不起作用。将其包装在 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.js
、tsconfig.app.json
和 tsconfig.server.json
的 compilerOptions.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
我会等一个星期左右才能得到更好的答案,否则就只接受这个。
我似乎无法让 Angular Universal 在 PokemonListComponent
中等待 this.pokemonDataService.getList()
,然后再提供页面。 PokemonDataService
使用 pokeapi-typescript
包,后者使用 cross-fetch
和 node-fetch
包。直接使用 cross-fetch
和 node-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
?也许创建某种包装器(将其直接包装在 Promise
或 Observable
中似乎不起作用。将其包装在 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.js
、tsconfig.app.json
和 tsconfig.server.json
的 compilerOptions.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
我会等一个星期左右才能得到更好的答案,否则就只接受这个。