【问题标题】:Angular 12 + Ionic 5: Resolver does not wait for the Storage and HTTP calls to finishAngular 12 + Ionic 5:解析器不等待存储和 HTTP 调用完成
【发布时间】:2022-01-16 23:11:11
【问题描述】:

情况:

这个问题是关于我正在使用 Angular 12 和 Ionic 5 构建的 SPA。当我在主页上时,我可以单击侧菜单中的“订单历史”链接,这会将我引导到订单历史页面.我正在使用解析器,以便在路由完成之前从数据库中获取订单历史记录,以便在路由完成时,用户可以看到数据,因为它可以通过解析器轻松获得。在此解析器中,执行了 2 个主要操作(严格按顺序)。它们是:

  1. 从 Ionic Storage 接收当前登录的用户 ID。

  2. 使用从上述步骤接收到的当前登录用户 ID,并向后端发出 HTTP 调用以获取与用户相关的订单。只有在 HTTP 调用成功完成后,导航到“订单历史”页面并将 HTTP 调用数据记录到控制台。

问题:

当我点击侧边菜单中的“订单历史记录”链接时,解析器运行,从存储中获取当前登录的用户 ID,但它等待 HTTP 调用完成.相反,它只是简单地路由到 Order History 页面,然后执行 HTTP 请求,然后给我 HTTP 请求的结果。但这违背了解析器的目的! Resolver 应该等待所有调用完成,然后导航到目标页面,但相反,它导航到目标页面,然后完成 API 调用并提供数据。 我正在尝试解决此问题,以便解析器在实际路由发生之前执行上述 2 个主要操作。

这是我的代码:

app-routing.module.ts:

import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
import { GetOrderHistoryResolver } from "@shared/resolvers/get-order-history/get-order-history.resolver";

const routes: Routes = [
  {
    path: 'order-history',
    resolve: {
      resolvedData: GetOrderHistoryResolver,
    },
    loadChildren: () => import('./order-history/order-history.module').then( m => m.OrderHistoryPageModule)
  },  
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule],
  providers: []
})
export class AppRoutingModule { }

get-order-history.resolver.ts

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { OrdersService } from "../../services/orders/orders.service";
import { AuthenticationService } from "@core/authentication/authentication.service";
import { Storage } from '@ionic/storage';

@Injectable({
  providedIn: 'root'
})
export class GetOrderHistoryResolver implements Resolve<any> {

  constructor(private router: Router,
              private storage: Storage,
              private authenticationService: AuthenticationService,
              private ordersService: OrdersService) {
  }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {

    return this.authenticationService.getUserId().then(currentUserId => {
      console.log(currentUserId); // This works correctly and logs the value as 5
      return this.ordersService.getOrdersByCustomer(currentUserId);
    });

  }
}

authentication.service.ts

getUserId() {
  return this.storage.get('user').then(user => {
    if (user) {
      // Make sure to parse the value from string to JSON object
      let userObj = JSON.parse(user);    
      return userObj.ID;
    }
  });
}

orders.service.ts

getOrdersByCustomer(userId): any {
  return this.http.get<any>(BASE_URL + '/orders?customer=' + userId )
}

order-history.page.ts

import { Component, OnInit } from '@angular/core';
import { OrdersService } from "@shared/services/orders/orders.service";
import { ActivatedRoute } from "@angular/router";
import { Storage } from '@ionic/storage';
import { AuthenticationService } from "@core/authentication/authentication.service";

@Component({
  selector: 'app-order-history',
  templateUrl: './order-history.page.html',
  styleUrls: ['./order-history.page.scss'],
})
export class OrderHistoryPage implements OnInit {

  constructor(private route: ActivatedRoute,
              private storage: Storage,
              private ordersService: OrdersService,
              private authenticationService: AuthenticationService) {
  }

  ngOnInit() {}

  ionViewWillEnter() {
    // If the Resolver is executed, then grab the data received from it
    if (this.route.snapshot.data.resolvedData) {
      this.route.snapshot.data.resolvedData.subscribe((response: any) => {
        console.log('PRODUCTS FETCHED FROM RESOLVE');
        console.log(response); // <-- Products are successfully logged here to console
      });
    } else {
      // Make a call to the API directly because the Resolve did not work
      this.getOrdersByCustomer();
    }
  }


  /**
   * Manual call to the API directly because the Resolve did not work
   * @returns {Promise<void>}
   */
  async getOrdersByCustomer() {
    // Wait to get the UserID from storage
    let currentCustomerId = await this.authenticationService.getUserId() ;

    // Once the UserID is retrieved from storage, get all the orders placed by this user
    if(currentCustomerId > 0) {
      this.ordersService.getOrdersByCustomer(currentCustomerId).subscribe((res: any) => {
        console.log(res);
      });
    }
  }

}

