简体   繁体   English

使 angular 服务返回 class 字段作为 observable

[英]Make angular service return class field as a observable

Here is what I want to achieve: There is a component that has a UserService in it.这是我想要实现的目标:有一个组件中有一个 UserService 。 After log in I want to call that service to retrieve user data from backend and store it in a service field.登录后,我想调用该服务以从后端检索用户数据并将其存储在服务字段中。 Then after I reload main page I want that service to do not call backend again, just return previously stored data.然后在我重新加载主页后,我希望该服务不再调用后端,只返回以前存储的数据。 Additionally I want some divs to do not load before a data is loaded.此外,我希望在加载数据之前不加载一些 div。 How can I achieve that?我怎样才能做到这一点? Should I make getAllUSerInfoNormal() method to return observable, if its possible how should I do that?我应该让getAllUSerInfoNormal()方法返回 observable,如果可能的话,我应该怎么做? Or there is some other way?还是有其他方法?

Here is what I have got right now:这是我现在得到的:

Service class:服务 class:

@Injectable()
export class AccountService {

    userAccount : UserAccount;
    constructor(private _httpClient: HttpClient) {
    }


     getAllUSerInfoNormal(): UserAccount {
    if (!this.userAccount) {
        this._httpClient.get<UserAccount>(Constants.apiRoot + 'account/accountInfo').subscribe(user => {
            this.userAccount = user;
            return this.userAccount
        })
    } else {
        return this.userAccount;
    }
}



Component class:组件 class:

@Component({
  selector: 'app-main-view',
  templateUrl: './main-view.component.html',
  styleUrls: ['./main-view.component.scss']
})
export class MainViewComponent implements OnInit {

  private account: UserAccount;
  private isDataLoaded = false;

  constructor(private _router: Router,
              private _accountService: AccountService) {
    setInterval(() => {
      this.updateResources();
    }, 1);
  }

  ngOnInit() {
    this._accountService.getAllUSerInfoNormal();
  }

  updateResources(){

  }
}



Component template:组件模板:

<div class="ingame-menu">
    <div *ngIf="isDataLoaded">
        <div class="resources">
            <div class="resource"><img src="../../../assets/images/resources/gold.png" title="Gold"/>
                <br/> {{account.gold}}</div>
            <div class="resource"><img src="../../../assets/images/resources/wood.png" title="Wood"/>
                <br/> {{account.wood}}</div>
            <div class="resource"><img src="../../../assets/images/resources/stone.png" title="Stone"/>
                <br/> {{account.stone}}</div>
            <div class="resource"><img src="../../../assets/images/resources/meat.png" title="Meat"/>
                <br/> {{account.people}} </div>
        </div>
    </div>
    <div class="ingame-nav">
        <ul>
            <li><a routerLink="overview">Overview</a></li>
            <li><a routerLink="population">Population</a></li>
            <li><a routerLink="resources">Resources</a></li>
            <li><a routerLink="research">Research</a></li>
            <li><a routerLink="buildings">Buildings</a></li>
            <li><a routerLink="barracks">Barracks</a></li>
            <li><a routerLink="tavern">Tavern</a></li>
            <li><a routerLink="defence">Defence</a></li>
        </ul>
    </div>
    <div id="content">
        <router-outlet></router-outlet>
    </div>
</div>

You can save the loaded user in the lcoalStorage after the first load and then just read it on the following accesses.您可以在第一次加载后将加载的用户保存在 lcoalStorage 中,然后在以下访问中读取它。

This is a simple solution to learn.这是一个简单的学习解决方案。 But in a real application scenario you shouldn't have "infinite time" cache like this.但在实际应用场景中,您不应该有这样的“无限时间”缓存。 And must have a way to update the cache sometimes.并且有时必须有办法更新缓存。

Change the service class to:服务 class更改为:

@Injectable()
export class AccountService {

    get userAccount() {
      return JSON.parse(localStorage.getItem('user'));
    };

    constructor(private _httpClient: HttpClient) {
    }


    getAllUSerInfoNormal(forceBanckendCall: boolean): Observable<UserAccount> {
        if (this.userAccount && !forceBackendCall) {
           return of(this.userAccount);
        }

        return this._httpClient.get<UserAccount>(Constants.apiRoot + 'account/accountInfo')
           .pipe(
             tap(user => localStorage.setItem('user', JSON.stringify(user)))
           );

    }

then the component class to:然后组件 class到:

@Component({
  selector: 'app-main-view',
  templateUrl: './main-view.component.html',
  styleUrls: ['./main-view.component.scss']
})
export class MainViewComponent implements OnInit {

  private account: UserAccount;
  private isDataLoaded = false;

