服务器在渲染之前不会等到 http 调用完成 - 角度 4 服务器端渲染
Posted
技术标签:
【中文标题】服务器在渲染之前不会等到 http 调用完成 - 角度 4 服务器端渲染【英文标题】:Server does not wait till http call completes before rendering - angular 4 server side rendering 【发布时间】:2018-04-19 11:50:44 【问题描述】:我已经实现了 Angular Universal,并且能够通过服务器端渲染来渲染 html 的静态部分。我面临的问题是,正在进行 API 调用并且服务器正在呈现 html 而无需等待 http 调用完成。因此,我的模板依赖于从 api 调用获得的数据的部分不会在服务器上呈现。
更多信息:
我在节点服务器中使用身份验证,仅当用户通过身份验证并在响应时设置 cookie 时才提供索引 html。
每当我从 Angular 进行 API 调用时,我也会将 cookie 作为标头发送,因为相关服务也会使用令牌验证用户。对于服务器端渲染,由于 cookie 在服务器级别不可用,我已成功注入请求并为 API 调用选择 cookie。因此,API 调用是成功的,但服务器在 promise 解决之前不会等待渲染。
我尝试过的步骤没有成功:
我已按照此评论 https://github.com/angular/universal-starter/issues/181#issuecomment-250177280 中的建议更改了我的区域版本
如果需要任何进一步的信息,请告诉我。
将我引导到一个包含 http 调用的有角度的通用样板将对我有所帮助。
【问题讨论】:
有什么消息吗?似乎是个大问题 【参考方案1】:我对以前的解决方案有一些问题/疑虑。这是我的解决方案
使用 Promise 和 Observables 为 Observables 提供确定任务何时完成的选项(例如,完成/错误、首次发射、其他) 可以选择在任务完成时间过长时发出警告 Angular UDK 似乎不尊重在组件外部发起的任务(例如,由 NGXS)。这提供了一个 awaitMacroTasks(),可以从组件中调用它来修复它。Gist
/// <reference types="zone.js" />
import Inject, Injectable, InjectionToken, OnDestroy, Optional from "@angular/core";
import BehaviorSubject, Observable, of, Subject, Subscription from "rxjs";
import finalize, switchMap, takeUntil, takeWhile, tap from "rxjs/operators";
export const MACRO_TASK_WRAPPER_OPTIONS = new InjectionToken<MacroTaskWrapperOptions>("MacroTaskWrapperOptions");
export interface MacroTaskWrapperOptions
wrapMacroTaskTooLongWarningThreshold?: number;
/*
* These utilities help Angular Universal know when
* the page is done loading by wrapping
* Promises and Observables in ZoneJS Macro Tasks.
*
* See: https://gist.github.com/sparebytes/e2bc438e3cfca7f6687f1d61287f8d72
* See: https://github.com/angular/angular/issues/20520
* See: https://***.com/a/54345373/787757
*
* Usage:
*
```ts
@Injectable
class MyService
constructor(private macroTaskWrapper: MacroTaskWrapperService)
doSomething(): Observable<any>
return this.macroTaskWrapper.wrapMacroTask("MyService.doSomething", getMyData())
@Component
class MyComponent
constructor(private macroTaskWrapper: MacroTaskWrapperService)
ngOnInit()
// You can use wrapMacroTask here
this.macroTaskWrapper.wrapMacroTask("MyComponent.ngOnInit", getMyData())
// If any tasks have started outside of the component use this:
this.macroTaskWrapper.awaitMacroTasks("MyComponent.ngOnInit");
```
*
*/
@Injectable( providedIn: "root" )
export class MacroTaskWrapperService implements OnDestroy
/** Override this value to change the warning time */
wrapMacroTaskTooLongWarningThreshold: number;
constructor(@Inject(MACRO_TASK_WRAPPER_OPTIONS) @Optional() options?: MacroTaskWrapperOptions)
this.wrapMacroTaskTooLongWarningThreshold =
options && options.wrapMacroTaskTooLongWarningThreshold != null ? options.wrapMacroTaskTooLongWarningThreshold : 10000;
ngOnDestroy()
this.macroTaskCount.next(0);
this.macroTaskCount.complete();
/**
* Useful for waiting for tasks that started outside of a Component
*
* awaitMacroTasks$().subscribe()
**/
awaitMacroTasks$(label: string, stackTrace?: string): Observable<number>
return this._wrapMacroTaskObservable(
"__awaitMacroTasks__" + label,
of(null)
// .pipe(delay(1))
.pipe(switchMap(() => this.macroTaskCount))
.pipe(takeWhile(v => v > 0)),
null,
"complete",
false,
stackTrace,
);
/**
* Useful for waiting for tasks that started outside of a Component
*
* awaitMacroTasks()
**/
awaitMacroTasks(label: string, stackTrace?: string): Subscription
// return _awaitMacroTasksLogged();
return this.awaitMacroTasks$(label, stackTrace).subscribe();
awaitMacroTasksLogged(label: string, stackTrace?: string): Subscription
console.error("MACRO START");
return this.awaitMacroTasks$(label, stackTrace).subscribe(() => , () => , () => console.error("MACRO DONE"));
/**
* Starts a Macro Task for a promise or an observable
*/
wrapMacroTask<T>(
label: string,
request: Promise<T>,
warnIfTakingTooLongThreshold?: number | null,
isDoneOn?: IWaitForObservableIsDoneOn<T> | null,
stackTrace?: string | null,
): Promise<T>;
wrapMacroTask<T>(
label: string,
request: Observable<T>,
warnIfTakingTooLongThreshold?: number | null,
isDoneOn?: IWaitForObservableIsDoneOn<T> | null,
stackTrace?: string | null,
): Observable<T>;
wrapMacroTask<T>(
/** Label the task for debugging purposes */
label: string,
/** The observable or promise to watch */
request: Promise<T> | Observable<T>,
/** Warn us if the request takes too long. Set to 0 to disable */
warnIfTakingTooLongThreshold?: number | null,
/** When do we know the request is done */
isDoneOn?: IWaitForObservableIsDoneOn<T> | null,
/** Stack trace to log if the task takes too long */
stackTrace?: string | null,
): Promise<T> | Observable<T>
if (request instanceof Promise)
return this.wrapMacroTaskPromise(label, request, warnIfTakingTooLongThreshold, stackTrace);
else if (request instanceof Observable)
return this.wrapMacroTaskObservable(label, request, warnIfTakingTooLongThreshold, isDoneOn, stackTrace);
// Backup type check
if ("then" in request && typeof (request as any).then === "function")
return this.wrapMacroTaskPromise(label, request, warnIfTakingTooLongThreshold, stackTrace);
else
return this.wrapMacroTaskObservable(label, request as Observable<T>, warnIfTakingTooLongThreshold, isDoneOn, stackTrace);
/**
* Starts a Macro Task for a promise
*/
async wrapMacroTaskPromise<T>(
/** Label the task for debugging purposes */
label: string,
/** The Promise to watch */
request: Promise<T>,
/** Warn us if the request takes too long. Set to 0 to disable */
warnIfTakingTooLongThreshold?: number | null,
/** Stack trace to log if the task takes too long */
stackTrace?: string | null,
): Promise<T>
// Initialize warnIfTakingTooLongThreshold
if (typeof warnIfTakingTooLongThreshold !== "number")
warnIfTakingTooLongThreshold = this.wrapMacroTaskTooLongWarningThreshold;
// Start timer for warning
let hasTakenTooLong = false;
let takingTooLongTimeout: any = null;
if (warnIfTakingTooLongThreshold! > 0 && takingTooLongTimeout == null)
takingTooLongTimeout = setTimeout(() =>
hasTakenTooLong = true;
clearTimeout(takingTooLongTimeout);
takingTooLongTimeout = null;
console.warn(
`wrapMacroTaskPromise: Promise is taking too long to complete. Longer than $warnIfTakingTooLongThresholdms.`,
);
console.warn("Task Label: ", label);
if (stackTrace)
console.warn("Task Stack Trace: ", stackTrace);
, warnIfTakingTooLongThreshold!);
// Start the task
const task: MacroTask = Zone.current.scheduleMacroTask("wrapMacroTaskPromise", () => , , () => , () => );
this.macroTaskStarted();
// Prepare function for ending the task
const endTask = () =>
task.invoke();
this.macroTaskEnded();
// Kill the warning timer
if (takingTooLongTimeout != null)
clearTimeout(takingTooLongTimeout);
takingTooLongTimeout = null;
if (hasTakenTooLong)
console.warn("Long Running Macro Task is Finally Complete: ", label);
;
// Await the promise
try
const result = await request;
endTask();
return result;
catch (ex)
endTask();
throw ex;
/**
* Starts a Macro Task for an observable
*/
wrapMacroTaskObservable<T>(
/** Label the task for debugging purposes */
label: string,
/** The observable to watch */
request: Observable<T>,
/** Warn us if the request takes too long. Set to 0 to disable */
warnIfTakingTooLongThreshold?: number | null,
/** When do we know the request is done */
isDoneOn?: IWaitForObservableIsDoneOn<T> | null,
/** Stack trace to log if the task takes too long */
stackTrace?: string | null,
): Observable<T>
return this._wrapMacroTaskObservable(label, request, warnIfTakingTooLongThreshold, isDoneOn, true, stackTrace);
protected _wrapMacroTaskObservable<T>(
label: string,
request: Observable<T>,
warnIfTakingTooLongThreshold?: number | null,
isDoneOn?: IWaitForObservableIsDoneOn<T> | null,
isCounted: boolean = true,
stackTrace?: string | null,
): Observable<T>
return of(null).pipe(
switchMap(() =>
let counts = 0;
// Determine emitPredicate
let emitPredicate: (d: T) => boolean;
if (isDoneOn == null || isDoneOn === "complete")
emitPredicate = alwaysFalse;
else if (isDoneOn === "first-emit")
emitPredicate = makeEmitCountPredicate(1);
else if ("emitCount" in isDoneOn)
emitPredicate = makeEmitCountPredicate(isDoneOn.emitCount);
else if ("emitPredicate" in isDoneOn)
emitPredicate = isDoneOn.emitPredicate;
else
console.warn("wrapMacroTaskObservable: Invalid isDoneOn value given. Defaulting to 'complete'.", isDoneOn);
emitPredicate = alwaysFalse;
// Initialize warnIfTakingTooLongThreshold
if (typeof warnIfTakingTooLongThreshold !== "number")
warnIfTakingTooLongThreshold = this.wrapMacroTaskTooLongWarningThreshold;
/** When task is null it means it hasn't been scheduled */
let task: MacroTask | null = null;
let takingTooLongTimeout: any = null;
let hasTakenTooLong = false;
/** Function to call when we have determined the request is complete */
const endTask = () =>
if (task != null)
task.invoke();
task = null;
if (hasTakenTooLong)
console.warn("Long Running Macro Task is Finally Complete: ", label);
this.macroTaskEnded(counts);
counts = 0;
// Kill the warning timer
if (takingTooLongTimeout != null)
clearTimeout(takingTooLongTimeout);
takingTooLongTimeout = null;
;
/** Used if the task is cancelled */
const unsubSubject = new Subject();
function unsub()
unsubSubject.next();
unsubSubject.complete();
return of(null)
.pipe(
tap(() =>
// Start the task if one hasn't started yet
if (task == null)
task = Zone.current.scheduleMacroTask("wrapMacroTaskObservable", () => , , () => , unsub);
if (isCounted)
this.macroTaskStarted();
counts++;
// Start timer for warning
if (warnIfTakingTooLongThreshold! > 0 && takingTooLongTimeout == null)
takingTooLongTimeout = setTimeout(() =>
hasTakenTooLong = true;
clearTimeout(takingTooLongTimeout);
takingTooLongTimeout = null;
console.warn(
`wrapMacroTaskObservable: Observable is taking too long to complete. Longer than $warnIfTakingTooLongThresholdms.`,
);
console.warn("Task Label: ", label);
if (stackTrace)
console.warn("Task Stack Trace: ", stackTrace);
, warnIfTakingTooLongThreshold!);
),
)
.pipe(switchMap(() => request.pipe(takeUntil(unsubSubject))))
.pipe(
tap(v =>
if (task != null)
if (emitPredicate(v))
endTask();
),
)
.pipe(
finalize(() =>
endTask();
unsubSubject.complete();
),
);
),
);
protected macroTaskCount = new BehaviorSubject(0);
protected macroTaskStarted(counts: number = 1)
const nextTaskCount = this.macroTaskCount.value + counts;
this.macroTaskCount.next(nextTaskCount);
// console.log("Macro Task Count + ", counts, " = ", nextTaskCount);
protected macroTaskEnded(counts: number = 1)
const nextTaskCount = this.macroTaskCount.value - counts;
this.macroTaskCount.next(nextTaskCount);
// console.log("Macro Task Count - ", counts, " = ", nextTaskCount);
export type IWaitForObservableIsDoneOn<T = any> =
| "complete"
| "first-emit"
| emitCount: number
| emitPredicate: (d: T) => boolean ;
// Utilities:
function makeEmitCountPredicate(emitCount: number)
let count = 0;
return () =>
count++;
return count >= emitCount;
;
function alwaysFalse()
return false;
【讨论】:
我尝试了一切,这是唯一对我有用的解决方案。使用角度 10 和 s-s-r。相当冗长的解决方案,我想知道是否有更优雅的方式来控制渲染......不敢相信如此必要的事情必须如此复杂。【参考方案2】:我创建了一个服务,用于使用muradm 代码进行异步 API 调用。
Gist link.
import Injectable from '@angular/core';
import Observable, Observer, Subscription from 'rxjs';
@Injectable(
providedIn: 'root'
)
export class AsyncApiCallHelperService
taskProcessor: MyAsyncTaskProcessor;
constructor()
this.taskProcessor = new MyAsyncTaskProcessor();
doTask<T>(promise: Promise<T>)
return <Observable<T>> this.taskProcessor.doTask(promise);
declare const Zone: any;
export abstract class ZoneMacroTaskWrapper<S, R>
wrap(request: S): Observable<R>
return new Observable((observer: Observer<R>) =>
let task;
let scheduled = false;
let sub: Subscription|null = null;
let savedResult: any = null;
let savedError: any = null;
// tslint:disable-next-line:no-shadowed-variable
const scheduleTask = (_task: any) =>
task = _task;
scheduled = true;
const delegate = this.delegate(request);
sub = delegate.subscribe(
res => savedResult = res,
err =>
if (!scheduled)
throw new Error(
'An http observable was completed twice. This shouldn\'t happen, please file a bug.');
savedError = err;
scheduled = false;
task.invoke();
,
() =>
if (!scheduled)
throw new Error(
'An http observable was completed twice. This shouldn\'t happen, please file a bug.');
scheduled = false;
task.invoke();
);
;
// tslint:disable-next-line:no-shadowed-variable
const cancelTask = (_task: any) =>
if (!scheduled)
return;
scheduled = false;
if (sub)
sub.unsubscribe();
sub = null;
;
const onComplete = () =>
if (savedError !== null)
observer.error(savedError);
else
observer.next(savedResult);
observer.complete();
;
// MockBackend for Http is synchronous, which means that if scheduleTask is by
// scheduleMacroTask, the request will hit MockBackend and the response will be
// sent, causing task.invoke() to be called.
const _task = Zone.current.scheduleMacroTask(
'ZoneMacroTaskWrapper.subscribe', onComplete, , () => null, cancelTask);
scheduleTask(_task);
return () =>
if (scheduled && task)
task.zone.cancelTask(task);
scheduled = false;
if (sub)
sub.unsubscribe();
sub = null;
;
);
protected abstract delegate(request: S): Observable<R>;
export class MyAsyncTaskProcessor extends
ZoneMacroTaskWrapper<Promise<any>, any>
constructor() super();
// your public task invocation method signature
doTask(request: Promise<any>): Observable<any>
// call via ZoneMacroTaskWrapper
return this.wrap(request);
// delegated raw implementation that will be called by ZoneMacroTaskWrapper
protected delegate(request: Promise<any>): Observable<any>
return new Observable<any>((observer: Observer<any>) =>
// calling observer.next / complete / error
request
.then(result =>
observer.next(result);
observer.complete();
).catch(error => observer.error(error));
);
我希望这对某人有所帮助。
【讨论】:
这也对我有用,谢谢,让 Angular 在渲染之前等待非 Angular 异步调用完成(我正在使用他们的 SDK 调用 AWS DynamoDB)。 Gist 链接中的其中一个 cmets 显示了如何注入和使用此帮助程序类。【参考方案3】:最后,解决方案是将外部 API 异步调用安排为宏任务。 issue 中的解释有所帮助。为外部 API 异步调用实现 ZoneMacroTaskWrapper
之类的辅助包装类,渲染过程是否等待外部承诺。
目前,ZoneMacroTaskWrapper
未向公共 API 公开。但提供文档的承诺存在争议。
为了说明目的,猴子打字示例:
export class MyAsyncTaskProcessor extends
ZoneMacroTaskWrapper<MyRequest, MyResult>
constructor() super();
// your public task invocation method signature
doTask(request: MyRequest): Observable<MyResult>
// call via ZoneMacroTaskWrapper
return this.wrap(request);
// delegated raw implementation that will be called by ZoneMacroTaskWrapper
protected delegate(request: MyRequest): Observable<MyResult>
return new Observable<MyResult>((observer: Observer<MyResult>) =>
// calling observer.next / complete / error
new Promise((resolve, error) =>
// do something async
).then(result =>
observer.next(result);
observer.complete();
).catch(error => observer.error(error));
);
【讨论】:
@muradam 你能用一些代码或链接功能解释一下吗? 我远离我的开发环境。检查ZoneClientBackend
实现。基本上它扩展了ZoneMacroTaskWrapper
,它具有受保护的抽象方法delegate
。在委托中,您执行异步代码。当用户调用handle
时,ZoneMacroTaskWrapper
将做必要的事情并调用您的delegate
。 ZoneClientBackend
与 ZoneMacroTaskWrapper
在同一个文件中。
包装器本身使用S
(输入)和R
(输出)进行参数化。所以你几乎可以用它做任何事情,不仅仅是 http。
@AbdulHameed 上面解释过
@AbdulHameed,添加粗略示例以说明用法【参考方案4】:
我只是直接使用Zone:
在你的组件中声明 Zone 变量:
declare const Zone: any;
创建宏任务。
const t = Zone.current.scheduleMacroTask (
i.reference, () => , , () => , () =>
);
进行 http 异步调用。在响应回调/承诺中让宏任务知道它的完成:
t.invoke();
以上是解决方案的最简单形式。您显然需要处理错误和超时。
【讨论】:
什么是i.reference
?
像魅力一样工作。值得注意的是,无需为每个异步调用创建任务。创建一个在所有异步调用完成后调用的就足够了。
@ShyAgam 它是一个用作标识符的字符串。见blog.bitsrc.io/…
确切的解决方案见here【参考方案5】:
我已经创建了一个适合我需要的解决方案。也许它对我们俩都有帮助:
const obs = new Observable<Item<any>>(subscriber =>
this.thirdPartyService.getItem(itemId).then((item) =>
subscriber.next(item);
subscriber.complete();
return item;
);
);
return obs.map(item => item.data);
【讨论】:
以上是关于服务器在渲染之前不会等到 http 调用完成 - 角度 4 服务器端渲染的主要内容,如果未能解决你的问题,请参考以下文章