神刀安全网

Get Your Angular 2 On: Upgrading from 1.5

I started out wanting to write a step-by-step guide for upgrading an app from Angular 1.5 to Angular 2, before I was politely informed by my editor that she needed an article rather than a novel. After much deliberation, I accepted that I needed to start with a broad survey of the changes in Angular 2, hitting all the points covered in Jason Aden’s Getting Past Hello World in Angular 2 article . …Oops. Go ahead and read it to get an overview of Angular 2’s new features, but for a hands-on approach keep your browser right here.

I want this to become a series that eventually encompasses the entire process of upgrading our demo app to Angular 2. For now, though, let’s start with a single service. Let’s take a meandering walk through the code and I’ll answer any questions you may have, such as….

‘OH NO WHY IS EVERYTHING SO DIFFERENT’

Angular: The Old Way

If you’re like me, the Angular 2 quickstart guide might have been the first time you ever looked at TypeScript. Real quickly, according to its own website , TypeScript is “a typed superset of JavaScript that compiles to plain JavaScript”. You install the transpiler (similar to Babel or Traceur) and you wind up with a magical language that supports ES2015 & ES2016 language features as well as strong typing.

You may find it reassuring to know that none of this arcane setup is strictly necessary. It’s not terribly difficult to write Angular 2 code in plain old JavaScript , although I don’t think it’s worth it to do so. It’s nice to recognize familiar territory, but so much of what’s new and exciting about Angular 2 is its new way of thinking rather than its new architecture.

Get Your Angular 2 On: Upgrading from 1.5

What’s new and exciting about Angular 2 is its new way of thinking rather than its new architecture.

So let’s look at this service that I upgraded from Angular 1.5 to 2.0.0-beta.17. It’s a fairly standard Angular 1.x service, with just a couple of interesting features that I tried to note in the comments. It’s a bit more complicated than your standard toy application, but all it’s really doing is querying Zilyo, a freely-available API that aggregates listings from rental providers like Airbnb. Sorry, it’s quite a bit of code.

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); 

The wrinkle in this particular app is that it shows results on a map. Other services handle multiple pages of results by implementing pagination or lazy scrollers, which allows them to retrieve one neat page of results at a time. However, we want to show all results within the search area, and we want them to appear as soon as they return from the server rather than suddenly appearing once all pages are loaded. Additionally, we want to display progress updates to the user so that they have some idea of what’s happening.

Related: The Vital Guide to AngularJS Interviewing

In order to accomplish this in Angular 1.5, we resort to callbacks. Promises get us partway there, as you can see from the $q.all wrapper that triggers the onCompleted callback, but things still get pretty messy.

Then we bring in lodash to create all of the page requests for us, and each request is responsible for executing the onFetchPage callback to make sure that it’s added to the map as soon as it’s available. But that gets complicated. As you can see from the comments, I got lost in my own logic and couldn’t get a handle on what was being returned to which promise when.

The overall neatness of the code suffers even more (far more than is strictly necessary), because once I become confused, it only spirals downward from there. Say it with me, please…

‘THERE HAS TO BE A BETTER WAY’

Angular 2: A New Way of Thinking

There is a better way, and I’m going to show it to you. I’m not going to spend too much time on the ES6 (a.k.a. ES2015) concepts, because there are far better places to learn about that stuff, and if you need a jumping-off point, ES6-Features.org has a good overview of all of the fun new features. Consider this updated AngularJS 2 code:

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

Cool! Let’s walk through this line by line. Again, the TypeScript transpiler lets us use any ES6 features that we want because it converts everything to vanilla JavaScript.

The import statements at the beginning are simply using ES6 to load in the modules that we need. Since I do most of my development in ES5 (aka regular JavaScript), I must admit that it’s a bit annoying to suddenly need to start listing every object that I plan to use.

However, keep in mind that TypeScript is transpiling everything down to JavaScript and is secretly using SystemJS to handle module loading. The dependencies are all being loaded in asynchronously, and it is (allegedly) able to bundle your code in a way that strips out symbols that you haven’t imported. Plus it all supports “aggressive minification”, which sounds very painful. Those import statements are a small price to pay to avoid dealing with all of that noise.

Get Your Angular 2 On: Upgrading from 1.5

Import statements are a small price to pay for what’s going on behind the scenes.

Anyway, apart from loading in selective features from Angular 2 itself, take special notice of the line import {Observable} from 'rxjs/Observable'; . RxJS is a mind-bending, crazy-cool reactive programming library that provides some of the infrastructure underlying Angular 2. We will definitely be hearing from it later.

Now we come to @Injectable() .

I’m still not totally sure what that does to be honest, but the beauty of declarative programming is that we don’t always need to understand the details. It’s called a decorator, which is a fancy TypeScript construct capable of applying properties to the class (or other object) that follows it. In this case, @Injectable() teaches our service how to be injected into a component. The best demonstration comes straight from the horse’s mouth, but it’s quite long so here’s a sneak peek of how it looks in our AppComponent:

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

Next up is the class definition itself. It has an export statement before it, which means, you guessed it, we can import our service into another file. In practice, we’ll be importing our service into our AppComponent component, as above.

Get Your Angular 2 On: Upgrading from 1.5

@Injectable() teaches our service how to be injected into a component.

