英雄指南——服务

作者: soojade | 来源:发表于2016-12-18 21:22 被阅读7次

    版本:4.0.0+2

    随着英雄指南应用的进化,你将会添加更多的需要访问英雄数据的组件。

    你将创建一个单独的可复用的数据服务,并把它注入到需要它的组件中,而不是反复地复制和粘贴相同的代码。使用一个单独的服务,让组件保持精简,专注于为视图提供支持,并且使用模拟服务使其易于编写单元测试组件。

    因为数据服务总是异步的,你将会使用一个基于 Future 版本的数据服务来完成本章。

    当你按照本章完成之后,应用看起来应该是这样的——在线示例 (查看源码)。

    我们离开的地方

    在继续英雄指南之前,验证你是否有如下结构。如果没有,回去查看前一章。

    如果应用不运行了,启动应用。当你做出修改时,通过刷新浏览器保持继续运行。

    创建英雄服务

    客户想要在不同的页面上以不同的方式显示英雄。现在用户已经可以从列表中选择一个英雄了。很快,你将添加一个仪表盘来显示表现最好的英雄,并创建一个独立视图来编辑英雄的详情。这三个视图都需要英雄数据。

    目前,AppComponent显示的是模拟数据。然而,定义英雄的数据不该是组件的任务,并且你不能很容易地在其它组件和视图中共享这个英雄列表。在本章,你将移动获取英雄数据的业务到一个单独的服务中,它将提供数据,并在所有需要这个数据的组件之间共享此服务。

    创建一个可注入的 HeroService

    lib/src目录下创建一个名为hero_service.dart的文件。

    服务文件的命名约定是——小写的服务名后紧跟着_service。对于多个单词的服务名,使用小写蛇形。例如,SpecialSuperHeroService服务的文件名应该是special_super_hero_service.dart

    把这个类命名为HeroService

    // lib/src/hero_service.dart (empty class)
    
    import 'package:angular/angular.dart';
    
    @Injectable()
    class HeroService {
    }
    

    可注入的服务

    注意你使用的 @Injectable() 注解。这告诉 Angular 编译器HeroService将会是一个注入的候补(更多信息很快就来)。

    获取英雄数据

    HeroService可以从任何地方获取英雄数据:Web 服务、本地存储(LocalStorage)或一个模拟的数据源。目前,导入HeromockHeroes,并且在一个getHeroes()方法中返回模拟的英雄:

    // lib/src/hero_service.dart
    
    import 'package:angular/angular.dart';
    
    import 'hero.dart';
    import 'mock_heroes.dart';
    
    @Injectable()
    class HeroService {
      List<Hero> getHeroes() => mockHeroes;
    }
    

    使用英雄服务

    你已经准备好在其它组件中使用HeroService了,先从AppComponent开始吧。

    导入HeroService以便你可以在代码中引用它。

    // lib/app_component.dart (hero service import)
    
    import 'src/hero_service.dart';
    

    不要对 HeroService 使用 new

    AppComponent应该如何获取HeroService的实例呢?

    你可以像这样使用new来创建一个新的HeroService实例:

    // lib/app_component.dart (excerpt)
    
    HeroService heroService = new HeroService(); // 不要这样做
    

    然而,这并不是一个好的选择,原因如下:

    • 组件必须知道如何创建一个HeroService实例。如果你修改了HeroService的构造函数,你就必须找到并更新创建过此服务的每一处地方。在多处地方修补代码容易引起错误并增加测试的负担。
    • 每使用一次new你就创建一个服务。假如服务缓存英雄并和其它的服务或组件共享这个缓存呢?你做不到那样。
    • AppComponent受限于一个特定的HeroService实现,难以针对不同的情景选择不同的实现,例如,离线操作或为测试使用不同的模拟版本。

    注入 HeroService

    添加如下所述的行,而不是使用new表达式:

    • 添加一个私有的HeroService属性。
    • 添加一个构造函数初始化这个私有属性。
    • 添加HeroService到组件的providers元数据。

    下面就是属性和构造函数:

    // lib/app_component.dart (constructor)
    
    final HeroService _heroService;
    AppComponent(this._heroService);
    

    构造函数除了设置_heroService属性什么也没做。_heroService的类型HeroService标志着构造函数的参数为HeroService的注入点。

    现在,当创建一个新的AppComponent组件时,Angular 知道提供一个HeroService的实例。

    更多关于依赖注入的内容,请看依赖注入章节。

    注入器(injector)还不知道怎样创建HeroService。如果你现在运行代码,Angular会失败,并报错:

    EXCEPTION: No provider for HeroService! (AppComponent -> HeroService)
    

    添加如下的providers列表作为@Component注解最后的参数,来告诉注入器如何制造HeroService实例。

    // lib/app_component.dart (providers)
    
    providers: const [HeroService],
    

    providers参数告诉 Angular,当它创建一个AppComponent组件时,创建一个新鲜的HeroService的实例。AppComponent及其子组件可以使用这个服务来获取英雄数据。

    AppComponent.getHeroes() 方法

    添加一个getHeroes()方法到 app component,并且移除heroes初始化程序:

    // lib/app_component.dart (heroes and getHeroes)
    
    List<Hero> heroes;
    
    void getHeroes() {
      heroes = _heroService.getHeroes();
    }
    

    ngOnInit 生命周期钩子

    AppComponent应该毫无问题地获取和显示英雄。

    你可能忍不住在构造函数中调用getHeroes()方法,但构造函数不应该包含复杂的逻辑,尤其是一个呼叫服务器的构造函数,比如一个数据存取方法。构造函数应该单纯用来初始化,比如把构造函数的参数赋值给属性。

    你可以实现 Angular 的 ngOnInit 生命周期钩子,来使 Angular 调用getHeroes()方法。Angular 为组件生命周期的几个关键时刻提供了接口:创建时、每次变化后,以及最终被销毁时。

    每个接口有一个单独的方法。当组件实现了那个方法,Angular 就会在适当的时机调用它。

    更多关于生命周期钩子的内容请看生命周期钩子章节。

    添加 OnInitAppComponent实现的接口列表,并编写一个内部带有初始化逻辑的ngOnInit方法。Angular 会在适当的时候调用它。在这个例子中,通过调用getHeroes()初始化。

    class AppComponent implements OnInit {
      void ngOnInit() => getHeroes();
    }
    

    刷新浏览器。应用应该显示一列英雄,并且当用户点击英雄名时,显示英雄详情视图。

    异步的英雄服务

    HeroService立刻返回了一个模拟英雄的列表;它的getHeroes()签名是同步的:

    // lib/src/hero_service.dart (getHeroes)
    
    List<Hero> getHeroes() => mockHeroes;
    

    最终,英雄数据会来自于一个远程服务器。当使用一个远程服务器时,用户不得不等待服务器响应;此外,在等待时你也不能够阻塞 UI。

    要协调视图和响应,可以使用 Futures,这是一种改变getHeroes()方法签名的异步技术。

    英雄服务返回一个 Future

    一个 Future 表示一个未来的计算或值。使用一个Future,你可以注册回调函数,它将会在计算完成(结果准备好)或计算出错需要报告时被调用。

    这里只是简单的说明。更多关于 Futures 的信息请看 Dart 语言教程的 异步编程: Futures

    添加一个 dart:async 的导入,因为它定义了Future,并且更新 HeroService,使用Future作为 getHeroes()方法的返回值类型:

    // lib/src/hero_service.dart (excerpt)
    
    Future<List<Hero>> getHeroes() async => mockHeroes;
    

    你仍然在使用模拟数据。通过返回一个模拟英雄立即可用的Future,模拟了一个超快、零延迟的服务器行为。

    设置返回值类型为 Future,使一个方法自动变成了异步方法。关于异步函数的更多信息,请看 Dart 语言教程的声明异步函数

    处理 Future

    由于HeroService改变的结果,app 组件的heroes属性现在是一个Future而不是一个英雄的列表。你必须改变这种实现,在它完成时处理Future结果。当Future成功完成,你就会有英雄来显示了。

    这里是目前的实现:

    // lib/app_component.dart (synchronous getHeroes)
    
    void getHeroes() {
      heroes = _heroService.getHeroes();
    }
    

    传递一个回调函数作为Future.then()方法的参数:

    // lib/app_component.dart (asynchronous getHeroes)
    
    void getHeroes() {
      _heroService.getHeroes().then((heroes) => this.heroes = heroes);
    }
    

    这个回调函数设置组件的heroes属性到通过服务返回的英雄列表。

    刷新浏览器。应用仍然运行,显示一列英雄,并使用一个详情视图响应名称的选择。

    使用 async/await

    一个异步方法包含一个或多个Future.then()方法难以阅读和理解。谢天谢地,Dart 的async/await语言特性让你写异步代码就像写同步代码一样。重写getHeroes()

    // lib/app_component.dart (revised async/await getHeroes)
    
    Future<Null> getHeroes() async {
      heroes = await _heroService.getHeroes();
    }
    

    Future<Null>返回值类型等价于异步的void

    更多关于使用async/await异步变成的知识,请看 Dart 语言教程异步编程: FuturesAsync and await部分。

    在本章的结尾,附录:慢一点 描述了连接不良的应用可能是什么样的。

    回顾应用结构

    确认经过所有重构之后,应该有如下文件结构:

    我们已经走过的路

    以下就是你在本章的收获:

    • 创建了一个能被多个组件共享的服务类。
    • 使用ngOnInit生命周期钩子,在AppComponent激活时获取英雄数据。
    • 定义HeroService作为 AppComponent的一个提供器。
    • 把服务设计为返回一个 Future,并且组件从 Future 中获取数据。

    你的应用看起来应该这样——在线示例 (查看源码)。

    附录:慢一点

    要模拟一个缓慢的连接,添加如下的getHeroesSlowly()方法到HeroService

    // lib/src/hero_service.dart (getHeroesSlowly)
    
    Future<List<Hero>> getHeroesSlowly() {
      return new Future.delayed(const Duration(seconds: 2), getHeroes);
    }
    

    getHeroes 一样,它也返回一个Future,但这个 Future 会在完成之前等待两秒钟。

    回到 AppComponent ,用 getHeroesSlowly() 替换掉 getHeroes() ,并观察本应用是如何表现的。

    下一步

    路由

    相关文章

      网友评论

        本文标题:英雄指南——服务

        本文链接:https://www.haomeiwen.com/subject/hqezmttx.html