发布于 2026-01-06 4 阅读
0

Angular - 神游!

Angular - 神游!

简要指南,帮助您理解 ZoneJs 并解决问题。


不如就不一开始就解释主题了,直接讲个故事吧。好吧,事情是这样的——那是一个普通的上班早晨。我像往常一样一边喝着咖啡,一边努力思考如何解决一个问题。这个问题是实现一个进度条,用来跟踪页面加载完成前的所有 API 调用(页面当然要加载 API 数据!)。由于这个应用要处理海量数据,所以这个加载器需要能够跟踪大量的 API 调用。“这能有多难?”我心想。但这个一开始看起来微不足道的问题,后来却变成了一场噩梦。

最初...


我当时对如何使用 Angular 的强大工具进行开发几乎一窍不通。所以,就像遇到其他问题一样,我开始寻找在 Angular 中实现这个功能的各种方法。我阅读了许多博客,也浏览了一些 Stack Overflow 上的帖子。但找到的几乎都没什么用。在当前情况下,它们似乎都不够高效。没有任何模块或库可以实现这个功能。我开始感到焦虑。但后来我想到了一个在我看来合理的解决方案,我又重新燃起了热情。

有时候最简单的解决方案是最好的,但有时候并非如此。


解决方案很简单。创建一个进度条组件,然后使用一个服务来触发进度条前进。就这么简单!

我首先创建了一个用于消息的模型类:

export class Message {
    id: string;
    message: string;
    active: boolean;
}
Enter fullscreen mode Exit fullscreen mode

模型准备就绪后,接下来我创建了进度加载组件,即 ProgressBarComponent:

// other imports 
import { Message } from '../../models/interfaces';

@Component({
  selector: 'progress-bar',
  templateUrl: './progress.bar.component.html',
  styleUrls: ['./progress.bar.component.scss']
})
export class ProgressBarComponent implements OnChanges {
  @Input() messages: Message[] = [];
  @Output() loadingEmitter = new EventEmitter<boolean>();

  constructor() { }

  public activeMessage: Message = { id: '', message: '', active: false };
  public progressCount = 0;

  ngOnChanges() {
   /* Code to check progress count and set the active message on the loader */ 

   /* Actual code removed for the sake of brevity. */
  }
}
Enter fullscreen mode Exit fullscreen mode

以及触发活动消息的服务,即:

// other imports
import { Message } from '../../../../models/interfaces';

@Injectable({
  providedIn: 'root'
})
export class LoadTrackerService {

  constructor() {}

  public loaderMessages: Message[] = [
    { id : 'm_id_1', message: 'Load Started,API 1 called', active: true },
    { id : 'm_id_2', message: 'API 2 called', active: false },
    { id : 'm_id_3', message: 'API 3 called', active: false },
    { id : 'm_id_4', message: 'API 4 called', active: false }
    { id : 'm_id_5', message: 'API 5 called, Load Complete', active: false }
  ];

  public loadingPercent: number;
  public loading = true;
  public messageSubject = new BehaviorSubject<Message[]>(this.loaderMessages);

  setMessage(messageId: string) {
    if (this.activateMessage(messageId)) {
      this.messageSubject.next(this.loaderMessages);
    }
  }

  activateMessage(messageId: string): Boolean {
     /* Code to activate message on the loader and return boolean on 
        activation*/ 

     /* Actual code removed for the sake of brevity. */
  }
}
Enter fullscreen mode Exit fullscreen mode

现在,messageSubject将由 APIService(所有 API 调用都通过该服务发起)触发,并由加载器组件订阅以检索活动消息并递增加载器。一切都说得通,直到我意识到真正的问题所在。

开局不利


我很快意识到没有办法跟踪 API 调用,我所能做的就是在 APIService 的每个方法中触发 LoadTrackerService,如下所示:


@Injectable({
  providedIn: 'root'
})
export class APIService {

  constructor(
   private loadTracker: LoadTrackerService) {}

  api_call_1() {
    /* Http call for API 1*/
    this.loadTracker.setMessage('m_id_1');
  }

   api_call_2() {
    /* Http call for API 2*/
    this.loadTracker.setMessage('m_id_2');
  }

  api_call_3() {
    /* Http call for API 3*/
    this.loadTracker.setMessage('m_id_3');
  }

  // and so on...

}
Enter fullscreen mode Exit fullscreen mode

上述方案当然适用于 API 调用次数不多的情况,但在实际应用中,如果 API 调用次数达到数百次,这种方法会使代码变得冗杂且重复。我需要一种更好、更简洁的方案。

