开始使用Angular 2:从1.5升级

本文概述

我开始是想写一个逐步指南, 将应用程序从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的许多新奇之处在于它的新思维方式, 而不是新架构。

本文介绍了将服务从1.5升级到Angular 2的过程。

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中的import语句在后台做了很多工作。

导入语句对于幕后发生的事情来说是很小的代价。

无论如何, 除了从Angular 2本身加载选择性功能外, 还要特别注意从’rxjs / Observable’;导入的行{Observable}。 RxJS是一个弯弯曲曲, 疯狂的反应式编程库, 它提供了一些基于Angular 2的基础架构。我们一定会在以后听到它。

现在我们来@Injectable()。

我仍然不确定到底该怎么做, 但是声明性编程的好处是我们不必总是了解细节。它称为装饰器, 它是一种精美的TypeScript构造, 能够将属性应用于其后的类(或其他对象)。在这种情况下, @ Injectable()会教我们的服务如何将其注入到组件中。最好的演示直接来自马的嘴, 但是时间很长, 因此可以一窥它在我们的AppComponent中的外观:

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

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

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

@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, 它将从一个流中一个接一个地接收每个页面。接受, 诺言。

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

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

在那之后, 一切都是反高潮的:

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

当每个响应对象从flatMap到达时, 其JSON的提取方式与来自count请求的响应相同。最后是catch运算符, 它有助于说明基于流的RxJS错误处理的工作方式。它与传统的try / catch范例非常相似, 不同之处在于Observable对象也可用于异步错误处理。

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

相关:雇用自由职业AngularJS开发人员中的前3%。

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

学习Angular 2更像是结识了一个全新的家庭, 他们之间的某些关系非常复杂。希望我已经证明了这些关系是有一定原因的, 并且通过尊重该生态系统中存在的动态有很多收获。希望你也喜欢这篇文章, 因为我几乎没有涉及任何内容, 并且关于这个主题还有很多要说的。

相关:所有特权, 没有麻烦:Angular 9教程

微信公众号
手机浏览(小程序)
0
分享到:
没有账号? 忘记密码?