我已经开始实施角度通用,并能够通过服务器端渲染渲染html的静态部分。我面临的问题是,正在进行API调用,服务器正在呈现html而不等待http调用完成。因此,我的模板依赖于从api调用获得的数据的部分不会在服务器上呈现。
进一步信息:
我在节点服务器中使用身份验证服务,只有在用户通过身份验证时才会为索引html提供身份验证,并在响应时设置cookie。
每当我从angular进行API调用时,我也会将cookie作为标头发送,因为依赖服务也会使用令牌验证用户。对于服务器端呈现,由于cookie在服务器级别不可用,我已成功注入请求并为API调用选择cookie。因此,API调用是成功的,但服务器不会等待呈现,直到承诺解析。
我尝试过的步骤没有成功:
我已根据此评论https://github.com/angular/universal-starter/issues/181#issuecomment-250177280
中的建议更改了我的区域版本如果需要进一步的信息,请告诉我。
将我引导到一个带有http调用的角度通用样板文件可以帮助我。
答案 0 :(得分:3)
最后,解决方案是将外部API异步调用安排为宏任务。 issue中的解释有所帮助。为外部API异步调用实现ZoneMacroTaskWrapper
类helper包装类,渲染过程等待外部promise。
目前,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));
});
}
}
答案 1 :(得分:2)
我直接使用Zone:
在组件中声明Zone变量:
declare const Zone: any;
创建一个宏任务。
const t = Zone.current.scheduleMacroTask (
i.reference, () => {}, {}, () => {}, () => {}
);
进行http异步调用。在响应回调/保证中,让宏任务知道它已完成:
t.invoke();
以上是最简单的解决方案。你显然需要处理错误和超时。
答案 2 :(得分:1)
我已经使用muradm代码创建了用于进行异步API调用的服务。
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));
});
}
}
我希望这对某人有帮助。
答案 3 :(得分:0)
我已经创建了一个符合我需求的解决方案。也许这对我们两个人都有帮助:
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);
答案 4 :(得分:0)
我对以前的解决方案有一些问题/担忧。这是我的解决方法
/// <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://stackoverflow.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 ${warnIfTakingTooLongThreshold}ms.`,
);
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 ${warnIfTakingTooLongThreshold}ms.`,
);
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;
}