在此頁面中,您將進行如下改進。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], )
再次運行應用程序。 在儀表板中,在搜索框中輸入一些文字。 若是你輸入的字符匹配任何現有的英雄名字,你會看到這樣的東西。
查看此頁面的實例(查看源代碼)中的示例源代碼。 確認您具備如下結構:
你在旅程的盡頭,你已經完成了不少。
返回到學習路徑,您能夠在這裏閱讀本教程中的概念和實踐。