"serve": { "options": { "browserTarget": "AngularPort:build", "port": 4800 } }
تدارک مقدمات مثال این قسمت
این مثال، در ادامهی همین سری کار با فرمهای مبتنی بر قالبها است. به همین جهت ابتدا ماژول جدید CustomValidators را به آن اضافه میکنیم:
>ng g m CustomValidators -m app.module --routing
>ng g c CustomValidators/user-register
در ادامه کلاس مدل معادل فرم ثبت نام کاربران را تعریف میکنیم:
>ng g cl CustomValidators/user
export class User { constructor( public username: string = "", public email: string = "", public password: string = "", public confirmPassword: string = "" ) {} }
- ورود نام کاربری اجباری بوده و باید بین 5 تا 8 حرف باشد.
- ورود ایمیل اجباری بوده و باید فرمت مناسبی نیز داشته باشد.
- ورود کلمهی عبور اجباری بوده و باید با confirmPassword تطابق داشته باشد.
- ورود «کلمهی عبور خود را مجددا وارد کنید» اجباری بوده و باید با password تطابق داشته باشد.
تعریف اعتبارسنج سفارشی ایمیلها
هرچند میتوان اعتبارسنجی ایمیلها را توسط ویژگی استاندارد pattern نیز مدیریت کرد، اما جهت بررسی نحوهی انتقال آن به یک اعتبارسنج سفارشی، کار را با ایجاد یک دایرکتیو مخصوص آن ادامه میدهیم:
>ng g d CustomValidators/EmailValidator -m custom-validators.module
در ادامه کدهای کامل این اعتبارسنج سفارشی را مشاهده میکنید:
import { Directive } from "@angular/core"; import { AbstractControl, NG_VALIDATORS, Validator } from "@angular/forms"; @Directive({ selector: "[appEmailValidator][formControlName],[appEmailValidator][formControl],[appEmailValidator][ngModel]", providers: [ { provide: NG_VALIDATORS, useExisting: EmailValidatorDirective, multi: true } ] }) export class EmailValidatorDirective implements Validator { validate(element: AbstractControl): { [key: string]: any } { const emailRegex = /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/; const valid = emailRegex.test(element.value); return valid ? null : { appEmailValidator: true }; } }
- علت تعریف این اعتبارسنج به صورت یک دایرکتیو جدید این است که بتوان selector آنرا همانند ویژگیهای HTML، به فیلد ورودی اضافه کرد:
<input #email="ngModel" required appEmailValidator type="text" class="form-control" name="email" [(ngModel)]="model.email">
- روش تعریف selector آن اندکی متفاوت است:
selector: "[appEmailValidator][formControlName],[appEmailValidator][formControl],[appEmailValidator][ngModel]",
الف) نام دایرکتیو باید با یک پیشوند شروع شود و این پیشوند در فایل angular-cli.json. به app تنظیم شدهاست:
"apps": [ { // ... "prefix": "app",
ب) در اینجا formControlName، formControl و ngModel قید شدهی در کنار نام selector این دایرکتیو را نیز مشاهده میکنید. وجود آنها به این معنا است که کلاس این دایرکتیو، به المانهایی که به آنها ویژگی appEmailValidator اضافه شدهاست و همچنین آن المانها از یکی از سه نوع ذکر شده هستند، اعمال میشود و در سایر موارد بیاثر خواهد بود. البته ذکر این سه نوع، اختیاری است و صرفا میتوان نوشت:
selector: "[appEmailValidator]"
- پس از آن قسمت providers را مشاهده میکنید:
providers: [ { provide: NG_VALIDATORS, useExisting: EmailValidatorDirective, multi: true }
- سپس پیاده سازی اینترفیس توکار Validator را مشاهده میکنید:
export class EmailValidatorDirective implements Validator {
برای پیاده سازی این اینترفیس، نیاز است متد اجباری ذیل را نیز افزود و تکمیل کرد:
validate(element: AbstractControl): { [key: string]: any }
validate(element: AbstractControl): { [key: string]: any } { const emailRegex = /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/; const valid = emailRegex.test(element.value); return valid ? null : { appEmailValidator: true }; }
- اکنون که این دایرکتیو جدید طراحی و ثبت شدهاست (در قسمت declarations فایل custom-validators.module.ts)، تنها کافی است selector آنرا به المان ورودی مدنظر اعمال کنیم تا کار اعتبارسنجی آنرا به صورت خودکار مدیریت کند:
<input #email="ngModel" required appEmailValidator type="text" class="form-control" name="email" [(ngModel)]="model.email">
نحوهی طراحی خروجی متد validate
هنگام پیاده سازی متد validate اینترفیس Validator، هیچ قالب خاصی برای خروجی آن درنظر گرفته نشدهاست و همینقدر که این خروجی یک شیء key/value باشد، کفایت میکند. برای مثال اگر اعتبارسنج استاندارد required با شکست مواجه شود، یک چنین شیءایی را بازگشت میدهد:
{ required:true }
{ minlength : { requiredLength : 3, actualLength : 1 } }
{ appEmailValidator: true }
<div class="alert alert-danger" *ngIf="email.errors.appEmailValidator"> The entered email is not valid. </div>
از دیدگاه اعتبارسنج فرمهای مبتنی بر قالبها، همینقدر که آرایهی email.errors دارای عضو و کلید جدیدی شد، کار به پایان رسیدهاست و اعتبارسنجی المان را شکست خورده ارزیابی میکند. مابقی آن، اطلاعاتی است که برنامه نویس ارائه میدهد (بر اساس نیازهای نمایشی برنامه).
تهیه اعتبارسنج سفارشی مقایسهی کلمات عبور با یکدیگر
در طراحی کلاس User که معادل فیلدهای فرم ثبت نام کاربران است، دو خاصیت کلمهی عبور و تائید کلمهی عبور را مشاهده میکنید:
public password: string = "", public confirmPassword: string = ""
>ng g d CustomValidators/EqualValidator -m custom-validators.module
در ادامه کدهای کامل آنرا در ذیل مشاهده میکنید:
import { Directive, Attribute } from "@angular/core"; import { Validator, AbstractControl, NG_VALIDATORS } from "@angular/forms"; @Directive({ selector: "[appValidateEqual][formControlName],[appValidateEqual][formControl],[appValidateEqual][ngModel]", providers: [ { provide: NG_VALIDATORS, useExisting: EqualValidatorDirective, multi: true } ] }) export class EqualValidatorDirective implements Validator { constructor(@Attribute("compare-to") public compareToControl: string) {} validate(element: AbstractControl): { [key: string]: any } { const selfValue = element.value; const otherControl = element.root.get(this.compareToControl); console.log("EqualValidatorDirective", { thisControlValue: selfValue, otherControlValue: otherControl ? otherControl.value : null }); if (otherControl && selfValue !== otherControl.value) { return { appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject. }; } if ( otherControl && otherControl.errors && selfValue === otherControl.value ) { delete otherControl.errors["appValidateEqual"]; if (!Object.keys(otherControl.errors).length) { otherControl.setErrors(null); } } return null; } }
- قسمت آغازین این اعتبارسنج سفارشی، مانند توضیحات EmailValidatorDirective است که در ابتدای بحث عنوان شد. این کلاس به یک Directive مزین شدهاست تا بتوان selector آنرا به المانهای HTML ایی فرم افزود (برای مثال در اینجا به دو فیلد ورود کلمات عبور). قسمت providers آن نیز تنظیم شدهاست تا EqualValidatorDirective جاری به لیست توکار NG_VALIDATORS اضافه شود.
- در ابتدای کار، پیاده سازی اینترفیس Validator، همانند قبل انجام شدهاست؛ اما چون در اینجا میخواهیم نام فیلدی را که قرار است کار مقایسه را با آن انجام دهیم نیز دریافت کنیم، ابتدا یک Attribute و سپس یک پارامتر و خاصیت عمومی دریافت کنندهی مقدار آنرا نیز افزودهایم:
export class EqualValidatorDirective implements Validator { constructor(@Attribute("compare-to") public compareToControl: string) {}
<input #password="ngModel" required type="password" class="form-control" appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password">
در اینجا محدودیتی هم از لحاظ تعداد ویژگیها نیست و اگر قرار است این اعتبارسنج اطلاعات بیشتری را نیز دریافت کند میتوان ویژگیهای بیشتری را به سازندهی آن نسبت داد.
یک نکته: میتوان نام این ویژگی را با نام selector نیز یکی انتخاب کرد. به این ترتیب ذکر نام ویژگی آن، هم سبب فعال شدن اعتبارسنج و هم نسبت دادن مقداری به آن، سبب مقدار دهی خاصیت متناظر با آن، در سمت کلاس اعتبارسنج میگردد.
- در ابتدای این اعتبارسنج، نحوهی دسترسی به مقدار یک کنترل دیگر را نیز مشاهده میکنید:
export class EqualValidatorDirective implements Validator { constructor(@Attribute("compare-to") public compareToControl: string) {} validate(element: AbstractControl): { [key: string]: any } { const selfValue = element.value; const otherControl = element.root.get(this.compareToControl); console.log("EqualValidatorDirective", { thisControlValue: selfValue, otherControlValue: otherControl ? otherControl.value : null });
بر اساس مقدار خاصیت compareToControl که از ویژگی compare-to دریافت میشود، میتوان به کنترل دوم، توسط element.root.get دسترسی یافت.
- در ادامهی کار، مقایسهی سادهای را مشاهده میکنید:
if (otherControl && selfValue !== otherControl.value) { return { appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject. }; }
- در پایان کدهای متد validate، چنین تنظیمی نیز قرار گرفتهاست:
if (otherControl && otherControl.errors && selfValue === otherControl.value) { delete otherControl.errors["appValidateEqual"]; if (!Object.keys(otherControl.errors).length) { otherControl.setErrors(null); } } return null;
تکمیل کامپوننت فرم ثبت نام کاربران
اکنون user-register.component.ts را که در ابتدای بحث اضافه کردیم، چنین تعاریفی را پیدا میکند:
import { NgForm } from "@angular/forms"; import { User } from "./../user"; import { Component, OnInit } from "@angular/core"; @Component({ selector: "app-user-register", templateUrl: "./user-register.component.html", styleUrls: ["./user-register.component.css"] }) export class UserRegisterComponent implements OnInit { model = new User(); constructor() {} ngOnInit() {} submitForm(form: NgForm) { console.log(this.model); console.log(form.value); } }
ابتدای فرم
<div class="container"> <h3>Registration Form</h3> <form #form="ngForm" (submit)="submitForm(form)" novalidate>
تکمیل قسمت ورود نام کاربری
<div class="form-group" [class.has-error]="username.invalid && username.touched"> <label class="control-label">User Name</label> <input #username="ngModel" required maxlength="8" minlength="4" type="text" class="form-control" name="username" [(ngModel)]="model.username"> <div *ngIf="username.invalid && username.touched"> <div class="alert alert-info"> errors: {{ username.errors | json }} </div> <div class="alert alert-danger" *ngIf="username.errors.required"> username is required. </div> <div class="alert alert-danger" *ngIf="username.errors.minlength"> username should be minimum {{username.errors.minlength.requiredLength}} characters. </div> <div class="alert alert-danger" *ngIf="username.errors.maxlength"> username should be max {{username.errors.maxlength.requiredLength}} characters. </div> </div> </div>
تکمیل قسمت ورود ایمیل
<div class="form-group" [class.has-error]="email.invalid && email.touched"> <label class="control-label">Email</label> <input #email="ngModel" required appEmailValidator type="text" class="form-control" name="email" [(ngModel)]="model.email"> <div *ngIf="email.invalid && email.touched"> <div class="alert alert-info"> errors: {{ email.errors | json }} </div> <div class="alert alert-danger" *ngIf="email.errors.required"> email is required. </div> <div class="alert alert-danger" *ngIf="email.errors.appEmailValidator"> The entered email is not valid. </div> </div> </div>
تکمیل قسمتهای ورود کلمهی عبور و تائید آن
<div class="form-group" [class.has-error]="password.invalid && password.touched"> <label class="control-label">Password</label> <input #password="ngModel" required type="password" class="form-control" appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password"> <div *ngIf="password.invalid && password.touched"> <div class="alert alert-info"> errors: {{ password.errors | json }} </div> <div class="alert alert-danger" *ngIf="password.errors.required"> password is required. </div> <div class="alert alert-danger" *ngIf="password.errors.appValidateEqual"> Password mismatch. Please complete the confirmPassword . </div> </div> </div> <div class="form-group" [class.has-error]="confirmPassword.invalid && confirmPassword.touched"> <label class="control-label">Retype password</label> <input #confirmPassword="ngModel" required type="password" class="form-control" appValidateEqual compare-to="password" name="confirmPassword" [(ngModel)]="model.confirmPassword"> <div *ngIf="confirmPassword.invalid && confirmPassword.touched"> <div class="alert alert-info"> errors: {{ confirmPassword.errors | json }} </div> <div class="alert alert-danger" *ngIf="confirmPassword.errors.required"> confirmPassword is required. </div> <div class="alert alert-danger" *ngIf="confirmPassword.errors.appValidateEqual"> Password mismatch. </div> </div> </div>
همچنین خروجی آن نیز در قسمت ngIf آخر بررسی شدهاست و سبب نمایش خطای اعتبارسنجی متناسبی میشود.
تکمیل انتهای فرم
<button class="btn btn-primary" [disabled]="form.invalid" type="submit">Ok</button> </form> </div>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-08.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات
>npm install >ng build --watch
>dotnet restore >dotnet watch run
API Versioning
- URI-based versioning
- Header-based versioning
- Media type-based versioning
پیاده سازی URI-based versioning
public class ItemViewModel { public int Id { get; set; } public string Name { get; set; } public string Country { get; set; } } public class ItemV2ViewModel : ItemViewModel { public double Price { get; set; } }
public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" }; return Ok(viewModel); } } public class ItemsV2Controller : ApiController { [ResponseType(typeof(ItemV2ViewModel))] public IHttpActionResult Get(int id) { var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 }; return Ok(viewModel); } }
config.Routes.MapHttpRoute("ItemsV2", "api/v2/items/{id}", new { controller = "ItemsV2", id = RouteParameter.Optional }); config.Routes.MapHttpRoute("Items", "api/items/{id}", new { controller = "Items", id = RouteParameter.Optional }); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } );
config.Routes.MapHttpRoute( "defaultVersioned", "api/v{version}/{controller}/{id}", new { id = RouteParameter.Optional }, new { version = @"\d+" }); config.Routes.MapHttpRoute( "DefaultApi", "api/{controller}/{id}", new { id = RouteParameter.Optional } );
با این تنظیمات فعلا به مسیریابی ورژن بندی شدهای دست نیافتهایم. زیرا فعلا به هیچ طریق به Web API اشاره نکردهایم که به چه صورت از این پارامتر version برای پیدا کردن کنترلر ورژن بندی شده استفاده کند و به همین دلیل این دو مسیریابی نوشته شده در عمل نتیجه یکسانی را خواهند داشت. برای رفع مشکل مطرح شده باید فرآیند پیش فرض انتخاب کنترلر را کمی شخصی سازی کنیم.
IHttpControllerSelector مسئول پیدا کردن کنترلر مربوطه با توجه به درخواست رسیده میباشد. شکل زیر مربوط است به مراحل ساخت کنترلر بر اساس درخواست رسیده:
به جای پیاده سازی مستقیم این اینترفیس، از پیاده سازی کننده پیش فرض موجود (DefaultHttpControllerSelector) اسفتاده کرده و HttpControllerSelector جدید ما از آن ارث بری خواهد کرد.
public class VersionFinder { private static bool NeedsUriVersioning(HttpRequestMessage request, out string version) { var routeData = request.GetRouteData(); if (routeData != null) { object versionFromRoute; if (routeData.Values.TryGetValue(nameof(version), out versionFromRoute)) { version = versionFromRoute as string; if (!string.IsNullOrWhiteSpace(version)) { return true; } } } version = null; return false; } private static int VersionToInt(string versionString) { int version; if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version)) return 0; return version; } public static int GetVersionFromRequest(HttpRequestMessage request) { string version; return NeedsUriVersioning(request, out version) ? VersionToInt(version) : 0; } }
کلاس VersionFinder برای یافتن ورژن رسیده در درخواست جاری مورد استفاده قرار خواهد گرفت. با استفاده از متد NeedsUriVersioning بررسی صورت میگیرد که آیا در این درخواست پارامتری به نام version وجود دارد یا خیر که درصورت موجود بودن، مقدار آن واکشی شده و درون پارامتر out قرار میگیرد. در متد GetVersionFromRequest بررسی میشود که اگر خروجی متد NeedsUriVersioning برابر با true باشد، با استفاده از متد VersionToInt مقدار به دست آمده را به int تبدیل کند.
public class VersionAwareControllerSelector : DefaultHttpControllerSelector { public VersionAwareControllerSelector(HttpConfiguration configuration) : base(configuration) { } public override string GetControllerName(HttpRequestMessage request) { var controllerName = base.GetControllerName(request); var version = VersionFinder.GetVersionFromRequest(request); return version > 0 ? GetVersionedControllerName(request, controllerName, version) : controllerName; } private string GetVersionedControllerName(HttpRequestMessage request, string baseControllerName, int version) { var versionControllerName = $"{baseControllerName}v{version}"; HttpControllerDescriptor descriptor; if (GetControllerMapping().TryGetValue(versionControllerName, out descriptor)) { return versionControllerName; } throw new HttpResponseException(request.CreateErrorResponse( HttpStatusCode.NotFound, $"No HTTP resource was found that matches the URI {request.RequestUri} and version number {version}")); } }
متد GetControllerName وظیفه بازگشت دادن نام کنترلر را برعهده دارد. ما نیز با لغو رفتار پیش فرض این متد و تهیه نام ورژن بندی شده کنترلر و معرفی این پیاده سازی از IHttpControllerSelector به شکل زیر میتوانیم به Web API بگوییم که به چه صورت از پارامتر version موجود در درخواست استفاده کند.
config.Services.Replace(typeof(IHttpControllerSelector), new VersionAwareControllerSelector(config));
حال با اجرای برنامه به نتیجه زیر خواهیم رسید:
راه حل دوم: برای زمانیکه Attribute Routing مورد استفاده شما است میتوان به راحتی با تعریف قالبهای مناسب مسیریابی، API ورژن بندی شده را ارائه دهید.
[RoutePrefix("api/v1/Items")] public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] [Route("{id:int}")] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" }; return Ok(viewModel); } } [RoutePrefix("api/V2/Items")] public class ItemsV2Controller : ApiController { [ResponseType(typeof(ItemV2ViewModel))] [Route("{id:int}")] public IHttpActionResult Get(int id) { var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 }; return Ok(viewModel); } }
اگر توجه کرده باشید در مثال ما، نامهای کنترلرها متفاوت از هم میباشند. اگر بجای در نظر گرفتن نامهای مختلف برای یک کنترلر در ورژنهای مختلف، آن را با یک نام یکسان درون namespaceهای مختلف احاطه کنیم یا حتی آنها را درون Class Libraryهای جدا نگهداری کنیم، به مشکل "یافت شدن چندین کنترلر که با درخواست جاری مطابقت دارند" برخواهیم خورد. برای حل این موضوع به راه حل سوم توجه کنید.
راه حل سوم: استفاده از یک NamespaceControllerSelector که پیاده سازی دیگری از اینترفیس IHttpControllerSelector میباشد. فرض بر این است که قالب پروژه به شکل زیر میباشد:
کار با پیاده سازی اینترفیس IHttpRouteConstraint آغاز میشود:
public class VersionConstraint : IHttpRouteConstraint { public VersionConstraint(string allowedVersion) { AllowedVersion = allowedVersion.ToLowerInvariant(); } public string AllowedVersion { get; private set; } public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection) { object value; if (values.TryGetValue(parameterName, out value) && value != null) { return AllowedVersion.Equals(value.ToString().ToLowerInvariant()); } return false; } }
کلاس بالا در واقع برای اعمال محدودیت خاصی که در ادامه توضیح داده میشود، پیاده سازی شده است.
متد Match آن وظیفه چک کردن برابری مقدار کلید parameterName موجود در درخواست را با مقدار allowedVersion ای که API از آن پشتیبانی میکند، برعهده دارد. با استفاده از این Constraint مشخص کردهایم که دقیقا چه زمانی باید Route نوشته شده انتخاب شود.
به روش استفاده از این Constraint توجه کنید:
namespace UriBasedVersioning.Namespace.Controllers.V1 { using Models.V1; RoutePrefix("api/{version:VersionConstraint(v1)}/items")] public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] [Route("{id}")] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" }; return Ok(viewModel); } } } namespace UriBasedVersioning.Namespace.Controllers.V2 { using Models.V2; RoutePrefix("api/{version:VersionConstraint(v2)}/items")] public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] [Route("{id}")] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 }; return Ok(viewModel); } } }
با توجه به کد بالا میتوان دلیل استفاده از VersionConstraint را هم درک کرد؛ از آنجایی که ما دو Route شبیه به هم داریم، لذا باید مشخص کنیم که در چه شرایطی و کدام یک از این Routeها انتخاب شود. خوب، اگر برنامه را اجرا کرده و یکی از APIهای بالا را تست کنید، با خطا مواجه خواهید شد؛ زیرا فعلا این Constraint به سیستم Web API معرفی نشده است. تنظیمات زیر را انجام دهید:
var constraintsResolver = new DefaultInlineConstraintResolver(); constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof (VersionConstraint)); config.MapHttpAttributeRoutes(constraintsResolver);
مرحله بعدی کار، پیاده سازی IHttpControllerSelector میباشد:
public class NamespaceControllerSelector : IHttpControllerSelector { private readonly HttpConfiguration _configuration; private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers; public NamespaceControllerSelector(HttpConfiguration config) { _configuration = config; _controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary); } public HttpControllerDescriptor SelectController(HttpRequestMessage request) { var routeData = request.GetRouteData(); if (routeData == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } var controllerName = GetControllerName(routeData); if (controllerName == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } var version = GetVersion(routeData); if (version == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", version, controllerName); HttpControllerDescriptor controllerDescriptor; if (_controllers.Value.TryGetValue(controllerKey, out controllerDescriptor)) { return controllerDescriptor; } throw new HttpResponseException(HttpStatusCode.NotFound); } public IDictionary<string, HttpControllerDescriptor> GetControllerMapping() { return _controllers.Value; } private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary() { var dictionary = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase); var assembliesResolver = _configuration.Services.GetAssembliesResolver(); var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver(); var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver); foreach (var controllerType in controllerTypes) { var segments = controllerType.Namespace.Split(Type.Delimiter); var controllerName = controllerType.Name.Remove(controllerType.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length); var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", segments[segments.Length - 1], controllerName); if (!dictionary.Keys.Contains(controllerKey)) { dictionary[controllerKey] = new HttpControllerDescriptor(_configuration, controllerType.Name, controllerType); } } return dictionary; } private static T GetRouteVariable<T>(IHttpRouteData routeData, string name) { object result; if (routeData.Values.TryGetValue(name, out result)) { return (T)result; } return default(T); } private static string GetControllerName(IHttpRouteData routeData) { var subroute = routeData.GetSubRoutes().FirstOrDefault(); var dataTokenValue = subroute?.Route.DataTokens.First().Value; var controllerName = ((HttpActionDescriptor[])dataTokenValue)?.First().ControllerDescriptor.ControllerName.Replace("Controller", string.Empty); return controllerName; } private static string GetVersion(IHttpRouteData routeData) { var subRouteData = routeData.GetSubRoutes().FirstOrDefault(); return subRouteData == null ? null : GetRouteVariable<string>(subRouteData, "version"); } }
سورس اصلی کلاس بالا از این آدرس قابل دریافت است. در تکه کد بالا بخشی که مربوط به چک کردن تکراری بودن آدرس میباشد، برای ساده سازی کار حذف شده است. ولی نکتهی مربوط به SubRoutes که برای واکشی مقادیر پارامترهای مرتبط با Attribute Routing میباشد، اضافه شده است. روال کار به این صورت است که ابتدا RouteData موجود در درخواست را واکشی کرده و با استفاده از متدهای GetControllerName و GetVersion، پارامترهای controller و version را جستجو میکنیم. بعد با استفاده از مقادیر به دست آمده، controllerKey را تشکیل داده و درون کنترلرهای موجود در برنامه به دنبال کنترلر مورد نظر خواهیم گشت.
کارکرد متد InitializeControllerDictionary :
همانطور که میدانید به صورت پیشفرض Web API توجهی به فضای نام مرتبط با کنترلرها ندارد. از طرفی برای پیاده سازی اینترفیس IHttpControllerSelector نیاز است متدی با امضای زیر را داشته باشیم:
public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
لذا در کلاس پیاده سازی شده، خصوصیتی به نام _controllers را که از به صورت Lazy و از نوع بازگشتی متد بالا میباشد، تعریف کردهایم. متد InitializeControllerDictionary کار آماده سازی دادههای مورد نیاز خصوصیت _controllers میباشد. به این صورت که تمام کنترلرهای موجود در برنامه را واکشی کرده و این بار کلیدهای مربوط به دیکشنری را با استفاده از نام کنترلر و آخرین سگمنت فضای نام آن، تولید و درون دیکشنری مورد نظر ذخیره کردهایم.
حال تنظیمات زیر را اعمال کنید:
public static class WebApiConfig { public static void Register(HttpConfiguration config) { var constraintsResolver = new DefaultInlineConstraintResolver(); constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof (VersionConstraint)); config.MapHttpAttributeRoutes(constraintsResolver); config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceControllerSelector(config)); } }
این بار برنامه را اجرا کرده و APIهای مورد نظر را تست کنید؛ بله بدون مشکل کار خواهد کرد.
نکته تکمیلی: سورس مذکور در سایت کدپلکس برای حالتی که از Centralized Routes استفاده میکنید آماده شده است. روش مذکور در این مقاله هم فقط قسمت Duplicate Routes آن را کم دارد که میتوانید اضافه کنید. پیاده سازی دیگری را از این راه حل هم میتوانید داشته باشید.
پیاده سازی Header-based versioning
public class HeaderVersionConstraint : IHttpRouteConstraint { private const string VersionHeaderName = "api-version"; public HeaderVersionConstraint(int allowedVersion) { AllowedVersion = allowedVersion; } public int AllowedVersion { get; } public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection) { if (!request.Headers.Contains(VersionHeaderName)) return false; var version = request.Headers.GetValues(VersionHeaderName).FirstOrDefault(); return VersionToInt(version) == AllowedVersion; } private static int VersionToInt(string versionString) { int version; if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version)) return 0; return version; } }
public sealed class HeaderVersionedRouteAttribute : RouteFactoryAttribute { public HeaderVersionedRouteAttribute(string template) : base(template) { Order = -1; } public int Version { get; set; } public override IDictionary<string, object> Constraints => new HttpRouteValueDictionary { {"", new HeaderVersionConstraint(Version)} }; }
[RoutePrefix("api/items")] public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] [HeaderVersionedRoute("{id}", Version = 1)] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" }; return Ok(viewModel); } } [RoutePrefix("api/items")] public class ItemsV2Controller : ApiController { [ResponseType(typeof(ItemV2ViewModel))] [HeaderVersionedRoute("{id}", Version = 2)] public IHttpActionResult Get(int id) { var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 }; return Ok(viewModel); } }
پیاده سازی Media type-based versioning
GET /api/Items HTTP 1.1 Accept: application/vnd.mediatype.versioning-v1+json GET /api/Items HTTP 1.1 Accept: application/vnd.mediatype.versioning-v2+json
public class MediaTypeVersionConstraint : IHttpRouteConstraint { private const string VersionMediaType = "vnd.mediatype.versioning"; public MediaTypeVersionConstraint(int allowedVersion) { AllowedVersion = allowedVersion; } public int AllowedVersion { get; } public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection) { if (!request.Headers.Accept.Any()) return false; var acceptHeaderVersion = request.Headers.Accept.FirstOrDefault(x => x.MediaType.Contains(VersionMediaType)); //Accept: application/vnd.mediatype.versioning-v1+json if (acceptHeaderVersion == null || !acceptHeaderVersion.MediaType.Contains("-v") || !acceptHeaderVersion.MediaType.Contains("+")) return false; var version = acceptHeaderVersion.MediaType.Between("-v", "+"); return VersionToInt(version)==AllowedVersion; } }
public class Product { public int ProductId { get; set; } public string ProductName { get; set; } public long ProductPrice { get; set; } }
public class ProductMetadata : MetadataClass<Product> { public ProductMetadata () { this.Validation(x => x.ProductName).Required("عنوان محصول وارد نشده است"); this.Validation(x => x.ProductPrice).Range(1000,100000,"قیمت محصول باید بین هزار تومان تا صدهزار تومان باشد"); } }
public class FluentMetadataConfiguration : IFluentMetadataConfiguration { public void OnTypeCreation(MetadataContainer metadataContainer) { metadataContainer.Add(new ProductMetadata()); } }
[EnableClientAccess()] [FluentMetadata(typeof(FluentMetadataConfiguration))] public class FluentMetadataTestDomainService : DomainService { ... }
یکی از شروط تهیه آزمونهای واحد، خارج نشدن از مرزهای سیستم در حین بررسی آزمونهای مورد نظر است؛ تا بتوان تمام آزمونها را با سرعت بسیار بالایی، بدون نگرانی از در دسترس نبودن منابع خارجی، درست در لحظه انجام آزمونها، به پایان رساند. اگر این خروج صورت گیرد، بجای unit tests با integration tests سر و کار خواهیم داشت. در این میان، کار با فایلها نیز مصداق بارز خروج از مرزهای سیستم است.
برای حل این مشکل راه حلهای زیادی توصیه شدهاند؛ منجمله تهیه یک اینترفیس محصور کننده فضای نام System.IO و سپس استفاده از فریم ورکهای mocking و امثال آن. یک نمونه از پیاده سازی آنرا اینجا میتوانید پیدا کنید : (+)
اما راه حل سادهتری نیز برای این مساله وجود دارد و آن هم افزودن فایلهای مورد نظر به پروژه آزمون واحد جاری و سپس مراجعه به خواص فایلها و تغییر Build Action آنها به Embedded Resource میباشد. به این صورت پس از کامپایل پروژه، فایلهای ما در قسمت منابع اسمبلی جاری قرار گرفته و به کمک متد زیر قابل دسترسی خواهند بود:
using System.IO;
using System.Reflection;
public class UtHelper
{
public static string GetInputFile(string filename)
{
var thisAssembly = Assembly.GetExecutingAssembly();
var stream = thisAssembly.GetManifestResourceStream(filename);
return new StreamReader(stream).ReadToEnd();
}
}
نکتهای را که اینجا باید به آن دقت داشت، filename متد GetInputFile است. چون این فایل دیگر به صورت متداول از فایل سیستم خوانده نخواهد شد، نام واقعی آن به صورت namespace.filename میباشد (همان نام منبع اسمبلی جاری).
اگر جهت یافتن این نام با مشکل مواجه شدید، تنها کافی است اسمبلی آزمون واحد را با برنامه Reflector یا ابزارهای مشابه گشوده و نام منابع آنرا بررسی کنید.
• Join
• GroupJoin
• Zip
عملگر Join
این عملگر همانند inner join در SQL، دو مجموعه را بر اساس کلیدهای مرتبط که از طریق پارامترها به آن ارسال میشوند، با یکدیگر ترکیب میکند.
در عملیات Join، یک توالی ورودی که به آن توالی خارجی (Outer Sequence) گفته میشود با یک توالی دیگر که به آن توالی داخلی (Inner Sequence) میگوییم، بر اساس کلیدهای مشخص شده، ترکیب شده و یک توالی خروجی تولید میشود.
بررسی پارامترهای عملگر Join:
public static IEnumerable<TResult> Join<TOuter,TInner,TKey,TResult> (this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter,TKey> outerKeySelector, Func<TInner,TKey> innerKeySelector, Func<TOuter,TInner,TResult> resultSelector)
• Func<Touter,Tkey> outerKeySelector : عنصر کلید، در توالی خارجی
• Func<Tinner,Tkey> innerKeySelector : عنصر کلید، در توالی داخلی
• Func<Touter,Tinner,Tresult> resultSelector : یک عبارت Lambda است که ظاهر عناصر خروجی را مشخص میکند.
نکته : بطور کلی T در پارامترهای بالا معرف Generic Type Parameter میباشد؛ T==>Type (هر نوع دادهای که ما مشخص کنیم).
نکته : عملگر Join یک امضاء دیگر نیز دارد که اجازه مشخص کردن IEqualityComparer را میدهد.
کد زیر استفاده از عملگر Join را نشان میدهد. توجه داشته باشید که اعلان صریح نوع دادهها در عبارات Lambda نوشته شده، فقط برای روشنتر شدن فرآیند عملیات میباشد.
تعریف دو آرایه از کلاسهای Recipe و Review:
Recipe[] recipes = { new Recipe {Id = 1, Name = "Mashed Potato"}, new Recipe {Id = 2, Name = "Crispy Duck"}, new Recipe {Id = 3, Name = "Sachertorte"} }; // inner sequence Review[] reviews = { new Review {RecipeId = 1, ReviewText = "Tasty!"}, new Review {RecipeId = 1, ReviewText = "Not nice :("}, new Review {RecipeId = 1, ReviewText = "Pretty good"}, new Review {RecipeId = 2, ReviewText = "Too hard"}, new Review {RecipeId = 2, ReviewText = "Loved it"} }; var query = recipes // recipes توالی خارجی .Join(reviews, // reviewsتوالی داخلی (Recipe outerKey) => outerKey.Id, // کلید انخاب شده از توالی خارجی (Review innerKey) => innerKey.RecipeId, // کلید انتخاب شده از توالی داخلی // نحوه قالب بندی خروجی (recipe, review) => recipe.Name + " - " + review.ReviewText); foreach (string item in query) { Console.WriteLine(item); }
Mashed Potato - Tasty! Mashed Potato - Not nice :( Mashed Potato - Pretty good Crispy Duck - Too hard Crispy Duck - Loved it
var query = recipes.Join (reviews, outerKey => outerKey.Id, innerKey => innerKey.RecipeId, (recipe, review) => recipe.Name + " - " + review.ReviewText);
پیاده سازی توسط عبارتهای جستجو
کلمه کلیدی Join، در زمان استفاده از روش عبارتهای پرس و جو، مورد استفاده قرار گرفت. دستور Join در قسمت چهارم از این سری آموزشی بطور کامل بررسی شده است. کد زیر نحوه اجرای دستور Join را به روش عبارتهای پرس و جو، نشان میدهد.
var query = from recipe in recipes join review in reviews on recipe.Id equals review.RecipeId select new //انواع بی نام { RecipeName = recipe.Name, RecipeReview = review.ReviewText }; foreach (var item in query) { Console.WriteLine(item.RecipeName + " - " + item.RecipeReview); }
عملگر GroupJoin
نحوه عملکرد عملگر GroupJoin، شبیه عملگر Join میباشد؛ با این تفاوت که خروجی حاصل از دستور GroupJoin، یک ساختار سلسله مراتبی میباشد. توالی خروجی، مجموعهای از گروهها میباشد که هر گروه، تشکیل شدهاست از عناصر توالی درونی.
بررسی پارامترهای عملگر GroupJoin
• <Inner IEnumerable<TInner : نشان دهنده توالی داخلی
• Func<Touter,Tkey> outerKeySelector : عنصر کلید، در توالی خارجی
• Func<Tinner,Tkey> innerKeySelector : عنصر کلید، در توالی داخلی
• Func<Touter,Ienumerable<Tinner>,Tresult> resultSelector : قالب بندی گروههای تولید شده خروجی را مشخص میکند
کد زیر استفاده از عملگر GroupJoin را نشان میدهد :
// outer sequence Recipe[] recipes = { new Recipe {Id = 1, Name = "Mashed Potato"}, new Recipe {Id = 2, Name = "Crispy Duck"}, new Recipe {Id = 3, Name = "Sachertorte"} }; // inner sequence Review[] reviews = { new Review {RecipeId = 1, ReviewText = "Tasty!"}, new Review {RecipeId = 1, ReviewText = "Not nice :("}, new Review {RecipeId = 1, ReviewText = "Pretty good"}, new Review {RecipeId = 2, ReviewText = "Too hard"}, new Review {RecipeId = 2, ReviewText = "Loved it"} }; var query = recipes .GroupJoin( reviews, (Recipe outerKey) => outerKey.Id,//outer key (Review innerKey) => innerKey.RecipeId,//inner key (Recipe recipe, IEnumerable<Review> rev )=>تعریف ساختار گروهها new { RecipeName = recipe.Name, Reviews = rev } ); foreach (var item in query) { Console.WriteLine($"Reviews for {item.RecipeName}"); foreach (var review in item.Reviews) { Console.WriteLine($" - {review.ReviewText}"); } }
Reviews for Mashed Potato - Tasty! - Not nice :( - Pretty good Reviews for Crispy Duck - Too hard - Loved it Reviews for Sachertorte
پیاده سازی توسط عبارتهای جستجو
var query = from recipe in recipes join review in reviews on recipe.Id equals review.RecipeId into reviewGroup select new //انواع بی نام { RecipeName = recipe.Name, Reviews = reviewGroup//کلیه بازخوردها مرتبط با یک دستور غذایی };
Reviews for Mashed Potato - Tasty! - Not nice :( - Pretty good Reviews for Crispy Duck - Too hard - Loved it Reviews for Sachertorte
عملگر Zip
عملگر Zip، رفتاری متفاوت نسبت به عملگر GroupJoin و Join دارد و هیچ آیتمی را به عنوان کلید، از دو توالی دریافت نمیکند. عملگر Zip همه عناصر دو توالی را یک به یک، به ترتیب کنار هم قرار میدهد. مثل زیپ در دنیای واقعی که لبههای دو طرف زیپ را به هم میرساند.
public class Ingredient { public string Name { get; set; } public int Calories { get; set; } } string[] names = { "Flour", "Butter", "Sugar" }; int[] calories = { 100, 400, 500 }; IEnumerable<Ingredient> ingredients = names.Zip(calories, (name, calorie) => new Ingredient { Name = name, Calories = calorie }); foreach (var item in ingredients) { Console.WriteLine($"{item.Name} has {item.Calories} calories"); }
Flour has 100 calories Butter has 400 calories Sugar has 500 calories
پیاده سازی توسط عبارتهای جستجو
معادل عملگر Zip، کلمه کلیدی در عبارتهای جستجو وجود ندارد. ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
EF Code First #12
ساخت (Build) برنامههای Angular
Angular CLI کار ساخت و کامپایل برنامه را به صورت خودکار انجام داده و خروجی را در مسیری مشخص درج میکند. در اینجا میتوان گزینههایی را بر اساس نوع کامپایل مدنظر مانند کامپایل برای حالت توسعه و یا کامپایل برای حالت توزیع نهایی، انتخاب کرد. همچنین مباحث bundling و یکی کردن تعداد بالای ماژولهای برنامه در آن لحاظ میشوند تا برنامه در حالت توزیع نهایی، سبب 100ها رفت و برگشت به سرور برای دریافت ماژولهای مختلف آن نشود. به علاوه مباحث uglification (به نوعی obfuscation کدهای جاوا اسکریپتی نهایی) و tree-shaking (حذف کدهایی که در برنامه استفاده نشدهاند؛ یا کدهای مرده) نیز پیاده سازی میشوند. با انجام tree-shaking، نه تنها اندازهی توزیع نهایی به کاربر کاهش پیدا میکند، بلکه مرورگر نیز حجم کمتری از کدهای جاوااسکریپتی را باید تفسیر کند.
برای شروع میتوان از دستور ذیل برای مشاهدهی تمام گزینههای مهیای ساخت برنامه استفاده کرد:
> ng build --help
"apps": [ { "outDir": "dist",
فایل | توضیح |
inline.bundle.js | WebPack runtime از آن برای بارگذاری ماژولهای برنامه و چسباندن قسمتهای مختلف به یکدیگر استفاده میشود. |
main.bundle.js | شامل تمام کدهای ما است. |
polyfills.bundle.js | Polyfills - جهت پشتیبانی از مرورگرهای مختلف. |
styles.bundle.js | شامل بسته بندی تمام شیوه نامههای برنامه است |
vendor.bundle.js | کدهای کتابخانههای ثالث مورد استفاده و همچنین خود Angular، در اینجا بسته بندی میشوند. |
روشی برای بررسی محتوای bundleهای تولید شده
تولید bundleها در جهت کاهش رفت و برگشتهای به سرور و بالا بردن کارآیی برنامه ضروری هستند؛ اما دقیقا این بسته بندیها شامل چه اطلاعاتی میشوند؟ این اطلاعات را میتوان از فایلهای source map تولیدی استخراج کرد و برای این منظور میتوان از برنامهی source-map-explorer استفاده کرد.
روش نصب عمومی آن:
> npm install -g source-map-explorer
> source-map-explorer dist/main.bundle.js
یک مثال: ساخت برنامهی مثال قسمت چهارم - تنظیمات مسیریابی در حالت dev
در ادامه، کار Build همان مثالی را که در قسمت قبل توضیح داده شد، بررسی میکنیم. برای این منظور از طریق خط فرمان به ریشهی پوشهی اصلی پروژه وارد شده و دستور ng build را صادر کنید. یک چنین خروجی را مشاهده خواهید کرد:
D:\Prog\angular-routing>ng build Hash: 123cae8bd8e571f44c31 Time: 33862ms chunk {0} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 158 kB {4} [initial] [rendered] chunk {1} main.bundle.js, main.bundle.js.map (main) 14.7 kB {3} [initial] [rendered] chunk {2} styles.bundle.js, styles.bundle.js.map (styles) 9.77 kB {4} [initial] [rendered] chunk {3} vendor.bundle.js, vendor.bundle.js.map (vendor) 2.34 MB [initial] [rendered] chunk {4} inline.bundle.js, inline.bundle.js.map (inline) 0 bytes [entry] [rendered]
<!doctype html> <html> <head> <meta charset="utf-8"> <title>AngularRouting</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> </head> <body> <app-root>Loading...</app-root> <script type="text/javascript" src="inline.bundle.js"> </script><script type="text/javascript" src="polyfills.bundle.js"> </script><script type="text/javascript" src="styles.bundle.js"> </script><script type="text/javascript" src="vendor.bundle.js"> </script><script type="text/javascript" src="main.bundle.js"></script> </body> </html>
یک نکته: زمانیکه دستور ng serve -o صادر میشود، در پشت صحنه دقیقا همین دستور ng build صادر شده و اطلاعات را درون حافظه تشکیل میدهد. اما اگر کار ng build را دستی انجام دهیم، اینبار ng serve -o اطلاعات را از پوشهی dist دریافت میکند. بنابراین در حین کار با ng serve -o نیازی به build دستی پروژه نیست.
سؤال: چرا حجم فایل endor.bundle.js اینقدر بالا است و شامل چه اجزایی میشود؟
نکتهای که در اینجا وجود دارد، حجم بالای فایل vendor.bundle.js آن است که 2.34 MB میباشد:
چون دستور ng build بدون پارامتری ذکر شدهاست، برنامه را برای حالت توسعه Build میکند و به همین جهت هیچگونه بهینه سازی در این مرحله صورت نخواهد گرفت. برای بررسی محتوای این فایل میتوان دستور ذیل را در ریشهی اصلی پروژه صادر کرد:
> source-map-explorer dist/vendor.bundle.js
همانطور که مشاهده میکنید، در حالت بهینه سازی نشده و Build برای توسعه، کامپایلر Angular حدود 41 درصد حجم فایل vendor.bundle.js را تشکیل میدهد. به علاوه ماژولها و قسمتهایی را ملاحظه میکنید که اساسا برنامهی فعلی مثال ما از آنها استفاده نمیکند؛ مانند http، فرمها و غیره.
سفارشی سازی Build برای محیطهای مختلف
اگر به پروژهی تولید شدهی توسط Angular CLI دقت کنید، حاوی پوشهای است به نام src\environments
هدف از فایلهای environment برای نمونه تغییر آدرس توزیع برنامه در حالت توسعه و ارائه نهایی است.
همچنین در اینجا میتوان نحوهی بهینه سازی فایلهای تولیدی را توسط Build Targets مشخص کرد و اینکار توسط ذکر پرچم prod-- (مخفف production) صورت میگیرد.
در ادامه، تفاوتهای دستورهای ng build و ng build --prod را ملاحظه میکنید:
- با اجرای ng build، از فایل environment.ts استفاده میشود؛ برخلاف حالت اجرای ng build --prod که از فایل environment.prod.ts استفاده میکند.
- Cache-busting در حالت ارائهی نهایی، به تمام اجزای پروژه اعمال میشود؛ اما در حالت توسعه فقط برای تصاویر قید شدهی در فایلهای css.
- فایلهای source map فقط برای حالت توسعه تولید میشوند.
- در حالت توسعه، cssها داخل فایلهای js تولیدی قرار میگیرند؛ اما در حالت ارائهی نهایی به صورت فایلهای css بسته بندی میشوند.
- در حالت توسعه برخلاف حالت ارائهی نهایی، کار uglification انجام نمیشود.
- در حالت توسعه برخلاف حالت ارائهی نهایی، کار tree-shaking یا حذف کدهای مرده و بدون ارجاع، انجام نمیشود.
- در حالت توسعه برخلاف حالت ارائهی نهایی، کار AOT انجام نمیشود. در اینجا AOT به معنای Ahead of time compilation است.
- در هر دو حالت توسعه و ارائهی نهایی کار bundling و دسته بندی فایلها انجام خواهد شد.
به همین جهت است که ng build سریع است؛ اما حجم بالاتری را هم تولید میکند. چون بسیاری از بهینه سازیهای حالت ارائهی نهایی را به همراه ندارد.
دستورات build برای حالت توسعه و ارائهی نهایی
برای حالت توسعه، هر 4 دستور ذیل یک مفهوم را دارند و به همین جهت مورد ng build متداولتر است:
>ng build --target=development --environment=dev >ng build --dev -e=dev >ng build --dev >ng build
برای حالت ارائهی نهایی، هر 3 دستور ذیل یک مفهوم را دارند و به همین جهت مورد ng build --prod متداولتر است:
>ng build --target=production --environment=prod >ng build --prod -e=prod >ng build --prod
همچنین هر کدام از این دستورات را توسط پرچمهای ذیل نیز میتوان سفارشی سازی کرد:
پرچم | مخفف | توضیح |
sourcemap-- | sm- | تولید سورسمپ |
aot-- | Ahead of Time compilation | |
watch-- | w- | تحت نظر قرار دادن فایلها و ساخت مجدد |
environment-- | e- | محیط ساخت |
target-- | t- | نوع ساخت |
dev-- | مخفف نوع ساخت جهت توسعه | |
prod-- | مخفف نوع ساخت جهت ارائه نهایی |
برای مثال در حالت prod، سورسمپها تولید نخواهند شد. اگر علاقمندید تا این فایلها نیز تولید شوند، پرچم souremap را نیز ذکر کنید.
و یا اگر برای حالت dev میخواهید AOT را فعالسازی کنید، پرچم aot-- را در آنجا قید کنید.
یک مثال: ساخت برنامهی مثال قسمت چهارم - تنظیمات مسیریابی در حالت prod
تا اینجا خروجی حالت dev ساخت برنامهی قسمت چهارم را بررسی کردیم. در ادامه دستور ng build --prod را در ریشهی پروژه صادر میکنیم:
D:\Prog\angular-routing>ng build --prod Hash: f5bd7fd555a85af8a86f Time: 39932ms chunk {0} polyfills.18173234f9641113b9fe.bundle.js (polyfills) 158 kB {4} [initial] [rendered] chunk {1} main.c6958def7c5f51c45261.bundle.js (main) 50.3 kB {3} [initial] [rendered] chunk {2} styles.d41d8cd98f00b204e980.bundle.css (styles) 69 bytes {4} [initial] [rendered] chunk {3} vendor.b426ba6883193375121e.bundle.js (vendor) 1.37 MB [initial] [rendered] chunk {4} inline.8cec210370dd3af5f1a0.bundle.js (inline) 0 bytes [entry] [rendered]
همانطور که ملاحظه میکنید، اینبار نه تنها حجم فایلها به میزان قابل ملاحظهای کاهش پیدا کردهاند، بلکه این نامها به همراه یک سری hash هم هستند که کار cache-busting (منقضی کردن کش مرورگر، با ارائهی نگارشی جدید) را انجام میدهند.
در ادامه اگر بخواهیم مجددا برنامهی source-map-explorer را جهت بررسی محتوای فایلهای js اجرا کنیم، به خطای عدم وجود sourcemapها خواهیم رسید (چون در حالت prod، به صورت پیش فرض غیرفعال هستند). به همینجهت برای این مقصود خاص نیاز است از پرچم فعالسازی موقت آن استفاده کرد:
> ng build --prod --sourcemap > source-map-explorer dist/vendor.b426ba6883193375121e.bundle.js
همانطور که در تصویر نیز مشخص است، اینبار کامپایلر Angular به همراه تمام ماژولهایی که در برنامه ارجاعی به آنها وجود نداشتهاست، حذف شدهاند و کل حجم بستهی Angular به 366 KB کاهش یافتهاست.
بررسی دستور ng serve
تا اینجا برای اجرای برنامه در حالت dev از دستور ng serve -o استفاده کردهایم. کار ارائهی برنامه توسط این دستور، از محتوای کامپایل شدهی درون حافظه با مدیریت webpack انجام میشود. به همین جهت بسیار سریع بوده و قابلیت live reload را ارائه میدهد (نمایش آنی تغییرات در مرورگر، با تغییر فایلها).
همانند تمام دستورات دیگر، اطلاعات بیشتری را در مورد این دستور، از طریق راهنمای آن میتوان به دست آورد:
> ng serve --help
که شامل این موارد هستند (علاوه بر تمام مواردی را که در حالت ng build میتوان مشخص کرد؛ مثلا ng serve --prod -o):
پرچم | مخفف | توضیح |
open-- | o- | بازکردن خودکار مرورگر پیش فرض. حالت پیش فرض آن گشودن مرورگر توسط خودتان است و سپس مراجعهی دستی به آدرس برنامه. |
port-- | p- | تغییر پورت پیش فرض مانند ng server -p 8626 |
live-reload-- | lr- |
فعال است مگر اینکه آنرا با false مقدار دهی کنید. |
ssl-- | ارائه به صورت HTTPS | |
proxy-config-- | pc- | Proxy configuration file |
استخراج فایل تنظیمات webpack از Angular CLI
Angular CLI برای مدیریت build، در پشت صحنه از webpack استفاده میکند. فایل تنظیمات آن نیز جزئی از فایلهای توکار این ابزار است و قرار نیست به صورت پیش فرض و مستقیم توسط پروژهی جاری ویرایش شود. به همین جهت آنرا در ساختار پروژهی تولید شده، مشاهده نمیکنید.
اگر علاقمند به سفارشی سازی بیشتر این تنظیمات پیش فرض باشید، ابتدا باید آنرا اصطلاحا eject کنید و سپس میتوان آنرا ویرایش کرد:
> ng eject Ejection was successful. To run your builds, you now need to do the following commands: - "npm run build" to build. - "npm run test" to run unit tests. - "npm start" to serve the app using webpack-dev-server. - "npm run e2e" to run protractor. Running the equivalent CLI commands will result in an error. ============================================ Some packages were added. Please run "npm install".
در این حالت است که فایل webpack.config.js به ریشهی پروژه جهت سفارشی سازی شما اضافه خواهد شد. همچنین فایلهای .angular-cli.json، package.json نیز جهت درج این تغییرات ویرایش میشوند.
و اگر در این لحظه پشیمان شدهاید (!) فقط کافی است تا این مرحلهی جدید commit شدهی به مخزن کد را لغو کنید و باز هم به همان Angular CLI قبلی میرسید.
در کل توصیه من این هست که یک VPS ارزون قیمت ویندوز تهیه کنید. ماهی 10 تومن یا همین حدود میشه. بعد لینکها رو در این VPS دریافت (سرعت پورت یک مگ دارند عموما) و سپس سرفرصت این طرف دریافت کنید.