开启 Angular 2:从 1.5 升级

已发表: 2022-03-11

在我的编辑礼貌地告诉我她需要一篇文章而不是小说之前,我开始想写一个将应用程序从 Angular 1.5 升级到 Angular 2 的分步指南。 经过深思熟虑,我接受了我需要从对 Angular 2 的变化进行广泛调查开始,达到 Jason Aden 的在 Angular 2 中超越 Hello World 文章中涵盖的所有要点。 …哎呀。 继续阅读它以了解 Angular 2 的新功能的概述,但如果要动手实践,请将您的浏览器放在这里。

我希望这成为一个系列,最终涵盖将我们的演示应用程序升级到 Angular 2 的整个过程。不过,现在让我们从一个服务开始。 让我们慢慢浏览代码,我会回答您可能遇到的任何问题,例如……。

“哦,不,为什么一切都如此不同”

Angular:老路

如果您像我一样,Angular 2 快速入门指南可能是您第一次查看 TypeScript。 很快,根据它自己的网站,TypeScript 是“编译为纯 JavaScript 的 JavaScript 的类型化超集”。 您安装了转译器(类似于 Babel 或 Traceur),您最终会得到一种支持 ES2015 和 ES2016 语言功能以及强类型的神奇语言。

知道这些神秘的设置都不是绝对必要的,您可能会感到欣慰。 用普通的旧 JavaScript 编写 Angular 2 代码并不难,尽管我认为这样做不值得。 很高兴认识熟悉的领域,但 Angular 2 的许多新奇和令人兴奋的是它的新思维方式,而不是它的新架构。

这篇文章介绍了将服务从 1.5 升级到 Angular 2。

Angular 2 的新奇和令人兴奋的是它的新思维方式,而不是它的新架构。
鸣叫

那么让我们看看我从 Angular 1.5 升级到 2.0.0-beta.17 的这个服务。 这是一个相当标准的 Angular 1.x 服务,只有几个我试图在评论中指出的有趣功能。 它比您的标准玩具应用程序复杂一点,但它真正做的只是查询 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 中实现这一点,我们求助于回调。 从触发onCompleted回调的$q.all包装器中可以看出,Promise 让我们走到了一半,但事情仍然变得非常混乱。

然后我们引入 lodash 来为我们创建所有的页面请求,每个请求都负责执行onFetchPage回调,以确保它在可用时立即添加到地图中。 但这变得复杂了。 正如您从评论中看到的那样,我迷失在自己的逻辑中,无法掌握何时返回哪个承诺。

代码的整体整洁度受到的影响更大(远远超过严格必要的程度),因为一旦我感到困惑,它只会从那里螺旋式下降。 请跟我说吧……

'一定有更好的方法'

Angular 2:一种新的思维方式

有一种更好的方法,我将向您展示。 我不会花太多时间在 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 来处理模块加载。 依赖项都是异步加载的,并且(据称)能够以一种去除您尚未导入的符号的方式捆绑您的代码。 加上它都支持“aggressive minification”,这听起来很痛苦。 为了避免处理所有这些噪音,这些导入语句是一个很小的代价。

Angular 中的导入语句在幕后做了很多工作。

导入语句是为幕后发生的事情付出的小代价。

无论如何,除了从 Angular 2 本身加载选择性功能外,请特别注意import {Observable} from 'rxjs/Observable'; . RxJS 是一个令人费解的、超酷的响应式编程库,它提供了 Angular 2 的一些基础设施。我们以后肯定会听到它的消息。

现在我们来到@Injectable()

老实说,我仍然不能完全确定它的作用,但声明式编程的美妙之处在于我们并不总是需要了解细节。 它被称为装饰器,它是一种奇特的 TypeScript 构造,能够将属性应用到它后面的类(或其他对象)。 在这种情况下, @Injectable()会教我们的服务如何注入到组件中。 最好的演示直接来自马的嘴,但它很长,所以这里先看看它在我们的 AppComponent 中的外观:

 @Component({ ... providers: [HTTP_PROVIDERS, ..., ZilyoService] })

接下来是类定义本身。 它前面有一个export语句,这意味着,你猜对了,我们可以import我们的服务导入另一个文件。 在实践中,我们将把我们的服务导入到我们的AppComponent组件中,如上所述。

@Injectable() 教我们的服务如何注入组件。

@Injectable() 教我们的服务如何注入组件。

紧随其后的是构造函数,您可以在其中看到一些真正的依赖注入。 行constructor(private http:Http) {}添加了一个名为http的私有实例变量,TypeScript 神奇地将其识别为 Http 服务的实例。 重点是 TypeScript!