Right after that is the constructor, where you can see some real dependency injection in action. The line constructor(private http:Http) {} adds a private instance variable named http that TypeScript magically recognizes as an instance of the Http service. Point goes to TypeScript!

After that, it’s just some regular-looking instance variables and a utility function before we get to the real meat and potatoes, the get function. Here we see Http in action. It looks a lot like Angular 1’s promise-based approach, but under the hood it’s way cooler. Being built on RxJS means that we get a couple of big advantages over promises:

  • We can cancel the Observable if we no longer care about the response. This might be the case if we’re building a typeahead autocomplete field, and no longer care about the results for “ca” once they’ve entered “cat”.
  • The Observable can emit multiple values and the subscriber will be called over and over to consume them as they are produced.

The first one is great in a lot of circumstances, but it’s the second that we’re focusing on in our new service. Let’s go through the get function line by line:

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

It looks pretty similar to the promise-based HTTP call you would see in Angular 1. In this case, we’re sending the query parameters to get a count of all matching results.

.map(this.extractData) 

Once the AJAX call returns, it will send the response down the stream. The method map is conceptually similar to an array’s map function, but it also behaves like a promise’s then method because it waits for whatever was happening upstream to complete, regardless of synchronicity or asynchronicity. In this case, it simply accepts the response object and teases out the JSON data to pass downstream. Now we have:

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

We still have one awkward callback that we need to slide in there. See, it’s not all magic, but we can process onCountResults as soon the AJAX call returns, all without leaving our stream. That’s not too bad. As for the next line:

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

Uh oh, can you feel it? A subtle hush has come over the onlooking crowd, and you can tell that something major is about to happen. What does this line even mean? The right-hand part isn’t that crazy. It creates an RxJS range, which I think of as a glorified Observable -wrapped array. If results.totalPages equals 5, you end up with something like Observable.of([1,2,3,4,5]) .

flatMap is, wait for it, a combination of flatten and map . There’s a great video explaining the concept at Egghead.io , but my strategy is to think of every Observable as an array. Observable.range creates its own wrapper, leaving us with the 2-dimensional array [[1,2,3,4,5]] . flatMap flattens the outer array, leaving us with [1,2,3,4,5] , then map simply maps over the array, passing the values downstream one at a time. So this line accepts an integer ( totalPages ) and converts it into a stream of integers from 1 to totalPages . It may not seem like much, but that’s all we need to set up.

THE PRESTIGE

I really wanted to get this on one line to increase its impact, but I guess you can’t win them all. Here we see what happens to the stream of integers that we set up on the last line. They flow into this step one by one, then are added to the query as a page parameter before finally being packaged into a brand new AJAX request and sent off to fetch a page of results. Here’s that code:

.flatMap(i => {   return this.http.get(this._searchUrl, {     search: this.parameterize(Object.assign({}, params, { page: i }))   }); }) 

If totalPages was 5, we construct 5 GET requests and send them all off simultaneously. flatMap subscribes to each new Observable , so when the requests return (in any order) they are unwrapped and each response (like a page of results) is pushed downstream one at a time.

Let’s look at how this whole thing works from another angle. From our originating “count” request, we find the total number of pages of results. We create a new AJAX request for each page, and no matter when they return (or in what order), they are pushed out into the stream as soon as they are ready. All that our component needs to do is subscribe to the Observable returned by our get method, and it will receive each page, one after the other, all from a single stream. Take that, promises.

Get Your Angular 2 On: Upgrading from 1.5

The component will receive each page, one after the other, all from a single stream.

It’s all a bit anti-climactic after that:

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

As each response object arrives from the flatMap , its JSON is extracted in the same manner as the response from the count request. Tacked on to the end there is the catch operator, which helps illustrate how stream-based RxJS error handling works. It’s pretty similar to the traditional try/catch paradigm, except that the Observable object works for asynchronous error handling as well.

Whenever an error is encountered, it races downstream, skipping past operators until it encounters an error handler. In our case, the handleError method re-throws the error, allowing us to intercept it within the service but also to let the subscriber provide its own onError callback that fires even further downstream. Error handling shows us that we haven’t taken full advantage of our stream, even with all the cool stuff we’ve already accomplished. It’s trivial to add a retry operator after our HTTP requests, which retries an individual request if it returns an error. As a preventative measure, we could also add an operator between the range generator and the requests, adding some form of rate-limiting so that we don’t spam the server with too many requests all at once.

Related: Hire the top 3% of freelance AngularJS developers.

Recap: Learning Angular 2 Isn’t Just About a New Framework

Learning Angular 2 is more like meeting an entirely new family, and some of their relationships are complicated. Hopefully I’ve managed to demonstrate that these relationships evolved for a reason, and there’s a lot to be gained by respecting the dynamics that exist within this ecosystem. Hopefully you enjoyed this article as well, because I’ve barely scratched the surface, and there’s a lot more to say on this subject.

About the author

Ethan James, United States

member since November 2, 2015

JavaScript HTML5 CSS3 Objective-C PHP jQuery WordPress PostgreSQL MySQL

Ethan has worked on many different stacks, but always returns to his first love: JavaScript. He delights in connecting RESTful APIs to painstakingly-engineered front-ends using AngularJS, or whichever other framework provides the best user experience for the project. He writes clean, semantic HTML5 and CSS3 and is as responsive as the sites he builds. He is pleasant and diligent. [click to continue…]

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Get Your Angular 2 On: Upgrading from 1.5

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址