英雄指南——HTTP

版本:4.0.0+2

在本章,你會做以下改進(jìn)。

  • 從一個服務(wù)器獲取英雄數(shù)據(jù)篮灼。
  • 讓用戶添加、編輯和刪除英雄徘禁。
  • 保存改變到服務(wù)器诅诱。

你會教給應(yīng)用發(fā)起到一個遠(yuǎn)程服務(wù)器的 web API 的相應(yīng)的 HTTP 請求。

當(dāng)你按照本章做完這一切送朱,應(yīng)用看起來應(yīng)該這樣——在線示例 (查看源碼)娘荡。

你離開的地方

前一章中,你學(xué)會了在儀表盤和固定的英雄列表之間導(dǎo)航驶沼,并編輯選定的英雄炮沐。這也就是本章的起點(diǎn)。

在繼續(xù)英雄指南之前回怜,驗(yàn)證你是否有如下結(jié)構(gòu)大年。

angular_tour_of_heroes/
|___lib/
|   |___app_component.{css,dart}
|___src/
|   |   |___dashboard_component.{css,dart,html}
|   |   |___hero.dart
|   |   |___hero_detail_component.{css,dart,html}
|   |   |___hero_service.dart
|   |   |___heroes_component.{css,dart,html}
|   |   |___mock_heroes.dart
|___test/
|   |___app_test.dart
|   |___...
|___web/
|   |___index.html
|   |___main.dart
|   |___styles.css
|___pubspec.yaml

如果應(yīng)用還沒有運(yùn)行,使用pub serve啟動應(yīng)用玉雾。當(dāng)你做出改變時翔试,通過刷新瀏覽器窗口,使其保持運(yùn)行复旬。

提供 HTTP 服務(wù)

你將使用 Dart的 http 包的客戶端類來與服務(wù)器通信垦缅。

更新 Pubspec

通過添加Dart httpstream_transform 包來更新包依賴。

// {toh-5 → toh-6}/pubspec.yaml

dependencies:            
    angular: ^4.0.0            
    angular_forms: ^1.0.0            
    angular_router: ^1.0.2            
+   http: ^0.11.0            
+   stream_transform: ^0.0.6

注冊 HTTP 服務(wù)

在應(yīng)用可以使用BrowserClient之前赢底,你必須把它當(dāng)一個服務(wù)提供器注冊失都。

你在應(yīng)用的任何地方都應(yīng)該能訪問BrowserClient服務(wù)。因此在bootstrap 調(diào)用中注冊它幸冻,那是你啟動應(yīng)用和根組件AppComponent的地方。

// 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: [])
  ]);
}

注意咳焚,你在bootstrap方法的第二個參數(shù)列表中提供BrowserClient洽损。這和在@Component注解中的providers列表中有同樣的效果。

注意:除非你有一個適當(dāng)配置的后端服務(wù)器(或一個模擬服務(wù)器)革半,否則碑定,這個應(yīng)用并不工作流码。接下來的部分展示如何與一個后端服務(wù)器模擬交互。

模擬 Web API

在你有一個處理英雄數(shù)據(jù)請求的 Web 服務(wù)器之前延刘,HTTP 客戶端會從一個模擬服務(wù)器——內(nèi)存 web API 獲取并保存數(shù)據(jù)漫试。

使用模擬服務(wù)器的版本來更新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: [])]
  ]);
}

你想要使用內(nèi)存 Web API 服務(wù)代替BrowserClient,與遠(yuǎn)程服務(wù)器進(jìn)行會話碘赖。內(nèi)存 Web API 服務(wù)如下所示驾荣,是通過http庫的MockClient類來實(shí)現(xiàn)的。所有的http客戶端實(shí)現(xiàn)都共享了一個通用的Client接口普泡。所以你要在應(yīng)用中使用Client 類型播掷,以便在各個實(shí)現(xiàn)之間自由地切換。

// 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撼班,它現(xiàn)在可以安全的刪除了歧匈。

作為通用的 Web API 服務(wù),模擬內(nèi)存服務(wù)將以 JSON 格式進(jìn)行編碼和解碼英雄砰嘁,所以給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);

