服务器在渲染之前不会等到 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 将做必要的事情并调用您的delegateZoneClientBackendZoneMacroTaskWrapper 在同一个文件中。 包装器本身使用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 服务器端渲染的主要内容,如果未能解决你的问题,请参考以下文章

命名管道不会等到在 bash 中完成

组件在 NgOnInit 完成之前渲染?

等到三个 ajax 调用解决触发函数(延迟?)

Vue:等到父函数完成运行

在 vue.js 中渲染子组件之前等到父组件安装/准备好

html “CSS被视为渲染阻止资源,这意味着浏览器在完成加载之前不会渲染任何内容。所以这就是