احصل على Angular 2 الخاص بك: الترقية من 1.5
نشرت: 2022-03-11لقد بدأت في الرغبة في كتابة دليل تفصيلي خطوة بخطوة لترقية تطبيق من Angular 1.5 إلى Angular 2 ، قبل أن أبلغني المحرر بأدب أنها بحاجة إلى مقال بدلاً من رواية. بعد الكثير من المداولات ، قبلت أنني بحاجة إلى البدء بمسح واسع للتغييرات في Angular 2 ، لتصل إلى جميع النقاط التي تناولها Jason Aden's Getting Past Hello World in Angular 2 مقالة. …وجه الفتاة. انطلق واقرأها للحصول على نظرة عامة على ميزات Angular 2 الجديدة ، ولكن للحصول على نهج عملي ، احتفظ بالمتصفح الخاص بك هنا.
أريد أن تصبح هذه سلسلة تشمل في النهاية العملية الكاملة لترقية تطبيقنا التجريبي إلى Angular 2. في الوقت الحالي ، على الرغم من ذلك ، فلنبدأ بخدمة واحدة. لنأخذ جولة متعرجة عبر الكود وسأجيب على أي أسئلة قد تكون لديكم ، مثل….
الزاوي: الطريق القديم
إذا كنت مثلي ، فقد يكون دليل البدء السريع Angular 2 هو المرة الأولى التي تنظر فيها إلى TypeScript. بسرعة حقيقية ، وفقًا لموقع الويب الخاص بها ، تعد TypeScript "مجموعة شاملة مكتوبة من JavaScript يتم تجميعها إلى JavaScript عادي". تقوم بتثبيت برنامج التحويل (على غرار Babel أو Traceur) وينتهي بك الأمر بلغة سحرية تدعم ميزات اللغة ES2015 و ES2016 بالإضافة إلى الكتابة القوية.
قد تجد أنه من المطمئن أن تعرف أن أيًا من هذا الإعداد الغامض ضروري تمامًا. ليس من الصعب للغاية كتابة رمز Angular 2 في JavaScript قديم ، على الرغم من أنني لا أعتقد أن الأمر يستحق ذلك. من الجيد التعرف على المنطقة المألوفة ، ولكن الكثير مما هو جديد ومثير حول Angular 2 هو طريقته الجديدة في التفكير بدلاً من هندسته المعمارية الجديدة.
لنلق نظرة على هذه الخدمة التي قمت بترقيتها من Angular 1.5 إلى 2.0.0-beta. إنها خدمة Angular 1.x قياسية إلى حد ما ، مع بعض الميزات المثيرة للاهتمام التي حاولت ملاحظتها في التعليقات. إنه أكثر تعقيدًا قليلاً من تطبيق لعبتك القياسي ، ولكن كل ما يفعله حقًا هو الاستعلام عن Zilyo ، وهي واجهة برمجة تطبيقات متاحة مجانًا تجمع القوائم من مزودي خدمات التأجير مثل 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);
التجاعيد في هذا التطبيق المعين هو أنه يظهر النتائج على الخريطة. تتعامل الخدمات الأخرى مع صفحات متعددة من النتائج من خلال تطبيق ترقيم الصفحات أو التمرير البطيء ، مما يسمح لهم باسترداد صفحة واحدة مرتبة من النتائج في كل مرة. ومع ذلك ، نريد عرض جميع النتائج داخل منطقة البحث ، ونريدها أن تظهر بمجرد عودتها من الخادم بدلاً من الظهور فجأة بمجرد تحميل جميع الصفحات. بالإضافة إلى ذلك ، نريد عرض تحديثات التقدم للمستخدم حتى يكون لديهم فكرة عما يحدث.
من أجل تحقيق ذلك في Angular 1.5 ، نلجأ إلى عمليات الاسترجاعات. تقودنا الوعود إلى جزء من الطريق ، كما ترون من غلاف $q.all
onCompleted
يقوم بتشغيل رد الاتصال الكامل ، لكن الأمور لا تزال فوضوية إلى حد كبير.
ثم نقوم بإحضار Lodash لإنشاء جميع طلبات الصفحة لنا ، ويكون كل طلب مسؤولاً عن تنفيذ رد الاتصال onFetchPage
للتأكد من إضافته إلى الخريطة بمجرد توفرها. لكن هذا معقد. كما ترون من التعليقات ، فقدت منطقتي الخاصة ولم أستطع التعامل مع ما تم إرجاعه إلى أي وعد ومتى.
إن الدقة العامة للشفرة تعاني أكثر (أكثر بكثير مما هو ضروري تمامًا) ، لأنه بمجرد أن أصبح مرتبكًا ، فإنه يتدحرج فقط إلى أسفل من هناك. قلها معي من فضلك ...
الزاوية 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); } }
بارد! دعنا نسير عبر هذا الخط سطرا. مرة أخرى ، يتيح لنا Transpiler TypeScript استخدام أي من ميزات ES6 التي نريدها لأنه يحول كل شيء إلى Vanilla JavaScript.
تستخدم عبارات import
في البداية ES6 لتحميل الوحدات التي نحتاجها. نظرًا لأنني أقوم بمعظم تطوري في ES5 (المعروف أيضًا باسم JavaScript العادي) ، يجب أن أعترف أنه من المزعج بعض الشيء أن أحتاج فجأة إلى البدء في سرد كل كائن أخطط لاستخدامه.
ومع ذلك ، ضع في اعتبارك أن TypeScript يقوم بنقل كل شيء إلى JavaScript ويستخدم بشكل سري SystemJS للتعامل مع تحميل الوحدة. يتم تحميل جميع التبعيات بشكل غير متزامن ، ومن (يُزعم) أنها قادرة على تجميع التعليمات البرمجية الخاصة بك بطريقة تزيل الرموز التي لم تقم باستيرادها. بالإضافة إلى أنه يدعم "تصغير شديد" ، والذي يبدو مؤلمًا للغاية. بيانات الاستيراد هذه هي ثمن ضئيل يجب دفعه لتجنب التعامل مع كل تلك الضوضاء.

