اشتراکها
تعریف ActionLink زیر را درنظر بگیرید:
پارامترهای دوم و سوم آن که به نامهای یک اکشن متد و کنترلر آن اشاره میکنند، توسط رشتهها تعریف شدهاند. مشکلاتی هم که با رشتهها در حالت کلی وجود دارند به شرح زیر است:
الف) میتوان نام کنترلر یا نام متد را در برنامه تغییر داد. به این ترتیب تمام ActionLink هایی که در برنامه به این کنترلر اشاره میکردند از کار میافتند (تکرار رشتهها به علاوه refactoring friendly نبودن آنها).
ب) برای نوشتن رشتهها intellisense کارآیی ندارد.
ج) امکان بروز اشتباهات تایپی در این بین بسیار زیاد است.
راه حل متداولی که برای حل این نوع مشکلات وجود دارد، تعریف یک کلاس عمومی و معرفی رشتهها به صورت فیلدهایی ثابت در آنها میباشند و سپس استفاده از این فیلدها بجای استفاده مستقیم از رشتهها.
و ... چقدر خوب میشد اگر ابزاری وجود میداشت که کلاسهای کنترلرهای ما را آنالیز میکرد و خودش این ثوابت رشتهای را از آنها استخراج و کلاسهای عمومی یاد شده را تشکیل میداد!
خوشبختانه نیازی به اختراع مجدد چرخ نیست و اینکار توسط پروژهی سورس بازی به نام T4MVC انجام شده است. برای دریافت آن به سایت زیر مراجعه نمائید:
این پروژه توسط David Ebbo از اعضای تیم ASP.NET MVC تهیه شده است.
پس از دریافت پروژه، تنها به دو فایل زیر از آن نیاز داریم:
T4MVC.tt و T4MVC.tt.settings.t4
دو فایل فوق را به درون پوشهای از پروژه جاری MVC خود کپی کنید (مثلا یک پوشه T4MVC را ایجاد و این دو فایل را به آن اضافه کنید). بلافاصله این فایلهای t4 وارد عمل شده و کلاسهای کنترلرها، Viewها، تصاویر و غیره را آنالیز و ... ثوابت رشتهای معادل آنها را تولید میکنند.
اکنون برای استفاده از این کلاسهای تولید شده میتوان به صورت زیر عمل کرد:
اصلاح ActionLinkها و حذف رشتههای موجود در آنها
اینبار بجای اینکه بنویسیم:
میتوان نوشت:
و یا:
برای دسترسی به امکانات آن با نام کلاس MVC شروع میکنیم و سپس برای مثال به نام کنترلر Home رسیده و توسط ActionNames آن به تمام اکشن متدهای موجود در آن میتوان دسترسی داشت.
البته این پروژه بسیار فراتر از تولید فیلدهای strongly typed معادل رشتهها است. همانطور که ملاحظه میکنید، یک سری overload را هم به متدهای پیش فرض ASP.NET MVC اضافه کرده است و حتی بجای معرفی رشته معادل اکشن متد Index، خود این اکشن متد را میتوان معرفی کرد (آرگومانی از نوع ActionResult را هم اضافه کرده است). نمونه دیگر آن به نحو زیر میتواند باشد:
که بجای مورد متداول ذیل قابل استفاده است:
امکان معرفی بهتر نام Partial Viewها
برای مثال اگر پیشتر یک Partial View را به این شکل تعریف میکردید:
اکنون امکان معرفی آن به نحو زیر فراهم شده است:
همچنین تمام این کلاسها در کنترلرها نیز قابل دسترسی هستند:
اینجا دیگر نیازی به ذکر کلاس MVC نبوده و میتوان کار را با کلاس جدید Views شروع کرد. یا اگر نیازی به ذکر نام اکشن متدی در کنترلر جاری بود میتوان از ActionNames مستقیما استفاده کرد.
و یا بجای:
میتوان نوشت:
و یا حتی به صورت خلاصهتر به نحو زیر:
این overload جدیدی است که با T4MVC به پروژه اضافه میشود و یک Action Result را میتواند بپذیرد. به این ترتیب به صورت خودکار نام کنترلر و متد Index آن تنها در یک پارامتر لحاظ میشوند. اگر نیاز به ارسال پارامتری هم به متد Index وجود داشت، همینجا میتوان اینکار را انجام داد (بجای استفاده از anonymously typed objects متداول).
و یا بجای مسیر دهی به شکل زیر:
میتوان نوشت:
همچنین برای مسیر دهی viewهای قرار گرفته در پوشه shared میتوان از MVC.Shared.Views شروع کرد.
امکان معرفی بهتر عناصر استاتیک سایت
این مورد نیز بسیار جالب توجه است. توسط کلاس Links آن میتوان به محتویات استاتیک (تصاویر، فایلهای css و غیره) پوشههای Content و Scripts هم دسترسی یافت و حتی این موارد را نیز refactor کرد:
امکان تعریف بهتر پارامترها و مقادیر route
بجای اینکه routeValues را همانند سابق با anonymously typed objects مقدار دهی کنیم:
اینبار میتوان نوشت:
در overload جدیدی که ملاحظه میکنید، هم دسترسی به متد Index یک کنترلر از نوع strongly typed است و هم امکان تعریف پارامترهای آن به نحو منطقیتری فراهم شده است (متد Comments.Index واقعا وجود خارجی داشته و پارامتری از نوع userName را میپذیرد).
چند نکته جانبی
-این ابزار بر اساس Reflection کار میکند (البته فقط در حین تشکیل خودکار کلاسهای مورد نیاز؛ وگرنه ثوابتی را که ایجاد میکند کامپایل شده و در زمان اجرا سرباری را به برنامه اضافه نمیکنند). بنابراین اگر کلاسی به پروژه اضافه شده است، کامپایل کردن آنرا فراموش نکنید.
-اگر تغییری در فایلهای View، در تعداد و نام آنها صورت گرفت، روی فایل T4MVC.tt کلیک راست کرده و گزینهی اجرای آنرا انتخاب کنید. پس از اینکار، مجددا کامپایل پروژه را فراموش نکنید.
-در فایل T4MVC.tt.settings.t4 یک سری تنظیمات پیش فرض قرار دارند. برای مثال اگر علاقمندید که به این فایلهای تولید شده خودکار، فضای نام سفارشی خاصی را اضافه کنید میشود آرایه ReferencedNamespaces آنرا مقدار دهی کرد.
- overloadهای جدید ActionResult دار آن نسبت به نمونههای استاندارد موجود، بسیار منطقیتر به نظر میرسند.
- توضیحات کامل امکانات T4MVC را در مستندات رسمی آن میتوانید مطالعه کنید.
و ... اگر یک مدت با آن کار کنید خواهید گفت: «من قبلا چطور با ASP.NET MVC کار میکردم؟!»
@Html.ActionLink("text", "Index", "Home")
پارامترهای دوم و سوم آن که به نامهای یک اکشن متد و کنترلر آن اشاره میکنند، توسط رشتهها تعریف شدهاند. مشکلاتی هم که با رشتهها در حالت کلی وجود دارند به شرح زیر است:
الف) میتوان نام کنترلر یا نام متد را در برنامه تغییر داد. به این ترتیب تمام ActionLink هایی که در برنامه به این کنترلر اشاره میکردند از کار میافتند (تکرار رشتهها به علاوه refactoring friendly نبودن آنها).
ب) برای نوشتن رشتهها intellisense کارآیی ندارد.
ج) امکان بروز اشتباهات تایپی در این بین بسیار زیاد است.
راه حل متداولی که برای حل این نوع مشکلات وجود دارد، تعریف یک کلاس عمومی و معرفی رشتهها به صورت فیلدهایی ثابت در آنها میباشند و سپس استفاده از این فیلدها بجای استفاده مستقیم از رشتهها.
و ... چقدر خوب میشد اگر ابزاری وجود میداشت که کلاسهای کنترلرهای ما را آنالیز میکرد و خودش این ثوابت رشتهای را از آنها استخراج و کلاسهای عمومی یاد شده را تشکیل میداد!
خوشبختانه نیازی به اختراع مجدد چرخ نیست و اینکار توسط پروژهی سورس بازی به نام T4MVC انجام شده است. برای دریافت آن به سایت زیر مراجعه نمائید:
این پروژه توسط David Ebbo از اعضای تیم ASP.NET MVC تهیه شده است.
پس از دریافت پروژه، تنها به دو فایل زیر از آن نیاز داریم:
T4MVC.tt و T4MVC.tt.settings.t4
دو فایل فوق را به درون پوشهای از پروژه جاری MVC خود کپی کنید (مثلا یک پوشه T4MVC را ایجاد و این دو فایل را به آن اضافه کنید). بلافاصله این فایلهای t4 وارد عمل شده و کلاسهای کنترلرها، Viewها، تصاویر و غیره را آنالیز و ... ثوابت رشتهای معادل آنها را تولید میکنند.
اکنون برای استفاده از این کلاسهای تولید شده میتوان به صورت زیر عمل کرد:
اصلاح ActionLinkها و حذف رشتههای موجود در آنها
اینبار بجای اینکه بنویسیم:
@Html.ActionLink("text", "Index", "Home")
@Html.ActionLink("text", result: MVC.Home.Index())
@Html.ActionLink("text", MVC.Home.ActionNames.Index, MVC.Home.Name)
برای دسترسی به امکانات آن با نام کلاس MVC شروع میکنیم و سپس برای مثال به نام کنترلر Home رسیده و توسط ActionNames آن به تمام اکشن متدهای موجود در آن میتوان دسترسی داشت.
البته این پروژه بسیار فراتر از تولید فیلدهای strongly typed معادل رشتهها است. همانطور که ملاحظه میکنید، یک سری overload را هم به متدهای پیش فرض ASP.NET MVC اضافه کرده است و حتی بجای معرفی رشته معادل اکشن متد Index، خود این اکشن متد را میتوان معرفی کرد (آرگومانی از نوع ActionResult را هم اضافه کرده است). نمونه دیگر آن به نحو زیر میتواند باشد:
@Url.Action(result: MVC.Article.Delete())
@Url.Action("Delete", "Article")
امکان معرفی بهتر نام Partial Viewها
برای مثال اگر پیشتر یک Partial View را به این شکل تعریف میکردید:
@{ Html.RenderPartial("_ViewPage1"); }
@{ Html.RenderPartial(MVC.Home.Views._ViewPage1); }
return PartialView(Views._ViewPage1);
و یا بجای:
return RedirectToAction(actionName: "Index", controllerName: "Menu");
return RedirectToAction(actionName: MVC.Menu.ActionNames.Index, controllerName: MVC.Menu.Name);
return RedirectToAction(result: MVC.Menu.Index());
و یا بجای مسیر دهی به شکل زیر:
return PartialView("~/Views/CommentsArchive/_LatestCommentsInfo.cshtml", data);
return PartialView(MVC.CommentsArchive.Views._LatestCommentsInfo, data);
امکان معرفی بهتر عناصر استاتیک سایت
این مورد نیز بسیار جالب توجه است. توسط کلاس Links آن میتوان به محتویات استاتیک (تصاویر، فایلهای css و غیره) پوشههای Content و Scripts هم دسترسی یافت و حتی این موارد را نیز refactor کرد:
<img src="@Links.Content.Images.arrow_right_png" alt="arrow" /> <script src="@Links.Scripts.jquery_1_5_1_min_js" type="text/javascript"></script>
امکان تعریف بهتر پارامترها و مقادیر route
بجای اینکه routeValues را همانند سابق با anonymously typed objects مقدار دهی کنیم:
Html.ActionLink(linkText: "عنوان", actionName: "Index", controllerName: "Comments", routeValues: new { userName = @Model.FriendlyName }, htmlAttributes: null))
Html.ActionLink(linkText: "عنوان", result: MVC.Comments.Index(userName: @Model.FriendlyName) htmlAttributes: null))
چند نکته جانبی
-این ابزار بر اساس Reflection کار میکند (البته فقط در حین تشکیل خودکار کلاسهای مورد نیاز؛ وگرنه ثوابتی را که ایجاد میکند کامپایل شده و در زمان اجرا سرباری را به برنامه اضافه نمیکنند). بنابراین اگر کلاسی به پروژه اضافه شده است، کامپایل کردن آنرا فراموش نکنید.
-اگر تغییری در فایلهای View، در تعداد و نام آنها صورت گرفت، روی فایل T4MVC.tt کلیک راست کرده و گزینهی اجرای آنرا انتخاب کنید. پس از اینکار، مجددا کامپایل پروژه را فراموش نکنید.
-در فایل T4MVC.tt.settings.t4 یک سری تنظیمات پیش فرض قرار دارند. برای مثال اگر علاقمندید که به این فایلهای تولید شده خودکار، فضای نام سفارشی خاصی را اضافه کنید میشود آرایه ReferencedNamespaces آنرا مقدار دهی کرد.
- overloadهای جدید ActionResult دار آن نسبت به نمونههای استاندارد موجود، بسیار منطقیتر به نظر میرسند.
- توضیحات کامل امکانات T4MVC را در مستندات رسمی آن میتوانید مطالعه کنید.
و ... اگر یک مدت با آن کار کنید خواهید گفت: «من قبلا چطور با ASP.NET MVC کار میکردم؟!»
مطالب
EF Code First #10
حین کار با ORMهای پیشرفته، ویژگیهای جالب توجهی در اختیار برنامه نویسها قرار میگیرد که در زمان استفاده از کلاسهای متداول SQLHelper از آنها خبری نیست؛ مانند:
الف) Deferred execution
ب) Lazy loading
ج) Eager loading
نحوه بررسی SQL نهایی تولیدی توسط EF
برای توضیح موارد فوق، نیاز به مشاهده خروجی SQL نهایی حاصل از ORM است و همچنین شمارش تعداد بار رفت و برگشت به بانک اطلاعاتی. بهترین ابزاری را که برای این منظور میتوان پیشنهاد داد، برنامه EF Profiler است. برای دریافت آن میتوانید به این آدرس مراجعه کنید: (^) و (^)
پس از وارد کردن نام و آدرس ایمیل، یک مجوز یک ماهه آزمایشی، به آدرس ایمیل شما ارسال خواهد شد.
زمانیکه این فایل را در ابتدای اجرای برنامه به آن معرفی میکنید، محل ذخیره سازی نهایی آن جهت بازبینی بعدی، مسیر MyUserName\Local Settings\Application Data\EntityFramework Profiler خواهد بود.
استفاده از این برنامه هم بسیار ساده است:
الف) در برنامه خود، ارجاعی را به اسمبلی HibernatingRhinos.Profiler.Appender.dll که در پوشه برنامه EFProf موجود است، اضافه کنید.
ب) در نقطه آغاز برنامه، متد زیر را فراخوانی نمائید:
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
نقطه آغاز برنامه میتواند متد Application_Start برنامههای وب، در متد Program.Main برنامههای ویندوزی کنسول و WinForms و در سازنده کلاس App برنامههای WPF باشد.
ج) برنامه EFProf را اجرا کنید.
مزایای استفاده از این برنامه
1) وابسته به بانک اطلاعاتی مورد استفاده نیست. (برخلاف برای مثال برنامه معروف SQL Server Profiler که فقط به همراه SQL Server ارائه میشود)
2) خروجی SQL نمایش داده شده را فرمت کرده و به همراه Syntax highlighting نیز هست.
3) کار این برنامه صرفا به لاگ کردن SQL تولیدی خلاصه نمیشود. یک سری از Best practices را نیز به شما گوشزد میکند. بنابراین اگر نیاز دارید سیستم خود را بر اساس دیدگاه یک متخصص بررسی کنید (یک Code review ارزشمند)، این ابزار میتواند بسیار مفید باشد.
4) میتواند کوئریهای سنگین و سبک را به خوبی تشخیص داده و گزارشات آماری جالبی را به شما ارائه دهد.
5) میتواند دقیقا مشخص کند، کوئری را که مشاهده میکنید از طریق کدام متد در کدام کلاس صادر شده است و دقیقا از چه سطری.
6) امکان گروه بندی خودکار کوئریهای صادر شده را بر اساس DbContext مورد استفاده به همراه دارد.
و ...
استفاده از این برنامه حین کار با EF «الزامی» است! (البته نسخههای NH و سایر ORMهای دیگر آن نیز موجود است و این مباحث در مورد تمام ORMهای پیشرفته صادق است)
مدام باید بررسی کرد که صفحه جاری چه تعداد کوئری را به بانک اطلاعاتی ارسال کرده و به چه نحوی. همچنین آیا میتوان با اعمال اصلاحاتی، این وضع را بهبود بخشید. بنابراین عدم استفاده از این برنامه حین کار با ORMs، همانند راه رفتن در خواب است! ممکن است تصور کنید برنامه دارد به خوبی کار میکند اما ... در پشت صحنه فقط صفحه جاری برنامه، 100 کوئری را به بانک اطلاعاتی ارسال کرده، در حالیکه شما تنها نیاز به یک کوئری داشتهاید.
کلاسهای مدل مثال جاری
کلاسهای مدل مثال جاری از یک دپارتمان که دارای تعدادی کارمند میباشد، تشکیل شده است. ضمنا هر کارمند تنها در یک دپارتمان میتواند مشغول به کار باشد و رابطه many-to-many نیست :
using System.Collections.Generic;
namespace EF_Sample06.Models
{
public class Department
{
public int DepartmentId { get; set; }
public string Name { get; set; }
//Creates Employee navigation property for Lazy Loading (1:many)
public virtual ICollection<Employee> Employees { get; set; }
}
}
namespace EF_Sample06.Models
{
public class Employee
{
public int EmployeeId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
//Creates Department navigation property for Lazy Loading
public virtual Department Department { get; set; }
}
}
نگاشت دستی این کلاسها هم ضرورتی ندارد، زیرا قراردادهای توکار EF Code first را رعایت کرده و EF در اینجا به سادگی میتواند primary key و روابط one-to-many را بر اساس navigation properties تعریف شده، تشخیص دهد.
در اینجا کلاس Context برنامه به شرح زیر است:
using System.Data.Entity;
using EF_Sample06.Models;
namespace EF_Sample06.DataLayer
{
public class Sample06Context : DbContext
{
public DbSet<Department> Departments { set; get; }
public DbSet<Employee> Employees { set; get; }
}
}
و تنظیمات ابتدایی نحوه به روز رسانی و آغاز بانک اطلاعاتی نیز مطابق کدهای زیر میباشد:
using System.Collections.Generic;
using System.Data.Entity.Migrations;
using EF_Sample06.Models;
namespace EF_Sample06.DataLayer
{
public class Configuration : DbMigrationsConfiguration<Sample06Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
protected override void Seed(Sample06Context context)
{
var employee1 = new Employee { FirstName = "f name1", LastName = "l name1" };
var employee2 = new Employee { FirstName = "f name2", LastName = "l name2" };
var employee3 = new Employee { FirstName = "f name3", LastName = "l name3" };
var employee4 = new Employee { FirstName = "f name4", LastName = "l name4" };
var dept1 = new Department { Name = "dept 1", Employees = new List<Employee> { employee1, employee2 } };
var dept2 = new Department { Name = "dept 2", Employees = new List<Employee> { employee3 } };
var dept3 = new Department { Name = "dept 3", Employees = new List<Employee> { employee4 } };
context.Departments.Add(dept1);
context.Departments.Add(dept2);
context.Departments.Add(dept3);
base.Seed(context);
}
}
}
نکته: تهیه خروجی XML از نگاشتهای خودکار تهیه شده
اگر علاقمند باشید که پشت صحنه نگاشتهای خودکار EF Code first را در یک فایل XML جهت بررسی بیشتر ذخیره کنید، میتوان از متد کمکی زیر استفاده کرد:
void ExportMappings(DbContext context, string edmxFile)
{
var settings = new XmlWriterSettings { Indent = true };
using (XmlWriter writer = XmlWriter.Create(edmxFile, settings))
{
System.Data.Entity.Infrastructure.EdmxWriter.WriteEdmx(context, writer);
}
}
بهتر است پسوند فایل XML تولیدی را edmx قید کنید تا بتوان آنرا با دوبار کلیک بر روی فایل، در ویژوال استودیو نیز مشاهده کرد:
using (var db = new Sample06Context())
{
ExportMappings(db, "mappings.edmx");
}
الف) بررسی Deferred execution یا بارگذاری به تاخیر افتاده
برای توضیح مفهوم Deferred loading/execution بهترین مثالی را که میتوان ارائه داد، صفحات جستجوی ترکیبی در برنامهها است. برای مثال یک صفحه جستجو را طراحی کردهاید که حاوی دو تکست باکس دریافت FirstName و LastName کاربر است. کنار هر کدام از این تکست باکسها نیز یک چکباکس قرار دارد. به عبارتی کاربر میتواند جستجویی ترکیبی را در اینجا انجام دهد. نحوه پیاده سازی صحیح این نوع مثالها در EF Code first به چه نحوی است؟
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EF_Sample06.DataLayer;
using EF_Sample06.Models;
namespace EF_Sample06
{
class Program
{
static IList<Employee> FindEmployees(string fName, string lName, bool byName, bool byLName)
{
using (var db = new Sample06Context())
{
IQueryable<Employee> query = db.Employees.AsQueryable();
if (byLName)
{
query = query.Where(x => x.LastName == lName);
}
if (byName)
{
query = query.Where(x => x.FirstName == fName);
}
return query.ToList();
}
}
static void Main(string[] args)
{
// note: remove this line if you received : create database is not supported by this provider.
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample06Context, Configuration>());
var list = FindEmployees("f name1", "l name1", true, true);
foreach (var item in list)
{
Console.WriteLine(item.FirstName);
}
}
}
}
نحوه صحیح این نوع پیاده سازی ترکیبی را در متد FindEmployees مشاهده میکنید. نکته مهم آن، استفاده از نوع IQueryable و متد AsQueryable است و امکان ترکیب کوئریها با هم.
به نظر شما با فراخوانی متد FindEmployees به نحو زیر که هر دو شرط آن توسط کاربر انتخاب شده است، چه تعداد کوئری به بانک اطلاعاتی ارسال میشود؟
var list = FindEmployees("f name1", "l name1", true, true);
شاید پاسخ دهید که سه بار : یکبار در متد db.Employees.AsQueryable و دوبار هم در حین ورود به بدنه شرطهای یاد شده و اینجا است که کسانی که قبلا با رویههای ذخیره شده کار کرده باشند، شروع به فریاد و فغان میکنند که ما قبلا این مسایل رو با یک SP در یک رفت و برگشت مدیریت میکردیم!
پاسخ صحیح: «فقط یکبار»! آنهم تنها در زمان فراخوانی متد ToList و نه قبل از آن.
برای اثبات این مدعا نیاز است به خروجی SQL لاگ شده توسط EF Profiler مراجعه کرد:
SELECT [Extent1].[EmployeeId] AS [EmployeeId],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName],
[Extent1].[Department_DepartmentId] AS [Department_DepartmentId]
FROM [dbo].[Employees] AS [Extent1]
WHERE ([Extent1].[LastName] = 'l name1' /* @p__linq__0 */)
AND ([Extent1].[FirstName] = 'f name1' /* @p__linq__1 */)
IQueryable قلب LINQ است و تنها بیانگر یک عبارت (expression) از رکوردهایی میباشد که مد نظر شما است و نه بیشتر. برای مثال زمانیکه یک IQueryable را همانند مثال فوق فیلتر میکنید، هنوز چیزی از بانک اطلاعاتی یا منبع دادهای دریافت نشده است. هنوز هیچ اتفاقی رخ نداده است و هنوز رفت و برگشتی به منبع دادهای صورت نگرفته است. به آن باید به شکل یک expression builder نگاه کرد و نه لیستی از اشیاء فیلتر شدهی ما. به این مفهوم، deferred execution (اجرای به تاخیر افتاده) نیز گفته میشود.
کوئری LINQ شما تنها زمانی بر روی بانک اطلاعاتی اجرا میشود که کاری بر روی آن صورت گیرد مانند فراخوانی متد ToList، فراخوانی متد First یا FirstOrDefault و امثال آن. تا پیش از این فقط به شکل یک عبارت در برنامه وجود دارد و نه بیشتر.
اطلاعات بیشتر: «تفاوت بین IQueryable و IEnumerable در حین کار با ORMs»
ب) بررسی Lazy Loading یا واکشی در صورت نیاز
در مطلب جاری اگر به کلاسهای مدل برنامه دقت کنید، تعدادی از خواص به صورت virtual تعریف شدهاند. چرا؟
تعریف یک خاصیت به صورت virtual، پایه و اساس lazy loading است و به کمک آن، تا به اطلاعات شیءایی نیاز نباشد، وهله سازی نخواهد شد. به این ترتیب میتوان به کارآیی بیشتری در حین کار با ORMs رسید. برای مثال در کلاسهای فوق، اگر تنها نیاز به دریافت نام یک دپارتمان هست، نباید حین وهله سازی از شیء دپارتمان، شیء لیست کارمندان مرتبط با آن نیز وهله سازی شده و از بانک اطلاعاتی دریافت شوند. به این وهله سازی با تاخیر، lazy loading گفته میشود.
Lazy loading پیاده سازی سادهای نداشته و مبتنی است بر بکارگیری AOP frameworks یا کتابخانههایی که امکان تشکیل اشیاء Proxy پویا را در پشت صحنه فراهم میکنند. علت virtual تعریف کردن خواص رابط نیز به همین مساله بر میگردد، تا این نوع کتابخانهها بتوانند در نحوه تعریف اینگونه خواص virtual در زمان اجرا، در پشت صحنه دخل و تصرف کنند. البته حین استفاده از EF یا انواع و اقسام ORMs دیگر با این نوع پیچیدگیها روبرو نخواهیم شد و تشکیل اشیاء Proxy در پشت صحنه انجام میشوند.
یک مثال: قصد داریم اولین دپارتمان ثبت شده در حین آغاز برنامه را یافته و سپس لیست کارمندان آنرا نمایش دهیم:
using (var db = new Sample06Context())
{
var dept1 = db.Departments.Find(1);
if (dept1 != null)
{
Console.WriteLine(dept1.Name);
foreach (var item in dept1.Employees)
{
Console.WriteLine(item.FirstName);
}
}
}
رفتار یک ORM جهت تعیین اینکه آیا نیاز است برای دریافت اطلاعات بین جداول Join صورت گیرد یا خیر، واکشی حریصانه و غیرحریصانه را مشخص میسازد.
در حالت واکشی حریصانه به ORM خواهیم گفت که لطفا جهت دریافت اطلاعات فیلدهای جداول مختلف، از همان ابتدای کار در پشت صحنه، Join های لازم را تدارک ببین. در حالت واکشی غیرحریصانه به ORM خواهیم گفت به هیچ عنوان حق نداری Join ایی را تشکیل دهی. هر زمانی که نیاز به اطلاعات فیلدی از جدولی دیگر بود باید به صورت مستقیم به آن مراجعه کرده و آن مقدار را دریافت کنی.
به صورت خلاصه برنامه نویس در حین کار با ORM های پیشرفته نیازی نیست Join بنویسد. تنها باید ORM را طوری تنظیم کند که آیا اینکار را حتما خودش در پشت صحنه انجام دهد (واکشی حریصانه)، یا اینکه خیر، به هیچ عنوان SQL های تولیدی در پشت صحنه نباید حاوی Join باشند (lazy loading).
در مثال فوق به صورت خودکار دو کوئری به بانک اطلاعاتی ارسال میگردد:
SELECT [Limit1].[DepartmentId] AS [DepartmentId],
[Limit1].[Name] AS [Name]
FROM (SELECT TOP (2) [Extent1].[DepartmentId] AS [DepartmentId],
[Extent1].[Name] AS [Name]
FROM [dbo].[Departments] AS [Extent1]
WHERE [Extent1].[DepartmentId] = 1 /* @p0 */) AS [Limit1]
SELECT [Extent1].[EmployeeId] AS [EmployeeId],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName],
[Extent1].[Department_DepartmentId] AS [Department_DepartmentId]
FROM [dbo].[Employees] AS [Extent1]
WHERE ([Extent1].[Department_DepartmentId] IS NOT NULL)
AND ([Extent1].[Department_DepartmentId] = 1 /* @EntityKeyValue1 */)
یکبار زمانیکه قرار است اطلاعات دپارتمان یک (db.Departments.Find) دریافت شود. تا این لحظه خبری از جدول Employees نیست. چون lazy loading فعال است و فقط اطلاعاتی را که نیاز داشتهایم فراهم کرده است.
زمانیکه برنامه به حلقه میرسد، نیاز است اطلاعات dept1.Employees را دریافت کند. در اینجا است که کوئری دوم، به بانک اطلاعاتی صادر خواهد شد (بارگذاری در صورت نیاز).
ج) بررسی Eager Loading یا واکشی حریصانه
حالت lazy loading بسیار جذاب به نظر میرسد؛ برای مثال میتوان خواص حجیم یک جدول را به جدول مرتبط دیگری منتقل کرد. مثلا فیلدهای متنی طولانی یا اطلاعات باینری فایلهای ذخیره شده، تصاویر و امثال آن. به این ترتیب تا زمانیکه نیازی به اینگونه اطلاعات نباشد، lazy loading از بارگذاری آنها جلوگیری کرده و سبب افزایش کارآیی برنامه میشود.
اما ... همین lazy loading در صورت استفاده نا آگاهانه میتواند سرور بانک اطلاعاتی را در یک برنامه چندکاربره از پا درآورد! نیازی هم نیست تا شخصی به سایت شما حمله کند. مهاجم اصلی همان برنامه نویس کم اطلاع است!
اینبار مثال زیر را درنظر بگیرید که بجای دریافت اطلاعات یک شخص، مثلا قصد داریم، اطلاعات کلیه دپارتمانها را توسط یک Grid نمایش دهیم (فرقی نمیکند برنامه وب یا ویندوز باشد؛ اصول یکی است):
using (var db = new Sample06Context())
{
foreach (var dept in db.Departments)
{
Console.WriteLine(dept.Name);
foreach (var item in dept.Employees)
{
Console.WriteLine(item.FirstName);
}
}
}
There is already an open DataReader associated with this Command which must be closed first
برای رفع این مشکل نیاز است گزینه MultipleActiveResultSets=True را به کانکشن استرینگ اضافه کرد:
<connectionStrings>
<clear/>
<add
name="Sample06Context"
connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true;MultipleActiveResultSets=True;"
providerName="System.Data.SqlClient"
/>
</connectionStrings>
سؤال: به نظر شما در دو حلقه تو در توی فوق چندبار رفت و برگشت به بانک اطلاعاتی صورت میگیرد؟ با توجه به اینکه در متد Seed ذکر شده در ابتدای مطلب، تعداد رکوردها مشخص است.
پاسخ: 7 بار!
و اینجا است که عنوان شد استفاده از EF Profiler در حین توسعه برنامههای مبتنی بر ORM «الزامی» است! اگر از این نکته اطلاعی نداشتید، بهتر است یکبار تمام صفحات گزارشگیری برنامههای خود را که حاوی یک Grid هستند، توسط EF Profiler بررسی کنید. اگر در این برنامه پیغام خطای n+1 select را دریافت کردید، یعنی در حال استفاده ناصحیح از امکانات lazy loading میباشید.
آیا میتوان این وضعیت را بهبود بخشید؟ زمانیکه کار ما گزارشگیری از اطلاعات با تعداد رکوردهای بالا است، استفاده ناصحیح از ویژگی Lazy loading میتواند به شدت کارآیی بانک اطلاعاتی را پایین بیاورد. برای حل این مساله در زمانهای قدیم (!) بین جداول join مینوشتند؛ الان چطور؟
در EF متدی به نام Include جهت Eager loading اطلاعات موجودیتهای مرتبط به هم درنظر گرفته شده است که در پشت صحنه همینکار را انجام میدهد:
using (var db = new Sample06Context())
{
foreach (var dept in db.Departments.Include(x => x.Employees))
{
Console.WriteLine(dept.Name);
foreach (var item in dept.Employees)
{
Console.WriteLine(item.FirstName);
}
}
}
همانطور که ملاحظه میکنید اینبار به کمک متد Include، نسبت به واکشی حریصانه Employees اقدام کردهایم. اکنون اگر برنامه را اجرا کنیم، فقط یک رفت و برگشت به بانک اطلاعاتی انجام خواهد شد و کار Join نویسی به صورت خودکار توسط EF مدیریت میگردد:
SELECT [Project1].[DepartmentId] AS [DepartmentId],
[Project1].[Name] AS [Name],
[Project1].[C1] AS [C1],
[Project1].[EmployeeId] AS [EmployeeId],
[Project1].[FirstName] AS [FirstName],
[Project1].[LastName] AS [LastName],
[Project1].[Department_DepartmentId] AS [Department_DepartmentId]
FROM (SELECT [Extent1].[DepartmentId] AS [DepartmentId],
[Extent1].[Name] AS [Name],
[Extent2].[EmployeeId] AS [EmployeeId],
[Extent2].[FirstName] AS [FirstName],
[Extent2].[LastName] AS [LastName],
[Extent2].[Department_DepartmentId] AS [Department_DepartmentId],
CASE
WHEN ([Extent2].[EmployeeId] IS NULL) THEN CAST(NULL AS int)
ELSE 1
END AS [C1]
FROM [dbo].[Departments] AS [Extent1]
LEFT OUTER JOIN [dbo].[Employees] AS [Extent2]
ON [Extent1].[DepartmentId] = [Extent2].[Department_DepartmentId]) AS [Project1]
ORDER BY [Project1].[DepartmentId] ASC,
[Project1].[C1] ASC
متد Include در نگارشهای اخیر EF پیشرفت کرده است و همانند مثال فوق، امکان کار با lambda expressions را جهت تعریف خواص مورد نظر به صورت strongly typed ارائه میدهد. در نگارشهای قبلی این متد، تنها امکان استفاده از رشتهها برای معرفی خواص وجود داشت.
همچنین توسط متد Include امکان eager loading چندین سطح با هم نیز وجود دارد؛ مثلا x.Employees.Kids و همانند آن.
چند نکته در مورد نحوه خاموش کردن Lazy loading
امکان خاموش کردن Lazy loading در تمام کلاسهای برنامه با تنظیم خاصیت Configuration.LazyLoadingEnabled کلاس Context برنامه به نحو زیر میسر است:
public class Sample06Context : DbContext
{
public Sample06Context()
{
this.Configuration.LazyLoadingEnabled = false;
}
یا اگر تنها در مورد یک کلاس نیاز است این خاموش سازی صورت گیرد، کلمه کلیدی virtual را حذف کنید. برای مثال با نوشتن public ICollection<Employee> Employees بجای public virtual ICollection<Employee> Employees در اولین بار وهله سازی کلاس دپارتمان، لیست کارمندان آن به نال تنظیم میشود. البته در این حالت null object pattern را نیز فراموش نکنید (وهله سازی پیش فرض Employees در سازنده کلاس):
public class Department
{
public int DepartmentId { get; set; }
public string Name { get; set; }
public ICollection<Employee> Employees { get; set; }
public Department()
{
Employees = new HashSet<Employee>();
}
}
به این ترتیب به خطای null reference object بر نخواهیم خورد. همچنین وهله سازی، با مقدار دهی لیست دریافتی از بانک اطلاعاتی متفاوت است. در اینجا نیز باید از متد Include استفاده کرد.
بنابراین در صورت خاموش کردن lazy loading، حتما نیاز است از متد Include استفاده شود. اگرlazy loading فعال است، جهت تبدیل آن به eager loading از متد Include استفاده کنید (اما اجباری نیست).
در مطلب قبلی با فایلهای پیکربندی وبپک، وب سرور وبپک، لودرها و ... آشنا شدیم .
سپس در ادامه به فایل پیکربندی وبپک مراجعه کرده و قسمت preLoader را به آن اضافه میکنیم:
همچنین برای تکمیل قابل ذکر است که وبپک دارای postLoaders نیز میباشد که پس از Loaderهای اصلی اجرا میشوند.
حال در صورتی که به باندل ساخته شده مراجعه کنید، باندل در حالت Minify شده قرار دارد.
سپس وارد فایل پیکربندی که فقط جهت تولید باندلهای اصلی پروژه ایجاد کردهایم (webpack.prod.config.js) میشویم و کدهای زیر را وارد میکنیم.
در توضیح کدهای بالا در خط اول ابتدا فایل پیکربندی را که در توسعهی عادی پروژه استفاده میشد، میخوانیم و در آبجکتی با نام devConfig ذخیره میکنیم. مزیت اینکار این میباشد که از تعریف تنظیمات تکراری و مورد نیاز جلوگیری میکند (نکته : اگر بخاطر داشته باشید در مطلب قبلی ذکر شد که فایلهای پیکربندی در فرمت commonjs میباشند و در نتیجه امکان وارد کردن آنها با استفاده از تابع require امکان پذیر است).
در کد بالا مواردی را که مورد نیاز است از سورس اصلی در سورس نهایی حذف شود، به لودر معرفی میکنیم. در اینجا تمامی دستوراتی که شامل console.log میشوند از بیلد نهایی باندل حذف خواهند شد.
تنها مرحلهی باقی مانده، فراخوانی وبپک با استفاده از فایل پیکربندی جدید میباشد. برای اینکار با استفاده از پرچم config محل فایل جدید پیکربندی را به وبپک معرفی میکنیم تا از فایل پیکربندی پیش فرض قبلی استفاده نکند.
در خط اول فایل پیکربندی جدید، ماژول توکار path از نود جی اس را وارد میکنیم و سپس کلید جدیدی را به تنظیمات وبپک با نام context اضافه میکنیم که زمینهی فایلهای ورودی را مشخص خواهد کرد. قبلا ذکر شد که فولدری با نام js را ساخته و اسکریپتها را در آن قرار میدهیم. پس با کمک ماژول path این مسیر را به وبپک معرفی میکنیم.
تغییر بعدی را در تنظیمات برای ساخت باندل داریم که مشخص کردن مسیر قرارگیری جدید باندل میباشد که با کلید جدیدی با نام path، مسیر قرارگیری باندل پس از ساخته شدن را به وبپک اطلاع میدهیم و در ادامه کلید دیگری با نام publicPath اضافه شده که راهنمایی برای وب سرور وبپک میباشد تا با استفاده از آن درخواستهایی که به مسیر مشخص شده میآیند، از مسیری که در کلید path نامیده شده سرو شوند.
آخرین تغییر در فایل پیکربندی، مربوط به اضافه شدن آبجکت جدید devServer میباشد که در آن کلیدی اضافه شده که مسیر اصلی فایلهای میزبانی شده را اعلام میکند. به طور مثال فایل html اصلی پروژه را بالاتر اشاره کردیم که در این مسیر قرار میدهیم.
قابل مشاهده است که مسیر باندل ذکر شده در اینجا وجود خارجی ندارد و وبپک آن را با کمک تنظیماتش، به صورت پویا پیدا خواهد کرد. در تصویر زیر سعی بر روشنتر شدن این مسئله شده است.
حال در هنگام فراخوانی وبپک فایل سورس مپ نیز ساخته خواهد شد و احتیاجی به استفاده از پرچم خط فرمان در هنگام فراخوانی نیست.
با اجرای وب سرور وبپک خواهید دید که سورس مپها در منوی توسعه دهندهی مرورگر قابل دستیابی میباشند.
ساخت چندین باندل گوناگون
قصد داریم به پروژه، دو صفحهی دیگر را نیز با نامهای aboutme و contact اضافه کنیم. هر یک از این صفحات اسکریپت مخصوص به خود را خواهد داشت و باندل نهایی نیز شامل تمامی آنها خواهد شد. در صورتی که این خروجی مطلوب ما نباشد و به طور مثال بخواهیم مکانیزمی شبیه به lazy loading اسکریپتها را داشته باشیم و فقط زمانی اسکریپتها بارگذاری شوند که به آنها احتیاج باشد، برای انجام این کار با وبپک به صورت زیر عمل خواهیم کرد.
دو صفحهی html جدید را با عناوین ذکر شدهی بالا به پوشهی assets اضافه میکنیم و برای هریک نیز اسکریپتی با همان نام خواهیم ساخت و در پوشهی js قرار میدهیم.
محتوای صفحات بدین شکل میباشد.
در هر یک از صفحات یک اسکریپت مخصوص آن صفحه و همچنین یک اسکریپت با نام shared.js که قبلا فرض کردیم نقش ماژولی را دارد که در سرتاسر پروژه از آن استفاده میشود، اضافه شده است. حال این تغییرات را در فایل پیکربندی وبپک به آن معرفی میکنیم.
جهت انجام این کار از یک پلاگین وبپک با نام CommonsChunkPlugin کمک گرفتهایم که به ما کمک میکند اسکریپت shared.js را به عنوان یک وابستگی در تمامی صفحاتمان داشته باشیم. نحوهی کار این پلاگین نیز از نامش مشخص است و نقاط Common را میتوان با آن مشخص کرد. تغییر بعدی نیز در کلید entry میباشد که اسکریپتهای جدید را با استفاده از اسمشان به وبپک معرفی کردهایم و سپس در کلید output نیز به وبپک خبر دادهایم که برای هر ورودی، باندل جداگانهی خود را بسازد. تکه کد filename:[name].js به وبپک میگوید که باندلهای جداگانه، با نام خود اسکریپت ساخته شوند و در نهایت کلید جدید plugins به وبپک پلاگین CommonsChunkPlugin را اضافه میکند.
حال با اجرای وبپک میتوان دید که سه باندل ساخته شده که همگی به اسکریپت shared.js وابستگی دارند و اگر این اسکریپت را از صفحات HTML حذف کنید، با خطا رو به رو خواهید شد. این پلاگین قدرت ساخت باندلهایی با خاصیت مشخص کردن وابستگیها و همچنین تو در توییها خاص را نیز دارد. برای مطالعهی بیشتر میتوانید به اینجا مراجعه کنید: پلاگین commonsChunk
در قسمت بعدی با استفاده از وبپک فایلهای css، فونتها و تصاویر را نیز باندل خواهیم کرد.
فایلهای مطلب:
سورس تا قبل از قسمت ایجاد تغییرات در ساختار فایلهای پروژه :dntwebpack-part3-beforeFileAndFolderManagment.zip
سورس برای بعد از ایجاد تغییرات در ساختار فایلهای پروژه : dntwebpack-part3AfterFileOrganization.zip
استفاده از preLoaderها در وبپک
پیشتر با Loaderها آشنا شدیم و دلیل استفادهی از آنها نیز ذکر و Loader تایپ اسکریپت را نیز نصب کرده و با استفاده از آن فایلهای پروژه را ترنسپایل کردیم. اما ممکن است که همهی کارها در استفاده از یک Loader خلاصه نشوند. ممکن است بخواهید از یک ابزار Linting مانند jsHint قبل از اجرای Loader ها بهره ببرید و این دقیقا کاری است که به preLoaderها سپرده میشود. به عنوان مثال پیش لودر jsHint را نصب خواهیم کرد. اضافه کردن preLoaderها در فایل پیکربندی وبپک تفاوتی با Loaderها نخواهد داشت و دارای همان قسمتهایی است که برای Loaderها تعریف کردیم.
پیش از هر کاری ابتدا jsHint را نصب کرده و سپس loader آن را نیز نصب میکنیم. دستورات مورد نیاز، در ادامه آورده شده اند:
npm install -D jsHint jsHint-loader
module.exports = { entry:['./shared.js','./main.ts'] ,output:{ filename:'bundle.js' } ,watch :true ,module:{ preLoaders:[ { test:/\.js$/ ,exclude:/node_modules/ ,loader:'jshint-loader' } ], loaders:[ { test:/\.ts$/ ,exclude:/node_modules/ ,loader:'ts-loader' } ] } }
حال وب سرور وبپک را اجرا میکنیم. در خط فرمان نتیجهی اجرا شدن jsHint در تصویر قابل مشاهده است که از دو فایل پروژه ایراد گرفته است:
همچنین برای تکمیل قابل ذکر است که وبپک دارای postLoaders نیز میباشد که پس از Loaderهای اصلی اجرا میشوند.
Minify کردن باندلها با استفاده از وبپک
در صورتی که باندل ساخته شده تا به اینجای کار را باز کرده باشید، مشاهده کردهاید که باندل ساخته شده Minify شده نیست. سادهترین روش جهت Minify کردن باندل ساخته شده در هنگام فراخوانی وبپک با استفاده از یک پرچم در خط فرمان میباشد. دستور مورد نیاز در ادامه آورده شده است.
// در حالتی که به صورت محلی وبپک نصب شده است npm run webpack -- -p // درحالتی که وبپک به صورت سراسری اجرا میشود webpack -p
اضافه کردن فایل پیکربندی مخصوص بیلدهای اصلی پروژه
قطعا شما نیز در حین توسعهی پروژه از دستورات لاگ یا اندازه گیری زمان اجرای یک قطعه کد و ... استفاده میکنید و حضور این کدها در باندل نهایی دلیلی ندارد. یک راهکار این است که کدهایی را که فقط جهت توسعهی پروژه و دیباگ بودند و سودی در نتیجهی نهایی ندارند، به صورت دستی پاک کنیم و در صورتی که حجم این طور دستورات بالا باشند، قطعا کار جالبی نخواهد بود و همچنین حذف کلی این دستورات نیز در ادامه برای برگشت به پروژه ممکن است مشکل زا باشد.
راهکاری که با وبپک میتوان پیش گرفت این است که از یک Loader جهت بیلدهای اصلی استفاده کرده و مثلن تمامی کامنتها و دستورات لاگ کننده و ... را از بیلد نهایی حذف کنیم. جهت اینکار یک فایل پیکربندی مخصوص را به بیلدهای اصلی به پروژه اضافه میکنیم و اسم فایل را webpack.prod.config.js میگذاریم.
قدم بعدی نصب یک loader میباشد که وظیفهی حذف مواردی را دارد که برای آن مشخص خواهیم کرد. این لودر strip-loader نام دارد و با دستور زیر آن را در پروژه اضافه میکنیم.
npm install -D strip-loader
// webpack.prod.config.js //تنظیمات قبلی را میخوانیم var devConfig = require("./webpack.config.js"); // لودری که وظیفهی حذف کردن دارد را وارد میکنیم var stripLoader = require("strip-loader"); // مانند قبل یک آبجکت با موارد مورد نظر برای لودر میسازیم var stripLoaderConfig = { test:[/\.js$/,/\.ts$/], exclude :/node_modules/ ,loader:stripLoader.loader("console.log") } // اضافه کردن به لیست لودرهای قبلی devConfig.module.loaders.push(stripLoaderConfig); // و در آخر اکسپورت کردن تنظیمات جدید وقبلی module.exports = devConfig;
خط بعدی نیز loader ی که وظیفهی حذف موارد مورد نظر ما را دارد وارد میکنیم و در ادامه یک آبجکت را با تعاریفی که قبلن از لودرها داشتیم میسازیم. تنها نکته در قسمت تعریف اسم لودر میباشد.
loader:stripLoader.loader("console.log")
// در حالتی که وبپک به صورت محلی نصب شده است npm run webpack -- --config webpack.prod.config.js -p // در حالتی که وبپک به صورت سراسری نصب شده باشد webpack --config webpack.prod.config.js -p
پس از اجرای این دستور و باز کردن صفحهی index.html خواهید دید که پیغامی در کنسول مرورگر ظاهر نخواهد شد و دستورات console.log همگی حذف شدهاند.
توجه داشته باشید که اگر برای ساخت باندل از وبپک استفاده کردهاید، برای میزبانی فایلهای پروژه از وب سرور وبپک استفاده نکنید و از وب سروری دیگر (مانند http-server یا IIS و ...) استفاده کنید؛ چرا که باندل توسط وب سرور وبپک دوباره ساخته میشود و تغییرات از بین میروند. در صورتی که میخواهید از وب سرور وبپک برای میزبانی بیلد نهایی پروژه نیز استفاده کنید، فایل پیکربندی بیلد نهایی را نیز به وب سرور وبپک با استفاده از دستور زیر معرفی کنید:
// زمانی که وبپک به صورت محلی در پروژه نصب شده است npm run webpackserver -- --config webpack.prod.config.js -p //در حالتی که وبپک به صورت گلوبال ( سراسری ) نصب میباشد webpack-dev-server --config webpack.prod.config.js -p
مدیریت فایل و فولدرها با استفاده وبپک
تا به اینجای کار اگر به ساختار چینش فایلها در پروژه دقت کنید خواهید دید که همگی فایلها در مسیر اصلی پروژه قرار دارند و این روش مناسبی برای مدیریت فایلها و فولدرها در پروژههای واقعی و بزرگ نیست. در ادامه قصد داریم این مسئله را حل کرده و ساختار مشخصی را برای محل قرارگیری فایلهای پروژه با کمک وبپک ایجاد کنیم.
در اولین قدم فولدری را برای اسکریپتها با نام js ایجاد کرده و اسکریپتها را به این فولدر انتقال میدهیم.
در قدم دوم برای فایلهای استاتیک پروژه مانند صفحات html و ... فولدر دیگری را با نام assets ایجاد میکنیم و این گونه فایلها را در آن قرار خواهیم داد.
تا اینجای کار در صورتی که وبپک را اجرا کنید، مسیرهای جدید را پیدا نخواهد کرد و دچار خطا خواهد شد. پس به فایل پیکربندی ( webpack.config.js ) مراجعه کرده و وبپک را از ساختار جدید پروژه خبردار میکنیم.
// new webpack.config.js file //ماژول توکار نود جی اس var path = require("path"); module.exports = { // مشخص کردن زمینه برای فایلهای ورودی context:path.resolve("js"), entry:['./shared.js','./main.ts'] ,output:{ // مشخص کردن محل قرارگیری باندل ساخته شده path:path.resolve("build/js"), // درخواست از سمت چه مسیری برای باندل خواهد آمد ؟ publicPath:"assets/js", filename:'bundle.js' } , devServer:{ //راهنمایی برای وب سرور جهت اینکه فایلها را از چه محلی سرو کند contentBase:"assets" } ,watch :true ,module:{ loaders:[ { test:/\.ts$/ ,exclude:/node_modules/ ,loader:'ts-loader' } ] } }
context:path.resolve("js")
// تغییرات در شی output // این کلید جدید مسیر قرار گیری جدید باندل را به وبپک اطلاع میدهد path:path.resolve("build/js"), // راهنما برای وب سرور وبپک جهت میزبانی مسیر زیر از کلید بالا publicPath:"assets/js",
در نهایت وارد فایل index.html میشویم و مسیر جدید باندل را به آن معرفی میکنیم.
//index.html <html> <head> first part of webpack tut! </head> <body> <h1>webpack is awesome !</h1> <script src="assets/js/bundle.js"></script> </body> </html>
حال با اجرا کردن وب سرور وبپک میتوان مشاهده کرد که مسیرهای جدید، بدون مشکل توسط وبپک پیدا شده و فایلها سرو میشوند. قابل توجه است که این نوع چینش پروژه قابل تغییر و شخصی سازی برای پروژههای گوناگون میباشد.
ساخت فایلهای سورس مپ (source map)
سادهترین راه جهت ساخت فایلهای سورس مپ با استفاده از یک پرچم در هنگام فراخوانی وبپک به صورت زیر میباشد.
// فعال کردن ساخت سورس مپها npm run webpack -- -d // یا در هنگام نصب گلوبال webpack -d // جهت استفاده به همراه وب سرور npm run webpackserver -- -d // یا به صورت نصب گلوبال webpack-dev-server -d
راه دوم با استفاده از انجام تغییرات در فایل پیکربندی وبپک میباشد؛ به این صورت که کلیدی را به این فایل اضافه میکنیم.
// webpack.config.js // کلید جدید اضافه شده در فایل پیکربندی devtool:"#source-map"
با اجرای وب سرور وبپک خواهید دید که سورس مپها در منوی توسعه دهندهی مرورگر قابل دستیابی میباشند.
ساخت چندین باندل گوناگون
قصد داریم به پروژه، دو صفحهی دیگر را نیز با نامهای aboutme و contact اضافه کنیم. هر یک از این صفحات اسکریپت مخصوص به خود را خواهد داشت و باندل نهایی نیز شامل تمامی آنها خواهد شد. در صورتی که این خروجی مطلوب ما نباشد و به طور مثال بخواهیم مکانیزمی شبیه به lazy loading اسکریپتها را داشته باشیم و فقط زمانی اسکریپتها بارگذاری شوند که به آنها احتیاج باشد، برای انجام این کار با وبپک به صورت زیر عمل خواهیم کرد.
دو صفحهی html جدید را با عناوین ذکر شدهی بالا به پوشهی assets اضافه میکنیم و برای هریک نیز اسکریپتی با همان نام خواهیم ساخت و در پوشهی js قرار میدهیم.
محتوای صفحات بدین شکل میباشد.
// index.html file <html> <head> <title> third part of webpack tut! </title> </head> <body> <nav> <a href="aboutme.html">about me</a> <a href="contact.html">contact</a> </nav> <h1>webpack is awesome !</h1> <script src="assets/js/shared.js"></script> <script src="assets/js/index.js"></script> </body> </html> // aboutme.html file <html> <head> <title> about me page ! </title> </head> <body> <nav> <a href="index.html">index</a> <a href="contact.html">contact</a> </nav> <h1>webpack is awesome !</h1> <script src="assets/js/shared.js"></script> <script src="assets/js/aboutme.js"></script> </body> </html> // contact.html file <html> <head> <title> contact me page ! </title> </head> <body> <nav> <a href="index.html">index</a> <a href="aboutme.html">about me</a> </nav> <h1>webpack is awesome !</h1> <script src="assets/js/shared.js"></script> <script src="assets/js/contact.js"></script> </body> </html>
var path = require("path"); var webpack = require("webpack"); // وارد کردن پلاگینی از وب پک برای ساخت تکههای مختلف اسکریپتها // معرفی اسکریپت shared.js var commonChunkPlugin = new webpack.optimize.CommonsChunkPlugin("shared.js"); module.exports = { context:path.resolve("js"), //entry:['./shared.js','./main.ts'] // معرفی اسکریپتهای جدید به وبپک entry:{ index:"./main.js", aboutme:"./aboutme.js", contact:"./contact.js" } ,output:{ path:path.resolve("build/js"), publicPath:"assets/js", // filename:'bundle.js' // به جای یک باندل کلی از وبپک میخاهیم برای هر ورودی باندلی جدید بسازد filename:"[name].js" } // رجیستر کردن پلاگین ,plugins:[commonChunkPlugin] , devServer:{ contentBase:"assets" } //,devtool:"#source-map" ,watch :true ,module:{... } }
حال با اجرای وبپک میتوان دید که سه باندل ساخته شده که همگی به اسکریپت shared.js وابستگی دارند و اگر این اسکریپت را از صفحات HTML حذف کنید، با خطا رو به رو خواهید شد. این پلاگین قدرت ساخت باندلهایی با خاصیت مشخص کردن وابستگیها و همچنین تو در توییها خاص را نیز دارد. برای مطالعهی بیشتر میتوانید به اینجا مراجعه کنید: پلاگین commonsChunk
در قسمت بعدی با استفاده از وبپک فایلهای css، فونتها و تصاویر را نیز باندل خواهیم کرد.
فایلهای مطلب:
سورس تا قبل از قسمت ایجاد تغییرات در ساختار فایلهای پروژه :dntwebpack-part3-beforeFileAndFolderManagment.zip
سورس برای بعد از ایجاد تغییرات در ساختار فایلهای پروژه : dntwebpack-part3AfterFileOrganization.zip
مطالب
MongoDB #13
توابع جمعی در MongoDB
عملگرهای جمعی، رکوردهای اطلاعات را پردازش میکنند و نتیجههای محاسبه شده را برمیگردانند. عملیات جمعی مقادیر چندین سند را باهم گروه بندی میکند و میتواند یک نوع از عملگرها را روی اطلاعات دسته بندی شده انجام دهد تا یک نتیجهی واحد را برگرداند. در sql، دستور (*)count همراه Group by معادل یک تابع جمعی در MongoDB است.
متد ()aggregate
برای توابع جمعی در MongoDB باید از متد ()aggregate استفاده کنید.
گرامر
گرامر پایه متد ()aggregate به صورت زیر است:
>db.COLLECTION_NAME.aggregate(AGGREGATE_OPERATION)
مثال
در این مجموعه، دادههای زیر را دارید:
{ _id: ObjectId(7df78ad8902c) title: 'MongoDB Overview', description: 'MongoDB is no sql database', by_user: 'user1', url: 'http://www.site.com', tags: ['mongodb', 'database', 'NoSQL'], likes: 100 }, { _id: ObjectId(7df78ad8902d) title: 'NoSQL Overview', description: 'No sql database is very fast', by_user: 'user1', url: 'http://www.site.com', tags: ['mongodb', 'database', 'NoSQL'], likes: 10 }, { _id: ObjectId(7df78ad8902e) title: 'Neo4j Overview', description: 'Neo4j is no sql database', by_user: 'Neo4j', url: 'http://www.neo4j.com', tags: ['neo4j', 'database', 'NoSQL'], likes: 750 },
حالا اگر بخواهید از مجموعهی بالا یک لیست را که تعداد دورههای نوشته شده توسط هر کاربر را نمایش میدهد، استخراج کنید، باید ار متد () aggregate به صورت زیر استفاده نمائید:
> db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : 1}}}]) { "result" : [ { "_id" : "user1", "num_tutorial" : 2 }, { "_id" : "Neo4j", "num_tutorial" : 1 } ], "ok" : 1 } >
معادل کوئری بالا در sql بصورت زیر خواهد بود:
select by_user, count(*) from mycol group by by_user
در مثال بالا، سندهای گروه بندی شدهی توسط فیلد by_user را داریم و در هر اجرای by_user مقدار قبلی جمع کلی افزایش مییابد. در اینجا لیست عبارتهای جمعی موجود، آمده است.
عبارت | توضیحات | مثال |
$sum | مقدار تعیین شده از همه سندهای مجموعه را جمع میکند. | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : "$likes"}}}]) |
$avg | میانگین همه مقادیر بدست آمده از سندهای مجموعه را محاسبه میکند. | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$avg : "$likes"}}}]) |
$min | کمترین مقادیر مشابه را از همه سندهای مجموعه، بر میگرداند. | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$min : "$likes"}}}]) |
$max | بیشترین مقادیر مشابه را از همه سندهای مجموعه، بر میگرداند. | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$max : "$likes"}}}]) |
$push | یک مقدار را در سند نتیجه، در یک آرایه درج میکند. | db.mycol.aggregate([{$group : {_id : "$by_user", url : {$push: "$url"}}}]) |
$addToSet | یک مقدار را در سند نتیجه در یک آرایه درج میکند، اما مقدار تکراری ایجاد نمیکند. | db.mycol.aggregate([{$group : {_id : "$by_user", url : {$addToSet : "$url"}}}]) |
$first | اولین سند از اسناد را برطبق گروه بندی بر میگرداند. معمولا این عبارت بعد از عبارتهای مرتب سازی مرحلهای استفاده میشود. | db.mycol.aggregate([{$group : {_id : "$by_user", first_url : {$first : "$url"}}}]) |
$last | آخرین سند از اسناد را برطبق گروه بندی بر میگرداند. معمولا این عبارت بعد از عبارتهای مرتب سازی مرحلهای استفاده میشود. | db.mycol.aggregate([{$group : {_id : "$by_user", last_url : {$last : "$url"}}}]) |
مفهوم Pipeline
در Command shell یونیکس، خط لوله (Pipeline) به معنی امکان اجرای یک عملیات روی چندین ورودی و استفاده از خروجی بعنوان ورودی برای دستور بعدی و ادامهی آن است. MongoDB نیز این مفهوم را در چارچوب توابع جمعی پشتیبانی میکند. یک مجموعه از مراحل وجود دارند که هرکدام از آنها یک مجموعه از اسناد را بعنوان ورودی میگیرند و یک مجموعه از سند را بعنوان نتیجه (یا نتیجه را بعنوان سند JSON در پایان خط لوله) ارائه میدهند. این عمل به نوبه خود میتواند برای مرحله بعد و یا مراحل بعدی، استفاده شود.
مراحل ممکن در چارچوب توابع جمعی در زیر آمده اند:
- $project : برای انتخاب چندین فیلد از یک مجموعه استفاده میشود.
- $match : این یک عملگر فیلترگذاری است که میتواند میزان اسنادی را که بعنوان ورودی در مرحله بعد گرفته میشوند، کاهش دهد.
- $group : این همان تابع جمعی است که در بالا توضیح داده شد.
- $skip : توسط این عبارت، در یک لیست بدست آمده (نتیجه)، میتوانید از لیست اسناد بصورت روبه جلو صرفنظر کنید.
- $limit : این عبارت تعداد اسناد را توسط عدد گرفته شده، از موقعیت فعلی برای نمایش محدود میکند.
- $unwind : این عبارت برای باز کردن (unwind) سندی که از آرایهها بهره گیری میکند استفاده میشود. وقتی از آرایه استفاده میکنید، داده از نوع پیش پیوست (Pre-joined) است و با این نوع داده، این عمل برای داشتن سندهای اختصاصی نا تمام خواهد ماند. بنابراین با این مرحله میتوانید میزان اسناد را برای مرحله بعد افزایش دهید.
استفاده از Redux درون پروژههای React، به روشهای مختلفی قابل انجام است؛ یعنی محدودیتی از لحاظ نحوه چیدمان فایلها، تغییر state و نحوهی dispatch کردن action وجود ندارد. به عبارتی این آزادی عمل را خواهیم داشت تا خودمان سیمکشی پروژه را انجام دهیم؛ ولی مشکل اصلی اینجاست که نمیتوانیم مطمئن شویم روشی که پروژه را با آن ستاپ کردهایم آیا به عنوان یک best-practice محسوب میشود یا خیر. در نهایت خروجی را که خواهیم داشت، حجم انبوهی از کدهای boilerplate و پکیجهای زیادی است که در حین توسعهی پروژه، به همراه Redux اضافه شدهاند. همچنین در نهایت یک store پیچیده را خواهیم داشت که مدیریت آن به مراتب سخت خواهد شد. یک مشکل دیگر این است که روال گفته شده را باید به ازای هر پروژهی جدید تکرار کنیم. برای حل این مشکل، یکی از maintainerهای اصلی تیم ریداکس، یک پروژه را تحت عنوان Redux Toolkit، مدتها قبل برای حل مشکلات عنوان شده شروع کرده است و این پکیج، جدیداً به قالب رسمی create-react-app اضافه شده است. که در واقع یک روش استاندارد و به اصطلاح opinionated برای ایجاد پروژههای ریداکسی میباشد و شامل تمامی وابستگیهای موردنیاز برای کار با Redux از قبیل redux-thunk و همچنین Redux DevTools است.
در کد فوق، نحوهی ایجاد store، نسبت به حالت معمول، خیلی تمیزتر است. نکتهی جالب این است که به همراه کد فوق، Redux DevTools و همچنین redux-thunk هم از قبل تنظیم شدهاند و در نتیجه، نیازی به تنظیم و نصب آنها نیست. تغییر مهم دیگر، پوشهی features میباشد که یک روش رایج برای گروهبندی کامپوننتها، همراه با فایلهای وابستهی آنها است. درون این پوشه، یک پوشه جدید را تحت عنوان counter داریم که حاوی فایلهای زیر میباشد:
Counter.js، کامپوننتی است که در نهایت درون صفحه رندر خواهد شد. درون این فایل با استفاده از Redux Hooks کار اتصال به store و همچنین dispatch کردن اکشنها انجام گرفته است:
کاری که immer انجام میدهد، تغییر یک شیء، به صورت مستقیم نیست؛ در واقع یک draftState را ایجاد خواهد کرد که در عمل یک proxy برای state فعلی میباشد. یعنی با mutate کردن state، یک شیء جدید را در نهایت clone خواهد کرد و به عنوان state نهایی برمیگرداند.
ایجاد یک برنامهی خالی React با قالب redux
در ادامه برای بررسی این قالب جدید، یک پروژهی جدید React را ایجاد خواهیم کرد:
> npx create-react-app redux-template --template redux > cd redux-template > yarn start
بررسی ساختار پروژهی ایجاد شده
ساختار پروژهی ایجاد شده به صورت زیر است:
این ساختار خیلی شبیه به قالب پیشفرض create-react-app میباشد. همانطور که در تصویر فوق نیز مشاهده میکنید، پروژهی ایجاد شدهی با قالب redux (تصویر سمت چپ)، یک فایل با نام store و همچنین یک دایرکتوری را به نام features دارد. اگر به فایل store.js مراجعه کنید، خواهید دید که تنظیمات اولیهی ایجاد store را در قالب یک مثال Counter ایجاد کردهاست:
import { configureStore } from '@reduxjs/toolkit'; import counterReducer from './features/counter/counterSlice'; export default configureStore({ reducer: { counter: counterReducer, }, });
Counter Counter.module.css counterSlice.js
import React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { decrement, increment, incrementByAmount, selectCount, } from './counterSlice'; import styles from './Counter.module.css'; export function Counter() { const count = useSelector(selectCount); const dispatch = useDispatch(); const [incrementAmount, setIncrementAmount] = useState(2); return ( <div> <div className={styles.row}> <button className={styles.button} aria-label="Increment value" onClick={() => dispatch(increment())} > + </button> <span className={styles.value}>{count}</span> <button className={styles.button} aria-label="Decrement value" onClick={() => dispatch(decrement())} > - </button> </div> <div className={styles.row}> <input className={styles.textbox} value={incrementAmount} onChange={e => setIncrementAmount(e.target.value)} /> <button className={styles.button} onClick={() => dispatch( incrementByAmount({ amount: Number(incrementAmount) || 0 }) ) } > Add Amount </button> </div> </div> ); }
فایل Counter.module.css نیز در واقع استایلهای مربوط به کامپوننت فوق میباشد که به صورت CSS module اضافه شدهاست. در نهایت فایل counterSlice.js را داریم که کار همان reducer سابق را برایمان انجام خواهد داد؛ اما اینبار با یک ساختار جدید و تحت عنوان slice. اگر به فایل عنوان شده مراجعه کنید، کدهای زیر را خواهید دید:
import { createSlice } from '@reduxjs/toolkit'; export const slice = createSlice({ name: 'counter', initialState: { value: 0, }, reducers: { increment: state => { // Redux Toolkit allows us to 'mutate' the state. It doesn't actually // mutate the state because it uses the immer library, which detects // changes to a "draft state" and produces a brand new immutable state // based off those changes state.value += 1; }, decrement: state => { state.value -= 1; }, incrementByAmount: (state, action) => { state.value += action.payload.amount; }, }, }); export const selectCount = state => state.counter.value; export const { increment, decrement, incrementByAmount } = slice.actions; export default slice.reducer;
در این قالب جدید، ترکیب این قطعات هستند که شیء اصلی یا در واقع همان state کلی پروژه را تشکیل خواهند داد. همانطور که مشاهده میکنید، برای ایجاد یک قطعه جدید، از تابع createSlice استفاده شده است. این تابع، تعدادی پارامتر را از ورودی دریافت میکند:
- name: برای هر بخش از state، میتوانیم یک نام را تعیین کنیم و این همان عنوانی خواهد بود که میتوانید توسط Redux DevTools مشاهده کنید.
- initialValue: در اینجا میتوانیم مقادیر اولیهای را برای این بخش از state، تعیین کنیم که در مثال فوق، value به مقدار صفر تنظیم شدهاست.
- reducers: این قسمت محل تعریف actionهایی هستند که قرار است state را تغییر دهند. نکته جالب توجه این است که state در هر کدام از متدهای فوق، به ظاهر mutate شده است؛ اما همانطور که به صورت کامنت نیز نوشتهاست، در پشت صحنه از کتابخانهای با عنوان immer استفاده میکند که در عمل بجای تغییر state اصلی، یک کپی از state جدید را جایگزین state قبلی خواهد کرد.
توسط selectCount نیز کار انتخاب مقدار موردنظر از state انجام شدهاست که معادل همان mapPropsToState است و در اینجا امکان دسترسی به state ذخیره شده در مخزن redux فراهم شده است. همچنین در خطوط پایانی فایل نیز اکشنها برای دسترسی سادهتر به درون کامپوننت، به صورت Object Destructuring به بیرون export شدهاند. در نهایت reducer نهایی را از slice ایجاد شده استخراج کردهایم. این پراپرتی برای ایجاد store مورداستفاده قرار میگیرد.
چرا قالب Redux Toolkit از immer برای تغییر state استفاده میکند؟
همانطور که در این قسمت از سری Redux توضیح داده شد، اعمال تغییرات بر روی آرایهها و اشیاء، باعث ایجاد ناخالصی خواهد شد؛ بنابراین به جای تغییر شیء اصلی، باید توسط یکی از روشهای Object.assign و یا spread operator، یک clone از state اصلی را ایجاد کرده و آن را به عنوان state نهایی لحاظ کنیم. اما در حین کار با اشیاء nested، انجام اینکار سخت خواهد شد و همچنین خوانایی کد را نیز کاهش میدهد:
return { ...state, models: state.models.map(c => c.model === action.payload.model ? { ...c, on: action.payload.toggle } : c ) };
اما با کمک immer میتوانیم به صورت مستقیم state را تغییر دهیم:
state.models.forEach(item => { if (item.model === action.payload.model) { item.on = action.payload.toggle; } });
نظرات اشتراکها
بررسی وضعیت فعلی پروژه Roslyn
کدهای IL درکی از پروسسور ندارند. به همین علت انتقال پذیر هستند. کار JIT هست که بسته به معماری سیستم، native machine instructions را تولید کند. به همین جهت است که کدهای IL شما (بسته بندی شده در فایلهای DLL یا EXE دات نت) به راحتی بر روی سیستمهای 64 و 32 بیتی اجرا میشوند و روی هر کدام از این سیستمها نیز رفتاری 64 یا 32 بیتی خواهند داشت. JIT هست که کدهای ماشین مخصوص پروسسور جاری را تولید میکند. JIT کامپایلر فعلی دات نت برای تولید سریع کد و نمایش سریع برنامههای دات نت طراحی شدهاست. اما همین سرعت بالای تولید کدها، الزاما به تولید کدهای بهینهای ختم نشدهاست. خصوصا در برنامههای سمت سرور اصلا اهمیتی ندارد که زمان start up یک برنامه وب چقدر است. کسی متوجه آن نخواهد شد چون در طول عمر برنامه وب تا ریاستارت بعدی آن، فقط یکبار رخ میدهد. ضمنا اکثر سرورهای امروز X64 هستند (در ویندوزهای سرور جدید، تولید نسخه 32 بیتی کلا کنار گذاشته شدهاست) و به این ترتیب میشود JIT کامپایلری بهینه سازی شده برای صرفا پروسسورهای 64 بیتی تولید کرد و ... اینکار در حال انجام است. نتیجه نمونه آزمایشی آن تا الان حداقل 30 درصد بهبود سرعت برنامهها بودهاست.
روشهای مختلف دریافت و نصب بوت استرپ 4
اولین کاری را که باید جهت شروع به کار با بوت استرپ 4 انجام داد، نصب و افزودن آن به صفحهی HTML جاری است. روشهای زیادی برای انجام اینکار وجود دارند:
الف) استفاده از نگارش SASS آن
بوت استرپ 4 در اصل مبتنی بر SASS توسعه یافتهاست و فایلهای آن با فرمت scss. ارائه میشوند. مزیت کار با این روش، امکان سفارشی سازی بوت استرپ 4 و یا مشارکت در پروژهی آن است و بدیهی است پس از آن باید SASS را به CSS کامپایل و مورد استفاده قرار داد.
ب) استفاده از CDN و یا Content delivery network
- مزیت آن بالا رفتن سرعت سایت با کش شدن آن در شبکه و یا شبکههای توزیع محتوا است.
- اما این روش محدودیت و الزام کار آنلاین با فایلهای بوت استرپ را نیز به همراه دارد.
برای کار با CDNهای بوت استرپ، مطابق راهنمای آن، تنها کافی است مدخل فایل css آنرا به head صفحه و مداخل فایلهای js ذیل را پیش از بسته شدن تگ body قرار دهید:
در اینجا شاید نام دو فایل، برای شما تازگی داشته باشند:
- jquery-3.3.1.slim : در اینجا slim یک نگارش بسیار کوچک از jQuery میباشد که بوت استرپ 4 بر مبنای آن کار میکند. البته در یک پروژهی واقعی احتمالا نیاز به نگارش کامل آنرا خواهید داشت و یا اگر قصد حذف کردن جیکوئری را دارید، این نگارش، کمحجمترین آن است.
- popper.min.js : برای نمونه Bootstrap dropdown برای کارکرد صحیح آن در نگارش 4، نیاز به این وابستگی جدید را دارد.
ج) استفاده از فایلهای از پیش پردازش شده
فایلهای از پیش آماده شدهی آنرا میتوان مستقیما از سایت بوت استرپ، با کلیک بر روی دکمهی download واقع در منوی راهبری سایت آن، دریافت کرد. مزیت این روش، امکان کار و توسعهی آفلاین صفحات مبتنی بر بوت استرپ است.
مشکل این روش عدم اطلاع رسانی خودکار از ارائهی نگارشهای جدید و نیاز به دریافت دستی مجدد این بسته، به ازای هر نگارش جدید آن میباشد.
د) استفاده از ابزارهای مدیریت بستهها
روشی را که ما در اینجا از آن استفاده خواهیم کرد، دریافت و نصب وابستگیهای مورد نیاز جهت کار با بوت استرپ 4، توسط npm است. به همین جهت یک فایل جدید package.json را با محتوای ذیل ایجاد کنید:
سپس از طریق خط فرمان به این پوشه وارد شده و دستور npm install را جهت دریافت این وابستگیها صادر کنید. یکی از مزیتهای مهم این روش، آگاه شدن خودکار از به روز رسانیهای وابستگیهای فوق، توسط افزونههایی مانند Version Lens است.
در اینجا font-awesome را نیز مشاهده میکنید؛ از این جهت که بوت استرپ 4 برخلاف نگارش 3 آن، به همراه گلیف آیکنهای پیشفرض آن نیست.
ایجاد قالب ابتدایی شروع به کار با بوت استرپ 4
پس از دریافت وابستگیهای مورد نیاز جهت شروع به کار با بوت استرپ 4 که هم اکنون باید در پوشهی node_modules واقع در ریشهی پوشهی جاری موجود باشند، در ادامه حداقل قالبی را که برای کار با آن نیاز است، مرور میکنیم:
- در ابتدا سازگاری با edge و الزام به استفادهی از آخرین نگارش IE نصب شده مشخص شدهاست.
- سپس viewport استاندارد، جهت تعیین اینکه این صفحه با ابزارهای موبایل نیز سازگار است، تعریف شدهاست.
- در قسمت head، مدخل فایل bootstrap.min.css تعریف شدهاست. همچنین مداخل مورد نیاز جهت کار با font-awesome را نیز مشاهده میکنید.
- پیش از بسته شدن تگ body، تعاریف jQuery، کتابخانهی popper و سپس bootstrap.min.js قید شدهاند. کتابخانهی popper از مسیر umd آن دریافت شدهاست تا همه جا کار کند.
نکتهی مهم!
در نگارش نهایی برنامهی شما، مسیرهای فایلهای شروع شدهی با /node_modules/ نباید وجود داشته باشند. این فایلها را بهتر است توسط ابزارهای bundling & minification یکی و سپس به صفحه اضافه کنید.
غنی سازی ویرایشگر VSCode برای کار سادهتر با بوت استرپ
VSCode یک ویرایشگر حرفهای چندسکویی است که برای ویندوز، مک و لینوکس تهیه شدهاست. این ویرایشگر را میتوان توسط افزونههای زیر برای کار سادهتر با بوت استرپ غنی کرد:
Bootstrap 4, Font awesome 4, Font Awesome 5 Free & Pro snippets: ساده سازی تشکیل تگهای بوت استرپ
Path Autocomplete: کار وارد کردن مسیر فایلها و تصاویر را ساده میکند.
HTML CSS Support: کار آن غنی سازی intellisense این ویرایشگر جهت کار با ویژگیها و همچنین کلاسهای CSS است.
IntelliSense for CSS class names in HTML: انتخاب کلاسهای CSS بوت استرپ را سادهتر میکند.
Live Server: کار آن راه اندازی یک وب سرور آزمایشی و سپس امکان مشاهدهی آنی تغییرات در برنامه و فایل HTML جاری، در مرورگر میباشد.
برای کار با آن، در حالیکه صفحهی HTML جاری در VSCode باز است، بر روی دکمهی Go Live اضافه شدهی در status bar آن کلیک کنید. پس از آن، یک وب سرور آزمایشی را بر روی پورت 5500 آغاز کرده و صفحهی جاری را در آدرس http://127.0.0.1:5500/index.html در مرورگر پیشفرض سیستم نمایش میدهد. اکنون فایل HTML خود را در VSCode ویرایش کنید. ملاحظه خواهید کرد که بلافاصله این تغییرات در مرورگر قابل مشاهده هستند.
نگارشهای راست به چپ بوت استرپ 4
قرار است بوت استرپ 4 نگارش رسمی راست به چپ نیز داشته باشد. به همین منظور میتوانید در اینجا رای خود را اظهار کنید.
همچنین پروژههای زیر نیز چنین قابلیتی را ارائه میدهند:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: Bootstrap4_01.zip
اولین کاری را که باید جهت شروع به کار با بوت استرپ 4 انجام داد، نصب و افزودن آن به صفحهی HTML جاری است. روشهای زیادی برای انجام اینکار وجود دارند:
الف) استفاده از نگارش SASS آن
بوت استرپ 4 در اصل مبتنی بر SASS توسعه یافتهاست و فایلهای آن با فرمت scss. ارائه میشوند. مزیت کار با این روش، امکان سفارشی سازی بوت استرپ 4 و یا مشارکت در پروژهی آن است و بدیهی است پس از آن باید SASS را به CSS کامپایل و مورد استفاده قرار داد.
ب) استفاده از CDN و یا Content delivery network
- مزیت آن بالا رفتن سرعت سایت با کش شدن آن در شبکه و یا شبکههای توزیع محتوا است.
- اما این روش محدودیت و الزام کار آنلاین با فایلهای بوت استرپ را نیز به همراه دارد.
برای کار با CDNهای بوت استرپ، مطابق راهنمای آن، تنها کافی است مدخل فایل css آنرا به head صفحه و مداخل فایلهای js ذیل را پیش از بسته شدن تگ body قرار دهید:
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
- jquery-3.3.1.slim : در اینجا slim یک نگارش بسیار کوچک از jQuery میباشد که بوت استرپ 4 بر مبنای آن کار میکند. البته در یک پروژهی واقعی احتمالا نیاز به نگارش کامل آنرا خواهید داشت و یا اگر قصد حذف کردن جیکوئری را دارید، این نگارش، کمحجمترین آن است.
- popper.min.js : برای نمونه Bootstrap dropdown برای کارکرد صحیح آن در نگارش 4، نیاز به این وابستگی جدید را دارد.
ج) استفاده از فایلهای از پیش پردازش شده
فایلهای از پیش آماده شدهی آنرا میتوان مستقیما از سایت بوت استرپ، با کلیک بر روی دکمهی download واقع در منوی راهبری سایت آن، دریافت کرد. مزیت این روش، امکان کار و توسعهی آفلاین صفحات مبتنی بر بوت استرپ است.
مشکل این روش عدم اطلاع رسانی خودکار از ارائهی نگارشهای جدید و نیاز به دریافت دستی مجدد این بسته، به ازای هر نگارش جدید آن میباشد.
د) استفاده از ابزارهای مدیریت بستهها
روشی را که ما در اینجا از آن استفاده خواهیم کرد، دریافت و نصب وابستگیهای مورد نیاز جهت کار با بوت استرپ 4، توسط npm است. به همین جهت یک فایل جدید package.json را با محتوای ذیل ایجاد کنید:
{ "name": "bootstrap.4", "version": "1.0.0", "description": "client side resources of the project", "scripts": {}, "author": "VahidN", "license": "ISC", "dependencies": { "bootstrap": "^4.1.3", "components-font-awesome": "5.0.6", "jquery": "^3.3.1", "popper.js": "^1.14.4" } }
در اینجا font-awesome را نیز مشاهده میکنید؛ از این جهت که بوت استرپ 4 برخلاف نگارش 3 آن، به همراه گلیف آیکنهای پیشفرض آن نیست.
ایجاد قالب ابتدایی شروع به کار با بوت استرپ 4
پس از دریافت وابستگیهای مورد نیاز جهت شروع به کار با بوت استرپ 4 که هم اکنون باید در پوشهی node_modules واقع در ریشهی پوشهی جاری موجود باشند، در ادامه حداقل قالبی را که برای کار با آن نیاز است، مرور میکنیم:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta http-equiv="x-ua-compatible" content="ie=edge"> <link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="/node_modules/components-font-awesome/css/fa-solid.min.css"> <link rel="stylesheet" href="/node_modules/components-font-awesome/css/fontawesome.min.css"> <title>Bootstrap</title> </head> <body> <div class="container"> </div> <script src="/node_modules/jquery/dist/jquery.min.js"></script> <script src="/node_modules/popper.js/dist/umd/popper.min.js"></script> <script src="/node_modules/bootstrap/dist/js/bootstrap.min.js"></script> </body> </html>
- سپس viewport استاندارد، جهت تعیین اینکه این صفحه با ابزارهای موبایل نیز سازگار است، تعریف شدهاست.
- در قسمت head، مدخل فایل bootstrap.min.css تعریف شدهاست. همچنین مداخل مورد نیاز جهت کار با font-awesome را نیز مشاهده میکنید.
- پیش از بسته شدن تگ body، تعاریف jQuery، کتابخانهی popper و سپس bootstrap.min.js قید شدهاند. کتابخانهی popper از مسیر umd آن دریافت شدهاست تا همه جا کار کند.
نکتهی مهم!
در نگارش نهایی برنامهی شما، مسیرهای فایلهای شروع شدهی با /node_modules/ نباید وجود داشته باشند. این فایلها را بهتر است توسط ابزارهای bundling & minification یکی و سپس به صفحه اضافه کنید.
غنی سازی ویرایشگر VSCode برای کار سادهتر با بوت استرپ
VSCode یک ویرایشگر حرفهای چندسکویی است که برای ویندوز، مک و لینوکس تهیه شدهاست. این ویرایشگر را میتوان توسط افزونههای زیر برای کار سادهتر با بوت استرپ غنی کرد:
Bootstrap 4, Font awesome 4, Font Awesome 5 Free & Pro snippets: ساده سازی تشکیل تگهای بوت استرپ
Path Autocomplete: کار وارد کردن مسیر فایلها و تصاویر را ساده میکند.
HTML CSS Support: کار آن غنی سازی intellisense این ویرایشگر جهت کار با ویژگیها و همچنین کلاسهای CSS است.
IntelliSense for CSS class names in HTML: انتخاب کلاسهای CSS بوت استرپ را سادهتر میکند.
Live Server: کار آن راه اندازی یک وب سرور آزمایشی و سپس امکان مشاهدهی آنی تغییرات در برنامه و فایل HTML جاری، در مرورگر میباشد.
برای کار با آن، در حالیکه صفحهی HTML جاری در VSCode باز است، بر روی دکمهی Go Live اضافه شدهی در status bar آن کلیک کنید. پس از آن، یک وب سرور آزمایشی را بر روی پورت 5500 آغاز کرده و صفحهی جاری را در آدرس http://127.0.0.1:5500/index.html در مرورگر پیشفرض سیستم نمایش میدهد. اکنون فایل HTML خود را در VSCode ویرایش کنید. ملاحظه خواهید کرد که بلافاصله این تغییرات در مرورگر قابل مشاهده هستند.
نگارشهای راست به چپ بوت استرپ 4
قرار است بوت استرپ 4 نگارش رسمی راست به چپ نیز داشته باشد. به همین منظور میتوانید در اینجا رای خود را اظهار کنید.
همچنین پروژههای زیر نیز چنین قابلیتی را ارائه میدهند:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: Bootstrap4_01.zip
میخواهیم زمان نمایش اولین قالب برنامه را به حداقل برسانیم تا تاثیر روانی بهتری را بر روی کاربرانی که برنامه را اجرا میکنند، بگذاریم. برای این منظور در Angular، از Lazy loading استفاده میشود. همچنین این فریم ورک به همراه قابلیت پیش بارگذاری ماژولها نیز هست تا سایر مسیرهای درخواستی را نیز با سرعت هرچه تمامتر نمایش دهد.
زمانیکه کاربری برنامهی تک صفحهای وب را در مرورگر باز میکند، ابتدا فایل index.html را در پاسخ دریافت خواهد کرد. این فایل تعاریف مداخل مورد نیاز برای رندر آنرا مانند فایلهای جاوا اسکریپت و CSS، به همراه دارد. سپس این فایلها توسط مرورگر از سرور دریافت میشوند. در این حالت با پردازش این فایلها، کامپوننت ریشهی سایت بارگذاری میشود. پس از پایان آن، قالب این کامپوننت به کاربر نمایش داده خواهد شد. بنابر سرعت دریافت فایلها توسط کاربر، این آغاز میتواند اندکی کند باشد. البته با رعایت نکات گفتهی شدهی در مطلب «Angular CLI - قسمت پنجم - ساخت و توزیع برنامه» میتوان این حجم را توسط AoT و Tree-Shaking به میزان قابل ملاحظهای کاهش داد. به علاوه با فعالسازی Lazy loading میتوان قسمتهای مختلف برنامه را تبدیل به یک سری Bundle کرد که در زمان درخواست، بارگذاری میشوند. به این ترتیب حجم فایلهای ابتدایی که باید از سرور دریافت شوند بسیار کمتر شده و به علاوه با کاهش این حجم، مرورگر نیز باید میزان کمتری از کدها را در جهت نمایش اولین کامپوننت، پردازش و اجرا کند. در این حالت زمانیکه کاربری شروع به پیمایش مسیر یک ماژول خاص را میکند، آنگاه فایلهای مرتبط با آن از سرور دریافت و در مرورگر پردازش میشوند. بنابراین اگر کاربری به قسمتی دسترسی ندارد، نیازی هم به دریافت فایلهای آن نخواهد داشت؛ چون کار به فعالسازی مسیریابی آن ماژول نمیرسد.
آماده شدن جهت Lazy loading
پیش از Lazy loading یک قسمت از برنامه (که به آن async routing هم میگویند)، این قسمت باید دارای شرایطی باشد:
- این قسمت از برنامه حتما باید در یک ماژول تعریف شده باشد. از این جهت که Lazy loading، لیست کامپوننتهای قید شدهی در تعریف یک ماژول را بارگذاری میکند.
- تمام مسیرهای این ماژول باید در ذیل یک مسیر والد، گروه بندی شده باشند. از این جهت که Lazy loading فقط بر روی مسیر ریشهی والد تنظیم و بارگذاری میشود.
- این ماژول نباید در هیچ ماژول دیگری import شده باشد. اگر این ماژول ارجاعی را در سایر ماژولها داشته باشد، هیچ راهی بجز دریافت و کامپایل کامل آن توسط Angular وجود نخواهد داشت.
در مثال جاری این سری:
- تمام ویژگیهای قسمت مدیریت محصولات، داخل ماژول product.module.ts تعریف شدهاند. بنابراین اولین شرط Lazy loading آن برقرار است.
- در فایل product-routing.module.ts، کار گروه بندی مسیریابیها ذیل یک والد مشخص انجام شدهاست (همان قسمت ششم این سری). بنابراین شرط دوم lazy loading این ماژول نیز پیشتر پیاده سازی شدهاست.
- اما اگر به فایل src\app\app.module.ts مراجعه کنیم، ارجاعی به این ماژول در قسمت imports آن وجود دارد. بنابراین باید این ارجاع را حذف کنیم. در غیراینصورت کار دریافت کامل آن به همراه سایر ماژولهای برنامه، در همان ابتدای کار صورت خواهد گرفت.
بنابراین در فایل src\app\app.module.ts، ابتدا import فایل آنرا از ابتدای ماژول حذف و سپس ارجاع به نام کلاس کامپوننت ProductModule را نیز حذف میکنیم. در این حالت اگر از طریق منوی سایت سعی در دسترسی به این مسیرها کنیم، خطای 404 را دریافت خواهیم کرد؛ چون اکنون برنامه اطلاعاتی را در مورد نحوهی مسیریابی قسمت محصولات برنامه، ندارد.
Lazy loading یک ماژول
برای بارگذاری غیرهمزمان یک ماژول و یا همان Lazy loading، میتوان از خاصیت loadChildren تنظیمات مسیریابی، استفاده کرد:
مقدار خاصیت loadChildren به صورت ذکر مسیر ماژول مرتبط به همراه یک # و سپس ذکر نام کلاس ماژول آن انجام میشود. مسیری هم که در اینجا ذکر میشود بر اساس محل قرارگیری فایل index.html، مقدار دهی شود.
با این تنظیم، زمانیکه مسیر ریشهی produtcs درخواست شد، کار بارگذاری ماژول آن صورت گرفته و تنظیمات مسیریابی آن به سیستم اضافه میشود. به علاوه کار فعالسازی و نمایش کامپوننت آن را نیز انجام خواهد داد.
به همین منظور فایل src\app\app-routing.module.ts را گشوده و تنظیم فوق را به آن اضافه میکنیم:
در این حالت اگر دستور ng serve -o را صادر کنید، خروجی آن اندکی متفاوت خواهد بود:
مورد {5} با فعالسازی lazy loading به لیست فایلهای موجود اضافه شدهاست. این فایلی است که تنها درصورت درخواست مسیر نمایش لیست محصولات، توسط مرورگر دریافت خواهد شد و هیچ ارجاع مستقیمی به آن در فایل index.html تولیدی نهایی وجود ندارد.
به علاوه اگر در منوی سایت بر روی لینک نمایش لیست محصولات کلیک کنیم، هنوز خروجی نمایش داده نمیشود (هرچند خطای 404 را هم دریافت نمیکنیم). علت اینجا است که اگر به فایل src\app\product\product-routing.module.ts مراجعه کنیم، تعریف این مسیر ریشه، در این فایل نیز وجود دارد:
بنابراین اکنون برای دسترسی به آن باید مسیر products/products را درخواست داد. به همین جهت، path و canActivate آنرا حذف کرده و هر دو را به فایل src\app\app-routing.module.ts منتقل میکنیم:
یک نکته: اکنون تنظیمات مسیریابی فایل src\app\product\product-routing.module.ts چنین شکلی را پیدا کردهاست:
تنظیمات مسیر والد به طور کامل حذف شدهاند. به علاوه دیگر نیازی به ذکر خاصیت children آن نیست و تمام تنظیمات مسیریابی فرزندان، داخل [] اصلی قرار گرفتهاند. همچنین دیگر نیازی به الحاق AuthGuard در ابتدای importهای این ماژول نیست؛ چون به فایل src\app\app-routing.module.ts منتقل شدهاست.
در این حالت اگر مسیر نمایش لیست محصولات را درخواست دهیم، مشاهده خواهیم کرد فایل 5.chunk.js که حاوی اطلاعات این ماژول است، به صورت مجزایی بارگذاری شده (lazy loading) و سپس با فعال شدن محافظ مسیر آن، صفحهی لاگین نمایش داده میشود:
این بارگذاری با تاخیر و در صورت نیاز، به دو علت آغاز برنامه را سریعتر میکند:
الف) مرورگر اطلاعی از وجود فایل 5.chunk.js در ابتدای کار نداشته و آنرا بارگذاری نمیکند (دریافت حجم کمتر، در آغاز نمایش برنامه).
ب) چون حجم کمتری از کدهای جاوا اسکریپت توسط مرورگر در آغاز کار دریافت میشود، کار پردازش و اجرای آنها نیز بسیار سریعتر خواهد شد.
بررسی محافظ canLoad
تعدادی از محافظهای مسیرها را در قسمت قبل بررسی کردیم. هنگامیکه کامپوننتها به صورت lazy loading فعالسازی شده و قالب آنها نمایش داده میشوند، میتوان از محافظ مسیر دیگری به نام canLoad نیز استفاده کرد و هدف از آن، بررسی منطقی، پیش از فعالسازی یک مسیر غیرهمزمان است. بنابراین اگر این محافظ false را برگرداند، حتی فایلهای اسکریپت این ماژول، بارگذاری اولیه نیز نخواهد شد. به این ترتیب کسانیکه دسترسی به یک مسیر را نداشته باشند، فایلهای اسکریپت متناظر با آنرا نیز دریافت نخواهند کرد.
در مثال جاری، اگر به برگهی network ابزار developer مرورگر دقت کنید، با درخواست نمایش مسیر لیست محصولات، ابتدا فایل js آن دریافت میشود که حاوی اطلاعات تمام کامپوننتها و قالبهای مرتبط با این مسیر است و سپس صفحهی login نمایش داده خواهد شد. بنابراین اگر کاربر به این قسمت دسترسی نداشته باشد، فایل js آن بیجهت دریافت و بارگذاری شدهاست. برای بهبود این وضعیت میتوان نمایش لاگین را پیش از بارگذاری فایل js این ماژول فعالسازی کرد و این مورد هدف اصلی محافظ canLoad است.
در ادامه برای تکمیل مثال جاری، میتوان AuthGuard را طوری تنظیم کرد که علاوه بر پیاده سازی CanActivate، اینترفیس CanLoad را نیز پیاده سازی کند:
همانطور که ملاحظه میکنید، متد canLoad بر خلاف متد canActivate دسترسی به سرویسهای اطلاعات مسیریابی و وضعیت مسیریابی را ندارد؛ از این جهت که هنوز در این مرحله، ماژول درخواستی حاوی تنظیمات مسیریابی، بارگذاری و فعالسازی نشدهاست.
مرحلهی بعد، تغییر فایل src\app\app-routing.module.ts و جایگزین کردن تعریف فعلی canActivate با canLoad است:
پس از این تغییر، برنامه را مجدا اجرا کرده و صفحه را refresh کنید. سپس برگهی network ابزار developers را نیز باز نگه دارید. اکنون بر روی لینک نمایش لیست محصولات کلیک کنید. مشاهده خواهید کرد که در این حالت صفحهی لاگین، بدون بارگذاری ماژول Js ایی نمایش داده میشود. در ادامه اگر لاگین کنیم، آنگاه فایل js این ماژول توسط مرورگر دریافت شده و بارگذاری میشود.
پیش بارگذاری ماژولها
با فعالسازی lazy loading، ماژولهای مورد نیاز کاربر دیگر به همراه فایلهای js ابتدایی برنامه که در فایل index.html ارجاع مستقیمی به آنها دارند، ارائه نمیشوند و تنها در صورت درخواست مشاهدهی مسیری، کار بارگذاری آنها توسط برنامه صورت خواهد گرفت. همین مساله میتواند در بار اول نمایش این ماژولها تاخیر کوتاهی را سبب شود. به همین جهت قابلیت پیش بارگذاری ماژولها نیز در سیستم مسیریاب Angular پیش بینی شدهاست. به این قابلیت preloading و یا eager lazy loading نیز میگویند. در این حالت برنامه در پشت صحنه، کار پیش واکشی ماژولها را انجام میدهد و زمانیکه کاربری مسیری را درخواست میدهد، آْن مسیر را بدون درنگ مشاهده خواهد کرد.
بدیهی است این قابلیت نباید برای ماژولهایی که قرار است توسط کاربرانی خاص مشاهده شوند فعال شود و هدف آن دسترسی سریع به ماژولهای پرکاربرد برنامهاست.
در اینجا سه استراتژی پیش بارگذاری ماژولها میسر است:
- No preloading که حالت پیش فرض است.
- Preload all سبب پیش بارگذاری تمام قسمتهای lazy load برنامه میشود.
- Custom که اجازهی تعریف یک استراتژی سفارشی را میدهد.
برای مثال برای فعالسازی حالت Preload all، باید به فایل src\app\app-routing.module.ts مراجعه کرده و تغییرات ذیل را اعمال کنیم:
در اینجا نحوهی تنظیم preloadingStrategy را به PreloadAllModules مشاهده میکنید. در این حالت پس از آغاز ابتدایی برنامه، مسیریاب بلافاصله تمام مسیرهای lazy load را در پشت صحنه بارگذاری میکند.
یک نکته: وجود محافظ canLoad، هر نوع استراتژی prealoading را غیرفعال میکند. اما prealoading با سایر انواع محافظها کار میکند.
بنابراین برای آزمایش تنظیم preloadingStrategy: PreloadAllModules، تعریف canLoad را به canActivate تغییر دهید.
تعریف استراتژیهای سفارشی پیش بارگذاری ماژولها
اگر نیاز به یک استراتژی پیش بارگذاری بهتر از هیچ یا همه باشد، میتوان یک استراتژی سفارشی را نیز تدارک دید و ایجاد آن سه مرحلهی ایجاد سرویس مرتبط، ثبت آن سرویس در ماژول و در آخر تنظیم مسیریابی را به همراه دارد.
برای این منظور ابتدا دستور ذیل را صادر کنید تا قالب ابتدایی سرویس SelectiveStrategy ایجاد شود:
که سبب تولید و به روز رسانی فایلهای ذیل در پوشهی src\app خواهد شد (چون مرتبط است به کل برنامه):
در این حالت لیست providers فایل app.module.ts نیز به صورت خودکار تکمیل میگردد.
سپس کدهای SelectiveStrategyService را به نحو ذیل تغییر دهید:
- این سرویس ویژه باید اینترفیس PreloadingStrategy را پیاده سازی کند. سپس باید متد اجباری preload آنرا افزود و تکمیل نمود.
- پارامتر اول این متد، اطلاعاتی را در مورد مسیر جاری در اختیار ما قرار میدهد و دومین پارامتر آن متدی است که کار preloading را انجام میدهد.
- در اینجا است که تصمیم میگیریم ماژولی را preload کنیم یا خیر. برای نمونه در اینجا از خاصیت data مسیریابی استفاده شدهاست. این خاصیت نیز به یک مقدار ثابت اشاره میکند (قسمت «ارسال اطلاعات ثابت به مسیرهای مختلف برنامه» قسمت چهارم). برای مثال نام دلخواه آنرا preload گذاشتهایم و اگر مقدار آن به true تنظیم شده بود، آنگاه این مسیر preload خواهد شد. فراخوانی متد load در اینجا به معنای preloading این مسیر است. در غیراینصورت null را بازگشت میدهیم.
در ادامه نیاز است در فایل src\app\app-routing.module.ts، بجای معرفی PreloadAllModules، این استراتژی سفارشی خود را معرفی کرد:
و همچنین تعریف مسیریابی برنامه به این صورت تغییر میکند:
در اینجا نحوهی مقدار دهی خاصیت data را به اطلاعات ثابت preload: true مشاهده میکنید. این اطلاعاتی است که در سرویس SelectiveStrategy سفارشی ما بررسی شده و بر اساس آن در مورد پیش بارگذاری این مسیر تصمیمگیری میشود.
برای آزمایش آن، برنامه را مجدا اجرا کرده و صفحه را refresh کنید. سپس برگهی network ابزار developers را نیز باز نگه دارید. مشاهده خواهید کرد که علاوه بر فایلهای js اصلی برنامه که در فایل index.html ارجاعی را دارند، فایل 5.chunk.js نیز پیش بارگذاری شدهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-09.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
زمانیکه کاربری برنامهی تک صفحهای وب را در مرورگر باز میکند، ابتدا فایل index.html را در پاسخ دریافت خواهد کرد. این فایل تعاریف مداخل مورد نیاز برای رندر آنرا مانند فایلهای جاوا اسکریپت و CSS، به همراه دارد. سپس این فایلها توسط مرورگر از سرور دریافت میشوند. در این حالت با پردازش این فایلها، کامپوننت ریشهی سایت بارگذاری میشود. پس از پایان آن، قالب این کامپوننت به کاربر نمایش داده خواهد شد. بنابر سرعت دریافت فایلها توسط کاربر، این آغاز میتواند اندکی کند باشد. البته با رعایت نکات گفتهی شدهی در مطلب «Angular CLI - قسمت پنجم - ساخت و توزیع برنامه» میتوان این حجم را توسط AoT و Tree-Shaking به میزان قابل ملاحظهای کاهش داد. به علاوه با فعالسازی Lazy loading میتوان قسمتهای مختلف برنامه را تبدیل به یک سری Bundle کرد که در زمان درخواست، بارگذاری میشوند. به این ترتیب حجم فایلهای ابتدایی که باید از سرور دریافت شوند بسیار کمتر شده و به علاوه با کاهش این حجم، مرورگر نیز باید میزان کمتری از کدها را در جهت نمایش اولین کامپوننت، پردازش و اجرا کند. در این حالت زمانیکه کاربری شروع به پیمایش مسیر یک ماژول خاص را میکند، آنگاه فایلهای مرتبط با آن از سرور دریافت و در مرورگر پردازش میشوند. بنابراین اگر کاربری به قسمتی دسترسی ندارد، نیازی هم به دریافت فایلهای آن نخواهد داشت؛ چون کار به فعالسازی مسیریابی آن ماژول نمیرسد.
آماده شدن جهت Lazy loading
پیش از Lazy loading یک قسمت از برنامه (که به آن async routing هم میگویند)، این قسمت باید دارای شرایطی باشد:
- این قسمت از برنامه حتما باید در یک ماژول تعریف شده باشد. از این جهت که Lazy loading، لیست کامپوننتهای قید شدهی در تعریف یک ماژول را بارگذاری میکند.
- تمام مسیرهای این ماژول باید در ذیل یک مسیر والد، گروه بندی شده باشند. از این جهت که Lazy loading فقط بر روی مسیر ریشهی والد تنظیم و بارگذاری میشود.
- این ماژول نباید در هیچ ماژول دیگری import شده باشد. اگر این ماژول ارجاعی را در سایر ماژولها داشته باشد، هیچ راهی بجز دریافت و کامپایل کامل آن توسط Angular وجود نخواهد داشت.
در مثال جاری این سری:
- تمام ویژگیهای قسمت مدیریت محصولات، داخل ماژول product.module.ts تعریف شدهاند. بنابراین اولین شرط Lazy loading آن برقرار است.
- در فایل product-routing.module.ts، کار گروه بندی مسیریابیها ذیل یک والد مشخص انجام شدهاست (همان قسمت ششم این سری). بنابراین شرط دوم lazy loading این ماژول نیز پیشتر پیاده سازی شدهاست.
- اما اگر به فایل src\app\app.module.ts مراجعه کنیم، ارجاعی به این ماژول در قسمت imports آن وجود دارد. بنابراین باید این ارجاع را حذف کنیم. در غیراینصورت کار دریافت کامل آن به همراه سایر ماژولهای برنامه، در همان ابتدای کار صورت خواهد گرفت.
بنابراین در فایل src\app\app.module.ts، ابتدا import فایل آنرا از ابتدای ماژول حذف و سپس ارجاع به نام کلاس کامپوننت ProductModule را نیز حذف میکنیم. در این حالت اگر از طریق منوی سایت سعی در دسترسی به این مسیرها کنیم، خطای 404 را دریافت خواهیم کرد؛ چون اکنون برنامه اطلاعاتی را در مورد نحوهی مسیریابی قسمت محصولات برنامه، ندارد.
Lazy loading یک ماژول
برای بارگذاری غیرهمزمان یک ماژول و یا همان Lazy loading، میتوان از خاصیت loadChildren تنظیمات مسیریابی، استفاده کرد:
{ path: 'products', loadChildren:'app/product/product.module#ProductModule' },
با این تنظیم، زمانیکه مسیر ریشهی produtcs درخواست شد، کار بارگذاری ماژول آن صورت گرفته و تنظیمات مسیریابی آن به سیستم اضافه میشود. به علاوه کار فعالسازی و نمایش کامپوننت آن را نیز انجام خواهد داد.
به همین منظور فایل src\app\app-routing.module.ts را گشوده و تنظیم فوق را به آن اضافه میکنیم:
const routes: Routes = [ { path: 'home', component: WelcomeComponent }, { path: 'welcome', redirectTo: 'home', pathMatch: 'full' }, { path: 'products', loadChildren: 'app/product/product.module#ProductModule' }, { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ];
chunk {0} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 165 kB {4} [initial] chunk {1} main.bundle.js, main.bundle.js.map (main) 32.7 kB {3} [initial] [rendered] chunk {2} styles.bundle.js, styles.bundle.js.map (styles) 129 kB {4} [initial] chunk {3} vendor.bundle.js, vendor.bundle.js.map (vendor) 2.72 MB [initial] [rendered] chunk {4} inline.bundle.js, inline.bundle.js.map (inline) 0 bytes [entry] chunk {5} 5.chunk.js, 5.chunk.js.map 51.1 kB {1} [rendered]
به علاوه اگر در منوی سایت بر روی لینک نمایش لیست محصولات کلیک کنیم، هنوز خروجی نمایش داده نمیشود (هرچند خطای 404 را هم دریافت نمیکنیم). علت اینجا است که اگر به فایل src\app\product\product-routing.module.ts مراجعه کنیم، تعریف این مسیر ریشه، در این فایل نیز وجود دارد:
const routes: Routes = [ { path: 'products', canActivate: [ AuthGuard ], children: [ ] } ];
import { AuthGuard } from './user/auth.guard'; const routes: Routes = [ { path: 'home', component: WelcomeComponent }, { path: 'welcome', redirectTo: 'home', pathMatch: 'full' }, { path: 'products', loadChildren: 'app/product/product.module#ProductModule', canActivate: [AuthGuard] }, { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ];
یک نکته: اکنون تنظیمات مسیریابی فایل src\app\product\product-routing.module.ts چنین شکلی را پیدا کردهاست:
const routes: Routes = [ { path: '', component: ProductListComponent }, { path: ':id', component: ProductDetailComponent, resolve: { product: ProductResolverService } }, { path: ':id/edit', component: ProductEditComponent, resolve: { product: ProductResolverService }, canDeactivate: [ProductEditGuard], children: [ { path: '', redirectTo: 'info', pathMatch: 'full' }, { path: 'info', component: ProductEditInfoComponent }, { path: 'tags', component: ProductEditTagsComponent } ] } ];
در این حالت اگر مسیر نمایش لیست محصولات را درخواست دهیم، مشاهده خواهیم کرد فایل 5.chunk.js که حاوی اطلاعات این ماژول است، به صورت مجزایی بارگذاری شده (lazy loading) و سپس با فعال شدن محافظ مسیر آن، صفحهی لاگین نمایش داده میشود:
این بارگذاری با تاخیر و در صورت نیاز، به دو علت آغاز برنامه را سریعتر میکند:
الف) مرورگر اطلاعی از وجود فایل 5.chunk.js در ابتدای کار نداشته و آنرا بارگذاری نمیکند (دریافت حجم کمتر، در آغاز نمایش برنامه).
ب) چون حجم کمتری از کدهای جاوا اسکریپت توسط مرورگر در آغاز کار دریافت میشود، کار پردازش و اجرای آنها نیز بسیار سریعتر خواهد شد.
بررسی محافظ canLoad
تعدادی از محافظهای مسیرها را در قسمت قبل بررسی کردیم. هنگامیکه کامپوننتها به صورت lazy loading فعالسازی شده و قالب آنها نمایش داده میشوند، میتوان از محافظ مسیر دیگری به نام canLoad نیز استفاده کرد و هدف از آن، بررسی منطقی، پیش از فعالسازی یک مسیر غیرهمزمان است. بنابراین اگر این محافظ false را برگرداند، حتی فایلهای اسکریپت این ماژول، بارگذاری اولیه نیز نخواهد شد. به این ترتیب کسانیکه دسترسی به یک مسیر را نداشته باشند، فایلهای اسکریپت متناظر با آنرا نیز دریافت نخواهند کرد.
در مثال جاری، اگر به برگهی network ابزار developer مرورگر دقت کنید، با درخواست نمایش مسیر لیست محصولات، ابتدا فایل js آن دریافت میشود که حاوی اطلاعات تمام کامپوننتها و قالبهای مرتبط با این مسیر است و سپس صفحهی login نمایش داده خواهد شد. بنابراین اگر کاربر به این قسمت دسترسی نداشته باشد، فایل js آن بیجهت دریافت و بارگذاری شدهاست. برای بهبود این وضعیت میتوان نمایش لاگین را پیش از بارگذاری فایل js این ماژول فعالسازی کرد و این مورد هدف اصلی محافظ canLoad است.
در ادامه برای تکمیل مثال جاری، میتوان AuthGuard را طوری تنظیم کرد که علاوه بر پیاده سازی CanActivate، اینترفیس CanLoad را نیز پیاده سازی کند:
import { ActivatedRouteSnapshot, RouterStateSnapshot, CanActivate, Router, CanLoad, Route } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate, CanLoad { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { return this.checkLoggedIn(state.url); } canLoad(route: Route): boolean { return this.checkLoggedIn(route.path); } // … the same as before }
مرحلهی بعد، تغییر فایل src\app\app-routing.module.ts و جایگزین کردن تعریف فعلی canActivate با canLoad است:
{ path: 'products', loadChildren: 'app/product/product.module#ProductModule', canLoad: [AuthGuard] },
پیش بارگذاری ماژولها
با فعالسازی lazy loading، ماژولهای مورد نیاز کاربر دیگر به همراه فایلهای js ابتدایی برنامه که در فایل index.html ارجاع مستقیمی به آنها دارند، ارائه نمیشوند و تنها در صورت درخواست مشاهدهی مسیری، کار بارگذاری آنها توسط برنامه صورت خواهد گرفت. همین مساله میتواند در بار اول نمایش این ماژولها تاخیر کوتاهی را سبب شود. به همین جهت قابلیت پیش بارگذاری ماژولها نیز در سیستم مسیریاب Angular پیش بینی شدهاست. به این قابلیت preloading و یا eager lazy loading نیز میگویند. در این حالت برنامه در پشت صحنه، کار پیش واکشی ماژولها را انجام میدهد و زمانیکه کاربری مسیری را درخواست میدهد، آْن مسیر را بدون درنگ مشاهده خواهد کرد.
بدیهی است این قابلیت نباید برای ماژولهایی که قرار است توسط کاربرانی خاص مشاهده شوند فعال شود و هدف آن دسترسی سریع به ماژولهای پرکاربرد برنامهاست.
در اینجا سه استراتژی پیش بارگذاری ماژولها میسر است:
- No preloading که حالت پیش فرض است.
- Preload all سبب پیش بارگذاری تمام قسمتهای lazy load برنامه میشود.
- Custom که اجازهی تعریف یک استراتژی سفارشی را میدهد.
برای مثال برای فعالسازی حالت Preload all، باید به فایل src\app\app-routing.module.ts مراجعه کرده و تغییرات ذیل را اعمال کنیم:
import { Routes, RouterModule, PreloadAllModules } from '@angular/router'; @NgModule({ imports: [RouterModule.forRoot( routes, { enableTracing: true, preloadingStrategy: PreloadAllModules /*, useHash: true*/ } )],
یک نکته: وجود محافظ canLoad، هر نوع استراتژی prealoading را غیرفعال میکند. اما prealoading با سایر انواع محافظها کار میکند.
بنابراین برای آزمایش تنظیم preloadingStrategy: PreloadAllModules، تعریف canLoad را به canActivate تغییر دهید.
تعریف استراتژیهای سفارشی پیش بارگذاری ماژولها
اگر نیاز به یک استراتژی پیش بارگذاری بهتر از هیچ یا همه باشد، میتوان یک استراتژی سفارشی را نیز تدارک دید و ایجاد آن سه مرحلهی ایجاد سرویس مرتبط، ثبت آن سرویس در ماژول و در آخر تنظیم مسیریابی را به همراه دارد.
برای این منظور ابتدا دستور ذیل را صادر کنید تا قالب ابتدایی سرویس SelectiveStrategy ایجاد شود:
>ng g s SelectiveStrategy -m app.module
installing service create src\app\selective-strategy.service.spec.ts create src\app\selective-strategy.service.ts update src\app\app.module.ts
سپس کدهای SelectiveStrategyService را به نحو ذیل تغییر دهید:
import { Injectable } from '@angular/core'; import { Route, PreloadingStrategy } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/of'; @Injectable() export class SelectiveStrategyService implements PreloadingStrategy { preload(route: Route, load: Function): Observable<any> { if (route.data && route.data['preload']) { return load(); } return Observable.of(null); } }
- پارامتر اول این متد، اطلاعاتی را در مورد مسیر جاری در اختیار ما قرار میدهد و دومین پارامتر آن متدی است که کار preloading را انجام میدهد.
- در اینجا است که تصمیم میگیریم ماژولی را preload کنیم یا خیر. برای نمونه در اینجا از خاصیت data مسیریابی استفاده شدهاست. این خاصیت نیز به یک مقدار ثابت اشاره میکند (قسمت «ارسال اطلاعات ثابت به مسیرهای مختلف برنامه» قسمت چهارم). برای مثال نام دلخواه آنرا preload گذاشتهایم و اگر مقدار آن به true تنظیم شده بود، آنگاه این مسیر preload خواهد شد. فراخوانی متد load در اینجا به معنای preloading این مسیر است. در غیراینصورت null را بازگشت میدهیم.
در ادامه نیاز است در فایل src\app\app-routing.module.ts، بجای معرفی PreloadAllModules، این استراتژی سفارشی خود را معرفی کرد:
import { SelectiveStrategyService } from './selective-strategy.service'; @NgModule({ imports: [RouterModule.forRoot( routes, { enableTracing: true, preloadingStrategy: SelectiveStrategyService /*, preloadingStrategy: PreloadAllModules*/ /*, useHash: true*/ } )],
{ path: 'products', loadChildren: 'app/product/product.module#ProductModule', //canLoad: [AuthGuard] canActivate: [AuthGuard], data: { preload: true } },
برای آزمایش آن، برنامه را مجدا اجرا کرده و صفحه را refresh کنید. سپس برگهی network ابزار developers را نیز باز نگه دارید. مشاهده خواهید کرد که علاوه بر فایلهای js اصلی برنامه که در فایل index.html ارجاعی را دارند، فایل 5.chunk.js نیز پیش بارگذاری شدهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-09.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.