在此页面中,您将进行如下改进。css
您将教会应用程序对远程服务器的Web API进行相应的HTTP调用。html
当你完成这个页面,应用程序应该看起来像这个实例(查看源代码)。java
你离开的地方
在前一页中,您学会了在仪表板和固定英雄列表之间导航,沿途编辑选定的英雄。 这是这个页面的起点。git
在继续英雄之旅以前,请确认您具备如下结构。github
若是该应用程序还没有运行,请启动该应用程序。 在进行更改时,请经过从新加载浏览器窗口来保持运行。web
您将使用Dart http软件包的客户端类与服务器进行通讯。json
经过添加Dart http和stream_transform软件包来更新软件包相关性:bootstrap
在应用程序可使用BrowserClient以前,您必须将其注册为服务提供者。后端
您应该能够从应用程序的任何位置访问BrowserClient服务。 所以,请在启动应用程序及其根AppComponent的引导程序调用中注册它。api
web/main.dart (v1)
import 'package:angular/angular.dart'; import 'package:angular_router/angular_router.dart'; import 'package:angular_tour_of_heroes/app_component.dart'; import 'package:http/browser_client.dart'; void main() { bootstrap(AppComponent, [ ROUTER_PROVIDERS, // Remove next line in production provide(LocationStrategy, useClass: HashLocationStrategy), provide(BrowserClient, useFactory: () => new BrowserClient(), deps: []) ]); }
请注意,您在列表中提供了BrowserClient,做为引导方法的第二个参数。 这与@Component注解中的提供者列表具备相同的效果。
注意:除非您有适当配置的后端服务器(或模拟服务器),不然此应用程序不起做用。 下一节将展现如何模拟与后端服务器的交互。
在你有一个能够处理英雄数据请求的Web服务器以前,HTTP客户端将从模拟服务(内存中的Web API)中获取并保存数据。
使用此版本更新web / main.dart,该版本使用模拟服务:web/main.dart (v2)
import 'package:angular/angular.dart'; import 'package:angular_router/angular_router.dart'; import 'package:angular_tour_of_heroes/app_component.dart'; import 'package:angular_tour_of_heroes/in_memory_data_service.dart'; import 'package:http/http.dart'; void main() { bootstrap(AppComponent, [ ROUTER_PROVIDERS, // Remove next line in production provide(LocationStrategy, useClass: HashLocationStrategy), provide(Client, useClass: InMemoryDataService), // Using a real back end? // Import browser_client.dart and change the above to: // [provide(Client, useFactory: () => new BrowserClient(), deps: [])] ]); }
您但愿将BrowserClient(与远程服务器交谈的服务)替换为内存中的Web API服务。 内存中的Web API服务,以下所示,使用http库MockClient类实现。 全部的http客户端实现共享一个共同的客户端接口,因此你将有应用程序使用客户端类型,以便您能够自由切换实现。
lib / in_memory_data_service.dart(init)
import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:angular/angular.dart'; import 'package:http/http.dart'; import 'package:http/testing.dart'; import 'src/hero.dart'; @Injectable() class InMemoryDataService extends MockClient { static final _initialHeroes = [ {'id': 11, 'name': 'Mr. Nice'}, {'id': 12, 'name': 'Narco'}, {'id': 13, 'name': 'Bombasto'}, {'id': 14, 'name': 'Celeritas'}, {'id': 15, 'name': 'Magneta'}, {'id': 16, 'name': 'RubberMan'}, {'id': 17, 'name': 'Dynama'}, {'id': 18, 'name': 'Dr IQ'}, {'id': 19, 'name': 'Magma'}, {'id': 20, 'name': 'Tornado'} ]; static List<Hero> _heroesDb; static int _nextId; static Future<Response> _handler(Request request) async { if (_heroesDb == null) resetDb(); var data; switch (request.method) { case 'GET': final id = int.parse(request.url.pathSegments.last, onError: (_) => null); if (id != null) { data = _heroesDb .firstWhere((hero) => hero.id == id); // throws if no match } else { String prefix = request.url.queryParameters['name'] ?? ''; final regExp = new RegExp(prefix, caseSensitive: false); data = _heroesDb.where((hero) => hero.name.contains(regExp)).toList(); } break; case 'POST': var name = JSON.decode(request.body)['name']; var newHero = new Hero(_nextId++, name); _heroesDb.add(newHero); data = newHero; break; case 'PUT': var heroChanges = new Hero.fromJson(JSON.decode(request.body)); var targetHero = _heroesDb.firstWhere((h) => h.id == heroChanges.id); targetHero.name = heroChanges.name; data = targetHero; break; case 'DELETE': var id = int.parse(request.url.pathSegments.last); _heroesDb.removeWhere((hero) => hero.id == id); // No data, so leave it as null. break; default: throw 'Unimplemented HTTP method ${request.method}'; } return new Response(JSON.encode({'data': data}), 200, headers: {'content-type': 'application/json'}); } static resetDb() { _heroesDb = _initialHeroes.map((json) => new Hero.fromJson(json)).toList(); _nextId = _heroesDb.map((hero) => hero.id).fold(0, max) + 1; } static String lookUpName(int id) => _heroesDb.firstWhere((hero) => hero.id == id, orElse: null)?.name; InMemoryDataService() : super(_handler); }
这个文件替换了mock_heroes.dart,如今能够安全删除了。
对于Web API服务来讲,模拟内存中的服务将以JSON格式对英雄进行编码和解码,因此使用如下功能来加强Hero类:lib/ src/ hero.dart
class Hero { final int id; String name; Hero(this.id, this.name); factory Hero.fromJson(Map<String, dynamic> hero) => new Hero(_toInt(hero['id']), hero['name']); Map toJson() => {'id': id, 'name': name}; } int _toInt(id) => id is int ? id : int.parse(id);
在目前的HeroService实现中,返回一个用模拟英雄解决的Future。
Future<List<Hero>> getHeroes() async => mockHeroes;
这是为了最终使用HTTP客户端获取英雄而实现的,这个客户端必须是异步操做。
如今转换getHeroes()使用HTTP。lib/src/hero_service.dart (updated getHeroes and new class members)
static const _heroesUrl = 'api/heroes'; // URL to web API final Client _http; HeroService(this._http); Future<List<Hero>> getHeroes() async { try { final response = await _http.get(_heroesUrl); final heroes = _extractData(response) .map((value) => new Hero.fromJson(value)) .toList(); return heroes; } catch (e) { throw _handleError(e); } } dynamic _extractData(Response resp) => JSON.decode(resp.body)['data']; Exception _handleError(dynamic e) { print(e); // for demo purposes only return new Exception('Server error; cause: $e'); }
更新导入语句lib/src/hero_service.dart (updated imports)
import 'dart:async'; import 'dart:convert'; import 'package:angular/angular.dart'; import 'package:http/http.dart'; import 'hero.dart';
刷新浏览器。 英雄数据应该从模拟服务器成功加载。
HTTP Future
要获取英雄列表,您首先要对http.get()进行异步调用。 而后使用_extractData辅助方法来解码响应主体。
响应JSON有一个单一的数据属性,它拥有主叫方想要的英雄列表。 因此你抓住这个列表并把它做为已解决的Future值返回。
请注意服务器返回的数据的形状。 这个特定的内存web API示例返回一个具备data属性的对象。 你的API可能会返回其余的东西。 调整代码以匹配您的Web API。
调用者不知道你从(模拟)服务器获取英雄。 它像之前同样接受英雄的将来。
在getHeroes()的结尾处,您能够捕获服务器故障并将其传递给错误处理程序。
} catch (e) { throw _handleError(e); }
这是关键的一步。 您必须预见HTTP失败,由于它们常常出于没法控制的缘由而发生。
Exception _handleError(dynamic e) { print(e); // for demo purposes only return new Exception('Server error; cause: $e'); }
此演示服务将错误记录到控制台; 在现实生活中,你会处理代码中的错误。 对于演示,这个工程。
该代码还包含传播异常给调用者的错误,以便调用者能够向用户显示适当的错误消息。
当HeroDetailComponent要求HeroService获取一个英雄时,HeroService当前获取全部英雄而且过滤器以id匹配一个hero。 对于模拟来讲这很好,可是当你只须要一个真正的服务器给全部英雄时,这是浪费的。 大多数web API支持以api / hero /:id(如api / hero / 11)的形式获取请求。
更新HeroService.getHero()方法以建立一个get-by-id请求:lib/src/hero_service.dart (getHero)
Future<Hero> getHero(int id) async { try { final response = await _http.get('$_heroesUrl/$id'); return new Hero.fromJson(_extractData(response)); } catch (e) { throw _handleError(e); } }
这个请求几乎和getHeroes()同样。 URL中的英雄id标识服务器应该更新哪一个英雄。
另外,响应中的数据是单个英雄对象而不是列表。
尽管您对getHeroes()和getHero()作了重大的内部更改,但公共签名没有更改。 你仍然从这两种方法返回一个将来。 您没必要更新任何调用它们的组件。
如今是时候添加建立和删除英雄的能力了。
尝试在英雄详情视图中编辑英雄的名字。 当你输入时,英雄的名字在视图标题中被更新。 可是,若是您单击后退按钮,更改将丢失。
更新以前没有丢失。 什么改变了? 当应用程序使用模拟英雄列表时,更新直接应用于单个应用程序范围的共享列表中的英雄对象。 如今,您正在从服务器获取数据,若是您但愿更改持续存在,则必须将其写回服务器。
在英雄细节模板的末尾,添加一个保存按钮,其中包含一个点击事件绑定,调用一个名为save()的新组件方法。lib/src/hero_detail_component.html (save)
<button (click)="save()">Save</button>
添加下面的save()方法,该方法使用英雄服务update()方法持续英雄名称更改,而后导航回到先前的视图。lib/src/hero_detail_component.dart (save)
Future<Null> save() async { await _heroService.update(hero); goBack(); }
update()方法的总体结构与getHeroes()相似,但它使用HTTP put()来保持服务器端的更改。lib/src/hero_service.dart (update)
static final _headers = {'Content-Type': 'application/json'}; Future<Hero> update(Hero hero) async { try { final url = '$_heroesUrl/${hero.id}'; final response = await _http.put(url, headers: _headers, body: JSON.encode(hero)); return new Hero.fromJson(_extractData(response)); } catch (e) { throw _handleError(e); } }
为了识别服务器应该更新哪一个英雄,英雄id在URL中被编码。 put()请求体是经过调用JSON.encode得到的英雄的JSON字符串编码。 正文内容类型(application / json)在请求头中被标识。
刷新浏览器,更改英雄名称,保存更改,而后单击浏览器“后退”按钮。 如今应该继续进行更改。
要添加英雄,应用程序须要英雄的名字。 您可使用与添加按钮配对的输入元素。
将如下内容插入到英雄组件HTML中,位于标题后面:lib / src / heroes_component.html(add)
<div> <label>Hero name:</label> <input #heroName /> <button (click)="add(heroName.value); heroName.value=''"> Add </button> </div>
为了响应点击事件,调用组件的单击处理程序,而后清除输入字段,以便为其余名称作好准备。lib/src/heroes_component.dart (add)
Future<Null> add(String name) async { name = name.trim(); if (name.isEmpty) return; heroes.add(await _heroService.create(name)); selectedHero = null; }
当给定的名字不是空白时,处理程序将建立的命名的英雄委托给英雄服务,而后将新的英雄添加到列表中。在HeroService类中实现create()方法。lib/src/hero_service.dart (create)
Future<Hero> create(String name) async { try { final response = await _http.post(_heroesUrl, headers: _headers, body: JSON.encode({'name': name})); return new Hero.fromJson(_extractData(response)); } catch (e) { throw _handleError(e); } }
刷新浏览器并建立一些英雄。
英雄视图中的每一个英雄都应该有一个删除按钮。
将如下按钮元素添加到英雄组件HTML中,位于重复的<li>元素中的英雄名称以后。
<button class="delete" (click)="delete(hero); $event.stopPropagation()">x</button>
<li>元素如今应该以下所示:lib/src/heroes_component.html (li element)
<li *ngFor="let hero of heroes" (click)="onSelect(hero)" [class.selected]="hero === selectedHero"> <span class="badge">{{hero.id}}</span> <span>{{hero.name}}</span> <button class="delete" (click)="delete(hero); $event.stopPropagation()">x</button> </li>
除了调用组件的delete()方法以外,删除按钮的单击处理程序代码会中止单击事件的传播 - 您不但愿触发<li> click处理程序,由于这样作会选择用户将要删除的英雄 。
delete()处理程序的逻辑有点棘手:lib/src/heroes_component.dart (delete)
Future<Null> delete(Hero hero) async { await _heroService.delete(hero.id); heroes.remove(hero); if (selectedHero == hero) selectedHero = null; }
固然,你能够把英雄删除委托给英雄服务,可是组件仍然负责更新显示:若是须要的话,它会从列表中删除被删除的英雄,并重置选择的英雄。
要将删除按钮放置在英雄项目的最右侧,请添加此CSS:lib/src/heroes_component.css (additions)
button.delete { float:right; margin-top: 2px; margin-right: .8em; background-color: gray !important; color:white; }
添加英雄服务的delete()方法,该方法使用delete()HTTP方法从服务器中删除英雄:lib/src/hero_service.dart (delete)
Future<Null> delete(int id) async { try { final url = '$_heroesUrl/$id'; await _http.delete(url, headers: _headers); } catch (e) { throw _handleError(e); } }
刷新浏览器并尝试新的删除功能。
回想一下,HeroService.getHeroes()等待一个http.get()响应,并产生一个Future List <Hero>,当你只对单个结果感兴趣的时候,这是很好的。
可是请求并不老是只作一次。 您能够启动一个请求,取消它,并在服务器响应第一个请求以前发出不一样的请求。 使用期货很难实现请求取消新请求序列,但使用Streams很容易。
你要添加一个英雄搜索功能的英雄之旅。 当用户在搜索框中输入一个名字时,你会对这个名字过滤的英雄进行重复的HTTP请求。
首先建立HeroSearchService,将搜索查询发送到服务器的Web API。
lib/src/hero_search_service.dart
import 'dart:async'; import 'dart:convert'; import 'package:angular/angular.dart'; import 'package:http/http.dart'; import 'hero.dart'; @Injectable() class HeroSearchService { final Client _http; HeroSearchService(this._http); Future<List<Hero>> search(String term) async { try { final response = await _http.get('app/heroes/?name=$term'); return _extractData(response) .map((json) => new Hero.fromJson(json)) .toList(); } catch (e) { throw _handleError(e); } } dynamic _extractData(Response resp) => JSON.decode(resp.body)['data']; Exception _handleError(dynamic e) { print(e); // for demo purposes only return new Exception('Server error; cause: $e'); } }
HeroSearchService中的_http.get()调用相似于HeroService中的调用,尽管URL如今有一个查询字符串。
建立一个调用新的HeroSearchService的HeroSearchComponent。
组件模板很简单 - 只是一个文本框和匹配的搜索结果列表。
lib/src/hero_search_component.html
<div id="search-component"> <h4>Hero Search</h4> <input #searchBox id="search-box" (change)="search(searchBox.value)" (keyup)="search(searchBox.value)" /> <div> <div *ngFor="let hero of heroes | async" (click)="gotoDetail(hero)" class="search-result" > {{hero.name}} </div> </div> </div>
另外,为新组件添加样式。lib/src/hero_search_component.css
.search-result { border-bottom: 1px solid gray; border-left: 1px solid gray; border-right: 1px solid gray; width:195px; height: 20px; padding: 5px; background-color: white; cursor: pointer; } #search-box { width: 200px; height: 20px; }
当用户键入搜索框时,键入事件绑定将使用新的搜索框值调用组件的search()方法。 若是用户使用鼠标操做粘贴文本,则会触发更改事件绑定。
正如所料,* ngFor从组件的英雄属性重复英雄对象。
但正如你很快就会看到的,英雄的财产如今是一个英雄列表的流,而不只仅是一个英雄名单。 * ngFor只能经过异步管道(AsyncPipe)进行路由才能对Stream执行全部操做。 异步管道subscribes 流并产生* ngFor的英雄列表。
建立HeroSearchComponent类和元数据。lib/src/hero_search_component.dart
import 'dart:async'; import 'package:angular/angular.dart'; import 'package:angular_router/angular_router.dart'; import 'package:stream_transform/stream_transform.dart'; import 'hero_search_service.dart'; import 'hero.dart'; @Component( selector: 'hero-search', templateUrl: 'hero_search_component.html', styleUrls: const ['hero_search_component.css'], directives: const [CORE_DIRECTIVES], providers: const [HeroSearchService], pipes: const [COMMON_PIPES], ) class HeroSearchComponent implements OnInit { HeroSearchService _heroSearchService; Router _router; Stream<List<Hero>> heroes; StreamController<String> _searchTerms = new StreamController<String>.broadcast(); HeroSearchComponent(this._heroSearchService, this._router) {} // Push a search term into the stream. void search(String term) => _searchTerms.add(term); Future<Null> ngOnInit() async { heroes = _searchTerms.stream .transform(debounce(new Duration(milliseconds: 300))) .distinct() .transform(switchMap((term) => term.isEmpty ? new Stream<List<Hero>>.fromIterable([<Hero>[]]) : _heroSearchService.search(term).asStream())) .handleError((e) { print(e); // for demo purposes only }); } void gotoDetail(Hero hero) { var link = [ 'HeroDetail', {'id': hero.id.toString()} ]; _router.navigate(link); } }
Search terms
聚焦 _searchTerms:
StreamController<String> _searchTerms = new StreamController<String>.broadcast(); // Push a search term into the stream. void search(String term) => _searchTerms.add(term);
正如其名称所暗示的,StreamController是Stream的控制器,例如,容许您经过向其添加数据来操做基础流。
在示例中,基础的字符串流(_searchTerms.stream)表示由用户输入的英雄名称搜索模式。 每次调用search()都会经过调用控制器上的add()将新的字符串放入流中。
初始化英雄属性(ngOnInit)
您能够将搜索条件流转换为英雄列表流,并将结果分配给heroes属性。
Stream<List<Hero>> heroes; Future<Null> ngOnInit() async { heroes = _searchTerms.stream .transform(debounce(new Duration(milliseconds: 300))) .distinct() .transform(switchMap((term) => term.isEmpty ? new Stream<List<Hero>>.fromIterable([<Hero>[]]) : _heroSearchService.search(term).asStream())) .handleError((e) { print(e); // for demo purposes only }); }
将每一个用户的按键直接传递给HeroSearchService将会建立过多的HTTP请求,从而致使服务器资源和经过蜂窝网络数据计划烧毁。
相反,您能够将减小请求流的Stream运算符连接到字符串Stream。 您将减小对HeroSearchService的调用,而且仍然能够获得及时的结果。 就是这样:
将英雄搜索HTML元素添加到DashboardComponent模板的底部。lib/src/dashboard_component.html
<h3>Top Heroes</h3> <div class="grid grid-pad"> <a *ngFor="let hero of heroes" [routerLink]="['HeroDetail', {id: hero.id.toString()}]" class="col-1-4"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </a> </div> <hero-search></hero-search>
最后,从hero_search_component.dart导入HeroSearchComponent,并将其添加到directives 列表中。
lib/src/dashboard_component.dart (search)
import 'hero_search_component.dart'; @Component( selector: 'my-dashboard', templateUrl: 'dashboard_component.html', styleUrls: const ['dashboard_component.css'], directives: const [CORE_DIRECTIVES, HeroSearchComponent, ROUTER_DIRECTIVES], )
再次运行应用程序。 在仪表板中,在搜索框中输入一些文字。 若是你输入的字符匹配任何现有的英雄名字,你会看到这样的东西。
查看此页面的实例(查看源代码)中的示例源代码。 确认您具备如下结构:
你在旅程的尽头,你已经完成了不少。
返回到学习路径,您能够在这里阅读本教程中的概念和实践。