على أي حال ، بصرف النظر عن تحميل الميزات الانتقائية من Angular 2 نفسها ، انتبه بشكل خاص import {Observable} from 'rxjs/Observable';
. RxJS هي مكتبة برمجة تفاعلية مذهلة ومذهلة توفر بعضًا من البنية التحتية الأساسية لـ Angular 2. وسوف نسمع منها بالتأكيد لاحقًا.
نأتي الآن إلى @Injectable()
.
ما زلت غير متأكد تمامًا مما يفعله ذلك لأكون صريحًا ، لكن جمال البرمجة التصريحية هو أننا لا نحتاج دائمًا إلى فهم التفاصيل. يطلق عليه decorator ، وهو عبارة عن بناء TypeScript خيالي قادر على تطبيق الخصائص على الفئة (أو أي كائن آخر) الذي يتبعه. في هذه الحالة ، @Injectable()
خدمتنا كيفية الحقن في أحد المكونات. يأتي أفضل عرض توضيحي مباشرةً من فم الحصان ، ولكنه طويل جدًا ، لذا إليك نظرة سريعة على كيفية ظهوره في AppComponent الخاص بنا:
@Component({ ... providers: [HTTP_PROVIDERS, ..., ZilyoService] })
التالي هو تعريف الفئة نفسها. يحتوي على بيان export
قبله ، مما يعني ، كما خمنت ، أنه يمكننا import
خدمتنا إلى ملف آخر. من الناحية العملية ، سنقوم باستيراد خدمتنا إلى مكون AppComponent
بنا ، على النحو الوارد أعلاه.
بعد ذلك مباشرة هو المُنشئ ، حيث يمكنك رؤية بعض التبعية الحقيقية قيد التنفيذ. يضيف constructor(private http:Http) {}
متغير مثيل خاص يسمى http
والذي يتعرف عليه TypeScript بطريقة سحرية كمثيل لخدمة Http. النقطة تذهب إلى TypeScript!
بعد ذلك ، إنها مجرد بعض متغيرات الحالة العادية ودالة المنفعة قبل أن نصل إلى اللحم الحقيقي والبطاطس ، وظيفة get
. هنا نرى Http
في العمل. إنه يشبه إلى حد كبير نهج Angular 1 القائم على الوعد ، ولكن تحت الغطاء يكون الأمر أكثر برودة. أن تكون مبنيًا على RxJS يعني أننا نحصل على ميزتين كبيرتين على الوعود:
- يمكننا إلغاء
Observable
إذا لم نعد نهتم بالاستجابة. قد يكون هذا هو الحال إذا كنا نبني حقل الإكمال التلقائي للرأس على الآلة الكاتبة ، ولم نعد نهتم بنتائج "ca" بمجرد إدخال كلمة "cat". - يمكن أن
Observable
قيمًا متعددة وسيتم استدعاء المشترك مرارًا وتكرارًا لاستهلاكها عند إنتاجها.
الأول رائع في كثير من الظروف ، لكنه الثاني الذي نركز عليه في خدمتنا الجديدة. دعنا ننتقل إلى get
وظيفة سطرًا بسطر:
return this.http.get(this._countUrl, { search: this.parameterize(params) })
يبدو مشابهًا تمامًا لاستدعاء HTTP المستند إلى الوعد الذي ستراه في Angular 1. في هذه الحالة ، نرسل معامِلات الاستعلام للحصول على عدد جميع النتائج المتطابقة.
.map(this.extractData)
بمجرد عودة مكالمة AJAX ، سترسل الاستجابة أسفل الدفق. تشبه map
الطريقة من الناحية المفاهيمية وظيفة map
المصفوفة ، ولكنها تتصرف أيضًا مثل طريقة الوعد في then
لأنها تنتظر اكتمال ما كان يحدث في المنبع ، بغض النظر عن التزامن أو عدم التزامن. في هذه الحالة ، يقبل ببساطة كائن الاستجابة ويثير بيانات JSON لتمريرها إلى المصب. الآن لدينا:
.map(results => { if (typeof onCountResults === "function") { onCountResults(results.totalResults); } return results; })
لا يزال لدينا رد اتصال محرج نحتاج إلى تمريره هناك. انظر ، ليس الأمر كله سحرًا ، ولكن يمكننا معالجة onCountResults
بمجرد عودة مكالمة AJAX ، كل ذلك دون مغادرة ساحة المشاركات. هذا ليس سيئا للغاية. بالنسبة للسطر التالي:
.flatMap (النتائج => Observable.range (1، results.totalPages))
اه اوه هل تشعر به؟ لقد حدث صمت خفي على الحشد الظاهر ، ويمكنك أن تقول أن شيئًا مهمًا على وشك الحدوث. ماذا يعني هذا الخط حتى؟ الجزء الأيمن ليس بهذا الجنون. إنه ينشئ نطاقًا Observable
، والذي أعتقد أنه مصفوفة ملفوفة يمكن ملاحظتها. إذا كانت results.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]
، ثم تقوم ببساطة بتعيين 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 جديد لكل صفحة ، وبغض النظر عن وقت عودتهم (أو بأي ترتيب) ، يتم دفعهم إلى الدفق بمجرد أن يكونوا جاهزين. كل ما يحتاجه المكون لدينا هو الاشتراك في Observable الذي يتم إرجاعه بواسطة طريقة get
الخاصة بنا ، وسيتلقى كل صفحة ، واحدة تلو الأخرى ، كل ذلك من دفق واحد. خذ هذا ، وعود.
كل شيء يصبح معادًا للذروة قليلاً بعد ذلك:
.map(this.extractData).catch(this.handleError);
عندما يصل كل كائن استجابة من flatMap
، يتم استخراج JSON الخاص به بنفس طريقة الاستجابة من طلب الجرد. تم تثبيت عامل catch
حتى النهاية ، والذي يساعد في توضيح كيفية عمل معالجة أخطاء RxJS القائمة على التدفق. إنه مشابه إلى حد كبير لنموذج try / catch التقليدي ، باستثناء أن الكائن Observable
يعمل لمعالجة الأخطاء غير المتزامنة أيضًا.
عند مواجهة أي خطأ ، فإنه يتقدم بسرعة ، ويتخطى العوامل السابقة حتى يواجه معالج خطأ. في حالتنا ، فإن طريقة handleError
تعيد طرح الخطأ ، مما يسمح لنا باعتراضه داخل الخدمة ولكن أيضًا للسماح للمشترك بتقديم رد onError
الخاص به والذي يؤدي إلى مزيد من التشغيل في اتجاه آخر. توضح لنا معالجة الأخطاء أننا لم نستفد بشكل كامل من البث لدينا ، حتى مع كل الأشياء الرائعة التي أنجزناها بالفعل. من السهل إضافة عامل retry
بعد طلبات HTTP الخاصة بنا ، والذي يعيد محاولة طلب فردي إذا أرجع خطأ. كإجراء وقائي ، يمكننا أيضًا إضافة عامل بين منشئ range
والطلبات ، مضيفًا شكلاً من أشكال تحديد المعدل حتى لا نرسل الرسائل العشوائية إلى الخادم بعدد كبير جدًا من الطلبات دفعة واحدة.
خلاصة: تعلم الزاوية 2 ليس مجرد إطار عمل جديد
يشبه تعلم Angular 2 مقابلة عائلة جديدة تمامًا ، وبعض علاقاتهم معقدة. آمل أن أكون قد تمكنت من إثبات أن هذه العلاقات تطورت لسبب ما ، وهناك الكثير الذي يمكن اكتسابه من خلال احترام الديناميكيات الموجودة داخل هذا النظام البيئي. آمل أن تكون قد استمتعت بهذا المقال أيضًا ، لأنني بالكاد خدشت السطح ، وهناك الكثير لأقوله حول هذا الموضوع.