Aktifkan Angular 2 Anda: Meningkatkan dari 1,5
Diterbitkan: 2022-03-11Saya mulai ingin menulis panduan langkah demi langkah untuk meningkatkan aplikasi dari Angular 1.5 ke Angular 2, sebelum saya dengan sopan diberitahu oleh editor saya bahwa dia membutuhkan artikel daripada novel. Setelah banyak pertimbangan, saya menerima bahwa saya perlu memulai dengan survei luas tentang perubahan di Angular 2, mengenai semua poin yang tercakup dalam artikel Getting Past Hello World in Angular 2 karya Jason Aden. …Ups. Silakan dan baca untuk mendapatkan gambaran umum tentang fitur-fitur baru Angular 2, tetapi untuk pendekatan langsung, simpan browser Anda di sini.
Saya ingin ini menjadi seri yang pada akhirnya mencakup seluruh proses peningkatan aplikasi demo kami ke Angular 2. Namun, untuk saat ini, mari kita mulai dengan satu layanan. Mari kita berjalan-jalan melalui kode dan saya akan menjawab pertanyaan apa pun yang mungkin Anda miliki, seperti….
Sudut: Cara Lama
Jika Anda seperti saya, panduan memulai cepat Angular 2 mungkin pertama kali Anda melihat TypeScript. Sangat cepat, menurut situs webnya sendiri, TypeScript adalah "superset yang diketik dari JavaScript yang dikompilasi ke JavaScript biasa". Anda menginstal transpiler (mirip dengan Babel atau Traceur) dan Anda mendapatkan bahasa ajaib yang mendukung fitur bahasa ES2015 & ES2016 serta pengetikan yang kuat.
Anda mungkin merasa meyakinkan untuk mengetahui bahwa tidak ada pengaturan misterius ini yang benar-benar diperlukan. Tidak terlalu sulit untuk menulis kode Angular 2 dalam JavaScript lama, meskipun menurut saya tidak layak untuk melakukannya. Sangat menyenangkan untuk mengenali wilayah yang sudah dikenal, tetapi banyak hal baru dan menarik tentang Angular 2 adalah cara berpikirnya yang baru daripada arsitektur barunya.
Jadi mari kita lihat layanan ini yang saya tingkatkan dari Angular 1.5 ke 2.0.0-beta.17. Ini adalah layanan Angular 1.x yang cukup standar, dengan hanya beberapa fitur menarik yang saya coba catat di komentar. Ini sedikit lebih rumit daripada aplikasi mainan standar Anda, tetapi yang sebenarnya dilakukannya hanyalah menanyakan Zilyo, API yang tersedia secara bebas yang mengumpulkan daftar dari penyedia persewaan seperti Airbnb. Maaf, ini sedikit kode.
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);Kerutan di aplikasi khusus ini adalah menunjukkan hasil di peta. Layanan lain menangani beberapa halaman hasil dengan menerapkan pagination atau lazy scroller, yang memungkinkan mereka untuk mengambil satu halaman hasil yang rapi dalam satu waktu. Namun, kami ingin menampilkan semua hasil dalam area pencarian, dan kami ingin mereka muncul segera setelah mereka kembali dari server daripada tiba-tiba muncul setelah semua halaman dimuat. Selain itu, kami ingin menampilkan pembaruan kemajuan kepada pengguna sehingga mereka memiliki gambaran tentang apa yang terjadi.
Untuk mencapai ini di Angular 1.5, kami menggunakan panggilan balik. Janji membuat kita setengah jalan, seperti yang Anda lihat dari pembungkus $q.all yang memicu panggilan balik onCompleted , tetapi semuanya masih menjadi sangat berantakan.
Kemudian kami membawa lodash untuk membuat semua permintaan halaman untuk kami, dan setiap permintaan bertanggung jawab untuk mengeksekusi panggilan balik onFetchPage untuk memastikan bahwa itu ditambahkan ke peta segera setelah tersedia. Tapi itu menjadi rumit. Seperti yang Anda lihat dari komentar, saya tersesat dalam logika saya sendiri dan tidak dapat menangani apa yang dikembalikan ke janji mana.
Kerapihan keseluruhan kode bahkan lebih menderita (jauh lebih dari yang diperlukan), karena begitu saya menjadi bingung, itu hanya berputar ke bawah dari sana. Katakan padaku, tolong…
Sudut 2: Cara Berpikir Baru
Ada cara yang lebih baik, dan saya akan menunjukkannya kepada Anda. Saya tidak akan menghabiskan terlalu banyak waktu untuk konsep ES6 (alias ES2015), karena ada tempat yang jauh lebih baik untuk mempelajari hal itu, dan jika Anda memerlukan titik awal, ES6-Features.org memiliki gambaran umum yang bagus dari semua fitur baru yang menyenangkan. Pertimbangkan kode AngularJS 2 yang diperbarui ini:
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); } }Dingin! Mari kita telusuri baris demi baris ini. Sekali lagi, transpiler TypeScript memungkinkan kita menggunakan fitur ES6 apa pun yang kita inginkan karena ia mengubah semuanya menjadi JavaScript vanilla.
Pernyataan import di awal hanya menggunakan ES6 untuk memuat modul yang kita butuhkan. Karena saya melakukan sebagian besar pengembangan saya di ES5 (alias JavaScript biasa), saya harus mengakui bahwa agak menjengkelkan tiba-tiba harus mulai membuat daftar setiap objek yang saya rencanakan untuk digunakan.
Namun, perlu diingat bahwa TypeScript mentranspilasikan semuanya ke JavaScript dan diam-diam menggunakan SystemJS untuk menangani pemuatan modul. Semua dependensi dimuat secara asinkron, dan (diduga) dapat menggabungkan kode Anda dengan cara menghapus simbol yang belum Anda impor. Plus semuanya mendukung "minifikasi agresif", yang terdengar sangat menyakitkan. Pernyataan impor itu adalah harga kecil yang harus dibayar untuk menghindari berurusan dengan semua kebisingan itu.