【问题讨论】:

  • 您是否尝试将 promise 转换为 observable 并将其与您的 HTTP 调用链接起来?
  • 我不知道该怎么做,因为我需要先从存储中获取当前登录用户的值,然后将该值发送到 HTTP 调用并让解析器等待来自 HTTP 的响应称呼。这就是我卡住的地方!

标签: angular angular-httpclient ionic5 angular-resolver ionic-storage


【解决方案1】:

您可以使用 defer from rxjs 将您的 promise 转换为 observable,然后将您的 observable 链接到管道中。

我不确定您是否可以使用from 代替defer,但defer 应该可以工作

resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    return  defer(() => this.authenticationService.getUserId())
                            .pipe(switchMap((currentUserId) => 
                                     this.ordersService.getOrdersByCustomer(currentUserId)));
  }

    

【讨论】:

  • 简单、直接、简洁。开箱即用,无需任何更改。非常感谢!!!
  • 不客气 :)
【解决方案2】:

我为您准备了一个演示,以了解如何在不使用 await 的情况下将第一个 promise 响应用于第二个响应,而不是在 RxJS 的同一链中,这可以保证一旦解析器解析了 observable,两者都已被评估:

https://stackblitz.com/edit/switchmap-2-promises?file=index.ts

关键部分在这里:

from(promise1())
  .pipe(
    tap((v) => console.log('Logging the 1st promise result', v)),
    // use above the first promise response for second promise call
    switchMap((v) => promise2(v)),
    tap((v) => console.log('Logging the 2st promise result', v))
  )
  .subscribe();

SwitchMap(以及其他高 obs 运算符)允许您将第一个 promise/observable 输出转换为链中的新输出。

【讨论】:

  • 感谢您的代码。我正在努力思考如何在您的代码中实现我的代码。关于什么/如何更改get-order-history.resolver.ts 文件中的代码以使其工作的任何指针?
  • 您只需将promise1 替换为第一个调用,将promise2 替换为第二个,并将“v”替换为currentUserId。无需订阅解析器,因为 resolve 方法会为您完成。
  • 非常感谢您的代码 sn-p。根据您的 sn-p,我确定我做错了什么,因为我无法让我的代码正常工作。我想接受你的回答和 Dusan 的回答,并将赏金平分给你们,但不幸的是,Stackoverflow 不允许我这样做。我不得不接受 Dusan 的回答作为解决方案,所以只是让我支持你的回答。我非常感谢您为提供解决方案所做的努力。再次感谢您!
【解决方案3】:

解决在内部向返回的 Promise/observable 添加处理程序。如果 获取数据后,它将路由到给定页面,否则不会。

在您的实现中,您将返回 Promise(离子存储),并且解析器在此 Promise 内部添加了处理程序,而不是您的 HTTP Observable。

这就是添加 2 个处理程序的原因。一个由您发起 HTTP 调用,另一个由解析器在内部添加。他们俩都被处决了。但是resolver只是在寻找this.authenticationService.getUserId()的resolved值,一旦得到user id就会路由到对应的页面。

解决方案: 使用 async/await 获取您的用户 ID,然后从解析器返回 HTTP observable。

async resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {

        const currentUserId=await this.authenticationService.getUserId();
        if(currentUserId){
            return this.ordersService.getOrdersByCustomer(currentUserId);
        }
        else{
            //Handle the scenario when you don't have user ID in storage
            // You can throw an error & add global error handler 
            // Or route to login / any other page according to your business needs
        }
       
      } 

现在,解析器将向返回的 HTTP 可观察对象添加处理程序并等待它从 BE 获取数据,然后再进行路由。

【讨论】:

  • 感谢您的回答。我尝试了您的解决方案,但没有解决问题。我还觉得它一定与用于检索当前登录用户 ID 的存储调用有关,但不知道如何解决这个问题。出于测试目的,我将用户 ID 硬编码为 5,然后解析器正常工作,即它一直等到它为 ID 为 5 的用户获取订单历史记录,然后根据需要路由到订单历史记录页面。通过对用户 ID 进行硬编码来测试代码证明需要对存储查询进行一些处理。因此,任何其他解决方案都值得赞赏。
  • 尝试在您的身份验证服务中添加 await getUserId() 方法也
  • 如您所述尝试添加等待。但还是一样的结果!这是添加 async/await 组合后的新代码:async getUserId() { return await this.storage.get('user').then(user =&gt; { if (user) { // Make sure to parse the value from string to JSON object let userObj = JSON.parse(user); return userObj.ID; } }); }
  • 我知道准备一个演示来重现它有时很麻烦,但值得肯定的是,我们可以与您分享解决方案。我认为这与其中有 2 个承诺有关,您可以尝试使用 from().pipe(switchMap()) 并处理链内的事情。
  • 我在回复中与您分享了一个链接和一个 sn-p @Devner
猜你喜欢
  • 1970-01-01
  • 2019-11-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-22
  • 2012-08-27
  • 2021-12-08
  • 1970-01-01
相关资源
最近更新 更多