简体   繁体   English

如何在访问范围变量之前正确地进行ajax调用,并在angular2中加载到模板中并缓存它?

[英]How to properly have to make an ajax call prior to accessing scope variable loaded into template in angular2 and cache it?

I have the following issue. 我有以下问题。 I have an issue with my component that is loaded after a route: 我的组件在路由后加载时遇到问题:

/myroute/:id

The problem is I have a function in it, that is used by the template to render: 问题是我有一个函数,模板使用它来渲染:

constructor(appState: AppState) {
}

ngOnInit() {
  // this retrieves an object that contains multiple ids (unless its already cached)
  if(!this.appState.state.cache || this.appState.state.id != <want to check against url parameter id to see if it had already been requested before/I alreadty have data for it. if I move this under the .subscribe in the block below, does this defeat the purpose of asynchronicity? should i be using observables instead somehow?>)
  this.asyncDataWithWebpack();

  // Purpose of this block is to just get the id from the route parameters
  this.route.params
    .subscribe((params: Params) => { 
          this.setSpecificDataRelated(params['id']);
     });

}

// this function pulls in the data that contains an object that 'id' can be matched to. I consider this a rest call to some data.
  private asyncDataWithWebpack() {
    setTimeout(() => {

      System.import('../../assets/mock-data/mock-data.json')
        .then((json) => {
          this.arrayOfDataWithManyIds = json;
        });

    });
  }


    setSpecificDataRelatedToId(id) {
       this.nestedArrayBasedOnOneId = this.arrayOfDataWithManyIds[id].nestedArray;
       this.appState.set("cache", this.arrayOfDataWithManyIds);
       this.appState.set("cacheId", Id);
    }

In my template I have: 在我的模板中,我有:

<div *ngFor="entry of nestedArrayBasedOnOneId">
  {{entry.name}}
</div>

I am getting errors I think because arrayOfDataWithManyIds does not exist because it is async. 我认为我得到错误因为arrayOfDataWithManyIds不存在,因为它是异步的。 What is the best way to handle this sort of situation? 处理这种情况的最佳方法是什么?

I want to make sure: 我想确定一下:

  1. Performance is good, it fetches the data, then make sure that I can access the setSpecificDataRelatedToId(id) function after data actually ocmes back. 性能很好,它获取数据,然后确保我可以在数据实际回复后访问setSpecificDataRelatedToId(id)函数。

  2. I don't want it such that every time I go to the route, it will have to keep requesting for data, instead, because it's pulled it in once, it should 'cache' and use that cached object without having to go back to the server all the time (unless the id changes). 我不希望这样每次我去路线时,它都必须继续请求数据,相反,因为它将其拉入一次,它应该“缓存”并使用该缓存对象而不必返回服务器始终(除非id更改)。

Right now I am using appState only global to application (no Rxjs or any stores), but am looking for the best way to go about handling this. 现在我只使用appState全局应用程序(没有Rxjs或任何商店),但我正在寻找处理这个问题的最佳方法。 Is this a problem that requires only observables can handle elegantly? 这是一个只需要观察者可以优雅处理的问题吗?

Am looking for a good solution/best practice to address this. 我正在寻找一个好的解决方案/最佳实践来解决这个问题。 I am worried about making bad code as I want to propogate this model throughout multiple components in my application. 我担心编写错误代码,因为我希望在我的应用程序中的多个组件中传播此模型。

Right now I am using appState only global to application (no Rxjs or any stores), but am looking for the best way to go about handling this. 现在我只使用appState全局应用程序(没有Rxjs或任何商店),但我正在寻找处理这个问题的最佳方法。 Is this a problem that requires only observables can handle elegantly? 这是一个只需要观察者可以优雅处理的问题吗?

1) You do not need any store to handle this (like ngrx with a Redux approach) here to achieve what you want. 1)你不需要任何商店来处理这个问题(比如使用Redux方法的ngrx)来实现你想要的。 (even tho it would play really well with it) . (即使它会很好地发挥它)

2) Yes, observables will help to handle this in a good way but as always, you don't have to use them. 2)是的,观测将有助于处理这个的一个好办法,但一如既往,你没有使用它们。 (you should definitely use observables tho, it's really awesome) . (你绝对应该使用observables,它真的很棒)

3) The problem here is more an architecture problem and I'll guide you step by step to have an optimized and well designed solution without having to cache anything . 3)这里的问题更多的是架构问题,我将逐步指导您拥有一个优化且设计良好的解决方案, 而无需缓存任何内容