英雄與 HTTP

在當(dāng)前的HeroService的實(shí)現(xiàn)中件炉,返回一個 Future 類型的模擬英雄。

Future<List<Hero>> getHeroes() async => mockHeroes;

這是最終使用 HTTP 客戶端來獲取英雄的預(yù)期實(shí)現(xiàn)矮湘,那一定是一個異步操作斟冕。

現(xiàn)在將getHeroes()轉(zhuǎn)換為使用 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');
}

更新導(dǎo)入語句如下:

// 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';

刷新瀏覽器板祝。英雄數(shù)據(jù)應(yīng)該從模擬服務(wù)器中成功加載宫静。

HTTP Future

為獲取到英雄列表,首先異步調(diào)用http.get()券时。然后使用_extractData助手方法來解碼響應(yīng)的正文孤里。

響應(yīng)的 JSON 有一個單一的data屬性,保存了調(diào)用者想要的英雄列表橘洞。所以提取這個列表捌袜,并將它作為解析后的 Future 值返回。

注意這個由服務(wù)器返回的數(shù)據(jù)的形態(tài)炸枣。這個特殊的內(nèi)存 Web API 的實(shí)例返回一個帶有data屬性的對象虏等。你的 API 可能返回其它東西。調(diào)整代碼以匹配你的 Web API适肠。

調(diào)用者并不知道從(模擬)服務(wù)器獲取英雄霍衫。和以前一樣它接收一個英雄的 Future。

錯誤處理

getHeroes()的最后侯养,你catch了服務(wù)器的失敗信息敦跌,并把它們傳給了錯誤處理器:

} catch (e) {
  throw _handleError(e);
}

這是一個關(guān)鍵的步驟!你必須預(yù)料到 HTTP 請求會失敗逛揩,因?yàn)橛刑喑隹刂频脑蚩赡軐?dǎo)致它們頻繁發(fā)生柠傍。

Exception _handleError(dynamic e) {
  print(e); // for demo purposes only
  return new Exception('Server error; cause: $e');
}

這個示例服務(wù)把錯誤記錄到控制臺中麸俘;在真實(shí)世界中,你應(yīng)該在代碼中處理錯誤惧笛。作為一個展示从媚,它能工作就夠了。

這個代碼還包含一個通過傳播異常給調(diào)用者的錯誤患整,以便調(diào)用者可以給用戶顯示恰當(dāng)?shù)腻e誤信息拜效。

通過 id 獲取英雄

當(dāng)HeroDetailComponent請求HeroService來獲取一個英雄時,HeroService當(dāng)前獲取所有英雄并通過匹配id來過濾這一個并级。對于一個模擬環(huán)境是很好的拂檩,但當(dāng)你只想要一個時,卻向真實(shí)的服務(wù)器請求所有的英雄是浪費(fèi)的嘲碧。多數(shù)的 web APIs 支持這樣的格式api/hero/:id(例如api/hero/11)的 get-by-id 請求稻励。

使用 get-by-id 請求更新HeroService.getHero()方法:

// 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 標(biāo)識服務(wù)器應(yīng)該更新哪個英雄愈涩。

另外望抽,響應(yīng)中的data是一個單一的英雄對象而不是一個列表。

未改變的 getHeroes API

盡管你對getHeroes()getHero()做了一些重要的內(nèi)部修改履婉,但公共簽名并沒有改變煤篙。從這兩個方法仍然返回一個 Future。你不需要更新任何調(diào)用了它們的組件毁腿。

現(xiàn)在是時候添加創(chuàng)建和刪除英雄的功能了辑奈。

更新英雄詳情

嘗試在英雄詳情視圖編輯一個英雄的名字了。隨著你的輸入已烤,英雄名字也會隨之在視圖的標(biāo)題更新鸠窗。但如果你點(diǎn)擊了 Back(后退)按鈕,這些修改就丟失了胯究。