在那之后,在我们进入真正的肉类和土豆之前,它只是一些常规的实例变量和一个实用函数,即get函数。 在这里,我们看到了Http的作用。 它看起来很像 Angular 1 的基于 Promise 的方法,但在底层它更酷。 建立在 RxJS 上意味着我们比 Promise 有几个很大的优势:

  • 如果我们不再关心响应,我们可以取消Observable 。 如果我们正在构建一个预先输入的自动完成字段,并且在他们输入“cat”后不再关心“ca”的结果,则可能会出现这种情况。
  • Observable可以发出多个值,并且订阅者将被一遍又一遍地调用以在它们产生时使用它们。

第一个在很多情况下都很棒,但这是我们在新服务中关注的第二个。 让我们逐行浏览get函数:

 return this.http.get(this._countUrl, { search: this.parameterize(params) })

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

 .map(this.extractData)

一旦 AJAX 调用返回,它将沿着流向下发送响应。 map方法在概念上类似于数组的map函数,但它的行为也类似于 Promise 的then方法,因为它等待上游发生的任何事情完成,而不管同步性或异步性如何。 在这种情况下,它只接受响应对象并梳理出 JSON 数据以传递到下游。 现在我们有:

 .map(results => { if (typeof onCountResults === "function") { onCountResults(results.totalResults); } return results; })

我们仍然有一个尴尬的回调需要滑入其中。 瞧,这并不全是魔法,但我们可以在 AJAX 调用返回后立即处理onCountResults ,而无需离开我们的流。 这还不错。 至于下一行:

.flatMap(results => Observable.range(1, results.totalPages))

哦哦,你能感觉到吗? 围观的人群中传来一阵微妙的安静,你可以看出有大事要发生了。 这条线甚至意味着什么? 右手部分没那么疯狂。 它创建了一个 RxJS 范围,我认为它是一个美化的Observable包装的数组。 如果results.totalPages等于 5,你最终会得到类似Observable.of([1,2,3,4,5])的东西。

flatMap是,等待它, flattenmap的组合。 Egghead.io 上有一个很棒的视频解释了这个概念,但我的策略是将每个Observable视为一个数组。 Observable.range创建了自己的包装器,给我们留下了二维数组[[1,2,3,4,5]]flatMap将外部数组展平,留下[1,2,3,4,5] ,然后map简单地映射到数组上,一次将值传递到下游。 所以这一行接受一个整数 ( 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 ,因此当请求返回(以任何顺序)时,它们会被解包,并且每个响应(如一页结果)一次被推送到下游。

让我们从另一个角度看看这整个事情是如何运作的。 从我们的原始“计数”请求中,我们找到了结果的总页数。 我们为每个页面创建一个新的 AJAX 请求,无论它们何时返回(或以何种顺序),一旦它们准备好就会被推送到流中。 我们的组件需要做的就是订阅我们的get方法返回的 Observable,它将一个接一个地接收来自单个流的每个页面。 拿那个,承诺。

每个响应一次被推送到下游。

该组件将一个接一个地从单个流中接收每个页面。

之后就有点反高潮了:

 .map(this.extractData).catch(this.handleError);

当每个响应对象从flatMap到达时,其 JSON 的提取方式与计数请求的响应相同。 附加到最后的是catch运算符,它有助于说明基于流的 RxJS 错误处理是如何工作的。 它与传统的 try/catch 范例非常相似,除了Observable对象也适用于异步错误处理。

每当遇到错误时,它都会向下游竞争,跳过过去的运算符,直到遇到错误处理程序。 在我们的例子中, handleError方法重新抛出错误,允许我们在服务中拦截它,但也让订阅者提供自己的onError回调,该回调会在下游触发。 错误处理向我们展示了我们没有充分利用我们的流,即使我们已经完成了所有很酷的事情。 在我们的 HTTP 请求之后添加一个retry运算符很简单,如果它返回错误,它会重试单个请求。 作为预防措施,我们还可以在range生成器和请求之间添加一个运算符,添加某种形式的速率限制,这样我们就不会一次向服务器发送过多请求的垃圾邮件。

相关:聘请前 3% 的自由 AngularJS 开发人员。

回顾:学习 Angular 2 不仅仅是一个新框架

学习 Angular 2 更像是认识一个全新的家庭,他们的一些关系很复杂。 希望我已经成功地证明了这些关系的演变是有原因的,尊重这个生态系统中存在的动态会有很多好处。 希望您也喜欢这篇文章,因为我几乎没有触及到表面,关于这个主题还有很多话要说。

相关:所有特权,无后顾之忧:Angular 9 教程