عموما در برنامههای SPA، اطلاعات از طریق HTTP و از طرف سرور دریافت میشوند. از نگارشهای ابتدایی Angular، اینکار از طریق HTTP Module آن مسیر بود و هست. در Angular 4.3 روش بهبودیافتهای نسبت به این روش متداول ارائه شدهاست که در ادامه تعدادی از ویژگیهای مقدماتی آنرا بررسی میکنیم.
هرچند ارتقاء به HttpClient الزامی نیست و کدهای پیشین، هنوز هم به خوبی کار میکنند؛ اما طراحی جدید آن شامل ویژگیهای توکاری است که به سختی توسط HTTP Module پیشین قابل پیاده سازی هستند.
به روز رسانی وابستگیهای پروژه
پیش از هرکاری نیاز است وابستگیهای پروژه را به روز رسانی کرد و یکی از روشهای سادهی یافتن شماره نگارشهای جدید بستههای تعریف شدهی در فایل package.json برنامه، استفاده از بستهی npm-check-updates است:
npm install npm-check-updates -g
ncu
دستور اول، این بسته را به صورت عمومی نصب کرده و صدور دستور دوم در ریشهی پروژه، سبب میشود تا گزارشی از آخرین به روز رسانیها، نمایش داده شود (بدون هیچگونه تغییری در پروژه):
در اینجا شماره نگارشهای جدید مشخص شدهاند و همچنین روش سریع ارتقاء به آنها نیز ذکر شدهاست. فقط کافی است دستورات ذیل را صادر کنیم تا این به روز رسانیها توسط ncu انجام شوند:
دستور اول صرفا شماره نگارشهای جدید را در فایل package.json، به صورت خودکار اصلاح میکند و دستور دوم سبب دریافت، نصب و اعمال آنها خواهد گردید.
تغییرات مورد نیاز جهت معرفی ماژول HttpClient
این ماژول جدید از طریق اینترفیس HttpClientModule ارائه میشود. بنابراین اولین تغییر در جهت ارتقاء به نگارش 4.3، اصلاح importهای لازم است:
از:
import { HttpModule } from '@angular/http';
به:
import { HttpClientModule } from '@angular/common/http';
پس از آن، این HttpClientModule را به لیست imports ماژول اصلی برنامه اضافه میکنیم؛ تا در کل برنامه قابل دسترسی شود:
@NgModule({
imports: [
// ...
HttpClientModule,
// ...
],
declarations: [ ... ],
providers: [ ... ],
exports: [ ... ]
})
export class AppModule { }
تغییرات مورد نیاز در سازندهها و تزریق وابستگیها
پس از تغییرات فوق، دیگر دسترسی به HttpModule پیشین را نداریم. بنابراین نیاز است هر جائی را که سرویس Http به سازندهی کلاسی تزریق شدهاست، یافته و به صورت ذیل تغییر دهیم:
از:
constructor(private http: Http) { }
به:
import { HttpClient } from '@angular/common/http';
// ...
constructor(private http: HttpClient) { }
تغییرات مورد نیاز در کدهای سرویسها جهت کار با HTTP Verbs
یکی از اهداف HTTP Client جدید، سادگی کار با اطلاعات دریافتی از سرور است. برای مثال در HTTP Module پیشین، روش دریافت اطلاعات از سرور به صورت ذیل است:
public get(): Observable<MyType> => {
return this.http.get(url)
.map((response: Response) => <MyType>response.json());
}
ابتدا درخواستی ارسال شده و سپس نتیجهی آن به JSON تبدیل گشته و در آخر به نوع بازگشتی متد تبدیل میشود.
در HTTP Client جدید دیگر نیازی نیست تا متد ()json. فراخوانی شود. در اینجا به صورت پیشفرض نوع بازگشتی از سرور JSON فرض میشود. همچنین اکنون متدهای get/put/post و امثال آن برخلاف HTTP Client قبلی، جنریک هستند. یعنی در همینجا میتوان نوع بازگشتی را هم مشخص کرد. به این ترتیب، قطعه کد قدیمی فوق، به کد سادهی ذیل تبدیل میشود که در آن خبری از map و همچنین یک cast اضافی نیست:
get<T>(url: string): Observable<T> {
return this.http.get<T>(url);
}
برای نمونه شبیه به همین نکته برای post نیز صادق است:
post<T>(url: string, body: string): Observable<T> {
return this.http.post<T>(url, body);
}
نکته 1: در اینجا اگر خروجی از سرور، نوع دیگری را داشت، نیاز است responseType را به صورت صریحی به شکل ذیل مشخص کرد:
getData() {
this.http.get(this.url, { responseType: 'text' }).subscribe(res => {
this.data = res;
});
}
در اینحالت خروجی متنی <Observable<string را دریافت میکنیم و نیازی به ذکر <get<string نیست.
نکته 2: ممکن است اطلاعات بازگشتی از سمت سرور، داخل یک فیلد محصور شده باشند:
{
"results": [
"Item 1",
"Item 2",
]
}
در این حالت برای دسترسی به اطلاعات این فیلد میتوان از حالت key/value بودن اشیاء جاوا اسکریپتی به شکل زیر برای دسترسی به خاصیت results استفاده کرد:
this.http.get('/api/items').subscribe(data => {
this.results = data['results'];
});
نکاتی را که باید حین کار با یک RxJS Observable-based API در نظر داشت
این API جدید نیز همانند قبل مبتنی بر RxJS Observables است. بنابراین نکات ذیل در مورد آن نیز صادق است:
- اگر متد subscribe بر روی این observables فراخوانی نشود، اتفاقی رخ نخواهد داد.
-
اگر چندین بار مشترک این observables شویم، چندین درخواست HTTP صادر میشوند.
-
این نوع خاص از observables، تنها یک مقدار را بازگشت میدهند. اگر درخواست HTTP موفقیت آمیز باشد، این observables یک نتیجه را بازگشت داده و سپس خاتمه پیدا میکنند.
-
این observables اگر در حین درخواست HTTP با خطایی مواجه شوند، سبب صدور استثنایی میشوند.
تغییرات مورد نیاز در کدهای سرویسها جهت کار با HTTP Headers
در اینجا برای تعریف headers میتوان به صورت ذیل عمل کرد:
import { HttpHeaders } from "@angular/common/http";
const headers = new HttpHeaders({ "Content-Type": "application/json" });
و یا به صورت fluent به شکل زیر:
const headers = new HttpHeaders().set("Accept", "application/json").set('Content-Type', 'application/json');
سپس آنرا به عنوان پارامتر سوم، به متدهای http ارسال میکنیم. یک مثال:
updateAppProduct(id: number, item: AppProduct): Observable<AppProduct> {
const header = new HttpHeaders({ "Content-Type": "application/json" });
return this.http
.put<AppProduct>(
`${this.baseUrl}/UpdateProduct/${id}`,
JSON.stringify(item),
{ headers: header }
)
.map(response => response || {});
}
تعریف پارامتر options اینبار به صورت یک شیء دارای چندین خاصیت درآمدهاست. به همین جهت است که در اینجا یک {} را نیز مشاهده میکنید:
(method) HttpClient.post(url: string, body: any, options?: {
headers?: HttpHeaders;
observe?: "body";
params?: HttpParams;
reportProgress?: boolean;
responseType?: "json";
withCredentials?: boolean;
}): Observable<Object>
یک نکته: شیء HttpHeaders به صورت immutable طراحی شدهاست. یعنی اگر آنرا به صورت ذیل فراخوانی کنیم:
const headers = new HttpHeaders();
headers = headers.set('Content-Type', 'application/json');
headers = headers.set('Accept', 'application/json');
headers تولیدی ... خالی خواهد بود. به همین جهت روش صحیح تشکیل آن به صورت ذیل و زنجیروار است:
const headers = new HttpHeaders()
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
;
امکان تعریف HttpParams
اگر به شیء options در تعریف فوق دقت کنید، دارای خاصیت اختیاری params نیز هست. از آن میتوان جهت تعریف کوئری استرینگها استفاده کرد. برای مثال درخواست ذیل:
http
.post('/api/items/add', body, {
params: new HttpParams().set('id', '3'),
})
.subscribe();
سبب تولید یک چنین URL ایی میگردد:
یک نکته: شیء HttpParams به صورت immutable طراحی شدهاست. یعنی اگر آنرا به صورت ذیل فراخوانی کنیم:
const params = new HttpParams();
params.set('orderBy', '"$key"')
params.set('limitToFirst', "1");
params تولیدی ... خالی خواهد بود. به همین جهت روش صحیح تشکیل آن به صورت ذیل و زنجیروار است:
const params = new HttpParams()
.set('orderBy', '"$key"')
.set('limitToFirst', "1");
به علاوه روش تعریف ذیل نیز برای کار با HttpParams مجاز است:
const params = new HttpParams({fromString: 'orderBy="$key"&limitToFirst=1'});
تغییرات مورد نیاز در کدهای سرویسها جهت مدیریت خطاها
در اینجا اینبار خطای بازگشتی، از نوع ویژهی HttpErrorResponse است که شامل اطلاعات شماره کد و متن خطای حاصل میباشد:
import { HttpClient, HttpHeaders, HttpErrorResponse } from "@angular/common/http";
postData() {
this.http.post(this.url, this.payload).subscribe(
res => {
console.log(res);
},
(err: HttpErrorResponse) => {
console.log(err.error);
console.log(err.name);
console.log(err.message);
console.log(err.status);
if (err.error instanceof Error) {
console.log("Client-side error occured.");
} else {
console.log("Server-side error occured.");
}
}
);
}
امکان سعی مجدد در اتصال توسط HTTP Client
ممکن است در اولین سعی در اتصال به سرور، خطایی رخ دهد و یا سرور در دسترس نباشد. در اینجا توسط متد retry میتوان درخواست سعی مجدد در اتصال را صادر کرد.
برای این منظور ابتدا عملگر retry مربوط به RxJS را import میکنیم:
import 'rxjs/add/operator/retry';
سپس:
http
.get<ItemsResponse>('/api/items')
.retry(3)
.subscribe(...);
این کد در صورت بروز خطایی، این عملیات را سه بار تکرار میکند. در انتها اگر بازهم خطایی دریافت شد، این خطا را به برنامه بازگشت میدهد.
امکان درخواست کل Response بجای Body
اگر به امضای پارامتر اختیاری options دقت کنید، خاصیت observe آن به صورت پیش فرض به body تنظیم شدهاست. به این معنا که تنها body یک response را تبدیل به یک شیء JSON میکند:
(method) HttpClient.post(url: string, body: any, options?: {
headers?: HttpHeaders;
observe?: "body";
params?: HttpParams;
reportProgress?: boolean;
responseType?: "json";
withCredentials?: boolean;
}): Observable<Object>
اما گاهی از اوقات نیاز است تا به کل Response دسترسی داشته باشیم. در این حالت باید نوع observe را به response تنظیم کرد:
http
.get<MyJsonData>('/data.json', {observe: 'response'})
.subscribe(resp => {
console.log(resp.headers.get('X-Custom-Header'));
console.log(resp.body.someField);
});
به این ترتیب اینبار resp از نوع <HttpResponse<MyJsonData خواهد بود که توسط آن میتوان به خواص headers و یا body، به صورت جداگانهای دسترسی یافت.
یک نکتهی تکمیلی: کدهای سری کار با فرمها در Angular را اگر به HttpClient ارتقاء دهیم، خلاصهی تغییرات آنها
به این صورت خواهند بود.