- مثال پیوست شده کمی بالاتر یک مثال ASP.NET MVC است.
- Process.Start را حذف کنید؛ نیازی نیست.
- به قسمت new FileStream آن دقت کنید. اینجا مسیر یک فایل را میشود مشخص کرد. فایل نهایی تولید شده در این مسیر نوشته میشود. از آن مسیر در برنامههای وب و ویندوز میتوان استفاده کرد.
- Process.Start را حذف کنید؛ نیازی نیست.
- به قسمت new FileStream آن دقت کنید. اینجا مسیر یک فایل را میشود مشخص کرد. فایل نهایی تولید شده در این مسیر نوشته میشود. از آن مسیر در برنامههای وب و ویندوز میتوان استفاده کرد.
فایلهای پروژهها
PdfRpt-1.6.zip
- متد FlushInBrowser اضافه شد؛ جهت سهولت تولید فایلهای PDF درون حافظهای برنامههای وب
- قالب سلول MonthCalendar اضافه شد؛ برای نمایش تقویم ماهیانه شمسی و میلادی. مثالی در این مورد: (^)
Generate(data => data.FlushInBrowser(fileName))
نظرات مطالب
بارگذاری یک یوزرکنترل با استفاده از جیکوئری
بله. اگر پارامتر ورودی نداشت یک '{}' قرار دهید کفایت میکنه.
برای دات نت 2 کمی کار شما بیشتر میشود چون در VS2008 خیلی از موارد را خودکار اضافه میکند.
در این حالت یا باید ASP.NET AJAX Extensions روی سرور نصب باشد یا dll های آنرا به صورت دستی به دایرکتوری bin برنامه اضافه کنید و یک ارجاع از آنها را نیز به برنامه اضافه کنید.
همچنین وب کانفیگ را نیز باید ویرایش کنید تا این تغییر در آن لحاظ شود. (در غیر اینصورت کار نمیکند)
یک نکته دیگر هم اینکه بجای msg.d شما فقط msg را در asp.net 2 دارید و این d از 3.5 به بعد اضافه شده (در قسمت success و هنگام دریافت پاسخ از سرور توسط jQuery).
برای دات نت 2 کمی کار شما بیشتر میشود چون در VS2008 خیلی از موارد را خودکار اضافه میکند.
در این حالت یا باید ASP.NET AJAX Extensions روی سرور نصب باشد یا dll های آنرا به صورت دستی به دایرکتوری bin برنامه اضافه کنید و یک ارجاع از آنها را نیز به برنامه اضافه کنید.
همچنین وب کانفیگ را نیز باید ویرایش کنید تا این تغییر در آن لحاظ شود. (در غیر اینصورت کار نمیکند)
یک نکته دیگر هم اینکه بجای msg.d شما فقط msg را در asp.net 2 دارید و این d از 3.5 به بعد اضافه شده (در قسمت success و هنگام دریافت پاسخ از سرور توسط jQuery).
عموما در برنامههای SPA، اطلاعات از طریق HTTP و از طرف سرور دریافت میشوند. از نگارشهای ابتدایی Angular، اینکار از طریق HTTP Module آن مسیر بود و هست. در Angular 4.3 روش بهبودیافتهای نسبت به این روش متداول ارائه شدهاست که در ادامه تعدادی از ویژگیهای مقدماتی آنرا بررسی میکنیم.
هرچند ارتقاء به HttpClient الزامی نیست و کدهای پیشین، هنوز هم به خوبی کار میکنند؛ اما طراحی جدید آن شامل ویژگیهای توکاری است که به سختی توسط HTTP Module پیشین قابل پیاده سازی هستند.
به روز رسانی وابستگیهای پروژه
پیش از هرکاری نیاز است وابستگیهای پروژه را به روز رسانی کرد و یکی از روشهای سادهی یافتن شماره نگارشهای جدید بستههای تعریف شدهی در فایل package.json برنامه، استفاده از بستهی npm-check-updates است:
دستور اول، این بسته را به صورت عمومی نصب کرده و صدور دستور دوم در ریشهی پروژه، سبب میشود تا گزارشی از آخرین به روز رسانیها، نمایش داده شود (بدون هیچگونه تغییری در پروژه):
در اینجا شماره نگارشهای جدید مشخص شدهاند و همچنین روش سریع ارتقاء به آنها نیز ذکر شدهاست. فقط کافی است دستورات ذیل را صادر کنیم تا این به روز رسانیها توسط ncu انجام شوند:
دستور اول صرفا شماره نگارشهای جدید را در فایل package.json، به صورت خودکار اصلاح میکند و دستور دوم سبب دریافت، نصب و اعمال آنها خواهد گردید.
تغییرات مورد نیاز جهت معرفی ماژول HttpClient
این ماژول جدید از طریق اینترفیس HttpClientModule ارائه میشود. بنابراین اولین تغییر در جهت ارتقاء به نگارش 4.3، اصلاح importهای لازم است:
از:
به:
پس از آن، این HttpClientModule را به لیست imports ماژول اصلی برنامه اضافه میکنیم؛ تا در کل برنامه قابل دسترسی شود:
تغییرات مورد نیاز در سازندهها و تزریق وابستگیها
پس از تغییرات فوق، دیگر دسترسی به HttpModule پیشین را نداریم. بنابراین نیاز است هر جائی را که سرویس Http به سازندهی کلاسی تزریق شدهاست، یافته و به صورت ذیل تغییر دهیم:
از:
به:
تغییرات مورد نیاز در کدهای سرویسها جهت کار با HTTP Verbs
یکی از اهداف HTTP Client جدید، سادگی کار با اطلاعات دریافتی از سرور است. برای مثال در HTTP Module پیشین، روش دریافت اطلاعات از سرور به صورت ذیل است:
ابتدا درخواستی ارسال شده و سپس نتیجهی آن به JSON تبدیل گشته و در آخر به نوع بازگشتی متد تبدیل میشود.
در HTTP Client جدید دیگر نیازی نیست تا متد ()json. فراخوانی شود. در اینجا به صورت پیشفرض نوع بازگشتی از سرور JSON فرض میشود. همچنین اکنون متدهای get/put/post و امثال آن برخلاف HTTP Client قبلی، جنریک هستند. یعنی در همینجا میتوان نوع بازگشتی را هم مشخص کرد. به این ترتیب، قطعه کد قدیمی فوق، به کد سادهی ذیل تبدیل میشود که در آن خبری از map و همچنین یک cast اضافی نیست:
برای نمونه شبیه به همین نکته برای post نیز صادق است:
نکته 1: در اینجا اگر خروجی از سرور، نوع دیگری را داشت، نیاز است responseType را به صورت صریحی به شکل ذیل مشخص کرد:
در اینحالت خروجی متنی <Observable<string را دریافت میکنیم و نیازی به ذکر <get<string نیست.
نکته 2: ممکن است اطلاعات بازگشتی از سمت سرور، داخل یک فیلد محصور شده باشند:
در این حالت برای دسترسی به اطلاعات این فیلد میتوان از حالت key/value بودن اشیاء جاوا اسکریپتی به شکل زیر برای دسترسی به خاصیت results استفاده کرد:
نکاتی را که باید حین کار با یک RxJS Observable-based API در نظر داشت
این API جدید نیز همانند قبل مبتنی بر RxJS Observables است. بنابراین نکات ذیل در مورد آن نیز صادق است:
- اگر متد subscribe بر روی این observables فراخوانی نشود، اتفاقی رخ نخواهد داد.
- اگر چندین بار مشترک این observables شویم، چندین درخواست HTTP صادر میشوند.
- این نوع خاص از observables، تنها یک مقدار را بازگشت میدهند. اگر درخواست HTTP موفقیت آمیز باشد، این observables یک نتیجه را بازگشت داده و سپس خاتمه پیدا میکنند.
- این observables اگر در حین درخواست HTTP با خطایی مواجه شوند، سبب صدور استثنایی میشوند.
تغییرات مورد نیاز در کدهای سرویسها جهت کار با HTTP Headers
در اینجا برای تعریف headers میتوان به صورت ذیل عمل کرد:
و یا به صورت fluent به شکل زیر:
سپس آنرا به عنوان پارامتر سوم، به متدهای http ارسال میکنیم. یک مثال:
تعریف پارامتر options اینبار به صورت یک شیء دارای چندین خاصیت درآمدهاست. به همین جهت است که در اینجا یک {} را نیز مشاهده میکنید:
یک نکته: شیء HttpHeaders به صورت immutable طراحی شدهاست. یعنی اگر آنرا به صورت ذیل فراخوانی کنیم:
headers تولیدی ... خالی خواهد بود. به همین جهت روش صحیح تشکیل آن به صورت ذیل و زنجیروار است:
امکان تعریف HttpParams
اگر به شیء options در تعریف فوق دقت کنید، دارای خاصیت اختیاری params نیز هست. از آن میتوان جهت تعریف کوئری استرینگها استفاده کرد. برای مثال درخواست ذیل:
سبب تولید یک چنین URL ایی میگردد:
یک نکته: شیء HttpParams به صورت immutable طراحی شدهاست. یعنی اگر آنرا به صورت ذیل فراخوانی کنیم:
params تولیدی ... خالی خواهد بود. به همین جهت روش صحیح تشکیل آن به صورت ذیل و زنجیروار است:
به علاوه روش تعریف ذیل نیز برای کار با HttpParams مجاز است:
تغییرات مورد نیاز در کدهای سرویسها جهت مدیریت خطاها
در اینجا اینبار خطای بازگشتی، از نوع ویژهی HttpErrorResponse است که شامل اطلاعات شماره کد و متن خطای حاصل میباشد:
امکان سعی مجدد در اتصال توسط HTTP Client
ممکن است در اولین سعی در اتصال به سرور، خطایی رخ دهد و یا سرور در دسترس نباشد. در اینجا توسط متد retry میتوان درخواست سعی مجدد در اتصال را صادر کرد.
برای این منظور ابتدا عملگر retry مربوط به RxJS را import میکنیم:
سپس:
این کد در صورت بروز خطایی، این عملیات را سه بار تکرار میکند. در انتها اگر بازهم خطایی دریافت شد، این خطا را به برنامه بازگشت میدهد.
امکان درخواست کل Response بجای Body
اگر به امضای پارامتر اختیاری options دقت کنید، خاصیت observe آن به صورت پیش فرض به body تنظیم شدهاست. به این معنا که تنها body یک response را تبدیل به یک شیء JSON میکند:
اما گاهی از اوقات نیاز است تا به کل Response دسترسی داشته باشیم. در این حالت باید نوع observe را به response تنظیم کرد:
به این ترتیب اینبار resp از نوع <HttpResponse<MyJsonData خواهد بود که توسط آن میتوان به خواص headers و یا body، به صورت جداگانهای دسترسی یافت.
یک نکتهی تکمیلی: کدهای سری کار با فرمها در Angular را اگر به HttpClient ارتقاء دهیم، خلاصهی تغییرات آنها به این صورت خواهند بود.
هرچند ارتقاء به HttpClient الزامی نیست و کدهای پیشین، هنوز هم به خوبی کار میکنند؛ اما طراحی جدید آن شامل ویژگیهای توکاری است که به سختی توسط HTTP Module پیشین قابل پیاده سازی هستند.
به روز رسانی وابستگیهای پروژه
پیش از هرکاری نیاز است وابستگیهای پروژه را به روز رسانی کرد و یکی از روشهای سادهی یافتن شماره نگارشهای جدید بستههای تعریف شدهی در فایل package.json برنامه، استفاده از بستهی npm-check-updates است:
npm install npm-check-updates -g ncu
در اینجا شماره نگارشهای جدید مشخص شدهاند و همچنین روش سریع ارتقاء به آنها نیز ذکر شدهاست. فقط کافی است دستورات ذیل را صادر کنیم تا این به روز رسانیها توسط ncu انجام شوند:
ncu -a npm update
تغییرات مورد نیاز جهت معرفی ماژول 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()); }
در HTTP Client جدید دیگر نیازی نیست تا متد ()json. فراخوانی شود. در اینجا به صورت پیشفرض نوع بازگشتی از سرور JSON فرض میشود. همچنین اکنون متدهای get/put/post و امثال آن برخلاف HTTP Client قبلی، جنریک هستند. یعنی در همینجا میتوان نوع بازگشتی را هم مشخص کرد. به این ترتیب، قطعه کد قدیمی فوق، به کد سادهی ذیل تبدیل میشود که در آن خبری از map و همچنین یک cast اضافی نیست:
get<T>(url: string): Observable<T> { return this.http.get<T>(url); }
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; }); }
نکته 2: ممکن است اطلاعات بازگشتی از سمت سرور، داخل یک فیلد محصور شده باشند:
{ "results": [ "Item 1", "Item 2", ] }
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" });
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');
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();
/api/items/add?id=3
یک نکته: شیء HttpParams به صورت immutable طراحی شدهاست. یعنی اگر آنرا به صورت ذیل فراخوانی کنیم:
const params = new HttpParams(); params.set('orderBy', '"$key"') params.set('limitToFirst', "1");
const params = new HttpParams() .set('orderBy', '"$key"') .set('limitToFirst', "1");
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>
http .get<MyJsonData>('/data.json', {observe: 'response'}) .subscribe(resp => { console.log(resp.headers.get('X-Custom-Header')); console.log(resp.body.someField); });
یک نکتهی تکمیلی: کدهای سری کار با فرمها در Angular را اگر به HttpClient ارتقاء دهیم، خلاصهی تغییرات آنها به این صورت خواهند بود.
دات نت 6 به همراه source generatorهای توکاری است که میتوانند کار serialization و deserialization نوع JSON را با کارآیی بسیار بیشتری انجام دهند؛ با آزمایشهایی که این بهبود را در حد 40 درصد سریعتر نسبت به حالت متداول آن نمایش میدهند و ... این مساله بسیار مهم است. از این جهت که این روزها، JSON را در همهجا مشاهده میکنیم؛ در Web APIها، در تنظیمات برنامهها، در ارسال پیامها بین برنامهها و غیره. بنابراین هرگونه بهبودی در زمینهی کارآیی serialization و deserialization آن، تاثیر بسیار قابل ملاحظهای را بر روی کارآیی کلی یک برنامه بجا خواهد گذاشت.
System.Text.Json source generator چیست؟
پایهی تمام اعمال serialization و deserialization در دات نت، استفاده از Reflection است که در زمینهی ارائهی برنامههایی با کارآیی بالا و با مصرف حافظهی پایین، بهینه عمل نمیکند. راهحل جایگزین استفاده از Reflection که در زمان اجرای برنامه رخ میدهد، به همراه دات نت 5 ارائه شد و source generators نام دارد. Source generators امکان تولید فایلهای #C را در زمان کامپایل برنامه میسر میکنند که نسبت به راهحل Reflection که در زمان اجرای برنامه فعال میشود، کارآیی بسیار بیشتری را ارائه میکنند. برای مثال به همراه دات نت 6، علاوه بر روش پیشفرض مبتنی بر Reflection ارائه شدهی توسط System.Text.Json، راه حل جدید امکان استفادهی از source generators توکار آن نیز پیش بینی شدهاست. کار اصلی آن، انجام تمام مراحلی است که پیشتر توسط Reflection در زمان اجرای برنامه صورت میگرفت، اینبار در زمان کامپایل برنامه و ارائهی آن به صورت از پیش آماده شده و مهیا.
مزایای این روش شامل موارد زیر است:
- بالا رفتن سرعت برنامه
- کاهش زمان آغاز اولیهی برنامه
- کاهش میزان حافظهی مورد نیاز برنامه
- عدم نیاز به استفادهی از System.Reflection و System.Reflection.Emit
- ارائهی Trim-compatible serialization که سبب کاهش اندازهی نهایی برنامه میشود. برای مثال در برنامههای Blazor میتوان با فعالسازی Trimming، کدهای استفاده نشده را از فایلهای بایناری نهایی حذف کرد. استفاده از source generators، با این روش سازگاری کاملی دارد.
مثالی از نحوهی کار با JSON در دات نت 6، توسط source generators آن
فرض کنید قصد داریم اعمال serialization و deserialization از نوع JSON را بر روی نمونههای کلاس زیر انجام دهیم:
اولین کاری که در این زمینه باید انجام شود، ایجاد یک کلاس خالی، با نامی دلخواه، اما مشتق شدهی از JsonSerializerContext است. در این حالت اخطارهایی را در IDE خود مبتنی بر نیاز به پیاده سازی تعدادی از متدهای این کلاس پایه دریافت میکنیم. اما ... ما قصد نداریم این متدها را پیاده سازی کنیم؛ Source generator قرار است اینکار را انجام دهد. به همین جهت این کلاس را partial تعریف کرده (تا source generator بتواند آنرا در فایلی دیگر تکمیل کند) و همچنین آنرا مزین به ویژگی JsonSerializable از نوع کلاسی که میخواهیم آنرا serialize کنیم، خواهیم کرد تا سبب فعال شدن source generator بر روی این کلاس شویم:
و ... همین! کدهای این کلاس partial توسط source generator در زمان کامپایل برنامه به صورت خودکار تولید و تکمیل میشوند.
پس از آن فقط کافی است MyJsonContext را به عنوان پارامتر متدهای جدید Serialize و یا Deserialize، به صورت زیر ارسال کنیم تا از آن استفاده شود:
متدهای جدید این API مبتنی بر source generators را در ادامه ملاحظه میکنید:
روش معرفی تنظیمات Serializer به Source generator
برای معرفی تنظیمات serialization و deserialization، برای مثال تهیهی خروجیهای CamelCase، میتوان از ویژگی JsonSourceGenerationOptions به صورت زیر استفاده کرد:
در این حالت مابقی کدها مانند قبل باقی خواهند ماند:
روش استفاده از JSON Source generators در برنامههای ASP.NET Core
در این نوع برنامهها، JsonSerializerContextها را میتوان توسط متد AddContext به صورت زیر به تنظیمات JSON برنامه معرفی کرد:
روش استفاده از JSON Source generators در برنامههای Blazor
البته در اینجا بیشتر منظور امکان استفادهی از آنها توسط HttpClient است که به صورت زیر توسط متد GetFromJsonAsync واقع در فضای نام System.Net.Http.Json، میسر شدهاست:
لیست کاملتر این API جدید به صورت زیر است:
System.Text.Json source generator چیست؟
پایهی تمام اعمال serialization و deserialization در دات نت، استفاده از Reflection است که در زمینهی ارائهی برنامههایی با کارآیی بالا و با مصرف حافظهی پایین، بهینه عمل نمیکند. راهحل جایگزین استفاده از Reflection که در زمان اجرای برنامه رخ میدهد، به همراه دات نت 5 ارائه شد و source generators نام دارد. Source generators امکان تولید فایلهای #C را در زمان کامپایل برنامه میسر میکنند که نسبت به راهحل Reflection که در زمان اجرای برنامه فعال میشود، کارآیی بسیار بیشتری را ارائه میکنند. برای مثال به همراه دات نت 6، علاوه بر روش پیشفرض مبتنی بر Reflection ارائه شدهی توسط System.Text.Json، راه حل جدید امکان استفادهی از source generators توکار آن نیز پیش بینی شدهاست. کار اصلی آن، انجام تمام مراحلی است که پیشتر توسط Reflection در زمان اجرای برنامه صورت میگرفت، اینبار در زمان کامپایل برنامه و ارائهی آن به صورت از پیش آماده شده و مهیا.
مزایای این روش شامل موارد زیر است:
- بالا رفتن سرعت برنامه
- کاهش زمان آغاز اولیهی برنامه
- کاهش میزان حافظهی مورد نیاز برنامه
- عدم نیاز به استفادهی از System.Reflection و System.Reflection.Emit
- ارائهی Trim-compatible serialization که سبب کاهش اندازهی نهایی برنامه میشود. برای مثال در برنامههای Blazor میتوان با فعالسازی Trimming، کدهای استفاده نشده را از فایلهای بایناری نهایی حذف کرد. استفاده از source generators، با این روش سازگاری کاملی دارد.
مثالی از نحوهی کار با JSON در دات نت 6، توسط source generators آن
فرض کنید قصد داریم اعمال serialization و deserialization از نوع JSON را بر روی نمونههای کلاس زیر انجام دهیم:
namespace Test { internal class Person { public string FirstName { get; set; } public string LastName { get; set; } } }
using System.Text.Json.Serialization; namespace Test { [JsonSerializable(typeof(Person))] internal partial class MyJsonContext : JsonSerializerContext { } }
پس از آن فقط کافی است MyJsonContext را به عنوان پارامتر متدهای جدید Serialize و یا Deserialize، به صورت زیر ارسال کنیم تا از آن استفاده شود:
Person person = new() { FirstName = "Jane", LastName = "Doe" }; byte[] utf8Json = JsonSerializer.SerializeToUtf8Bytes(person, MyJsonContext.Default.Person); person = JsonSerializer.Deserialize(utf8Json, MyJsonContext.Default.Person);
متدهای جدید این API مبتنی بر source generators را در ادامه ملاحظه میکنید:
namespace System.Text.Json { public static class JsonSerializer { public static object? Deserialize(ReadOnlySpan<byte> utf8Json, Type returnType, JsonSerializerContext context) => ...; public static object? Deserialize(ReadOnlySpan<char> json, Type returnType, JsonSerializerContext context) => ...; public static object? Deserialize(string json, Type returnType, JsonSerializerContext context) => ...; public static object? Deserialize(ref Utf8JsonReader reader, Type returnType, JsonSerializerContext context) => ...; public static ValueTask<object?> DeserializeAsync(Stream utf8Json, Type returnType, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static ValueTask<TValue?> DeserializeAsync<TValue>(Stream utf8Json, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static TValue? Deserialize<TValue>(ReadOnlySpan<byte> utf8Json, JsonTypeInfo<TValue> jsonTypeInfo) => ...; public static TValue? Deserialize<TValue>(string json, JsonTypeInfo<TValue> jsonTypeInfo) => ...; public static TValue? Deserialize<TValue>(ReadOnlySpan<char> json, JsonTypeInfo<TValue> jsonTypeInfo) => ...; public static TValue? Deserialize<TValue>(ref Utf8JsonReader reader, JsonTypeInfo<TValue> jsonTypeInfo) => ...; public static string Serialize(object? value, Type inputType, JsonSerializerContext context) => ...; public static void Serialize(Utf8JsonWriter writer, object? value, Type inputType, JsonSerializerContext context) { } public static Task SerializeAsync(Stream utf8Json, object? value, Type inputType, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task SerializeAsync<TValue>(Stream utf8Json, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static byte[] SerializeToUtf8Bytes(object? value, Type inputType, JsonSerializerContext context) => ...; public static byte[] SerializeToUtf8Bytes<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) => ...; public static void Serialize<TValue>(Utf8JsonWriter writer, TValue value, JsonTypeInfo<TValue> jsonTypeInfo) { } public static string Serialize<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) => ...; } }
برای معرفی تنظیمات serialization و deserialization، برای مثال تهیهی خروجیهای CamelCase، میتوان از ویژگی JsonSourceGenerationOptions به صورت زیر استفاده کرد:
using System.Text.Json.Serialization; namespace Test { [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] [JsonSerializable(typeof(Person))] internal partial class MyJsonContext : JsonSerializerContext { } }
string json = JsonSerializer.Serialize(person, MyJsonContext.Default.Person); Person person = JsonSerializer.Deserialize(json, MyJsonContext.Default.Person);
روش استفاده از JSON Source generators در برنامههای ASP.NET Core
در این نوع برنامهها، JsonSerializerContextها را میتوان توسط متد AddContext به صورت زیر به تنظیمات JSON برنامه معرفی کرد:
services.AddControllers().AddJsonOptions(options => options.AddContext<MyJsonContext>());
روش استفاده از JSON Source generators در برنامههای Blazor
البته در اینجا بیشتر منظور امکان استفادهی از آنها توسط HttpClient است که به صورت زیر توسط متد GetFromJsonAsync واقع در فضای نام System.Net.Http.Json، میسر شدهاست:
[JsonSerializable(typeof(WeatherForecast[]))] internal partial class MyJsonContext : JsonSerializerContext { } @code { private WeatherForecast[] forecasts; private static JsonSerializerOptions Options = new(JsonSerializerDefaults.Web); private static MyJsonContext Context = new MyJsonContext(Options); protected override async Task OnInitializedAsync() { forecasts = await Http.GetFromJsonAsync("sample-data/weather.json", Context.WeatherForecastArray); } }
namespace System.Net.Http.Json { public static partial class HttpClientJsonExtensions { public static Task<object?> GetFromJsonAsync(this HttpClient client, string? requestUri, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task<object?> GetFromJsonAsync(this HttpClient client, System.Uri? requestUri, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, string? requestUri, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; } public static partial class HttpContentJsonExtensions { public static Task<object?> ReadFromJsonAsync(this HttpContent content, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task<T?> ReadFromJsonAsync<T>(this HttpContent content, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; } }
نظرات مطالب
ایجاد نقشه سایت (Site Map) داینامیک
چند نکته تکمیلی
- نسخهی MVC این بحث را در اینجا میتوانید مطالعه کنید. این نسخه را میتوان تبدیل به یک فایل ashx وب فرمها نیز کرد. context آن قابل انتقال است و با کد انتهای بحث یکی است.
- در وب فرمها الزاما نیازی نیست تا حتما یک فایل XML ثابت را روی سخت دیسک ذخیره کنید. میشود با استفاده از مباحث routing در وب فرمها، فایلیرا که وجود خارجی ندارد، ایجاد کرد:
در اینجا مسیر sitemap.ashx که یک Generic handler است، به مسیر sitemap.xml نگاشت میشود (کاربر اگر sitemap.xml را در مرورگر وارد کند، مدیریت آن توسط sitemap.ashx انجام خواهد شد).
- سپس برای تولید فایل XML به صورت پویا فقط از sitemap.ashx استفاده کنید به همراه مباحث caching اطلاعات :
- نسخهی MVC این بحث را در اینجا میتوانید مطالعه کنید. این نسخه را میتوان تبدیل به یک فایل ashx وب فرمها نیز کرد. context آن قابل انتقال است و با کد انتهای بحث یکی است.
- در وب فرمها الزاما نیازی نیست تا حتما یک فایل XML ثابت را روی سخت دیسک ذخیره کنید. میشود با استفاده از مباحث routing در وب فرمها، فایلیرا که وجود خارجی ندارد، ایجاد کرد:
void Application_Start(object sender, EventArgs e) { RouteTable.Routes.Add(new Route( "sitemap.xml", new PageRouteHandler("~/sitemap.ashx"))); }
- سپس برای تولید فایل XML به صورت پویا فقط از sitemap.ashx استفاده کنید به همراه مباحث caching اطلاعات :
public class Sitemap : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType = "text/xml"; //مابقی نکات بحث در اینجا context.Response.End(); } public bool IsReusable { get { return false; } } }
فرض کنید قصد داریم خاصیت htmlContent زیر را در قالب این کامپوننت نمایش دهیم:
اگر از روش متداول binding استفاده شود:
چنین خروجی حاصل خواهد شد:
همچنین اگر به کنسول developer tools مرورگر مراجعه کنیم، چنین اخطاری نیز درج شده است:
به این معنا که Angular به صورت توکار تمام خروجیها را به صورت encode شده نمایش میدهد و در مقابل حملات XSS مقاوم است. Sanitizing نیز در اینجا به معنای تغییر ورودی و تبدیل آن به مقداری است که جهت درج در DOM امن است.
روش نمایش HTML در برنامههای Angular
اما اگر خواستیم اطلاعات HTML ایی را به همان صورتی که هستند نمایش دهیم چطور؟ در این حالت باید از روش ویژهی ذیل استفاده کرد:
برای نمایش HTML نیاز است آنرا به ویژگی innerHTML متصل کرد؛ با این خروجی:
همانطور که مشاهده میکنید، هنوز هم عملیات پاکسازی قسمتهایی که ممکن است مخرب باشند صورت میگیرد (برای مثال تگ script حذف شدهاست). اما مابقی تگهای امن به همان حالتی که هستند نمایش داده خواهند شد.
روش دیگر کار با innerHTML، تعریف یک template reference variable در قالب کامپوننت است:
و سپس دسترسی به آن از طریق یک ViewChild و انتساب مقداری بهinnerHTML آن به صورت ذیل:
با این خروجی:
که اینبار قسمت script آن به طور کامل حذف شدهاست.
حالات مختلفی که Angular برنامه را از حملات XSS محافظت میکند
در ذیل، لیست مواردی را مشاهده میکنید که به صورت پیشفرض توسط Angular در مقابل حملات XSS محافظت میشوند و اطلاعات انتساب داده شدهی به آنها تمیزسازی خواهند شد:
تبدیل کردن یک HTML نا امن ورودی به یک HTML امن در Angular
بهتر است اطلاعات دریافتی از کاربران پیش از ارسال به سرور تمیز شوند. برای این منظور میتوان از سرویس ویژهای به نام DomSanitizer کمک گرفت. کار این سرویس، امن سازی اطلاعات نمایش داده شدهی در برنامههای Angular است.
در این حالت سرویس DomSanitizer به سازندهی کلاس تزریق شده و سپس میتوان از متدهای مختلف آن مانند sanitize استفاده کرد. خروجی آن صرفا حذف تگ اسکریپت و نگهداری کدهای درون آن است.
در این حالت میتوان موارد ذیل را کنترل کرد. برای مثال اگر مقدار دریافتی CSS است، میتوان از SecurityContext.STYLE استفاده کرد و سایر حالات آن مانند امن سازی HTML، اسکریپت و آدرسهای اینترنتی به شرح ذیل هستند:
غیرفعال کردن سیستم امنیتی Angular جهت نمایش کامل یک مقدار HTML ایی
اگر خواستیم اطلاعات HTML ایی را با فرض امن بودن آن، به همان نحوی که هست نمایش دهیم چطور؟
سرویس DomSanitizer شامل متدهای ذیل نیز میباشد:
اولین متد آن sanitize است که در مورد آن توضیح داده شد. سایر متدها، کار غیرفعال سازی سیستم امنیتی توکار Angular را انجام میدهند.
برای کار با آنها همانند مثال استفادهی از متد sanitize میتوان سرویس DomSanitizer را به سازندهی یک کامپوننت تزریق کرد و یا میتوان این عملیات تکراری فرمت اطلاعات ورودی را تبدیل به یک Pipe جدید کرد:
کار این Pipe غیرفعال کردن سیستم امنیتی Angular و نمایش html، style و غیره به همان صورتی که هستند، میباشد.
برای استفادهی از آن، ابتدا این Pipe به قسمت declarations ماژول مدنظر اضافه خواهد شد:
و سپس در قالب کامپوننت به نحو ذیل میتوان با آن کار کرد:
در این حالت متد bypassSecurityTrustHtml بر روی htmlContent، فراخوانی شده و نتیجهی نهایی نمایش داده خواهد شد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
export class ShowHtmlComponent { htmlContent = "Template <script>alert(\"Hello!\")</script> <b>Syntax</b>"; }
<h3>Binding innerHTML</h3> <p>Bound value:</p> <p>{{htmlContent}}</p>
همچنین اگر به کنسول developer tools مرورگر مراجعه کنیم، چنین اخطاری نیز درج شده است:
WARNING: sanitizing HTML stripped some content (see http://g.co/ng/security#xss).
روش نمایش HTML در برنامههای Angular
اما اگر خواستیم اطلاعات HTML ایی را به همان صورتی که هستند نمایش دهیم چطور؟ در این حالت باید از روش ویژهی ذیل استفاده کرد:
<p>Result of binding to innerHTML:</p> <p [innerHTML]="htmlContent"></p>
همانطور که مشاهده میکنید، هنوز هم عملیات پاکسازی قسمتهایی که ممکن است مخرب باشند صورت میگیرد (برای مثال تگ script حذف شدهاست). اما مابقی تگهای امن به همان حالتی که هستند نمایش داده خواهند شد.
روش دیگر کار با innerHTML، تعریف یک template reference variable در قالب کامپوننت است:
<p #dataContainer></p>
export class ShowHtmlComponent implements OnInit { @ViewChild("dataContainer") dataContainer: ElementRef; ngOnInit() { this.dataContainer.nativeElement.innerHTML = "nativeElement <script>alert(\"Hello!\")</script> <b>Syntax</b>"; } }
که اینبار قسمت script آن به طور کامل حذف شدهاست.
حالات مختلفی که Angular برنامه را از حملات XSS محافظت میکند
در ذیل، لیست مواردی را مشاهده میکنید که به صورت پیشفرض توسط Angular در مقابل حملات XSS محافظت میشوند و اطلاعات انتساب داده شدهی به آنها تمیزسازی خواهند شد:
HTML Attributes – <div [innerHTML]="UNTRUSTED"></div> OR <input value="UNTRUSTED"> Style— <div [style]="height:UNTRUSTED"></div> URL — <a [href]="UNTRUSTED-URL"></a> OR <script [src]="UNTRUSTED-URL"></script> OR <iframe src="UNTRUSTED-URL" /> GET Parameter – <a href="/user?id=UNTRUSTED">link</a> JavaScript Variable – <script> var value='UNTRUSTED';</script>
تبدیل کردن یک HTML نا امن ورودی به یک HTML امن در Angular
بهتر است اطلاعات دریافتی از کاربران پیش از ارسال به سرور تمیز شوند. برای این منظور میتوان از سرویس ویژهای به نام DomSanitizer کمک گرفت. کار این سرویس، امن سازی اطلاعات نمایش داده شدهی در برنامههای Angular است.
export class ShowHtmlComponent implements OnInit { sanitizedHtml: string; constructor(private sanitizer: DomSanitizer) { } ngOnInit() { this.sanitizedHtml = this.sanitizer.sanitize(SecurityContext.HTML, "<b>Sanitize</b><script>attackerCode()</script>"); } }
در این حالت میتوان موارد ذیل را کنترل کرد. برای مثال اگر مقدار دریافتی CSS است، میتوان از SecurityContext.STYLE استفاده کرد و سایر حالات آن مانند امن سازی HTML، اسکریپت و آدرسهای اینترنتی به شرح ذیل هستند:
SecurityContext.NONE SecurityContext.HTML SecurityContext.STYLE SecurityContext.SCRIPT SecurityContext.URL SecurityContext.RESOURCE_URL
غیرفعال کردن سیستم امنیتی Angular جهت نمایش کامل یک مقدار HTML ایی
اگر خواستیم اطلاعات HTML ایی را با فرض امن بودن آن، به همان نحوی که هست نمایش دهیم چطور؟
سرویس DomSanitizer شامل متدهای ذیل نیز میباشد:
export enum SecurityContext { NONE, HTML, STYLE, SCRIPT, URL, RESOURCE_URL } export abstract class DomSanitizer implements Sanitizer { abstract sanitize(context: SecurityContext, value: SafeValue|string|null): string|null; abstract bypassSecurityTrustHtml(value: string): SafeHtml; abstract bypassSecurityTrustStyle(value: string): SafeStyle; abstract bypassSecurityTrustScript(value: string): SafeScript; abstract bypassSecurityTrustUrl(value: string): SafeUrl; abstract bypassSecurityTrustResourceUrl(value: string): SafeResourceUrl; }
برای کار با آنها همانند مثال استفادهی از متد sanitize میتوان سرویس DomSanitizer را به سازندهی یک کامپوننت تزریق کرد و یا میتوان این عملیات تکراری فرمت اطلاعات ورودی را تبدیل به یک Pipe جدید کرد:
import { Pipe, PipeTransform } from "@angular/core"; import { DomSanitizer, SafeHtml, SafeResourceUrl, SafeScript, SafeStyle, SafeUrl } from "@angular/platform-browser"; @Pipe({ name: "safe" }) export class SafePipe implements PipeTransform { constructor(protected sanitizer: DomSanitizer) { } public transform(value: any, type: string): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl { switch (type) { case "html": return this.sanitizer.bypassSecurityTrustHtml(value); case "style": return this.sanitizer.bypassSecurityTrustStyle(value); case "script": return this.sanitizer.bypassSecurityTrustScript(value); case "url": return this.sanitizer.bypassSecurityTrustUrl(value); case "resourceUrl": return this.sanitizer.bypassSecurityTrustResourceUrl(value); default: throw new Error(`Invalid safe type specified: ${type}`); } } }
برای استفادهی از آن، ابتدا این Pipe به قسمت declarations ماژول مدنظر اضافه خواهد شد:
@NgModule({ imports: [ // ... ], declarations: [ SafePipe] })
<p [innerHTML]="htmlContent | safe: 'html'"></p>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
در قسمت قبل، اطلاعات نمایش داده شده، از یک سری آرایه ثابت جاوا اسکریپتی تامین شدند. در یک برنامهی واقعی نیاز است دادهها را یا از HTML 5 local storage تامین کرد و یا از سرور به کمک Ajax. برای اینگونه اعمال، ember.js به همراه افزونهای است به نام Ember Data که جزئیات کار با آنرا در این قسمت بررسی خواهیم کرد.
استفاده از Ember Data با Local Storage
برای کار با HTML 5 local storage نیاز به Ember Data Local Storage Adapter نیز هست که در قسمت اول این سری، آدرس دریافت آن معرفی شد. این فایلها نیز در پوشهی Scripts\Libs برنامه کپی خواهند شد.
در ادامه به فایل Scripts\App\store.js که در قسمت قبل جهت تعریف دو آرایه ثابت مطالب و نظرات اضافه شد، مراجعه کرده و محتوای فعلی آنرا با کدهای زیر جایگزین کنید:
این تعاریف سبب خواهند شد تا Ember Data از Local Storage Adapter استفاده کند.
در ادامه با توجه به حذف دو آرایهی posts و comments که پیشتر در فایل store.js تعریف شده بودند، نیاز است مدلهای متناظری را جهت تعریف خواص آنها، به برنامه اضافه کنیم. اینکار را با افزودن دو فایل جدید comment.js و post.js به پوشهی Scripts\Models انجام خواهیم داد.
محتوای فایل Scripts\Models\post.js :
محتوای فایل Scripts\Models\comment.js :
سپس مداخل تعریف آنها را به فایل index.html نیز اضافه خواهیم کرد:
برای تعاریف مدلها در Ember data مرسوم است که نام مدلها، اسامی جمع نباشند. سپس با ایجاد وهلهای از DS.Model.extend یک مدل ember data را تعریف خواهیم کرد. در این مدل، خواص هر شیء را مشخص کرده و مقدار آنها همیشه ()DS.attr خواهد بود. این نکته را در دو مدل Post و Comment مشاهده میکنید. اگر دقت کنید به هر دو مدل، خاصیت id اضافه نشدهاست. این خاصیت به صورت خودکار توسط Ember data تنظیم میشود.
اکنون نیاز است برنامه را جهت استفاده از این مدلهای جدید به روز کرد. برای این منظور فایل Scripts\Routes\posts.js را گشوده و مدل آنرا به نحو ذیل ویرایش کنید:
در اینجا this.store معادل data store برنامه است که مطابق تنظیمات برنامه، همان ember data میباشد. سپس متد find را به همراه نام مدل، به صورت رشتهای در اینجا مشخص میکنیم.
به همین ترتیب فایل Scripts\Routes\recent-comments.js را نیز جهت استفاده از data store ویرایش خواهیم کرد:
و فایل Scripts\Routes\post.js که در آن منطق یافتن یک مطلب بر اساس آدرس مختص به آن قرار دارد، به صورت ذیل بازنویسی میشود:
اگر متد find بدون پارامتر ذکر شود، به معنای بازگشت تمامی عناصر موجود در آن مدل خواهد بود و اگر پارامتر دوم آن مانند این مثال تنظیم شود، تنها همان وهلهی درخواستی را بازگشت میدهد.
افزودن امکان ثبت یک مطلب جدید
تا اینجا اگر برنامه را اجرا کنید، برنامه بدون خطا بارگذاری خواهد شد اما فعلا رکوردی را برای نمایش ندارد. در ادامه، برنامه را جهت افزودن مطالب جدید توسعه خواهیم داد. برای اینکار ابتدا به فایل Scripts\App\router.js مراجعه کرده و سپس مسیریابی جدید new-post را تعریف خواهیم کرد:
اکنون در صفحهی اول سایت، توسط قالب Scripts\Templates\posts.hbs، دکمهای را جهت ایجاد یک مطلب جدید اضافه خواهیم کرد:
در اینجا دکمهی New Post به مسیریابی جدید new-post اشاره میکند.
برای تعریف عناصر نمایشی این مسیریابی، فایل جدید قالب Scripts\Templates\new-post.hbs را با محتوای زیر اضافه کنید:
با نمونهی این فرم در قسمت قبل در حین ویرایش یک مطلب، آشنا شدیم. دو المان دریافت اطلاعات در آن قرار دارند که هر کدام به خواص مدل برنامه bind شدهاند. همچنین یک دکمهی save، با اکشنی به همین نام در اینجا تعریف شدهاست.
پس از آن نیاز است نام فایل قالب new-post را به template loader برنامه در فایل index.html اضافه کرد:
برای مدیریت دکمهی save این قالب جدید نیاز است کنترلر جدیدی را در فایل جدید Scripts\Controllers\new-post.js تعریف کنیم؛ با این محتوا:
به همراه افزودن مدخلی از آن به فایل index.html برنامه:
در اینجا کنترلر جدید NewPostController را مشاهده میکنید. از این جهت که برای دسترسی به خواص مدل تغییر کرده، از متد this.get استفاده شدهاست، نیازی نیست حتما از یک ObjectController مانند قسمت قبل استفاده کرد و Controller معمولی نیز برای اینکار کافی است.
آرگومان اول this.store.createRecord نام مدل است و آرگومان دوم آن، وهلهای که قرار است به آن اضافه شود. همچنین باید دقت داشت که برای تنظیم یک خاصیت، از متد this.set و برای دریافت مقدار یک خاصیت تغییر کرده از this.get به همراه نام خاصیت مورد نظر استفاده میشود و نباید مستقیما برای مثال از this.title استفاده کرد.
this.store.createRecord صرفا یک شیء جدید (ember data object) را ایجاد میکند. برای ذخیره سازی نهایی آن باید متد save آنرا فراخوانی کرد (پیاده سازی الگوی active record است). به این ترتیب این شیء در local storage ذخیره خواهد شد.
پس از ذخیرهی مطلب جدید، از متد this.transitionToRoute استفاده شدهاست. این متد، برنامه را به صورت خودکار به صفحهی متناظر با مسیریابی posts هدایت میکند.
اکنون برنامه را اجرا کنید. بر روی دکمهی سبز رنگ new post در صفحهی اول کلیک کرده و یک مطلب جدید را تعریف کنید. بلافاصله عنوان و لینک متناظر با این مطلب را در صفحهی اول سایت مشاهده خواهید کرد.
همچنین اگر برنامه را مجددا بارگذاری کنید، این مطالب هنوز قابل مشاهده هستند؛ زیرا در local storage مرورگر ذخیره شدهاند.
در اینجا اگر به لینکهای تولید شده دقت کنید، id آنها عددی نیست. این روشی است که local storage با آن کار میکند.
افزودن امکان حذف یک مطلب به سایت
برای حذف یک مطلب، دکمهی حذف را به انتهای قالب Scripts\Templates\post.hbs اضافه خواهیم کرد:
سپس کنترلر Scripts\Controllers\post.js را جهت مدیریت اکشن جدید delete به نحو ذیل تکمیل میکنیم:
متد destroyRecord، مدل انتخابی را هم از حافظه و هم از data store حذف میکند. سپس کاربر را به صفحهی اصلی سایت هدایت خواهیم کرد.
متد save نیز در اینجا بهبود یافتهاست. ابتدا مدل جاری دریافت شده و سپس متد save بر روی آن فراخوانی میشود. به این ترتیب اطلاعات از حافظه به local storage نیز منتقل خواهند شد.
ثبت و نمایش نظرات به همراه تنظیمات روابط اشیاء در Ember Data
در ادامه قصد داریم امکان افزودن نظرات را به مطالب، به همراه نمایش آنها در ذیل هر مطلب، پیاده سازی کنیم. برای اینکار نیاز است رابطهی بین یک مطلب و نظرات مرتبط با آنرا در مدل ember data مشخص کنیم. به همین جهت فایل Scripts\Models\post.js را گشوده و تغییرات ذیل را به آن اعمال کنید:
در اینجا خاصیت جدیدی به نام comments به مدل مطلب اضافه شدهاست و توسط آن میتوان به تمامی نظرات یک مطلب دسترسی یافت؛ تعریف رابطهی یک به چند، به کمک متد DS.hasMany که پارامتر اول آن نام مدل مرتبط است. تعریف async: true برای کار با local storage اجباری است و در نگارشهای آتی ember data حالت پیش فرض خواهد بود.
همچنین نیاز است یک سر دیگر رابطه را نیز مشخص کرد. برای این منظور فایل Scripts\Models\comment.js را گشوده و به نحو ذیل تکمیل کنید:
در اینجا خاصیت جدید post به مدل نظر اضافه شدهاست و مقدار آن از طریق متد DS.belongsTo که مدل post را به یک نظر، مرتبط میکند، تامین خواهد شد. بنابراین در این حالت اگر به شیء comment مراجعه کنیم، خاصیت جدید post.id آن، به id مطلب متناظر اشاره میکند.
در ادامه نیاز است بتوان تعدادی نظر را ثبت کرد. به همین جهت با تعریف مسیریابی آن شروع میکنیم. این مسیریابی تعریف شده در فایل Scripts\App\router.js نیز باید تو در تو باشد؛ زیرا قسمت ثبت نظر (new-comment) دقیقا داخل همان صفحهی نمایش یک مطلب ظاهر میشود:
لینک آنرا نیز به انتهای فایل Scripts\Templates\post.hbs اضافه میکنیم. از این جهت که این لینک به مدل جاری اشاره میکند، با استفاده از متغیر this، مدل جاری را به عنوان مدل مورد استفاده مشخص خواهیم کرد:
پس از تکمیل روابط مدلها، قالب Scripts\Templates\post.hbs را جهت استفاده از این خواص به روز خواهیم کرد. در تغییرات جدید، قسمت <h2>Comments</h2> به انتهای صفحه اضافه شدهاست. سپس حلقهای بر روی خاصیت جدید comments تشکیل شده و مقدار خاصیت text هر آیتم نمایش داده میشود.
در انتهای قالب نیز یک {{outlet}} اضافه شدهاست. کار آن نمایش قالب ارسال یک نظر جدید، پس از کلیک بر روی لینک New Comment میباشد. این قالب را با افزودن فایل Scripts\Templates\new-comment.hbs با محتوای ذیل ایجاد خواهیم کرد:
سپس نام این قالب را به template loader فایل index.html نیز اضافه میکنیم؛ تا در ابتدای بارگذاری برنامه شناسایی شده و استفاده شود:
این قالب به خاصیت text یک comment متصل بوده و همچنین اکشن جدیدی به نام save دارد. بنابراین برای مدیریت اکشن save، نیاز به کنترلری متناظر خواهد بود. به همین جهت فایل جدید Scripts\Controllers\new-comment.js را با محتوای ذیل ایجاد کنید:
و مدخل تعریف آنرا نیز به صفحهی index.html اضافه میکنیم:
قسمت ذخیره سازی comment جدید با ذخیره سازی یک post جدید که پیشتر بررسی کردیم، تفاوتی ندارد. از متد this.store.createRecord جهت معرفی وهلهای جدید از comment استفاده و سپس متد save آن، برای ثبت نهایی فراخوانی شدهاست.
در ادامه باید این نظر جدید را به post متناظر با آن مرتبط کنیم. برای اینکار نیاز است تا به مدل کنترلر post دسترسی داشته باشیم. به همین جهت خاصیت needs را به تعاریف کنترلر جاری به همراه نام کنترلر مورد نیاز، اضافه کردهایم. به این ترتیب میتوان توسط متد this.get و پارامتر controllers.post.model در کنترلر NewComment به اطلاعات کنترلر post دسترسی یافت. سپس خاصیت comments شیء post جاری را یافته و مقدار آنرا به comment جدیدی که ثبت کردیم، تنظیم میکنیم. در ادامه با فراخوانی متد save، کار تنظیم ارتباطات یک مطلب و نظرهای جدید آن به پایان میرسد.
در آخر با فراخوانی متد transitionToRoute به مطلبی که نظر جدیدی برای آن ارسال شدهاست باز میگردیم.
همانطور که در این تصویر نیز مشاهده میکنید، اطلاعات ذخیره شده در local storage را توسط افزونهی Ember Inspector نیز میتوان مشاهده کرد.
افزودن دکمهی حذف به لیست نظرات ارسالی
برای افزودن دکمهی حذف، به قالب Scripts\Templates\post.hbs مراجعه کرده و قسمتی را که لیست نظرات را نمایش میدهد، به نحو ذیل تکمیل میکنیم:
همچنین برای مدیریت اکشن جدید delete، کنترلر جدید comment را در فایل Scripts\Controllers\comment.js اضافه خواهیم کرد.
به همراه تعریف مدخل آن در فایل index.html :
در این حالت اگر برنامه را اجرا کنید، پیام «Do you want to delete this post» را مشاهده خواهید کرد بجای پیام «Do you want to delete this comment». علت اینجا است که قالب post به صورت پیش فرض به کنترلر post متصل است و نه کنترلر comment. برای رفع این مشکل تنها کافی است از itemController به نحو ذیل استفاده کنیم:
به این ترتیب اکشن delete به کنترلر comment ارسال خواهد شد و نه کنترلر پیش فرض post جاری.
در کنترلر Comment روش دیگری را برای حذف یک رکورد مشاهده میکنید. میتوان ابتدا متد deleteRecord را بر روی مدل فراخوانی کرد و سپس آنرا save نمود تا نهایی شود. همچنین در اینجا نیاز است نظر حذف شده را از سر دیگر رابطه نیز حذف کرد. روش دسترسی به post جاری در این حالت، همانند توضیحات NewCommentController است که پیشتر بحث شد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
EmberJS03_04.zip
استفاده از Ember Data با Local Storage
برای کار با HTML 5 local storage نیاز به Ember Data Local Storage Adapter نیز هست که در قسمت اول این سری، آدرس دریافت آن معرفی شد. این فایلها نیز در پوشهی Scripts\Libs برنامه کپی خواهند شد.
در ادامه به فایل Scripts\App\store.js که در قسمت قبل جهت تعریف دو آرایه ثابت مطالب و نظرات اضافه شد، مراجعه کرده و محتوای فعلی آنرا با کدهای زیر جایگزین کنید:
Blogger.ApplicationSerializer = DS.LSSerializer.extend(); Blogger.ApplicationAdapter = DS.LSAdapter.extend();
در ادامه با توجه به حذف دو آرایهی posts و comments که پیشتر در فایل store.js تعریف شده بودند، نیاز است مدلهای متناظری را جهت تعریف خواص آنها، به برنامه اضافه کنیم. اینکار را با افزودن دو فایل جدید comment.js و post.js به پوشهی Scripts\Models انجام خواهیم داد.
محتوای فایل Scripts\Models\post.js :
Blogger.Post = DS.Model.extend({ title: DS.attr(), body: DS.attr() });
Blogger.Comment = DS.Model.extend({ text: DS.attr() });
<script src="Scripts/Models/post.js" type="text/javascript"></script> <script src="Scripts/Models/comment.js" type="text/javascript"></script>
برای تعاریف مدلها در Ember data مرسوم است که نام مدلها، اسامی جمع نباشند. سپس با ایجاد وهلهای از DS.Model.extend یک مدل ember data را تعریف خواهیم کرد. در این مدل، خواص هر شیء را مشخص کرده و مقدار آنها همیشه ()DS.attr خواهد بود. این نکته را در دو مدل Post و Comment مشاهده میکنید. اگر دقت کنید به هر دو مدل، خاصیت id اضافه نشدهاست. این خاصیت به صورت خودکار توسط Ember data تنظیم میشود.
اکنون نیاز است برنامه را جهت استفاده از این مدلهای جدید به روز کرد. برای این منظور فایل Scripts\Routes\posts.js را گشوده و مدل آنرا به نحو ذیل ویرایش کنید:
Blogger.PostsRoute = Ember.Route.extend({ //controllerName: 'posts', // مقدار پیش فرض است و نیازی به ذکر آن نیست //renderTemplare: function () { // this.render('posts'); // مقدار پیش فرض است و نیازی به ذکر آن نیست //}, model: function () { return this.store.find('post'); } });
به همین ترتیب فایل Scripts\Routes\recent-comments.js را نیز جهت استفاده از data store ویرایش خواهیم کرد:
Blogger.RecentCommentsRoute = Ember.Route.extend({ model: function () { return this.store.find('comment'); } });
Blogger.PostRoute = Ember.Route.extend({ model: function (params) { return this.store.find('post', params.post_id); } });
افزودن امکان ثبت یک مطلب جدید
تا اینجا اگر برنامه را اجرا کنید، برنامه بدون خطا بارگذاری خواهد شد اما فعلا رکوردی را برای نمایش ندارد. در ادامه، برنامه را جهت افزودن مطالب جدید توسعه خواهیم داد. برای اینکار ابتدا به فایل Scripts\App\router.js مراجعه کرده و سپس مسیریابی جدید new-post را تعریف خواهیم کرد:
Blogger.Router.map(function () { this.resource('posts', { path: '/' }); this.resource('about'); this.resource('contact', function () { this.resource('email'); this.resource('phone'); }); this.resource('recent-comments'); this.resource('post', { path: 'posts/:post_id' }); this.resource('new-post'); });
<h2>Ember.js blog</h2> <ul> {{#each post in arrangedContent}} <li>{{#link-to 'post' post.id}}{{post.title}}{{/link-to}}</li> {{/each}} </ul> <a href="#" class="btn btn-primary" {{action 'sortByTitle' }}>Sort by title</a> {{#link-to 'new-post' classNames="btn btn-success"}}New Post{{/link-to}}
برای تعریف عناصر نمایشی این مسیریابی، فایل جدید قالب Scripts\Templates\new-post.hbs را با محتوای زیر اضافه کنید:
<h1>New post</h1> <form> <div class="form-group"> <label for="title">Title</label> {{input value=title id="title" class="form-control"}} </div> <div class="form-group"> <label for="body">Body</label> {{textarea value=body id="body" class="form-control" rows="5"}} </div> <button class="btn btn-primary" {{action 'save'}}>Save</button> </form>
پس از آن نیاز است نام فایل قالب new-post را به template loader برنامه در فایل index.html اضافه کرد:
<script type="text/javascript"> EmberHandlebarsLoader.loadTemplates([ 'posts', 'about', 'application', 'contact', 'email', 'phone', 'recent-comments', 'post', 'new-post' ]); </script>
Blogger.NewPostController = Ember.Controller.extend({ actions: { save: function () { var newPost = this.store.createRecord('post', { title: this.get('title'), body: this.get('body') }); newPost.save(); this.transitionToRoute('posts'); } } });
<script src="Scripts/Controllers/new-post.js" type="text/javascript"></script>
در اینجا کنترلر جدید NewPostController را مشاهده میکنید. از این جهت که برای دسترسی به خواص مدل تغییر کرده، از متد this.get استفاده شدهاست، نیازی نیست حتما از یک ObjectController مانند قسمت قبل استفاده کرد و Controller معمولی نیز برای اینکار کافی است.
آرگومان اول this.store.createRecord نام مدل است و آرگومان دوم آن، وهلهای که قرار است به آن اضافه شود. همچنین باید دقت داشت که برای تنظیم یک خاصیت، از متد this.set و برای دریافت مقدار یک خاصیت تغییر کرده از this.get به همراه نام خاصیت مورد نظر استفاده میشود و نباید مستقیما برای مثال از this.title استفاده کرد.
this.store.createRecord صرفا یک شیء جدید (ember data object) را ایجاد میکند. برای ذخیره سازی نهایی آن باید متد save آنرا فراخوانی کرد (پیاده سازی الگوی active record است). به این ترتیب این شیء در local storage ذخیره خواهد شد.
پس از ذخیرهی مطلب جدید، از متد this.transitionToRoute استفاده شدهاست. این متد، برنامه را به صورت خودکار به صفحهی متناظر با مسیریابی posts هدایت میکند.
اکنون برنامه را اجرا کنید. بر روی دکمهی سبز رنگ new post در صفحهی اول کلیک کرده و یک مطلب جدید را تعریف کنید. بلافاصله عنوان و لینک متناظر با این مطلب را در صفحهی اول سایت مشاهده خواهید کرد.
همچنین اگر برنامه را مجددا بارگذاری کنید، این مطالب هنوز قابل مشاهده هستند؛ زیرا در local storage مرورگر ذخیره شدهاند.
در اینجا اگر به لینکهای تولید شده دقت کنید، id آنها عددی نیست. این روشی است که local storage با آن کار میکند.
افزودن امکان حذف یک مطلب به سایت
برای حذف یک مطلب، دکمهی حذف را به انتهای قالب Scripts\Templates\post.hbs اضافه خواهیم کرد:
<h2>{{title}}</h2> {{#if isEditing}} <form> <div class="form-group"> <label for="title">Title</label> {{input value=title id="title" class="form-control"}} </div> <div class="form-group"> <label for="body">Body</label> {{textarea value=body id="body" class="form-control" rows="5"}} </div> <button class="btn btn-primary" {{action 'save' }}>Save</button> </form> {{else}} <p>{{body}}</p> <button class="btn btn-primary" {{action 'edit' }}>Edit</button> <button class="btn btn-danger" {{action 'delete' }}>Delete</button> {{/if}}
سپس کنترلر Scripts\Controllers\post.js را جهت مدیریت اکشن جدید delete به نحو ذیل تکمیل میکنیم:
Blogger.PostController = Ember.ObjectController.extend({ isEditing: false, actions: { edit: function () { this.set('isEditing', true); }, save: function () { var post = this.get('model'); post.save(); this.set('isEditing', false); }, delete: function () { if (confirm('Do you want to delete this post?')) { this.get('model').destroyRecord(); this.transitionToRoute('posts'); } } } });
متد save نیز در اینجا بهبود یافتهاست. ابتدا مدل جاری دریافت شده و سپس متد save بر روی آن فراخوانی میشود. به این ترتیب اطلاعات از حافظه به local storage نیز منتقل خواهند شد.
ثبت و نمایش نظرات به همراه تنظیمات روابط اشیاء در Ember Data
در ادامه قصد داریم امکان افزودن نظرات را به مطالب، به همراه نمایش آنها در ذیل هر مطلب، پیاده سازی کنیم. برای اینکار نیاز است رابطهی بین یک مطلب و نظرات مرتبط با آنرا در مدل ember data مشخص کنیم. به همین جهت فایل Scripts\Models\post.js را گشوده و تغییرات ذیل را به آن اعمال کنید:
Blogger.Post = DS.Model.extend({ title: DS.attr(), body: DS.attr(), comments: DS.hasMany('comment', { async: true }) });
همچنین نیاز است یک سر دیگر رابطه را نیز مشخص کرد. برای این منظور فایل Scripts\Models\comment.js را گشوده و به نحو ذیل تکمیل کنید:
Blogger.Comment = DS.Model.extend({ text: DS.attr(), post: DS.belongsTo('post', { async: true }) });
در ادامه نیاز است بتوان تعدادی نظر را ثبت کرد. به همین جهت با تعریف مسیریابی آن شروع میکنیم. این مسیریابی تعریف شده در فایل Scripts\App\router.js نیز باید تو در تو باشد؛ زیرا قسمت ثبت نظر (new-comment) دقیقا داخل همان صفحهی نمایش یک مطلب ظاهر میشود:
Blogger.Router.map(function () { this.resource('posts', { path: '/' }); this.resource('about'); this.resource('contact', function () { this.resource('email'); this.resource('phone'); }); this.resource('recent-comments'); this.resource('post', { path: 'posts/:post_id' }, function () { this.resource('new-comment'); }); this.resource('new-post'); });
<h2>{{title}}</h2> {{#if isEditing}} <form> <div class="form-group"> <label for="title">Title</label> {{input value=title id="title" class="form-control"}} </div> <div class="form-group"> <label for="body">Body</label> {{textarea value=body id="body" class="form-control" rows="5"}} </div> <button class="btn btn-primary" {{action 'save' }}>Save</button> </form> {{else}} <p>{{body}}</p> <button class="btn btn-primary" {{action 'edit' }}>Edit</button> <button class="btn btn-danger" {{action 'delete' }}>Delete</button> {{/if}} <h2>Comments</h2> {{#each comment in comments}} <p> {{comment.text}} </p> {{/each}} <p>{{#link-to 'new-comment' this class="btn btn-success"}}New comment{{/link-to}}</p> {{outlet}}
در انتهای قالب نیز یک {{outlet}} اضافه شدهاست. کار آن نمایش قالب ارسال یک نظر جدید، پس از کلیک بر روی لینک New Comment میباشد. این قالب را با افزودن فایل Scripts\Templates\new-comment.hbs با محتوای ذیل ایجاد خواهیم کرد:
<h2>New comment</h2> <form> <div class="form-group"> <label for="text">Your thoughts:</label> {{textarea value=text id="text" class="form-control" rows="5"}} </div> <button class="btn btn-primary" {{action "save"}}>Add your comment</button> </form>
<script type="text/javascript"> EmberHandlebarsLoader.loadTemplates([ 'posts', 'about', 'application', 'contact', 'email', 'phone', 'recent-comments', 'post', 'new-post', 'new-comment' ]); </script>
Blogger.NewCommentController = Ember.ObjectController.extend({ needs: ['post'], actions: { save: function () { var comment = this.store.createRecord('comment', { text: this.get('text') }); comment.save(); var post = this.get('controllers.post.model'); post.get('comments').pushObject(comment); post.save(); this.transitionToRoute('post', post.id); } } });
<script src="Scripts/Controllers/new-comment.js" type="text/javascript"></script>
قسمت ذخیره سازی comment جدید با ذخیره سازی یک post جدید که پیشتر بررسی کردیم، تفاوتی ندارد. از متد this.store.createRecord جهت معرفی وهلهای جدید از comment استفاده و سپس متد save آن، برای ثبت نهایی فراخوانی شدهاست.
در ادامه باید این نظر جدید را به post متناظر با آن مرتبط کنیم. برای اینکار نیاز است تا به مدل کنترلر post دسترسی داشته باشیم. به همین جهت خاصیت needs را به تعاریف کنترلر جاری به همراه نام کنترلر مورد نیاز، اضافه کردهایم. به این ترتیب میتوان توسط متد this.get و پارامتر controllers.post.model در کنترلر NewComment به اطلاعات کنترلر post دسترسی یافت. سپس خاصیت comments شیء post جاری را یافته و مقدار آنرا به comment جدیدی که ثبت کردیم، تنظیم میکنیم. در ادامه با فراخوانی متد save، کار تنظیم ارتباطات یک مطلب و نظرهای جدید آن به پایان میرسد.
در آخر با فراخوانی متد transitionToRoute به مطلبی که نظر جدیدی برای آن ارسال شدهاست باز میگردیم.
همانطور که در این تصویر نیز مشاهده میکنید، اطلاعات ذخیره شده در local storage را توسط افزونهی Ember Inspector نیز میتوان مشاهده کرد.
افزودن دکمهی حذف به لیست نظرات ارسالی
برای افزودن دکمهی حذف، به قالب Scripts\Templates\post.hbs مراجعه کرده و قسمتی را که لیست نظرات را نمایش میدهد، به نحو ذیل تکمیل میکنیم:
{{#each comment in comments}} <p> {{comment.text}} <button class="btn btn-xs btn-danger" {{action 'delete' }}>delete</button> </p> {{/each}}
Blogger.CommentController = Ember.ObjectController.extend({ needs: ['post'], actions: { delete: function () { if (confirm('Do you want to delete this comment?')) { var comment = this.get('model'); comment.deleteRecord(); comment.save(); var post = this.get('controllers.post.model'); post.get('comments').removeObject(comment); post.save(); } } } });
<script src="Scripts/Controllers/comment.js" type="text/javascript"></script>
در این حالت اگر برنامه را اجرا کنید، پیام «Do you want to delete this post» را مشاهده خواهید کرد بجای پیام «Do you want to delete this comment». علت اینجا است که قالب post به صورت پیش فرض به کنترلر post متصل است و نه کنترلر comment. برای رفع این مشکل تنها کافی است از itemController به نحو ذیل استفاده کنیم:
{{#each comment in comments itemController="comment"}} <p> {{comment.text}} <button class="btn btn-xs btn-danger" {{action 'delete' }}>delete</button> </p> {{/each}}
در کنترلر Comment روش دیگری را برای حذف یک رکورد مشاهده میکنید. میتوان ابتدا متد deleteRecord را بر روی مدل فراخوانی کرد و سپس آنرا save نمود تا نهایی شود. همچنین در اینجا نیاز است نظر حذف شده را از سر دیگر رابطه نیز حذف کرد. روش دسترسی به post جاری در این حالت، همانند توضیحات NewCommentController است که پیشتر بحث شد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
EmberJS03_04.zip
نظرات مطالب
خلاصه اشتراکهای روز جمعه 6 آبان 1390
من WPF رو با سیلورلایت یادگرفتم. اکثر مطالب آن مشترک است. به همین جهت با یک کتاب قدم به قدم سیلورلایت شروع کنید سادهتر است (چون حجم مطالب سیلورلایت کمتر است). همین کتابی که تهیه کردم برای یادگیری هر دو مناسب است: (^)