英雄指南——HTTP

版本:4.0.0+2

在本章,你會做以下改進。

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

你會教給應用發(fā)起到一個遠程服務器的 web API 的相應的 HTTP 請求。

當你按照本章做完這一切,應用看起來應該這樣——在線示例 (查看源碼)。

你離開的地方

前一章中,你學會了在儀表盤和固定的英雄列表之間導航,并編輯選定的英雄。這也就是本章的起點。

在繼續(xù)英雄指南之前,驗證你是否有如下結(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

如果應用還沒有運行,使用pub serve啟動應用。當你做出改變時,通過刷新瀏覽器窗口,使其保持運行。

提供 HTTP 服務

你將使用 Dart的 http 包的客戶端類來與服務器通信。

更新 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 服務

在應用可以使用BrowserClient之前,你必須把它當一個服務提供器注冊。

你在應用的任何地方都應該能訪問BrowserClient服務。因此在bootstrap 調(diào)用中注冊它,那是你啟動應用和根組件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列表中有同樣的效果。

注意:除非你有一個適當配置的后端服務器(或一個模擬服務器),否則,這個應用并不工作。接下來的部分展示如何與一個后端服務器模擬交互。

模擬 Web API

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

使用模擬服務器的版本來更新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 服務代替BrowserClient,與遠程服務器進行會話。內(nèi)存 Web API 服務如下所示,是通過http庫的MockClient類來實現(xiàn)的。所有的http客戶端實現(xiàn)都共享了一個通用的Client接口。所以你要在應用中使用Client 類型,以便在各個實現(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 服務,模擬內(nèi)存服務將以 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的實現(xiàn)中,返回一個 Future 類型的模擬英雄。

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

這是最終使用 HTTP 客戶端來獲取英雄的預期實現(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');
}

更新導入語句如下:

// 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ù)應該從模擬服務器中成功加載。

HTTP Future

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

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

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

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

錯誤處理

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

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

這是一個關(guān)鍵的步驟!你必須預料到 HTTP 請求會失敗,因為有太多超出控制的原因可能導致它們頻繁發(fā)生。

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

這個示例服務把錯誤記錄到控制臺中;在真實世界中,你應該在代碼中處理錯誤。作為一個展示,它能工作就夠了。

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

通過 id 獲取英雄

HeroDetailComponent請求HeroService來獲取一個英雄時,HeroService當前獲取所有英雄并通過匹配id來過濾這一個。對于一個模擬環(huán)境是很好的,但當你只想要一個時,卻向真實的服務器請求所有的英雄是浪費的。多數(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 標識服務器應該更新哪個英雄。

另外,響應中的data是一個單一的英雄對象而不是一個列表。

未改變的 getHeroes API

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

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

更新英雄詳情

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

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

添加保存英雄詳情的功能

在英雄詳情模板的結(jié)尾添加一個帶click事件綁定的保存按鈕,事件綁定會調(diào)用組件中一個名為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()方法的總體結(jié)構(gòu)和getHeroes()很相似,但它使用 HTTPput()來把修改保存到服務器端。

// 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 中。putt()body 參數(shù)是通過調(diào)用JSON.encode獲得的英雄的 JSON 字符串編碼。body 的內(nèi)容類型(application/json)被標記在請求頭中。

刷新瀏覽器,改變一個英雄的名字,保存你的修改,并點擊瀏覽器的后退按鈕。修改現(xiàn)在應該保存了。

增加添加英雄的功能

要添加一個英雄,應用需要這個英雄的名字。你可以使用一個input元素搭配一個添加按鈕。

在 heroes 組件的 HTML 中,緊跟標題的后面,插入如下內(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>

響應一個點擊事件,調(diào)用組件的點擊處理器,然后清空這個輸入框,以便準備好輸入另一個名字。

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

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

HeroService類中實現(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)建一些英雄。

增加刪除英雄的功能

在英雄視圖中的每個英雄都應該有一個刪除按鈕。

把下面的按鈕元素添加到 heroes 組件的 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>

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

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 的delete()方法從服務器上移除英雄:

// 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()響應,并生成一個List<Hero>Future。當你只對一個單一的結(jié)果感興趣時,這很不錯。

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

增加按名搜索的功能

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

先創(chuàng)建HeroSearchService服務,它會把搜索查詢發(fā)送到服務器的 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;
}

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

不出所料,*ngFor從該組件的heroes屬性重復 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(),把新的字符串放進數(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 請求,浪費服務器資源并消耗大量網(wǎng)絡流量。

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

  • transform(debounce(... 300))):在傳遞最終的字符串之前,會一直等待,直到搜索條目暫停輸入300毫秒。你發(fā)起請求的頻率永遠不會超過300ms。
  • distinct():確保只在過濾文本變化時才發(fā)送請求。
  • transform(switchMap(...)):為每個已經(jīng)通過debounce()distinct()的搜索條目調(diào)用搜索服務。它取消并丟棄之前的搜索,僅返回最新的搜索服務流元素。
  • 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],
)

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

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

回顧本章示例的源代碼 在線示例 (查看源碼)。驗證你是否有如下結(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)收獲頗豐。

  • 添加了在應用中使用 HTTP 時必要的依賴。
  • 重構(gòu)了HeroService,從一個 Web API 來加載英雄。
  • 擴展了HeroService,以支持post()、put()delete()方法。
  • 更新了組件,以允許英雄的添加、編輯和刪除。
  • 配置了一個內(nèi)存 Web API。
  • 學會了如何使用 Streams。

下一步

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,545評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,983評論 25 709
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,211評論 4 61
  • —————傳說中的目錄—————2017年目標 學習.護膚化妝.身材.服裝.理財 學習 1堅持早起并翻譯艾力老師的...
    田田07056閱讀 308評論 1 1
  • 《夏洛特煩惱》在2014年的時候已經(jīng)上映,直到最近才完整的看了一遍。劇情講述的是男主人公夏洛前去參加自己曾經(jīng)暗戀的...
    這些與夢想飛舞的日子閱讀 317評論 0 1

友情鏈接更多精彩內(nèi)容