AngularDart4.0 英雄之旅-教程-08HTTP

在此頁面中,您將進行如下改進。css

  • 從服務器獲取英雄數據。
  • 讓用戶添加,編輯和刪除英雄的名字。
  • 將更改保存到服務器。

您將教會應用程序對遠程服務器的Web API進行相應的HTTP調用。html

當你完成這個頁面,應用程序應該看起來像這個實例(查看源代碼)。java

你離開的地方
在前一頁中,您學會了在儀表板和固定英雄列表之間導航,沿途編輯選定的英雄。 這是這個頁面的起點。git

在繼續英雄之旅以前,請確認您具備如下結構。github

若是該應用程序還沒有運行,請啓動該應用程序。 在進行更改時,請經過從新加載瀏覽器窗口來保持運行。web

提供HTTP服務

您將使用Dart http軟件包的客戶端類與服務器進行通訊。json

Pubspec更新

經過添加Dart httpstream_transform軟件包來更新軟件包相關性:bootstrap

註冊HTTP服務

在應用程序可使用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 API

在你有一個能夠處理英雄數據請求的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);

英雄和HTTP

在目前的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');
}

此演示服務將錯誤記錄到控制檯; 在現實生活中,你會處理代碼中的錯誤。 對於演示,這個工程。

該代碼還包含傳播異常給調用者的錯誤,以便調用者能夠向用戶顯示適當的錯誤消息。

經過id獲取英雄

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 API

儘管您對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()方法

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

Hero服務的delete()方法

添加英雄服務的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);
  }
}

刷新瀏覽器並嘗試新的刪除功能。

Streams

回想一下,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如今有一個查詢字符串。

HeroSearchComponent

建立一個調用新的HeroSearchServiceHeroSearchComponent
組件模板很簡單 - 只是一個文本框和匹配的搜索結果列表。

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

正如其名稱所暗示的,StreamControllerStream的控制器,例如,容許您經過向其添加數據來操做基礎流。

在示例中,基礎的字符串流(_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的調用,而且仍然能夠獲得及時的結果。 就是這樣:

  • 轉換(debounce(... 300)))等待,直到搜索項的流程暫停300毫秒,而後傳遞最新的字符串。 你永遠不會比300ms更頻繁地發出請求。
  • distinct()確保僅當過濾器文本發生更改時才發送請求。
  • transform(switchMap(...))爲經過debounce()distinct()建立的每一個搜索項調用搜索服務。 它取消並放棄之前的搜索,只返回最新的搜索服務流元素。
  • handleError()處理錯誤。 這個簡單的例子將錯誤輸出到控制檯。 一個真實的應用程序應該作的更好。

將搜索組件添加到儀表板

將英雄搜索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],
)

再次運行應用程序。 在儀表板中,在搜索框中輸入一些文字。 若是你輸入的字符匹配任何現有的英雄名字,你會看到這樣的東西。

Hero Search Component

應用程序結構和代碼

查看此頁面的實例(查看源代碼)中的示例源代碼。 確認您具備如下結構:

終點直道

你在旅程的盡頭,你已經完成了不少。

  • 您添加了必要的依賴關係,以在應用程序中使用HTTP。
  • 您重構了HeroService以從Web API加載英雄。
  • 您將HeroService擴展爲支持post()put()delete()方法。
  • 您更新了組件以容許添加,編輯和刪除英雄。
  • 您配置了內存中的Web API。
  • 您瞭解瞭如何使用Streams

下一步

返回到學習路徑,您能夠在這裏閱讀本教程中的概念和實踐。

相關文章
相關標籤/搜索