之前更新是不會丟失的稍计。有什么被改變了?當(dāng)應(yīng)用使用模擬英雄列表時裕循,更新直接被應(yīng)用到了單一的臣嚣、全應(yīng)用范圍共享的列表中的英雄對象。現(xiàn)在你從一個服務(wù)器獲取數(shù)據(jù)剥哑,如果你想要保存這些更改硅则,你必須把它們寫回到服務(wù)器。

添加保存英雄詳情的功能

在英雄詳情模板的結(jié)尾添加一個帶click事件綁定的保存按鈕株婴,事件綁定會調(diào)用組件中一個名為save()的新方法抢埋。

// lib/src/hero_detail_component.html (save)

<button (click)="save()">Save</button>

添加下面的save()方法,它使用英雄服務(wù)的update()方法來保存英雄名的改變督暂,然后導(dǎo)航回前一個視圖揪垄。

// lib/src/hero_detail_component.dart (save)

Future<Null> save() async {
  await _heroService.update(hero);
  goBack();
}

給英雄服務(wù)添加 update() 方法

update()方法的總體結(jié)構(gòu)和getHeroes()很相似,但它使用 HTTPput()來把修改保存到服務(wù)器端逻翁。

// 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);
  }
}

為了確定服務(wù)器應(yīng)該更新哪個英雄饥努,英雄id被編碼進(jìn) URL 中。putt()body 參數(shù)是通過調(diào)用JSON.encode獲得的英雄的 JSON 字符串編碼八回。body 的內(nèi)容類型(application/json)被標(biāo)記在請求頭中酷愧。

刷新瀏覽器,改變一個英雄的名字缠诅,保存你的修改溶浴,并點(diǎn)擊瀏覽器的后退按鈕。修改現(xiàn)在應(yīng)該保存了管引。

增加添加英雄的功能

要添加一個英雄士败,應(yīng)用需要這個英雄的名字。你可以使用一個input元素搭配一個添加按鈕褥伴。

在 heroes 組件的 HTML 中谅将,緊跟標(biāo)題的后面,插入如下內(nèi)容:

// lib/src/heroes_component.html (add)

<div>
  <label>Hero name:</label> <input #heroName />
  <button (click)="add(heroName.value); heroName.value=''">
    Add
  </button>
</div>

響應(yīng)一個點(diǎn)擊事件重慢,調(diào)用組件的點(diǎn)擊處理器饥臂,然后清空這個輸入框,以便準(zhǔn)備好輸入另一個名字似踱。

// 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;
}

當(dāng)給定的名字不為空時隅熙,處理器委托英雄服務(wù)來創(chuàng)建這個命名英雄,然后把這個新英雄添加到列表中核芽。

HeroService類中實(shí)現(xiàn)這個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);
  }
}

刷新瀏覽器,并創(chuàng)建一些英雄狞洋。

增加刪除英雄的功能

在英雄視圖中的每個英雄都應(yīng)該有一個刪除按鈕弯淘。

把下面的按鈕元素添加到 heroes 組件的 HTML 中,把它放在重復(fù)的<li>元素里英雄名的后面吉懊。

<button class="delete"
  (click)="delete(hero); $event.stopPropagation()">x</button>

<li>元素應(yīng)該看起來像這樣:

// 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>

除了調(diào)用組件的delete()方法庐橙,這個刪除按鈕的點(diǎn)擊處理器代碼阻止點(diǎn)擊事件冒泡——你并不希望<li>的點(diǎn)擊事件處理器被觸發(fā),因?yàn)檫@樣做想要選擇英雄時用戶會刪除這個英雄借嗽。

delete()處理器的邏輯有點(diǎn)棘手:

// 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;
}

你當(dāng)然委托給英雄服務(wù)來刪除英雄态鳖,但該組件仍然負(fù)責(zé)更新顯示:如果有必要,它從列表中移除 已刪除的英雄恶导,并重置所選英雄浆竭。

添加如下 CSS 把刪除按鈕放在英雄條目的最右邊:

// 
lib/src/heroes_component.css (additions)

button.delete {
  float:right;
  margin-top: 2px;
  margin-right: .8em;
  background-color: gray !important;
  color:white;
}

英雄服務(wù)的 delete() 方法

