在渲染视图/模板之前等待 Angular 2 加载/解析模型

Posted

技术标签:

【中文标题】在渲染视图/模板之前等待 Angular 2 加载/解析模型【英文标题】:Wait for Angular 2 to load/resolve model before rendering view/template 【发布时间】:2016-04-16 08:55:40 【问题描述】:

在 Angular 1.x 中,UI-Router 是我的主要工具。通过返回“resolve”值的承诺,路由器将在呈现指令之前简单地等待承诺完成。

另外,在 Angular 1.x 中,空对象不会使模板崩溃 - 因此,如果我不介意暂时不完整的渲染,我可以在 promise.then() 填充初始为空后使用 $digest 进行渲染模型对象。

在这两种方法中,如果可能的话,我宁愿等待加载视图,如果无法加载资源,则取消路线导航。这为我节省了“取消导航”的工作。 编辑:请注意,这特别意味着此问题要求使用 Angular 2 期货兼容或最佳实践方法来执行此操作,并要求尽可能避免使用“猫王操作员”!因此,我没有选择那个答案。

但是,这两种方法在 Angular 2.0 中都不起作用。当然,有一个标准的解决方案计划或可用于此。有人知道是什么吗?

@Component() 
    template: 'cats.captchans.funniest'

export class CatsComponent 

    public cats: CatsModel;

    ngOnInit () 
        this._http.get('/api/v1/cats').subscribe(response => cats = response.json());
    

以下问题可能反映了同样的问题:Angular 2 render template after the PROMISE with data is loaded。请注意,问题中没有代码或可接受的答案。

【问题讨论】:

你可以从@TGH读到lazy loading article @shannon 整个路由器刚刚被弃用并重新编写。希望他们今天在 NgConf 上讨论这个问题。 @Langley 你的意思是他们将完全重写 Angular 2 路由器? @Pascal 好像是这样,看看这个:angular.io/docs/ts/latest/guide/router-deprecated.html 好吧......他们也可以写出用“RC angular 2”替换的“deprected beta angular 2”让我们看看 RC 路由器是否可以处理像 aureliajs 这样的无限子路由器 ;-)跨度> 【参考方案1】:

在您的@Component 中实现routerOnActivate 并返回您的承诺:

https://angular.io/docs/ts/latest/api/router/OnActivate-interface.html

编辑:这明确不起作用,尽管当前文档在这个主题上可能有点难以解释。更多信息请参见 Brandon 的第一条评论:https://github.com/angular/angular/issues/6611

编辑:其他通常准确的 Auth0 站点上的相关信息不正确:https://auth0.com/blog/2016/01/25/angular-2-series-part-4-component-router-in-depth/

编辑:Angular 团队正在为此目的计划一个 @Resolve 装饰器。

【讨论】:

ngOnInit中返回一个promise是否有同样的效果? 我不这么认为,但如果这样做也无济于事,因为直到之后才会调用该函数。来自:angular.io/docs/ts/latest/api/core/…,“在第一次检查指令的数据绑定属性后立即调用 ngOnInit” 有趣:如果你不能用它初始化数据,他们在所有演示中都使用它似乎很愚蠢。 文档说 > * 如果routerOnActivate 返回一个promise,路由更改将等到promise 稳定后 * 实例化并激活子组件。 elvis 运算符不允许用于双向绑定。【参考方案2】:

尝试model?.person.name 这应该等待模型不是undefined 然后渲染。

Angular 2 将此?. 语法称为Elvis operator。在文档中很难找到对它的引用,所以这里是它的副本,以防他们更改/移动它:

Elvis 运算符 (?.) 和 null 属性路径

Angular 的“Elvis”运算符 (?.) 是一种流畅且方便的方法,可以防止属性路径中出现空值和未定义值。就是这样,如果 currentHero 为 null,则防止视图渲染失败。

The current hero's name is currentHero?.firstName

让我们详细说明这个问题和这个特定的解决方案。

