服务器在渲染之前不等待http调用完成 - Angular 4服务器端渲染

11

我已经开始实施Angular Universal,并能够通过服务器端渲染呈现HTML的静态部分。我面临的问题是,在等待HTTP调用完成之前,API调用正在进行,服务器正在渲染HTML。因此,我的模板依赖于从API调用获取的数据的部分没有在服务器上呈现。

更多信息:

我在Node服务器中使用身份验证,只有在用户经过身份验证并在响应中设置Cookie时才提供索引HTML文件。

每当我从Angular发出API调用时,我还将Cookie作为标头发送,因为依赖服务也会使用令牌验证用户。对于服务器端渲染,由于Cookie在服务器级别上不可用,因此我已成功注入请求并获取了API调用的Cookie。因此,API调用是成功的,但服务器没有等待Promise解析就进行渲染。

我尝试过的步骤但没有成功:

我根据此评论中建议的方式更改了我的区域版本 https://github.com/angular/universal-starter/issues/181#issuecomment-250177280

如果需要任何进一步的信息,请告诉我。

指向包含HTTP调用的Angular Universal样板文件将有所帮助。


有任何消息吗?似乎是一个重大问题。 - Toolkit
5个回答

5
我已经使用muradm的代码创建了一个用于执行异步API调用的服务。 Gist链接。
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异步调用完成(我正在使用AWS DynamoDB调用其SDK),可以使用此方法。 Gist链接中的评论之一显示了如何注入和使用此辅助类。 - rangfu

4

我直接使用了Zone:

在你的组件中声明Zone变量:

declare const Zone: any;

创建宏任务。
const t = Zone.current.scheduleMacroTask (
  i.reference, () => {}, {}, () => {}, () => {}
);

进行您的http异步调用。在响应回调/承诺中通知宏任务已经完成:

t.invoke();

以上是解决方案的最简形式。显然,您需要处理错误和超时。


11
i.reference是什么? - Shy Agam
运行得非常好。值得注意的是,没有必要为每个异步调用创建一个任务。只需要创建一个任务,在所有异步调用完成后调用它一次即可。 - Shy Agam
@ShyAgam它是一个用作标识符的字符串。请参见https://blog.bitsrc.io/how-angular-uses-ngzone-zone-js-for-dirty-checking-faa12f98cd49。 - pubkey
要获取精确的解决方案,请查看此处 - Jonathan

3

最终的解决方案是将外部API异步调用作为宏任务进行调度。这个问题中的解释很有帮助。实现ZoneMacroTaskWrapper作为外部API异步调用的辅助包装类,使得渲染过程等待外部承诺。

目前,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,你能否用一些代码或链接函数来解释一下吗? - Abdul Hameed
我离开了我的开发环境。请检查ZoneClientBackend的实现。基本上它扩展了ZoneMacroTaskWrapper,该类具有受保护的抽象方法delegate。在delegate中,您可以编写异步代码。当用户调用handle时,ZoneMacroTaskWrapper将执行必要的操作并调用您的delegateZoneClientBackendZoneMacroTaskWrapper在同一个文件中。 - muradm
包装器本身是使用 S(输入)和 R(输出)进行参数化的。因此,您可以对其执行几乎任何操作,而不仅仅是http。 - muradm
@AbdulHameed 上面已经解释过了。 - muradm
@AbdulHameed,这里提供一个简单的示例来说明用法。 - muradm
太好了!我正在寻找同样的东西,今天完成当前任务后一定会去查看 (y) - Abdul Hameed

3
我对之前的解决方案有一些问题和顾虑。以下是我的解决方案:
  • 适用于 Promises 和 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://dev59.com/X1YN5IYBdhLWcg3wopUD#54345373
*
* 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;
}

3
我尝试了所有方法,这是唯一对我有效的解决方案。使用 Angular 10 和 SSR。解决方案相当冗长,我想知道是否有更简洁的方式来控制渲染……难以置信这么必要的东西居然如此复杂。 - Blasco
感谢@sparebytes的分享。我正在尝试您的解决方案,听起来不错,但是我在使用它时遇到了困难。您是否有一个使用它的工作项目?我需要更多的解释,因为我遇到了HttpErrorResponse :(。谢谢! - Raphaël Roux

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); 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接