Angular ngOnInit 中的Async/Await使用 TypeScript 装饰器

很多时候,需要在页面加载或类初始化之前使用 API 的 Promises 加载数据。

为了实现这一点,我看到我的许多开发人员在 ngOnInit 上使用 async ,因此他们可以在数据获取 API 方法上使用 await

async ngOnit {
  this.movies = await this.service.getMovies();
}

但是如果你仔细看,没有人在等待 ngOnInit,即使你想也不能 await ngOnInit

它将运行 async 函数,但它不会等待它完成,它只允许我们使用 await 关键字,但它不是 aysnc 函数,尽管有 async 关键字。

它看起来有点尴尬,有点难以理解,似乎不合常规。

理想情况下,方法是使用路由解析器,以便在路由完成导航之前加载数据,并且我可以在加载视图之前知道数据可用。

许多 Stack Overflow 的答案都指向了一种更易读的方法,无需向 ngOnInit 添加 async 关键字,但解决方案的行为相同。

ngOnInit() {
  (async () => {
    this.movies = await this.service.getMovies();
  });
}

这不仅适用于 Angular 组件。 想象一个简单的类,它加载了一些数据并且还具有该数据的 getter 和 setter。

class MovieService {
  private movies: Movie[];
  constructor(){
    this.loadMovies();
  }
  getMovieById(id: number) {
    return this.movies.find(movie => movie.id === id);
  }
  getAllMovies() {
    return this.movies;
  }
  private async loadMovies(){
    this.movies = await MovieStore.getTodos();
  }
}

在上面的例子中,数据将在类的实例创建后立即开始加载,并且在完全获取数据之前不能阻塞构造函数。 因此,如果在请求仍处于挂起状态时调用 getter,它可能会以默认的空或未定义的电影对象结束。

因此,我们只能希望在调用任何 getter 时加载数据。 转过来就是添加一些延迟加载并让访问器等待加载请求,如下所示。

async getMoviesById(id: number){
  if(!this.movies){
    await this.loadMovies();
  }
  return this.movies.find(movie => movie.id === id);
}

看起来不错,但是现在这段代码变成了重复的代码,并且加载请求可能会被触发不止一次,前提是我们可以找到一种方法来用其他东西抽象所有这些连接。

TypeScript 装饰器

装饰器是一种特殊的声明,可以附加到类声明、方法、访问器、属性或参数上。

简洁明了,装饰器是一个函数,它接受另一个函数并扩展后者函数的行为,而无需显式修改它。

抽象接线的一个好方法是在我们可以等待调用所有方法的地方使用装饰器,这些方法首先要运行和完成,然后运行依赖于这些方法的方法。 如下所示:

class MoviesService {
    private movies: Movies[];

    @waitForInit
    async getMoviesById(id: number) {
        return this.movies.find(user => user.id === id);
    }

    @waitForInit
    async getAllMovies() {
        return this.movies;
    }

    @init
    private async loadMovies() {
        this.users = await myMoviesStore.getUsers();
    }
}

以下是如何使这些装饰器工作。 我们可以将每个装饰器添加到任意数量的方法中。

const INIT_METHODS = new Map<any, string[]>();

type InitMethodDescriptor = TypedPropertyDescriptor<() => void>
        | TypedPropertyDescriptor<() => Promise<void>>;

export function init(target: any, key: string, _descriptor: InitMethodDescriptor) {
    if (!INIT_METHODS.has(target)) {
        INIT_METHODS.set(target, []);
    }
    INIT_METHODS.get(target)!.push(key);
}

const INIT_PROMISE_SYMBOL = Symbol.for("init_promise");

export function waitForInit(target: any, _key: string, descriptor: PropertyDescriptor) {
    const method = descriptor.value!;
    descriptor.value = function(...args: any[]) {
        if (!Object.getOwnPropertySymbols(this).includes(INIT_PROMISE_SYMBOL)) {
            if (!INIT_METHODS.has(target)) {
                this[INIT_PROMISE_SYMBOL] = Promise.resolve();
            } else {
                const promises = INIT_METHODS.get(target)!.map(methodname => {
                    return Promise.resolve(this[methodname]());
                });
                this[INIT_PROMISE_SYMBOL] = Promise.all(promises);
            }
        }
        return this[INIT_PROMISE_SYMBOL].then(() => method.apply(this, args));
    };
    return descriptor;
}

给刚接触装饰器的人的代码解释

要了解 init/waitForInit 装饰器代码,我们首先必须了解装饰器是如何计算的。

让我们以下面的例子来理解:

function first() {
  console.log("first(): factory evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("first(): called");
  };
}
 
function second() {
  console.log("second(): factory evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("second(): called");
  };
}
 
class ExampleClass {
  @first()
  @second()
  method() {}
}

和上面一样,我们所有的 init 装饰方法都将首先被评估,在 MoviesService 案例中,loadMovies 将被注册为 INIT_METHODS 之一,然后当任何一个用 waitForInit 装饰的方法(如 getAllMovies)被调用时,它首先会被调用 先运行 loadMovies Promise,再运行对应的调用函数。

不用担心,我们为大家提供保障。 以下是如何在 ngOnInit 上使用相同的 init/waitForInit 装饰器。

这是在 ngOnInit 上演示上述装饰器的 Stackblitz 示例。

import { Component } from '@angular/core';
import { init, waitForInit } from './init';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  movies: any[];
  @init
  private async loadMovies() {
    this.movies = await Promise.resolve([
      'Toy Story',
      'Bahubali',
      'Terminator',
      'Iron Man',
    ]);
  }

  @waitForInit
  ngOnInit() {
    console.log('Initializing view template with pre-loaded data', this.movies);
  }
}

最后,我们简要讨论了使用 TypeScript 装饰器在 Angular 中的 ngOnInit 中预加载数据或在模板初始化之前在任何类中预加载数据的实现。

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » Angular ngOnInit 中的Async/Await使用 TypeScript 装饰器