The example I made to match (as close as I understand) your problem, is the following : 我所做的匹配(尽可能接近我的问题)的例子如下:

  • Display a list of person 显示人员列表
    在此输入图像描述
  • When you click on a person, fetch his details 当您点击某人时,请获取他的详细信息
    ( little bonus : Show a loading indicator while retrieving data ) 小奖励:检索数据时显示加载指示
    在此输入图像描述

  • Once the details are fetched, display them 获取详细信息后,显示它们
    ( notice that a user has an array of dogs in order to simulate your nested array ) 注意用户有一组狗来模拟你的嵌套数组
    在此输入图像描述

If you want to try a live demo, here's a Plunkr : 如果你想尝试一个现场演示,这里是一个Plunkr:
http://plnkr.co/edit/f1hxU5EWIxxskPqc93RV?p=preview http://plnkr.co/edit/f1hxU5EWIxxskPqc93RV?p=preview


Now let's get back to the code, shall we ? 现在让我们回到代码,好吗? (I'll explain piece by piece why and how I built the app) (我将逐一解释为什么以及如何构建应用程序)

First of all, let's gear up for our reactive application ! 首先,让我们为我们的反应应用做好准备吧! We have to add few RxJs operators that we'll be using. 我们必须添加一些我们将要使用的RxJs运算符。 Let's add them into src/main.ts : 让我们将它们添加到src/main.ts

import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/do';
import 'rxjs/add/observable/of';

If you're not familiar with RxJs yet, I'd recommend you to : 如果您还不熟悉RxJ,我建议您:
1) Read https://www.learnrxjs.io/operators 1)阅读https://www.learnrxjs.io/operators
2) Play with http://rxmarbles.com 2)与http://rxmarbles.com一起
3) Go to official documentation with marble diagrams : http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html 3)使用大理石图表转到官方文档: http//reactivex.io/rxjs/class/es6/Observable.js~Observable.html

Then, in your simple App component, define some people : 然后,在您的简单App组件中,定义一些人:

  public people = [
    {id: 'persId0', name: 'Person 0'},
    {id: 'persId1', name: 'Person 1'},
    {id: 'persId2', name: 'Person 2'}
  ];

In the template, show them with a link to /people/:id : 在模板中,使用指向/people/:id的链接显示它们:

  <div *ngFor="let person of people">
    <a [routerLink]="['/people', person.id]">{{ person.name }}</a>
  </div>

To make this work, you'll have to import the RouterModule from @angular/router and add it to AppModule , into the imports array. 要使其工作,您必须从@angular/router导入RouterModule并将其添加到AppModule ,进入imports数组。

Instead of simply importing the RouterModule, we'd like to define the route we were talking about : /people/:id . 我们不想简单地导入RouterModule,而是定义我们正在谈论的路线: /people/:id

So our imports array should now look like that : 所以我们的进口数组现在应该是这样的:

  imports: [
    BrowserModule,
    RouterModule.forRoot({
      path: 'people/:id',
      component: PersonComponent
    })
  ]

Yes ! 是的! We want to display a PersonComponent when we reach that route, but we haven't defined it yet and we'll get back to it in a sec. 我们想要在到达那条路线时显示一个PersonComponent ,但是我们还没有定义它,我们会在一秒钟内回到它。 But let's define our service to retrieve a person first : 但是我们先定义我们的服务来检索一个人:

src/people.service.ts : src/people.service.ts

import {Injectable} from '@angular/core'
import {Observable} from 'rxjs/Observable'

@Injectable()
export class PeopleService {
  // raw data to simulate your database on backend
  private people = {
    persId0: {
      id: 'persId0',
      name: 'Person 0', 
      weightKg: 60.8,
      heightCm: 183,
      dogs: [
        {id: 'dogId 0', name: 'Dog 0'},
        {id: 'dogId 1', name: 'Dog 1'}
      ]
    },
    persId1: {
      id: 'persId1',
      name: 'Person 1', 
      weightKg: 65.3,
      heightCm: 165,
      dogs: [
        {id: 'dogId 2', name: 'Dog 2'},
        {id: 'dogId 3', name: 'Dog 3'}
      ]
    },
    persId2: {
      id: 'persId2',
      name: 'Person 2', 
      weightKg: 77.6,
      heightCm: 192,
      dogs: [
        {id: 'dogId 2', name: 'Dog 2'},
        {id: 'dogId 2', name: 'Dog 2'}
      ]
    }
  };

  constructor() { }