当以下数据绑定的title属性为空时会发生什么?

The title is title

视图仍在渲染,但显示的值为空白;我们只看到“标题是”,后面什么都没有。这是合理的行为。至少应用不会崩溃。

假设模板表达式包含一个属性路径,就像在下一个示例中我们显示空英雄的名字一样。

The null hero's name is nullHero.firstName

javascript 会抛出空引用错误,Angular 也是如此:

TypeError: Cannot read property 'firstName' of null in [null]

更糟糕的是,整个视图都消失了。

如果我们认为 hero 属性绝不能为空,我们可以声称这是合理的行为。如果它一定不能为空,但它仍然为空,我们就犯了一个应该被捕获和修复的编程错误。抛出异常是正确的做法。

另一方面,属性路径中的空值有时可能没问题,尤其是当我们知道数据最终会到达时。

当我们等待数据时,视图应该毫无问题地呈现,并且 null 属性路径应该显示为空白,就像 title 属性一样。

不幸的是,当 currentHero 为 null 时,我们的应用程序崩溃了。

我们可以用 NgIf 来解决这个问题

<!--No hero, div not displayed, no error --> <div *ngIf="nullHero">The null hero's name is nullHero.firstName</div>

或者我们可以尝试用 && 链接属性路径的一部分,知道表达式遇到第一个 null 时会退出。

The null hero's name is nullHero && nullHero.firstName

这些方法有优点,但它们可能很麻烦,尤其是在属性路径很长的情况下。想象一下,在诸如 a.b.c.d 之类的长属性路径中的某处防范 null。

Angular 的“Elvis”运算符 (?.) 是一种更流畅、更方便的方法来防止属性路径中出现空值。表达式在遇到第一个空值时退出。显示为空白,但应用程序不断滚动并且没有错误。

<!-- No hero, no problem! --> The null hero's name is nullHero?.firstName

它也适用于长属性路径:

a?.b?.c?.d

【讨论】:

谢谢。这作为一种解决方法效果很好,但最终我认为将对象及其所有子对象视为强制性并等待它可以渲染,正如其他答案所建议的那样(如果我能让它们工作)更有意义)。否则这种装饰将出现在我所有页面中的每个值上,在这方面我将回到 Angular 1.x 功能。它也可能存在性能问题(额外的脏检查)。 @shannon 我不认为是这样。请参阅我的更新答案和 ng2 文档。 我怀疑性能的原因是Observable(同样适用于Promise)已经被解包。现在确定它何时发生变化的唯一方法是使用我们值得信赖的 Angular 1.x 机制,即脏检查。将此与使用Observable 进行比较,模板本身中的async 管道支持它。 Angular 在内部可以(我怀疑确实如此)将管道连接起来,这样就不需要观察者了,并且仅当可观察对象到达时才呈现节点。它实际上是具有事件驱动操作的消息总线。 另一个小问题是,让 Elvis 操作员无处不在,在某种程度上破坏了 Angular 团队改变主意的目标,即默默地吞下错误。您自己可能遇到过这样的问题,发现当函数需要处理空值时,模板(以及整个应用程序)更难调试。此外,如果我必须选择要显示的内容,例如在 A:B 场景中,现在我必须实现额外的ngIf 逻辑。我真的很想让“等待渲染组件”的方法起作用。 此运算符现在称为"safe navigation operator"。【参考方案3】:

用观察者设置局部值

...另外,不要忘记使用虚拟数据初始化值以避免uninitialized 错误。

export class ModelService 
    constructor() 
      this.mode = new Model();

      this._http.get('/api/v1/cats')
      .map(res => res.json())
      .subscribe(
        json => 
          this.model = new Model(json);
        ,
        error => console.log(error);
      );
    

这里假设 Model,是一个代表数据结构的数据模型。

没有参数的模型应该创建一个所有值都已初始化(但为空)的新实例。这样,如果模板在接收到数据之前呈现,它就不会抛出错误。