添加英雄服務(wù)的delete()方法,使用 HTTP 的delete()方法從服務(wù)器上移除英雄:

// 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);
  }
}

刷新瀏覽器,并試試這個新的刪除功能邦泄。

數(shù)據(jù)流

回想一下删窒,HeroService.getHeroes()等待一個http.get()響應(yīng),并生成一個List<Hero>Future顺囊。當(dāng)你只對一個單一的結(jié)果感興趣時肌索,這很不錯。

但是請求并不總是只發(fā)起一次特碳。你可能開始發(fā)起一個請求诚亚,然后取消,并在服務(wù)器對第一個請求作出響應(yīng)前發(fā)起一個不同的請求午乓。一個請求-取消-新請求的序列難以通過Futures 實(shí)現(xiàn)站宗,但使用 Streams 卻很容易。

增加按名搜索的功能

你將為英雄指南添加一個英雄搜索 的特性益愈。當(dāng)用戶在搜索框中輸入名字時梢灭,你會重復(fù)發(fā)起根據(jù)名字過濾英雄的 HTTP 請求棍苹。

先創(chuàng)建HeroSearchService服務(wù)惜互,它會把搜索查詢發(fā)送到服務(wù)器的 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()調(diào)用和HeroService中的那一個類似橙依,盡管現(xiàn)在這個 URL 帶了查詢字符串枣接。

HeroSearchComponent

創(chuàng)建一個HeroSearchComponent颂暇,它調(diào)用這個新的HeroSearchService

組件模板很簡單但惶,就是一個輸入框和一個相匹配的搜索結(jié)果列表耳鸯。

// 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;
}

當(dāng)用戶在搜索框中輸入時县爬,一個 keyup 事件綁定調(diào)用該組件的search()方法,并傳入新的搜索框的值添谊。如果用戶使用鼠標(biāo)粘貼文本财喳,change 事件綁定會被觸發(fā)。

不出所料斩狱,*ngFor從該組件的heroes屬性重復(fù) hero 對象耳高。

但你很快就會看到,現(xiàn)在heroes屬性是一個英雄列表的 Stream所踊,而不再只是一個英雄列表泌枪。*ngFor 不能使用Stream做任何事,除非你通過async管道(AsyncPipe)聯(lián)通它秕岛。async管道訂閱Stream碌燕,并為*ngFor生成一個英雄列表误证。

創(chuàng)建HeroSearchComponent類及其元數(shù)據(jù)。

// 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);
  }
}
搜索條目

聚焦_searchTerms

StreamController<String> _searchTerms =
    new StreamController<String>.broadcast();

// Push a search term into the stream.
void search(String term) => _searchTerms.add(term);

StreamController修壕,顧名思義愈捅,是一個 Stream 的控制器,比如它允許你通過給它添加數(shù)據(jù)來操作潛在的數(shù)據(jù)流叠殷。

在這個例子中改鲫,字符串的潛在的數(shù)據(jù)流(_searchTerms.stream),表示與用戶輸入的搜索詞匹配的英雄名林束。每次調(diào)用search(),都會通過調(diào)用控制器的add()稽亏,把新的字符串放進(jìn)數(shù)據(jù)流壶冒。

初始化 heroes 屬性(ngOnInit

你可以把搜索條目的數(shù)據(jù)流轉(zhuǎn)換成Hero列表的數(shù)據(jù)流,并把結(jié)果賦值給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胖腾,就會創(chuàng)建海量的 HTTP 請求,浪費(fèi)服務(wù)器資源并消耗大量網(wǎng)絡(luò)流量瘪松。

相反咸作,你可以使用鏈?zhǔn)秸{(diào)用Stream操作符,減少流向字符串Stream的請求宵睦。你將發(fā)起較少的HeroSearchService調(diào)用记罚,并且仍然及時地獲得結(jié)果。做法如下:

  • transform(debounce(... 300))):在傳遞最終的字符串之前壳嚎,會一直等待桐智,直到搜索條目暫停輸入300毫秒。你發(fā)起請求的頻率永遠(yuǎn)不會超過300ms烟馅。
  • distinct():確保只在過濾文本變化時才發(fā)送請求说庭。
  • transform(switchMap(...)):為每個已經(jīng)通過debounce()distinct()的搜索條目調(diào)用搜索服務(wù)。它取消并丟棄之前的搜索郑趁,僅返回最新的搜索服務(wù)流元素刊驴。
  • handleError():處理錯誤。這個簡單的例子只是把錯誤信息打印到控制臺;實(shí)際的應(yīng)用應(yīng)該做得更好寡润。