Bagaimanapun, selain memuat fitur selektif dari Angular 2 itu sendiri, perhatikan secara khusus import {Observable} from 'rxjs/Observable'; . RxJS adalah pustaka pemrograman reaktif yang menakjubkan dan keren yang menyediakan beberapa infrastruktur yang mendasari Angular 2. Kami pasti akan mendengarnya nanti.
Sekarang kita sampai pada @Injectable() .
Sejujurnya, saya masih tidak sepenuhnya yakin apa artinya itu, tetapi keindahan pemrograman deklaratif adalah bahwa kita tidak selalu perlu memahami detailnya. Ini disebut dekorator, yang merupakan konstruksi TypeScript mewah yang mampu menerapkan properti ke kelas (atau objek lain) yang mengikutinya. Dalam hal ini, @Injectable() mengajarkan layanan kami cara disuntikkan ke dalam komponen. Demonstrasi terbaik datang langsung dari mulut kuda, tapi cukup panjang jadi inilah sekilas tampilannya di AppComponent kami:
@Component({ ... providers: [HTTP_PROVIDERS, ..., ZilyoService] }) Selanjutnya adalah definisi kelas itu sendiri. Ini memiliki pernyataan export sebelumnya, yang berarti, Anda dapat menebaknya, kami dapat import layanan kami ke file lain. Dalam praktiknya, kami akan mengimpor layanan kami ke dalam komponen AppComponent kami, seperti di atas.
Tepat setelah itu adalah konstruktor, di mana Anda dapat melihat beberapa injeksi ketergantungan nyata beraksi. constructor(private http:Http) {} menambahkan variabel instance pribadi bernama http yang secara ajaib dikenali TypeScript sebagai instance dari layanan Http. Point pergi ke TypeScript!
Setelah itu, hanya beberapa variabel instan yang tampak biasa dan fungsi utilitas sebelum kita masuk ke daging dan kentang asli, fungsi get . Di sini kita melihat Http beraksi. Ini sangat mirip dengan pendekatan berbasis janji Angular 1, tetapi di bawah tenda itu jauh lebih keren. Dibangun di RxJS berarti kami mendapatkan beberapa keuntungan besar dibandingkan janji:
- Kami dapat membatalkan
Observablejika kami tidak lagi peduli dengan responsnya. Ini mungkin terjadi jika kita sedang membangun bidang pelengkapan otomatis typeahead, dan tidak lagi peduli dengan hasil untuk "ca" setelah mereka memasukkan "cat". -
Observabledapat memancarkan banyak nilai dan pelanggan akan dipanggil berulang kali untuk mengkonsumsinya saat diproduksi.
Yang pertama bagus dalam banyak situasi, tetapi yang kedua yang kami fokuskan dalam layanan baru kami. Mari kita lihat fungsi get baris demi baris:
return this.http.get(this._countUrl, { search: this.parameterize(params) })Ini terlihat sangat mirip dengan panggilan HTTP berbasis janji yang akan Anda lihat di Angular 1. Dalam hal ini, kami mengirimkan parameter kueri untuk mendapatkan hitungan semua hasil yang cocok.
.map(this.extractData) Setelah panggilan AJAX kembali, itu akan mengirimkan respons ke aliran. map metode secara konseptual mirip dengan fungsi map array, tetapi juga berperilaku seperti metode janji then itu karena menunggu apa pun yang terjadi di hulu selesai, terlepas dari sinkronisitas atau asinkronisitas. Dalam hal ini, ia hanya menerima objek respons dan mengeluarkan data JSON untuk diteruskan ke hilir. Sekarang kita punya:
.map(results => { if (typeof onCountResults === "function") { onCountResults(results.totalResults); } return results; }) Kami masih memiliki satu panggilan balik canggung yang perlu kami luncurkan di sana. Lihat, ini tidak semuanya ajaib, tetapi kami dapat memproses onCountResults segera setelah panggilan AJAX kembali, semuanya tanpa meninggalkan aliran kami. Itu tidak terlalu buruk. Adapun baris berikutnya:
.flatMap(hasil => Observable.range(1, hasil.totalPages))
Uh oh, bisakah kamu merasakannya? Keheningan halus telah menyelimuti kerumunan yang melihat, dan Anda dapat mengatakan bahwa sesuatu yang besar akan segera terjadi. Apa arti garis ini? Bagian tangan kanan tidak terlalu gila. Itu menciptakan rentang Observable , yang saya anggap sebagai array yang dapat diamati - yang dimuliakan. Jika results.totalPages sama dengan 5, Anda akan mendapatkan sesuatu seperti Observable.of([1,2,3,4,5]) .
flatMap adalah, tunggu dulu, kombinasi dari flatten dan map . Ada video bagus yang menjelaskan konsep di Egghead.io, tetapi strategi saya adalah menganggap setiap Observable sebagai array. Observable.range membuat pembungkusnya sendiri, meninggalkan kita dengan array 2 dimensi [[1,2,3,4,5]] . flatMap meratakan larik luar, meninggalkan kita dengan [1,2,3,4,5] , lalu map hanya memetakan di atas larik, meneruskan nilai ke hilir satu per satu. Jadi baris ini menerima bilangan bulat ( totalPages ) dan mengubahnya menjadi aliran bilangan bulat dari 1 ke totalPages . Ini mungkin tidak terlihat banyak, tetapi hanya itu yang perlu kita siapkan.
Saya benar-benar ingin mendapatkan ini di satu baris untuk meningkatkan dampaknya, tapi saya rasa Anda tidak bisa memenangkan semuanya. Di sini kita melihat apa yang terjadi pada aliran bilangan bulat yang kita atur pada baris terakhir. Mereka mengalir ke langkah ini satu per satu, kemudian ditambahkan ke kueri sebagai parameter halaman sebelum akhirnya dikemas ke dalam permintaan AJAX baru dan dikirim untuk mengambil halaman hasil. Berikut kode itu:
.flatMap(i => { return this.http.get(this._searchUrl, { search: this.parameterize(Object.assign({}, params, { page: i })) }); }) Jika totalPages adalah 5, kami membuat 5 permintaan GET dan mengirimkan semuanya secara bersamaan. flatMap berlangganan ke setiap Observable baru, jadi ketika permintaan kembali (dalam urutan apa pun) mereka dibuka dan setiap respons (seperti halaman hasil) didorong ke hilir satu per satu.
Mari kita lihat bagaimana semua ini bekerja dari sudut lain. Dari permintaan "hitungan" awal kami, kami menemukan jumlah total halaman hasil. Kami membuat permintaan AJAX baru untuk setiap halaman, dan tidak peduli kapan mereka kembali (atau dalam urutan apa), mereka didorong keluar ke aliran segera setelah mereka siap. Yang perlu dilakukan oleh komponen kita adalah berlangganan Observable yang dikembalikan oleh metode get kita, dan itu akan menerima setiap halaman, satu demi satu, semuanya dari satu aliran. Ambil itu, janji.
Semuanya agak anti-klimaks setelah itu:
.map(this.extractData).catch(this.handleError); Karena setiap objek respons datang dari flatMap , JSON-nya diekstraksi dengan cara yang sama seperti respons dari permintaan penghitungan. Di bagian akhir terdapat operator catch , yang membantu mengilustrasikan cara kerja penanganan kesalahan RxJS berbasis aliran. Ini sangat mirip dengan paradigma try/catch tradisional, kecuali bahwa objek Observable juga berfungsi untuk penanganan kesalahan asinkron.
Setiap kali terjadi kesalahan, ia berlari ke hilir, melewati operator hingga menemukan penangan kesalahan. Dalam kasus kami, metode handleError mengembalikan kesalahan, memungkinkan kami untuk mencegatnya di dalam layanan, tetapi juga membiarkan pelanggan menyediakan panggilan balik onError sendiri yang memicu lebih jauh ke hilir. Penanganan kesalahan menunjukkan kepada kami bahwa kami belum memanfaatkan sepenuhnya aliran kami, bahkan dengan semua hal keren yang telah kami capai. Sangat sepele untuk menambahkan operator retry setelah permintaan HTTP kami, yang mencoba ulang permintaan individual jika mengembalikan kesalahan. Sebagai tindakan pencegahan, kami juga dapat menambahkan operator antara generator range dan permintaan, menambahkan beberapa bentuk pembatasan kecepatan sehingga kami tidak mengirim spam ke server dengan terlalu banyak permintaan sekaligus.
Rekap: Belajar Angular 2 Bukan Hanya Tentang Kerangka Baru
Belajar Angular 2 lebih seperti bertemu keluarga yang sama sekali baru, dan beberapa hubungan mereka rumit. Semoga saya berhasil menunjukkan bahwa hubungan ini berkembang karena suatu alasan, dan ada banyak hal yang bisa diperoleh dengan menghormati dinamika yang ada dalam ekosistem ini. Semoga Anda menikmati artikel ini juga, karena saya baru saja menggores permukaannya, dan masih banyak lagi yang bisa dikatakan tentang hal ini.