进入安全区(js) ;)


经过大量的研究和阅读各种关于 Angular 的深度文章后,我偶然发现了这篇文章。原来 Angular 会在一个叫做Zone的机制中处理和跟踪所有的 API 调用。Zone 是处理逻辑上相互关联的异步调用的机制。Angular(或者更准确地说是 ZoneJS)将其称为微任务。现在,如何利用这个神奇的功能就变得非常清晰了。

我首先通过 fork Angular 默认区域创建了一个新的区域,并将其命名为 trackerZone。务必将此区域逻辑放在解析器(Angular 路由解析器)中,以便在跳转到实际页面之前触发并解析它。


import { Injectable, NgZone } from '@angular/core';
// other imports...

@Injectable()
export class ProjectResolver implements Resolve<any> {
  constructor(
    private ngZone: NgZone,
    private loadTracker: LoadTrackerService,
  ) { }

  public trackerZone: NgZone;

  resolve() {
    return this.resolveInTrackerZone();
  }

  resolveInTrackerZone() {
    this.trackerZone = this.ngZone['_inner'].fork({
      properties: {
        countSchedule: 0,
        loaderRef: this.loadTracker
      },
      onScheduleTask(delegate, currentZone, targetZone, task) 
      {}   
    });

Enter fullscreen mode Exit fullscreen mode

让我快速解释一下这里发生了什么。要访问默认的 Angular Zone,我们可以从 'angular/core' 导入它。所以我把它实例化到一个名为 ngZone 的私有变量中,这样我们之后就可以用它来 fork 实例了。接下来,我创建了自己的trackerZone
现在我们可以 fork 这个 Zone 实例,并将其赋值给我们的 trackerZone。

现在我们可以将属性/值/引用传递给属性对象中的 trackerZone。同时,我们还会获得一个onScheduleTask回调方法,该方法会在每次任务触发时执行。值得一提的是,除了微任务之外,还有其他类型的任务,我们这里就不赘述了,但它们同样重要。如果您想更深入地了解,我强烈推荐这篇博客。任务是一个包含各种属性的对象,例如类型、数据等(如下所示)。

下一步是使用 trackerZone.run() 方法在跟踪器区域内运行所有 API 调用。只需完成这些步骤,Angular 就能触发 Zone 功能并生成微任务。



/
import { Injectable, NgZone } from '@angular/core';
// other imports...

@Injectable()
export class ProjectResolver implements Resolve<any> {
  constructor(
    private ngZone: NgZone,
    private loadTracker: LoadTrackerService,
  ) { }

  public trackerZone: NgZone;

  resolve() {
    return this.resolveInTrackerZone();
  }

  resolveInTrackerZone() {
    this.trackerZone = this.ngZone['_inner'].fork({
      properties: {
        countSchedule: 0,
        loaderRef: this.loadTracker
      },
      onScheduleTask(delegate, currentZone, targetZone, task) 
      {
        const result = delegate.scheduleTask(targetZone, 
        task);
        const url = task['data']['url'] || '';
        const tracker = this.properties.loaderRef;

        if (task.type === 'macroTask' && task._state !== 
        'unknown') {
           /* Triggering the message service based on URL */
        }
        return result;
      }
      }   
    });

    this.trackerZone.run(() => {
      /* return Observable / API call / Parallel Calls*/
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

这里我们使用delegate.scheduleTask(targetZone, task)在 trackerZone 中手动安排了任务。现在我们只需要将 URL 与消息 ID 映射起来,然后触发服务即可。


if (task.type === 'macroTask' && task._state !== 'unknown') {
          this.properties.countSchedule += 1;
          if (url.indexOf('id_1') > -1) {
            tracker.setMessage('m_id_1');
          } else if (url.indexOf('id_2') > -1) {
            tracker.setMessage('m_id_2');
          } else if (url.indexOf('id_3') > -1) {
            tracker.setMessage('id_3');
          }

          // and so on...
        }

Enter fullscreen mode Exit fullscreen mode

就这些了!我真的很喜欢 ZoneJS 让整个过程变得如此简单便捷。当然,为了确保万无一失,使用 HTTP 拦截器也是一种方法,但我感觉 ZoneJS 更优雅、更直观。这只是我个人的看法。


最后,这是我的第一篇博客文章。请不要因为我写作新手而嫌弃我,我会努力进步的。如果您喜欢这篇文章,请在下方评论区留下一些鼓励的话语。

和平🖖


文章来源:https://dev.to/suvi/angular-zoned-out-hi0