اشتراکها
نگاهی به Xamarin.Forms
فرض کنید قصد دارید همزمان با تایپ کاربر، نتایج جستجو را به او نمایش دهید. این جستجو نیز عموما به همراه ارسال یک درخواست HTTP به سمت سرور و نمایش اطلاعات بازگشتی به کاربر است. جهت کاهش تعداد رفت و برگشتهای به سرور، کاهش بار سرور و همچنین کاهش تعداد بار به روز رسانی رابط کاربری، کتابخانهی RxJS به همراه متدهایی است که امکان کاهش نرخ ورودی کاربر را میسر میکنند.
کنترلر جستجوی سمت سرور و سرویس سمت کلاینت استفاده کنندهی از آن
در اینجا کنترلر و اکشن متدی را جهت جستجوی قسمتی از نام کشورها، مشاهده میکنید:
از این کنترلر به نحو ذیل در برنامهی Angular برای ارسال اطلاعات و انجام جستجو استفاده میشود:
در اینجا از اپراتور pipe مخصوص RxJS 5.5 استفاده شدهاست.
جستجوی ورودی کاربر به ازای هربار ورود اطلاعات توسط او
صرفنظر از نوع فرمی که استفاده میکنید (مبتنی بر قالبها و یا واکنشی)، جهت انتقال هربار فشرده شدن کلیدی به کدهای کامپوننت، میتوان از رخداد input استفاده کرد:
و سپس متد مدیریت کنندهی آن در کامپوننت نیز به صورت زیر تعریف میشود:
در این حالت روش ابتدایی واکنش نشان دادن به هر ورودی، تزریق SearchService فوق به سازندهی این کامپوننت
و سپس مشترک متد جستجوی سمت سرور آن، شدن است.
این روش ابتدایی سه مشکل را به همراه دارد:
الف) به ازای هر بار فشرده شدن کلیدی در Input box، یک درخواست به سمت سرور ارسال میشود. برای مثال اگر هدف اصلی کاربر، جستجوی کشورهای شروع شدهی با alg باشد، سه درخواست به سمت سرور ارسال میشوند و سه بار هم رابط کاربری به روز میشود.
ب) اگر در این بین، کاربر حرفی را کم و زیاد کند، درخواستهای قبلی لغو نمیشوند.
ج) درخواستها به صورت موازی به سرور ارسال میشوند. ممکن است نتیجهی یکی زودتر و دیگری دیرتر دریافت شود. در این حالت آخرین نتیجهی رسیده، نتایج قبلی را بازنویسی میکند که ممکن است الزاما نتیجهای نباشد که کاربر درخواست کردهاست.
کنترل نرخ ورود اطلاعات توسط متد debounceTime
با اعمال اپراتور debounceTime به رخداد تغییرات ورودی، میتوان نرخ ورودی کاربر و واکنش نشان دادن به آنرا کاهش داد. برای مثال اگر این عدد به 300 میلی ثانیه تنظیم شده باشد، صرفا به اولین ورودی رسیدهی پس از 300 میلی ثانیه واکنش نشان داده میشود و از مابقی صرفنظر خواهد شد. به این ترتیب دیگر به ازای هربار فشرده شدن کلیدی توسط کاربر جستجو صورت نمیگیرد. همچنین با ترکیب آن با اپراتور distinctUntilChanged میتوان تنها به تغییرات غیرتکراری واکنش نشان داد:
بنابراین بجای اینکه متد this.searchService.searchCountries دقیقا داخل onSearch1Change فراخوانی شود، باید بتوان تغییرات صورت گرفتهی نهایی را پس از اعمال debounceTime و distinctUntilChanged به آن ارسال کرد و سپس نتیجه را به کاربر نمایش داد.
برای این منظور یک Subject تعریف شدهاست تا کار مدیریت تغییرات رسیده (کلیدهای فشرده شدهی توسط کاربر) را انجام دهد. در اینحالت فرصت خواهیم داشت تا انواع و اقسام اپراتورهای RxJS را با هم ترکیب و صرفا نتیجهی نهایی (آخرین ورودی یکتای با تاخیر او) را به searchService ارسال کنیم.
متد onSearch1Change نیز تنها کافی است با فراخوانی متد next این Subject، جریان تغییرات رسیده را به آن انتقال دهد.
در اینجا برای انتقال آخرین ورودی یکتای با تاخیر به متد this.searchService.searchCountries از اپراتور flatMap استفاده شدهاست. این اپراتور، آخرین ورودی فیلتر شده را دریافت کرده و به متد searchCountries ارسال میکند. همچنین خروجی آن نیز یک Observable است. به همین جهت در ادامه میتوان توسط متد subscribe، مشترک آن شد و آرایهی countries دریافتی از سرور را به کاربر نمایش داد.
بهبود کارآیی جستجو با لغو درخواستهای پیشین
تا اینجا توانستیم نرخ ورود اطلاعات کاربر را به صورت کنترل شدهای به متد this.searchService.searchCountries ارسال کنیم و نه اینکه به ازای هر بار ورود اطلاعات توسط آن، یکبار این متد فراخوانی شود. اما همانطور که در تصویر فوق مشاهده میکنید، در اینجا هدف نهایی کاربر، جستجوی نام کشورهای شروع شدهی با alg بوده است و در این بین چندین بار سعی و خطا انجام دادهاست تا به alg رسیدهاست. مشکل اینجا است که هیچکدام از درخواستهای قبلی او که مدنظر نبودهاند، لغو نشدهاند و تمام آنها صورت گرفته و همچنین سبب به روز رسانیهای مکرر رابط کاربری شدهاند.
برای رفع یک چنین مشکلی و لغو خودکار درخواستهای قبلی، اپراتور دیگری به نام switchMap وجود دارد که دقیقا یک چنین کاری را انجام میدهد. در اینجا برخلاف اپراتور flatMap، تمام درخواستهای تمام نشدهی قبلی، لغو شده و صرفا آخرین مورد پردازش میشود.
برای اعمال آن نیز در کدهای فوق تنها کافی است flatMap را با switchMap جایگزین کنید. پس از آن نتیجه را در تصویر فوق ملاحظه میکنید. اینبار اگر هدف نهایی کاربر جستجوی alg باشد، تمام ورودیهای قبلی او به صورت خودکار لغو میشوند و دیگر پردازش نخواهند شد که در نهایت سبب بالا رفتن کارآیی برنامه با کاهش تعداد بار به روز رسانی رابط کاربری خواهد شد.
همچنین در حالت استفادهی از flatMap، ممکن است کاربر نتیجهی اشتباهی را نیز دریافت کند. از این جهت که درخواستهای ارسالی به سمت سرور، به صورت موازی اجرا میشوند. در این حالت ممکن است یکی زودتر و دیگری دیرتر به پایان برسد و کاربر نتیجهای را که مشاهده میکند، دقیقا آن چیزی نباشد که جستجو کردهاست (رابط کاربری آخرین درخواست پایان یافته را نمایش میدهد که نتیجهی آن الزاما به ترتیب ورود اطلاعات کاربر نیست).
برای نمونه فرض کنید دو درخواست A1 و B1 به همراه پاسخهای A2 و B2 را داریم. درخواست A1 پیش از B1 ارسال شدهاست؛ اما پاسخ B1 زودتر از پاسخ A2 از سرور دریافت شدهاست. در این حالت کاربر عبارت ABCX را وارد کردهاست اما پاسخ عبارت ABC پیشین را در رابط کاربری مشاهده میکند (آخرین پاسخ رسیده در رابط کاربری (یا همان A2)، پاسخهای قبلی (یا همان B2) را بازنویسی میکند).
در حالت استفادهی از flatMap، مشترک هر رخداد رسیده خواهیم شد؛ بدون قطع اشتراک خودکار از سایر observableهای ایجاد شدهی پیشین. اما در حالت استفادهی از switchMap، ابتدا کار لغو اشتراک خودکار از تمام observableهای قبلی صورت میگیرد و سپس یک observable جدید را ایجاد میکند. به همین جهت است که استفادهی از switchMap به همراه درخواستهای http، سبب لغو خودکار درخواستهای پیشین میشود. در این حالت نه تنها تعداد بار به روز رسانی رابط کاربری کاهش پیدا میکند، بلکه تضمین خواهد شد دیگر کاربر نتیجهی اشتباهی را نیز مشاهده نکند.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
کنترلر جستجوی سمت سرور و سرویس سمت کلاینت استفاده کنندهی از آن
در اینجا کنترلر و اکشن متدی را جهت جستجوی قسمتی از نام کشورها، مشاهده میکنید:
[Route("api/[controller]")] public class TypeaheadController : Controller { [HttpGet("[action]")] public async Task<IActionResult> SearchCountries(string term) { await Task.Delay(1000); // simulating a slow operation var items = new[] { "Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", "Antarctica", "Antigua and/or Barbuda" }; var results = string.IsNullOrWhiteSpace(term) ? items : items.Where(item => item.StartsWith(term, StringComparison.OrdinalIgnoreCase)); return Json(results.ToArray()); } }
import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable } from "rxjs/Observable"; import { ErrorObservable } from "rxjs/observable/ErrorObservable"; import { catchError, map } from "rxjs/operators"; @Injectable() export class SearchService { constructor(private http: HttpClient) { } searchCountries(term: string): Observable<string[]> { return this.http .get(`/api/Typeahead/SearchCountries?term=${encodeURIComponent(term)}`) .pipe( map(response => response || {}), catchError((error: HttpErrorResponse) => ErrorObservable.create(error)) ); } }
جستجوی ورودی کاربر به ازای هربار ورود اطلاعات توسط او
صرفنظر از نوع فرمی که استفاده میکنید (مبتنی بر قالبها و یا واکنشی)، جهت انتقال هربار فشرده شدن کلیدی به کدهای کامپوننت، میتوان از رخداد input استفاده کرد:
<label>Country: </label> <input type="text" (input)="onSearch1Change($event.target.value)" /> <ul class="list-group"> <li class="list-group-item" *ngFor="let country of countries1"> {{country}} </li> </ul>
onSearch1Change(value: string) { }
constructor(private searchService: SearchService) { }
این روش ابتدایی سه مشکل را به همراه دارد:
الف) به ازای هر بار فشرده شدن کلیدی در Input box، یک درخواست به سمت سرور ارسال میشود. برای مثال اگر هدف اصلی کاربر، جستجوی کشورهای شروع شدهی با alg باشد، سه درخواست به سمت سرور ارسال میشوند و سه بار هم رابط کاربری به روز میشود.
ب) اگر در این بین، کاربر حرفی را کم و زیاد کند، درخواستهای قبلی لغو نمیشوند.
ج) درخواستها به صورت موازی به سرور ارسال میشوند. ممکن است نتیجهی یکی زودتر و دیگری دیرتر دریافت شود. در این حالت آخرین نتیجهی رسیده، نتایج قبلی را بازنویسی میکند که ممکن است الزاما نتیجهای نباشد که کاربر درخواست کردهاست.
کنترل نرخ ورود اطلاعات توسط متد debounceTime
با اعمال اپراتور debounceTime به رخداد تغییرات ورودی، میتوان نرخ ورودی کاربر و واکنش نشان دادن به آنرا کاهش داد. برای مثال اگر این عدد به 300 میلی ثانیه تنظیم شده باشد، صرفا به اولین ورودی رسیدهی پس از 300 میلی ثانیه واکنش نشان داده میشود و از مابقی صرفنظر خواهد شد. به این ترتیب دیگر به ازای هربار فشرده شدن کلیدی توسط کاربر جستجو صورت نمیگیرد. همچنین با ترکیب آن با اپراتور distinctUntilChanged میتوان تنها به تغییرات غیرتکراری واکنش نشان داد:
export class AutocompleteSampleComponent implements OnInit { countries1: string[] = []; private model1Changed: Subject<string> = new Subject<string>(); private dueTime = 300; constructor(private searchService: SearchService) { } ngOnInit() { this.model1Changed .pipe( debounceTime(this.dueTime), distinctUntilChanged(), flatMap(inputValue => { console.log("debounced input value1", inputValue); return this.searchService.searchCountries(inputValue); }) ) .subscribe(countries => { this.countries1 = countries; }); } onSearch1Change(value: string) { this.model1Changed.next(value); } }
برای این منظور یک Subject تعریف شدهاست تا کار مدیریت تغییرات رسیده (کلیدهای فشرده شدهی توسط کاربر) را انجام دهد. در اینحالت فرصت خواهیم داشت تا انواع و اقسام اپراتورهای RxJS را با هم ترکیب و صرفا نتیجهی نهایی (آخرین ورودی یکتای با تاخیر او) را به searchService ارسال کنیم.
متد onSearch1Change نیز تنها کافی است با فراخوانی متد next این Subject، جریان تغییرات رسیده را به آن انتقال دهد.
در اینجا برای انتقال آخرین ورودی یکتای با تاخیر به متد this.searchService.searchCountries از اپراتور flatMap استفاده شدهاست. این اپراتور، آخرین ورودی فیلتر شده را دریافت کرده و به متد searchCountries ارسال میکند. همچنین خروجی آن نیز یک Observable است. به همین جهت در ادامه میتوان توسط متد subscribe، مشترک آن شد و آرایهی countries دریافتی از سرور را به کاربر نمایش داد.
بهبود کارآیی جستجو با لغو درخواستهای پیشین
تا اینجا توانستیم نرخ ورود اطلاعات کاربر را به صورت کنترل شدهای به متد this.searchService.searchCountries ارسال کنیم و نه اینکه به ازای هر بار ورود اطلاعات توسط آن، یکبار این متد فراخوانی شود. اما همانطور که در تصویر فوق مشاهده میکنید، در اینجا هدف نهایی کاربر، جستجوی نام کشورهای شروع شدهی با alg بوده است و در این بین چندین بار سعی و خطا انجام دادهاست تا به alg رسیدهاست. مشکل اینجا است که هیچکدام از درخواستهای قبلی او که مدنظر نبودهاند، لغو نشدهاند و تمام آنها صورت گرفته و همچنین سبب به روز رسانیهای مکرر رابط کاربری شدهاند.
برای رفع یک چنین مشکلی و لغو خودکار درخواستهای قبلی، اپراتور دیگری به نام switchMap وجود دارد که دقیقا یک چنین کاری را انجام میدهد. در اینجا برخلاف اپراتور flatMap، تمام درخواستهای تمام نشدهی قبلی، لغو شده و صرفا آخرین مورد پردازش میشود.
برای اعمال آن نیز در کدهای فوق تنها کافی است flatMap را با switchMap جایگزین کنید. پس از آن نتیجه را در تصویر فوق ملاحظه میکنید. اینبار اگر هدف نهایی کاربر جستجوی alg باشد، تمام ورودیهای قبلی او به صورت خودکار لغو میشوند و دیگر پردازش نخواهند شد که در نهایت سبب بالا رفتن کارآیی برنامه با کاهش تعداد بار به روز رسانی رابط کاربری خواهد شد.
همچنین در حالت استفادهی از flatMap، ممکن است کاربر نتیجهی اشتباهی را نیز دریافت کند. از این جهت که درخواستهای ارسالی به سمت سرور، به صورت موازی اجرا میشوند. در این حالت ممکن است یکی زودتر و دیگری دیرتر به پایان برسد و کاربر نتیجهای را که مشاهده میکند، دقیقا آن چیزی نباشد که جستجو کردهاست (رابط کاربری آخرین درخواست پایان یافته را نمایش میدهد که نتیجهی آن الزاما به ترتیب ورود اطلاعات کاربر نیست).
// A1: Request for `ABC` // A2: Response for `ABC` // B1: Request for `ABCX` // B2: Response for `ABCX` --A1----------A2--> ------B1--B2------>
در حالت استفادهی از flatMap، مشترک هر رخداد رسیده خواهیم شد؛ بدون قطع اشتراک خودکار از سایر observableهای ایجاد شدهی پیشین. اما در حالت استفادهی از switchMap، ابتدا کار لغو اشتراک خودکار از تمام observableهای قبلی صورت میگیرد و سپس یک observable جدید را ایجاد میکند. به همین جهت است که استفادهی از switchMap به همراه درخواستهای http، سبب لغو خودکار درخواستهای پیشین میشود. در این حالت نه تنها تعداد بار به روز رسانی رابط کاربری کاهش پیدا میکند، بلکه تضمین خواهد شد دیگر کاربر نتیجهی اشتباهی را نیز مشاهده نکند.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
Architecture with .NET Core 3.1, ASP.NET Core 3.1, Entity Framework Core 3.1, C#, Angular 9.1, Clean Code, SOLID, DDD, Code Analysis, Docker and more.
Technologies
- .NET Core 3.1
- ASP.NET Core 3.1
- Entity Framework Core 3.1
- C# 8.0
- Angular 9.1
- Typescript
- JWT
- FluentValidation
- Scrutor
- Serilog
- Docker
- Azure DevOps
- ...
Practices
- Clean Code
- SOLID Principles
- DDD (Domain-Driven Design)
- Unit of Work Pattern
- Repository Pattern
- ...
پروژههای Angular CLI در حالت پیش فرض آنها به همراه دو نوع آزمون واحد و آزمون end to end ایجاد میشوند. Angular CLI از Karma برای اجرای آزمونهای واحد استفاده میکند و از Protractor برای اجرای آزمونهای end to end. برای شروع میتوان از راهنمای آن کمک گرفت:
زمانیکه دستور ng test را اجرا میکنیم، به صورت خودکار تمام فایلهای spec.ts.* را یافته و آزمونهای واحد موجود در آنها را اجرا میکند. این نوع فایلهای ویژه نیز به صورت خودکار، زمانیکه اجزای مختلف Angular را توسط Angular CLI ایجاد میکنیم، تولید میشوند. به علاوه دستور ng test تغییرات این فایلها را تحت نظر قرار داده و در صورت نیاز، آزمونهای واحد را مجددا و به صورت خودکار اجرا میکند.
یک مثال: بررسی اجرای دستور ng test
یکی از مثالهای بررسی شدهی در این سری را انتخاب و یا حتی یک برنامهی جدید را توسط Angular CLI ایجاد کرده و سپس دستور ng test را در ریشهی این پروژه اجرا کنید. به این ترتیب برنامه به صورت خودکار کامپایل شده و سپس به صورت خودکار آزمونهای واحد آنرا که در فایلهای spec.ts.* قرار دارند، اجرا میکند. در آخر نتیجه را در مرورگر گزارش میدهد:
همانطور که مشخص است، 3 specs, 3 failures داریم. در اینجا میتوان بر روی لینک Spec List کلیک کرد و لیست آزمونهای واحد موجود را مشاهده نمود:
هر کدام از عناوین ذکر شده نیز به جزئیات مشکلات آنها، لینک شدهاند. برای مثال اگر بر روی اولین مورد کلیک کنیم، خطایی مانند «'alert' is not a known element» قابل مشاهدهاست. به این معنا که برای نمونه در قسمت قبل کامپوننت alert را به صفحه اضافه کردیم:
اما اجرا کنندهی آزمونهای واحد اطلاعاتی در مورد آن ندارد؛ از این جهت که آزمونهای واحد به صورت ایزوله فقط همان کامپوننت خاص برنامه را آزمایش میکنند و کاری به وابستگیهای آن ندارد. به همین جهت فایل src\app\app.component.spec.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
در اینجا ابتدا ماژول NO_ERRORS_SCHEMA معرفی شده و سپس به قسمت schemas معرفی گشتهاست.
پس از این تغییر، بلافاصله مجدد برنامه کامپایل شده و آزمونهای واحد آن با موفقیت اجرا میشوند (با این فرض که هنوز پنجرهی اجرا کنندهی دستور ng test را باز نگه داشتهاید):
تغییر افزودن schemas: [NO_ERRORS_SCHEMA] را باید در مورد تمام فایلهای spec موجود تکرار کرد.
گزینههای مختلف دستور ng test
دستور ng test به همراه گزینههای متعددی است که شرح آنها را در جدول ذیل مشاهده میکنید:
بنابراین اجرا دستور ng test بدون ذکر هیچ گزینهای به معنای اجرای مداوم آزمونهای واحد، در صورت مشاهدهی تغییراتی در آنها، به کمک Karma است.
همچنین دو دستور ذیل نیز به یک معنا هستند و هر دو سبب یکبار اجرای آزمونهای واحد میشوند:
اجرای بررسی میزان پوشش آزمونهای واحد
یکی از گزینههای ng test روشن کردن پرچم code-coverage است:
برای آزمایش آن دستور ذیل را در ریشهی پروژه اجرا کنید (که سبب اجرای یکبار برررسی میزان پوشش آزمونهای واحد میشود):
پس از اجرای این آزمون ویژه، پوشهی جدیدی به نام coverage در ریشهی پروژهی جاری تشکیل میشود. فایل index.html آنرا در مرورگر باز کنید تا بتوان گزارش تولید شده را مشاهده کرد:
کار این آزمون بررسی قسمتهای مختلف برنامه و ارائه گزارشی است که مشخص میکند آیا آزمونهای واحد نوشته شده تمام انشعابات برنامه را پوشش میدهند یا خیر؟ برای مثال اگر در متدی if/else دارید، آیا فقط قسمت if را پوشش دادهاید و یا آیا قسمت else هم در آزمونهای واحد، بررسی شدهاست.
اجرای آزمونهای end to end
هدف از ساخت یک برنامه ... استفادهی از آن توسط دیگران است؛ اینجا است که آزمونهای end to end مفهوم پیدا میکنند. در آزمونهای e2e رفتار برنامه همانند حالتی که یک کاربر از آن استفاده میکند، بررسی میشود (برای مثال باز کردن مرورگر، لاگین و مرور صفحات). برای این منظور، Angular CLI در پشت صحنه از Protractor برای این نوع آزمونها استفاده میکند.
برای مشاهدهی راهنما و گزینههای مختلف مرتبط با آزمونهای e2e، میتوان دستور ذیل را صادر کرد:
البته با توجه به اینکه این دستور کار توزیع برنامه را نیز انجام میدهد، تمام گزینههای ng serve نیز در اینجا صادق هستند، به علاوهی موارد ذیل:
بنابراین زمانیکه دستور ng e2e صادر میشود، به معنای کامپایل، توزیع برنامه بر روی پورتی اتفاقی و اجرای آزمونها است.
از این جهت که این نوع آزمونها، وابستهی به جزئی خاص از برنامه نیستند، حالت عمومی داشته و فایلهای spec آنها را میتوان در پوشهی e2e واقع در ریشهی پروژه، یافت. برای مثال در قسمتی از آن کار یافتن متن نمایش داده شدهی در صفحهی اول سایت انجام میشود
و سپس در فایل spec آن بررسی میکند که آیا مساوی app works هست یا خیر؟
برای آزمایش آن دستور ng e2e را در ریشهی پروژه صادر کنید. همچنین دقت داشته باشید که در این حالت نیاز است به اینترنت نیز متصل باشد؛ چون از chromedriver api گوگل نیز استفاده میکند. در غیراینصورت خطای ذیل را دریافت خواهید کرد:
> ng test --help
یک مثال: بررسی اجرای دستور ng test
یکی از مثالهای بررسی شدهی در این سری را انتخاب و یا حتی یک برنامهی جدید را توسط Angular CLI ایجاد کرده و سپس دستور ng test را در ریشهی این پروژه اجرا کنید. به این ترتیب برنامه به صورت خودکار کامپایل شده و سپس به صورت خودکار آزمونهای واحد آنرا که در فایلهای spec.ts.* قرار دارند، اجرا میکند. در آخر نتیجه را در مرورگر گزارش میدهد:
همانطور که مشخص است، 3 specs, 3 failures داریم. در اینجا میتوان بر روی لینک Spec List کلیک کرد و لیست آزمونهای واحد موجود را مشاهده نمود:
هر کدام از عناوین ذکر شده نیز به جزئیات مشکلات آنها، لینک شدهاند. برای مثال اگر بر روی اولین مورد کلیک کنیم، خطایی مانند «'alert' is not a known element» قابل مشاهدهاست. به این معنا که برای نمونه در قسمت قبل کامپوننت alert را به صفحه اضافه کردیم:
<alert type="success">Alert success!</alert>
import { NO_ERRORS_SCHEMA } from '@angular/core'; describe('AppComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); }));
پس از این تغییر، بلافاصله مجدد برنامه کامپایل شده و آزمونهای واحد آن با موفقیت اجرا میشوند (با این فرض که هنوز پنجرهی اجرا کنندهی دستور ng test را باز نگه داشتهاید):
تغییر افزودن schemas: [NO_ERRORS_SCHEMA] را باید در مورد تمام فایلهای spec موجود تکرار کرد.
گزینههای مختلف دستور ng test
دستور ng test به همراه گزینههای متعددی است که شرح آنها را در جدول ذیل مشاهده میکنید:
گزینه | مخفف | توضیح |
code-coverage-- | cc- | تولید گزارش code coverage که به صورت پیش فرض خاموش است. |
colors-- | به صورت پیش فرض فعال است و سبب نمایش رنگهای قرمز و سبز، برای آزمونهای شکست خورده و یا موفق میشود. | |
single-run-- | sr- | اجرای یکبارهی آزمونهای واحد، بدون فعال سازی گزینهی مشاهدهی مداوم تغییرات که به صورت پیش فرض خاموش است. |
progress-- | نمایش جزئیات کامپایل و اجرای آزمونهای واحد که به صورت پیش فرض فعال است. | |
sourcemaps-- | sm- | تولید فایلهای سورسمپ که به صورت پیش فرض فعال است. |
watch-- | w- | بررسی مداوم تغییرات فایلها و اجرای آزمونهای واحد به صورت خودکار که به صورت پیش فرض فعال است. |
بنابراین اجرا دستور ng test بدون ذکر هیچ گزینهای به معنای اجرای مداوم آزمونهای واحد، در صورت مشاهدهی تغییراتی در آنها، به کمک Karma است.
همچنین دو دستور ذیل نیز به یک معنا هستند و هر دو سبب یکبار اجرای آزمونهای واحد میشوند:
> ng test -sr > ng test -w false
اجرای بررسی میزان پوشش آزمونهای واحد
یکی از گزینههای ng test روشن کردن پرچم code-coverage است:
> ng test --code-coverage
> ng test -sr -cc
کار این آزمون بررسی قسمتهای مختلف برنامه و ارائه گزارشی است که مشخص میکند آیا آزمونهای واحد نوشته شده تمام انشعابات برنامه را پوشش میدهند یا خیر؟ برای مثال اگر در متدی if/else دارید، آیا فقط قسمت if را پوشش دادهاید و یا آیا قسمت else هم در آزمونهای واحد، بررسی شدهاست.
اجرای آزمونهای end to end
هدف از ساخت یک برنامه ... استفادهی از آن توسط دیگران است؛ اینجا است که آزمونهای end to end مفهوم پیدا میکنند. در آزمونهای e2e رفتار برنامه همانند حالتی که یک کاربر از آن استفاده میکند، بررسی میشود (برای مثال باز کردن مرورگر، لاگین و مرور صفحات). برای این منظور، Angular CLI در پشت صحنه از Protractor برای این نوع آزمونها استفاده میکند.
برای مشاهدهی راهنما و گزینههای مختلف مرتبط با آزمونهای e2e، میتوان دستور ذیل را صادر کرد:
>ng e2e --help
گزینه | مخفف | توضیح |
config-- | c- | به فایل کانفیگ آزمونهای e2e اشاره میکند که به صورت پیشفرض همان protractor.conf.js واقع در ریشهی پروژهاست. |
element-explorer-- | ee- | بررسی و دیباگ protractor از طریق خط فرمان |
serve-- | s- | کامپایل و توزیع برنامه بر روی پورتی اتفاقی (حالت پیش فرض آن true است) |
specs-- | sp- | پیش فرض آن بررسی تمام specهای موجود در پروژهاست. اگر نیاز به لغو آن باشد میتوان از این گزینه استفاده کرد. |
webdriver-update-- | wu- |
به روز رسانی web driver که به صورت پیش فرض فعال است. |
بنابراین زمانیکه دستور ng e2e صادر میشود، به معنای کامپایل، توزیع برنامه بر روی پورتی اتفاقی و اجرای آزمونها است.
از این جهت که این نوع آزمونها، وابستهی به جزئی خاص از برنامه نیستند، حالت عمومی داشته و فایلهای spec آنها را میتوان در پوشهی e2e واقع در ریشهی پروژه، یافت. برای مثال در قسمتی از آن کار یافتن متن نمایش داده شدهی در صفحهی اول سایت انجام میشود
getParagraphText() { return element(by.css('app-root h1')).getText(); }
expect(page.getParagraphText()).toEqual('app works!');
برای آزمایش آن دستور ng e2e را در ریشهی پروژه صادر کنید. همچنین دقت داشته باشید که در این حالت نیاز است به اینترنت نیز متصل باشد؛ چون از chromedriver api گوگل نیز استفاده میکند. در غیراینصورت خطای ذیل را دریافت خواهید کرد:
Error: getaddrinfo ENOTFOUND chromedriver.storage.googleapis.com chromedriver.storage.googleapis.com:443
Second Level Cache In NHibernate 4
همان طور که میدانیم کش در NHibernate در دو سطح قابل انجام میباشد:
- کش سطح اول که همان اطلاعات سشن، در تراکنش جاری هست و با اتمام تراکنش، محتویات آن خالی میگردد. این سطح همیشه فعال میباشد و در این بخش قصد پرداختن به آن را نداریم.
- کش سطح دوم که بین همهی تراکنشها مشترک و پایدار میباشد. این مورد به طور پیش فرض فعال نمیباشد و میبایستی از طریق کانفیگ برنامه فعال گردد.
جهت پیاده سازی باید قسمتهای ذیل را در کانفیگ مربوط به NHibernate اضافه نمود:
پیاده سازی Caching در NHibernate در سه مرحله قابل اعمال میباشد :
- کش در سطح Load موجودیتهای مستقل
- کش در سطح Load موجودیتهای وابسته Bag , List , Set , …
- کش در سطح Query ها
Providerهای مختلفی برای اعمال و پیاده سازی آن وجود دارند که معروفترین آنها SysCache بوده و ما هم از همان استفاده مینماییم.
- مدت زمان پیش فرض کش سطح دوم، ۵ دقیقه میباشد و در صورت نیاز به تغییر آن، باید تگ مربوط به SysCache را تنظیم نمود. محدودیتی در تعریف تعداد متفاوتی از زمانهای خالی شدن کش وجود ندارد و مدت زمان آن بر حسب ثانیه مشخص میگردد. نحوهی تخصیص زمان انقضای کش به هر مورد بدین شکل صورت میگیرد که region مربوطه در آن معرفی میگردد.
جهت اعمال کش در سطح Load موجودیتهای مستقل، علاوه بر کانفیگ اصلی، میبایستی کدهای زیر را به Mapping موجودیت اضافه نمود مانند :
این مورد برای موجودیتهای وابسته هم نیز صادق است؛ به شکل کد زیر:
ویژگی usage نیز با مقادیر زیر قابل تنظیم است:
- read-only : این مورد جهت موجودیتهایی مناسب است که امکان بروزرسانی آنها توسط کاربر وجود ندارد. این مورد بهترین کارآیی را دارد.
- read-write : این مورد جهت موجودیتهایی بکار میرود که امکان بروزرسانی آنها توسط کاربر وجود دارد. این مورد کارآیی پایینتری دارد.
- nonstrict-read-write : این مورد جهت موجودیتهایی مناسب میباشد که امکان بروزرسانی آنها توسط کاربر وجود دارد؛ اما امکان همزمان بروز کردن آنها توسط چندین کاربر وجود نداشته باشد. این مورد در قیاس، کارآیی بهتر و بهینهتری نسبت به مورد قبل دارد.
جهت اعمال کش در کوئریها نیز باید مراحل خاص خودش را انجام داد. به عنوان مثال برای یک کوئری Linq به شکل زیر خواهیم داشت:
در واقع کد اضافه شده به کوئری بالا، قابل کش بودن کوئری را مشخص مینماید و مدت زمان کش شدن آن نیز از طریق کانفیگ مربوطه مشخص میگردد. این نکته را هم درنظر داشته باشید که کش در سطح کوئری برای کوئریهایی که دقیقا مثل هم هستند اعمال میگردد و با افزوده یا کاسته شدن یک شرط جدید به کوئری، مجددا کوئری سمت پایگاه داده ارسال میگردد.
در انتها لینکهای زیر هم جهت مطالعه بیشتر پیشنهاد میگردند:
http://www.nhforge.org/doc/nh/en/index.html#performance-cache-readonly
http://nhforge.org/blogs/nhibernate/archive/2009/02/09/quickly-setting-up-and-using-nhibernate-s-second-level-cache.aspx
http://www.klopfenstein.net/lorenz.aspx/using-syscache-as-secondary-cache-in-nhibernate
http://stackoverflow.com/questions/1837651/hibernate-cache-strategy
همان طور که میدانیم کش در NHibernate در دو سطح قابل انجام میباشد:
- کش سطح اول که همان اطلاعات سشن، در تراکنش جاری هست و با اتمام تراکنش، محتویات آن خالی میگردد. این سطح همیشه فعال میباشد و در این بخش قصد پرداختن به آن را نداریم.
- کش سطح دوم که بین همهی تراکنشها مشترک و پایدار میباشد. این مورد به طور پیش فرض فعال نمیباشد و میبایستی از طریق کانفیگ برنامه فعال گردد.
جهت پیاده سازی باید قسمتهای ذیل را در کانفیگ مربوط به NHibernate اضافه نمود:
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate"/> <section name="syscache" type="NHibernate.Caches.SysCache.SysCacheSectionHandler, NHibernate.Caches.SysCache" requirePermission="false"/> </configSections> <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2" > <session-factory> <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property> <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property> <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property> <property name="connection.connection_string_name">LocalSqlServer</property> <property name="show_sql">false</property> <property name="hbm2ddl.keywords">none</property> <property name="cache.use_second_level_cache">true</property> <property name="cache.use_query_cache" >true</property> <property name="cache.provider_class">NHibernate.Caches.SysCache.SysCacheProvider, NHibernate.Caches.SysCache</property> </session-factory> </hibernate-configuration> <syscache> <cache region="LongExpire" expiration="3600" priority="5"/> <cache region="ShortExpire" expiration="600" priority="3"/> </syscache> </configuration>
پیاده سازی Caching در NHibernate در سه مرحله قابل اعمال میباشد :
- کش در سطح Load موجودیتهای مستقل
- کش در سطح Load موجودیتهای وابسته Bag , List , Set , …
- کش در سطح Query ها
Providerهای مختلفی برای اعمال و پیاده سازی آن وجود دارند که معروفترین آنها SysCache بوده و ما هم از همان استفاده مینماییم.
- مدت زمان پیش فرض کش سطح دوم، ۵ دقیقه میباشد و در صورت نیاز به تغییر آن، باید تگ مربوط به SysCache را تنظیم نمود. محدودیتی در تعریف تعداد متفاوتی از زمانهای خالی شدن کش وجود ندارد و مدت زمان آن بر حسب ثانیه مشخص میگردد. نحوهی تخصیص زمان انقضای کش به هر مورد بدین شکل صورت میگیرد که region مربوطه در آن معرفی میگردد.
جهت اعمال کش در سطح Load موجودیتهای مستقل، علاوه بر کانفیگ اصلی، میبایستی کدهای زیر را به Mapping موجودیت اضافه نمود مانند :
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Core.Domain" namespace="Core.Domain.Model"> <class name="Organization" table="Core_Enterprise_Organization"> <cache usage="nonstrict-read-write" region="ShortExpire"/> <id name="Id" > <generator/> </id> <version name="Version"/> <property name="Title" not-null="true" unique="true"/> <property name="Code" not-null="true" unique="true"/> </class> </hibernate-mapping>
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Core.Domain" namespace="Core.Domain.Model"> <class name="Party" table="Core_Enterprise_Party"> <id name="Id" > <generator /> </id> <version name="Version"/> <property name="Username" unique="true"/> <property name="DisplayName" not-null="true"/> <bag name="PartyGroups" inverse="true" table="Core_Enterprise_PartyGroup" cascade="all-delete-orphan"> <cache usage="nonstrict-read-write" region="ShortExpire"/> <key column="Party_id_fk"/> <one-to-many/> </bag> </class> </hibernate-mapping>
ویژگی usage نیز با مقادیر زیر قابل تنظیم است:
- read-only : این مورد جهت موجودیتهایی مناسب است که امکان بروزرسانی آنها توسط کاربر وجود ندارد. این مورد بهترین کارآیی را دارد.
- read-write : این مورد جهت موجودیتهایی بکار میرود که امکان بروزرسانی آنها توسط کاربر وجود دارد. این مورد کارآیی پایینتری دارد.
- nonstrict-read-write : این مورد جهت موجودیتهایی مناسب میباشد که امکان بروزرسانی آنها توسط کاربر وجود دارد؛ اما امکان همزمان بروز کردن آنها توسط چندین کاربر وجود نداشته باشد. این مورد در قیاس، کارآیی بهتر و بهینهتری نسبت به مورد قبل دارد.
جهت اعمال کش در کوئریها نیز باید مراحل خاص خودش را انجام داد. به عنوان مثال برای یک کوئری Linq به شکل زیر خواهیم داشت:
public IList<Organization> Search(QueryOrganizationDto dto) { var q = SessionInstance.Query<Organization>(); if (!String.IsNullOrEmpty(dto.Title)) q = q.Where(x => x.Title.Contains(dto.Title)); if (!String.IsNullOrEmpty(dto.Code)) q = q.Where(x => x.Code.Contains(dto.Code)); q = q.OrderBy(x => x.Title); q = q.CacheRegion("ShortExpire").Cacheable(); return q.ToList(); }
در انتها لینکهای زیر هم جهت مطالعه بیشتر پیشنهاد میگردند:
http://www.nhforge.org/doc/nh/en/index.html#performance-cache-readonly
http://nhforge.org/blogs/nhibernate/archive/2009/02/09/quickly-setting-up-and-using-nhibernate-s-second-level-cache.aspx
http://www.klopfenstein.net/lorenz.aspx/using-syscache-as-secondary-cache-in-nhibernate
http://stackoverflow.com/questions/1837651/hibernate-cache-strategy
وقتی پروژه انگیولاریتان کمی گسترش پیدا کند، تعداد زیادی فایل شامل کنترلرها، سرویسها، دایرکتیوها و ... خواهید داشت. واضح است که همه این اجزا همراه با هم مورد نیاز نیستند و برای افزایش سرعت بارگذاری سایت و صرفه جویی در مصرف پهنای باند بهتر است هرکدام از آنها را در هنگام نیاز بارگذاری کنیم. این یعنی همان lazy loading خودمان!
در AngularJS امکانی برای lazy loading فایلها پیشبینی نشده است، پس باید از ابزارهای دیگری که این امکان را فراهم میکنند استفاده کرد. من در ادامه از Script.js برای این کار استفاده خواهم کرد، ولی شما میتوانید از هر کتابخانه دیگری استفاده کنید.
اما مسئله دیگری که پیش از lazy loading فایلها باید تکلیفش را معلوم کنیم، این است که چطور میتوانیم اجزایی را به ماژولی که قبلا راهاندازی (bootstrap) شده اضافه کنیم. اگر بخواهیم برای مثال کنترلری را در یک فایل مجزا تعریف کنیم، باید آن را به شکلی در ماژول برنامهمان ثبت کنیم. فرض کنید این کار را به این ترتیب انجام دهیم:
angular.module('app').controller('SomeLazyController', function($scope) { $scope.key = '...'; });
Error: Argument ‘SomeLazyController’ is not a function, got undefined
// Registering a controller after app bootstrap $controllerProvider.register('SomeLazyController', function($scope) { $scope.key = '...'; }); // Registering a directive after app bootstrap $compileProvider.directive('SomeLazyDirective', function() { return { restrict: 'A', templateUrl: 'templates/some-lazy-directive.html' } }) // etc
اما نکتهای که درباره providerها وجود دارد این است که آنها تنها در روال config یک ماژول در دسترس هستند. بنا بر این برای دسترسی به آنها پس از اجرای این روال، ارجاعی به آنها را باید نگهداری کنیم:
(function () { app = angular.module("app", []); app.config([ '$controllerProvider', '$compileProvider', '$filterProvider', '$provide', function ($controllerProvider, $compileProvider, $filterProvider, $provide) { //برای رجیستر کردن غیر همروند اجزای انگیولاری در آینده app.lazy = { controller: $controllerProvider.register, directive: $compileProvider.directive, filter: $filterProvider.register, factory: $provide.factory, service: $provide.service }; }]); })();
angular.module('app').lazy.controller('SomeLazyController', function($scope) { $scope.key = '...'; });
$stateProvider .state('state1', { url: '/state1', template: '<div>{{st1Ctrl.msg}}</div>', controller: 'state1Controller as st1Ctrl', resolve: { fileDeps: ['$q', '$rootScope', function ($q, $rootScope) { var deferred = $q.defer(); var deps = [ 'app/messageService.js', 'app/state1Controller.js']; $script(deps, function () { $rootScope.$apply(function () { deferred.resolve(); }); }); return deferred.promise; }] } }) .state('state2', { url: '/state2', template: '<div>{{st2Ctrl.msg}}</div>', controller: 'state2Controller as st2Ctrl', resolve: { fileDeps: ['$q', '$rootScope', function ($q, $rootScope) { var deferred = $q.defer(); var deps = [ 'app/messageService.js', 'app/state2Controller.js']; $script(deps, function () { $rootScope.$apply(function () { deferred.resolve(); }); }); return deferred.promise; }] } }); }]);
کنترلر state1Controller که در فایلی با همین نام پیادهسازی شده است تنها در مسیر state1/ مورد نیاز است، و state2Controller تنها در مسیر state2/ لازم است بارگذاری شود. هردوی این کنترلرها به messageService وابستگی دارند که در messageService.js پیاده سازی شده است (همانطور که در این مطلب اشاره شده میتوانیم یک حالت انتزاعی به عنوان پدر دو حالت موجود تعریف کرده و وابستگی مشترک را به آن منتقل کنیم).
برای بارگذاری فایلهای مورد نیاز در ابتدای کار و راه اندازی اولیه برنامه هم میتوان به این ترتیب عمل کرد:
<script type="text/javascript"> // ----Script.js---- !function (a, b, c) { function t(a, c) { var e = b.createElement("script"), f = j; e.onload = e.onerror = e[o] = function () { e[m] && !/^c|loade/.test(e[m]) || f || (e.onload = e[o] = null, f = 1, c()) }, e.async = 1, e.src = a, d.insertBefore(e, d.firstChild) } function q(a, b) { p(a, function (a) { return !b(a) }) } var d = b.getElementsByTagName("head")[0], e = {}, f = {}, g = {}, h = {}, i = "string", j = !1, k = "push", l = "DOMContentLoaded", m = "readyState", n = "addEventListener", o = "onreadystatechange", p = function (a, b) { for (var c = 0, d = a.length; c < d; ++c) if (!b(a[c])) return j; return 1 }; !b[m] && b[n] && (b[n](l, function r() { b.removeEventListener(l, r, j), b[m] = "complete" }, j), b[m] = "loading"); var s = function (a, b, d) { function o() { if (!--m) { e[l] = 1, j && j(); for (var a in g) p(a.split("|"), n) && !q(g[a], n) && (g[a] = []) } } function n(a) { return a.call ? a() : e[a] } a = a[k] ? a : [a]; var i = b && b.call, j = i ? b : d, l = i ? a.join("") : b, m = a.length; c(function () { q(a, function (a) { h[a] ? (l && (f[l] = 1), o()) : (h[a] = 1, l && (f[l] = 1), t(s.path ? s.path + a + ".js" : a, o)) }) }, 0); return s }; s.get = t, s.ready = function (a, b, c) { a = a[k] ? a : [a]; var d = []; !q(a, function (a) { e[a] || d[k](a) }) && p(a, function (a) { return e[a] }) ? b() : !function (a) { g[a] = g[a] || [], g[a][k](b), c && c(d) }(a.join("|")); return s }; var u = a.$script; s.noConflict = function () { a.$script = u; return this }, typeof module != "undefined" && module.exports ? module.exports = s : a.$script = s }(this, document, setTimeout) $script('Scripts/angular.js', function () { $script('Scripts/angular-ui-router.js', function () { $script('app/app.js', function () { angular.bootstrap(document, ['app']); }); }); }); </script>
توجه داشته باشید که لازم نیست بارگذاری فایلها حتما یکی پس از دیگری باشد. ترتیب بارگذاری فایلها تنها در آنهایی که وابستگی به هم دارند باید رعایت شود. همچنین، میتوانید همه فایلهای مورد نیاز در این مرحله را Bundle کنید.
از اینجا میتوانید پروژه بسیار سادهای که در آن lazy loading پیاده شده است را دانلود کرده و مطالب توضیح داده شده را مشاهده کنید.