  getPersonDetails(personId: string) {
    console.warn(`Calling the service to fetch the details a the person with ID : ${personId}`);

    // here, you may want to read your JSON file but avoid using setTimeout in such a case
    // and prefer Observable.of to wrap your data in a stream and use the delay operator to 
    // simulate network latency
    return Observable
      .of(this.people[personId])
      .delay(10000);
  }
}

As you can see, I mocked the network call by using Observable.of(...).delay() . 如您所见,我使用Observable.of(...).delay()Observable.of(...).delay()网络调用。 And here, you might just replace it in production with a real HTTP request. 在这里,您可以在生产中用真正的HTTP请求替换它。 But that's another topic. 但这是另一个话题。

Don't forget to declare the service into the AppModule with : providers: [PeopleService] . 不要忘了申报服务到AppModule有: providers: [PeopleService]

Now that we can fetch our person details, let's create a component to display them : 现在我们可以获取我们的人员详细信息,让我们创建一个组件来显示它们:
(enough comments in code so I won't have to add anything here) (代码中有足够的注释,所以我不必在这里添加任何内容)

src/person.component.ts : src/person.component.ts

import {Component, OnInit} from '@angular/core'
import {ActivatedRoute} from '@angular/router'
import {Observable} from 'rxjs/Observable'
import {PeopleService} from 'src/people.service'

@Component({
  selector: 'app-person',
  template: `
    <hr>

    <p *ngIf="isLoading">Loading ...</p>

    <!-- use the new syntax with "as" to save the observable result and re-use it accross the template -->
    <div *ngIf="personWithDetails$ | async as personWithDetails">
      <p>
        <b>{{ personWithDetails.name }}</b> is <b>{{ personWithDetails.heightCm }}</b>cm tall and weight <b>{{ personWithDetails.weightKg }}kg</b>
      </p>

      <div>
        <p>He/she also has {{ personWithDetails.dogs.length }} dogs :</p>
        <ul>
          <li *ngFor="let dog of personWithDetails.dogs">{{ dog.name }}</li>
        </ul>
      </div>
    </div>
  `
})
export class PersonComponent implements OnInit {
  // will be fetched from the service
  public personWithDetails$: Observable<any>;
  public isLoading: boolean;

  constructor(private route: ActivatedRoute, private peopleService: PeopleService) { }

  ngOnInit() {
    this.personWithDetails$ = this
      .route
      // every time the route params changes ...
      .params
      // pick only the id ...
      .map(params => params['id'])
      // and set a variable isLoading to true so we can display a spinner or some text into the view
      .do(x => this.isLoading = true)
      // use switchMap to auto-magically subscribe/unsubscribe from the service call
      .switchMap(personId => this
        .peopleService
        // this is the last function returning something and thus, it'll be that return saved in personWithDetails$
        .getPersonDetails(personId)
        // simply set the isLoading variable to false so we can hide/remove our spinner or text
        .do(x => this.isLoading = false));
  }
}

I think we've been through the whole app and I hope it was helpful. 我想我们已经完成了整个应用程序,我希望它有所帮助。 Also, few things to notice : 此外,很少有事情需要注意:

  • As we're using switchMap , previous not ended service calls are canceled and if you click on another person while it's still fetching the data, you'll not see intermediate response (as we're not interested in them anymore) 当我们使用switchMap ,之前未结束的服务调用被取消,如果您在仍在获取数据时点击另一个人,您将看不到中间响应(因为我们不再对它们感兴趣)
  • I put a console.warn into the service every time the service method getPersonDetails is called so you can see that we don't need any cache system here to avoid calling that method (unless the URL param :id changes) 每次调用服务方法getPersonDetails时,我都会将console.warn放入服务中,这样你就可以看到我们这里不需要任何缓存系统来避免调用该方法(除非URL参数:id更改)

Cheers ! 干杯!

How about this, nest your method after data is back. 怎么样,在数据恢复后嵌套你的方法。

ngOnInit() {
    let that = this;
    // this retrieves an object that contains multiple ids
    this.asyncDataWithWebpack(() => {
        // Purpose of this block is to just get the id from the route parameters
        that.route.params
            .subscribe((params: Params) => {
                that.setSpecificDataRelated(id);
            });
    });

}

// this function pulls in the data that contains an object that 'id' can be matched to. I consider this a rest call to some data.
private asyncDataWithWebpack(execAfter:any) {
    if (this.arrayOfDataWithManyIds && ... ){ // you may add your cache logic here 
        execAfter();
    } else {
        setTimeout(() => {
            System.import('../../assets/mock-data/mock-data.json')
                .then((json) => {
                    this.arrayOfDataWithManyIds = json;
                    execAfter();
                });
        });
    }
}