理想情况下,如果您想持久化数据以避免不必要的 http 请求,您应该将其放在一个对象中,该对象有自己的观察者可以订阅。

【讨论】:

这对我来说没有意义,埃文。这不是通过将可观察序列扁平化为需要脏检查的普通对象来规避 Angular 2 中性能更好的可观察模式吗?此外,我真的不希望维护一堆空的初始化程序,一个用于我客户端中的每个模型的初始化程序。我希望有另一种方法。 我完全同意ngIf 也是一种功能性解决方法 - 在预期结果对象很复杂的情况下,它比您的其他建议需要更少的重复代码。但是,它具有相同的弱点,因为它解开 Observable 并要求模板执行脏检查(或不必要地重新渲染)。我知道您在上次评论中不同意这一点,但如果您进一步考虑,我怀疑您会同意。另请参阅我对@tehaaron 的评论,他的回答具有类似的效果。 @shannon Angular2 中的脏检查很便宜,但这无关紧要,因为它没有回答问题。您想要的是在页面加载之前预热的数据缓存,因此没有重新渲染闪存。为此,您需要将数据作为单独的服务加载,并且您需要一种在页面加载之前触发数据加载的方法。 (cont) 服务直到第一次被注入时才被构建。所以其他一些组件必须触发构造。一种方法是将服务注入父组件。另一种方法是延迟加载路由并在加载组件之前以某种方式触发服务启动。 脏检查在小批量时总是很便宜,我不反对在 Angular 1.x 中使用它。我同意简单的好处大于成本。然而,为了支持 Angular 2 中的Observable 驱动机制,做出了一项基本的架构决策,其中一个原因是在包含大量项目的页面上提高了性能。它具有更高的性能,并且作为消费者仍然很容易获得“正确”。所以,我想使用它。【参考方案4】:

编辑:角度团队已经发布了@Resolve 装饰器。它仍然需要一些澄清,它是如何工作的,但在那之前我会在这里接受其他人的相关答案,并提供其他来源的链接:

***: Using Resolve In Angular2 Routes ***: Usage sample by Günter Zöchbauer Resolve documentation The router patch on GitHub RC4 ChangeLog RC4 Release Tweet

编辑:此答案仅适用于 Angular 2 BETA。在本次编辑中,没有为 Angular 2 RC 发布路由器。相反,在使用 Angular 2 RC 时,将对 router 的引用替换为 router-deprecated 以继续使用 beta 路由器。

Angular2 未来的实现方式将是通过 @Resolve 装饰器。在那之前,最接近的传真是CanActivate Component decorator,根据 Brandon Roberts。见https://github.com/angular/angular/issues/6611

虽然 beta 0 不支持向组件提供解析值,但这是计划好的,这里还描述了一种解决方法:Using Resolve In Angular2 Routes

可以在此处找到 beta 1 示例:http://run.plnkr.co/BAqA98lphi4rQZAd/#/resolved。它使用非常相似的解决方法,但使用RouteData 对象而不是RouteParams 更准确。

