نظرات مطالب
نظرات مطالب
Globalization در ASP.NET MVC
نظرات مطالب
EF Code First #7
سلام، من اگه بخوام بین دوتا فیلد از یک جدول به یک جدول دیگه رابطه برقرار کنم به چه صورت است :
در این حالت اولین رابطه که UserId هست ازحذف میشه و UserId2 با جدول User رابطه ش برقرار میشه چطور میشه دو فیلد از یک جدول رو با یک جدول دیگه رابطه زد ؟
pulbic class User { public int Id { get; set; } public string FullName { get; set; } public ICollection<Comment> Comments { get; set; } } public class Comment { public int Id { get; set; } public string Text { get; set; } public int UserId { get; set; } public int? UserId2 { get; set; } [ForeignKey(nameof(UserId)) public virtual User User { get; set; } [ForeignKey(nameof(UserId2)) public virtual User User2 { get; set; } }
سلام اسحق،
در مثالی که من تهیه کردم، میانگین دادههای مربوط به 3 سطر قبل محاسبه شده، بدون لحاظ مقدار جاری. اما در مساله مربوط به آن سایت میانگین دادههای مرتبط به 19 سطر قبل و سطر جاری محاسبه شده.
و همچنین در بخش Specification مربوط به آن تابع میانگین، در مثال سایت از PARTITION استفاده شده آن هم به این خاطر که دادههای جدول به گروههای مختلفی بر اساس مقادیر ستون StockId تقسیم شده است. و میخواسته میانگین مرتبط به هر StockId بطور مجزا محاسبه بشه. در واقع نتیجه را به تعداد StorckIdها بخش بندی کرده.
نام مستعاری که به جدول Quotes داده شده، غیر ضروری بوده، چرا که تنها یک جدول بیشتر در Query مشارکت نداشته و نیازی به ذکر نام جدول یا نام مستعار جدول نیست.
همچنین برای شفافیت بیشتر و ابهام زدایی، بهتر است قسمت Rows تابع تجمعی را کامل و صریح بنویسیم به این صورت:
در مثالی که من تهیه کردم، میانگین دادههای مربوط به 3 سطر قبل محاسبه شده، بدون لحاظ مقدار جاری. اما در مساله مربوط به آن سایت میانگین دادههای مرتبط به 19 سطر قبل و سطر جاری محاسبه شده.
و همچنین در بخش Specification مربوط به آن تابع میانگین، در مثال سایت از PARTITION استفاده شده آن هم به این خاطر که دادههای جدول به گروههای مختلفی بر اساس مقادیر ستون StockId تقسیم شده است. و میخواسته میانگین مرتبط به هر StockId بطور مجزا محاسبه بشه. در واقع نتیجه را به تعداد StorckIdها بخش بندی کرده.
نام مستعاری که به جدول Quotes داده شده، غیر ضروری بوده، چرا که تنها یک جدول بیشتر در Query مشارکت نداشته و نیازی به ذکر نام جدول یا نام مستعار جدول نیست.
همچنین برای شفافیت بیشتر و ابهام زدایی، بهتر است قسمت Rows تابع تجمعی را کامل و صریح بنویسیم به این صورت:
SELECT StockId, QuoteId, QuoteDay, QuoteClose, AVG (QuoteClose) OVER (PARTITION BY StockId ORDER BY QuoteId ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS MA20 FROM dbo.Quotes AS T0;
مطالب
ASP.NET MVC #17
فیلترهای امنیتی ASP.NET MVC
ASP.NET MVC به همراه تعدادی فیلتر امنیتی توکار است که در این قسمت به بررسی آنها خواهیم پرداخت.
بررسی اعتبار درخواست (Request Validation) در ASP.NET MVC
ASP.NET MVC امکان ارسال اطلاعاتی را که دارای تگهای HTML باشند، نمیدهد. این قابلیت به صورت پیش فرض فعال است و جلوی ارسال انواع و اقسام اطلاعاتی که ممکن است سبب بروز حملات XSS Cross site scripting attacks شود را میگیرد. نمونهای از خطای نمایش داده:
A potentially dangerous Request.Form value was detected from the client (Html="<a>").
بنابراین تصمیم گرفته شده صحیح است؛ اما ممکن است در قسمتی از سایت نیاز باشد تا کاربران از یک ویرایشگر متنی پیشرفته استفاده کنند. خروجی این نوع ویرایشگرها هم HTML است. در این حالت میتوان صرفا برای متدی خاص امکانات Request Validation را به کمک ویژگی ValidateInput غیرفعال کرد:
[HttpPost]
[ValidateInput(false)]
public ActionResult CreateBlogPost(BlogPost post)
از ASP.NET MVC 3.0 به بعد راه حل بهتری به کمک ویژگی AllowHtml معرفی شده است. غیرفعال کردن ValidateInput ایی که معرفی شد، بر روی تمام خواص شیء BlogPost اعمال میشود. اما اگر فقط بخواهیم که مثلا خاصیت Text آن از مکانیزم بررسی اعتبار درخواست خارج شود، بهتر است دیگر از ویژگی ValidateInput استفاده نشده و به نحو زیر عمل گردد:
using System;
using System.Web.Mvc;
namespace MvcApplication14.Models
{
public class BlogPost
{
public int Id { set; get; }
public DateTime AddDate { set; get; }
public string Title { set; get; }
[AllowHtml]
public string Text { set; get; }
}
}
در اینجا فقط خاصیت Text مجاز به دریافت محتوای HTML ایی خواهد بود. اما خاصیت Title چنین مجوزی را ندارد. همچنین دیگر نیازی به استفاده از ویژگی ValidateInput غیرفعال شده نیز نخواهد بود.
به علاوه همانطور که در قسمتهای قبل نیز ذکر شد، خروجی Razor به صورت پیش فرض Html encoded است مگر اینکه صریحا آنرا تبدیل به HTML کنیم (مثلا استفاده از متد Html.Raw). به عبارتی خروجی Razor در حالت پیش فرض در مقابل حملات XSS مقاوم است مگر اینکه آگاهانه بخواهیم آنرا غیرفعال کنیم.
مطلب تکمیلی
مقابله با XSS ؛ یکبار برای همیشه!
فیلتر RequireHttps
به کمک ویژگی یا فیلتر RequireHttps، تمام درخواستهای رسیده به یک متد خاص باید از طریق HTTPS انجام شوند و حتی اگر شخصی سعی به استفاده از پروتکل HTTP معمولی کند، به صورت خودکار به HTTPS هدایت خواهد شد:
[RequireHttps]
public ActionResult LogOn()
{
}
فیلتر ValidateAntiForgeryToken
نوع دیگری از حملات که باید در برنامههای وب به آنها دقت داشت به نام CSRF یا Cross site request forgery معروف هستند.
برای مثال فرض کنید کاربری قبل از اینکه بتواند در سایت شما کار خاصی را انجام دهد، نیاز به اعتبار سنجی داشته باشد. پس از لاگین شخص و ایجاد کوکی و سشن معتبر، همین شخص به سایت دیگری مراجعه میکند که در آن مهاجمی بر اساس وضعیت جاری اعتبار سنجی او مثلا لینک حذف کاربری یا افزودن اطلاعات جدیدی را به برنامه ارائه میدهد. چون سشن شخص و کوکی مرتبط به سایت اول هنوز معتبر هستند و شخص سایت را نبسته است، «احتمال» اجرا شدن درخواست مهاجم بالا است (خصوصا اگر از مرورگرهای قدیمی استفاده کند).
بنابراین نیاز است بررسی شود آیا درخواست رسیده واقعا از طریق فرمهای برنامه ما صادر شده است یا اینکه شخصی از طریق سایت دیگری اقدام به جعل درخواستها کرده است.
برای مقابله با این نوع خطاها ابتدا باید داخل فرمهای برنامه از متد Html.AntiForgeryToken استفاده کرد. کار این متد ایجاد یک فیلد مخفی با مقداری منحصربفرد بر اساس اطلاعات سشن جاری کاربر است، به علاوه ارسال یک کوکی خودکار تا بتوان از تطابق اطلاعات اطمینان حاصل کرد:
@using (Html.BeginForm()) {
@Html.AntiForgeryToken()
در مرحله بعد باید فیلتر ValidateAntiForgeryToken جهت بررسی مقدار token دریافتی به متد ثبت اطلاعات اضافه شود:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateBlogPost(BlogPost post)
در اینجا مقدار دریافتی از فیلد مخفی فرم :
<input name="__RequestVerificationToken" type="hidden" value="C0iPfy/3T....=" />
با مقدار موجود در کوکی سایت بررسی و تطابق داده خواهند شد. اگر این مقادیر تطابق نداشته باشند، یک استثنا صادر شده و از پردازش اطلاعات رسیده جلوگیری میشود.
علاوه بر اینها بهتر است حین استفاده از متد و فیلتر یاد شده، از یک salt مجزا نیز به ازای هر فرم، استفاده شود:
[ValidateAntiForgeryToken(Salt="1234")]
@Html.AntiForgeryToken(salt:"1234")
به این ترتیب tokenهای تولید شده در فرمهای مختلف سایت یکسان نخواهند بود.
به علاوه باید دقت داشت که ValidateAntiForgeryToken فقط با فعال بودن کوکیها در مرورگر کاربر کار میکند و اگر کاربری پذیرش کوکیها را غیرفعال کرده باشد، قادر به ارسال اطلاعاتی به برنامه نخواهد بود. همچنین این فیلتر تنها در حالت HttpPost قابل استفاده است. این مورد هم در قسمتهای قبل تاکید گردید که برای مثال بهتر است بجای داشتن لینک delete در برنامه که با HttpGet ساده کار میکند، آنرا تبدیل به HttpPost نمود تا میزان امنیت برنامه بهبود یابد. از HttpGet فقط برای گزارشگیری و خواندن اطلاعات از برنامه استفاده کنید و نه ثبت اطلاعات.
بنابراین استفاده از AntiForgeryToken را به چک لیست اجباری تولید تمام فرمهای برنامه اضافه نمائید.
مطلب مشابه
Anti CSRF module for ASP.NET
فیلتر سفارشی بررسی Referrer
یکی دیگر از روشهای مقابله با CSRF، بررسی اطلاعات هدر درخواست ارسالی است. اگر اطلاعات Referrer آن با دومین جاری تطابق نداشت، به معنای مشکل دار بودن درخواست رسیده است. فیلتر سفارشی زیر میتواند نمونهای باشد جهت نمایش نحوه بررسی UrlReferrer درخواست رسیده:
using System.Web.Mvc;
namespace MvcApplication14.CustomFilter
{
public class CheckReferrerAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext.HttpContext != null)
{
if (filterContext.HttpContext.Request.UrlReferrer == null)
throw new System.Web.HttpException("Invalid submission");
if (filterContext.HttpContext.Request.UrlReferrer.Host != "mysite.com")
throw new System.Web.HttpException("This form wasn't submitted from this site!");
}
base.OnAuthorization(filterContext);
}
}
}
و برای استفاده از آن:
[HttpPost]
[CheckReferrer]
[ValidateAntiForgeryToken]
public ActionResult DeleteTask(int id)
نکتهای امنیتی در مورد آپلود فایلها در ASP.NET
هر جایی که کاربر بتواند فایلی را به سرور شما آپلود کند، مشکلات امنیتی هم از همانجا شروع خواهند شد. مثلا در متد Upload قسمت 11 این سری، منعی در آپلود انواع فایلها نیست و کاربر میتواند انواع و اقسام شلها را جهت تحت کنترل گرفتن سایت و سرور آپلود و اجرا کند. راه حل چیست؟
از همان روش امنیتی مورد استفاده توسط تیم ASP.NET MVC استفاده میکنیم. فایل web.config قرار گرفته در پوشه Views را باز کنید (نه فایل وب کانفیگ ریشه اصلی سایترا). چنین تنظیمی را میتوان مشاهده کرد:
برای IIS6 :
<system.web>
<httpHandlers>
<add path="*" verb="*" type="System.Web.HttpNotFoundHandler"/>
</httpHandlers>
</system.web>
<system.webServer>
<handlers>
<remove name="BlockViewHandler"/>
<add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
</handlers>
</system.webServer>
تنظیم فوق، موتور اجرایی ASP.NET را در این پوشه خاص از کار میاندازد. به عبارتی اگر شخصی مسیر یک فایل aspx یا cshtml یا هر فایل قرار گرفته در پوشه Views را مستقیما در مرورگر خود وارد کند، با پیغام HttpNotFound مواجه خواهد شد.
این روش هم با ASP.NET Web forms سازگار است و هم با ASP.NET MVC؛ چون مرتبط است به موتور اجرایی ASP.NET که هر دوی این فریم ورکها برفراز آن معنا پیدا میکنند.
بنابراین در پوشه فایلهای آپلودی به سرور خود یک web.config را با محتوای فوق ایجاد کنید (و فقط باید مواظب باشید که این فایل حین آپلود فایلهای جدید، overwrite نشود. مهم!). به این ترتیب این مسیر دیگر از طریق مرورگر قابل دسترسی نخواهد بود (با هر محتوایی). سپس برای ارائه فایلهای آپلودی به کاربران از روش زیر استفاده کنید:
public ActionResult Download()
{
return File(Server.MapPath("~/Myfiles/test.txt"), "text/plain");
}
مزیت مهم روش ذکر شده این است که کاربران مجاز به آپلود هر نوع فایلی خواهند بود و نیازی نیست لیست سیاه تهیه کنید که مثلا فایلهایی با پسوندهای خاص آپلود نشوند (که در این بین ممکن است لیست سیاه شما کامل نباشد ...).
علاوه بر تمام فیلترهای امنیتی که تاکنون بررسی شدند، فیلتر دیگری نیز به نام Authorize وجود دارد که در قسمتهای بعدی بررسی خواهد شد.
- اصول و قواعد کاربردپذیری در وب | فرشاد | blog.datisdesign.com
- چند نکته در مورد فوتر سایت | سپیده | blog.sgheysar.com
- SSMS Tools Pack 2.0 منتشر شد | weblogs.sqlteam.com
- unit test explorer در نگارش بعدی ویژوال استودیو | geekswithblogs.net
- استخراج اطلاعات از @@VERSION در اس کیوال سرور | beyondrelational.com
- آیا روزی شاهد استفاده از پردازندههای ARM در کامپیوترهای رومیزی خواهیم بود؟ | rss.slashdot.org
- چگونه آزمونهای واحد NUnit را به MSTest تبدیل کنیم؟ | blogs.microsoft.co.il
- لیستی از ویدیوهای دات نت 4 و نیم در کنفرانس Build | blogs.msdn.com
- مروری بر ASP.NET MVC 4 قسمت اول | weblogs.asp.net
- مزایا و معایب self-signed certificates | blogs.microsoft.co.il
- معرفی برنامه SQL SERVER Backup And FTP | beyondrelational.com
- معرفی برنامه Synalyze It مخصوص آنالیز فایلهای باینری | www.reddit.com
- معرفی پروژه QRCode.Net | geekswithblogs.net
با سلام؛ خیلی ممنونم. ولی میخواستم اون کاری که میخوام انجام بدم رو بیشتر توضیح بدم تا موضوع روشنتر بشه
همون طور که در عکس مشخصه من 3 تا جدول دارم
-Programs برای نگه داری اطلاعات برنامه که به صورت تو در تو است
-PrgElements برای نگه داری اطلاعات مربوط به المانهای تعریف شده در برنامه (مثل فرم -منو- کنترل و ...) که ElementID به صورت HiearachyID تعریف شده و متاسفانه Entity Framwork,LingToSql این DataType رو ساپرت نمیکنه برای همین باید از ADO استفاده کنم.
من میخوام گریدی داشته باشم که کاربر اطلاعات راجع به المنتهای برنامه رو تو در تو وارد کنه و این اطلاعات برای کاربر نمایش داده بشه تا کاربر بتونه با انتخاب سطر مربوطه اون رو ویرایش کنه حذف کنه و مورد جدیدی رو اضافه کنه
یک گریدی به این شکل:
من در قسمت کدهای سمت سرور ، اون قسمتی که row رو مقدار دهی میکنید رو متاسفانه نمیتونم پیاده سازی کنم (در واقع تبدیل داده مورد نظر به فرمت jqGridData جایی که Row رو مقدار دهی میکنیم)
من در قسمت کدهای سمت سرور ، اون قسمتی که row رو مقدار دهی میکنید رو متاسفانه نمیتونم پیاده سازی کنم (در واقع تبدیل داده مورد نظر به فرمت jqGridData جایی که Row رو مقدار دهی میکنیم)
در مطلب «Angular CLI - قسمت ششم - استفاده از کتابخانههای ثالث» با نحوهی دریافت، نصب و راه اندازی کتابخانهی ngx-bootstrap آشنا شدیم. در اینجا میخواهیم نحوهی کار با کامپوننت Modal آن را بررسی کنیم.
سازماندهی بهتر کامپوننتهای ngx-bootstrap
پس از نصب بستهی npm کتابخانهی ngx-bootstrap و تنظیم فایل angular-cli.json. که در مطلب «Angular CLI - قسمت ششم - استفاده از کتابخانههای ثالث» بررسی شدند، برای کار با کامپوننتهای این کتابخانه باید متدهای BsDropdownModule.forRoot، TooltipModule.forRoot، ModalModule.forRoot و ... را به قسمت imports فایل app.module.ts اضافه کرد. با انجام اینکار پس از مدتی به یک فایل بسیار شلوغ app.module.ts خواهیم رسید. برای مدیریت بهتر آن میتوان شبیه به مطلب «سازماندهی برنامههای Angular توسط ماژولها» در پوشهی Shared برنامه، ماژول ذیل را تدارک دید. برای اینکار ابتدا فایل جدید src\app\shared\shared.bootstrap.module.ts را ایجاد نمائید. سپس کامپوننتهای این کتابخانه را به صورت ذیل در این تک ماژول اختصاصی قرار دهید:
متدهای forRoot در قسمت imports قرار میگیرند (فلسفهی وجودی این متد و الگوی ویژه را در مطلب «سازماندهی برنامههای Angular توسط ماژولها» پیشتر بررسی کردهایم). سپس برای اینکه این کامپوننتها در سایر ماژولهای برنامه قابل استفاده باشند، باید نام ماژول مرتبط با هر کدام را در قسمت exports نیز ذکر کرد.
اکنون برای استفادهی از SharedBootstrapModule اختصاصی فوق، میتوان دو روش را بکار برد:
الف) import مستقیم آن در فایل app.module.ts
ب) import آن در SharedModule
و یا اگر فایل src\app\shared\shared.module.ts را مطابق مطلب «سازماندهی برنامههای Angular توسط ماژولها» ایجاد کردهاید، این ماژول به صورت ذیل، در دو قسمت imports و exports آن اضافه خواهد شد:
نمایش یک modal dialog توسط کامپوننت Modal
پس از تعریف ModalModule.forRoot، اکنون میتوان به کامپوننت Modal این ماژول دسترسی یافت. برای این منظور کامپوننتی که قرار است یک Modal را نمایش دهد، چنین ساختاری را پیدا میکند:
توسط سرویس BsModalService که به سازندهی کلاس کامپوننت تزریق شدهاست، میتوان به متد show آن دسترسی یافت. این متد یک ng-template را قبول میکند. بنابراین در قالب این کامپوننت باید قسمتی را که قرار است به صورت modal نمایش داده شود، توسط یک ng-template تعریف کرد.
سپس با فراخوانی متد this.modalService.show میتوان این قالب را نمایش داد. خروجی این متد ارجاعی را به این modal بازگشت میدهد. از این ارجاع میتوان در جهت بستن آن استفاده کرد (مانند متد closeModal).
بنابراین در ادامه، قالب کامپوننت مثال این قسمت، یک چنین شکلی را پیدا میکند:
در اینجا محتوای modal داخل یک ng-template قرار گرفتهاست و این قالب توسط یک template reference variable به نام template1 مشخص شدهاست. این نام را در متد openModal(template1) استفاده خواهیم کرد تا به متد show سرویس نمایش modal منتقل شود.
طراحی یک کامپوننت عمومی مودال جهت دریافت تائید انجام عملیات
در ادامه میخواهیم توسط یک modal dialog، کار دریافت تائید و یا لغو انجام یک عملیات را انجام دهیم. چون این کامپوننت عمومی قرار است در بیش از یک ماژول استفاده شود، بنابراین نیاز است آنرا در Shared Module ثبت کرد. به همین جهت این کامپوننت را به نحو ذیل در پوشهی Shared ایجاد میکنیم:
پرچم skip-import نیز ذکر شدهاست، چون قصد نداریم به صورت مستقیم از طریق درج selector آن در صفحه، با آن کار کنیم. سرویس سفارشی مودالی که برای این منظور تدارک خواهیم دید، کار نمایش آنرا انجام میدهد.
نحوهی درج و تعریف این کامپوننت اندکی متفاوت است. چون این کامپوننت قرار است «به صورت پویا» توسط متد show سرویس BsModalService نمایش داده شود (پارامتر اول آن میتواند یک قالب و یا یک کامپوننت کامل باشد)، باید در قسمت entryComponents و declarations مربوط به SharedModule درج شود. آنرا در قسمت exports ذکر نمیکنیم، چون قرار نیست با درج مستقیم selector آن در صفحه، آنرا نمایش دهیم.
این کامپوننت دریافت تائید کاربر به صورت ذیل تعریف میشود:
در اینجا args آن توسط سرویسی که در ادامه طراحی میکنیم، مقدار دهی خواهد شد (طراحی args در اینجا کاملا دلخواه است و در کامپوننتهای مشابه دیگر میتواند متفاوت باشد). متد close آن نیز کار گزارش دهی به فراخوان را انجام میدهد.
قالب این کامپوننت نیز بدون استفاده از ng-template تعریف میشود:
چون این کامپوننت قرار است به صورت پویا توسط متد show بارگذاری شود، نیازی نیست محتوای قالب آنرا توسط ng-template مخفی کرد و سپس نمایش داد. زمانیکه این کامپوننت بارگذاری شد، یعنی قصد داریم یک modal کامل را نمایش دهیم.
تا اینجا یک کامپوننت نمایش دریافت تائید انجام عملیات را تهیه کردیم. در ادامه نیاز است یک سرویس را جهت بارگذاری پویای اینگونه کامپوننتهای مودال طراحی کنیم. این سرویس عمومی در پوشهی Core و CoreModule ثبت خواهد شد:
با این محتوا
کار این سرویس، نمایش یک کامپوننت مودال مانند ConfirmModalComponent به صورت پویا است؛ از این جهت که متد this.bsModalService.show هم امکان نمایش یک ng-template را دارد و هم یک کامپوننت کامل را به صورت پویا.
یک مودال در سه حالت ممکن است بسته شود:
الف) کلیک بر روی دکمهی close و یا cancel
ب) کلیک بر روی علامت ضربدر درج شدهی در یک سمت عنوان آن
ج) کلیک بر روی قسمتی از صفحه، خارج از مودال
در حالات ب و ج، رخداد this.bsModalService.onHidden فراخوانی میشود. در حالت الف، همان متد close درج شدهی در کامپوننت فراخوانی میشود.
برای اینکه بتوان نتیجهی عملیات را از طرف یک سرویس به کامپوننت فراخوان آن گزارش دهیم، یکی از روشها، استفاده از Promiseها است که مشاهده میکنید. با فراخوانی resolve(result)، کار ارسال نتیجهی فراخوانی متدهای close(true) و ()close صورت میگیرد (یا true و یا undefined).
خاصیت modal.content امکان دسترسی به خواص عمومی کامپوننت در حال استفاده را میسر میکند (content به کامپوننت بارگذاری شده اشاره میکند). اینجا است که میتوان برای مثال به خاصیت args یک کامپوننت، مقادیری را نسبت داد و یا به متد close آن دسترسی یافت.
پس از افزودن این سرویس، محل تعریف آن در قسمت providers مربوط به CoreModule است تا در تمام برنامه قابل دسترسی شود:
در پایان برای آزمایش این سرویس جدید، یک دکمه و یک برچسب را به قالب کامپوننت ModalDialogTestComponent ابتدای بحث اضافه میکنیم:
با این کدها:
در اینجا نحوهی استفادهی از این ModalService سفارشی را ملاحظه میکنید. ابتدا به سازندهی کلاس تزریق شدهاست و سپس در متد deleteRecord توسط متد show آن، کامپوننت ConfirmModalComponent به صورت پویا بارگذاری شدهاست. همچنین خاصیت args آن نیز با خواص title و message سفارشی، مقدار دهی شدهاست. چون این متد یک Promise را باز میگرداند، میتوان مشترک آن شد و نتیجهی نهایی را از آن دریافت کرد و بر اساس آن تصمیم گرفت که آیا باید عملیاتی رخدهد، یا خیر.
توسط this.modalService.show میتوان انواع و اقسام کامپوننتهای مودال را به صورت پویا بارگذاری کرد و نمایش داد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
سازماندهی بهتر کامپوننتهای ngx-bootstrap
پس از نصب بستهی npm کتابخانهی ngx-bootstrap و تنظیم فایل angular-cli.json. که در مطلب «Angular CLI - قسمت ششم - استفاده از کتابخانههای ثالث» بررسی شدند، برای کار با کامپوننتهای این کتابخانه باید متدهای BsDropdownModule.forRoot، TooltipModule.forRoot، ModalModule.forRoot و ... را به قسمت imports فایل app.module.ts اضافه کرد. با انجام اینکار پس از مدتی به یک فایل بسیار شلوغ app.module.ts خواهیم رسید. برای مدیریت بهتر آن میتوان شبیه به مطلب «سازماندهی برنامههای Angular توسط ماژولها» در پوشهی Shared برنامه، ماژول ذیل را تدارک دید. برای اینکار ابتدا فایل جدید src\app\shared\shared.bootstrap.module.ts را ایجاد نمائید. سپس کامپوننتهای این کتابخانه را به صورت ذیل در این تک ماژول اختصاصی قرار دهید:
import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { BsDropdownModule } from "ngx-bootstrap/dropdown"; import { TooltipModule } from "ngx-bootstrap/tooltip"; import { ModalModule } from "ngx-bootstrap/modal"; @NgModule({ imports: [ CommonModule, BsDropdownModule.forRoot(), TooltipModule.forRoot(), ModalModule.forRoot() ], exports: [ BsDropdownModule, TooltipModule, ModalModule ] }) export class SharedBootstrapModule { }
اکنون برای استفادهی از SharedBootstrapModule اختصاصی فوق، میتوان دو روش را بکار برد:
الف) import مستقیم آن در فایل app.module.ts
import { SharedBootstrapModule } from './shared/shared.bootstrap.module'; @NgModule({ imports: [BrowserModule, SharedBootstrapModule], // ... }) export class AppModule {}
و یا اگر فایل src\app\shared\shared.module.ts را مطابق مطلب «سازماندهی برنامههای Angular توسط ماژولها» ایجاد کردهاید، این ماژول به صورت ذیل، در دو قسمت imports و exports آن اضافه خواهد شد:
import { SharedBootstrapModule } from "./shared.bootstrap.module"; @NgModule({ imports: [ CommonModule, SharedBootstrapModule ], exports: [ CommonModule, SharedBootstrapModule ] })
نمایش یک modal dialog توسط کامپوننت Modal
پس از تعریف ModalModule.forRoot، اکنون میتوان به کامپوننت Modal این ماژول دسترسی یافت. برای این منظور کامپوننتی که قرار است یک Modal را نمایش دهد، چنین ساختاری را پیدا میکند:
import { Component, OnInit, TemplateRef } from "@angular/core"; import { BsModalRef, BsModalService } from "ngx-bootstrap"; @Component({ selector: "app-modal-dialog-test", templateUrl: "./modal-dialog-test.component.html", styleUrls: ["./modal-dialog-test.component.css"] }) export class ModalDialogTestComponent implements OnInit { modalRef: BsModalRef; constructor(private modalService: BsModalService) { } openModal(template: TemplateRef<any>) { this.modalRef = this.modalService.show(template, { animated: true, keyboard: true, backdrop: true, ignoreBackdropClick: false }); } closeModal() { this.modalRef.hide(); } }
سپس با فراخوانی متد this.modalService.show میتوان این قالب را نمایش داد. خروجی این متد ارجاعی را به این modal بازگشت میدهد. از این ارجاع میتوان در جهت بستن آن استفاده کرد (مانند متد closeModal).
بنابراین در ادامه، قالب کامپوننت مثال این قسمت، یک چنین شکلی را پیدا میکند:
<h1>Displaying modal bootstrap dialogs</h1> <button type="button" class="btn btn-info" (click)="openModal(template1)">Create template modal</button> <ng-template #template1> <div class="modal-header"> <h4 class="modal-title pull-left">Modal</h4> <button type="button" class="close pull-right" aria-label="Close" (click)="closeModal()"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> This is a modal. </div> </ng-template>
طراحی یک کامپوننت عمومی مودال جهت دریافت تائید انجام عملیات
در ادامه میخواهیم توسط یک modal dialog، کار دریافت تائید و یا لغو انجام یک عملیات را انجام دهیم. چون این کامپوننت عمومی قرار است در بیش از یک ماژول استفاده شود، بنابراین نیاز است آنرا در Shared Module ثبت کرد. به همین جهت این کامپوننت را به نحو ذیل در پوشهی Shared ایجاد میکنیم:
ng g c Shared/ConfirmModal --skip-import
import { ConfirmModalComponent } from "./confirm-modal/confirm-modal.component"; @NgModule({ imports: [ ], entryComponents: [ ConfirmModalComponent ], declarations: [ ConfirmModalComponent ] }) export class SharedModule {}
این کامپوننت دریافت تائید کاربر به صورت ذیل تعریف میشود:
import { Component } from "@angular/core"; @Component({ selector: "app-confirm-modal", templateUrl: "./confirm-modal.component.html", styleUrls: ["./confirm-modal.component.css"] }) export class ConfirmModalComponent { args: { title: string; message: string; }; close: (val?: any) => void; }
قالب این کامپوننت نیز بدون استفاده از ng-template تعریف میشود:
<div class="modal-header"> <h4 class="modal-title pull-left">{{ args?.title }}</h4> <button type="button" class="close pull-right" aria-label="Close" (click)="close()"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <p>{{ args?.message }}</p> </div> <div class="modal-footer"> <button class="btn btn-danger" (click)="close(true)">Yes</button> <button class="btn btn-primary" (click)="close()">Cancel</button> </div>
تا اینجا یک کامپوننت نمایش دریافت تائید انجام عملیات را تهیه کردیم. در ادامه نیاز است یک سرویس را جهت بارگذاری پویای اینگونه کامپوننتهای مودال طراحی کنیم. این سرویس عمومی در پوشهی Core و CoreModule ثبت خواهد شد:
>ng g s Core/Modal
import { Injectable } from "@angular/core"; import { BsModalService } from "ngx-bootstrap"; @Injectable() export class ModalService { constructor(private bsModalService: BsModalService) { } show(component: any, args?: any, options?: any): Promise<any> { return new Promise(resolve => { options = options || {}; const modal = this.bsModalService.show(component, options); let result: any; const sub = this.bsModalService.onHidden.subscribe(() => { sub.unsubscribe(); resolve(result); }); modal.content.args = args; modal.content.close = (val?: any) => { result = val; modal.hide(); }; }); } }
یک مودال در سه حالت ممکن است بسته شود:
الف) کلیک بر روی دکمهی close و یا cancel
ب) کلیک بر روی علامت ضربدر درج شدهی در یک سمت عنوان آن
ج) کلیک بر روی قسمتی از صفحه، خارج از مودال
در حالات ب و ج، رخداد this.bsModalService.onHidden فراخوانی میشود. در حالت الف، همان متد close درج شدهی در کامپوننت فراخوانی میشود.
برای اینکه بتوان نتیجهی عملیات را از طرف یک سرویس به کامپوننت فراخوان آن گزارش دهیم، یکی از روشها، استفاده از Promiseها است که مشاهده میکنید. با فراخوانی resolve(result)، کار ارسال نتیجهی فراخوانی متدهای close(true) و ()close صورت میگیرد (یا true و یا undefined).
خاصیت modal.content امکان دسترسی به خواص عمومی کامپوننت در حال استفاده را میسر میکند (content به کامپوننت بارگذاری شده اشاره میکند). اینجا است که میتوان برای مثال به خاصیت args یک کامپوننت، مقادیری را نسبت داد و یا به متد close آن دسترسی یافت.
پس از افزودن این سرویس، محل تعریف آن در قسمت providers مربوط به CoreModule است تا در تمام برنامه قابل دسترسی شود:
import { ModalService } from "./modal.service"; @NgModule({ providers: [ ModalService ] }) export class CoreModule {}
<button type="button" class="btn btn-danger" (click)="deleteRecord()">Delete record</button> <div *ngIf="confirmResult" class="alert alert-info">{{confirmResult}}</div>
import { ModalService } from "./../../core/modal.service"; import { ConfirmModalComponent } from "./../../shared/confirm-modal/confirm-modal.component"; export class ModalDialogTestComponent implements OnInit { confirmResult: string; constructor(private modalService: ModalService) { } deleteRecord() { this.confirmResult = ""; this.modalService.show( ConfirmModalComponent, { title: "Confirm", message: "Do you want to delete this record?" }, { animated: true, keyboard: true, backdrop: true, ignoreBackdropClick: false }).then(confirmed => { if (confirmed) { this.confirmResult = "Deleted!"; } else { this.confirmResult = "Canceled!"; } }); } }
توسط this.modalService.show میتوان انواع و اقسام کامپوننتهای مودال را به صورت پویا بارگذاری کرد و نمایش داد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
مطالب
مدیریت پیشرفتهی حالت در React با Redux و Mobx - قسمت سوم - روش اتصال Redux به برنامههای React
پس از بررسی ساختار کتابخانهی Redux به صورت مستقل و متکی به خود، اکنون در این قسمت، نحوهی اتصال آنرا به برنامههای React بررسی میکنیم.
نصب پیشنیازها
میتوان همانند قسمت قبل، تمام کارها را با کتابخانهی redux انجام داد و یا میتوان قسمت به روز رسانی UI آنرا و همچنین مدیریت state را به کتابخانهی ساده کنندهی دیگری به نام react-redux واگذار کرد. به همین جهت در ادامهی همان برنامهی قسمت قبل، دو کتابخانهی redux و همچنین react-redux را به همراه types آن نصب میکنیم (نصب types، سبب ارائهی intellisense بهتری در VSCode میشود؛ حتی اگر نخواهیم با TypeScript کار کنیم).
برای این منظور پس از باز کردن پوشهی اصلی برنامه توسط VSCode، دکمههای ctrl+` را فشرده (ctrl+back-tick) و دستورات زیر را در ترمینال ظاهر شده وارد کنید:
به علاوه در ادامه توئیتر بوت استرپ 4 را نیز نصب میکنیم:
سپس برای افزودن فایل bootstrap.css به پروژهی React خود، ابتدای فایل index.js را به نحو زیر ویرایش خواهیم کرد:
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام میدهد، مورد استفاده قرار میگیرد.
معرفی ساختار ابتدایی برنامه
برنامهای را که در این قسمت بررسی میکنیم، ساختار بسیار سادهای را داشته و به همراه دو دکمهی افزایش و کاهش مقدار یک شمارشگر است؛ به همراه دکمهی برای به حالت اول در آوردن آن. هدف اصلی دنبال شدهی در اینجا نیز نحوهی برپایی redux و همچنین react-redux و اتصال آنها به برنامهی React جاری است:
به همین جهت ابتدا کامپوننت جدید src\components\counter.jsx را به نحو زیر تشکیل میدهیم تا markup ابتدایی فوق را به همراه سه دکمه و یک span، برای نمایش مقدار شمارشگر، رندر کند:
سپس المان آنرا جهت نمایش در برنامه، به فایل src\App.js اضافه میکنیم:
پوشه بندی مخصوص برنامههای مبتنی بر Redux
هدف ما در ادامه ایجاد یک store مخصوص redux است و سپس اتصال آن به کامپوننت شمارشگر برنامه. به همین جهت نیاز به 4 پوشهی جدید، برای مدیریت بهتر برنامه خواهیم داشت:
- پوشه constants: برای اینکه نام رشتهای نوع اکشنهای مختلف را بتوانیم در قسمتهای مختلف برنامه استفاده کنیم، بهتر است فایل جدید src\actions\index.js را ایجاد کرده و این ثوابت را داخل آن export کنیم.
- پوشهی actions: در فایل جدید src\actions\index.js، تمام متدهای ایجاد کنندهی شیء خاص action، که در قسمت قبل در مورد آن بحث شد، قرار میگیرند. نمونهی آن، متد createAddAction قسمت قبل است.
- پوشهی reducers: تمام توابع reducer برنامه را در فایلهای مجزایی در پوشهی reducers قرار میدهیم. سپس در فایل src\reducers\index.js با استفاده از متد combineReducer آنها را یکی کرده و به متد createStore ارسال میکنیم.
- پوشهی containers: این پوشه جائی است که کار فراخوانی متد connect کتابخانهی react-redux به ازای هر کامپوننت استفاده کنندهی از redux store، صورت میگیرد.
ایجاد نام نوع اکشن متناظر با دکمهی افزودن مقدار
میخواهیم با کلیک بر روی دکمهی +، مقدار شمارشگر افزایش یابد. به همین جهت نیاز به یک نام وجود دارد تا در تابع Reducer متناظر و قسمتهای دیگر برنامه، بتوان بر اساس آن، این اکشن خاص را شناسایی کرد و سپس عکس العمل نشان داد. به همین جهت فایل جدید src\constants\ActionTypes.js را ایجاد کرده و به صورت زیر تکمیل میکنیم:
البته هرچند مرسوم است نام و مقدار این نوع ثوابت را تشکیل شدهی از حروف بزرگ، معرفی کنند ولی این موضوع اختیاری است.
ایجاد متد Action Creator
در قسمت قبل مشاهده کردیم که شیء ارسالی به یک reducer از طریق dispatch یک action خاص، دارای فرمت ویژهی زیر است:
به همین جهت برای نظم بخشیدن به تعریف این نوع اشیاء و یکدست سازی آنها، فایل جدید src\actions\index.js را ایجاد کرده و آنرا به صورت زیر تکمیل میکنیم:
همانطور که ملاحظه میکنید در این متد، فعلا فقط نام رشتهای نوع این اکشن، بیشتر مدنظر است تا بر اساس action.type رسیده در reducer متناظر با آن، عملی رخ دهد. بنابراین فقط قسمت type آنرا مقدار دهی کردهایم. مقدار ثابت رشتهای types.Increment نیز از فایل مجزای src\constants\ActionTypes.js که پیشتر تعریف کردیم، تامین شدهاست.
ایجاد تابع reducer مخصوص افزودن مقدار
ابتدا فایل جدید src\reducers\counter.js را با محتوای زیر ایجاد میکنیم:
- اگر دقت کرده باشید، کامپوننت شمارشگر این قسمت، دارای state نیست و همچنین نمیخواهیم هم که دارای state باشد؛ چون قرار است توسط redux مدیریت شود. به همین جهت state اولیه را به صورت initialState که محتوای یک شیء با خاصیت count با مقدار اولیهی صفر است، خارج از کلاس کامپوننت، ایجاد کردهایم.
- سپس میخواهیم رویداد کلیک بر روی دکمه + را مدیریت کنیم. به همین جهت نیاز به یک اکشن جدید به نام Increment داریم که توسط مقدار ثابت رشتهای types.Increment، از فایل مجزای src\constants\ActionTypes.js، تامین میشود.
- پس از مشخص کردن نوع action ای که قرار است مدیریت شود و همچنین ایجاد متدی برای تولید شیء حاوی اطلاعات آن که در فایل src\actions\index.js قرار دارد، اکنون میتوان متد reducer را که state و action را دریافت میکند و سپس state جدیدی را بر اساس action.type دریافتی و در صورت نیاز بازگشت میدهد، ایجاد کرد. این متد بررسی میکند که آیا action.type رسیده همان ثابت Increment است؟ اگر بله، بجای تغییر مستقیم state.count، یک شیء جدید را بازگشت میدهد. البته روش صحیحتر اینکار را در قسمت اول این سری با معرفی روشهایی برای کپی اشیاء و آرایهها، بررسی کردیم. در اینجا جهت سادگی بیشتر، یک شیء کاملا جدید را دستی ایجاد میکنیم. در آخر اگر action.type رسیده قابل پردازش نبود، همان state ابتدایی دریافتی را بازگشت میدهیم تا در صورت وجود چندین reducer تعریف شدهی در سیستم، زنجیرهی آنها قابل پردازش باشد. این مورد را در قسمت قبل، ذیل عنوان «بررسی تابع combineReducers با یک مثال» بیشتر بررسی کردیم.
پس از ایجاد reducer اختصاصی عمل افزودن مقدار شمارشگر، فایل جدید src\reducers\index.js را نیز با محتوای زیر ایجاد میکنیم:
کار این فایل، مدیریت مرکزی تمام reducerهای سفارشی تعریف شدهی در برنامهاست. لیست آنها را به متد combineReducers ارسال کرده و در نهایت یک rootReducer ترکیب شدهی از تمام آنها را دریافت میکنیم.
ایجاد store مخصوص Redux
تا اینجا رسیدیم به یک rootReducer متشکل از تمام reducerهای سفارشی برنامه. اکنون بر اساس آن در فایل src\index.js، یک store جدید را ایجاد میکنیم:
نکته 1: چون شیء rootReducer در فایل src\reducers\index.js واقع شدهاست، دیگر در حین import، نیازی به ذکر نام فایل index آن نیست.
نکته 2: در اینجا روش فعالسازی افزونهی redux-devtools را نیز ملاحظه میکنید. ابتدا بررسی میشود که آیا متد ویژهی فراخوانی این افزونه وجود دارد یا خیر؟ اگر بله، فراخوانی میشود. بدون این پارامتر دوم، افزونهی redex dev tools، هیچ خروجی را نمایش نخواهد داد.
اتصال React به Redux
کتابخانهی react-redux تنها به همراه دو شیء مهم connect و Provider است. شیء Provider آن شبیه به Context API خود React است و هدف آن، ارسال ارجاعی از store ایجاد شده، به برنامهی React است. پس از ایجاد store در فایل src\index.js، اکنون نوبت به اتصال آن به برنامهی React ای جاری است. به همین جهت در بالاترین سطح برنامه، ابتدا شیء کامپوننت App را با شیء Provider محصور میکنیم:
کامپوننت Provider، از طریق props خود نیاز به دریافت store تعریف شده را دارد. به این ترتیب هر کامپوننتی که در درخت کامپوننتهای App قرار میگیرد، میتواند با redux store کار کند.
تامین state کامپوننت شمارشگر از طریق props
همانطور که عنوان شد، کامپوننت Counter به همراه state نیست و ما قصد نداریم در آن از state خود React استفاده کنیم؛ البته فلسفهی آنرا در قسمت اول این سری بررسی کردیم و همچنین اگر کامپوننتی نیاز به اشتراک گذاری اطلاعات خودش را با لایههای زیرین یا بالاتر از خود ندارد، شاید اصلا نیازی به Redux نداشته باشد و همان state استاندارد React برای آن کافی است. بنابراین میتوان برنامهای را داشت که ترکیبی از state استاندارد React، در کامپوننتهای متکی به خود و Redux، در کامپوننتهایی که باید اطلاعاتی را با هم به اشتراک بگذارند، باشد. برای مثال، کامپوننت مثال جاری، واقعا نیازی را به Redux، برای مدیریت حالت خود، ندارد؛ هدف ما در اینجا بررسی نحوهی برقراری ارتباطات یک سیستم مبتنی بر Redux، در برنامههای React است.
بنابراین در اینجا و کامپوننتی که قرار است از Redux برای مدیریت حالت خود استفاده کند، هر اطلاعاتی که به آن از طریق react-redux store وارد میشود، از طریق props به آن ارسال خواهد شد. برای مثال در اینجا مقدار count، از طریق props خوانده میشود و همچنین امکان ارسال action ای خاص به متد reducer تعریف شده نیز باید تعریف شود. بنابراین در ادامه نیاز داریم تا یک کامپوننت React را به redux store متصل کنیم. برای این منظور فایل جدید src\containers\Counter.js را با محتوای زیر ایجاد میکنیم:
ابتدا متد connect را از react-redux دریافت میکنیم. connect خود متدی است که منتظر یک کامپوننت React است؛ مانند Counter. همچنین به عنوان پارامتر، توابعی را دریافت میکند که اطلاعات redux store را به کامپوننت، نگاشت میکنند؛ مانند props و actions. در اینجا دو تابع نگاشت state به props و همچنین dispatch به props را ملاحظه میکنید (توابع mapStateToProps و mapDispatchToProps)؛ هرچند الزامی نیست، ولی بهتر است از همین روش نامگذاری استفاده شود.
زمانیکه در مورد store در redux صحبت میشود، داخل آن یک شیء بزرگ state قرار گرفتهاست که حاوی کل state برنامهاست. اما شاید هر کامپوننت به تمام آن نیازی نداشته باشد. برای مثال شاید کامپوننت شمارشگر، اهمیتی به اطلاعات خطاهای سیستم و یا کاربر وارد شدهی به سیستم که در شیء کلی state موجود در store وجود دارند، ندهد. به همین جهت متد mapStateToProps، کل state برنامه را دریافت کرده و به ما اجازه میدهد تا تنها اطلاعاتی را که از آن نیاز داریم، به صورت props دریافت کنیم. به این ترتیب از رندر مجدد این کامپوننت نیز جلوگیری خواهد شد؛ چون این کامپوننت دیگر وابستهی به تغییرات سایر اجزای کل state برنامه، نخواهد بود و اگر آنها تغییر کردند، این کامپوننت رندر مجدد نخواهد شد.
بنابراین میتوان متد mapStateToProps را به صورت کلی زیر نیز تعریف کرد:
هرچند این روش در مثال ما بدون مشکل کار میکند، اما چون کل state را دریافت میکند، مشکل رندر مجدد کامپوننت را به ازای هر تغییری در state کلی برنامه به همراه خواهد داشت.
یک نکته: اگر کامپوننتی نیاز به تامین state خود را از طریق props نداشت و فقط کارش صدور رخدادها است، میتوان پارامتر اول متد connect را نال وارد کرد.
پارامتر dispatch متد mapDispatchToProps، به متد store.dispatch اشاره میکند. بنابراین توسط آن امکان ارسال actions را میسر کرده و میتوان state را توسط reducerهای تعریف شده، تغییر داد که در نتیجهی آن props جدیدی به کامپوننت منتقل میشوند. این تابع نیز یک شیء را باز میگرداند. این شیء را فعلا با یک متد دلخواه مقدار دهی میکنیم که توسط پارامتر dispatch رسیدهی به آن، متد action creator تعریف شدهی در فایل src\actions\index.js را به نام incrementValue، فراخوانی میکند؛ دقیقا عملی شبیه به فراخوانی store.dispatch(createAddAction(2)) در قسمت قبل که از آن برای ارسال یک اکشن، به reducer متناظری استفاده شد.
یک نکته: اگر کامپوننتی کار صدور رخدادها را انجام نمیدهد، میتوان پارامتر دوم متد connect را بطور کامل حذف کرد و قید نکرد.
استفاده از کامپوننت جدید خروجی متد connect، جهت تامین props کامپوننت شمارشگر
در انتهای فایل src\components\counter.jsx، چنین سطری درج شدهاست:
این شیء حاصل، به خودی خود، سبب بروز تغییری در کامپوننت شمارشگر نمیشود. بلکه یک کامپوننت دربرگیرندهی کامپوننت Counter را ایجاد میکند (به همین جهت آنرا در پوشهی containers یا دربرگیرندهها قرار دادیم). بنابراین برای استفادهی از آن، به کامپوننت src\App.js مراجعه کرده و جائیکه المان کامپوننت Counter قبلی درج شده، آنرا به صورت زیر تغییر میدهیم:
ابتدا کامپوننت جدید CounterContainer را که تبادل اطلاعات بین کامپوننت Counter اصلی و state و action مخزن redux را برقرار میکند، import کرده و سپس المان جدید آنرا جایگزین المان کامپوننت شمارشگر اصلی میکنیم.
اکنون کامپوننت شمارشگر src\components\counter.jsx، دو شیء را از طریق props دریافت میکند؛ یکی کل state است که خاصیت count داخل آن قرار دارد و از طریق mapStateToProps تامین میشود. دیگری متد increment ای است که در متد mapDispatchToProps تعریف کردیم و کار صدور رخدادی را به reducer متناظر، انجام میدهد. به همین جهت تغییرات ذیل را در کامپوننت Counter اعمال میکنیم:
لاگ اولین بار دریافت this.pros این کامپوننت که اکنون توسط دربرگیرندهی آن ارائه میشود، به صورت زیر است:
به همین جهت، خاصیت تو در توی this.props.counterReducer.count و همچنین اشارهگر به متد increment، توسط Object Destructuring به صورت زیر از this.props دریافتی، تجزیه شدهاند:
سپس مقدار count، توسط span نمایش داده و همچنین دکمه + را به صورت onClick={increment} تکمیل کردهایم تا با کلیک بر روی آن، متد increment که در حقیقت معادل فراخوانی store.dispatch(incrementValue()) است، اجرا شود. حاصل آن، افزایش مقدار شمارشگر است:
جزئیات کار با Redux store را نیز میتوان در افزونهی redux dev tools مشاهده کرد:
این افزونه در نوار ابزار پایین آن، امکان export کل state و سپس import و بازیابی آنرا نیز به همراه دارد.
دریافت props از طریق کامپوننت دربرگیرنده و ارسال آن به کامپوننت اصلی
فرض کنید نیاز باشد تا اطلاعاتی را به صورت متداول React از طریق props، به کامپوننت دربرگیرندهی کامپوننت شمارشگر ارسال کرد:
برای دسترسی به آن، پارامتر دومی به متد mapStateToProps به نام ownProps اضافه میشود که حاوی props ارسالی به کامپوننت container است:
در این حالت اگر نیاز به انتقال آن به کامپوننت اصلی بود، میتوان شیء بازگشت داده شدهی از mapStateToProps را به همراه یک سری خواص سفارشی دریافتی از ownProps، تعریف کرد.
پیاده سازی دکمهی کاهش مقدار شمارشگر
پس از آشنایی با روش کلی برقراری اتصالات سیستم react-redux، پیاده سازی دکمهی کاهش مقدار شمارشگر بسیار سادهاست و شامل مراحل زیر است:
1) ایجاد نام نوع اکشن متناظر با دکمهی کاهش مقدار
به فایل src\constants\ActionTypes.js، نوع جدید کاهشی را اضافه میکنیم:
2) ایجاد متد Action Creator
در فایل src\actions\index.js، متد ایجاد کنندهی شیء اکشن ارسالی به reducer متناظری را تعریف میکنیم تا بتوان بر اساس نوع آن در reducer کاهشی، منطق کاهش را پیاده سازی کرد:
3) ایجاد تابع reducer مخصوص کاهش مقدار
اکنون در فایل src\reducers\counter.js، بر اساس نوع شیء رسیده، تصمیم به کاهش یا افزایش مقدار موجود در state گرفته میشود:
4) تامین state کامپوننت شمارشگر از طریق props
در ادامه نیاز است بتوان اکشن کاهش را به این reducer ارسال کرد. به همین جهت به کامپوننت دربرگیرندهی کامپوننت شمارشگر در فایل src\containers\Counter.js مراجعه کرده و به شیء خروجی متد mapDispatchToProps، متد کاهش را اضافه میکنیم:
5) استفاده از نتایج دریافتی از props
در آخر به فایل src\components\counter.jsx مراجعه کرده و اشارهگر به متد decrement را از طریق this.props دریافت میکنیم:
سپس آنرا به onClick دکمهی کاهش، انتساب خواهیم داد:
به عنوان تمرین، پیاده سازی دکمهی Reset را نیز انجام دهید که جزئیات آن بسیار شبیه به دو مثال قبلی افزودن و کاهش مقدار شمارشگر است.
بهبود کیفیت کدهای کامپوننت دربرگیرندهی کامپوننت Counter
متد mapDispatchToProps فایل src\containers\Counter.js اکنون چنین شکلی را پیدا کردهاست:
میتوان با استفاده از تابع bindActionCreators که در قسمت قبل در مورد آن بحث شد، تعریف آنرا به صورت زیر خلاصه کرد:
با استفاده از تابع bindActionCreators کتابخانهی redux، میتوان تمام action creators واقع در فایل src\actions\index.js را به صورت یک شیء به آن ارسال کرد و پارامتر دوم آنرا نیز به store.dispatch یا در اینجا به همان dispatch دریافتی توسط پارامتر dispatch متد mapDispatchToProps، تنظیم کرد. البته در این حالت props دریافتی در کامپوننت شمارشگر به صورت زیر تغییر میکنند:
به همین جهت نیاز است در متد رندر کامپوننت src\components\counter.jsx، نامهایی را که به متدهای action creator اشاره میکنند، به صورت زیر تغییر داد:
و همچنین نامهای منتسب به onClickها را نیز بر این اساس، اصلاح کرد.
روش دوم: در نگارشهای اخیر react-redux میتوان متد mapDispatchToProps را به صورت زیر نیز خلاصه و تعریف کرد که بسیار سادهتر است:
البته در این حالت نیز مابقی آن که شامل تغییر نامها میشود، یکسان است.
همچنین بجای بازگشت کل state در متد mapStateToProps، میتوان تنها خواص مدنظر را بازگشت داد:
در این حالت props ارسالی به کامپوننت یک چنین شکلی را پیدا میکنند:
بنابراین باید در متد رندر کامپوننت شمارشگر، خاصیت count را به صورت معمولی دریافت کرد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: state-management-redux-mobx-part03.zip
نصب پیشنیازها
میتوان همانند قسمت قبل، تمام کارها را با کتابخانهی redux انجام داد و یا میتوان قسمت به روز رسانی UI آنرا و همچنین مدیریت state را به کتابخانهی ساده کنندهی دیگری به نام react-redux واگذار کرد. به همین جهت در ادامهی همان برنامهی قسمت قبل، دو کتابخانهی redux و همچنین react-redux را به همراه types آن نصب میکنیم (نصب types، سبب ارائهی intellisense بهتری در VSCode میشود؛ حتی اگر نخواهیم با TypeScript کار کنیم).
برای این منظور پس از باز کردن پوشهی اصلی برنامه توسط VSCode، دکمههای ctrl+` را فشرده (ctrl+back-tick) و دستورات زیر را در ترمینال ظاهر شده وارد کنید:
> npm install --save redux react-redux > npm install --save-dev @types/react-redux
> npm install --save bootstrap
import "bootstrap/dist/css/bootstrap.css";
معرفی ساختار ابتدایی برنامه
برنامهای را که در این قسمت بررسی میکنیم، ساختار بسیار سادهای را داشته و به همراه دو دکمهی افزایش و کاهش مقدار یک شمارشگر است؛ به همراه دکمهی برای به حالت اول در آوردن آن. هدف اصلی دنبال شدهی در اینجا نیز نحوهی برپایی redux و همچنین react-redux و اتصال آنها به برنامهی React جاری است:
به همین جهت ابتدا کامپوننت جدید src\components\counter.jsx را به نحو زیر تشکیل میدهیم تا markup ابتدایی فوق را به همراه سه دکمه و یک span، برای نمایش مقدار شمارشگر، رندر کند:
import React, { Component } from "react"; class Counter extends Component { render() { return ( <section className="card mt-5"> <div className="card-body text-center"> <span className="badge m-2 badge-primary">0</span> </div> <div className="card-footer"> <div className="d-flex justify-content-center align-items-center"> <button className="btn btn-secondary btn-sm">+</button> <button className="btn btn-secondary btn-sm m-2">-</button> <button className="btn btn-danger btn-sm">Reset</button> </div> </div> </section> ); } } export default Counter;
import "./App.css"; import React from "react"; import Counter from "./components/counter"; function App() { return ( <main className="container"> <Counter /> </main> ); } export default App;
پوشه بندی مخصوص برنامههای مبتنی بر Redux
هدف ما در ادامه ایجاد یک store مخصوص redux است و سپس اتصال آن به کامپوننت شمارشگر برنامه. به همین جهت نیاز به 4 پوشهی جدید، برای مدیریت بهتر برنامه خواهیم داشت:
- پوشه constants: برای اینکه نام رشتهای نوع اکشنهای مختلف را بتوانیم در قسمتهای مختلف برنامه استفاده کنیم، بهتر است فایل جدید src\actions\index.js را ایجاد کرده و این ثوابت را داخل آن export کنیم.
- پوشهی actions: در فایل جدید src\actions\index.js، تمام متدهای ایجاد کنندهی شیء خاص action، که در قسمت قبل در مورد آن بحث شد، قرار میگیرند. نمونهی آن، متد createAddAction قسمت قبل است.
- پوشهی reducers: تمام توابع reducer برنامه را در فایلهای مجزایی در پوشهی reducers قرار میدهیم. سپس در فایل src\reducers\index.js با استفاده از متد combineReducer آنها را یکی کرده و به متد createStore ارسال میکنیم.
- پوشهی containers: این پوشه جائی است که کار فراخوانی متد connect کتابخانهی react-redux به ازای هر کامپوننت استفاده کنندهی از redux store، صورت میگیرد.
این موارد را با جزئیات بیشتری در ادامه بررسی میکنیم.
ایجاد نام نوع اکشن متناظر با دکمهی افزودن مقدار
میخواهیم با کلیک بر روی دکمهی +، مقدار شمارشگر افزایش یابد. به همین جهت نیاز به یک نام وجود دارد تا در تابع Reducer متناظر و قسمتهای دیگر برنامه، بتوان بر اساس آن، این اکشن خاص را شناسایی کرد و سپس عکس العمل نشان داد. به همین جهت فایل جدید src\constants\ActionTypes.js را ایجاد کرده و به صورت زیر تکمیل میکنیم:
export const Increment = "Increment";
ایجاد متد Action Creator
در قسمت قبل مشاهده کردیم که شیء ارسالی به یک reducer از طریق dispatch یک action خاص، دارای فرمت ویژهی زیر است:
{ type: "ADD", payload: { amount // = amount: amount }, meta: {} }
import * as types from "../constants/ActionTypes"; export const incrementValue = () => ({ type: types.Increment });
ایجاد تابع reducer مخصوص افزودن مقدار
ابتدا فایل جدید src\reducers\counter.js را با محتوای زیر ایجاد میکنیم:
import * as types from "../constants/ActionTypes"; const initialState = { count: 0 }; export default function counterReducer(state = initialState, action) { if (action.type === types.Increment) { return { count: state.count + 1 }; } return state; }
- سپس میخواهیم رویداد کلیک بر روی دکمه + را مدیریت کنیم. به همین جهت نیاز به یک اکشن جدید به نام Increment داریم که توسط مقدار ثابت رشتهای types.Increment، از فایل مجزای src\constants\ActionTypes.js، تامین میشود.
- پس از مشخص کردن نوع action ای که قرار است مدیریت شود و همچنین ایجاد متدی برای تولید شیء حاوی اطلاعات آن که در فایل src\actions\index.js قرار دارد، اکنون میتوان متد reducer را که state و action را دریافت میکند و سپس state جدیدی را بر اساس action.type دریافتی و در صورت نیاز بازگشت میدهد، ایجاد کرد. این متد بررسی میکند که آیا action.type رسیده همان ثابت Increment است؟ اگر بله، بجای تغییر مستقیم state.count، یک شیء جدید را بازگشت میدهد. البته روش صحیحتر اینکار را در قسمت اول این سری با معرفی روشهایی برای کپی اشیاء و آرایهها، بررسی کردیم. در اینجا جهت سادگی بیشتر، یک شیء کاملا جدید را دستی ایجاد میکنیم. در آخر اگر action.type رسیده قابل پردازش نبود، همان state ابتدایی دریافتی را بازگشت میدهیم تا در صورت وجود چندین reducer تعریف شدهی در سیستم، زنجیرهی آنها قابل پردازش باشد. این مورد را در قسمت قبل، ذیل عنوان «بررسی تابع combineReducers با یک مثال» بیشتر بررسی کردیم.
پس از ایجاد reducer اختصاصی عمل افزودن مقدار شمارشگر، فایل جدید src\reducers\index.js را نیز با محتوای زیر ایجاد میکنیم:
import { combineReducers } from "redux"; import counterReducer from "./counter"; const rootReducer = combineReducers({ counterReducer }); export default rootReducer;
ایجاد store مخصوص Redux
تا اینجا رسیدیم به یک rootReducer متشکل از تمام reducerهای سفارشی برنامه. اکنون بر اساس آن در فایل src\index.js، یک store جدید را ایجاد میکنیم:
import { createStore } from "redux"; import reducer from "./reducers"; //... const store = createStore( reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ); //...
نکته 2: در اینجا روش فعالسازی افزونهی redux-devtools را نیز ملاحظه میکنید. ابتدا بررسی میشود که آیا متد ویژهی فراخوانی این افزونه وجود دارد یا خیر؟ اگر بله، فراخوانی میشود. بدون این پارامتر دوم، افزونهی redex dev tools، هیچ خروجی را نمایش نخواهد داد.
اتصال React به Redux
کتابخانهی react-redux تنها به همراه دو شیء مهم connect و Provider است. شیء Provider آن شبیه به Context API خود React است و هدف آن، ارسال ارجاعی از store ایجاد شده، به برنامهی React است. پس از ایجاد store در فایل src\index.js، اکنون نوبت به اتصال آن به برنامهی React ای جاری است. به همین جهت در بالاترین سطح برنامه، ابتدا شیء کامپوننت App را با شیء Provider محصور میکنیم:
import { Provider } from "react-redux"; import { createStore } from "redux"; import reducer from "./reducers"; // ... const store = createStore( reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") );
تامین state کامپوننت شمارشگر از طریق props
همانطور که عنوان شد، کامپوننت Counter به همراه state نیست و ما قصد نداریم در آن از state خود React استفاده کنیم؛ البته فلسفهی آنرا در قسمت اول این سری بررسی کردیم و همچنین اگر کامپوننتی نیاز به اشتراک گذاری اطلاعات خودش را با لایههای زیرین یا بالاتر از خود ندارد، شاید اصلا نیازی به Redux نداشته باشد و همان state استاندارد React برای آن کافی است. بنابراین میتوان برنامهای را داشت که ترکیبی از state استاندارد React، در کامپوننتهای متکی به خود و Redux، در کامپوننتهایی که باید اطلاعاتی را با هم به اشتراک بگذارند، باشد. برای مثال، کامپوننت مثال جاری، واقعا نیازی را به Redux، برای مدیریت حالت خود، ندارد؛ هدف ما در اینجا بررسی نحوهی برقراری ارتباطات یک سیستم مبتنی بر Redux، در برنامههای React است.
بنابراین در اینجا و کامپوننتی که قرار است از Redux برای مدیریت حالت خود استفاده کند، هر اطلاعاتی که به آن از طریق react-redux store وارد میشود، از طریق props به آن ارسال خواهد شد. برای مثال در اینجا مقدار count، از طریق props خوانده میشود و همچنین امکان ارسال action ای خاص به متد reducer تعریف شده نیز باید تعریف شود. بنابراین در ادامه نیاز داریم تا یک کامپوننت React را به redux store متصل کنیم. برای این منظور فایل جدید src\containers\Counter.js را با محتوای زیر ایجاد میکنیم:
import { connect } from "react-redux"; import { incrementValue } from "../actions"; import Counter from "../components/counter"; const mapStateToProps = state => { return state; }; const mapDispatchToProps = dispatch => { return { increment() { dispatch(incrementValue()); } }; }; export default connect(mapStateToProps, mapDispatchToProps)(Counter);
زمانیکه در مورد store در redux صحبت میشود، داخل آن یک شیء بزرگ state قرار گرفتهاست که حاوی کل state برنامهاست. اما شاید هر کامپوننت به تمام آن نیازی نداشته باشد. برای مثال شاید کامپوننت شمارشگر، اهمیتی به اطلاعات خطاهای سیستم و یا کاربر وارد شدهی به سیستم که در شیء کلی state موجود در store وجود دارند، ندهد. به همین جهت متد mapStateToProps، کل state برنامه را دریافت کرده و به ما اجازه میدهد تا تنها اطلاعاتی را که از آن نیاز داریم، به صورت props دریافت کنیم. به این ترتیب از رندر مجدد این کامپوننت نیز جلوگیری خواهد شد؛ چون این کامپوننت دیگر وابستهی به تغییرات سایر اجزای کل state برنامه، نخواهد بود و اگر آنها تغییر کردند، این کامپوننت رندر مجدد نخواهد شد.
بنابراین میتوان متد mapStateToProps را به صورت کلی زیر نیز تعریف کرد:
const mapStateToProps = (state) => { return state };
یک نکته: اگر کامپوننتی نیاز به تامین state خود را از طریق props نداشت و فقط کارش صدور رخدادها است، میتوان پارامتر اول متد connect را نال وارد کرد.
پارامتر dispatch متد mapDispatchToProps، به متد store.dispatch اشاره میکند. بنابراین توسط آن امکان ارسال actions را میسر کرده و میتوان state را توسط reducerهای تعریف شده، تغییر داد که در نتیجهی آن props جدیدی به کامپوننت منتقل میشوند. این تابع نیز یک شیء را باز میگرداند. این شیء را فعلا با یک متد دلخواه مقدار دهی میکنیم که توسط پارامتر dispatch رسیدهی به آن، متد action creator تعریف شدهی در فایل src\actions\index.js را به نام incrementValue، فراخوانی میکند؛ دقیقا عملی شبیه به فراخوانی store.dispatch(createAddAction(2)) در قسمت قبل که از آن برای ارسال یک اکشن، به reducer متناظری استفاده شد.
یک نکته: اگر کامپوننتی کار صدور رخدادها را انجام نمیدهد، میتوان پارامتر دوم متد connect را بطور کامل حذف کرد و قید نکرد.
استفاده از کامپوننت جدید خروجی متد connect، جهت تامین props کامپوننت شمارشگر
در انتهای فایل src\components\counter.jsx، چنین سطری درج شدهاست:
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
import "./App.css"; import React from "react"; import CounterContainer from "./containers/Counter"; function App() { return ( <main className="container"> <CounterContainer /> </main> ); } export default App;
اکنون کامپوننت شمارشگر src\components\counter.jsx، دو شیء را از طریق props دریافت میکند؛ یکی کل state است که خاصیت count داخل آن قرار دارد و از طریق mapStateToProps تامین میشود. دیگری متد increment ای است که در متد mapDispatchToProps تعریف کردیم و کار صدور رخدادی را به reducer متناظر، انجام میدهد. به همین جهت تغییرات ذیل را در کامپوننت Counter اعمال میکنیم:
import React, { Component } from "react"; class Counter extends Component { render() { console.log("props", this.props); const { counterReducer: { count }, increment } = this.props; return ( <section className="card mt-5"> <div className="card-body text-center"> <span className="badge m-2 badge-primary">{count}</span> </div> <div className="card-footer"> <div className="d-flex justify-content-center align-items-center"> <button className="btn btn-secondary btn-sm" onClick={increment}> + </button> <button className="btn btn-secondary btn-sm m-2">-</button> <button className="btn btn-danger btn-sm">Reset</button> </div> </div> </section> ); } } export default Counter;
به همین جهت، خاصیت تو در توی this.props.counterReducer.count و همچنین اشارهگر به متد increment، توسط Object Destructuring به صورت زیر از this.props دریافتی، تجزیه شدهاند:
const { counterReducer: { count }, increment } = this.props;
جزئیات کار با Redux store را نیز میتوان در افزونهی redux dev tools مشاهده کرد:
این افزونه در نوار ابزار پایین آن، امکان export کل state و سپس import و بازیابی آنرا نیز به همراه دارد.
دریافت props از طریق کامپوننت دربرگیرنده و ارسال آن به کامپوننت اصلی
فرض کنید نیاز باشد تا اطلاعاتی را به صورت متداول React از طریق props، به کامپوننت دربرگیرندهی کامپوننت شمارشگر ارسال کرد:
function App() { const prop1 = 123 return ( <main className="container"> <CounterContainer prop1={prop1} /> </main> ); }
const mapStateToProps = (state, ownProps) => { console.log("mapStateToProps", { state, ownProps }); return state; };
پیاده سازی دکمهی کاهش مقدار شمارشگر
پس از آشنایی با روش کلی برقراری اتصالات سیستم react-redux، پیاده سازی دکمهی کاهش مقدار شمارشگر بسیار سادهاست و شامل مراحل زیر است:
1) ایجاد نام نوع اکشن متناظر با دکمهی کاهش مقدار
به فایل src\constants\ActionTypes.js، نوع جدید کاهشی را اضافه میکنیم:
export const Decrement = "Decrement";
در فایل src\actions\index.js، متد ایجاد کنندهی شیء اکشن ارسالی به reducer متناظری را تعریف میکنیم تا بتوان بر اساس نوع آن در reducer کاهشی، منطق کاهش را پیاده سازی کرد:
export const decrementValue = () => ({ type: types.Decrement });
اکنون در فایل src\reducers\counter.js، بر اساس نوع شیء رسیده، تصمیم به کاهش یا افزایش مقدار موجود در state گرفته میشود:
export default function counterReducer(state = initialState, action) { // ... if (action.type === types.Decrement) { return { count: state.count - 1 }; } return state; }
در ادامه نیاز است بتوان اکشن کاهش را به این reducer ارسال کرد. به همین جهت به کامپوننت دربرگیرندهی کامپوننت شمارشگر در فایل src\containers\Counter.js مراجعه کرده و به شیء خروجی متد mapDispatchToProps، متد کاهش را اضافه میکنیم:
import { decrementValue, incrementValue } from "../actions"; // ... const mapDispatchToProps = dispatch => { return { // ... decrement() { dispatch(decrementValue()); } }; };
در آخر به فایل src\components\counter.jsx مراجعه کرده و اشارهگر به متد decrement را از طریق this.props دریافت میکنیم:
const { // ... decrement } = this.props;
<button className="btn btn-secondary btn-sm m-2" onClick={decrement} > - </button>
به عنوان تمرین، پیاده سازی دکمهی Reset را نیز انجام دهید که جزئیات آن بسیار شبیه به دو مثال قبلی افزودن و کاهش مقدار شمارشگر است.
بهبود کیفیت کدهای کامپوننت دربرگیرندهی کامپوننت Counter
متد mapDispatchToProps فایل src\containers\Counter.js اکنون چنین شکلی را پیدا کردهاست:
const mapDispatchToProps = dispatch => { return { increment() { dispatch(incrementValue()); }, decrement() { dispatch(decrementValue()); } }; };
import { bindActionCreators } from "redux"; // ... const mapDispatchToProps = dispatch => { return bindActionCreators( { incrementValue, decrementValue }, dispatch ); };
به همین جهت نیاز است در متد رندر کامپوننت src\components\counter.jsx، نامهایی را که به متدهای action creator اشاره میکنند، به صورت زیر تغییر داد:
const { counterReducer: { count }, incrementValue, decrementValue } = this.props;
روش دوم: در نگارشهای اخیر react-redux میتوان متد mapDispatchToProps را به صورت زیر نیز خلاصه و تعریف کرد که بسیار سادهتر است:
const mapDispatchToProps = { incrementValue, decrementValue };
همچنین بجای بازگشت کل state در متد mapStateToProps، میتوان تنها خواص مدنظر را بازگشت داد:
const mapStateToProps = state => { //return state; return { count: state.counterReducer.count }; };
بنابراین باید در متد رندر کامپوننت شمارشگر، خاصیت count را به صورت معمولی دریافت کرد:
const { //counterReducer: { count }, count, incrementValue, decrementValue } = this.props;
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: state-management-redux-mobx-part03.zip
در قسمت قبل بیان شد که Angular برای اعمال Pipe بر روی Template expressions باید تمامی رخدادهای برنامه را تحت نظر قرار داده و با مشاهدهی هر تغییری بر روی عبارت ورودی Pipe، فراخوانی Pipe را آغاز کند. از جمله این رخدادها میتوان به رخدادهای mouse move، timer tick، server response و فشرده شدن کلیدهای ماوس و یا کیبورد اشاره کرد. واضح است که بررسی تغییرات عبارت در این همه رخداد میتواند مخرب باشد و بر روی کارآئی (Performance) تاثیر منفی خواهد گذاشت. اما Angular برای حل این مشکل و همچنین هنگام مشاهده سریع تغییرات هنگام استفاده از Pipeها، الگوریتمهای سریع و سادهای در نظر گرفته است که آنها را در این بخش مورد برسی قرار خواهیم داد.
Pipeهای Pure و Impure
Pipeها کلا در دو دستهی Pure و Impure قرار میگیرند. هنگام ساخت Pipe سفارشی در صورتیکه نوع Pipe مشخص نشود، به صورت پیش فرض از نوع Pure خواهد بود. برای تعریف Pipeهایی از نوع Impure کافی است در متادیتای Pipe@، پرچم Pure را به مقدار false تنظیم کنید.
تفاوت این Pipeها در زمان فراخوانی دوباره آنها است.
Pure Pipe
این نوع Pipeها تنها زمانی فراخوانی مجدد میشوند که یک تغییر محض (Pure Change) بر روی عبارت ورودی آنها رخ دهد. هر نوع تغییری بر روی عبارات ورودی از جنس string ، number ، Boolean ، Symbol و عبارات اولیه، یا هرنوع تغییری در ارجاع یک شیء مانند Date ، Array ، Function و Object نیز تغییر محض محسوب میشود. به عنوان مثال هیچکدام از تغییرات زیر یک تغییر محض محسوب نمیشوند:
زیرا با اضافه شدن عنصری به یک آرایه یا تغییر خصوصیتی از یک شیء، باعث تغییری در ارجاع آنها نمیشود و همانطور که اشاره شد، در عبارات از نوع آرایه و Object، فقط تغییر در ارجاع آنها یک تغییر محض محسوب میشود.
حالا میتوان به این نتیجه رسید که اضافه شدن مقداری به آرایه یا بهروزرسانی یک property از object، باعث فراخوانی مجدد Pure Pipe نخواهد نشد. شاید این نوع از Pipeها محدود کننده باشند، اما بسیار سریع هستند (برسی تغییر در ارجاع یک شیء بسیار سریعتر از بررسی کامل یک شیء، صورت میگیرد).
Impure Pipe
این نوع Pipeها در اغلب رخدادهای کامپوننت از جمله فشره شدن کلید یا حرکت ماوس و رخدادهای دیگر فراخوانی مجدد میشوند. با در نظر گرفتن این نگرانی، هنگام پیاده سازی این نوع Pipeها باید مراقب بود؛ زیرا این نوع Pipeها با اجرای طولانی خود میتوانند رابط کاربری شما را نابود کنند. برای درک کامل تفاوت این دو نوع از Pipeها مثالی را دنبال میکنیم.
مثال: قصد داریم Pipe سفارشی را پیاده سازی کنیم تا آرایهای از اعداد را دریافت و فقط اعداد زوج را فیلتر کرده و نمایش دهد.
برای این منظور یک فایل جدید را با نام even-numbers.pipe.ts با محتویات زیر ایجاد میکنیم:
همانطور که مشخص است این Pipe در متد transform، آرایهای از اعداد را دریافت کرده و فقط اعداد زوج را بازگشت میدهد. حالا باید Pipe تعریف شده خود را در AppModule در قسمت declares تعریف کنیم.
سپس در کامپوننت مورد نظر خود متغیری را به نام numbers از نوع آرایه، با مقدار اولیهی اعداد از یک تا ده، تعریف میکنیم:
برای نمایش این اعداد در رابط کاربری تگهای زیر را به قالب کامپوننت خود اضافه میکنیم:
همچنین با استفاده از تگهای زیر یک input برای اضافه کردن مقدار جدید به آرایه درنظر میگیریم:
تگهای زیر را نیز برای اعمال Pipe نمایش اعداد زوج، به قالب کامپوننت اضافه میکنیم:
بعد از اجرای برنامه، یک عدد جدید زوج را به آرایه اضافه کنید. متوجه خواهید شد با اینکه لیست اعداد در قسمت All numbers بهروز میشوند، ولی Pipe، متوجه تغییری بر روی آرایه نشدهاست و همچنان اعداد قبلی را نمایش میدهد. دلیل این امر همانطور که قبلا نیز اشاره شد، بخاطر Pure بودن Pipe و عدم فراخوانی مجدد این نوع Pipeها در زمان اضافه شدن مقداری به آرایه یا تغییری در خصوصیت یک شیء است.
برای حل این مشکل، هنگام اضافه شدن عدد به آرایه، اگر ارجاع آرایه را تغییر دهیم، Pure Pipe متوجه تغییرات خواهد شد و لیست اعداد را بهروز رسانی میکند (تغییر در ارجاع یک شیء، از نوع تغییرات محض است):
با تغییر نحوه اضافه شدن عنصر به آرایه به شکل بالا خواهیم دید که با افزودن اعداد جدید، لیست اعداد زوج نیز در لحظه اعمال خواهند شد. این راهحل همیشه کارآمد نخواهد بود. همیشه تشخیص محل اضافه شدن عنصر به آرایه در برنامه کار سادهای نیست تا در آنجا ارجاع آرایه را نیز تغییر دهیم. راهحل، استفاده از Impure Pipe است. کافی است متادیتای Pipe@ را هنگام تعریف به شکل زیر تغییر دهید:
کسانیکه با Angular 1.x آشنایی دارند، شاید اکنون متوجه این شدهاند که چرا در Angular به مشابه Angular 1.x دیگر خبری ازfilter و orderBy نیست. با توجه به اینکه این دو فیلتر فقط با عبارات از نوع object سروکار داشتند، پیادهسازی آنها فقط با Impure Pipeها امکان پذیر بود و با توجه به اینکه Impure Pipeها در هر بار چرخه تغییرات کامپوننت اجرا خواهند شد، باعث کندی در صفحات خواهند شد.
Pipeهای Pure و Impure
Pipeها کلا در دو دستهی Pure و Impure قرار میگیرند. هنگام ساخت Pipe سفارشی در صورتیکه نوع Pipe مشخص نشود، به صورت پیش فرض از نوع Pure خواهد بود. برای تعریف Pipeهایی از نوع Impure کافی است در متادیتای Pipe@، پرچم Pure را به مقدار false تنظیم کنید.
@Pipe({ name: 'impurePipe', pure: false })
Pure Pipe
این نوع Pipeها تنها زمانی فراخوانی مجدد میشوند که یک تغییر محض (Pure Change) بر روی عبارت ورودی آنها رخ دهد. هر نوع تغییری بر روی عبارات ورودی از جنس string ، number ، Boolean ، Symbol و عبارات اولیه، یا هرنوع تغییری در ارجاع یک شیء مانند Date ، Array ، Function و Object نیز تغییر محض محسوب میشود. به عنوان مثال هیچکدام از تغییرات زیر یک تغییر محض محسوب نمیشوند:
numbers.push(10); obj.name = ‘javad’;
حالا میتوان به این نتیجه رسید که اضافه شدن مقداری به آرایه یا بهروزرسانی یک property از object، باعث فراخوانی مجدد Pure Pipe نخواهد نشد. شاید این نوع از Pipeها محدود کننده باشند، اما بسیار سریع هستند (برسی تغییر در ارجاع یک شیء بسیار سریعتر از بررسی کامل یک شیء، صورت میگیرد).
Impure Pipe
این نوع Pipeها در اغلب رخدادهای کامپوننت از جمله فشره شدن کلید یا حرکت ماوس و رخدادهای دیگر فراخوانی مجدد میشوند. با در نظر گرفتن این نگرانی، هنگام پیاده سازی این نوع Pipeها باید مراقب بود؛ زیرا این نوع Pipeها با اجرای طولانی خود میتوانند رابط کاربری شما را نابود کنند. برای درک کامل تفاوت این دو نوع از Pipeها مثالی را دنبال میکنیم.
مثال: قصد داریم Pipe سفارشی را پیاده سازی کنیم تا آرایهای از اعداد را دریافت و فقط اعداد زوج را فیلتر کرده و نمایش دهد.
برای این منظور یک فایل جدید را با نام even-numbers.pipe.ts با محتویات زیر ایجاد میکنیم:
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'evenNumbers' }) export class EvenNumbersPipe implements PipeTransform { transform(numbers: Array<number>): Array<number> { var x=numbers.filter(r => r % 2 == 0); return x; } }
// . . . import { EvenNumbersPipe } from './pipes/even-numbers.pipe' @NgModule({ declarations: [ . . . EvenNumbersPipe ], . . . }) export class AppModule { }
سپس در کامپوننت مورد نظر خود متغیری را به نام numbers از نوع آرایه، با مقدار اولیهی اعداد از یک تا ده، تعریف میکنیم:
numbers: Array<number> = [1,2,3,4,5,6,7,8,9,10];
<h1>All numbers</h1> <span *ngFor="let number of numbers"> {{number}} </span>
<p> <input type="text" #number /> <input type="button" (click)="numbers.push(number.value)" value="Add number"/> </p>
تگهای زیر را نیز برای اعمال Pipe نمایش اعداد زوج، به قالب کامپوننت اضافه میکنیم:
<h1>even numbers</h1> <span *ngFor="let number of numbers | evenNumbers"> {{number}} </span>
برای حل این مشکل، هنگام اضافه شدن عدد به آرایه، اگر ارجاع آرایه را تغییر دهیم، Pure Pipe متوجه تغییرات خواهد شد و لیست اعداد را بهروز رسانی میکند (تغییر در ارجاع یک شیء، از نوع تغییرات محض است):
<p> <input type="text" #number /> <input type="button" (click)="numbers = numbers.concat(number.value)" value="Add number"/> </p>
@Pipe({ name: 'evenNumbers', pure: false }) export class EvenNumbersPipe implements PipeTransform { //… }
کسانیکه با Angular 1.x آشنایی دارند، شاید اکنون متوجه این شدهاند که چرا در Angular به مشابه Angular 1.x دیگر خبری ازfilter و orderBy نیست. با توجه به اینکه این دو فیلتر فقط با عبارات از نوع object سروکار داشتند، پیادهسازی آنها فقط با Impure Pipeها امکان پذیر بود و با توجه به اینکه Impure Pipeها در هر بار چرخه تغییرات کامپوننت اجرا خواهند شد، باعث کندی در صفحات خواهند شد.