setSpecificDataRelatedToId(id) {
    this.nestedArrayBasedOnOneId = this.arrayOfDataWithManyIds[id].nestedArray;
}

I think @maxime has the right of it. 我认为@maxime有权利。 But, for a quick fix, make nestedArrayBasedOnOneId observable: 但是,要快速修复,请使nestedArrayBasedOnOneId可观察:

In your header: 在你的标题中:

import { Subject } from 'rxjs/Rx';

In your component: 在您的组件中:

private nestedArrayBasedOnOneId = new Subject();

In setSpecificDataRelatedToId(id) : setSpecificDataRelatedToId(id)

private setSpecificDataRelatedToId(id) { this.nestedArrayBasedOnOneId.next(this.arrayOfDataWithManyIds[id].nestedArray); this.appState.set("cache", this.arrayOfDataWithManyIds); this.appState.set("cacheId", Id); }

In your template: 在您的模板中:

<div *ngFor="entry of nestedArrayBasedOnOneId | async"> {{entry.name}} </div>

When you do it this way, nestedArrayBasedOnOneId is not undefined when your template is evaluated; 当您这样做时,在评估模板时,未定义nestedArrayBasedOnOneId ; it's a Subject that hasn't emitted a value yet. 它是一个尚未发布值的Subject The Async pipe subscribes to nestedArrayBasedOnOneId and sits around waiting for a value. Async管道订阅了nestedArrayBasedOnOneId并且等待一个值。 When setSpecificDataRelatedToId gets called, it instructs the nestedArrayBasedOnOneId to emit a value (the nestedArray ), provoking the template to update. setSpecificDataRelatedToId时,它会指示nestedArrayBasedOnOneId发出一个值( nestedArray ),从而激发要更新的模板。

If I have one thing to nag about @maxime's approach, it's just that they're not using the new *ngElse in Angular 4.0 for the loading indicator. 如果我对@ maxime的方法有一点唠叨,那就是他们没有在Angular 4.0中使用新的* ngElse作为加载指标。 *shrug* *耸肩*

I think this is a case of using Route Resolvers. 我认为这是使用Route Resolvers的情况。 They help you resolve data based on route parameters before the Component is rendered. 它们可帮助您在呈现Component before根据路由参数解析数据。

I included all logic in the Resolve Service but the logic should probably be in your ApiService Class later. 我在Resolve Service中包含了所有逻辑,但逻辑应该稍后在你的ApiService类中。

You even can catch fetching errors in the Resolve Service and do a redirect if you like. 您甚至可以在Resolve Service中捕获提取错误,并根据需要进行重定向。

Based on your sample this could look like this: 根据您的示例,这可能如下所示:

blah.resolver.ts

@Injectable()
export class BlahResolve implements Resolve<any[]> {
    private cache: any[];

    resolve(route: ActivatedRouteSnapshot) {
        let id = route.paramMap.get('id');

        if (this.cache && this.cache[id]) {
           return Observable.of(this.cache[id].nestedArray);
        }

        return Observable.create((observer) => {
            System.import('../../assets/mock-data/mock-data.json')
            .then((json) => {
                this.cache[id] = json;
                observer.next(this.cache[id].nestedArray);
                observer.complete();
             });
        });
    }
}

my.component.ts

ngOnInit() {
   this.nestedArrayBasedOnOneId = this.route.snapshot.data.nestedData;
}

the name of the data (in our case nestedData ) is specified in the routing definition. 数据的名称(在我们的例子中是nestedData )在路由定义中指定。

routing.ts

{
    path: 'myroute/:id',
    component: MyComponent,
    resolve: {
        nestedData: BlahResolve
    }
},

Disclaimer: I did not actually test the code here, but it should work, maybe with minor necessary changes. 免责声明:我实际上并没有在这里测试代码,但它应该可以工作,可能需要进行一些必要的修改。 For example i normally Resolve to an actual object not an array of objects. 例如,我通常解析为实际对象而不是对象数组。 Like Resolve<Person> and then call for example person.contacts in my Component. Resolve<Person> ,然后在我的Component中调用例如person.contacts

Also depending on how long the actual resolving takes, for the user it could look like nothing happens, so make sure you actually show the user that something actually happens in the background :) 同样取决于实际解析需要多长时间,对于用户来说,它看起来似乎没有任何反应,因此请确保您实际向用户显示在后台实际发生的事情:)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM