معماری پیازی توسط جفری پالرمو در سال 2008 ابداع شد. این معماری راه بهتری را برای ساخت برنامههای کاربردی جهت تست پذیری، نگهداری و قابلیت اطمینان بهتر بر روی زیرساختهایی مانند پایگاههای داده و خدمات ارائه میدهد. هدف اصلی این معماری، پرداختن به چالشهای پیش روی معماری 3 لایه و ارائه راه حلی برای مشکلات رایج مانند اتصال و جداسازی وابستگیها است. دو نوع اتصال وجود دارند؛ اتصال محکم و اتصال ضعیف که در ادامه آنها را بررسی میکنیم.
اتصال محکم
هنگامی که یک کلاس، به یک وابستگی مشخصی وابسته است، گفته میشود که به شدت با آن کلاس همراه است. یک اتصال محکم جفت شده، به یک شیء دیگر وابسته است. این بدان معناست که تغییر یک شیء در یک برنامهی با اتصال محکم جفت شده، اغلب نیاز به تغییر در تعدادی از اشیاء دیگر دارد. هنگامیکه یک برنامه کوچک است، دشوار نیست، اما در یک برنامهی بزرگ، ایجاد تغییرات بسیار دشوار است.
اتصال ضعیف
یعنی دو شیء مستقل هستند و یک شیء میتواند بدون اینکه به آن وابسته باشد، از شیء دیگری استفاده کند. این یک هدف طراحی است که به دنبال کاهش وابستگیهای متقابل بین اجزای یک سیستم، با هدف کاهش خطر این است که تغییرات در یک جزء، مستلزم تغییر در هر جزء دیگر باشد.
مزایای معماری پیازی
چندین مزیت برای معماری پیازی وجود دارند که در زیر ذکر شدهاند:
- قابلیت نگهداری بهتری را فراهم میکند؛ زیرا همه کدها به لایهها یا مرکز، بستگی دارند.
- تست پذیری بهتری را فراهم میکند؛ زیرا آزمون واحد را میتوان برای لایههای جداگانه، بدون تأثیر بر سایر ماژولهای برنامه ایجاد کرد.
- این برنامه یک برنامهی کاربردی با اتصال آزاد را ایجاد میکند؛ زیرا لایه بیرونی برنامه، همیشه از طریق واسطها با لایه داخلی، ارتباط برقرار میکند.
- هرگونه پیاده سازی پیوسته، در زمان اجرا به برنامه ارائه میشود.
- موجودیتهای دامنه، هسته و بخش مرکزی هستند. میتواند به هر دو لایه پایگاه داده و UI دسترسی داشته باشد.
- لایههای داخلی هرگز به لایه خارجی وابسته نیستند. کدی که ممکن است تغییر کرده باشد، باید بخشی از یک لایه خارجی باشد.
لایههای معماری پیاز
این معماری به شدت به اصل وارونگی وابستگی، متکی است. رابط کاربری از طریق واسطها با منطق تجاری ارتباط برقرار میکند و دارای چهار لایه است. لایهها به سمت مرکز هستند. بخش مرکزی، موجودیتهای Domain است که نشاندهنده موضوعات تجاری و رفتاری است. این لایهها میتوانند متفاوت باشند اما لایه موجودیتهای دامنه، همیشه بخشی از دامنهی مرکزی است. لایه دیگر، رفتار بیشتر یک شیء را تعریف میکند. در ادامه به توضیح لایههای معماری پیاز توجه فرمایید:
Domain Entities Layer
این بخش مرکزی معماری است. تمام اشیاء دامنهی برنامه را در خود نگه میدارد. اگر برنامه ای با چهارچوب موجودیت ORM توسعه داده شود، این لایه دارای کلاسهای POCO (Code First) یا Edmx (Database First) با موجودیتها است. این نهادهای دامنه هیچ وابستگی ندارند.
Repository Layer
این لایه برای ایجاد یک لایه Abstraction بین لایه نهادهای دامنه و لایه منطق تجاری یک برنامه، در نظر گرفته شدهاست. این یک الگوی دسترسی به دادهاست که باعث میشود یک رویکرد مرتبطتر برای دسترسی به دادهها وجود داشته باشد. ما یک مخزن عمومی را ایجاد میکنیم که منبع داده را برای دادهها جستجو میکند، دادهها را از منبع داده به یک نهاد تجاری نگاشت میکند و تغییرات موجودیت تجاری را به منبع داده ارائه میدهد.
Service Layer
این لایه دارای رابطهایی است که برای برقراری ارتباط بین لایه UI و لایه مخزن استفاده میشود و به همراه منطق تجاری برای یک موجودیت است. بنابراین به آن لایه منطق تجاری نیز میگویند.
UI Layer
خارجیترین لایه است و میتواند برنامهی وب، Web API یا پروژه واحد تست باشد. این لایه دارای یک پیاده سازی از جنس Dependency Inversion Principle است بطوری که برنامه، یک برنامهی کاربردی جفت شدهی آزاد میسازد و از طریق واسطها با لایه داخلی ارتباط برقرار میکند.
در مطالب بعدی با مبحث معماری پیازی، نکات تکمیلی و مهمتری از لایه پیاز را تشریح میکنیم و یک پروژه را با معماری پیاز، راه اندازی میکنیم.
نمایش قسمتی از صفحه بر اساس وضعیت اعتبارسنجی کاربر
فرض کنید میخواهیم در کامپوننت Shared\LoginDisplay.razor که در قسمت قبل آنرا اضافه کردیم، لینکهای ثبت نام و لاگین را به کاربران غیر اعتبارسنجی شده (هنوز لاگین نکرده) نمایش دهیم و اگر کاربر، اعتبارسنجی شده بود (لاگین کرده بود)، لینک خروج را به او نمایش دهیم. برای این منظور کامپوننت Shared\LoginDisplay.razor را به صورت زیر تغییر میدهیم:
<AuthorizeView> <Authorized> <a href="Identity/Account/Logout">Logout</a> </Authorized> <NotAuthorized> <a href="Identity/Account/Register">Register</a> <a href="Identity/Account/Login">Login</a> </NotAuthorized> </AuthorizeView>
البته اگر برنامه را در همین حالت اجرا کنیم، به استثنای زیر خواهیم رسید:
InvalidOperationException: Authorization requires a cascading parameter of type Task<AuthenticationState>. Consider using CascadingAuthenticationState to supply this. Microsoft.AspNetCore.Components.Authorization.AuthorizeViewCore.OnParametersSetAsync()
بنابراین به فایل BlazorServer.App\App.razor که محل تعریف ریشهی مسیریابی برنامهاست، مراجعه کرده و کامپوننت آنرا با کامپوننت توکار CascadingAuthenticationState محصور میکنیم:
<CascadingAuthenticationState> <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> </CascadingAuthenticationState>
اکنون اگر برنامه را اجرا کنیم، مشاهده خواهیم کرد که در اولین بار مراجعهی به آن (پیش از لاگین)، لینک به صفحهی خروج، نمایش داده نشدهاست؛ چون آنرا در فرگمنت مخصوص Authorized قرار دادیم:
آزمایش نمایش منوی خروج برنامه
برای آزمایش برنامه، نیاز است ابتدا یک کاربر جدید را ثبت کنیم؛ چون هنوز هیچ کاربری در آن ثبت نشدهاست و همچنین کاربر پیشفرضی را هم به همراه ندارد. در مورد روش ثبت کاربران پیشفرض ASP.NET Core Identity، میتوانید به مطلب «بازنویسی متد مقدار دهی اولیهی کاربر ادمین در ASP.NET Core Identity توسط متد HasData در EF Core» مراجعه کنید و تمام نکات آن، در اینجا هم صادق است (چون پایهی سیستم Identity مورد استفاده، یکی است و هدف ما در اینجا بیشتر بررسی نکات یکپارچه سازی آن با Blazor Server است و نه مرور تمام نکات ریز Identity).
بنابراین ابتدا از منوی بالای صفحه، گزینهی Register را انتخاب کرده و کاربری را ثبت میکنیم. پس از ثبت نام، بلافاصله به منوی جدید زیر میرسیم که در آن گزینههای ورود و ثبت نام، مخفی شدهاند و اکنون گزینهی خروج از سیستم را نمایش میدهد:
بهبود تجربهی کاربری خروج از سیستم
در همین حال که گزینهی خروج نمایش داده شدهاست، اگر بر روی لینک آن کلیک کنیم، ابتدا ما را به صفحهی مجزای logout هدایت میکند. سپس باید در این صفحه، مجددا بر روی لینک logout بالای آن کلیک کنیم. زمانیکه اینکار را انجام دادیم، اکنون صفحهی دیگری را نمایش میدهد که به همراه پیام «خروج موفقیت آمیز از سیستم» است! در این پروسه، کاربر احساس میکند که کاملا از برنامهی اصلی خارج شدهاست و همچنین مراحل طولانی را نیز باید طی کند.
مدیریت این مراحل توسط دو فایل زیر انجام میشوند:
Areas\Identity\Pages\Account\Logout.cshtml
Areas\Identity\Pages\Account\Logout.cshtml.cs
میخواهیم کدهای این دو فایل را به نحوی تغییر دهیم که اگر کاربری بر روی لینک logout برنامهی اصلی کلیک کرد، به صورت خودکار logout شده و سپس مجددا به صفحهی اصلی برنامهی Blazor Server هدایت شود و مجبور نباشد تا مراحل طولانی یاد شده را تکرار کند.
به همین جهت ابتدا فایل Logout.cshtml.cs را حذف میکنیم؛ چون نیازی به آن نداریم. سپس محتوای فایل Logout.cshtml را به صورت زیر تغییر میدهیم:
@page @using Microsoft.AspNetCore.Identity @inject SignInManager<IdentityUser> SignInManager @functions { public async Task<IActionResult> OnGet() { if (SignInManager.IsSignedIn(User)) { <p>You have successfully logged out of the application.</p> await SignInManager.SignOutAsync(); } return Redirect("~/"); } }
نمایش User Claims، در یک برنامهی Blazor Server
سیستم ASP.NET Core Identity، بر اساس User Claims کار میکند؛ اطلاعات بیشتر. پس از استفاده از CascadingAuthenticationState در بالاترین سطح برنامه، اطلاعات آن در سراسر برنامهی Blazor Server هم قابل دسترسی است. برای مثال در کامپوننت Shared\LoginDisplay.razor، به نحو زیر میتوان نام کاربر ثبت نام شده را که یکی از User Claims او است، نمایش داد:
<AuthorizeView> <Authorized> Hello, @context.User.Identity.Name <a href="Identity/Account/Logout">Logout</a> </Authorized>
محدود کردن دسترسی به صفحات برنامه تنها برای کاربران اعتبارسنجی شده
پس از لاگین موفق به سیستم، اکنون میخواهیم دسترسی به صفحات تعریف اتاقها و یا امکانات رفاهی هتل را تنها به کاربران لاگین شده، محدود کنیم. برای اینکار تنها کافی است از ویژگی Authorize استفاده کنیم. برای مثال به کامپوننت Pages\HotelRoom\HotelRoomList.razor مراجعه کرده و یک سطر زیر را به آن اضافه میکنیم:
@attribute [Authorize]
مشکل! با اینکه تمام کامپوننتهای مثال جاری را به ویژگی Authorize مزین کردهایم، اما ... کار نمیکند! و هنوز هم میتوان بدون لاگین به سیستم، به محتوای آنها دسترسی داشت.
برای رفع این مشکل، مجددا نیاز است کامپوننت BlazorServer.App\App.razor را ویرایش کرد:
<CascadingAuthenticationState> <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Found Context="routeData"> @*<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />*@ <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> <NotAuthorized> <p>Sorry, you do not have access to this page</p> </NotAuthorized> </AuthorizeRouteView> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> </CascadingAuthenticationState>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-22.zip
برای شناسایی ایرادات در کد و بهبود کیفیت کدها میتوانید از ابزارهای دستهی lint استفاده کنید که تعدادی از معروفترین این ابزارها (jslint ,cpplint ,eslint ,nodelint و ...) هستند.
برای نصب چنین ابزاری مثل lint میتوانید به شکل زیر آن را نصب کنید:
npm install -g eslint
برای اولین بار نیاز است که آماده سازی ابتدایی برای این کتابخانه صورت بگیرد که با دستور زیر قابل انجام است:
eslint --init
? How would you like to configure ESLint? Answer questions about your style ? Are you using ECMAScript 6 features? Yes ? Are you using ES6 modules? Yes ? Where will your code run? Browser, Node ? Do you use CommonJS? No ? Do you use JSX? No ? What style of indentation do you use? Spaces ? What quotes do you use for strings? Double ? What line endings do you use? Windows ? Do you require semicolons? Yes ? What format do you want your config file to be in? JSON Successfully created .eslintrc.json file in D:\ali\electron
eslint . یا eslint index.js
میتوانید این دستور را در در خصوصیت test، در بخش اسکریپت، به شکل زیر وارد کنید:
{ "name": "electron", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "electron .", "test":"eslint ." }, "author": "", "license": "ISC" }
npm test
D:\ali\electron\index.js 1:26 error Strings must use doublequote quotes 16:8 error Strings must use doublequote quotes 19:3 error Expected indentation of 4 space characters but found 2 indent 22:3 error Expected indentation of 4 space characters but found 2 indent ✖ 4 problems (4 errors, 0 warnings)
برای نامگذاری فایلهای js، به جای استفاده از _ از - استفاده کنید. این قاعدهای است که گیت هاب در نامگذاری فایلهای js انجام میدهد. یعنی به جای عبارت dotnet_tips.js از عبارت dotnet-tips.js استفاده کنید. خاطرتان جمع باشد که هیچ فایل جاوااسکرپیتی رسمی در الکترون، خارج از این قاعده نامگذاری نشده است.
سعی کنید از جدیدترین قواعد موجود در ES6 استفاده کنید مانند:
let برای تعریف متغیرها
const برای تعریف ثابت ها
توابع جهتی به جای نوشتن عبارت function
استفاده از template stringها به جای چسباندن رشتهها از طریق عملگر +
برای نامگذاری متغیرها از همان قوانین تعریف شده برای node.js استفاده کنید که شامل موارد زیر است:
- موقعی که یک ماژول را به عنوان یک کلاس استفاده میکنید از متد CamelCase استفاده کنید مثل BrowserWindow
- موقعی که از یک ماژول که حاوی مجموعهای از دستورات و apiها است استفاده میکنید، از قانون mixedCase استفاده کنید مثل app یا globalShortcut
- موقعی که یک api به عنوان خصوصیت یک شیء مورد استفاده قرار میگیرد، از mixedCase استفاده کنید؛ به عنوان مثال win.webContents یا fs.readFileSync
- برای مابقی اشیا مثل دستورات process و یا تگ webview که به شکل متفاوتتری مورد استفاده قرار میگیرند، از همان عنوانهای خودشان استفاده میکنیم.
موقعی که api جدیدی را میسازید و قصد تعریف متدی را دارید بهتر است دو متد برای get و set تعریف شود، نسبت به حالتی که در کتابخانه جی کوئری مورد استفاده قرار میگیرد یعنی عبارتهای زیر
setText('test'); getText();
.text([text]);
افزودن strong typing به کامپوننت نمایش لیست محصولات
یکی از مزایای کار با TypeScript امکان انتساب نوعهای مشخص یا سفارشی، به متغیرها و اشیاء تعریف شدهاست. برای مثال تاکنون هر خاصیت تعریف شدهای دارای نوع است. اما هنوز نوعی را برای آرایهی محصولات تعریف نکردهایم و نوع آن، نوع پیش فرض any است که برخلاف رویهی متداول کار با TypeScript است.
برای تعریف نوعهای سفارشی میتوان از اینترفیسهای TypeScript استفاده کرد. یک اینترفیس، قراردادی است که نحوهی تعریف تعدادی خاصیت و متد به هم مرتبط را مشخص میکند. سپس کلاسهای مختلف میتوانند با پیاده سازی این اینترفیس، قرارداد تعریف شدهی در آن را عملی کنند. همچنین از اینترفیسها میتوان به عنوان یک data type جدید نیز استفاده کرد. البته ES 5 و ES 6 از اینترفیسها پشتیبانی نمیکنند و تعریف آنها در TypeScript صرفا جهت کمک به کامپایلر، برای یافتن خطاها، پیش از اجرای برنامه است و به کدهای جاوا اسکریپتی معادلی ترجمه نمیشوند.
در ادامه برای تکمیل مثال این سری، فایل جدید App\products\product.ts را به پروژه اضافه کنید؛ با این محتوا:
export interface IProduct { productId: number; productName: string; productCode: string; releaseDate: string; price: number; description: string; starRating: number; imageUrl: string; }
همچنین از آنجائیکه این اینترفیس را در یک فایل ts مجزا قرار دادهایم، برای اینکه بتوان از آن در سایر قسمتهای برنامه استفاده کرد، نیاز است در ابتدای آن، واژهی کلیدی export را نیز ذکر کرد.
پس از تعریف این اینترفیس، برای استفاده از آن به عنوان یک data type جدید، ابتدا ماژول آن import خواهد شد و سپس از نام آن به عنوان نوع دادهی جدیدی، استفاده میشود. برای این منظور فایل product-list.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
import { Component } from 'angular2/core'; import { IProduct } from './product'; @Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html' }) export class ProductListComponent { // as before ... products: IProduct[] = [ // as before ... ]; // as before ... }
مزیت اینکار این است که برای مثال در اینجا اگر در لیست اعضای آرایهی products، نام خاصیتی اشتباه تایپ شده باشد یا حتی بجای عدد، از رشته استفاده شده باشد، بلافاصله در ادیتور مورد استفاده، خطای مرتبط گوشزد شده و همچنین این فایل دیگر کامپایل نخواهد شد. به علاوه اینبار برای تعریف خواص اعضای آرایهی products، ادیتور مورد استفاده، intellisense را نیز دراختیار ما قرار میدهد و کاملا مشخص است که چه اعضایی مدنظر هستند و نوع آنها چیست.
مدیریت cssهای هر کامپوننت به صورت مجزا
هنگام ساخت یک قالب یا template، در بسیاری از اوقات نیاز است css مرتبط با آن نیز، منحصر به همان قالب بوده و نشتی نداشته باشد. برای مثال زمانیکه یک کامپوننت را درون کامپوننتی دیگر قرار میدهیم، باید css آن نیز در دسترس قرار بگیرد و css فعلی کامپوننت دربرگیرنده را بازنویسی نکند. روشهای مختلفی برای مدیریت این مساله وجود دارند:
الف) تعریف شیوه نامهها به صورت inline داخل خود قالبها. این حالت، مشکلات نگهداری و استفادهی مجدد را دارد.
ب) تعریف شیوه نامهها در یک فایل خارجی css و سپس لینک کردن آن به صفحهای اصلی یا index.html
در این حالت به ازای هر فایل، یکبار باید این تعریف در صفحهای اصلی سایت صورت گیرد. همچنین این فایلها میتوانند مقادیر یکدیگر را بازنویسی کرده و بر روی هم تاثیر بگذارند.
ج) تعریف شیوه نامهها به همراه تعریف کامپوننت. این روشی است که توسط AngularJS توصیه شدهاست و نگهداری و مقیاس پذیری آن سادهتر است.
تزئین کنندهی Component به همراه دو خاصیت دیگر به نامهای styles و stylesUrl نیز میباشد.
در حالت استفاده از خاصیت styles، شیوهنامهی متناظر با کامپوننت، در همانجا به صورت inline تعریف میشود:
@Component({ //... styles: ['thead {color: blue;}'] })
روش بهتر، استفاده از خاصیت styleUrls است که در آن میتوان مسیر یک یا چند فایل css را مشخص کرد:
@Component({ //... styleUrls: ['app/products/product-list.component.css'] })
برای آزمایش آن فایل جدید product-list.component.css را به پوشهی products مثال این سری اضافه کنید؛ با این محتوا:
thead { color: #337AB7; }
@Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html', styleUrls: ['app/products/product-list.component.css'] }) export class ProductListComponent { //...
یک نکته
شیوه نامهای که به این صورت توسط AngularJS 2.0 اضافه میشود، با سایر شیوه نامههای موجود تداخل نخواهد کرد. علت آنرا در تصویر ذیل که با استفاده از developer tools مرورگرها قابل بررسی است، میتوان مشاهده کرد:
در اینجا AngularJS 2.0، با ایجاد ویژگیهای سفارشی خودکار (attributes) میدان دید css را کنترل میکند. به این ترتیب شیوه نامهی کامپوننت یک، که درون کامپوننت دو قرار گرفتهاست، نشتی نداشته و بر روی سایر قسمتهای صفحه تاثیری نخواهد گذاشت؛ برخلاف شیوه نامههایی که به صورت متداولی به صفحهی اصلی سایت لینک شدهاند.
بررسی چرخهی حیات کامپوننتها
هر کامپوننت دارای چرخهی حیاتی است که توسط AngularJS 2.0 مدیریت میشود و شامل مراحلی مانند ایجاد، رندر، ایجاد و رندر فرزندان آن، پردازش تغییرات آن و در نهایت تخریب آن کامپوننت میشود. برای اینکه بتوان با برنامه نویسی به این مراحل چرخهی حیات یک کامپوننت دسترسی یافت، تعدادی life cycle hook طراحی شدهاند. سه مورد از مهمترین life cycle hooks شامل موارد ذیل هستند:
الف) OnInit: از این hook برای انجام کارهای آغازین یک کامپوننت مانند دریافت اطلاعات از سرور، استفاده میشود.
ب) OnChanges: از آن جهت انجام اعمالی پس از تغییرات input properties استفاده میشود.
خواص ورودی و همچنین کار با سرور را در قسمتهای بعدی بررسی خواهیم کرد.
ج) OnDestroy: از آن جهت پاکسازی منابع اختصاص داده شده استفاده میشود.
برای استفادهی از این hookها، نیاز است اینترفیس آنها را پیاده سازی کنیم. از آنجائیکه AngularJS 2.0 نیز با TypeScript نوشته شدهاست، به همراه تعدادی اینترفیس از پیش تعریف شده میباشد. برای مثال به ازای هر life cycle hook، یک اینترفیس تعریف شده در آن وجود دارد. برای نمونه اینترفیس hook ایی به نام OnInit، دقیقا همان OnInit، نام دارد (و با I شروع نشدهاست):
export class ProductListComponent implements OnInit {
import { Component, OnInit } from 'angular2/core';
ngOnInit(): void { console.log('In OnInit'); }
به عنوان تمرین، فایل product-list.component.ts را گشوده و سه مرحلهی implements سپس import و در آخر تعریف متد ngOnInit فوق را به آن اضافه کنید.
در ادامه برنامه را اجرا کرده و به کنسول developer tools مرورگر خود جهت مشاهدهی console.log فوق مراجعه کنید:
ساخت یک Pipe سفارشی جهت فعال سازی textbox فیلتر کردن محصولات
همانطور که در قسمت قبل نیز عنوان شد، کار pipes، تغییر اطلاعات حاصل از data binding، پیش از نمایش آنها در رابط کاربری است و AngularJS 2.0 به همراه تعدادی pipe توکار است؛ مانند currency، percent و غیره. در ادامه قصد داریم یک pipe سفارشی را ایجاد کنیم تا بر روی حلقهی ngFor* نمایش لیست محصولات تاثیرگذار شود و همچنین ورودی خود را از مقدار وارد شدهی توسط کاربر دریافت کند.
برای این منظور، یک فایل جدید را به نام product-filter.pipe.ts به پوشهی products اضافه کنید. سپس کدهای آنرا به نحو ذیل تغییر دهید:
import { PipeTransform, Pipe } from 'angular2/core'; import { IProduct } from './product'; @Pipe({ name: 'productFilter' }) export class ProductFilterPipe implements PipeTransform { transform(value: IProduct[], args: string[]): IProduct[] { let filter: string = args[0] ? args[0].toLocaleLowerCase() : null; return filter ? value.filter((product: IProduct) => product.productName.toLocaleLowerCase().indexOf(filter) != -1) : value; } }
برای مثال در اینجا میخواهیم شرایط فیلتر محصولات وارد شدهی توسط کاربر را دریافت کنیم.
خروجی این متد نیز از نوع آرایهای از IProduct تعریف شدهاست؛ از این جهت که نتیجه نهایی فیلتر اطلاعات نیز آرایهای از همین نوع است. کار این pipe پیاده سازی متد contains به صورت غیرحساس به کوچکی و بزرگی حروف است.
سپس بلافاصله بالای نام این کلاس، از یک decorator جدید به نام Pipe استفاده شدهاست تا به AngularJS 2.0 اعلام شود، این کلاس، صرفا یک کلاس معمولی نیست و یک Pipe است.
در ابتدای فایل هم importهای لازم جهت تعریف اینترفیسهای مورد استفادهی در این ماژول، ذکر شدهاند.
اگر دقت کنید، الگوی ایجاد یک pipe جدید، بسیار شبیه است به الگوی ایجاد یک کامپوننت و از این لحاظ سعی شدهاست طراحی یک دستی در سراسر این فریم ورک بکار گرفته شود.
پس از تعریف این pipe سفارشی، برای استفادهی از آن در یک template، به فایل product-list.component.html مراجعه کرده و سپس ngFor* آنرا به نحو ذیل تغییر میدهیم:
<tr *ngFor='#product of products | productFilter:listFilter'>
اگر از قسمت قبل به خاطر داشته باشید، این خاصیت را توسط two-way binding به روز میکنیم (اطلاعات وارد شدهی در textbox، بلافاصله به این خاصیت منعکس میشوند و برعکس):
<input type='text' [(ngModel)]='listFilter' />
import { Component, OnInit } from 'angular2/core'; import { IProduct } from './product'; import { ProductFilterPipe } from './product-filter.pipe'; @Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html', styleUrls: ['app/products/product-list.component.css'], pipes: [ProductFilterPipe] }) export class ProductListComponent implements OnInit { //...
اکنون اگر برنامه را اجرا کنید، خروجی ذیل را مشاهده خواهید کرد:
در اینجا چون مقدار فیلتر وارد شدهی پیش فرض، cart است، فقط ردیف Garden Cart نمایش داده شدهاست. اگر این مقدار را خالی کنیم، تمام ردیفها نمایش داده میشوند و اگر برای مثال ham را جستجو کنیم، فقط ردیف Hammer نمایش داده میشود.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part5.zip
خلاصهی بحث
- اینترفیسها یکی از روشهای بهبود strong typing برنامههای AngularJS 2.0 هستند.
- جهت مدیریت بهتر شیوهنامههای هر کامپوننت بهتر است از روش styleUrls استفاده شود تا از نشتیهای تعاریف شیوهنامهها جلوگیری گردد.
- از life cycle hooks برای مدیریت رخدادهای مرتبط با طول عمر یک کامپوننت استفاده میشود؛ برای مثال دریافت اطلاعات از سرور و یا پاکسازی منابع مصرفی.
- تعریف یک pipe سفارشی با پیاده سازی اینترفیس PipeTransform انجام میشود. سپس نام این Pipe، به قالب مدنظر اضافه شده و در ادامه نیاز است کامپوننت استفاده کنندهی از این قالب را نیز از وجود این Pipe مطلع کرد.