مثل اینکه خودم تونستم درستش کنم! :)
فقط تو قسمتی که خطا میده try catch گذاشتم! الان بدرستی کار میکنه،
اینم فایل اصلاح شده:PersianDatePicker.min.js
تا اینجا تنظیمات اصلی فرم ثبت اطلاعات کارمندان را انجام دادیم. اکنون نوبت به ارسال این اطلاعات به سمت سرور است. پیشنیاز آن نیز تدارک مواردی است که در مطلب «یکپارچه سازی Angular CLI و ASP.NET Core در VS 2017» پیشتر بحث شدند. از این مطلب تنها تنظیمات موارد ذیل را نیاز خواهیم داشت و از تکرار آنها در اینجا صرفنظر میشود تا هم مطلب کوتاهتر شود و هم بتوان بر روی اصل موضوع جاری، تمرکز کرد:
- ایجاد یک پروژهی جدید ASP.NET Core در VS 2017
- تنظیمات یک برنامهی ASP.NET Core خالی برای اجرای یک برنامهی Angular CLI
- تنظیمات فایل آغازین یک برنامهی ASP.NET Core جهت ارائهی برنامههای Angular
- ایجاد ساختار اولیهی برنامهی Angular CLI در داخل پروژهی جاری: این مورد را تاکنون انجام دادهایم و تکمیل کردهایم. بنابراین تنها کاری که نیاز است انجام شود، cut و paste محتوای پوشهی angular-template-driven-forms-lab (پروژهی این سری) به ریشهی پروژهی ASP.NET Core است.
- تنظیم محل خروجی نهایی Angular CLI به پوشهی wwwroot
- روش اول و یا دوم اجرای برنامههای مبتنی بر ASP.NET Core و Angular CLI
البته سورس کامل تمام این تنظیمات را از انتهای بحث نیز میتوانید دریافت کنید.
ضمن اینکه هیچ نیازی هم به استفاده از VS 2017 نیست و هر دوی برنامهی Angular و ASP.NET Core را میتوان توسط VSCode به خوبی مدیریت و اجرا کرد.
ایجاد ساختار مقدماتی سرویس ارسال اطلاعات به سرور
در برنامههای Angular مرسوم است جهت کاهش مسئولیتهای یک کلاس و امکان استفادهی مجدد از کدها، منطق ارسال اطلاعات به سرور، به درون کلاس یک سرویس منتقل شود و سپس این سرویس به کلاسهای کامپوننتها، برای مثال یک فرم ثبت اطلاعات، برای ارسال و یا دریافت اطلاعات، تزریق گردد. به همین جهت، ابتدا ساختار ابتدایی این سرویس و تنظیمات مرتبط با آنرا انجام میدهیم.
ابتدا از طریق خط فرمان به پوشهی ریشهی برنامه وارد شده (جائیکه فایل Startup.cs قرار دارد) و سپس دستور ذیل را اجرا میکنیم:
با این خروجی
همانطور که در سطر آخر نیز ملاحظه میکنید، فایل employee.module.ts را جهت درج کلاس جدید FormPosterService در قسمت providers ماژول آن به روز رسانی میکند؛ تا بتوانیم این سرویس را در کامپوننتهای این ماژول تزریق کرده و استفاده کنیم.
ساختار ابتدایی این سرویس را نیز به نحو ذیل تغییر میدهیم:
در اینجا سرویس Http انگیولار به سازندهی کلاس تزریق شدهاست و این نحوهی تعریف سبب میشود تا بتوان به پارامتر http، به صورت یک فیلد خصوصی تعریف شدهی در سطح کلاس نیز دسترسی پیدا کنیم.
چون این کلاس از ماژول توکار Http استفاده میکند، نیاز است این ماژول را نیز به قسمت imports فایل src\app\app.module.ts اضافه کنیم:
اکنون میتوانیم این سرویس جدید FormPosterService را به سازندهی کامپوننت EmployeeRegisterComponent در فایل src\app\employee\employee-register\employee-register.component.ts تزریق کنیم:
در ادامه برای آزمایش برنامه، به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی، دستورات:
و در دومی، دستورات ذیل را اجرا کنید:
دستورات اول کار بازیابی وابستگیهای سمت کلاینت و سپس ساخت تدریجی برنامهی Angular را دنبال میکند. دستورات دوم، وابستگیهای برنامهی ASP.NET Core را دریافت و نصب کرده و سپس برنامه را در حالت watch ساخته و بر روی پورت 5000 ارائه میکند (بدون نیاز به اجرای VS 2017؛ این دستور عمومی است).
به همین جهت برای آزمایش ابتدایی آن، آدرس http://localhost:5000 را در مرورگر باز کنید. برگهی developer tools مرورگر را نیز بررسی کنید تا خطایی در آن ظاهر نشده باشد. برای مثال اگر فراموش کرده باشید تا HttpModule را به app.module اضافه کنید، خطای no provider for HttpModule را مشاهده خواهید کرد.
مدیریت رخداد submit فرم در Angular
تا اینجا کار برپایی تنظیمات اولیهی کار با سرویس Http را انجام دادیم. مرحلهی بعد مدیریت رخداد submit فرم است. به همین جهت فایل src\app\employee\employee-register\employee-register.component.html را گشوده و سپس رخدادگردان submit را به فرم آن اضافه کنید:
در حین رخدادگردانی submit میتوان به template reference variable تعریف شدهی form# برای دسترسی به وهلهای از ngForm نیز کمک گرفت.
امضای متد submitForm را در اینجا مشاهده میکنید. form دریافتی آن از نوع NgForm است که در ابتدای فایل import شدهاست.
در همین حال اگر بر روی دکمهی ok کلیک کنیم، چنین خروجی را در کنسول developer مروگر میتوان مشاهده کرد:
اولین مورد، محتوای this.model است و دومی محتوای form.value را گزارش کردهاست. همانطور که مشاهده میکنید، مقدار form.value بسیار شبیه است به وهلهای از مدلی که در سطح کلاس تعریف کردهایم و این مقدار همواره توسط Angular نگهداری و مدیریت میشود. بنابراین حتما الزامی نیست تا مدلی را جهت کار با فرمهای مبتنی بر قالبها به صورت جداگانهای تهیه کرد. توسط شیء form نیز میتوان به تمام اطلاعات فیلدها دسترسی یافت.
تکمیل سرویس ارسال اطلاعات به سرور
در ادامه میخواهیم اطلاعات مدل فرم را به سرور ارسال کنیم. برای این منظور سرویس FormPoster را به صورت ذیل تکمیل میکنیم:
برای کار با Observables یا میتوان نوشت 'import 'rxjs/Rx که تمام بستهی RxJS را import میکند، یا همانند این مثال بهتر است تنها اپراتورهایی را که به آنها نیاز پیدا میکنیم، import نمائیم. به این ترتیب حجم نهایی ارائهی برنامه نیز کاهش خواهد یافت.
در متد postEmployeeForm، ابتدا توسط JSON.stringify محتوای شیء کارمند encode میشود. البته متد post اینکار را به صورت توکار نیز میتواند مدیریت کند. سپس ذکر هدر مناسب در اینجا الزامی است تا در سمت سرور بتوانیم اطلاعات دریافتی را به شیء متناظری نگاشت کنیم. در غیراینصورت model binder سمت سرور نمیداند که چه نوع فرمتی را دریافت کردهاست و چه نوع decoding را باید انجام دهد.
در قسمت map، کار بررسی اطلاعات دریافتی از سرور را انجام خواهیم داد و اگر در این بین خطایی وجود داشت، توسط متد handleError در کنسول developer مرورگر نمایش داده میشود.
خروجی متد postEmployeeForm یک Observable است. بنابراین تا زمانیکه یک subscriber نداشته باشد، اجرا نخواهد شد. به همین جهت به کلاس EmployeeRegisterComponent مراجعه کرده و متد submitForm را به نحو ذیل تکمیل میکنیم:
در اینجا ابتدا اعتبارسنجی سفارشی drop down را که در قسمت قبل بررسی کردیم، قرار دادهایم. پس از آن متد postEmployeeForm سرویس formPoster فراخوانی شدهاست و در اینجا کار subscribe به نتیجهی عملیات صورت گرفتهاست که میتواند حاوی اطلاعاتی از سمت سرور و یا خطایی در این بین باشد.
یک نکته: اگر علاقمند باشید تا ساختار واقعی شیء NgForm را مشاهده کنید، در ابتدای متد فوق، console.log(form.form) را فراخوانی کنید و سپس شیء حاصل را در کنسول developer مرورگر بررسی نمائید.
تکمیل Web API برنامهی ASP.NET Core جهت دریافت اطلاعات از کلاینتها
در ابتدای سرویس formPoster، یک چنین تعریفی را داریم:
به همین جهت نیاز است سرویس Web API سمت سرور خود را بر این مبنا تکمیل کنیم.
ابتدا مدل زیر را به پروژهی ASP.NET Core جاری، معادل نمونهی تایپاسکریپتی سمت کلاینت آن اضافه میکنیم. البته در اینجا یک Id نیز اضافه شدهاست:
سپس کنترلر جدید EmployeeController را با محتوای ذیل اضافه خواهیم کرد:
این کنترلر با شیوهی Web API تعریف شدهاست. مسیریابی آن با api شروع میشود تا با مسیر baseUrl سرویس formPoster تطابق پیدا کند.
در اینجا پس از ثبت فرضی مدل، Id آن به همراه اطلاعات مدل، به نحوی که ملاحظه میکنید، بازگشت داده شدهاست. این نوع خروجی، یک چنین JSON ایی را تولید میکند:
به همین جهت است که در متد extractData، دسترسی به body.fields را مشاهده میکنید. این fields در اینجا دربرگیرندهی اطلاعات بازگشتی از سرور است (نام آن دلخواه است و درصورت تغییر آن در سمت سرو، باید این نام را در متد extractData نیز اصلاح کنید).
اکنون اگر برنامه را با دستورات dotnet watch build و ng build --watch اجرا کنیم، بر روی پورت 5000 قابل دسترسی خواهد بود و پس از ارسال فرم به سرور، چنین خروجی را میتوان در کنسول developer مرورگر مشاهده کرد:
نمایش success به همراه شیءایی که از سمت سرور دریافت شدهاست؛ که حاصل اجرای سطر ذیل در متد submitForm است:
همانطور که مشاهده میکنید، این شیء به همراه Id نیز هست. بنابراین درصورت نیاز به آن در سمت کلاینت، خاصیت معادل آنرا به کلاس کارمند اضافه کرده و در همین سطر فوق میتوان به آن دسترسی یافت.
بارگذاری اطلاعات drop down از سرور
تا اینجا اطلاعات drop down نمایش داده شده از یک آرایهی مشخص سمت کلاینت تامین شدند. در ادامه قصد داریم تا آنها را از سرور دریافت کنیم. به همین جهت اکشن متد ذیل را به کنترلر سمت سرور برنامه اضافه کنید:
که برای آزمایش آن میتوانید مسیر http://localhost:5000/api/employee/languages را جداگانه در مرورگر درخواست کنید.
پس از آن در سمت کلاینت این تغییرات نیاز هستند:
ابتدا به سرویس FormPosterService دو متد ذیل را اضافه میکنیم که کار آنها دریافت و پردازش اطلاعات از api/employee/languages سمت سرور هستند:
اینبار چون خروجی سمت سرور را مانند قبل (متد extractData) داخل فیلدی مانند fields محصور نکردیم، همان body دریافتی بازگشت داده شدهاست.
پس از آن دو تغییر ذیل را نیاز است به EmployeeRegisterComponent اعمال کنیم:
ابتدا آرایهی زبانها با یک آرایهی خالی مقدار دهی شدهاست و سپس در متد ngOnInit، کار دریافت اطلاعات آن از سرور، صورت گرفتهاست.
مشکل! ممکن است مدت زمانی طول بکشد تا این اطلاعات از سمت سرور دریافت شوند. در این حالت میتوان به شکل زیر در فایل employee-register.component.html فرم را تا زمان پر شدن دراپ داون آن مخفی کرد:
در این حالت هر زمانیکه آرایهی زبانها پر شد، loading حذف شده و div نمایان میگردد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-05.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات:
و در دومی دستورات ذیل را اجرا کنید:
اکنون میتوانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.
- ایجاد یک پروژهی جدید ASP.NET Core در VS 2017
- تنظیمات یک برنامهی ASP.NET Core خالی برای اجرای یک برنامهی Angular CLI
- تنظیمات فایل آغازین یک برنامهی ASP.NET Core جهت ارائهی برنامههای Angular
- ایجاد ساختار اولیهی برنامهی Angular CLI در داخل پروژهی جاری: این مورد را تاکنون انجام دادهایم و تکمیل کردهایم. بنابراین تنها کاری که نیاز است انجام شود، cut و paste محتوای پوشهی angular-template-driven-forms-lab (پروژهی این سری) به ریشهی پروژهی ASP.NET Core است.
- تنظیم محل خروجی نهایی Angular CLI به پوشهی wwwroot
- روش اول و یا دوم اجرای برنامههای مبتنی بر ASP.NET Core و Angular CLI
البته سورس کامل تمام این تنظیمات را از انتهای بحث نیز میتوانید دریافت کنید.
ضمن اینکه هیچ نیازی هم به استفاده از VS 2017 نیست و هر دوی برنامهی Angular و ASP.NET Core را میتوان توسط VSCode به خوبی مدیریت و اجرا کرد.
ایجاد ساختار مقدماتی سرویس ارسال اطلاعات به سرور
در برنامههای Angular مرسوم است جهت کاهش مسئولیتهای یک کلاس و امکان استفادهی مجدد از کدها، منطق ارسال اطلاعات به سرور، به درون کلاس یک سرویس منتقل شود و سپس این سرویس به کلاسهای کامپوننتها، برای مثال یک فرم ثبت اطلاعات، برای ارسال و یا دریافت اطلاعات، تزریق گردد. به همین جهت، ابتدا ساختار ابتدایی این سرویس و تنظیمات مرتبط با آنرا انجام میدهیم.
ابتدا از طریق خط فرمان به پوشهی ریشهی برنامه وارد شده (جائیکه فایل Startup.cs قرار دارد) و سپس دستور ذیل را اجرا میکنیم:
>ng g s employee/FormPoster -m employee.module
installing service create src\app\employee\form-poster.service.spec.ts create src\app\employee\form-poster.service.ts update src\app\employee\employee.module.ts
ساختار ابتدایی این سرویس را نیز به نحو ذیل تغییر میدهیم:
import { Injectable } from '@angular/core'; import { Http } from '@angular/http'; import { Employee } from './employee'; @Injectable() export class FormPosterService { constructor(private http:Http) { } postEmployeeForm(employee: Employee) { } }
چون این کلاس از ماژول توکار Http استفاده میکند، نیاز است این ماژول را نیز به قسمت imports فایل src\app\app.module.ts اضافه کنیم:
import { HttpModule } from "@angular/http"; @NgModule({ imports: [ BrowserModule, FormsModule, HttpModule, EmployeeModule, AppRoutingModule ]
import { FormPosterService } from "../form-poster.service"; export class EmployeeRegisterComponent implements OnInit { constructor(private formPoster: FormPosterService) {} }
در ادامه برای آزمایش برنامه، به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی، دستورات:
>npm install >ng build --watch
>dotnet restore >dotnet watch run
به همین جهت برای آزمایش ابتدایی آن، آدرس http://localhost:5000 را در مرورگر باز کنید. برگهی developer tools مرورگر را نیز بررسی کنید تا خطایی در آن ظاهر نشده باشد. برای مثال اگر فراموش کرده باشید تا HttpModule را به app.module اضافه کنید، خطای no provider for HttpModule را مشاهده خواهید کرد.
مدیریت رخداد submit فرم در Angular
تا اینجا کار برپایی تنظیمات اولیهی کار با سرویس Http را انجام دادیم. مرحلهی بعد مدیریت رخداد submit فرم است. به همین جهت فایل src\app\employee\employee-register\employee-register.component.html را گشوده و سپس رخدادگردان submit را به فرم آن اضافه کنید:
<form #form="ngForm" (submit)="submitForm(form)" novalidate>
export class EmployeeRegisterComponent implements OnInit { submitForm(form: NgForm) { console.log(this.model); console.log(form.value); } }
در همین حال اگر بر روی دکمهی ok کلیک کنیم، چنین خروجی را در کنسول developer مروگر میتوان مشاهده کرد:
اولین مورد، محتوای this.model است و دومی محتوای form.value را گزارش کردهاست. همانطور که مشاهده میکنید، مقدار form.value بسیار شبیه است به وهلهای از مدلی که در سطح کلاس تعریف کردهایم و این مقدار همواره توسط Angular نگهداری و مدیریت میشود. بنابراین حتما الزامی نیست تا مدلی را جهت کار با فرمهای مبتنی بر قالبها به صورت جداگانهای تهیه کرد. توسط شیء form نیز میتوان به تمام اطلاعات فیلدها دسترسی یافت.
تکمیل سرویس ارسال اطلاعات به سرور
در ادامه میخواهیم اطلاعات مدل فرم را به سرور ارسال کنیم. برای این منظور سرویس FormPoster را به صورت ذیل تکمیل میکنیم:
import { Injectable } from "@angular/core"; import { Http, Response, Headers, RequestOptions } from "@angular/http"; import { Observable } from "rxjs/Observable"; import "rxjs/add/operator/do"; import "rxjs/add/operator/catch"; import "rxjs/add/observable/throw"; import "rxjs/add/operator/map"; import "rxjs/add/observable/of"; import { Employee } from "./employee"; @Injectable() export class FormPosterService { private baseUrl = "api/employee"; constructor(private http: Http) {} private extractData(res: Response) { const body = res.json(); return body.fields || {}; } private handleError(error: Response): Observable<any> { console.error("observable error: ", error); return Observable.throw(error.statusText); } postEmployeeForm(employee: Employee): Observable<Employee> { const body = JSON.stringify(employee); const headers = new Headers({ "Content-Type": "application/json" }); const options = new RequestOptions({ headers: headers }); return this.http .post(this.baseUrl, body, options) .map(this.extractData) .catch(this.handleError); } }
در متد postEmployeeForm، ابتدا توسط JSON.stringify محتوای شیء کارمند encode میشود. البته متد post اینکار را به صورت توکار نیز میتواند مدیریت کند. سپس ذکر هدر مناسب در اینجا الزامی است تا در سمت سرور بتوانیم اطلاعات دریافتی را به شیء متناظری نگاشت کنیم. در غیراینصورت model binder سمت سرور نمیداند که چه نوع فرمتی را دریافت کردهاست و چه نوع decoding را باید انجام دهد.
در قسمت map، کار بررسی اطلاعات دریافتی از سرور را انجام خواهیم داد و اگر در این بین خطایی وجود داشت، توسط متد handleError در کنسول developer مرورگر نمایش داده میشود.
خروجی متد postEmployeeForm یک Observable است. بنابراین تا زمانیکه یک subscriber نداشته باشد، اجرا نخواهد شد. به همین جهت به کلاس EmployeeRegisterComponent مراجعه کرده و متد submitForm را به نحو ذیل تکمیل میکنیم:
submitForm(form: NgForm) { console.log(this.model); console.log(form.value); // validate form this.validatePrimaryLanguage(this.model.primaryLanguage); if (this.hasPrimaryLanguageError) { return; } this.formPoster .postEmployeeForm(this.model) .subscribe( data => console.log("success: ", data), err => console.log("error: ", err) ); }
یک نکته: اگر علاقمند باشید تا ساختار واقعی شیء NgForm را مشاهده کنید، در ابتدای متد فوق، console.log(form.form) را فراخوانی کنید و سپس شیء حاصل را در کنسول developer مرورگر بررسی نمائید.
تکمیل Web API برنامهی ASP.NET Core جهت دریافت اطلاعات از کلاینتها
در ابتدای سرویس formPoster، یک چنین تعریفی را داریم:
export class FormPosterService { private baseUrl = "api/employee";
ابتدا مدل زیر را به پروژهی ASP.NET Core جاری، معادل نمونهی تایپاسکریپتی سمت کلاینت آن اضافه میکنیم. البته در اینجا یک Id نیز اضافه شدهاست:
namespace AngularTemplateDrivenFormsLab.Models { public class Employee { public int Id { set; get; } public string FirstName { get; set; } public string LastName { get; set; } public bool IsFullTime { get; set; } public string PaymentType { get; set; } public string PrimaryLanguage { get; set; } } }
سپس کنترلر جدید EmployeeController را با محتوای ذیل اضافه خواهیم کرد:
using Microsoft.AspNetCore.Mvc; using AngularTemplateDrivenFormsLab.Models; namespace AngularTemplateDrivenFormsLab.Controllers { [Route("api/[controller]")] public class EmployeeController : Controller { public IActionResult Post([FromBody] Employee model) { //todo: save model model.Id = 100; return Created("", new { fields = model }); } } }
در اینجا پس از ثبت فرضی مدل، Id آن به همراه اطلاعات مدل، به نحوی که ملاحظه میکنید، بازگشت داده شدهاست. این نوع خروجی، یک چنین JSON ایی را تولید میکند:
{"fields":{"id":100,"firstName":"Vahid","lastName":"N","isFullTime":true,"paymentType":"FullTime","primaryLanguage":"Persian"}}
private extractData(res: Response) { const body = res.json(); return body.fields || {}; }
نمایش success به همراه شیءایی که از سمت سرور دریافت شدهاست؛ که حاصل اجرای سطر ذیل در متد submitForm است:
data => console.log("success: ", data)
بارگذاری اطلاعات drop down از سرور
تا اینجا اطلاعات drop down نمایش داده شده از یک آرایهی مشخص سمت کلاینت تامین شدند. در ادامه قصد داریم تا آنها را از سرور دریافت کنیم. به همین جهت اکشن متد ذیل را به کنترلر سمت سرور برنامه اضافه کنید:
[HttpGet("/api/[controller]/[action]")] public IActionResult Languages() { string[] languages = { "Persian", "English", "Spanish", "Other" }; return Ok(languages); }
پس از آن در سمت کلاینت این تغییرات نیاز هستند:
ابتدا به سرویس FormPosterService دو متد ذیل را اضافه میکنیم که کار آنها دریافت و پردازش اطلاعات از api/employee/languages سمت سرور هستند:
private extractLanguages(res: Response) { const body = res.json(); return body || {}; } getLanguages(): Observable<any> { return this.http .get(`${this.baseUrl}/languages`) .map(this.extractLanguages) .catch(this.handleError); }
پس از آن دو تغییر ذیل را نیاز است به EmployeeRegisterComponent اعمال کنیم:
languages = []; ngOnInit() { this.formPoster .getLanguages() .subscribe( data => this.languages = data, err => console.log("get error: ", err) ); }
مشکل! ممکن است مدت زمانی طول بکشد تا این اطلاعات از سمت سرور دریافت شوند. در این حالت میتوان به شکل زیر در فایل employee-register.component.html فرم را تا زمان پر شدن دراپ داون آن مخفی کرد:
<h3 *ngIf="languages.length == 0">Loading...</h3> <div class="container" *ngIf="languages.length > 0">
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-05.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات:
>npm install >ng build --watch
>dotnet restore >dotnet watch run
پیشنیازها
«بررسی روش آپلود فایلها در ASP.NET Core»
«ارسال فایل و تصویر به همراه دادههای دیگر از طریق jQuery Ajax»
- در مطلب اول، روش دریافت فایلها از کلاینت، در سمت سرور و ذخیره سازی آنها در یک برنامهی ASP.NET Core بررسی شدهاست که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شدهاست. هرچند در مطلب جاری از jQuery استفاده نمیشود، اما نکات نحوهی کار با شیء FormData استاندارد، در اینجا نیز یکی است.
تدارک مقدمات مثال این قسمت
این مثال در ادامهی همین سری کار با فرمهای مبتنی بر قالبها است. به همین جهت ابتدا ماژول جدید UploadFile را به آن اضافه میکنیم:
همچنین به فایل app.module.ts مراجعه کرده و UploadFileModule را بجای UploadFileRoutingModule در قسمت imports معرفی میکنیم. سپس به این ماژول جدید، کامپوننت فرم ثبت یک درخواست پشتیبانی را اضافه خواهیم کرد:
که اینکار سبب به روز رسانی فایل upload-file.module.ts و افزوده شدن UploadFileSimpleComponent به قسمت declarations آن میشود.
در ادامه کلاس مدل معادل فرم ثبت نام یک درخواست پشتیبانی را تعریف میکنیم:
با این محتوا:
در اینجا Ticket تعریف شده دارای یک خاصیت توضیحات است و این فرم به همراه فیلد ارسال چندین فایل نیز میباشد که نیازی به درج آنها در کلاس فوق نیست:
ایجاد مقدمات کامپوننت UploadFileSimple و قالب آن
پس از ایجاد ساختار کلاس Ticket، یک وهله از آنرا به نام model ایجاد کرده و در اختیار قالب آن قرار میدهیم:
سپس قالب این کامپوننت و یا همان فایل upload-file-simple.component.html را به صورت ذیل تکمیل میکنیم:
در اینجا ابتدا فیلد توضیحات درخواست جدید، ارائه و به خاصیت model.description متصل شدهاست. همچنین این فیلد با ویژگی required مزین، و اجباری بودن آن بررسی گردیدهاست.
سپس در انتها، فیلد آپلود را مشاهده میکنید؛ با این ویژگیها:
الف) ngModel ایی به آن متصل نشدهاست؛ چون روش کار با آن متفاوت است.
ب) یک template reference variable به نام screenshotInput# در آن تعریف شدهاست. از این متغیر، در کامپوننت قالب استفاده خواهیم کرد.
ج) به رخداد change این کنترل، متد fileChange متصل شدهاست که رخداد جاری را نیز دریافت میکند.
د) ذکر ویژگی استاندارد multiple را نیز در اینجا مشاهده میکنید. وجود آن سبب خواهد شد تا کاربر بتواند چندین فایل را با هم انتخاب کند. اگر نیازی به ارسال چندین فایل نیست، این ویژگی را حذف کنید.
دسترسی به المان ارسال فایل در کامپوننت متناظر
تا اینجا یک المان ارسال فایل را به فرم، اضافه کردهایم. اما چگونه باید به فایلهای آن برای ارسال به سرور دسترسی پیدا کنیم؟
برای این منظور در ادامه دو روش را بررسی خواهیم کرد:
1) دسترسی به المان ارسال فایل از طریق رخداد change
در تعریف فیلد ارسال فایل، اتصال به رخداد change تعریف شدهاست:
معادل آن در سمت کامپوننت متناظر، به صورت ذیل است:
همانطور که مشاهده میکنید، event.target، امکان دسترسی مستقیم به المان متناظری را در قالب کامپوننت میسر میکند. سپس میتوان به خاصیت files آن دسترسی یافت.
در اینجا ساختار شیء استاندارد FileList و اجزای آنرا مشاهده میکنید. برای مثال چون دو فایل انتخاب شدهاست، این لیست به همراه یک خاصیت طول و دو شیء File است.
تعاریف این اشیاء استاندارد، در فایل ذیل قرار دارند و به همین جهت است که VSCode، بدون نیاز به تنظیمات دیگری، آنها را شناسایی و intellisense متناظری را مهیا میکند:
همچنین اگر به فایل tsconfig.json پروژه نیز مراجعه کنید، یک چنین تعاریفی در آن قرار دارند:
وجود و تعریف کتابخانهی dom است که سبب کامپایل شدن کدهای فوق، بدون بروز هیچگونه خطایی میشود.
2) دسترسی به المان آپلود فایل از طریق یک template reference variable
در حین تعریف المان فایل در فرم برنامه، متغیر screenshotInput# نیز ذکر شدهاست. میتوان به یک چنین متغیرهایی در کامپوننت متناظر به روش ذیل دسترسی یافت:
ابتدا یک خاصیت جدید را به نام screenshotInput از نوع ElementRef که در angular/core@ تعریف شدهاست، اضافه میکنیم. سپس برای اتصال آن به template reference variable ایی به نام screenshotInput، از ویژگی به نام ViewChild، با پارامتری مساوی نام همین متغیر، استفاده خواهیم کرد.
اکنون خاصیت screenshotInput کامپوننت، به متغیری به همین نام در قالب متناظر با آن متصل شدهاست. بنابراین با استفاده از خاصیت nativeElement آن همانند کدهایی که در متد submitForm فوق ملاحظه میکنید، میتوان به خاصیت files این کنترل ارسال فایلها دسترسی یافت.
نوع جدید و استاندارد HTMLInputElement نیز در فایل lib.dom.d.ts که پیشتر معرفی شد، ثبت شدهاست.
ارسال فرم درخواست پشتیبانی به سرور
تا اینجا فرمی را تشکیل داده و همچنین به فیلد file آن دسترسی پیدا کردیم. اکنون میخواهیم این اطلاعات را به سمت سرور ارسال کنیم. برای این منظور، سرویس جدیدی را ایجاد خواهیم کرد:
که سبب به روز رسانی خودکار قسمت providers فایل upload-file.module.ts نیز میشود.
در ادامه کدهای کامل این سرویس را مشاهده میکنید:
توضیحات تکمیلی:
روش کار با فرمهایی که فیلدهای ارسال فایل را به همراه دارند، متفاوت است با روش کار با فرمهای معمولی. در فرمهای معمولی، اصل شیء Ticket را به متد this.http.post واگذار میکنیم. مابقی آن خودکار است. در اینجا باید شیء استاندارد FormData را تشکیل داده و سپس اطلاعات را از طریق آن ارسال کنیم:
الف) افزودن مقادیر خواص شیء Ticket به FormData
با استفاده از حلقهی for میتوان بر روی خواص یک شیء جاوا اسکریپتی حرکت کرد. به این ترتیب میتوان نام و مقدار آنها را یافت و سپس به formData به صورت key/value افزود.
ب) افزودن فایلها به شیء FormData
پس از افزودن اطلاعات ticket به FormData، اکنون نوبت به افزودن فایلهای فرم است:
این مورد نیز به سادگی تشکیل یک حلقه، بر روی خاصیت files المان آپلود فایل است. به همین جهت بود که به دو روش سعی کردیم، به این خاصیت دسترسی پیدا کنیم.
یک نکته: چون در اینجا کلید اضافه شده، نام فایل است، دیگر نمیتوان در سمت سرور از روش model binding استفاده کرد. چون این نام دیگر ثابت نیست و هربار میتواند متغیر باشد (در حالت model binding دقیقا مشخص است که کلید مشخصی قرار است به سرور ارسال شود و بر همین اساس، نام خاصیت یا پارامتر سمت سرور تعیین میگردد). به همین جهت در سمت سرور برای دسترسی به این مجموعه، از روش Request.Form.Files استفاده میکنیم.
ج) ارسال اطلاعات نهایی به سرور
اکنون که formData را بر اساس اطلاعات اضافی ticket و فایلهای متصل به آن تشکیل دادیم، روش ارسال آن به سرور همانند قبل است:
یک نکته: در اینجا در روش استفاده از formData نباید Content-Type را به multipart/form-data تنظیم کرد. در غیراینصورت خطای Missing content-type boundary error را دریافت میکنید.
تکمیل کامپوننت ارسال درخواست پشتیبانی
پس از تکمیل سرویس ارسال اطلاعات به سمت سرور، اکنون نوبت به استفادهی از آن در کامپوننت ارسال فرم درخواست پشتیبانی است. بنابراین ابتدا این سرویس جدید را به سازندهی UploadFileSimpleComponent تزریق میکنیم:
و سپس متد submitForm چنین شکلی را پیدا میکند:
در اینجا this.model حاوی اطلاعات شیء ticket است (برای مثال اطلاعات توضیحات آن) و fileInput.files امکان دسترسی به اطلاعات فایلهای انتخابی توسط کاربر را میدهد. پس از آن فراخوانی متدهای this.uploadService.postTicket و subscribe، سبب ارسال این اطلاعات به سمت سرور میشوند.
دریافت فرم درخواست پشتیبانی در سمت سرور و ذخیرهی فایلهای آن
کدهای کامل SimpleUpload که در سرویس فوق مشخص شدهاست، به صورت ذیل هستند. ابتدا مدل Ticket مشخص شدهاست:
و سپس کنترلر ذخیره سازی اطلاعات Ticket را مشاهده میکنید:
توضیحات تکمیلی
- تزریق IHostingEnvironment در سازندهی کلاس کنترلر، سبب میشود تا از طریق خاصیت WebRootPath آن، به مسیر wwwroot سایت دسترسی پیدا کنیم و فایلهای نهایی را در آنجا ذخیره سازی کنیم.
- همانطور که ملاحظه میکنید، هنوز هم model binding کار کرده و میتوان شیء Ticket را به نحو متداولی دریافت کرد:
اما همانطور که عنوان شد، چون در حلقهی افزودن فایلها در سمت کلاینت، کلید نام این فایلها هربار متفاوت است:
مجبور هستیم در سمت سرور بر روی Request.Form.Files یک حلقه را تشکیل داده و تمام فایلهای رسیده را پردازش کنیم:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
«بررسی روش آپلود فایلها در ASP.NET Core»
«ارسال فایل و تصویر به همراه دادههای دیگر از طریق jQuery Ajax»
- در مطلب اول، روش دریافت فایلها از کلاینت، در سمت سرور و ذخیره سازی آنها در یک برنامهی ASP.NET Core بررسی شدهاست که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شدهاست. هرچند در مطلب جاری از jQuery استفاده نمیشود، اما نکات نحوهی کار با شیء FormData استاندارد، در اینجا نیز یکی است.
تدارک مقدمات مثال این قسمت
این مثال در ادامهی همین سری کار با فرمهای مبتنی بر قالبها است. به همین جهت ابتدا ماژول جدید UploadFile را به آن اضافه میکنیم:
>ng g m UploadFile -m app.module --routing
>ng g c UploadFile/UploadFileSimple
در ادامه کلاس مدل معادل فرم ثبت نام یک درخواست پشتیبانی را تعریف میکنیم:
>ng g cl UploadFile/Ticket
export class Ticket { constructor(public description: string = "") {} }
ایجاد مقدمات کامپوننت UploadFileSimple و قالب آن
پس از ایجاد ساختار کلاس Ticket، یک وهله از آنرا به نام model ایجاد کرده و در اختیار قالب آن قرار میدهیم:
import { Ticket } from "./../ticket"; export class UploadFileSimpleComponent implements OnInit { model = new Ticket();
<div class="container"> <h3>Support Form</h3> <form #form="ngForm" (submit)="submitForm(form)" novalidate> <div class="form-group" [class.has-error]="description.invalid && description.touched"> <label class="control-label">Description</label> <input #description="ngModel" required type="text" class="form-control" name="description" [(ngModel)]="model.description"> <div *ngIf="description.invalid && description.touched"> <div class="alert alert-danger" *ngIf="description.errors.required"> description is required. </div> </div> </div> <div class="form-group"> <label class="control-label">Screenshot(s)</label> <input #screenshotInput required type="file" multiple (change)="fileChange($event)" class="form-control" name="screenshot"> </div> <button class="btn btn-primary" [disabled]="form.invalid" type="submit">Ok</button> </form> </div>
سپس در انتها، فیلد آپلود را مشاهده میکنید؛ با این ویژگیها:
الف) ngModel ایی به آن متصل نشدهاست؛ چون روش کار با آن متفاوت است.
ب) یک template reference variable به نام screenshotInput# در آن تعریف شدهاست. از این متغیر، در کامپوننت قالب استفاده خواهیم کرد.
ج) به رخداد change این کنترل، متد fileChange متصل شدهاست که رخداد جاری را نیز دریافت میکند.
د) ذکر ویژگی استاندارد multiple را نیز در اینجا مشاهده میکنید. وجود آن سبب خواهد شد تا کاربر بتواند چندین فایل را با هم انتخاب کند. اگر نیازی به ارسال چندین فایل نیست، این ویژگی را حذف کنید.
دسترسی به المان ارسال فایل در کامپوننت متناظر
تا اینجا یک المان ارسال فایل را به فرم، اضافه کردهایم. اما چگونه باید به فایلهای آن برای ارسال به سرور دسترسی پیدا کنیم؟
برای این منظور در ادامه دو روش را بررسی خواهیم کرد:
1) دسترسی به المان ارسال فایل از طریق رخداد change
در تعریف فیلد ارسال فایل، اتصال به رخداد change تعریف شدهاست:
(change)="fileChange($event)"
fileChange(event) { const filesList: FileList = event.target.files; console.log("fileChange() -> filesList", filesList); }
در اینجا ساختار شیء استاندارد FileList و اجزای آنرا مشاهده میکنید. برای مثال چون دو فایل انتخاب شدهاست، این لیست به همراه یک خاصیت طول و دو شیء File است.
تعاریف این اشیاء استاندارد، در فایل ذیل قرار دارند و به همین جهت است که VSCode، بدون نیاز به تنظیمات دیگری، آنها را شناسایی و intellisense متناظری را مهیا میکند:
C:\Program Files (x86)\Microsoft VS Code\resources\app\extensions\node_modules\typescript\lib\lib.dom.d.ts
{ "lib": [ "es2016", "dom" ] } }
2) دسترسی به المان آپلود فایل از طریق یک template reference variable
در حین تعریف المان فایل در فرم برنامه، متغیر screenshotInput# نیز ذکر شدهاست. میتوان به یک چنین متغیرهایی در کامپوننت متناظر به روش ذیل دسترسی یافت:
import { Component, OnInit, ViewChild, ElementRef } from "@angular/core"; export class UploadFileSimpleComponent implements OnInit { @ViewChild("screenshotInput") screenshotInput: ElementRef; submitForm(form: NgForm) { const fileInput: HTMLInputElement = this.screenshotInput.nativeElement; console.log("fileInput.files", fileInput.files); }
اکنون خاصیت screenshotInput کامپوننت، به متغیری به همین نام در قالب متناظر با آن متصل شدهاست. بنابراین با استفاده از خاصیت nativeElement آن همانند کدهایی که در متد submitForm فوق ملاحظه میکنید، میتوان به خاصیت files این کنترل ارسال فایلها دسترسی یافت.
نوع جدید و استاندارد HTMLInputElement نیز در فایل lib.dom.d.ts که پیشتر معرفی شد، ثبت شدهاست.
ارسال فرم درخواست پشتیبانی به سرور
تا اینجا فرمی را تشکیل داده و همچنین به فیلد file آن دسترسی پیدا کردیم. اکنون میخواهیم این اطلاعات را به سمت سرور ارسال کنیم. برای این منظور، سرویس جدیدی را ایجاد خواهیم کرد:
>ng g s UploadFile/UploadFileSimple -m upload-file.module
در ادامه کدهای کامل این سرویس را مشاهده میکنید:
import { Http, RequestOptions, Response, Headers } from "@angular/http"; import { Injectable } from "@angular/core"; import { Observable } from "rxjs/Observable"; import "rxjs/add/operator/do"; import "rxjs/add/operator/catch"; import "rxjs/add/observable/throw"; import "rxjs/add/operator/map"; import "rxjs/add/observable/of"; import { Ticket } from "./ticket"; @Injectable() export class UploadFileSimpleService { private baseUrl = "api/SimpleUpload"; constructor(private http: Http) {} private extractData(res: Response) { const body = res.json(); return body || {}; } private handleError(error: Response): Observable<any> { console.error("observable error: ", error); return Observable.throw(error.statusText); } postTicket(ticket: Ticket, filesList: FileList): Observable<any> { if (!filesList || filesList.length === 0) { return Observable.throw("Please select a file."); } const formData: FormData = new FormData(); for (const key in ticket) { if (ticket.hasOwnProperty(key)) { formData.append(key, ticket[key]); } } for (let i = 0; i < filesList.length; i++) { formData.append(filesList[i].name, filesList[i]); } const headers = new Headers(); headers.append("Accept", "application/json"); const options = new RequestOptions({ headers: headers }); return this.http .post(`${this.baseUrl}/SaveTicket`, formData, options) .map(this.extractData) .catch(this.handleError); } }
روش کار با فرمهایی که فیلدهای ارسال فایل را به همراه دارند، متفاوت است با روش کار با فرمهای معمولی. در فرمهای معمولی، اصل شیء Ticket را به متد this.http.post واگذار میکنیم. مابقی آن خودکار است. در اینجا باید شیء استاندارد FormData را تشکیل داده و سپس اطلاعات را از طریق آن ارسال کنیم:
الف) افزودن مقادیر خواص شیء Ticket به FormData
postTicket(ticket: Ticket, filesList: FileList): Observable<any> { const formData: FormData = new FormData(); for (const key in ticket) { if (ticket.hasOwnProperty(key)) { formData.append(key, ticket[key]); } }
ب) افزودن فایلها به شیء FormData
پس از افزودن اطلاعات ticket به FormData، اکنون نوبت به افزودن فایلهای فرم است:
for (let i = 0; i < filesList.length; i++) { formData.append(filesList[i].name, filesList[i]); }
یک نکته: چون در اینجا کلید اضافه شده، نام فایل است، دیگر نمیتوان در سمت سرور از روش model binding استفاده کرد. چون این نام دیگر ثابت نیست و هربار میتواند متغیر باشد (در حالت model binding دقیقا مشخص است که کلید مشخصی قرار است به سرور ارسال شود و بر همین اساس، نام خاصیت یا پارامتر سمت سرور تعیین میگردد). به همین جهت در سمت سرور برای دسترسی به این مجموعه، از روش Request.Form.Files استفاده میکنیم.
ج) ارسال اطلاعات نهایی به سرور
اکنون که formData را بر اساس اطلاعات اضافی ticket و فایلهای متصل به آن تشکیل دادیم، روش ارسال آن به سرور همانند قبل است:
const headers = new Headers(); headers.append("Accept", "application/json"); const options = new RequestOptions({ headers: headers }); return this.http .post(`${this.baseUrl}/SaveTicket`, formData, options) .map(this.extractData) .catch(this.handleError);
یک نکته: در اینجا در روش استفاده از formData نباید Content-Type را به multipart/form-data تنظیم کرد. در غیراینصورت خطای Missing content-type boundary error را دریافت میکنید.
تکمیل کامپوننت ارسال درخواست پشتیبانی
پس از تکمیل سرویس ارسال اطلاعات به سمت سرور، اکنون نوبت به استفادهی از آن در کامپوننت ارسال فرم درخواست پشتیبانی است. بنابراین ابتدا این سرویس جدید را به سازندهی UploadFileSimpleComponent تزریق میکنیم:
import { UploadFileSimpleService } from "./../upload-file-simple.service"; export class UploadFileSimpleComponent implements OnInit { constructor(private uploadService: UploadFileSimpleService ) {}
submitForm(form: NgForm) { const fileInput: HTMLInputElement = this.screenshotInput.nativeElement; console.log("fileInput.files", fileInput.files); this.uploadService .postTicket(this.model, fileInput.files) .subscribe(data => { console.log("success: ", data); }); }
دریافت فرم درخواست پشتیبانی در سمت سرور و ذخیرهی فایلهای آن
کدهای کامل SimpleUpload که در سرویس فوق مشخص شدهاست، به صورت ذیل هستند. ابتدا مدل Ticket مشخص شدهاست:
namespace AngularTemplateDrivenFormsLab.Models { public class Ticket { public int Id { set; get; } public string Description { set; get; } } }
using System.IO; using System.Threading.Tasks; using AngularTemplateDrivenFormsLab.Models; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; namespace AngularTemplateDrivenFormsLab.Controllers { [Route("api/[controller]")] public class SimpleUploadController : Controller { private readonly IHostingEnvironment _environment; public SimpleUploadController(IHostingEnvironment environment) { _environment = environment; } [HttpPost("[action]")] public async Task<IActionResult> SaveTicket(Ticket ticket) { //TODO: save the ticket ... get id ticket.Id = 1001; var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads"); if (!Directory.Exists(uploadsRootFolder)) { Directory.CreateDirectory(uploadsRootFolder); } var files = Request.Form.Files; foreach (var file in files) { //TODO: do security checks ...! if (file == null || file.Length == 0) { continue; } var filePath = Path.Combine(uploadsRootFolder, file.FileName); using (var fileStream = new FileStream(filePath, FileMode.Create)) { await file.CopyToAsync(fileStream).ConfigureAwait(false); } } return Created("", ticket); } } }
- تزریق IHostingEnvironment در سازندهی کلاس کنترلر، سبب میشود تا از طریق خاصیت WebRootPath آن، به مسیر wwwroot سایت دسترسی پیدا کنیم و فایلهای نهایی را در آنجا ذخیره سازی کنیم.
- همانطور که ملاحظه میکنید، هنوز هم model binding کار کرده و میتوان شیء Ticket را به نحو متداولی دریافت کرد:
SaveTicket(Ticket ticket)
formData.append(filesList[i].name, filesList[i]);
var files = Request.Form.Files; foreach (var file in files)
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
تغییر loadChildren در نسخه 8
در نسخه 8، استفاده از syntax رشتهای برای loadChildren در lazy loading، منسوخ شدهاست:
const routes: Routes = [{ path: 'lazy', // The following string syntax for loadChildren is deprecated loadChildren: './lazy/lazy.module#LazyModule' }];
const routes: Routes = [{ path: 'lazy', // The new import() syntax loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule) }];
import مجزای componentهای Material
اگر ازAngular Material استفاده میکنید باید Component های مورد استفاده را بطور مجزا import کنید. به عنوان مثال اگر از button استفاده کردهاید، باید @angular/material/button را import کنید . البته در اینجا نیز دستور ng update این کار را بطور خودکار انجام میدهد.
بهبود CLI با تولید بستههای متفاوت ES 5 و ES 6
در نسخه 8، دستور build ارائه شده توسط CLI میتواند دو خروجی متفاوت با ES2015 مخصوص مرورگرهای جدید را با حداقل polyfills و ES5 سازگار با مرورگرهای قدیمی را بدهد. در صورتی که بخواهیم build سازگاری با نسخههای قدیمی داشته باشیم، میتوانیم گزینه target در فایل tsconfig.json را به ES5 تغییر دهیم.
برای بروزرسانی پروژههای جاری خود میتوانید از ابزار کمکی ارائه شده توسط تیم angular استفاده کنید.
روش فعالسازی کامپایلر جدید Angular
برای ساخت پروژهای با قابلیت استفاده از ivy compiler میتوانیم از دستور زیر استفاده کنیم:
ng new angularProjectName --enable-ivy
توجه داشته باشید نیاز است nodejs نسخه 10.9 یا 10.9 به بعد نصب باشد.
پس از ساخت پروژه قسمتی با نام angularCompilerOptions در فایل tsconfig.json مشاهد میشودکه نشان دهندهی فعال بودن کامپایلر ivy است:
"angularCompilerOptions": { "enableIvy": true }
تا اینجا «نحوهی نصب و راه اندازی TypeScript را در VSCode» به همراه «تنظیمات کامپایلر TypeScript» و «دریافت فایلهای d.ts. را توسط بستههای NodeJS» بررسی کردیم. در ادامه قصد داریم این تنظیمات را به نگارش کامل VS.NET نیز اعمال کنیم.
به روز رسانی وابستگیهای VS.NET
برای دریافت آخرین نگارش TypeScript نیاز است افزونههای آنرا از سایت رسمی زبان TypeScript دریافت و نصب کرد:
به علاوه نصب افزونهی Web Essentials نیز جهت تکمیل امکانات کار با TypeScript مانند امکان مشاهدهی خروجی جاوا اسکریپت تولیدی، در حین کار با فایل TypeScript فعلی توصیه میشود. همچنین TSLint را نیز نصب میکند.
افزودن فایل تنظیمات tslint
افزونهی Web Essentials که Web Analyzer نیز اکنون جزئی از آن است، به همراه TSLint هم هست که کار آن ارائه راهنماهایی جهت تولید کدهای با کیفیت TypeScript است. گزینههای آنرا در منوی Tools -> Options میتوانید مشاهده کنید:
برای بازنویسی تنظیمات آن (در صورت نیاز) فایل جدیدی را به نام tslint.json به ریشهی پروژه (کنار فایل web.config) اضافه کنید. فایل پیش فرض آن چنین شکلی را دارد:
settings-defaults/tslint.json
و یک نمونهی اصلاح شدهی آن به صورت ذیل است که میتواند به ریشهی پروژه کپی شود:
tslint.json
تنظیمات کامپایلر TypeScript در VS.NET
هرچند قالب افزودن یک پروژهی جدید TypeScript نیز به همراه نصب بستههای TypeScript به لیست پروژههای موجود اضافه میشود، اما عموما نیاز است تا فایلهای ts. را به یک پروژهی وب موجود اضافه کرد. بنابراین، یک پوشهی جدید را به برای مثال به نام TypeScript ایجاد کرده و بر روی آن کلیک راست کنید. سپس گزینهی Add->new item را انتخاب کرده و در اینجا TypeScript را جستجو کنید:
پس از اضافه شدن اولین فایل ts. به پروژه، دیالوگ زیر نیز ظاهر خواهد شد:
در اینجا جستجوی فایلهای d.ts. را پیشنهاد میدهد. فعلا بر روی No کلیک کنید. اینکار را در ادامه انجام خواهیم داد.
پس از افزودن اولین فایل ts. به پروژه، اگر به خواص پروژهی جاری مراجعه کنید، برگهی جدید تنظیمات کامپایلر TypeScript را مشاهده خواهید کرد:
با این تنظیمات در مطلب «تنظیمات کامپایلر TypeScript» پیشتر آشنا شدهاید. برای مثال فرمت خروجی جاوا اسکریپت آن ES 5 باشد و یا در اینجا نوعهای any که به صورت صریح any تعریف نشدهاند، ممنوع شدهاست (تیک پیش فرض آنرا بردارید). نوع ماژولهای تولیدی نیز به commonjs تنظیم شدهاست.
همچنین در اینجا میتوانید گزینهی redirect JavaScript output to directory را هم مثلا به پوشهی Scripts واقع در ریشهی پروژه تنظیم کنید تا فایلهای js. نهایی را در آنجا قرار دهد.
پس از این تنظیمات اولیه، به منوی tools->options مراجعه کرده و گزینهی کامپایل فایلهای ts. ایی را که به solution explorer اضافه نشدهاند، نیز فعال کنید:
اعمال این تنظیمات نیاز به یکبار بستن و گشودن مجدد پروژه را دارد.
فعال سازی کامپایل خودکار فایلهای ts. پس از ذخیرهی آنها
پس از اعمال تغییرات فوق، اگر فایل ts. ایی را تغییر داده و ذخیره کردید و بلافاصله خروجی js. آنرا مشاهده نکردید (این فایلها در پوشهی TypeScriptOutDir تنظیمات ذیل ذخیره میشوند و برای مشاهدهی آنها باید گزینهی show all files را در solution explorer فعال کنید)، فایل csproj پروژهی جاری را در یک ادیتور متنی باز کرده و مداخل تنظیمات تنظیم شدهی در قسمت قبل را پیدا کنید. در اینجا نیاز است مدخل جدید TypeScriptCompileOnSaveEnabled را به صورت دستی اضافه کنید:
پس از این تغییرات بدیهی است یکبار باید پروژه را بسته و مجددا بارگذاری نمائید.
رفع مشکل عدم کامپایل پروژه
زمانیکه افزونههای TypeScript را نصب کنید و تنظیمات فوق را اعمال نمائید، در دو حالت ذخیرهی یک فایل ts و یا کامپایل کل پروژه، فایلهای js تولید خواهند شد. اما ممکن است نگارش نصب شدهی بر روی سیستم شما ناقص باشد و چنین خطایی را در حین کامپایل پروژه دریافت کنید:
اگر این خطا را دریافت کردید، سریعترین راه رفع آن به صورت زیر است:
الف) ابتدا به تمام مسیرهای ذیل (در صورت وجود) مراجعه کرده و پوشهی TypeScript را تغییر نام دهید (یا کلا آنرا حذف کنید):
ب) سپس نصاب افزونهی TypeScript را مجددا اجرا کنید. اینبار گزینهی repair ظاهر میشود. با ترمیم صورت گرفته، مشکل فوق برطرف خواهد شد. این گزینهی repair را در کنترلپنل و قسمت add/remove programs هم میتوانید پیدا کنید (اگر فایل نصاب افزونه را حذف کردهاید).
اصلاح شماره نگارش کامپایلر TypeScript خط فرمان ویژوال استودیو
در فایل C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Tools\VsDevCmd.bat که مربوط به خط فرمان VS.NET است، شماره نگارش TypeScript به 1.5 تنظیم شدهاست که نیاز به اصلاح دستی دارد؛ برای مثال تنظیم آن به نگارش 1.8 به صورت زیر است:
اگر از VS 2013 استفاده میکنید، چنین تنظیمی در فایل C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\Tools\VsDevCmd.bat آن نیز وجود دارد که به نگارش 1 تنظیم شدهاست و این مورد هم باید اصلاح شود (تنظیمات آن دقیقا مانند تنظیم فوق است).
تداخل ReSharper با شماره نگارش TypeScript نصب شده
برای نمونه اگر بخواهیم از decorators استفاده کنیم، یک چنین خطایی نمایش داده میشود:
هرچند در ابتدای بحث، آخرین نگارش TypeScript برای دریافت معرفی شدهاست، اما پس از نصب آن، ممکن است هنوز خطای استفاده از نگارش قدیمی 1.4 را مشاهده کنید. علت آن به نصب بودن ReSharper بر میگردد:
به منوی ReSharper و سپس گزینهی Options آن مراجعه کنید.
در اینجا میتوان نگارش TypeScript مورد استفاده را تغییر داد. این شمارهها، نگارشهایی هستند که ReSharper از آنها پشتیبانی میکند و نه شمارهای که نصب شدهاست.
و یا حتی میتوان به صورت کامل فایلهای ts را از کنترل ReSharper خارج کرد:
این مورد زمانی مفید خواهد بود که شماره نگارش فعلی TypeScript، از شماره نگارش پشتیبانی شدهی توسط ReSharper بالاتر باشد. در این حالت ممکن است syntaxهای جدید زبان TypeScript را ReSharper به صورت خطا اعلام کند که اشتباه است. بنابراین باید به ReSharper اعلام کرد که از این فایلها صرفنظر کند. برای نمونه در زمان نگارش این مطلب، جهت کار با decorators، حتما نیاز است ReSharper را جهت حذف بررسی فایلهای ts تنظیم کرد و گرنه ذیل کدهای مرتبط، خطوط قرمز نمایش خطا را مشاهده خواهید کرد که با توجه به کامپایلر جدید موجود، بیمورد است.
افزودن فایل tsconfig.json به پروژه
همانطور که در مطلب «تنظیمات کامپایلر TypeScript» نیز مطالعه کردید، روش دیگری نیز برای ذکر تنظیمات ویژهی کامپایلر، خصوصا مواردی که در برگهی خواص پروژه هنوز اضافه نشدهاند، با استفاده از افزودن فایل ویژهی tsconfig.json وجود دارد.
پشتیبانی کاملی از فایلهای tsconfig.json در پروژههای VS 2015 با ASP.Core 1.0 وجود دارد و حتی گزینهای در منوی add->new item برای آن درنظر گرفته شدهاست.
اگر گزینهی فوق را در لیست موارد add->new item پیدا نمیکنید (تحت عنوان TypeScript JSON Configuration File)، مهم نیست. تنها کافی است فایل جدیدی را به نام tsconfig.json به ریشهی پوشهی فایلهای ts خود اضافه کنید؛ با این محتوا:
حتی اگر از VS 2013 هم استفاده میکنید، این فایل توسط کامپایلر tsc شناسایی شده و استفاده میشود. برای آزمایش آن، گزینهای غیرمتعارف را به گزینههای موجود اضافه کرده و سپس پروژه را کامپایل کنید. بلافاصله خطایی را در لیست خطاهای کامپایل پروژه دریافت خواهید کرد.
در اینجا نیازی به استفاده از گزینهی watch نیست و ممکن است سبب بروز خطای JsErrorScriptException (0x30001) شود. قرار است این مشکل در نگارشهای بعدی افزونهی TypeScript مخصوص VS.NET برطرف شود.
افزودن فایلهای d.ts. از طریق نیوگت
به ازای هر کتابخانهی جاوا اسکریپتی معروف، یک بستهی نیوگت تعاریف نوعهای TypeScript آن هم وجود دارد.
یک مثال: فرض کنید میخواهیم فایل d.ts. کتابخانهی jQuery را اضافه کنیم. برای این منظور jquery.typescript را در بین بستههای نیوگت موجود، جستجو کنید:
برای سایر کتابخانهها نیز به همین صورت است. نام کتابخانه را به همراه typescript جستجو کنید.
به روز رسانی وابستگیهای VS.NET
برای دریافت آخرین نگارش TypeScript نیاز است افزونههای آنرا از سایت رسمی زبان TypeScript دریافت و نصب کرد:
به علاوه نصب افزونهی Web Essentials نیز جهت تکمیل امکانات کار با TypeScript مانند امکان مشاهدهی خروجی جاوا اسکریپت تولیدی، در حین کار با فایل TypeScript فعلی توصیه میشود. همچنین TSLint را نیز نصب میکند.
افزودن فایل تنظیمات tslint
افزونهی Web Essentials که Web Analyzer نیز اکنون جزئی از آن است، به همراه TSLint هم هست که کار آن ارائه راهنماهایی جهت تولید کدهای با کیفیت TypeScript است. گزینههای آنرا در منوی Tools -> Options میتوانید مشاهده کنید:
برای بازنویسی تنظیمات آن (در صورت نیاز) فایل جدیدی را به نام tslint.json به ریشهی پروژه (کنار فایل web.config) اضافه کنید. فایل پیش فرض آن چنین شکلی را دارد:
settings-defaults/tslint.json
و یک نمونهی اصلاح شدهی آن به صورت ذیل است که میتواند به ریشهی پروژه کپی شود:
tslint.json
تنظیمات کامپایلر TypeScript در VS.NET
هرچند قالب افزودن یک پروژهی جدید TypeScript نیز به همراه نصب بستههای TypeScript به لیست پروژههای موجود اضافه میشود، اما عموما نیاز است تا فایلهای ts. را به یک پروژهی وب موجود اضافه کرد. بنابراین، یک پوشهی جدید را به برای مثال به نام TypeScript ایجاد کرده و بر روی آن کلیک راست کنید. سپس گزینهی Add->new item را انتخاب کرده و در اینجا TypeScript را جستجو کنید:
پس از اضافه شدن اولین فایل ts. به پروژه، دیالوگ زیر نیز ظاهر خواهد شد:
در اینجا جستجوی فایلهای d.ts. را پیشنهاد میدهد. فعلا بر روی No کلیک کنید. اینکار را در ادامه انجام خواهیم داد.
پس از افزودن اولین فایل ts. به پروژه، اگر به خواص پروژهی جاری مراجعه کنید، برگهی جدید تنظیمات کامپایلر TypeScript را مشاهده خواهید کرد:
با این تنظیمات در مطلب «تنظیمات کامپایلر TypeScript» پیشتر آشنا شدهاید. برای مثال فرمت خروجی جاوا اسکریپت آن ES 5 باشد و یا در اینجا نوعهای any که به صورت صریح any تعریف نشدهاند، ممنوع شدهاست (تیک پیش فرض آنرا بردارید). نوع ماژولهای تولیدی نیز به commonjs تنظیم شدهاست.
همچنین در اینجا میتوانید گزینهی redirect JavaScript output to directory را هم مثلا به پوشهی Scripts واقع در ریشهی پروژه تنظیم کنید تا فایلهای js. نهایی را در آنجا قرار دهد.
پس از این تنظیمات اولیه، به منوی tools->options مراجعه کرده و گزینهی کامپایل فایلهای ts. ایی را که به solution explorer اضافه نشدهاند، نیز فعال کنید:
اعمال این تنظیمات نیاز به یکبار بستن و گشودن مجدد پروژه را دارد.
فعال سازی کامپایل خودکار فایلهای ts. پس از ذخیرهی آنها
پس از اعمال تغییرات فوق، اگر فایل ts. ایی را تغییر داده و ذخیره کردید و بلافاصله خروجی js. آنرا مشاهده نکردید (این فایلها در پوشهی TypeScriptOutDir تنظیمات ذیل ذخیره میشوند و برای مشاهدهی آنها باید گزینهی show all files را در solution explorer فعال کنید)، فایل csproj پروژهی جاری را در یک ادیتور متنی باز کرده و مداخل تنظیمات تنظیم شدهی در قسمت قبل را پیدا کنید. در اینجا نیاز است مدخل جدید TypeScriptCompileOnSaveEnabled را به صورت دستی اضافه کنید:
<PropertyGroup Condition="'$(Configuration)' == 'Debug'"> <TypeScriptModuleKind>commonjs</TypeScriptModuleKind> <TypeScriptCompileOnSaveEnabled>True</TypeScriptCompileOnSaveEnabled> <TypeScriptOutDir>.\Scripts</TypeScriptOutDir> <TypeScriptNoImplicitAny>True</TypeScriptNoImplicitAny> <TypeScriptTarget>ES5</TypeScriptTarget> <TypeScriptRemoveComments>false</TypeScriptRemoveComments> <TypeScriptOutFile></TypeScriptOutFile> <TypeScriptGeneratesDeclarations>false</TypeScriptGeneratesDeclarations> <TypeScriptSourceMap>true</TypeScriptSourceMap> <TypeScriptMapRoot></TypeScriptMapRoot> <TypeScriptSourceRoot></TypeScriptSourceRoot> <TypeScriptNoEmitOnError>true</TypeScriptNoEmitOnError> </PropertyGroup>
رفع مشکل عدم کامپایل پروژه
زمانیکه افزونههای TypeScript را نصب کنید و تنظیمات فوق را اعمال نمائید، در دو حالت ذخیرهی یک فایل ts و یا کامپایل کل پروژه، فایلهای js تولید خواهند شد. اما ممکن است نگارش نصب شدهی بر روی سیستم شما ناقص باشد و چنین خطایی را در حین کامپایل پروژه دریافت کنید:
Your project file uses a different version of the TypeScript compiler and tools than is currently installed on this machine. No compiler was found at C:\Program Files (x86)\Microsoft SDKs\TypeScript\1.8\tsc.exe. You may be able to fix this problem by changing the <TypeScriptToolsVersion> element in your project file.
الف) ابتدا به تمام مسیرهای ذیل (در صورت وجود) مراجعه کرده و پوشهی TypeScript را تغییر نام دهید (یا کلا آنرا حذف کنید):
C:\Program Files (x86)\Microsoft SDKs C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v11.0\ C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v12.0\ C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v14.0\
اصلاح شماره نگارش کامپایلر TypeScript خط فرمان ویژوال استودیو
در فایل C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Tools\VsDevCmd.bat که مربوط به خط فرمان VS.NET است، شماره نگارش TypeScript به 1.5 تنظیم شدهاست که نیاز به اصلاح دستی دارد؛ برای مثال تنظیم آن به نگارش 1.8 به صورت زیر است:
@rem Add path to TypeScript Compiler @if exist "%ProgramFiles%\Microsoft SDKs\TypeScript\1.8" set PATH=%ProgramFiles%\Microsoft SDKs\TypeScript\1.8;%PATH% @if exist "%ProgramFiles(x86)%\Microsoft SDKs\TypeScript\1.8" set PATH=%ProgramFiles(x86)%\Microsoft SDKs\TypeScript\1.8;%PATH%
تداخل ReSharper با شماره نگارش TypeScript نصب شده
برای نمونه اگر بخواهیم از decorators استفاده کنیم، یک چنین خطایی نمایش داده میشود:
هرچند در ابتدای بحث، آخرین نگارش TypeScript برای دریافت معرفی شدهاست، اما پس از نصب آن، ممکن است هنوز خطای استفاده از نگارش قدیمی 1.4 را مشاهده کنید. علت آن به نصب بودن ReSharper بر میگردد:
به منوی ReSharper و سپس گزینهی Options آن مراجعه کنید.
ReSharper -> Options -> Code Editing -> TypeScript -> Inspections -> Typescript language level
در اینجا میتوان نگارش TypeScript مورد استفاده را تغییر داد. این شمارهها، نگارشهایی هستند که ReSharper از آنها پشتیبانی میکند و نه شمارهای که نصب شدهاست.
و یا حتی میتوان به صورت کامل فایلهای ts را از کنترل ReSharper خارج کرد:
Tools -> Options -> ReSharper Options -> Code Inspection -> Settings -> File Masks to Skip -> add *.ts
افزودن فایل tsconfig.json به پروژه
همانطور که در مطلب «تنظیمات کامپایلر TypeScript» نیز مطالعه کردید، روش دیگری نیز برای ذکر تنظیمات ویژهی کامپایلر، خصوصا مواردی که در برگهی خواص پروژه هنوز اضافه نشدهاند، با استفاده از افزودن فایل ویژهی tsconfig.json وجود دارد.
پشتیبانی کاملی از فایلهای tsconfig.json در پروژههای VS 2015 با ASP.Core 1.0 وجود دارد و حتی گزینهای در منوی add->new item برای آن درنظر گرفته شدهاست.
اگر گزینهی فوق را در لیست موارد add->new item پیدا نمیکنید (تحت عنوان TypeScript JSON Configuration File)، مهم نیست. تنها کافی است فایل جدیدی را به نام tsconfig.json به ریشهی پوشهی فایلهای ts خود اضافه کنید؛ با این محتوا:
{ "compilerOptions": { "target": "es5", "outDir": "../Scripts", "module": "commonjs", "sourceMap": true, //"watch": true, // JsErrorScriptException (0x30001) //"compileOnSave": true, // https://github.com/Microsoft/TypeScript/issues/7362#issuecomment-196586037 "experimentalDecorators": true, "emitDecoratorMetadata": true } }
در اینجا نیازی به استفاده از گزینهی watch نیست و ممکن است سبب بروز خطای JsErrorScriptException (0x30001) شود. قرار است این مشکل در نگارشهای بعدی افزونهی TypeScript مخصوص VS.NET برطرف شود.
افزودن فایلهای d.ts. از طریق نیوگت
به ازای هر کتابخانهی جاوا اسکریپتی معروف، یک بستهی نیوگت تعاریف نوعهای TypeScript آن هم وجود دارد.
یک مثال: فرض کنید میخواهیم فایل d.ts. کتابخانهی jQuery را اضافه کنیم. برای این منظور jquery.typescript را در بین بستههای نیوگت موجود، جستجو کنید:
برای سایر کتابخانهها نیز به همین صورت است. نام کتابخانه را به همراه typescript جستجو کنید.
اغلب برنامههای AngularJS 2.0، اطلاعات خود را از طریق پروتکل HTTP، از سرور دریافت میکنند. برنامه یک درخواست Get را صادر کرده و سپس سرور پاسخ مناسبی را ارائه میدهد.
مقدمهای بر RxJS
اگر به پیشنیازهای نصب AngularJS 2.0 در قسمت اول این سری دقت کرده باشید، یکی از موارد آن، RxJS است:
یک Observable، آرایهای است که اعضای آن به صورت غیر همزمان (asynchronously) در طول زمان دریافت میشوند. برای مثال پس از شروع یک عملیات async، ابتدا عنصر اول آرایه دریافت میشود، پس از مدتی عنصر دوم و در آخر عنصر سوم آن. به همین جهت از Observableها برای مدیریت دادههای async مانند دریافت اطلاعات از یک وب سرور، استفاده میشود.
قرار است Observableها به ES 2016 یا نگارش پس از ES 6 اضافه شوند و یکی از پیشنهادات آن هستند. اما هم اکنون AngularJS 2.0 از این امکان، توسط یک کتابخانهی ثالث، به نام reactive extensions یا Rx، استفاده میکند. از RxJS در سرویس HTTP و همچنین مدیریت سیستم رخدادهای AngularJS 2.0 استفاده میشود. Observableها امکانی را فراهم میکنند تا به ازای دریافت هر اطلاعات async از سرور، بتوان توسط رخدادهایی از وقوع آنها مطلع شد.
در نگارش قبلی AngularJS از Promises برای مدیریت اعمال غیرهمزمان استفاده میشد. Observableها تفاوتهای قابل ملاحظهای با Promises دارند:
- یک Promise تنها یک مقدار، یا خطا را بر میگرداند؛ اما یک Observable چندین مقدار را در طول یک بازهی زمانی باز میگرداند.
- برخلاف Promises، میتوان عملیات یک Observable را لغو کرد.
- Observableها از عملگرهایی مانند map، reduce، filter و غیره نیز پشتیبانی میکنند.
البته باید عنوان کرد که هنوز هم میتوان از Promises در صورت تمایل در AngularJS 2.0 نیز استفاده کرد.
تنظیمات اولیهی کار با RxJS در AngularJS 2.0
برای استفاده از RxJS در AngularJS 2.0، مراحلی مانند افزودن مدخل اسکریپت http.dev.js، ثبت پروایدر HTTP و importهای لازم، باید طی شوند که در ادامه آنها را بررسی خواهیم کرد:
الف) سرویس HTTP جزئی از angular2/core نیست. به همین جهت مدخل اسکریپت متناظر با آن، باید به صفحهی اصلی سایت اضافه شود که این مورد، در قسمت اول بررسی پیشنیازهای نصب AngularJS 2.0 صورت گرفتهاست:
این تعریف در فایل Views\Shared\_Layout.cshtml (و یا index.html) پروژهی جاری موجود است. همچنین در این صفحه، مدخل Rx.js نیز ذکر شدهاست.
ب) اکنون فایل app.component.ts را گشوده و سرویس HTTP را به آن اضافه میکنیم. با نحوهی ثبت سرویسها در قسمت قبل آشنا شدیم:
از آنجائیکه میخواهیم سرویس HTTP، در تمام کامپوننتهای برنامه در دسترس باشد، آنرا در بالاترین سطح سلسه مراتب کامپوننتهای موجود، یا همان کامپوننت ریشهی سایت، ثبت و معرفی میکنیم. بنابراین سرویس توکار HTTP یا HTTP_PROVIDERS به لیست پروایدرها، اضافه شدهاست.
ج) پس از آن نیاز است importهای متناظر نیز به ابتدای ماژول فعلی، جهت شناسایی این سرویس و همچنین امکانات rx.js اضافه شوند.
تعریف 'import 'rxjs/Rx به این شکل، به module loader اعلام میکند که این کتابخانه را بارگذاری کن، اما چیزی را import نکن. هنگامیکه این کتابخانه بارگذاری میشود، کدهای جاوا اسکریپتی آن اجرا شده و سبب میشوند که عملگرهای ویژهی Observable آن مانند map و filter نیز در دسترس برنامه قرار گیرند.
ساخت یک سرویس سمت سرور بازگشت لیست محصولات به صورت JSON
چون در ادامه میخواهیم لیست محصولات را از سرور دریافت کنیم، برنامهی ASP.NET MVC فعلی را اندکی تغییر میدهیم تا این لیست را به صورت JSON بازگشت دهد.
بنابراین ابتدا کلاس مدل محصولات را به نحو ذیل به پوشهی Models اضافه کرده:
و سپس اکشن متد Products، لیست محصولات فرضی این سرویس را بازگشت میدهد:
در اینجا از JSON.NET جهت بازگشت camel case نام خواص مورد نیاز در سمت کاربر، استفاده شدهاست.
برای مطالعهی بیشتر:
«استفاده از JSON.NET در ASP.NET MVC»
«تنظیمات و نکات کاربردی کتابخانهی JSON.NET»
به این ترتیب، آدرس http://localhost:2222/home/products، خروجی JSON سرویس لیست محصولات را در مثال جاری، ارائه میدهد.
ارسال یک درخواست HTTP به سرور توسط AngularJS 2.0
اکنون پس از تنظیمات ثبت و معرفی سرویس HTTP و همچنین برپایی یک سرویس سمت سرور ارائهی لیست محصولات، میخواهیم سرویس ProductService را که در قسمت قبل ایجاد کردیم (فایل product.service.ts)، جهت دریافت لیست محصولات از سمت سرور، تغییر دهیم:
از آنجائیکه این سرویس دارای یک وابستگی تزریق شدهاست، ذکر ()Injectable@، پیش از تعریف نام کلاس، ضروری است.
در سازندهی کلاس ProductService، کار تزریق وابستگی سرویس Http انجام شدهاست. به این ترتیب با استفاده از متغیر خصوصی http_، میتوان در کلاس جاری به امکانات این سرویس دسترسی یافت (همان «تزریق سرویسها به کامپوننتها» در قسمت قبل).
سپس متد get آن، یک درخواست HTTP از نوع GET را به آدرس مشخص شدهی در متغیر productUrl_ ارسال میکند (یا همان سرویس سمت سرور برنامه).
سرویس Http و همچنین شیء Response آن در ماژولهای Http و Response قرار دارند که در ابتدای صفحه import شدهاند.
متد http get یک Observable را بازگشت میدهد که در نهایت خروجی این متد نیز به همان <[]Observable<IProduct، تنظیم شدهاست. Observable یک شیء جنریک است و در اینجا نوع آن، آرایهای از محصولات درنظر گرفته شدهاست.
اکنون که امضای این متد تغییر یافته است (پیش از این صرفا یک آرایهی ساده از محصولات بود)، استفاده کننده (در کلاس ProductListComponent) باید به تغییرات آن از طریق متد subscribe گوش فرا دهد.
فعلا در کلاس جاری، پس از پایان کار دریافت اطلاعات از سرور، اطلاعات نهایی در متد map در دسترس قرار میگیرد (که یکی از عملگرهای RxJs است). کار متد map، اصطلاحا projection است. این متد، هر عضو دریافتی از خروجی سرور را به فرمتی جدید نگاشت میکند.
هر درخواست HTTP، در اصل یک عملیات async است. یعنی در اینجا توالی که در اختیار Observable ما قرار میگیرد، تنها یک المان دارد که همان شیء HTTP Response است.
بنابراین کار متد map فوق، تبدیل شیء خروجی از سرور، به آرایهای از محصولات است.
در اینجا یک سری کدهای مدیریت استثناءها را نیز در صورت بروز مشکلی میتوان تعریف کرد. برای مثال در اینجا متد catch، کار پردازش خطاهای رخ داده را انجام میدهد.
از متد do جهت لاگ کردن عملیات رخ داده و دادههای دریافتی در کنسول developer tools مرورگرها استفاده شدهاست.
یک نکته:
اگر خروجی JSON از سرور، برای مثال داخل خاصیتی به نام data محصور شده بود، بجای ()response.json میبایستی از response.json().data استفاده میشد.
گوش فرا دادن به Observable دریافتی از سرور
تا اینجا یک درخواست HTTP GET را به سمت سرور ارسال کردیم و خروجی آن به صورت Observable در اختیار ما است. اکنون نیاز است کدهای ProductListComponent را جهت گوش فرا دادن به این Observable تغییر دهیم. برای این منظور فایل product-list.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
در کلاس ProductListComponent، در متد ngOnInit که در آن کار آغاز و مقدار دهی وابستگیهای کامپوننت انجام میشود، متد ()productService.getProducts_ فراخوانی شدهاست. این متد یک Observable را بر میگرداند. بنابراین برای پردازش نتیجهی آن نیاز است متد subscribe را در ادامهی آن، زنجیر وار ذکر کرد.
اولین پارامتر متد subscribe، کار دریافت نتایج حاصل را به عهده دارد. برای مثال اگر حاصل عملیات در طی سه مرحله صورت گیرد، سه بار نتیجهی دریافتی را میتوان در اینجا پردازش کرد. البته همانطور که عنوان شد، یک عملیات غیرهمزمان HTTP، تنها در طی یک مرحله، HTTP Response را دریافت میکند؛ بنابراین، پارامتر اول متد subscribe نیز تنها یکبار اجرا میشود. در اینجا فرصت خواهیم داشت تا آرایهی دریافتی حاصل از متد map قسمت قبل را به خاصیت عمومی products کلاس جاری نسبت دهیم.
پارامتر دوم متد subscribe در صورت شکست عملیات فراخوانی میشود. در اینجا حاصل آن به خاصیت جدید errorMessage نسبت داده شدهاست.
اکنون برنامه را مجددا اجرا کنید، هنوز باید لیست محصولات، مانند قبل نمایش داده شود.
یک نکته
اگر برنامه را اجرا کردید و خروجی مشاهده نشد، به کنسول developer tools مرورگر مراجعه کنید؛ احتمالا خطای ذیل در آن درج شدهاست:
به این معنا که پروایدر HTTP یا همان HTTP_PROVIDERS، جایی معرفی نشدهاست. البته مشکلی از این لحاظ در برنامه وجود ندارد و این پروایدر در بالاترین سطح ممکن و در فایل app.component.ts ثبت شدهاست. مشکل اینجا است که مرورگر، فایل قدیمی http://localhost:2222/app/app.component.js را کش کردهاست (به همراه تمام اسکریپتهای دیگر) و این فایل قدیمی، فاقد تعریف سرویس HTTP است. بنابراین با حذف کش مرورگر و دریافت فایلهای js جدید، مشکل برطرف خواهد شد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part8.zip
خلاصهی بحث
برای کار با سرور و ارسال درخواستهای HTTP، ابتدا نیاز است مدخل تعریف http.dev.js به index.html اضافه شود و سپس HTTP_PROVIDERS را در بالاترین سطح کامپوننتهای تعریف شده، ثبت و معرفی کرد. پس از آن نیاز است RxJs را نیز import کرد. در ادامه، سرویس دریافت لیست محصولات، وابستگی سرویس HTTP را توسط سازندهی خود دریافت کرده و از آن برای صدور یک فرمان HTTP GET استفاده میکند. سپس با استفاده از متد map، کار نگاشت شیء Response دریافتی، به فرمت مناسب مدنظر، صورت میگیرد.
در ادامه هر کلاسی که نیاز دارد با این کلاس سرویس دریافت اطلاعات کار کند، متد subscribe را فراخوانی کرده و نتیجهی عملیات را پردازش میکند.
مقدمهای بر RxJS
اگر به پیشنیازهای نصب AngularJS 2.0 در قسمت اول این سری دقت کرده باشید، یکی از موارد آن، RxJS است:
"dependencies": { "rxjs": "5.0.0-beta.2" },
قرار است Observableها به ES 2016 یا نگارش پس از ES 6 اضافه شوند و یکی از پیشنهادات آن هستند. اما هم اکنون AngularJS 2.0 از این امکان، توسط یک کتابخانهی ثالث، به نام reactive extensions یا Rx، استفاده میکند. از RxJS در سرویس HTTP و همچنین مدیریت سیستم رخدادهای AngularJS 2.0 استفاده میشود. Observableها امکانی را فراهم میکنند تا به ازای دریافت هر اطلاعات async از سرور، بتوان توسط رخدادهایی از وقوع آنها مطلع شد.
در نگارش قبلی AngularJS از Promises برای مدیریت اعمال غیرهمزمان استفاده میشد. Observableها تفاوتهای قابل ملاحظهای با Promises دارند:
- یک Promise تنها یک مقدار، یا خطا را بر میگرداند؛ اما یک Observable چندین مقدار را در طول یک بازهی زمانی باز میگرداند.
- برخلاف Promises، میتوان عملیات یک Observable را لغو کرد.
- Observableها از عملگرهایی مانند map، reduce، filter و غیره نیز پشتیبانی میکنند.
البته باید عنوان کرد که هنوز هم میتوان از Promises در صورت تمایل در AngularJS 2.0 نیز استفاده کرد.
تنظیمات اولیهی کار با RxJS در AngularJS 2.0
برای استفاده از RxJS در AngularJS 2.0، مراحلی مانند افزودن مدخل اسکریپت http.dev.js، ثبت پروایدر HTTP و importهای لازم، باید طی شوند که در ادامه آنها را بررسی خواهیم کرد:
الف) سرویس HTTP جزئی از angular2/core نیست. به همین جهت مدخل اسکریپت متناظر با آن، باید به صفحهی اصلی سایت اضافه شود که این مورد، در قسمت اول بررسی پیشنیازهای نصب AngularJS 2.0 صورت گرفتهاست:
<!-- Required for http --> <script src="~/node_modules/angular2/bundles/http.dev.js"></script>
ب) اکنون فایل app.component.ts را گشوده و سرویس HTTP را به آن اضافه میکنیم. با نحوهی ثبت سرویسها در قسمت قبل آشنا شدیم:
import { Component } from 'angular2/core'; import { HTTP_PROVIDERS } from 'angular2/http'; import 'rxjs/Rx'; // Load all features import { ProductListComponent } from './products/product-list.component'; import { ProductService } from './products/product.service'; @Component({ selector: 'pm-app', template:` <div><h1>{{pageTitle}}</h1> <pm-products></pm-products> </div> `, directives: [ProductListComponent], providers: [ ProductService, HTTP_PROVIDERS ] }) export class AppComponent { pageTitle: string = "DNT AngularJS 2.0 APP"; }
ج) پس از آن نیاز است importهای متناظر نیز به ابتدای ماژول فعلی، جهت شناسایی این سرویس و همچنین امکانات rx.js اضافه شوند.
تعریف 'import 'rxjs/Rx به این شکل، به module loader اعلام میکند که این کتابخانه را بارگذاری کن، اما چیزی را import نکن. هنگامیکه این کتابخانه بارگذاری میشود، کدهای جاوا اسکریپتی آن اجرا شده و سبب میشوند که عملگرهای ویژهی Observable آن مانند map و filter نیز در دسترس برنامه قرار گیرند.
ساخت یک سرویس سمت سرور بازگشت لیست محصولات به صورت JSON
چون در ادامه میخواهیم لیست محصولات را از سرور دریافت کنیم، برنامهی ASP.NET MVC فعلی را اندکی تغییر میدهیم تا این لیست را به صورت JSON بازگشت دهد.
بنابراین ابتدا کلاس مدل محصولات را به نحو ذیل به پوشهی Models اضافه کرده:
namespace MVC5Angular2.Models { public class Product { public int ProductId { set; get; } public string ProductName { set; get; } public string ProductCode { set; get; } public string ReleaseDate { set; get; } public decimal Price { set; get; } public string Description { set; get; } public double StarRating { set; get; } public string ImageUrl { set; get; } } }
using System.Collections.Generic; using System.Text; using System.Web.Mvc; using MVC5Angular2.Models; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace MVC5Angular2.Controllers { public class HomeController : Controller { // GET: Home public ActionResult Index() { return View(); } public ActionResult Products() { var products = new List<Product> { new Product { ProductId= 2, ProductName= "Garden Cart", ProductCode= "GDN-0023", ReleaseDate= "March 18, 2016", Description= "15 gallon capacity rolling garden cart", Price= (decimal) 32.99, StarRating= 4.2, ImageUrl= "app/assets/images/garden_cart.png" }, new Product { ProductId= 5, ProductName= "Hammer", ProductCode= "TBX-0048", ReleaseDate= "May 21, 2016", Description= "Curved claw steel hammer", Price= (decimal) 8.9, StarRating= 4.8, ImageUrl= "app/assets/images/rejon_Hammer.png" } }; return new ContentResult { Content = JsonConvert.SerializeObject(products, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }), ContentType = "application/json", ContentEncoding = Encoding.UTF8 }; } } }
برای مطالعهی بیشتر:
«استفاده از JSON.NET در ASP.NET MVC»
«تنظیمات و نکات کاربردی کتابخانهی JSON.NET»
به این ترتیب، آدرس http://localhost:2222/home/products، خروجی JSON سرویس لیست محصولات را در مثال جاری، ارائه میدهد.
ارسال یک درخواست HTTP به سرور توسط AngularJS 2.0
اکنون پس از تنظیمات ثبت و معرفی سرویس HTTP و همچنین برپایی یک سرویس سمت سرور ارائهی لیست محصولات، میخواهیم سرویس ProductService را که در قسمت قبل ایجاد کردیم (فایل product.service.ts)، جهت دریافت لیست محصولات از سمت سرور، تغییر دهیم:
import { Injectable } from 'angular2/core'; import { IProduct } from './product'; import { Http, Response } from 'angular2/http'; import { Observable } from 'rxjs/Observable'; @Injectable() export class ProductService { private _productUrl = '/home/products'; constructor(private _http: Http) { } getProducts(): Observable<IProduct[]> { return this._http.get(this._productUrl) .map((response: Response) => <IProduct[]>response.json()) .do(data => console.log("All: " + JSON.stringify(data))) .catch(this.handleError); } private handleError(error: Response) { console.error(error); return Observable.throw(error.json().error || 'Server error'); } }
در سازندهی کلاس ProductService، کار تزریق وابستگی سرویس Http انجام شدهاست. به این ترتیب با استفاده از متغیر خصوصی http_، میتوان در کلاس جاری به امکانات این سرویس دسترسی یافت (همان «تزریق سرویسها به کامپوننتها» در قسمت قبل).
سپس متد get آن، یک درخواست HTTP از نوع GET را به آدرس مشخص شدهی در متغیر productUrl_ ارسال میکند (یا همان سرویس سمت سرور برنامه).
سرویس Http و همچنین شیء Response آن در ماژولهای Http و Response قرار دارند که در ابتدای صفحه import شدهاند.
متد http get یک Observable را بازگشت میدهد که در نهایت خروجی این متد نیز به همان <[]Observable<IProduct، تنظیم شدهاست. Observable یک شیء جنریک است و در اینجا نوع آن، آرایهای از محصولات درنظر گرفته شدهاست.
اکنون که امضای این متد تغییر یافته است (پیش از این صرفا یک آرایهی ساده از محصولات بود)، استفاده کننده (در کلاس ProductListComponent) باید به تغییرات آن از طریق متد subscribe گوش فرا دهد.
فعلا در کلاس جاری، پس از پایان کار دریافت اطلاعات از سرور، اطلاعات نهایی در متد map در دسترس قرار میگیرد (که یکی از عملگرهای RxJs است). کار متد map، اصطلاحا projection است. این متد، هر عضو دریافتی از خروجی سرور را به فرمتی جدید نگاشت میکند.
هر درخواست HTTP، در اصل یک عملیات async است. یعنی در اینجا توالی که در اختیار Observable ما قرار میگیرد، تنها یک المان دارد که همان شیء HTTP Response است.
بنابراین کار متد map فوق، تبدیل شیء خروجی از سرور، به آرایهای از محصولات است.
در اینجا یک سری کدهای مدیریت استثناءها را نیز در صورت بروز مشکلی میتوان تعریف کرد. برای مثال در اینجا متد catch، کار پردازش خطاهای رخ داده را انجام میدهد.
از متد do جهت لاگ کردن عملیات رخ داده و دادههای دریافتی در کنسول developer tools مرورگرها استفاده شدهاست.
یک نکته:
اگر خروجی JSON از سرور، برای مثال داخل خاصیتی به نام data محصور شده بود، بجای ()response.json میبایستی از response.json().data استفاده میشد.
گوش فرا دادن به Observable دریافتی از سرور
تا اینجا یک درخواست HTTP GET را به سمت سرور ارسال کردیم و خروجی آن به صورت Observable در اختیار ما است. اکنون نیاز است کدهای ProductListComponent را جهت گوش فرا دادن به این Observable تغییر دهیم. برای این منظور فایل product-list.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
errorMessage: string; ngOnInit(): void { //console.log('In OnInit'); this._productService.getProducts() .subscribe( products => this.products = products, error => this.errorMessage = <any>error); }
اولین پارامتر متد subscribe، کار دریافت نتایج حاصل را به عهده دارد. برای مثال اگر حاصل عملیات در طی سه مرحله صورت گیرد، سه بار نتیجهی دریافتی را میتوان در اینجا پردازش کرد. البته همانطور که عنوان شد، یک عملیات غیرهمزمان HTTP، تنها در طی یک مرحله، HTTP Response را دریافت میکند؛ بنابراین، پارامتر اول متد subscribe نیز تنها یکبار اجرا میشود. در اینجا فرصت خواهیم داشت تا آرایهی دریافتی حاصل از متد map قسمت قبل را به خاصیت عمومی products کلاس جاری نسبت دهیم.
پارامتر دوم متد subscribe در صورت شکست عملیات فراخوانی میشود. در اینجا حاصل آن به خاصیت جدید errorMessage نسبت داده شدهاست.
اکنون برنامه را مجددا اجرا کنید، هنوز باید لیست محصولات، مانند قبل نمایش داده شود.
یک نکته
اگر برنامه را اجرا کردید و خروجی مشاهده نشد، به کنسول developer tools مرورگر مراجعه کنید؛ احتمالا خطای ذیل در آن درج شدهاست:
EXCEPTION: No provider for Http!
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part8.zip
خلاصهی بحث
برای کار با سرور و ارسال درخواستهای HTTP، ابتدا نیاز است مدخل تعریف http.dev.js به index.html اضافه شود و سپس HTTP_PROVIDERS را در بالاترین سطح کامپوننتهای تعریف شده، ثبت و معرفی کرد. پس از آن نیاز است RxJs را نیز import کرد. در ادامه، سرویس دریافت لیست محصولات، وابستگی سرویس HTTP را توسط سازندهی خود دریافت کرده و از آن برای صدور یک فرمان HTTP GET استفاده میکند. سپس با استفاده از متد map، کار نگاشت شیء Response دریافتی، به فرمت مناسب مدنظر، صورت میگیرد.
در ادامه هر کلاسی که نیاز دارد با این کلاس سرویس دریافت اطلاعات کار کند، متد subscribe را فراخوانی کرده و نتیجهی عملیات را پردازش میکند.
اشتراکها
معرفی پیش نمایش TypeScript 2.0
- شاید Angular 2 بهترین نباشد یا نشود، اما این مهم نیست. عوامل زیادی در انتخاب یک فریم ورک مؤثر هستند:
بنابراین آیا پس از ارائهی نهایی Angular 2، ارزش یادگیری را دارد؟ بله؛ حتما. ابتدا (و هم اکنون) اعتراضهای شدیدی در مورد عدم سازگاری آن با نگارشهای قبلی وجود خواهد داشت و پس از مدتی همه آنرا فراموش کرده و خودشان را وفق میدهند.
- چه کسانی این فریم ورک را توسعه میدهند؟ گوگل. این مورد خیلی مهم است. در این بین شاید Aurelia مدرنتر به نظر برسد اما تیم آن سابقهی خوبی در نگهداری محصولات قبلی آن ندارد. برای مثال Durandal آنها به طور کامل رها شده و الان Aurelia را شروع کردهاند.
- تیم Angular اینقدر شجاعت داشته که اقرار کند نگارش 1 آن مشکلات زیادی دارد و دست به یک بازنویسی کامل زدهاند. باید دقت کرد که چقدر اینکار برای یک تیم محبوب در وب مشکل است و قطعا نگارش 2 آن که با این حجم بالای اعتراضات عدم سازگاری با نگارش 1 آن تولید شدهاست، بسیاری از مشکلات نگارش قبلی را ندارد. همچنین همین مساله سازگاری آنرا با نگارشهای بعد از 2 نیز تضمین خواهد کرد. چون اینبار دست به یک طراحی مجدد زدهاند و ... این طراحی مجدد خیلی برای آنها گران تمام شدهاست (خصوصا از لحاظ حجم انتقادهای رسیده). بنابراین در آینده در زمینه سازگاری با نگارشهای قبلی به شدت محتاط خواهند بود. این موردی است که استفاده کنندگان از ReactJS به زودی با آن مواجه خواهند شد.
- گروه کاربری مصرف کنندگان آن. چه تعداد مقاله، ویدیوی آموزشی، انجمن رفع اشکال و امثال اینها را در مورد یک محصول میتوانید پیدا کنید؟ قطعا AngularJS در این زمینه حرف اول را میزند.
- Angular 2 بر مبنای استاندارد web component طراحی شدهاست که در دراز مدت نیز سبب برد AngularJS 2 خواهد شد و نیاز کمتر به بازنویسیهای کلی.
- Angular 2 برای کار با محصولات موبایل بهینه سازی شدهاست و مانند بوت استرپ 3 یک فریم ورک mobile first است.
- استفادهی از Type Script به عنوان زبان اول این پلتفرم (البته استفادهی از آن اجباری نیست). این مساله در دراز مدت سبب تولید کدهایی با کیفیت بالاتر میشود. برای مثال الان ReactJS مخلوطی است از ES 5 و ES 6. اما AngularJS 2 تصمیم بهتری را اتخاذ کردهاست. همین مساله سبب شدهاست که توسعهی Type Script توسط مایکروسافت سرعت بیشتری پیدا کند.
- داشتن امکانات توکار بیشتری نسبت به رقبا. برای مثال ReactJS یک کتابخانه است؛ اما AngularJS 2 یک فریم ورک کامل که تمام کتابخانههای جانبی رقبا را یکجا از روز اول دارد.
بنابراین آیا پس از ارائهی نهایی Angular 2، ارزش یادگیری را دارد؟ بله؛ حتما. ابتدا (و هم اکنون) اعتراضهای شدیدی در مورد عدم سازگاری آن با نگارشهای قبلی وجود خواهد داشت و پس از مدتی همه آنرا فراموش کرده و خودشان را وفق میدهند.
Unhandled exceptions are a bit of a misnomer. In .NET, every exception is handled. By the time you access the specifics of an error in your Try-Catch block, the Framework has already analyzed the problem, built a structure to contain its details, examined the stack trace, and used reflection to pinpoint the location of the error, among other mundane tasks. In short, when errors occur, .NET serves them up to your code in a neatly packaged, highly examinable data block.