  constructor(private _router: Router,
              private _accountService: AccountService) {
    setInterval(() => {
      this.updateResources();
    }, 1);
  }

  ngOnInit() {
    this._accountService.getAllUSerInfoNormal().subscribe(account => this.userAccount = account);
  }

  updateResources(){

  }
}

then in your component template place a *ngIf="userAccount" in the elements that you want to be loaded just after the user is present.然后在您的组件模板中放置一个*ngIf="userAccount"在您想要在用户出现后立即加载的元素中。

  1. You cannot return the value from the subscription synchronously like you're attempting.您无法像尝试那样同步地从订阅中返回值。 The variable from an observable is emitted asynchronously.来自 observable 的变量是异步发出的。 Please see here for more info on async data.有关异步数据的更多信息,请参阅此处 In short, the value is available only inside the subscription.简而言之,该值仅在订阅内可用。

  2. There are multiple ways to cache a notification from an observable.有多种方法可以缓存来自 observable 的通知。 One of the quickest way is to use a multicast observable (like Subject ) in the service to hold the data.最快的方法之一是在服务中使用多播 observable(如Subject )来保存数据。 I'll illustrate using ReplaySubject with buffer 1 since it can hold on to the current value.我将说明使用带有缓冲区 1 的ReplaySubject ,因为它可以保持当前值。

Service服务

import { ReplaySubject } from 'rxjs';

@Injectable()
export class AccountService {
  userAccountSource: ReplaySubject<UserAccount> = new ReplaySubject<UserAccount>(1);
  userAccount$ = this.userAccountSource.asObservable();
  
  constructor(private _httpClient: HttpClient) {
    this.getAllUSerInfoNormal().subscribe();  // <-- push to cache on app start
  }

  getAllUSerInfoNormal(): Observable<UserAccount> {
    return this._httpClient.get<UserAccount>(Constants.apiRoot + 'account/accountInfo').pipe(
      tap({
        next: user => this.userAccountSource.next(user),
        error: error => { }  // <-- handle error
      })
    );
  }
}
  1. Now if the emission is not needed in the component controller, you can straight up skip the subscription in it and using the Angular async pipe in the template.现在,如果组件 controller 中不需要发射,您可以直接跳过其中的订阅,并在模板中使用 Angular async pipe。 Besides keeping the code tidy, it'll handle the potential leak issue of an open subscription.除了保持代码整洁之外,它还将处理开放订阅的潜在泄漏问题。

Controller Controller

@Component({...})
export class MainViewComponent implements OnInit {
  private isDataLoaded = false;

  constructor(
    private _router: Router,
    private _accountService: AccountService
  ) {
    setInterval(() => {
      this.updateResources();
    }, 1);
  }

  ngOnInit() {
  }

  updateResources() {
  }
}

Template模板

<div class="ingame-menu">
  <div *ngIf="(_accountService.userAccount$ | async) as account">
    <div class="resources">
      <div class="resource"><img src="../../../assets/images/resources/gold.png" title="Gold" />
        <br /> {{account.gold}}</div>
      <div class="resource"><img src="../../../assets/images/resources/wood.png" title="Wood" />
        <br /> {{account.wood}}</div>
      <div class="resource"><img src="../../../assets/images/resources/stone.png" title="Stone" />
        <br /> {{account.stone}}</div>
      <div class="resource"><img src="../../../assets/images/resources/meat.png" title="Meat" />
        <br /> {{account.people}} </div>
    </div>
  </div>
  <div class="ingame-nav">
    <ul>
      <li><a routerLink="overview">Overview</a></li>
      <li><a routerLink="population">Population</a></li>
      <li><a routerLink="resources">Resources</a></li>
      <li><a routerLink="research">Research</a></li>
      <li><a routerLink="buildings">Buildings</a></li>
      <li><a routerLink="barracks">Barracks</a></li>
      <li><a routerLink="tavern">Tavern</a></li>
      <li><a routerLink="defence">Defence</a></li>
    </ul>
  </div>
  <div id="content">
    <router-outlet></router-outlet>
  </div>
</div>
  1. As said, there are multiple other ways to cache the data from an observable.如前所述,还有多种其他方法可以缓存来自 observable 的数据。 You can start from here and here .你可以从这里这里开始。

  2. Unrelated to the issue, I find the usage of setInterval() with argument 1 very inelegant.与问题无关,我发现setInterval()与参数 1 的用法非常不雅。 Do you wish to poll the end-point regularly to keep the data updated?您是否希望定期轮询端点以保持数据更新? If so you could look into RxJS timer or interval function.如果是这样,您可以查看 RxJS timerinterval function。 They let you poll with a single subscription instead of opening a new subscription every n seconds.它们允许您使用单个订阅进行轮询,而不是每 n 秒打开一个新订阅。

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

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