前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >AngularDart4.0 英雄之旅-教程-08HTTP 顶

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

作者头像
南郭先生
发布2018-08-14 15:24:27
11K0
发布2018-08-14 15:24:27
举报
文章被收录于专栏:Google DartGoogle Dart

在此页面中,您将进行以下改进。

  • 从服务器获取英雄数据。
  • 让用户添加,编辑和删除英雄的名字。
  • 将更改保存到服务器。

您将教会应用程序对远程服务器的Web API进行相应的HTTP调用。

当你完成这个页面,应用程序应该看起来像这个实例(查看源代码)。

你离开的地方 在前一页中,您学会了在仪表板和固定英雄列表之间导航,沿途编辑选定的英雄。 这是这个页面的起点。

在继续英雄之旅之前,请确认您具有以下结构。

如果该应用程序尚未运行,请启动该应用程序。 在进行更改时,请通过重新加载浏览器窗口来保持运行。

提供HTTP服务

您将使用Dart http软件包的客户端类与服务器进行通信。

Pubspec更新

通过添加Dart http和stream_transform软件包来更新软件包相关性:

注册HTTP服务

在应用程序可以使用BrowserClient之前,您必须将其注册为服务提供者。

您应该可以从应用程序的任何位置访问BrowserClient服务。 因此,请在启动应用程序及其根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: [])
  ]);
}

请注意,您在列表中提供了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

创建一个调用新的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的调用,并且仍然可以得到及时的结果。 就是这样:

  • 转换(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
Hero Search Component

应用程序结构和代码

查看此页面的实例(查看源代码)中的示例源代码。 确认您具有以下结构:

终点直道

你在旅程的尽头,你已经完成了很多。

  • 您添加了必要的依赖关系,以在应用程序中使用HTTP。
  • 您重构了HeroService以从Web API加载英雄。
  • 您将HeroService扩展为支持post(),put()和delete()方法。
  • 您更新了组件以允许添加,编辑和删除英雄。
  • 您配置了内存中的Web API。
  • 您了解了如何使用Streams。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017/11/28 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 提供HTTP服务
    • Pubspec更新
    • 注册HTTP服务
    • 模拟Web API
    • 英雄和HTTP
      • 错误处理
        • 通过id获取英雄
          • 未更改的getHeroes API
          • 更新英雄的细节
            • 添加保存英雄详情的能力
              • 添加英雄服务update()方法
                • 添加加入英雄的能力
                • 添加删除英雄的能力
                  • Hero服务的delete()方法
                  • Streams
                    • 添加按名称搜索的功能
                      • HeroSearchComponent
                        • 将搜索组件添加到仪表板
                        • 应用程序结构和代码
                        • 终点直道
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档