开始使用Angular 2(从1.5升级)

本文概述

  • Angular:旧方法
  • Angular2:新的思维方式
我开始是想写一个逐步指南, 将应用程序从Angular 1.5升级到Angular 2, 然后编辑器向我礼貌地告知我她需要一篇文章而不是一本小说。经过深思熟虑, 我接受了我需要对Angular 2的变化进行广泛调查的开始, 达到了Jason Aden在Angular 2中获得超越世界的所有要点。 …糟糕。继续阅读它, 以大致了解Angular 2的新功能, 但要动手操作, 请将浏览器放在此处。
我希望这成为一个系列, 最终包含将我们的演示应用程序升级到Angular 2的整个过程。不过, 现在, 让我们从一个服务开始。让我们来回遍一下代码, 我将回答你可能遇到的任何问题, 例如…。
‘哦, 为什么一切都不一样’
Angular:旧方法 如果你像我一样, Angular 2快速入门指南可能是你第一次查看TypeScript。根据其自己的网站, TypeScript很快就实现了, 它是” JavaScript的类型化超集, 可以编译为纯JavaScript” 。你安装了Transpiler(类似于Babel或Traceur), 并且最终使用了一种神奇的语言, 该语言支持ES2015和ES2016语言功能以及强类型。
你可能会放心地知道, 这些奥术设置完全不是必需的。用普通的旧JavaScript编写Angular 2代码并不困难, 尽管我认为这样做是不值得的。很高兴认识熟悉的领域, 但是Angular 2的许多新奇之处在于它的新思维方式, 而不是新架构。
开始使用Angular 2(从1.5升级)