為儀表盤添加搜索組件

把英雄搜索的 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中導(dǎo)入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],
)

再次運(yùn)行應(yīng)用。在儀表盤中的搜索框中輸入一些文字栗柒。如果你輸入的字符匹配到了任何現(xiàn)有英雄名礁扮,你將會看到如下效果:

應(yīng)用的結(jié)構(gòu)與代碼

回顧本章示例的源代碼 在線示例 (查看源碼)知举。驗(yàn)證你是否有如下結(jié)構(gòu):

angular_tour_of_heroes/
|___lib/
|    |___app_component.{css,dart}
|    |___in_memory_data_service.dart (new)
|    |___src/
|    |     |___dashboard_component.{css,dart,html}
|    |     |___hero.dart
|    |     |___hero_detail_component.{css,dart,html}
|    |     |___hero_search_component.{css,dart,html} (new)
|    |     |___hero_search_service.dart (new)
|    |     |___hero_service.dart
|    |     |___heroes_component.{css,dart,html}
|___test/
|    |___app_test.dart
|    |___...
|___web/
|    |___main.dart
|    |___index.html
|    |___styles.css
|___pubspec.yaml

最后沖刺

旅程即將結(jié)束,不過你已經(jīng)收獲頗豐太伊。

  • 添加了在應(yīng)用中使用 HTTP 時必要的依賴雇锡。
  • 重構(gòu)了HeroService,從一個 Web API 來加載英雄僚焦。
  • 擴(kuò)展了HeroService锰提,以支持post()put()delete()方法芳悲。
  • 更新了組件立肘,以允許英雄的添加、編輯和刪除名扛。
  • 配置了一個內(nèi)存 Web API谅年。
  • 學(xué)會了如何使用 Streams。

下一步

回到學(xué)習(xí)路徑肮韧,在哪里你可以閱讀更多關(guān)于在本教程中的概念和練習(xí)融蹂。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市弄企,隨后出現(xiàn)的幾起案子超燃,更是在濱河造成了極大的恐慌,老刑警劉巖拘领,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件意乓,死亡現(xiàn)場離奇詭異,居然都是意外死亡院究,警方通過查閱死者的電腦和手機(jī)洽瞬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來业汰,“玉大人伙窃,你說我怎么就攤上這事⊙幔” “怎么了为障?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長放祟。 經(jīng)常有香客問我鳍怨,道長,這世上最難降的妖魔是什么跪妥? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任鞋喇,我火速辦了婚禮,結(jié)果婚禮上眉撵,老公的妹妹穿的比我還像新娘侦香。我一直安慰自己落塑,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布罐韩。 她就那樣靜靜地躺著憾赁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪散吵。 梳的紋絲不亂的頭發(fā)上龙考,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機(jī)與錄音矾睦,去河邊找鬼晦款。 笑死,一個胖子當(dāng)著我的面吹牛顷锰,可吹牛的內(nèi)容都是我干的柬赐。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼官紫,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了州藕?” 一聲冷哼從身側(cè)響起束世,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎床玻,沒想到半個月后毁涉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡锈死,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年贫堰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片待牵。...
    茶點(diǎn)故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡其屏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缨该,到底是詐尸還是另有隱情偎行,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布贰拿,位于F島的核電站蛤袒,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏膨更。R本人自食惡果不足惜妙真,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望荚守。 院中可真熱鬧珍德,春花似錦练般、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至晴及,卻和暖如春都办,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背虑稼。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工琳钉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蛛倦。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓歌懒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親溯壶。 傳聞我的和親對象是個殘疾皇子及皂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容