لینک معرفی پروژه
با توجه به آخرین نگارشهای موجود Angular و React، انتخاب شما برای انجام یک پروژه بزرگ کدام است؟
npm install -g @angular/cli
ng new HowToKeepAngularSizeSmall
ng serve --open --prod
"@agm/core": "^1.0.0-beta.5", "@angular/flex-layout": "^7.0.0-beta.23", "@angular/material": "^7.3.3", "@angular/platform-browser": "~7.2.0", "@asymmetrik/ngx-leaflet": "^5.0.1", "@ngx-loading-bar/router": "1.3.1", "@ngx-translate/core": "^11.0.1", "@ngx-translate/http-loader": "^4.0.0", "@progress/kendo-angular-buttons": "^4.3.3", "@progress/kendo-angular-dateinputs": "^3.6.0", "@progress/kendo-angular-dialog": "^3.10.1", "@progress/kendo-angular-dropdowns": "^3.5.1", "@progress/kendo-angular-excel-export": "^2.3.0", "@progress/kendo-angular-grid": "^3.13.0", "@progress/kendo-angular-inputs": "^4.2.0", "@progress/kendo-angular-intl": "^1.7.0", "@progress/kendo-angular-l10n": "^1.3.0", "@progress/kendo-angular-layout": "^3.2.0", "@progress/kendo-data-query": "^1.5.0", "@progress/kendo-drawing": "^1.5.8", "@progress/kendo-theme-default": "^3.3.1", "@swimlane/ngx-datatable": "^14.0.0", "angular-calendar": "0.23.7", "angular-tree-component": "7.0.1", "bootstrap": "^3.4.0", "chart.js": "2.7.2", "d3": "4.13.0", "dragula": "3.7.2", "hammerjs": "2.0.8", "intl": "1.2.5", "leaflet": "1.3.1", "moment": "2.21.0", "ng2-charts": "1.6.0", "ng2-dragula": "1.5.0", "ng2-file-upload": "1.3.0", "ng2-validation": "4.2.0", "ngx-perfect-scrollbar": "5.3.5", "ngx-quill": "2.2.0", "screenfull": "3.3.2", "font-awesome": "^4.7.0", "jalali-moment": "^3.3.1", "jquery": "^3.3.1", "ng-snotify": "^4.3.1", "normalize.css": "^8.0.1",
@import "~bootstrap/dist/css/bootstrap.css"; @import "~@progress/kendo-theme-default/scss/all"; @import '@angular/material/prebuilt-themes/pink-bluegrey.css'; @import '~perfect-scrollbar/css/perfect-scrollbar.css'; @import "~ng-snotify/styles/material";
"scripts": [ "node_modules/jquery/dist/jquery.js", "node_modules/bootstrap/dist/js/bootstrap.min.js", "node_modules/hammerjs/hammer.min.js" ],
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; // Import Material import { MatFormFieldModule, MatInputModule, MatButtonModule, MatButtonToggleModule, MatDialogModule, MatIconModule, MatSelectModule, MatToolbarModule, MatDatepickerModule, DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatTableModule, MatCheckboxModule, MatRadioModule, MatCardModule, fadeInContent, MatListModule, MatProgressBarModule, MatTabsModule, MatSidenavModule, MatSlideToggleModule, MatMenuModule } from '@angular/material'; // Import kendo angular ui import { ButtonsModule } from '@progress/kendo-angular-buttons'; import { GridModule, ExcelModule } from '@progress/kendo-angular-grid'; import { DropDownsModule } from '@progress/kendo-angular-dropdowns'; import { InputsModule } from '@progress/kendo-angular-inputs'; import { DateInputsModule } from '@progress/kendo-angular-dateinputs'; import { DialogsModule } from '@progress/kendo-angular-dialog'; import { RTL } from '@progress/kendo-angular-l10n'; import { LayoutModule } from '@progress/kendo-angular-layout'; import { WindowService, WindowRef, WindowCloseResult, DialogService, DialogRef, DialogCloseResult } from '@progress/kendo-angular-dialog'; import { SnotifyModule, SnotifyService, SnotifyPosition, SnotifyToastConfig, ToastDefaults } from 'ng-snotify'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, // material MatSidenavModule, MatSlideToggleModule, MatInputModule, MatFormFieldModule, MatButtonModule, MatButtonToggleModule, MatDialogModule, MatIconModule, MatSelectModule, MatToolbarModule, MatDatepickerModule, MatCheckboxModule, MatRadioModule, MatCardModule, MatMenuModule, MatListModule, MatProgressBarModule, MatTabsModule, // kendo-angular ButtonsModule, GridModule, ExcelModule, DropDownsModule, InputsModule, DateInputsModule, DialogsModule, LayoutModule, SnotifyModule, ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
همانطور که میبینید بدون افزودن کامپوننت جدیدی، حجم خروجی از 222KB به 582KB رسیدهاست. معمولا در هر پروژه نیاز به تعدادی دایرکتیو و سرویس پایه نیز میباشد که کم کم به حجم خروجی صفحات میافزاید. در نظر بگیرید که هنوز هیچ قالب خاصی برای صفحه مورد نظرمان استفاده نشده و به حجم 582KB رسیدهایم. برای نمونه میتوانیم سری به سایت madewithangular.com بزنیم و حجم خروجی تعدادی از سایتهای نوشته شدهی با انگیولار را بررسی کنیم. سایتهای زیر خروجی بالای 1.5MB دارند. همچنین سایتی را که خودم تقریبا یک سال پیش شروع کرده بودم، حجم خروجی آن 2.7MB است:
دلیل بالا رفتن حجم خروجی، اضافه شدن فایلهای JavaScript و style-sheet به bundle اصلی پروژه است. برای مثال حجم فایل main.js را در نمونههای ذکر شده بررسی کنید.
در قسمتهای بعدی، به نحوهی کار صحیح با انگیولار میپردازیم و حجم صفحات را بررسی میکنیم.
RxJS اکنون جزئی از پروژههای گوگل است
توسعه دهندهی اصلی RxJS یا همان Ben Lesh اکنون به گوگل پیوستهاست و جزو تیم Angular است. بنابراین در آینده شاهد یکپارچگی بهتر این دو با هم خواهیم بود. البته RxJS هنوز هم به عنوان یک پروژهی مستقل از Angular مدیریت خواهد شد.
آشنایی با تغییرات RxJS 5.5 جهت مهاجرت به RxJS 6.0 ضروری است
در مطلب «کاهش حجم قابل ملاحظهی برنامههای Angular با استفاده از RxJS 5.5» با pipe-able operators آشنا شدیم و این موارد پایههای مهاجرت به RxJS 6.0 هستند. بنابراین پیش از مطالعهی ادامهی بحث نیاز است این مطلب را به خوبی مطالعه و بررسی کنید.
تغییر رفتار خطاهای مدیریت نشده در RxJS 6.0
تا پیش از RxJS 6.0 اگر خطای مدیریت نشدهای رخ میداد، این خطا به صورت synchronous به فراخوان صادر میشد. این رفتار در نگارش 6 تغییر کرده و صدور آن اینبار asynchronous شدهاست.
برای مثال یک چنین کدی تا پیش از RxJS 6.0 کار میکرد:
try { source$.subscribe(nextFn, undefined, completeFn); } catch (err) { handleError(err); }
برای اصلاح این کد در نگارش 6، همان پارامتر دوم متد را مقدار دهی کنید و try/catch را در صورت وجود حذف نمائید.
تغییرات مهم importها در RxJS 6.0
همانطور که در مطلب «کاهش حجم قابل ملاحظهی برنامههای Angular با استفاده از RxJS 5.5» نیز بررسی کردیم، تا نگارش 5 این کتابخانه، importها به صورت زیر بودند:
import 'rxjs/add/operator/map';
import { map } from 'rxjs/operators';
import { timer } from 'rxjs/observable/timer'; import { of } from 'rxjs/observable/of'; import { from } from 'rxjs/observable/from'; import { range } from 'rxjs/observable/range';
import { interval, of } from 'rxjs'; import { filter, mergeMap, scan } from 'rxjs/operators';
البته RxJS 6.0 در کل به همراه 4 گروه کلی importها است که در زیر مشاهده میکنید (در اینجا مواردی که کمتر در برنامههای Angular به صورت مستقیم استفاده میشوند مانند ajax آن و یا webSocket هم قابل مشاهده هستند):
rxjs rxjs/operators rxjs/testing rxjs/webSocket rxjs/ajax
مواردی که از RxJS 6.0 حذف شدهاند
برای کاهش حجم کتابخانهی RxJS و همچنین جلوگیری از بکارگیری متدهایی که نمیبایستی خارج از کدهای اصلی خود RxJS استفاده شوند، تعداد زیادی از متدهای قدیمی آن و روشهای کار پیشین با RxJS حذف شدهاند. برای مثال شما در RxJS 5.5 میتوانید برای کار با عملگر of، یا آنرا از مسیر rxjs/add/observable/of دریافت کنید (همان روش وصله کردن تا پیش از RxJS 5.5) و یا آنرا از مسیر rxjs/observable/of به روش مخصوص ES 6.0 به برنامه اضافه کنید و یا حتی امکان دریافت آن از مسیر rxjs/observable/fromArray نیز میسر است.
در RxJS 6.0 تمام اینها حذف شدهاند و فقط روش زیر باقی ماندهاست:
import { of } from 'rxjs';
معرفی بستهی rxjs-compat
در مطلب «ارتقاء به Angular 6: بررسی تغییرات Angular CLI» روش ارتقاء وابستگیهای پروژه به نگارش 6 را بررسی کردیم. یکی از مراحل آن اجرای دستور زیر بود:
ng update rxjs
پس از آن اگر پروژه را کامپایل کنید، پر خواهد بود از خطاهای rxjs، مانند:
ERROR in node_modules/ng2-slim-loading-bar/src/slim-loading-bar.service.d.ts(1,10): error TS2305: Module '"/node_modules/rxjs/Observable"' has no exported member 'Observable'.
برای رفع این مشکل و ارائهی راهحلی کوتاه مدت، بستهای به نام rxjs-compat ارائه شدهاست که سبب هدایت تعاریف قدیمی به تعاریف جدید میشود و به این ترتیب کدهای کتابخانهی ثالث، بدون مشکل با نگارش 6 نیز قابل استفاده خواهند بود.
برای نصب آن نیاز است دستور زیر را صادر کنید:
npm i rxjs-compat --save
البته دقت داشته باشید از rxjs-compat به عنوان یک راه حل موقت باید استفاده کرد و نیاز است ابتدا کدهای خود را به روش pipe-able operators بازنویسی کنید و مسیرهای importها را اصلاح کنید و در آخر بستههای جدید وابستگیهای ثالث را که از RxJS6 استفاده میکنند، نصب نمائید. در نهایت rxjs-compat را حذف کنید.
خودکار سازی اصلاح importها در برنامههای پیشین، جهت مهاجرت به RxJS 6.0
با توجه به این تغییرات و حذف و اضافه شدنها در نگارش 6، تقریبا دیگر هیچکدام از importهای قبلی شما کار نمیکنند! و اصلاح آنها نیاز به زمان زیادی خواهد داشت. به همین جهت تیم RxJS ابزاری را طراحی کردهاند که با اجرای آن بر روی پروژه، به صورت خودکار تمام importهای قبلی را به نگارش جدید تبدیل میکند. برای اینکار ابتدا ابزار rxjs-tslint را نصب کنید:
npm i -g rxjs-tslint
{ "rulesDirectory": [ "node_modules/rxjs-tslint" ], "rules": { "rxjs-collapse-imports": true, "rxjs-pipeable-operators-only": true, "rxjs-no-static-observable-methods": true, "rxjs-proper-imports": true } }
rxjs-5-to-6-migrate -p src/tsconfig.app.json
البته توصیه شدهاست این ابزار را بیش از یکبار نیاز است اجرا کنید.
خلاصهی روش مهاجرت به RxJS 6x
ابتدا آخرین نگارش rxjs را نصب کنید:
ng update rxjs
npm i rxjs-compat --save
npm i -g rxjs-tslint rxjs-5-to-6-migrate -p src/tsconfig.app.json
یافتن معادلهای جدید دستورات قدیمی
در حین تبدیل کدهای قدیمی به جدید نیاز خواهید داشت تا معادلها را بیابید. برای این منظور به مستندات رسمی این مهاجرت مراجعه کنید:
https://github.com/ReactiveX/rxjs/blob/master/MIGRATION.md
برای مثال در اینجا مشاهده خواهید کرد که معادل Observable.throw حذف شده، اکنون throwError است و همینطور برای مابقی.
یک مثال واقعی تغییر یافته
مخزن کد تمام مثالهای سایت جاری که پیشتر منتشر شدهاند، به نسخهی 6 ارتقاء داده شد. ریز تغییرات RxJS 6.0 آنها را در اینجا میتوانید مشاهده کنید.
At the time of writing this post, default ASP.NET Core SPA templates allow you to create Angular 5 based app with Visual Studio without installing any third-party extensions or templates. Angular 6 is out now and you can also upgrade the Angular 5 app to Angular 6 . What if you want to create Angular 6 app with VS 2017? This post talks about how to create an Angular 6 App with Visual Studio 2017 and how to extend it with a simple example.
یکی دیگر از تغییرات عمدهی ASP.NET Core با نگارشهای قبلی آن، نحوهی مدیریت مسیریابیهای سیستم است. در نگارشهای قبلی مبتنی بر HTTP Moduleها، مسیریابیها توسط یک HTTP Module مخصوص، با pipeline اصلی ASP.NET یکپارچه شدهاند و زمانیکه مسیر درخواستی با تنظیمات سیستم تطابق داشته باشد، پردازش کار به HTTP Handler مخصوص ASP.NET MVC منتقل میشود:
اما در ASP.NET Core مبتنی بر میان افزارها، زیر ساخت مسیریابی به صورت زیر تغییر کردهاست:
میان افزار ASP.NET MVC را که در قسمت قبل فعال کردیم، باید بتواند کنترلر و اکشن متد متناظر با URL درخواستی را مشخص کند. این تصمیم گیری نیز بر اساس تنظیماتی به نام Routing انجام میشود. در قسمت قبل، حالت ساده و پیش فرض این تنظیمات را مورد استفاده قرار دادیم
app.UseMvcWithDefaultRoute();
public static IApplicationBuilder UseMvcWithDefaultRoute(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException("app"); return app.UseMvc((Action<IRouteBuilder>) (routes => routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}"))); }
روش دیگر معرفی این تنظیمات، استفاده از Attribute routing است:
[Route("[controller]/[action]")]
مسیریابیهای قراردادی
در قسمت قبل، یک POCO Controller را به صورت ذیل تعریف کردیم و این کنترلر، بدون تعریف هیچ نوع مسیریابی خاصی در دسترس بود:
namespace Core1RtmEmptyTest.Controllers { public class HomeController { public string Index() { return "Running a POCO controller!"; } } }
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
در این تعاریف، هر کدام از قسمتهای قرارگرفتهی داخل {}، مشخص کنندهی قسمتی از URL دریافتی بوده و نامهای controller و action در اینجا جزو نامهای از پیش مشخص شده هستند و برای نگاشت اطلاعات مورد استفاده قرار میگیرند. برای مثال اگر آدرس home/index/ درخواست شد، برنامه به کلاس HomeController و متد عمومی Index آن هدایت میشود. همچنین قسمت آخر این پردازش به ?id ختم شدهاست. وجود ?، به معنای اختیاری بودن این پارامتر است و اگر در URL ذکر شود، به پارامتر id این اکشن متد، نگاشت خواهد شد. مواردی که پس از = ذکر شدهاند، مقادیر پیش فرض مسیریابی هستند. برای مثال اگر صرفا آدرس home/ درخواست شود، مقدار اکشن متد آن با مقدار پیش فرض index جایگزین خواهد شد و اگر تنها مسیر / درخواست شود، کنترل Home و اکشن متد Index آن پردازش میشوند.
در اینجا به هر تعدادی که نیاز است میتوان متدهای routes.MapRoute را فراخوانی و استفاده کرد؛ اما ترتیب تعریف آنها حائز اهمیت است. هر مسیریابی که در ابتدای لیست اضافه شود، حق تقدم بالاتری خواهد داشت و هر تطابقی با یکی از مسیریابیهای تعریف شده، در همان سطح سبب خاتمهی پردازش سایر مسیریابیها میشود.
استفاده از Attributes برای تعریف مسیریابیها
بجای تعریف قرار دادهای پیش فرض مسیریابی در کلاس آغازین برنامه، میتوان از ویژگی Route نیز استفاده کرد. هرچند روش تعریف مسیریابیهای قراردادی، از نگارشهای آغازین ASP.NET MVC به همراه آن بودهاند، اما با زیاد شدن تعداد کنترلرها و مسیریابیهای سفارشی هر کدام، اینبار با نگاه کردن به یک کنترلر، سریع نمیتوان تشخیص داد که چه مسیریابیهای خاصی به آن مرتبط هستند. برای ساده سازی مدیریت برنامههای بزرگ و ساده سازی تعاریف مسیریابیهای خاص آنها، استفاده از ویژگی Route نیز به ASP.NET MVC اضافه شدهاست.
یک مثال: کنترلر About را درنظر بگیرید:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class AboutController : Controller { public ActionResult Hello() { return Content("Hello from DNT!"); } public ActionResult SiteName() { return Content("DNT"); } } }
اما اگر آدرس /About/ را درخواست دهیم چطور؟ چون در مسیریابی پیش فرض، تعریف {action=Index} را داریم، یعنی هر زمانیکه در URL درخواستی، قسمت action آن ذکر نشد، آنرا با index جایگزین کن و این کنترلر دارای متد Index نیست. در ادامه اگر بخواهیم متد Hello را تبدیل به متد پیش فرض این کنترلر کنیم، میتوان با استفاده از ویژگی Route به صورت ذیل عمل کرد:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { [Route("About")] public class AboutController : Controller { [Route("")] public ActionResult Hello() { return Content("Hello from DNT!"); } [Route("SiteName")] public ActionResult SiteName() { return Content("DNT"); } } }
AmbiguousActionException: Multiple actions matched. The following actions matched route data and had all constraints satisfied: Core1RtmEmptyTest.Controllers.AboutController.Hello (Core1RtmEmptyTest) Core1RtmEmptyTest.Controllers.AboutController.SiteName (Core1RtmEmptyTest)
روش بهتر و refactoring friendly آن نیز به صورت ذیل است:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { [Route("[controller]")] public class AboutController : Controller { [Route("")] public ActionResult Hello() { return Content("Hello from DNT!"); } [Route("[action]")] public ActionResult SiteName() { return Content("DNT"); } } }
یک نکته: در حین تعریف مسیریابی یک کنترلر میتوان پیشوندهایی را نیز ذکر کرد؛ برای مثال:
[Route("api/[controller]")]
تعریف قیود، برای مسیریابیهای تعریف شده
فرض کنید به کنترلر About فوق، اکشن متد ذیل را که یک خروجی JSON را بازگشت میدهد، اضافه کردهایم:
//[Route("/Users/{userid}")] [Route("Users/{userid}")] public IActionResult GetUsers(int userId) { return Json(new { userId = userId }); }
و اگر مسیریابی Users/{userid} را تعریف کنیم، یعنی این مسیریابی پس از ذکر کنترلر about، به عنوان یک اکشن متد آن مفهوم پیدا میکند:
http://localhost:7742/about/users/1
در هر دو حالت، ذکر پارامتر userid الزامی است (چون با ? مشخص نشدهاست)؛ مانند:
[Route("/Users/{userid:int?}")]
[Route("Users/{userid:int}")]
قیودی را که در اینجا میتوان ذکر کرد به شرح زیر هستند:
• alpha - معادل است با (a-z, A-Z).
• bool - برای تطابق با مقادیر بولی.
• datetime - برای تطابق با تاریخ میلادی.
• decimal - برای تطابق با ورودیهای اعشاری.
• double - برای تطابق با اعداد اعشاری 64 بیتی.
• float - برای تطابق با اعداد اعشاری 32 بیتی.
• guid - برای تطابق با GUID ها
• int - برای تطابق با اعداد صحیح 32 بیتی.
• length - برای تعیین طول رشته.
• long - برای تطابق با اعداد صحیح 64 بیتی.
• max - برای ذکر حداکثر مقدار یک عدد صحیح.
• maxlength - جهت ذکر حداکثر طول رشتهی مجاز ورودی.
• min - برای ذکر حداقل مقدار یک عدد صحیح.
• minlength - جهت ذکر حداقل طول رشتهی مجاز ورودی.
• range - ذکر بازهی اعداد صحیح مجاز.
• regex - ذکر یک عبارت با قاعده جهت مشخص سازی الگوی قابل پذیرش.
برای ترکیب چندین قید مختلف نیز میتوان از : استفاده کرد:
[Route("/Users/{userid:int:max(1000):min(10)}")]
ذکر نام Route برای ساده سازی تعریف آدرسی به آن
در حین تعریف یک Route میتوان نام دلخواهی را نیز به آن انتساب داد (همانند نام default مسیریابی ثبت شدهی در کلاس آغازین برنامه):
[Route("/Users/{userid:int}", Name="GetUserById")]
string uri = Url.Link("GetUserById", new { userid = 1 });
مشخص سازی ترتیب پردازش مسیریابیها
ترتیب مسیریابیهای ثبت شدهی در کلاس آغازین برنامه، همان ترتیب افزوده شدن و ذکر آنها است.
در اینجا میتوان از خاصیت order نیز استفاده کرد و اعداد کوچکتر، ابتدا پردازش میشوند (مقدار پیش فرض آن نیز صفر است):
[Route("/Users/{userid:int}", Name = "GetUserById", Order = 1)]
امکان تعریف قیود سفارشی
اگر قیودی که تا اینجا ذکر شدند، برای کار شما مناسب نبودند و نیاز بود تا الگوریتم خاصی را جهت محدود سازی دسترسی به یک مسیریابی خاص پیاده سازی کنید، میتوان به صورت ذیل عمل کرد:
using System; using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace Core1RtmEmptyTest { public class CustomRouteConstraint : IRouteConstraint { public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) { object value; if (!values.TryGetValue(routeKey, out value) || value == null) { return false; } long longValue; if (value is long) { longValue = (long)value; return longValue != 10; } var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue)) { return longValue != 10; } return false; } } }
public class CustomRouteConstraint : IRouteConstraint
در آخر برای ثبت و معرفی آن باید به متد ConfigureServices کلاس آغازین برنامه مراجعه کرد:
public void ConfigureServices(IServiceCollection services) { services.AddRouting(options =>options.ConstraintMap.Add("Custom", typeof(CustomRouteConstraint)));
[Route("/Users/{userid:int:custom}")]
یک نکته: اگر به سورس ASP.NET Core مراجعه کنید ، تمام قیودی را که پیشتر نام بردیم (مانند int، guid و امثال آن) نیز به همین روش تعریف و پیشتر ثبت شدهاند.
معرفی بستهی نیوگت Microsoft.AspNetCore.SpaServices
مسیریابیهای پیش فرض ASP.NET Core با مسیریابیهای برنامههای SPA مانند AngularJS (و امثال آن) تداخل دارند؛ از این جهت که درخواستهای رسیدهی به سرور، ابتدا به موتور پردازشی ASP.NET وارد میشوند و اگر یافت نشدند، کاربر با پیام 404 مواجه خواهد شد و دیگر در اینجا برنامه به مسیریابی خاص مثلا AngularJS 2.0 هدایت نمیشود.
برای این موارد مرسوم است که یک fallback route را در انتهای مسیریابیهای موجود اضافه کنند (به آن catch all هم میگویند)
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "spa-fallback", template: "{*url}", defaults: new { controller = "Home", action = "Index" }); });
برای حل این مشکل مایکروسافت بستهای را به نام Microsoft.AspNetCore.SpaServices ارائه داده است.
برای افزودن آن بر روی گره references کلیک راست کرده و گزینهی manage nuget packages را انتخاب کنید. سپس در برگهی browse آن Microsoft.AspNetCore.SpaServices را جستجو کرده و نصب نمائید:
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{ "dependencies": { //same as before "Microsoft.AspNetCore.SpaServices": "1.0.0-beta-000007" },
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Home", action = "Index" }); });
// Serve wwwroot as root app.UseFileServer(); // Serve /node_modules as a separate root (for packages that use other npm modules client side) app.UseFileServer(new FileServerOptions { // Set root of file server FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "node_modules")), // Only react to requests that match this path RequestPath = "/node_modules", // Don't expose file system EnableDirectoryBrowsing = false });
با کلیک بر روی لینک منوی نمایش لیست محصولات، ابتدا قاب خالی لیست محصولات نمایش داده میشود:
سپس بعد از یک ثانیه، شاهد بارگذاری اطلاعات جدول لیست محصولات خواهید بود. این یک ثانیه تاخیر را نیز به عمد توسط منبع داده درون حافظهای برنامه ایجاد کردیم، تا بتوان شرایط دنیای واقعی را شبیه سازی کرد:
InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }),
ارسال اطلاعات ثابت به مسیرهای مختلف برنامه
روشهای متعددی برای ارسال اطلاعات به مسیرهای مختلف برنامه وجود دارند که تعدادی از آنها را مانند پارامترهای اختیاری، پارامترهای اجباری و پارامترهای کوئری، در قسمت قبل بررسی کردیم. روش دیگری را که در اینجا میتوان بکار برد، استفاده از خاصیت data تعاریف مسیریابی برنامه است:
{ path: 'products', component: ProductListComponent, data: { pageTitle: 'Product List'} },
برای خواندن این اطلاعات ثابت میتوان از شیء route.snapshot سرویس ActivatedRoute استفاده کرد:
this.pageTitle = this.route.snapshot.data['pageTitle'];
پیش بارگذاری اطلاعات پویای مسیرهای مختلف برنامه
زمانیکه به صفحهی جزئیات یک محصول مراجعه میکنیم، ابتدا این کامپوننت آغاز شده و قالب آن نمایش داده میشود. سپس در متد ngOnInit آن کار درخواست اطلاعات از سرور و نمایش آن صورت خواهد گرفت. در این بین، چون زمانی بین درخواست اطلاعات از سرور و دریافت آن صرف میشود، کاربر ابتدا شاهد قالب خالی کامپوننت، به همراه برچسبهای مختلف آن خواهد بود که فاقد اطلاعات هستند و پس از مدتی این اطلاعات نمایش داده میشوند.
برای حل این مشکل از سرویسی به نام Route Resolver استفاده میشود. در این حالت زمانیکه کاربر صفحهی جزئیات یک محصول را درخواست میکند، ابتدا مسیریابی آن فعال شده و سپس سرویس Route Resolver اجرا میشود که کار آن درخواست اطلاعات از وب سرور است. در این حالت پس از دریافت اطلاعات از سرور، کار فعالسازی کامپوننت صورت میگیرد. به این ترتیب قالب کاملا آمادهی کامپوننت، به همراه اطلاعات مرتبط با آن، به کاربر نمایش داده خواهد شد.
بدون استفادهی از Route Resolver، کامپوننت کلاس، پس از آغاز آن، اطلاعات را دریافت میکند. اما با بکارگیری Route Resolver، این سرویس ویژهاست که پیش از هر مرحلهی دیگری اطلاعات را دریافت میکند.
پیاده سازی یک Route Resolver شامل سه مرحلهاست:
الف) ایجاد و ثبت سرویس Route Resolver
ب) معرفی Route Resolver به تنظیمات مسیریابی
ج) خواندن اطلاعات دریافتی توسط Route Resolver به کمک سرویس ActivatedRoute
ایجاد سرویس Route Resolver
یک Route Resolver به صورت یک سرویس جدید ایجاد میشود:
> ng g s product/ProductResolver -m product/product.module installing service create src\app\product\product-resolver.service.spec.ts create src\app\product\product-resolver.service.ts update src\app\product\product.module.ts
providers: [ProductService, ProductResolverService]
فایل src\app\product\product-resolver.service.ts را به نحو ذیل تکمیل کنید:
import { Injectable } from '@angular/core'; import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/map'; import { ProductService } from './product.service'; import { IProduct } from './iproduct'; @Injectable() export class ProductResolverService implements Resolve<IProduct> { constructor(private productService: ProductService, private router: Router) { } resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IProduct> { let id = route.params['id']; if (isNaN(id)) { console.log(`Product id was not a number: ${id}`); this.router.navigate(['/products']); return Observable.of(null); } return this.productService.getProduct(+id) .map(product => { if (product) { return product; } console.log(`Product was not found: ${id}`); this.router.navigate(['/products']); return null; }) .catch(error => { console.log(`Retrieval error: ${error}`); this.router.navigate(['/products']); return Observable.of(null); }); } }
مرحلهی اول تعریف یک سرویس Route Resolver، پیاده سازی اینترفیس جنریک Resolve است:
export class ProductResolverService implements Resolve<IProduct> {
این اینترفیس پیاده سازی متد resolve را با امضایی که مشاهده میکنید، درخواست میکند:
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IProduct> {
RouterStateSnapshot وضعیت مسیریاب را در این لحظه در اختیار این سرویس قرار میدهد.
خروجی این متد یک Observable است؛ از نوع اطلاعاتی که دریافت میکند. زمانیکه مسیریابی فعال میشود، متد resolve را فراخوانی کرده و منتظر پایان کار Observable آن میشود. پس از آن است که کامپوننت این مسیریابی را فعالسازی خواهد کرد.
در پیاده سازی متد resolve، تعدادی اعتبارسنجی اطلاعات را نیز مشاهده میکنید. برای مثال اگر id وارد شده، عددی نباشد، در اینجا فرصت خواهیم داشت پیش از فعالسازی کامپوننت نمایش جزئیات یک محصول، کاربر را به صفحهای دیگر هدایت کنیم.
پس از آن نیاز به دریافت اطلاعات محصول درخواست شده، از REST Web API برنامه است. به همین جهت سرویس ProductService را که در قسمت قبل معرفی کردیم، به سازندهی کلاس تزریق کردهایم تا از طریق متد getProduct آن، کار دریافت اطلاعات یک محصول را انجام دهیم.
در اینجا متد getProduct(+id) به همراه عملگر + است تا id دریافتی را به عدد تبدیل کند. سپس بر روی این متد، عملگر map فراخوانی شدهاست. به این ترتیب میتوان به اطلاعات دریافتی از سرور، پیش از بازگشت آن به فراخوان متد resolve، دسترسی یافت. به این ترتیب در اینجا نیز میتوان یک سری اعتبارسنجی را انجام داد. برای مثال آیا id دریافتی، متناظر با محصولی در سمت سرور است یا خیر؟
map operator خروجی را به صورت یک observable بازگشت میدهد. به همین جهت در اینجا نیازی به ذکر return Observable.of نیست.
معرفی Route Resolver به تنظیمات مسیریابی
بعد از پیاده سازی سرویس Route Resolver، نیاز است آنرا به تنظیمات مسیریابی برنامه اضافه کنیم. به همین جهت فایل src\app\product\product-routing.module.ts را گشوده و تنظیمات آنرا به شکل زیر تغییر دهید:
import { ProductResolverService } from './product-resolver.service'; const routes: Routes = [ { path: 'products', component: ProductListComponent }, { path: 'products/:id', component: ProductDetailComponent, resolve: { product: ProductResolverService } }, { path: 'products/:id/edit', component: ProductEditComponent, resolve: { product: ProductResolverService } } ];
در اینجا هر تعداد Route Resolver مورد نیاز را میتوان تعریف کرد. برای مثال اگر مسیریابی خاصی، اطلاعات دیگری را نیز از سرویس خاصی دریافت میکند، میتوان یک جفت کلید/مقدار دیگر را نیز برای آن تعریف کرد. فقط باید دقت داشت که keyها باید منحصربفرد باشند.
به این ترتیب اطمینان حاصل خوهیم کرد که اطلاعات مورد نیاز این مسیریابیها، پیش از فعالسازی کامپوننت آنها، از REST Web API برنامه دریافت میشوند.
خواندن اطلاعات دریافتی توسط Route Resolver به کمک سرویس ActivatedRoute
پس از تعریف سرویس Route Resolver سفارشی خود و معرفی آن به تنظیمات مسیریابی برنامه، قسمت نهایی این عملیات، خواندن این اطلاعات پیش واکشی شدهاست. به همین جهت فایل src\app\product\product-detail\product-detail.component.ts را گشوده و محتوای آنرا به نحو ذیل اصلاح کنید:
constructor(private route: ActivatedRoute) { } ngOnInit(): void { this.product = this.route.snapshot.data['product']; }
- همانطور که مشاهده میکنید، دیگر در این کامپوننت نیازی به تزریق سرویس ProductService نبوده و قسمت دریافت اطلاعات آن از طریق این سرویس، حذف شدهاست.
برای آزمایش آن، لیست محصولات را مشاهده کرده و سپس بر روی لینک مشاهدهی جزئیات یک محصول کلیک کنید. البته در اینجا چون هنوز Route Resolver ایی را برای پیش دریافت لیست محصولات ایجاد نکردهایم، ابتدا قاب خالی لیست محصولات نمایش داده میشود و سپس لیست محصولات. اما دیگر صفحهی نمایش جزئیات یک محصول، این چنین نیست. ابتدا یک وقفهی یک ثانیهای را حس خواهید کرد و سپس صفحهی کامل جزئیات یک محصول نمایان میشود.
یک نکته: اگر یک سرویس Route Resolver، در دو کامپوننت مختلف استفاده شود، اطلاعات آن، بین این دو کامپوننت به اشتراک گذاشته خواهد شد.
مرحلهی بعد، ویرایش فایل src\app\product\product-edit\product-edit.component.ts است تا کامپوننت ویرایش جزئیات اطلاعات نیز بتواند از قابلیت پیش واکشی اطلاعات استفاده کند. در اینجا هنوز نیاز به سرویس ProductService است تا بتوان اطلاعات را ذخیره و یا حذف کرد. تنها قسمتی که باید تغییر کند، حذف متد getProduct و تغییر متد ngOnInit است:
ngOnInit(): void { this.route.data.subscribe(data => { this.onProductRetrieved(data['product']); }); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-03.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
[BreezeController] public class AccountController : ApiController { ... }
module Interfaces { export interface IAuthService { user: Models.IUserToken getUserInfo(accessToken); login(data); logOut(); register(data); changePassword(data); accessToken(accessToken, data); } }
سرویس AuthService پیاده سازی اینترفیس IAuthService را برعهده دارد. در سازنده آن، وابستگیهای آن مقداردهی شدهاست و همچنین تنظیمات manager را انجام دادهایم.
"grant_type=password & username=myusername & password=mypassword";
var ajaxAdapter = breeze.config.getAdapterInstance("ajax"); breeze.ajaxpost(ajaxAdapter);
.withParameters({ $method: 'POST', $encoding: 'JSON', $data: newData }
module AdApps { var securityUrls = { site: '/', login: '/token', logout: 'logout', register: 'register', userInfo: 'getUserInfo', changePassword: 'changePassword', } export class AuthService implements Interfaces.IAuthService { private manager: breeze.EntityManager; constructor( private _breeze: typeof breeze, private $http: ng.IHttpProvider, private toaster: ngtoaster.IToasterService, private $location: ng.ILocationService) { var dataService = new _breeze.DataService({ serviceName: "/breeze/Account", hasServerMetadata: false }); var metadataStore = new _breeze.MetadataStore({ namingConvention: _breeze.NamingConvention.camelCase }); this.manager = new _breeze.EntityManager({ dataService: dataService, metadataStore: metadataStore, saveOptions: new _breeze.SaveOptions({ allowConcurrentSaves: true, tag: [{}] }) }); } user: Models.IUserToken; accessToken(accessToken, data): string { if (accessToken === 'clear') { localStorage.removeItem('accessToken'); delete this.$http.defaults.headers.common.Authorization; } else { window.localStorage.setItem("accessToken", accessToken); this.$http.defaults.headers.common.Authorization = 'Bearer ' + accessToken; } return accessToken; } getUserInfo(): ng.IPromise<any> { var query = this._breeze.EntityQuery.from(securityUrls.userInfo); return this.manager.executeQuery(query).then(data => { return data.results[0]; }); } login(data: any): ng.IPromise<any> { var newData = "grant_type=password&username=" + data.userName + "&password=" + data.password; var query = this._breeze.EntityQuery.from(securityUrls.login) .withParameters({ $method: 'POST', $encoding: 'JSON', $data: newData }); return this.manager.executeQuery(query).then(data => { var self = this; var result = data.results[0] as any; self.accessToken(result.access_token, data.results[0]); self.user = <Models.IUserToken>{}; self.user = <Models.IUserToken>result; return result; }); } logOut(): ng.IPromise<any> { var query = this._breeze.EntityQuery.from(securityUrls.logout) .withParameters({ $method: 'POST', $encoding: 'JSON', }); return this.manager.executeQuery(query).then(data => { this.user = null; this.accessToken('clear', null); this.$location.path("/"); }); } register(data: Object): ng.IPromise<any> { var query = this._breeze.EntityQuery.from(securityUrls.register) .withParameters({ $method: 'POST', $encoding: 'JSON', $data: data }); return this.manager.executeQuery(query).then(data => { }); } changePassword(data: Object): ng.IPromise<any> { var query = this._breeze.EntityQuery.from(securityUrls.changePassword) .withParameters({ $method: 'POST', $encoding: 'JSON', $data: data }); return this.manager.executeQuery(query).then(data => { }); } } }
سرویس HttpInterceptor : رهگیری و پیگیری کردن نتیجه درخواستهای http را بر عهده دارد.
module AdApps { export class HttpInterceptor { private static _toaster: ngtoaster.IToasterService; private static _$q: ng.IQService; constructor( private $q: ng.IQService, private toaster: ngtoaster.IToasterService, private $location: ng.ILocationService) { HttpInterceptor._toaster = toaster; HttpInterceptor._$q = $q; } request(config): string { config.headers = config.headers || {}; var authData = window.localStorage.getItem("accessToken"); if (authData) { config.headers.Authorization = "Bearer " + authData; } return config; }; response(response): ng.IPromise<any> { if (response.data && response.data.message && response.status === 200) { HttpInterceptor._toaster.success(response.data.message) } return HttpInterceptor._$q.resolve(response); }; responseError(response): ng.IPromise<any> { var self = this; var data = response.data; var title = "خطا"; var messages = []; if (data) { if (data.error) { title = data.error; } if (data.message) { messages.push(data.message); } if (data.Message) { messages.push(data.Message); } if (data.ModelState) { angular.forEach(data.ModelState, function (errors, key) { if (key.substr(0, 1) != "$") { messages.push(errors); } }); } if (data.exceptionMessage) { messages.push(data.exceptionMessage); } if (data.ExceptionMessage) { messages.push(data.ExceptionMessage); } if (data.error_description) { messages.push(data.error_description); } if (messages.length > 0) { HttpInterceptor._toaster.error(title, messages.join("<br/>")); } if (response.status === "401") { self.$location.path("/ورود"); } } return HttpInterceptor._$q.reject(response); } } }
معرفی کردن مسیرهای ورود، ثبت نام و تغییر رمز عبور به انگولار
module AdApps { class SecurityCtrl { constructor(private $scope: Interfaces.IAuthScope, private authService: AuthService) { $scope.authService = authService; if (window.localStorage.getItem("accessToken") != null) { authService.getUserInfo().then(function (data) { $scope.authService.user = data; }); } $scope.logOut = function () { return authService.logOut().then(function () { }); } } } define(["angularAmd", "angular", "factory/AuthService", "factory/httpInterceptor"], (angularAmd, ng) => { angularAmd = angularAmd.__proto__; var app = ng.module("AngularTypeScript", ['ngRoute', 'breeze.angular', 'toaster']); var viewPath = "app/views/"; var controllerPath = "app/controller/"; app.config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) { $httpProvider.interceptors.push("HttpInterceptor"); $routeProvider .when("/", angularAmd.route({ templateUrl: viewPath + "home.html", controllerUrl: controllerPath + "home.js" })) .when("/login", angularAmd.route({ templateUrl: viewPath + "login.html", controllerUrl: controllerPath + "login.js" })) .when("/register", angularAmd.route({ templateUrl: viewPath + "register.html", controllerUrl: controllerPath + "register.js" })) .when("/changePassword", angularAmd.route({ templateUrl: viewPath + "change-password.html", controllerUrl: controllerPath + "changePassword.js" })) .otherwise({ redirectTo: '/' }); } ]); app.service('AuthService', ['breeze', '$http', 'toaster', '$location', AuthService]); app.service("HttpInterceptor", ["$q", "toaster", "$location", HttpInterceptor]); app.controller('SecurityCtrl', ['$scope', 'AuthService', SecurityCtrl]); return angularAmd.bootstrap(app); }); }
ایجاد کنترلر .login.ts و ارسال سرویسهای لازم به کلاس LoginCtrl
در صورت صحیح بودن نام کاربری و رمز عبور به صفحه اصلی هدایت خواهد شد.
module AdApps { define(['app'], function (app) { app.controller('LoginCtrl', ["$scope", "AuthService", "$location", LoginCtrl]); }); export class LoginCtrl { constructor($scope: Interfaces.ILoginScope, authService: AuthService, $location: ng.ILocationService) { $scope.submit = function () { authService.login(angular.copy($scope.form)) .then(function (data) { this.$location.path("/"); }) }; } } }
ایجاد login.html
<div ng-controller="LoginCtrl"> <div> <i></i> <span>ورود</span> <div> <div> </div> </div> </div> <div> <div> <div> <form name="Form" id="form1"> <fieldset> <div> <div> <input name="username" ng-model="form.userName" placeholder="نام کاربری" required> <span> <i></i> </span> </div> </div> <div> <div> <input name="password" type="password" ng-model="form.password" placeholder="{{'Password'}}" validator="required"> <span> <i></i> </span> </div> </div> </fieldset> <div> <button type="submit" ng-click="submit()">ورود</button> </div> </form> </div> </div> </div> </div>
requirejs.config({ paths: { "app": "app", "angularAmd": "/Scripts/angularAmd", "angular": "/Scripts/angular", "breezeAjaxpost": "/Scripts/breeze/breeze.ajaxpost", "breeze": "/Scripts/breeze/breeze.debug", "breezeAngular": "/Scripts/breeze/breeze.angular", "bootstrap": "/Scripts/bootstrap", "angularRoute": "/Scripts/angular-route", "jquery": "/Scripts/jquery-2.2.2", "entityManagerService": "factory/entityManagerService", "toaster": "/Scripts/toaster", }, waitSeconds: 0, shim: { "angular": { exports: "angular" }, "angularRoute": { deps: ["angular"] }, "bootstrap": { deps: ["jquery"] }, "breeze": { deps: ["jquery"] }, "breezeAngular": { deps: ["angular", "breeze"] }, "toaster": { deps: ["angular"] }, "app": { deps: ["bootstrap", "angularRoute", "toaster", "breezeAngular", "breezeAjaxpost"] } } }); require(["app"]);