文章图片
Angular 2的新奇之处在于它的新思维方式而不是新架构。
鸣叫
因此, 让我们看一下我从Angular 1.5升级到2.0.0-beta.17的服务。这是一个相当标准的Angular 1.x服务, 只有一些有趣的功能, 我尝试在注释中加以说明。它比你的标准玩具应用程序复杂一些, 但实际上它只是在查询Zilyo, Zilyo是一个免费提供的API, 可汇总来自Airbnb等租赁提供商的列表。抱歉, 其中有很多代码。
zilyo.service.js(1.5.5)
'use strict'; function zilyoService($http, $filter, $q) {// it's a singleton, so set up some instance and static variables in the same place var baseUrl = "https://zilyo.p.mashape.com/search"; var countUrl = "https://zilyo.p.mashape.com/count"; var state = { callbacks: {}, params: {} }; // interesting function - send the parameters to the server and ask // how many pages of results there will be, then process them in handleCount function get(params, callbacks) {// set up the state object if (params) { state.params = params; } if (callbacks) { state.callbacks = callbacks; } // get a count of the number of pages of search results return $http.get(countUrl + "?" + parameterize(state.params)) .then(extractData, handleError) .then(handleCount); }// make the factory return { get : get }; // boring function - takes an object of URL query params and stringifies them function parameterize(params) { return Object.keys(params).map(key => `${key}=${params[key]}`).join("& "); }// interesting function - takes the results of the "count" AJAX call and // spins off a call for each results page - notice the unpleasant imperativeness function handleCount(response) { var pages = response.data.result.totalPages; if (typeof state.callbacks.onCountResults === "function") { state.callbacks.onCountResults(response.data); } // request each page var requests = _.times(pages, function (i) { var params = Object.assign({}, { page : i + 1 }, state.params); return fetch(baseUrl, params); }); // and wrap all requests in a promise return $q.all(requests).then(function (response) { if (typeof state.callbacks.onCompleted === "function") { state.callbacks.onCompleted(response); } return response; }); }// interesting function - fetch an individual page of results // notice how a special callback is required because the $q.all wrapper // will only return once ALL pages have been fetched function fetch(url, params) { return $http.get(url + "?" + parameterize(params)).then(function(response) { if (typeof state.callbacks.onFetchPage == "function") { // emit each page as it arrives state.callbacks.onFetchPage(response.data); } return response.data; // took me 15 minutes to realize I needed this }, (response) => console.log(response)); } // boring function - takes the result object and makes sure it's defined function extractData(res) { return res || { }; }// boring function - log errors, provide teaser for greater ambitions function handleError (error) { // In a real world app, we might send the error to remote logging infrastructure var errMsg = error.message || 'Server error'; console.error(errMsg); // log to console instead return errMsg; } }// register the service angular.module('angularZilyoApp').factory('zilyoService', zilyoService);

这个特定应用程式的缺点在于, 它会在地图上显示结果。其他服务通过实现分页或惰性滚动条来处理多页结果, 这使它们一次可以检索一个整齐的结果页。但是, 我们希望在搜索区域中显示所有结果, 我们希望它们在从服务器返回后立即显示, 而不是在所有页面加载后立即显示。此外, 我们希望向用户显示进度更新, 以便他们对正在发生的事情有所了解。
相关:AngularJS面试的重要指南
为了在Angular 1.5中完成此操作, 我们采用了回调。从$ q.all包装器中可以看到, 触发了onCompleted回调的是诺言, 但事情仍然变得很混乱。
然后, 我们引入lodash来为我们创建所有页面请求, 每个请求都负责执行onFetchPage回调, 以确保将其尽快添加到地图中。但这变得复杂。正如你从评论中看到的那样, 我迷失了自己的逻辑, 无法处理何时应返还给什么承诺。
代码的整体简洁性甚至会遭受更大的损失(远远超出严格的要求), 因为一旦我感到困惑, 它只会从那里向下盘旋。跟我说吧, 请…
‘ 一定有更好的方法’
Angular2:新的思维方式 有更好的方法, 我将向你展示。我不会花太多时间在ES6(又名ES2015)概念上, 因为那里有很多更好的地方可以学习这些知识, 并且如果你需要起点, ES6-Features.org会提供很好的概述所有有趣的新功能。考虑以下更新的AngularJS 2代码:
zilyo.service.ts(2.0.0-beta.17)
import {Injectable} from 'angular2/core'; import {Http, Response, Headers, RequestOptions} from 'angular2/http'; import {Observable} from 'rxjs/Observable'; import 'rxjs/Rx'; @Injectable() export class ZilyoService { constructor(private http: Http) {}private _searchUrl = "https://zilyo.p.mashape.com/search"; private _countUrl = "https://zilyo.p.mashape.com/count"; private parameterize(params: {}) { return Object.keys(params).map(key => `${key}=${params[key]}`).join("& "); }get(params: {}, onCountResults) { return this.http.get(this._countUrl, { search: this.parameterize(params) }) .map(this.extractData) .map(results => { if (typeof onCountResults === "function") { onCountResults(results.totalResults); } return results; }) .flatMap(results => Observable.range(1, results.totalPages)) .flatMap(i => { return this.http.get(this._searchUrl, { search: this.parameterize(Object.assign({}, params, { page: i })) }); }) .map(this.extractData) .catch(this.handleError); }private extractData(res: Response) { if (res.status < 200 || res.status > = 300) { throw new Error('Bad response status: ' + res.status); } let body = res.json(); return body.result || { }; }private handleError (error: any) { // In a real world app, we might send the error to remote logging infrastructure let errMsg = error.message || 'Server error'; console.error(errMsg); // log to console instead return Observable.throw(errMsg); } }

凉!让我们逐行浏览。同样, TypeScript编译器使我们可以使用所需的任何ES6功能, 因为它将所有内容都转换为原始JavaScript。
开头的import语句只是使用ES6加载所需的模块。由于我大部分的开发工作都是在ES5(又称常规JavaScript)中进行的, 因此我必须承认, 突然需要开始列出我打算使用的每个对象有点烦人。
但是, 请记住, TypeScript正在将所有内容转换为JavaScript, 并且秘密使用SystemJS来处理模块加载。依赖项全部以异步方式加载, 并且(可以)能够捆绑你的代码, 从而剔除尚未导入的符号。加上所有这些都支持” 积极的缩小” , 这听起来很痛苦。那些进口货单是要付出很小的代价, 以避免应付所有这些噪音。
开始使用Angular 2(从1.5升级)

文章图片
导入语句对于幕后发生的事情来说是很小的代价。
无论如何, 除了从Angular 2本身加载选择性功能外, 还要特别注意从’ rxjs / Observable’ ; 导入的行{Observable}。 RxJS是一个弯弯曲曲, 疯狂的反应式编程库, 它提供了一些基于Angular 2的基础架构。我们一定会在以后听到它。
现在我们来@Injectable()。
我仍然不确定到底该怎么做, 但是声明性编程的好处是我们不必总是了解细节。它称为装饰器, 它是一种精美的TypeScript构造, 能够将属性应用于其后的类(或其他对象)。在这种情况下, @ Injectable()会教我们的服务如何将其注入到组件中。最好的演示直接来自马的嘴, 但是时间很长, 因此可以一窥它在我们的AppComponent中的外观:
@Component({ ... providers: [HTTP_PROVIDERS, ..., ZilyoService] })

接下来是类定义本身。它前面有一个导出语句, 这意味着, 你猜对了, 我们可以将我们的服务导入另一个文件。实际上, 我们将如上所述将服务导入到AppComponent组件中。
开始使用Angular 2(从1.5升级)

文章图片
@Injectable()教我们的服务如何将其注入到组件中。
紧随其后的是构造函数, 你可以在其中看到一些实际的依赖注入。该行的构造函数(private http:Http){}添加了一个名为http的私有实例变量, TypeScript神奇地将其识别为Http服务的实例。重点是TypeScript!
之后, 只有一些看起来很普通的实例变量和一个实用程序函数, 然后才是真正的土豆, get函数。在这里, 我们看到了运行中的Http。看起来很像Angular 1基于诺言的方法, 但是在幕后它却更酷。建立在RxJS之上意味着我们比诺言有两个很大的优势:
  • 如果我们不再关心响应, 则可以取消Observable。如果我们要建立一个预先输入的自动填充字段, 并且在输入” cat” 后不再关心” ca” 的结果, 则可能是这种情况。
  • Observable可以发出多个值, 并且订户将一遍又一遍地调用订户以消耗它们。
第一个在很多情况下都很好, 但是第二个是我们在新服务中重点关注的。让我们逐行浏览get函数:
return this.http.get(this._countUrl, { search: this.parameterize(params) })

它看起来与Angular 1中的基于promise的HTTP调用非常相似。在这种情况下, 我们将发送查询参数以获取所有匹配结果的计数。
.map(this.extractData)

一旦AJAX调用返回, 它将沿流发送响应。方法映射在概念上与数组的映射函数相似, 但是它的行为也类似于promise的then方法, 因为它等待上游发生的所有事情完成, 而与同步性或异步性无关。在这种情况下, 它仅接受响应对象并挑出JSON数据以向下传递。现在我们有:
.map(results => { if (typeof onCountResults === "function") { onCountResults(results.totalResults); } return results; })

我们仍然有一个尴尬的回调, 我们需要在其中进行滑动。瞧, 这并不全是魔术, 但是我们可以在AJAX调用返回后立即处理onCountResults, 而所有这些都不会离开我们的流。还算不错至于下一行:
.flatMap(结果=> Observable.range(1, results.totalPages))
哦, 你能感觉到吗?细微的嘘声笼罩着周围的人群, 你可以说重大事件即将发生。这行甚至是什么意思?右侧部分并不那么疯狂。它创建一个RxJS范围, 我认为这是一个美化的Observable-wrapped数组。如果result.totalPages等于5, 则最终得到类似Observable.of([[1, 2, 3, 4, 5]))的信息。
等待它, flatMap是flatten和map的组合。 Egghead.io上有一段精彩的视频解释了这一概念, 但我的策略是将每个Observable视为一个数组。 Observable.range创建自己的包装器, 剩下二维数组[[1, 2, 3, 4, 5]]。 flatMap展平外部数组, 使我们剩下[1, 2, 3, 4, 5], 然后简单地映射整个数组, 一次将值向下传递一个。因此, 此行接受一个整数(totalPages), 并将其转换为从1到totalPages的整数流。看起来似乎不多, 但这就是我们需要设置的全部。
威望
我真的很想把它放在一条线上来增加影响, 但是我想你不可能全部赢得他们。在这里, 我们看到在最后一行设置的整数流发生了什么。它们一步一步地进入此步骤, 然后作为页面参数添加到查询中, 最后被打包到全新的AJAX请求中并发送出去以获取结果页面。这是代码:
.flatMap(i => { return this.http.get(this._searchUrl, { search: this.parameterize(Object.assign({}, params, { page: i })) }); })

如果totalPages为5, 则我们构造5个GET请求并同时发送所有请求。 flatMap订阅了每个新的Observable, 因此, 当请求返回(以任何顺序)时, 它们将被解包, 并且每个响应(如结果页)一次都向下游推送。
让我们从另一个Angular看待整个事情。从原始的” 计数” 请求中, 我们找到结果的总页数。我们为每个页面创建一个新的AJAX请求, 无论它们何时返回(或以什么顺序), 它们在准备就绪后都会被推送到流中。组件所需要做的就是订阅get方法返回的Observable, 它将从一个流中一个接一个地接收每个页面。接受, 诺言。
开始使用Angular 2(从1.5升级)

文章图片
该组件将从一个流中接一个接一个地接收每一页。
在那之后, 一切都是反高潮的:
.map(this.extractData).catch(this.handleError);

当每个响应对象从flatMap到达时, 其JSON的提取方式与来自count请求的响应相同。最后是catch运算符, 它有助于说明基于流的RxJS错误处理的工作方式。它与传统的try / catch范例非常相似, 不同之处在于Observable对象也可用于异步错误处理。
每当遇到错误时, 它就会向下游竞争, 跳过过去的运算符, 直到遇到错误处理程序为止。在我们的例子中, handleError方法重新抛出该错误, 使我们可以在服务中拦截该错误, 还允许订阅者提供自己的onError回调, 并在更远的下游触发。错误处理向我们表明, 即使我们已经完成了所有很酷的工作, 我们也没有充分利用我们的信息流。在我们的HTTP请求之后添加重试运算符很简单, 如果返回错误, 该操作会重试单个请求。作为一种预防措施, 我们还可以在范围生成器和请求之间添加一个运算符, 添加某种形式的速率限制, 以免我们一次不会向服务器发送太多请求。
相关:雇用自由职业AngularJS开发人员中的前3%。
回顾:学习Angular 2不只是一个新框架
学习Angular 2更像是结识了一个全新的家庭, 他们之间的某些关系非常复杂。希望我已经证明了这些关系是有一定原因的, 并且通过尊重该生态系统中存在的动态有很多收获。希望你也喜欢这篇文章, 因为我几乎没有涉及任何内容, 并且关于这个主题还有很多要说的。
【开始使用Angular 2(从1.5升级)】相关:所有特权, 没有麻烦:Angular 9教程

    推荐阅读