@CanActivate((to) => 
    return new Promise((resolve) => 
        to.routeData.data.user =  name: 'John' 

另外,请注意,如果您使用了 1.x UI-Router,还有一个示例解决方法可用于访问嵌套/父路由“已解析”值以及您期望的其他功能。

请注意,您还需要手动注入完成此操作所需的任何服务,因为 CanActivate 装饰器中当前不提供 Angular Injector 层次结构。只需导入一个 Injector 将创建一个新的注入器实例,而无需从 bootstrap() 访问提供程序,因此您可能希望存储引导注入器的应用程序范围的副本。 Brandon 在此页面上的第二个 Plunk 链接是一个很好的起点:https://github.com/angular/angular/issues/4112

【讨论】:

您推荐的 plnker:run.plnkr.co/BAqA98lphi4rQZAd/#/resolved 不再可用。感谢您推送此功能。也许您可以将计划用于 RC1 发布问题的当前功能添加到您的答案中:github.com/angular/angular/issues/4015 RouteData 是不可变的。 @HristoVenev:您的评论(以及相关的反对票)是否与解决方案的适用性有关,而 Angular 2 Beta 路由器仍然是最新的? @shannon RouteData 即使对于 beta 路由器 (router-deprecated) 也是不可变的。 是的,我知道 RouteData 被记录为“不可变”。让我们暂时忽略这对 JavaScript 属性集意味着什么。让我们直奔你的重点。是说解决方案不起作用,还是有更好的解决方案可用?最后我检查了一下,我的应用程序在这方面没有显示任何缺陷,这个解决方案是 Angular 团队提出的,没有其他人提供过与之竞争的解决方案。尽管遵循文档的指导会很好,但 Resolve 尚未实施,因此该指导不值得反对。【参考方案5】:

@angular/router 具有用于路由的Resolve 属性。因此,您可以在渲染路线视图之前轻松解析数据。

见:https://angular.io/docs/ts/latest/api/router/index/Resolve-interface.html

截至 2017 年 8 月 28 日的文档示例:

class Backend 
  fetchTeam(id: string) 
    return 'someTeam';
  


@Injectable()
class TeamResolver implements Resolve<Team> 
  constructor(private backend: Backend) 

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<any>|Promise<any>|any 
    return this.backend.fetchTeam(route.params.id);
  


@NgModule(
  imports: [
    RouterModule.forRoot([
      
        path: 'team/:id',
        component: TeamCmp,
        resolve: 
          team: TeamResolver
        
      
    ])
  ],
  providers: [TeamResolver]
)
class AppModule 

现在您的路线将不会被激活,直到数据被解析并返回。

在您的组件中访问已解析的数据

要在运行时从组件内访问解析的数据,有两种方法。因此,根据您的需要,您可以使用以下任一方式:

    route.snapshot.paramMap 返回一个字符串,或者 route.paramMap 返回一个 Observable,您可以通过 .subscribe() 访问。

例子:

  // the no-observable method
  this.dataYouResolved= this.route.snapshot.paramMap.get('id');
  // console.debug(this.licenseNumber);

  // or the observable method
  this.route.paramMap
     .subscribe((params: ParamMap) => 
        // console.log(params);
        this.dataYouResolved= params.get('id');
        return params.get('dataYouResolved');
        // return null
     );
  console.debug(this.dataYouResolved);

希望对你有帮助。

【讨论】:

它可以工作,但是在你的组件中,你如何访问resolve返回的数据? 好像可以从快照***.com/a/38313301/1138984得到它 解析器不适用于应用程序中的第一条路由; 您的代码有点过时了。查看angular.io/api/router/Resolve 了解更新的语法。【参考方案6】:

我发现的一个不错的解决方案是在 UI 上执行以下操作:

<div *ngIf="vendorServicePricing && quantityPricing && service">
 ...Your page...
</div

仅当:vendorServicePricingquantityPricingservice 加载时才会呈现页面。

【讨论】:

这应该放在首位,因为它是最简单的解决方案。不需要 Resolve 或 ?. "notnull-Operator" 如果您有一个非常大的模板怎么办?将所有内容存储在一个大的 IF 子句中?最好有一种像 React 一样拦截预渲染的方法,然后返回 null 直到你有一些数据。

以上是关于在渲染视图/模板之前等待 Angular 2 加载/解析模型的主要内容,如果未能解决你的问题,请参考以下文章

在Angular 2中渲染动态模板时如何提供@Input

Angular2:如何在渲染组件之前加载数据?

Angular2:如何在渲染组件之前加载数据?

在渲染模板之前等待两个 observables(包括失败的一个)

Angular 6/7 AOT:动态模板渲染 - 为模块加载 JitCompiler

Angular UI-Router,父 ui-view 渲染,但带有模板的子视图不渲染