مطالب
نوشتن پرس و جو در Entity Framework‌ با استفاده از LINQ To Entity قسمت سوم
اجرای پرس و جو روی داده‌های به هم مرتبط (Related Data)
اگر به موجودیت Customer دقت کنید دارای خصوصیتی با نام Orders می‌باشد که از نوع <IList<Order هست یعنی دارای لیستی از Order هاست بنابراین یک رابطه یک به چند بین Customer و Order وجود دارد. در ادامه به بررسی نحوه پرس و جو کردن روی داده‌های به هم مرتبط خواهیم پرداخت.
ابتدا به کد زیر دقت کنید:
private static void Query10()
{
    using (var context = new StoreDbContext())
    {
        var customers = context.Customers;
        foreach (var customer in customers)
        {
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family);
            foreach (var order in customer.Orders)
            {
                Console.WriteLine("\t Order Date: {0}", order.Date);
            }
        }
    }
}
اگر کد بالا را اجرا کنید هنگام اجرای حلقه داخلی با خطای زیر مواجه خواهید شد:
System.InvalidOperationException: There is already an open DataReader associated with this Command which must be closed first
همانطور که قبلا اشاره شد EF با اجرای یک پرس و جو به یکباره داده‌ها را باز نمی‌گرداند بنابراین در حلقه اصلی که روی Customers زده شده است با هر پیمایش یک customer از Database فراخوانی می‌شود درنتیجه DataReader تا پایان یافتن حلقه باز می‌ماند. حال آنکه حلقه داخلی نیز برای خواندن Order‌ها نیاز به اجرای یک پرس و جو دارد بنابراین DataReader ای جدید باز می‌شود و در نتیجه با خطایی مبنی بر اینکه DataReader دیگری باز است، مواجه می‌شویم. برای حل این مشکل می‌بایست جهت باز بودن چند DataReader همزمان، کد زیر را به ConnectionString اضافه کنیم
MultipleActiveResultSets = true
که با این تغییر کد بالا به درستی اجرا می‌شود. 
در بارگذاری داده‌های به هم مرتبط EF سه روش را در اختیار ما قرار می‌دهد:
  •  Lazy Loading
  • Eager Loading
  • Explicit Loading
که در ادامه به بررسی آنها خواهیم پرداخت.
Lazy Loading: در این روش داده‌های مرتبط در صورت نیاز با یک پرس وجوی جدید که به صورت اتوماتیک توسط EF ساخته می‌شود، گرفته خواهند شد. کد زیر را در نظر بگیرید:
private static void Query11()
{
    using (var context = new StoreDbContext())
    {
        var customer = context.Customers.First();

        Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family);
        foreach (var order in customer.Orders)
        {
            Console.WriteLine("\t Order Date: {0}", order.Date);
        }
    }
}
اگر این کد را اجرا کنید خواهید دید که یک بار پرس و جویی مبنی بر دریافت اولین Customer روی database زده خواهد شد و پس از چاپ آن در ادامه برای نمایش Order‌های این Customer پرس و جوی دیگری زده خواهد شد. در حقیقت پرس و جوی اول فقط Customer را بازگشت می‌دهد و در ادامه،  اول حلقه، جایی که نیاز به Order‌های این Customer می‌شود EF پرس و جو دوم را بصورت هوشمندانه و اتوماتیک اجرا می‌کند. به این روش بارگذاری داده‌های مرتبط Lazy Loading گفته می‌شود که به صورت پیش فرض در EF فعال است.
برای غیرفعال کردن این روش، کد زیر را اجرا کنید:
context.Configuration.LazyLoadingEnabled = false;
EF از dynamic proxy برای Lazy Loading استفاده می‌کند. به این صورت که در زمان اجرا کلاسی جدید که از کلاس POCO مان ارث برده است، ساخته می‌شود. این کلاس proxy می‌باشد و در آن navigation property‌ها بازنویسی شده‌اند و کمی منطق برای خواندن داده‌های وابسته اضافه شده است.
برای ایجاد dynamic proxy شروط زیر لازم است:
کلاس POCO می‌بایست public بوده و sealed نباشد.
Navigation property‌ها می‌بایست virtual باشد.
در صورتیکه هرکدام از این دو شرط برقرار نباشند کلاس proxy ساخته نمی‌شود و Lazy Loading حتی در صورت فعال بودن انجام نخواهد شد. مثلا اگر پراپرتی Orders در کلاس Customer مان virtual نباشد.  در شروع حلقه کد بالا پرس و جوی جدید اجرا نشده و در نتیجه مقدار این پراپرتی null خواهد ماند.
Lazy Loading به ما در عدم بارگذاری داده‌های مرتبط که به آنها نیازی نداریم، کمک می‌کند. اما در صورتیکه به داده‌های مرتبط نیاز داشته باشیم "مسئله Select n+1" پیش خواهد آمد که باید این مسئله را مد نظر داشته باشیم.
مسئله Select n+1: کد زیر را در نظر بگیرد
private static void Query12()
{
    using (var context = new StoreDbContext())
    {
        var customers = context.Customers;
        foreach (var customer in customers)
        {
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family);
            foreach (var order in customer.Orders)
            {
                Console.WriteLine("\t Order Date: {0}", order.Date);
            }
        }
    }
}
هنگام اجرای کد بالا یک پرس و جو برای خواندن Customer‌ها زده خواهد شد و به ازای هر Customer یک پرس و جوی دیگر برای گرفتن Order‌ها زده خواهد شد. در این صورت پرس و جوی اول ما اگر n مشتری را برگرداند، n پرس و جو نیز برای گرفتن Order‌ها زده خواهد شد که روهم n+1 دستور Select می‌شود. این تعداد پرس و جو موجب عدم کارایی می‌شود و برای رفع این مسئله نیاز به امکانی جهت بارگذاری هم زمان داده‌های مرتبط مورد نیاز خواهد بود. این امکان با استفاده از Eager Loading برآورده می‌شود.

روش Eager Loading: هنگامی که در یک پرس و جو نیاز به بارگذاری همزمان داده‌های مرتبط نیز باشد، از این روش استفاده می‌شود. برای این منظور از متد Include استفاده می‌شود که ورودی آن navigation property مربوطه می‌باشد. این پارامتر ورودی را همانطور که در کد زیر مشاهده می‌کنید، می‌توان به صورت string و یا Lambda Expression مشخص کرد.
دقت شود که برای حالت Lambda Expression بایدSystem.Data.Entity به using‌ها اضافه شود.
private static void Query13()
{
    using (var context = new StoreDbContext())
    {
        var customers = context.Customers.Include(c => c.Orders);
        //var customers = context.Customers.Include("Orders");
        foreach (var customer in customers)
        {
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family);
            foreach (var order in customer.Orders)
            {
                Console.WriteLine("\t Order Date: {0}", order.Date);
            }
        }
}
در این صورت یک پرس و جو به صورت join اجرا خواهد شد.
اگر داده‌های مرتبط در چند سطح باشند، می‌‌توان با دادن مسیر داده‌های مرتبط اقدام به بارگذاری آنها کرد. به مثالهای زیر توجه کنید:
context.OrderDetails.Include(o => o.Order.Customer)
در پرس و جوی بالا به ازای هر OrderDetail داده‌های مرتبط Order و Customer آن بارگذاری می‌شود.
context.Orders.Include(o => o.OrderDetail.Select(od => od.Product))
در پرس و جوی بالا به ازای هر Order لیست  OrderDetail ها و برای هر OrderDetail داده مرتبط Product آن بارگذاری می‌شود.
context.Orders.Include(o => o.Customer).Include(o => o.OrderDetail)
در پرس و جوی بالا به ازای هر Order داده‌های مرتبط  OrderDetail  و Customer آن بارگذاری می‌شود.

روش Explicit Loading: این روش مانند Lazy Loading می‌باشد که می‌توان داده‌های مرتبط را جداگانه فراخوانی کرد اما نه به صورت اتوماتیک توسط  EF بلکه به صورت صریح توسط خودمان انجام می‌شود. این روش حتی اگر navigation property‌های ما virtual نباشند نیز قابل انجام است. برای انجام این روش از متد DbContext.Entry استفاده می‌شود.
private static void Query14()
{
    using (var context = new StoreDbContext())
    {
        var customer = context.Customers.First(c => c.Family == "Jamshidi");

        context.Entry(customer).Collection(c => c.Orders).Load();

        foreach (var order in customer.Orders)
        {
            Console.WriteLine(order.Date);
        }
    }
}
در پرس و جوی بالا تمام Order‌های یک Customer به صورت جدا گرفته شده است برای این منظور از چون Orders یک لیست می‌باشد، از متد Collection استفاده شده است.
private static void Query15()
{
    using (var context = new StoreDbContext())
    {
        var order = context.Orders.First();

        context.Entry(order).Reference(o => o.Customer).Load();

        Console.WriteLine(order.Customer.FullName);
    }
}
در پرس و جوی بالا Customer یک Order صراحتا و به صورت جداگانه از database گرفته شده است.
با توجه به دو مثال بالا مشخص است که اگر داده مرتبط ما به صورت لیست است از Collection و درغیر این صورت از Reference استفاده می‌شود.
در صورتیکه بخواهیم ببینیم آیا داده‌ی مرتبط مان بازگذاری شده است یا خیر، از خصوصیت IsLoaded به صورت زیر استفاده می‌کنیم:
if (context.Entry(order).Reference(o => o.Customer).IsLoaded)
    context.Entry(order).Reference(o => o.Customer).Load();
و در آخر اگر بخواهیم روی داده‌های مرتبط پرس و جو اجرا کنیم نیز این قابلیت وجود دارد. برای این منظور از Query استفاده می‌کنیم.
private static void Query16()
{
    using (var context = new StoreDbContext())
    {
        var customer = context.Customers.First(c => c.Family == "Jamshidi");

        IQueryable<Order> query = context.Entry(customer).Collection(c => c.Orders).Query();

        var order = query.First();
    }
}

اشتراک‌ها
Lazy loading در Blazor Wasm

هنگام اجرای یک برنامه Blazor WebAssembly  در ابتدا تمام اسمبلیهای مورد نیاز در مرورگر بارگیری شده و ممکن است اسمبلی باشد که در همان ابتدا , به لود کردن آن نیازی نداشته باشیم و مختص یک صفحه خاص باشد و فقط پس از مراجعه به این صفحه بارگذاری شود. با Lazy Loading  میتوانیم در موقع نیاز اسمبلیها را بارگذاری کرد و سرعت اولیه را بهیود بخشید.

Lazy loading در Blazor Wasm
مطالب
Globalization در ASP.NET MVC - قسمت دوم

به‌روزرسانی فایلهای Resource در زمان اجرا

یکی از ویژگیهای مهمی که در پیاده سازی محصول با استفاده از فایلهای Resource باید به آن توجه داشت، امکان بروز رسانی محتوای این فایلها در زمان اجراست. از آنجاکه احتمال اینکه کاربران سیستم خواهان تغییر این مقادیر باشند بسیار زیاد است، بنابراین درنظر گرفتن چنین ویژگی‌ای برای محصول نهایی میتواند بسیار تعیین کننده باشد. متاسفانه پیاده سازی چنین امکانی درباره فایلهای Resource چندان آسان نیست. زیرا این فایلها همانطور که در قسمت قبل توضیح داده شد پس از کامپایل به صورت اسمبلی‌های ستلایت (Satellite Assembly) درآمده و دیگر امکان تغییر محتوای آنها بصورت مستقیم و به آسانی وجود ندارد.

نکته: البته نحوه پیاده سازی این فایلها در اسمبلی نهایی (و در حالت کلی نحوه استفاده از هر فایلی در اسمبلی نهایی) در ویژوال استودیو توسط خاصیت Build Action تعیین میشود. برای کسب اطلاعات بیشتر راجع به این خاصیت به اینجا رجوع کنید.

یکی از روشهای نسبتا من‌درآوردی که برای ویرایش و به روزرسانی کلیدهای Resource وجود دارد بدین صورت است:
- ابتدا باید اصل فایلهای Resource به همراه پروژه پابلیش شود. بهترین مکان برای نگهداری این فایلها فولدر App_Data است. زیرا محتویات این فولدر توسط سیستم FCN (همان File Change Notification) در ASP.NET رصد نمیشود.
نکته: علت این حساسیت این است که FCN در ASP.NET تقریبا تمام محتویات فولدر سایت در سرور (فولدر App_Data یکی از معدود استثناهاست) را تحت نظر دارد و رفتار پیشفرض این است که با هر تغییری در این محتویات، AppDomain سایت Unload میشود که پس از اولین درخواست دوباره Load میشود. این اتفاق موجب از دست دادن تمام سشن‌ها و محتوای کش‌ها و ... میشود (اطلاعات بیشتر و کاملتر درباره نحوه رفتار FCN در اینجا).
- سپس با استفاده یک مقدار کدنویسی امکاناتی برای ویرایش محتوای این فایلها فراهم شود. ازآنجا که محتوای این فایلها به صورت XML ذخیره میشود بنابراین براحتی میتوان با امکانات موجود این ویژگی را پیاده سازی کرد. اما در فضای نام System.Windows.Forms کلاسهایی وجود دارد که مخصوص کار با این فایلها طراحی شده اند که کار نمایش و ویرایش محتوای فایلهای Resource را ساده‌تر میکند. به این کلاسها در قسمت قبلی اشاره کوتاهی شده بود.
- پس از ویرایش و به روزرسانی محتوای این فایلها باید کاری کنیم تا برنامه از این محتوای تغییر یافته به عنوان منبع جدید بهره بگیرد. اگر از این فایلهای Rsource به صورت embed استفاده شده باشد در هنگام build پروژه محتوای این فایلها به صورت Satellite Assembly در کنار کتابخانه‌های دیگر تولید میشود. اسمبلی مربوط به هر زبان هم در فولدری با عنوان زبان مربوطه ذخیره میشود. مسیر و نام فایل این اسمبلی‌ها مثلا به صورت زیر است:
bin\fa\Resources.resources.dll
بنابراین در این روش برای استفاده از محتوای به روز رسانی شده باید عملیات Build این کتابخانه دوباره انجام شود و کتابخانه‌های جدیدی تولید شود. راه حل اولی که به ذهن میرسد این است که از ابزارهای پایه و اصلی برای تولید این کتابخانه‌ها استفاده شود. این ابزارها (همانطور که در قسمت قبل نیز توضیح داده شد) عبارتند از Resource Generator و Assembly Linker. اما استفاده از این ابزارها و پیاده سازی روش مربوطه سختتر از آن است که به نظر می‌آید. خوشبختانه درون مجموعه عظیم دات نت ابزار مناسبتری برای این کار نیز وجود دارد که کار تولید کتابخانه‌های موردنظر را به سادگی انجام میدهد. این ابزار با عنوان Microsoft Build شناخته میشود که در اینجا توضیح داده شده است. 

خواندن محتویات یک فایل resx.
همانطور که در بالا توضیح داده شد برای راحتی کار میتوان از کلاس زیر که در فایل System.Windows.Forms.dll قرار دارد استفاده کرد:
System.Resources.ResXResourceReader
این کلاس چندین کانستراکتور دارد که مسیر فایل resx. یا استریم مربوطه به همراه چند گزینه دیگر را به عنوان ورودی میگیرد. این کلاس یک Enumator دارد که یک شی از نوع IDictionaryEnumerator برمیگرداند. هر عضو این enumerator از نوع object است. برای استفاده از این اعضا ابتدا باید آنرا به نوع DictionaryEntry تبدیل کرد. مثلا بصورت زیر:
private void TestResXResourceReader()
{
  using (var reader = new ResXResourceReader("Resource1.fa.resx"))
  {
    foreach (var item in reader)
    {
      var resource = (DictionaryEntry)item;
      Console.WriteLine("{0}: {1}", resource.Key, resource.Value);
    }
  }
}
همانطور که ملاحظه میکنید استفاده از این کلاس بسیار ساده است. ازآنجاکه DictionaryEntry یک struct است، به عنوان یک راه حل مناسبتر بهتر است ابتدا کلاسی به صورت زیر تعریف شود:
public class ResXResourceEntry
{
  public string Key { get; set; }
  public string Value { get; set; }
  public ResXResourceEntry() { }
  public ResXResourceEntry(object key, object value)
  {
    Key = key.ToString();
    Value = value.ToString();
  }
  public ResXResourceEntry(DictionaryEntry dictionaryEntry)
  {
    Key = dictionaryEntry.Key.ToString();
    Value = dictionaryEntry.Value != null ? dictionaryEntry.Value.ToString() : string.Empty;
  }
  public DictionaryEntry ToDictionaryEntry()
  {
    return new DictionaryEntry(Key, Value);
  }
}
سپس با استفاده از این کلاس خواهیم داشت:
private static List<ResXResourceEntry> Read(string filePath)
{
  using (var reader = new ResXResourceReader(filePath))
  {
    return reader.Cast<object>().Cast<DictionaryEntry>().Select(de => new ResXResourceEntry(de)).ToList();
  }
}
حال این متد برای استفاده‌های آتی آماده است.

نوشتن در فایل resx.
برای نوشتن در یک فایل resx. میتوان از کلاس ResXResourceWriter استفاده کرد. این کلاس نیز در کتابخانه System.Windows.Forms در فایل System.Windows.Forms.dll قرار دارد:
System.Resources.ResXResourceWriter
متاسفانه در این کلاس امکان افزودن یا ویرایش یک کلید به تنهایی وجود ندارد. بنابراین برای ویرایش یا اضافه کردن حتی یک کلید کل فایل باید دوباره تولید شود. برای استفاده از این کلاس نیز میتوان به شکل زیر عمل کرد:
private static void Write(IEnumerable<ResXResourceEntry> resources, string filePath)
{
  using (var writer = new ResXResourceWriter(filePath))
  {
    foreach (var resource in resources)
    {
      writer.AddResource(resource.Key, resource.Value);
    }
  }
}
در متد فوق از همان کلاس ResXResourceEntry که در قسمت قبل معرفی شد، استفاده شده است. از متد زیر نیز میتوان برای حالت کلی حذف یا ویرایش استفاده کرد:
private static void AddOrUpdate(ResXResourceEntry resource, string filePath)
{
  var list = Read(filePath);
  var entry = list.SingleOrDefault(l => l.Key == resource.Key);
  if (entry == null)
  {
    list.Add(resource);
  }
  else
  {
    entry.Value = resource.Value;
  }
  Write(list, filePath);
}
در این متد از متدهای Read و Write که در بالا نشان داده شده‌اند استفاده شده است.

حذف یک کلید در فایل resx.
برای اینکار میتوان از متد زیر استفاده کرد:
private static void Remove(string key, string filePath)
{
  var list = Read(filePath);
  list.RemoveAll(l => l.Key == key); 
  Write(list, filePath);
}
در این متد، از متد Write که در قسمت معرفی شد، استفاده شده است.

راه حل نهایی
قبل از بکارگیری روشهای معرفی شده در این مطلب بهتر است ابتدا یکسری قرارداد بصورت زیر تعریف شوند:
- طبق راهنماییهای موجود در قسمت قبل یک پروژه جداگانه با عنوان Resources برای نگهداری فایلهای resx. ایجاد شود.
- همواره آخرین نسخه از محتویات موردنیاز از پروژه Resources باید درون فولدری با عنوان Resources در پوشه App_Data قرار داشته باشد.
- آخرین نسخه تولیدی از محتویات موردنیاز پروژه Resource در فولدری با عنوان Defaults در مسیر App_Data\Resources برای فراهم کردن امکان "بازگرداندن به تنظیمات اولیه" وجود داشته باشد.
برای فراهم کردن این موارد بهترین راه حل استفاده از تنظیمات Post-build event command line است. اطلاعات بیشتر درباره Build Eventها در اینجا.

برای اینکار من از دستور xcopy استفاده کردم که نسخه توسعه یافته دستور copy است. دستورات استفاده شده در این قسمت عبارتند از:
xcopy $(ProjectDir)*.* $(SolutionDir)MvcApplication1\App_Data\Resources /e /y /i /exclude:$(ProjectDir)excludes.txt
xcopy $(ProjectDir)*.* $(SolutionDir)MvcApplication1\App_Data\Resources\Defaults /e /y /i /exclude:$(ProjectDir)excludes.txt
xcopy $(ProjectDir)$(OutDir)*.* $(SolutionDir)MvcApplication1\App_Data\Resources\Defaults\bin /e /y /i 
در دستورات فوق آرگومان e/ برای کپی تمام فولدرها و زیرفولدرها، y/ برای تایید تمام کانفیرم ها، و i/ برای ایجاد خودکار فولدرهای موردنیاز استفاده میشود. آرگومان exclude/ نیز همانطور که از نامش پیداست برای خارج کردن فایلها و فولدرهای موردنظر از لیست کپی استفاده میشود. این آرگومان مسیر یک فایل متنی حاوی لیست این فایلها را دریافت میکند. در تصویر زیر یک نمونه از این فایل و مسیر و محتوای مناسب آن را مشاهده میکنید:

با استفاده از این فایل excludes.txt فولدرهای bin و obj و نیز فایلهای با پسوند user. و vspscc. (مربوط به TFS) و نیز خود فایل excludes.txt از لیست کپی دستور xcopy حذف میشوند و بنابراین کپی نمیشوند. درصورت نیاز میتوانید گزینه‌های دیگری نیز به این فایل اضافه کنید.
همانطور که در اینجا اشاره شده است، در تنظیمات Post-build event command line یکسری متغیرهای ازپیش تعریف شده (Macro) وجود دارند که از برخی از آنها در دستوارت فوق استفاده شده است:
(ProjectDir)$ : مسیر کامل و مطلق پروژه جاری به همراه یک کاراکتر \ در انتها
(SolutionDir)$ : مسیر کامل و مطلق سولوشن به همراه یک کاراکتر \ در انتها
(OutDir)$ : مسیر نسبی فولدر Output پروژه جاری به همراه یک کاراکتر \ در انتها

نکته: این دستورات باید در Post-Build Event پروژه Resources افزوده شوند.

با استفاده از این تنظیمات مطمئن میشویم که پس از هر Build آخرین نسخه از فایلهای موردنیاز در مسیرهای تعیین شده کپی میشوند. درنهایت با استفاده از کلاس ResXResourceManager که در زیر آورده شده است، کل عملیات را ساماندهی میکنیم:
public class ResXResourceManager
{
  private static readonly object Lock = new object();
  public string ResourcesPath { get; private set; }
  public ResXResourceManager(string resourcesPath)
  {
    ResourcesPath = resourcesPath;
  }
  public IEnumerable<ResXResourceEntry> GetAllResources(string resourceCategory)
  {
    var resourceFilePath = GetResourceFilePath(resourceCategory);
    return Read(resourceFilePath);
  }
  public void AddOrUpdateResource(ResXResourceEntry resource, string resourceCategory)
  {
    var resourceFilePath = GetResourceFilePath(resourceCategory);
    AddOrUpdate(resource, resourceFilePath);
  }
  public void DeleteResource(string key, string resourceCategory)
  {
    var resourceFilePath = GetResourceFilePath(resourceCategory);
    Remove(key, resourceFilePath);
  }
  private string GetResourceFilePath(string resourceCategory)
  {
    var extension = Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName == "en" ? ".resx" : ".fa.resx";
    var resourceFilePath = Path.Combine(ResourcesPath, resourceCategory.Replace(".", "\\") + extension);
    return resourceFilePath;
  }
  private static void AddOrUpdate(ResXResourceEntry resource, string filePath)
  {
    var list = Read(filePath);
    var entry = list.SingleOrDefault(l => l.Key == resource.Key);
    if (entry == null)
    {
      list.Add(resource);
    }
    else
    {
      entry.Value = resource.Value;
    }
    Write(list, filePath);
  }
  private static void Remove(string key, string filePath)
  {
    var list = Read(filePath);
    list.RemoveAll(l => l.Key == key); 
    Write(list, filePath);
  }
  private static List<ResXResourceEntry> Read(string filePath)
  {
    lock (Lock)
    {
      using (var reader = new ResXResourceReader(filePath))
      {
        var list = reader.Cast<object>().Cast<DictionaryEntry>().ToList();
        return list.Select(l => new ResXResourceEntry(l)).ToList();
      }
    }
  }
  private static void Write(IEnumerable<ResXResourceEntry> resources, string filePath)
  {
    lock (Lock)
    {
      using (var writer = new ResXResourceWriter(filePath))
      {
        foreach (var resource in resources)
        {
          writer.AddResource(resource.Key, resource.Value);
        }
      }
    }
  }
}
در این کلاس تغییراتی در متدهای معرفی شده در قسمتهای بالا برای مدیریت دسترسی همزمان با استفاده از بلاک lock ایجاد شده است.
با استفاده از کلاس BuildManager عملیات تولید کتابخانه‌ها مدیریت میشود. (در مورد نحوه استفاده از MSBuild در اینجا توضیحات کافی آورده شده است):
public class BuildManager
{
  public string ProjectPath { get; private set; }
  public BuildManager(string projectPath)
  {
    ProjectPath = projectPath;
  }
  public void Build()
  {
    var regKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\MSBuild\ToolsVersions\4.0");
    if (regKey == null) return;
    var msBuildExeFilePath = Path.Combine(regKey.GetValue("MSBuildToolsPath").ToString(), "MSBuild.exe");
    var startInfo = new ProcessStartInfo
    {
      FileName = msBuildExeFilePath,
      Arguments = ProjectPath,
      WindowStyle = ProcessWindowStyle.Hidden
    };
    var process = Process.Start(startInfo);
    process.WaitForExit();
  }
}
درنهایت مثلا با استفاده از کلاس ResXResourceFileManager مدیریت فایلهای این کتابخانه‌ها صورت میپذیرد:
public class ResXResourceFileManager
{
  public static readonly string BinPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase.Replace("file:///", ""));
  public static readonly string ResourcesPath = Path.Combine(BinPath, @"..\App_Data\Resources");
  public static readonly string ResourceProjectPath = Path.Combine(ResourcesPath, "Resources.csproj");
  public static readonly string DefaultsPath = Path.Combine(ResourcesPath, "Defaults");
  public static void CopyDlls()
  {
    File.Copy(Path.Combine(ResourcesPath, @"bin\debug\Resources.dll"), Path.Combine(BinPath, "Resources.dll"), true);
    File.Copy(Path.Combine(ResourcesPath, @"bin\debug\fa\Resources.resources.dll"), Path.Combine(BinPath, @"fa\Resources.resources.dll"), true);
    Directory.Delete(Path.Combine(ResourcesPath, "bin"), true);
    Directory.Delete(Path.Combine(ResourcesPath, "obj"), true);
  }
  public static void RestoreAll()
  {
    RestoreDlls();
    RestoreResourceFiles();
  }
  public static void RestoreDlls()
  {
    File.Copy(Path.Combine(DefaultsPath, @"bin\Resources.dll"), Path.Combine(BinPath, "Resources.dll"), true);
    File.Copy(Path.Combine(DefaultsPath, @"bin\fa\Resources.resources.dll"), Path.Combine(BinPath, @"fa\Resources.resources.dll"), true);
  }
  public static void RestoreResourceFiles(string resourceCategory)
  {
    RestoreFile(resourceCategory.Replace(".", "\\"));
  }
  public static void RestoreResourceFiles()
  {
    RestoreFile(@"Global\Configs");
    RestoreFile(@"Global\Exceptions");
    RestoreFile(@"Global\Paths");
    RestoreFile(@"Global\Texts");

    RestoreFile(@"ViewModels\Employees");
    RestoreFile(@"ViewModels\LogOn");
    RestoreFile(@"ViewModels\Settings");

    RestoreFile(@"Views\Employees");
    RestoreFile(@"Views\LogOn");
    RestoreFile(@"Views\Settings");
  }

  private static void RestoreFile(string subPath)
  {
    File.Copy(Path.Combine(DefaultsPath, subPath + ".resx"), Path.Combine(ResourcesPath, subPath + ".resx"), true);
    File.Copy(Path.Combine(DefaultsPath, subPath + ".fa.resx"), Path.Combine(ResourcesPath, subPath + ".fa.resx"), true);
  }
}
در این کلاس از مفهومی با عنوان resourceCategory برای استفاده راحتتر در ویوها استفاده شده است که بیانگر فضای نام نسبی فایلهای Resource و کلاسهای متناظر با آنهاست که براساس استانداردها باید برطبق مسیر فیزیکی آنها در پروژه باشد مثل Global.Texts یا Views.LogOn. همچنین در متد RestoreResourceFiles نمونه هایی از مسیرهای این فایلها آورده شده است.
پس از اجرای متد Build از کلاس BuildManager، یعنی پس از build پروژه Resource در زمان اجرا، باید ابتدا فایلهای تولیدی به مسیرهای مربوطه در فولدر bin برنامه کپی شده سپس فولدرهای تولیدشده توسط msbuild، حذف شوند. این کار در متد CopyDlls از کلاسResXResourceFileManager انجام میشود. هرچند در این قسمت فرض شده است که فایل csprj. موجود برای حالت debug تنظیم شده است.
نکته: دقت کنید که در این قسمت بلافاصله پس از کپی فایلها در مقصد با توجه به توضیحات ابتدای این مطلب سایت Restart خواهد شد که یکی از ضعفهای عمده این روش به شمار میرود.
سایر متدهای موجود نیز برای برگرداندن تنظیمات اولیه بکار میروند. در این متدها از محتویات فولدر Defaults استفاده میشود.
نکته: درصورت ساخت دوباره اسمبلی و یا بازگرداندن اسمبلی‌های اولیه، از آنجاکه وب‌سایت Restart خواهد شد، بنابراین بهتر است تا صفحه جاری بلافاصله پس از اتمام عملیات،دوباره بارگذاری شود. مثلا اگر از ajax برای اعمال این دستورات استفاده شده باشد میتوان با استفاده از کدی مشابه زیر در پایان فرایند صفحه را دوباره بارگذاری کرد:
window.location.reload();

در قسمت بعدی راه حل بهتری با استفاده از فراهم کردن پرووایدر سفارشی برای مدیریت فایلهای Resource ارائه میشود.
مطالب
مسیریابی تو در تو در Angularjs با استفاده از UI-Router

UI-Router   ابزاری برای مسیریابی در AngularJS است که این امکان را برایتان فراهم می‌کند تا بخش‌های برنامه رابط کاربریتان را به شکل یک ماشین حالت ساماندهی کنید. برخلاف سرویس route$ که بر اساس مسیریابی URL‌ها ساماندهی شده و کار می‌کند، UI-Router بر اساس حالت‌ها کار می‌کند، که این حالت‌ها می‌توانند در صورت لزوم مسیریابی هم داشته باشند.


UI-Router یکی از افزونه‌های مجموعه Angular-ui، و پاراگراف بالا معرفی آن در صفحه خانگیش است (تقریبا!). این افزونه جزئیات مفصلی دارد و در این مطلب تنها به معرفی آن خواهم پرداخت (بر اساس مطالب صفحه خانگیش). پیش از ادامه پیشنهاد می‌کنم اگر مطالب زیر را نخوانده‌اید ابتدا آن‌ها را مرور کنید:
برای استفاده از UI-Router باید:
  1. فایل جاوا اسکریپت آن را دانلود کنید (released یا minified).
  2. در صفحه اصلی برنامه‌تان پس از include کردن فایل اصلی AngularJS فایل angular-ui-router.js (یا angular-ui-router.min.js) را include کنید.
  3. 'ui.router' را به لیست وابستگی‌های ماژول اصلی اضافه کنید.
نتیجه چیزی شبیه این خواهد بود:
<!doctype html>
<html ng-app="myApp">
<head>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
    <script src="js/angular-ui-router.min.js"></script>
    <script>
        var myApp = angular.module('myApp', ['ui.router']);
        // For Component users, it should look like this:
        // var myApp = angular.module('myApp', [require('angular-ui-router')]);
    </script>
    ...
</head>
<body>
    ...
</body>
</html>

حالت‌ها و viewهای تو در تو

قابلیت اصلی UI-Router امکان تعریف حالت‌ها و vieweهای تو در تو است. در مطلب مسیریابی در AngularJs #بخش اول دایرکتیو ng-view معرفی شده است. هنگام استفاده از سرویس route$ با این دایرکتیو می‌توان محل مورد نظر برای بارگذاری محتویات مربوط به مسیرها را مشخص کرد. دایرکتیو ui-view در UI-Router همین نقش را دارد. فرض کنید این کد فایل index.html باشد:
<!-- index.html -->
<body>
    <div ui-view></div>
    <!-- We'll also add some navigation: -->
    <a ui-sref="state1">State 1</a>
    <a ui-sref="state2">State 2</a>
</body>
همانطور که ملاحظه می‌کنید در تگ‌های a از دایرکتیو ui-sref استفاده شده است. این دایرکتیو علاوه بر مدیریت تغییر حالت، خصوصیت href تگ a را در صورتی که حالت مشخص شده URL داشته باشد تولید می‌کند. البته برای استفاده از UI-Router ملزم به استفاده از دایرکتیو ui-sref نیستید و می‌توانید href را مشخص کنید. ولی با استفاده از ui-sref لازم نیست مسیر یک حالت را به یاد داشته باشید، و یا در صورت تغییر آن، همه hrefها را به روز کنید.
در ادامه برای هر کدام از حالت‌ها یک template اضافه می‌کنیم:
فایل state1.html:
<!-- partials/state1.html -->
<h1>State 1</h1>
<hr/>
<a ui-sref="state1.list">Show List</a>
<div ui-view></div>
فایل state2.html:
 <!-- partials/state2.html -->
<h1>State 2</h1> 
<hr /> 
<a ui-sref="state2.list">Show List</a> 
<div ui-view></div>

دو نکته قابل توجه در این templateها وجود دارد. اول اینکه همانطور که می‌بینید templateها خود شامل تگی با دایرکتیو ui-view هستند. و دوم مقدار دایرکتیو ui-sref است که به صورت state1.list و state2.list آمده است. این جدا سازی با نقطه نشان دهنده سلسله مراتب حالت‌هاست. یعنی حالت‌های state1 و state2 هرکدام حالت فرزندی به نام list دارند. در ادامه وقتی حالت‌ها و مسیریابی را در ()app.config تعریف کنیم این مسائل از هاله‌ای از ابهام که در آن هستند خارج می‌شوند! فعلا بیاید با راهنمای UI-Router پیش برویم و فایل‌های template حالت‌های فرزند را تعریف کنیم. templateهایی که قرار است در ui-view پدرانشان بارگذاری شوند:
<!-- partials/state1.list.html -->
<h3>List of State 1 Items</h3>
<ul>
  <li ng-repeat="item in items">{{ item }}</li>
</ul>

<!-- partials/state2.list.html -->
<h3>List of State 2 Things</h3>
<ul>
  <li ng-repeat="thing in things">{{ thing }}</li>
</ul>
خوب! حالا برویم سراغ شعبده بازی! برای اینکه از UI-Router استفاده کنید لازم است stateProvider$ و urlRouterProvider$ را به عنوان وابستگی به ()app.config تزریق کنید:
myApp.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
  //
  // For any unmatched url, redirect to /state1
  $urlRouterProvider.otherwise("/state1");
  //
  // Now set up the states
  $stateProvider
    .state('state1', {
      url: "/state1",
      templateUrl: "partials/state1.html"
    })
    .state('state1.list', {
      url: "/list",
      templateUrl: "partials/state1.list.html",
      controller: function($scope) {
        $scope.items = ["A", "List", "Of", "Items"];
      }
    })
    .state('state2', {
      url: "/state2",
      templateUrl: "partials/state2.html"
    })
    .state('state2.list', {
      url: "/list",
        templateUrl: "partials/state2.list.html",
        controller: function($scope) {
          $scope.things = ["A", "Set", "Of", "Things"];
        }
      })
    }]);
در ابتدا با متد ()urlRouterProvider.otherwise$ مسیر پیشفرض مشخص شده است. متد otherwise را باید از مقالات مسیریابی در AngularJS به یاد داشته باشید. سپس حالت‌های برنامه با استفاده از متد state تعریف شده است. این متد دو پارامتر ورودی دارد؛ اولی نام حالت و دومی یک شی شامل خصوصیات حالت. همانطور که می‌بینید این شی خصوصیاتی شبیه به همان‌ها که در متد ()routeProvider.when$ وجود داشت دارد. می‌شود گفت این خصوصیات همان‌ها هستند و همان عملکرد را دارند.
خصوصیت url مشخص کننده مسیر حالت است. این خصوصیت همان مقداریست که به عنوان پارامتر اول به ()routeProvider.when$ پاس می‌شد. در این پارامتر می‌شود متغیرهای url را هم به همان ترتیب تعریف کرد. مثلا اگر حالت state1 در آدرسش یک پارامتر id داشته باشد می‌شود آن را به این ترتیب تعریف کرد:
.state('state1', {
      url: "/state1/:id",
      templateUrl: "partials/state1.html"
    })
برای خواندن مقدار این متغیر باید از stateParams$ استفاده کرد:
$stateParams.id
به خصوصیت url دو حالت state1.list و state2.list دقت کنید. هردو برابر 'list/' است. یعنی هردو یک مسیر دارند؟ نه! بلکه مسیر state1.list برابر '/state1/list' و مسیر state2.list برابر '/state2/list' است. در واقع حالت state1.list یعنی list فرزند state1 و به همین ترتیب state2.list یعنی list فرزند state2. و می‌توان گفت UI-Router آدرس url حالت فرزند را، آدرسی نسبی، نسبت به url حالت پدر می‌داند. این رابطه سلسله مراتبی و پدر و فرزندی را می‌توان با استفاده از خصوصیت parent به صورت صریح‌تری مشخص کرد:
.state('list', {
    parent: "state1",
    url: "/list",
    templateUrl: "partials/state1.list.html",
    controller: function($scope) {
        $scope.items = ["A", "List", "Of", "Items"];
    }
})
.state('list', {
    parent: "state2",
    url: "/list",
    templateUrl: "partials/state2.list.html",
    controller: function($scope) {
        $scope.items = ["A", "List", "Of", "Items"];
    }
})
تا اینجای کار، اگر آدرس "state1/" وارد شود، فایل "partials/state1.html" در "ui-view" فایل "index.html" بارگذاری خواهد شد. اگر آدرس "state1/list/" وارد شود، ابتدا فایل "partials/state1.html" در "ui-view" فایل "index.html" بارگذاری شده، سپس فایل "partials/state1.list.html" در "ui-view"  آمده در فایل فایل "partials/state1.html" بارگذاری می‌شود. این همان امکان حالت‌ها و viewهای تو در تو است که UI-Router فراهم می‌کند. 
اینجا می‌توانید خروجی کدهای بالا را مشاهده کنید.
اگر مستقیما url یک حالت فرزند وارد شود، یا به عبارت دیگر، اگر بخواهیم مستقیما برنامه به حالتی که فرزند حالت دیگر است برود، UI-Router برنامه را ابتدا به حالت پدر، و پس از آن به حالت فرزند خواهد برد. حالت فرزند دو چیز را از حالت پدر به ارث می‌برد:
  1. وابستگی‌های فراهم شده در حالت پدر به وسیله "resolve"
  2. داده‌های سفارشی مشخص شده در خصوصیت data حالت پدر
استفاده از resolve در UI-Router مشابه استفاده از آن در route$  است. ولی افزودن داده‌های سفارشی کمی متفاوت است. برای افزودن داده‌های سفارشی باید از خصوصیت data یک حالت استفاده کرد:
.state('state1', {
    url: "/state1",
    templateUrl: "partials/state1.html",
    data:{
        foodata: 'addorder'
    }
})
برای دسترسی به این داده‌ها هم می‌توان از state.current.data$ استفاده کرد:
$state.current.data.foodata


Viewهای نامگذاری شده و چندگانه

یکی دیگر از قابلیت‌های کاربردی UI-Router امکان داشتن چند ui-view در هر template است (استفاده همزمان از این قابلیت و حالت‌های تو در تو، امکان مدیریت واسط کاربری را به خوبی فراهم می‌کند).  برای توضیح این قابلیت، با راهنمای UI-Router همراه شویم:
1. دستورالعمل برپایی UI-Router که در بالا آمده را اجرا کنید.
2. یک یا چند ui-view به برنامه‌تان اضافه کنید و آن‌ها را نامگذاری کنید:
<!-- index.html -->
<body>
    <div ui-view="viewA"></div>
    <div ui-view="viewB"></div>
    <!-- Also a way to navigate -->
    <a ui-sref="route1">Route 1</a>
    <a ui-sref="route2">Route 2</a>
</body>
3. حالت‌های برنامه‌تان را در روال config ماژول تعریف کنید:
myApp.config(function ($stateProvider) {
    $stateProvider
      .state('index', {
          url: "",
          views: {
              "viewA": { template: "index.viewA" },
              "viewB": { template: "index.viewB" }
          }
      })
      .state('route1', {
          url: "/route1",
          views: {
              "viewA": { template: "route1.viewA" },
              "viewB": { template: "route1.viewB" }
          }
      })
      .state('route2', {
          url: "/route2",
          views: {
              "viewA": { template: "route2.viewA" },
              "viewB": { template: "route2.viewB" }
          }
      })
});
4. خروجی کدهای بالا را اینجا مشاهده کنید.


چند نکته

UI-Router جزئیات فراوانی دارد و آنچه آمد تنها پرده برداری از آن بود. دلم می‌خواست می‌توانستم بیش از این آن را معرفی کنم، اما متاسفانه این روزها وقت آزاد کافی ندارم. در انتها می‌خواهم به چند نکته اشاره کنم:
روش controller as
برای استفاده از روش controller as در UI-Router باید به این ترتیب عمل کنید:
.state('list', {
    parent: "state1",
    url: "/list",
    templateUrl: "partials/state1.list.html",
    controller: "state1ListController as listCtrl1"
    }
})
.state('list', {
    parent: "state2",
    url: "/list",
    templateUrl: "partials/state2.list.html",
    controller: "state2ListController as listCtrl2"
    }
})

حالت‌های انتزاعی

حالت انتزاعی حالتی است که url ندارد و در نتیجه برنامه نمی‌تواند در آن حالت قرار گیرد. حالت‌های انتزاعی بسیار به درد خور هستند! مثلا فرض کنید چند حالت دارید که اشتراکاتی با هم دارند (همه باید در template مشابهی بارگذاری شود، یا وابستگی‌های یکسانی دارند، یا حتی سطح دسترسی یکسان). با تعریف یک حالت انتزاعی و جمع کردن همه وابستگی‌ها در آن، و تعریف حالت‌های مورد نظرتان به عنوان فرزندان حالت انتزاعی، می‌توانید اشتراکات حالت‌های برنامه را ساده‌تر مدیریت کنید.

حساسیت به حروف بزرگ و کوچک

در سرویس route$ با مقداردهی خصوصیت caseInsensitiveMatch می‌توانستیم مشخص کنیم که بزرگ و کوچک بودن حروف در تطبیق آدرس صفحه با پارامتر route در نظر گرفته بشود یا نه. خودمانیش اینکه url به حروف بزرگ و کوچک حساس باشد یا نه. متاسفانه در UI-Router از این امکان خبری نیست (البته فعلا) و آدرس‌های تعریف شده به حروف بزگ و کوچک حساس هستند.
 اینجا روشی برای حل این مشکل پیشنهاد شده، به این ترتیب که همه url‌های وارد شده به حروف کوچک تبدیل شود (راستش من این راه حل را نمی‌پسندم!).
چند روز قبل هم تغییراتی در کد UI-Router داده شده که امکان حساس نبودن به حروف کوچک و بزرگ فراهم شود. این تغییر هنوز در نسخه نهایی فایل UI-Router نیامده است. هرچند اگر بیاید هم آنچه تا امروز (23 اسفند 92) انجام شده مشکل را حل نمی‌کند.
اگر شما هم مثل من می‌خواهید کلا آدرس‌ها به حروف بزرگ و کوچک حساس نباشند، و فرصت حل کردن اساسی مشکل را هم ندارید به این ترتیب عمل کنید:
  • در فایل "angular-ui-router.js" عبارت "new RegExp(compiled)" را پیدا کرده و آن را به  "RegExp(compiled, 'i')" تبدیل کنید. و یا در "angular-ui-router.min.js" (هرکدام از فایل‌ها که استفاده می‌کنید) عبارت new RegExp(o) را پیدا کرده و آن را به new RegExp(o, "i")  تبدیل کنید. همین؛ صدایش را هم در نیاورید!


مطالب
نمایش در حال بارگذاری بودن صفحه در برنامه‌های Angular
پیشتر در مورد HTTP Client جدید Angular، مطلب «ارتقاء به HTTP Client در Angular 4.3» را بررسی کردیم. یکی دیگر از قابلیت‌های HttpClient که روش اصلی دسترسی به اطلاعات از راه دور در Angular 5 نیز می‌باشد، امکان تعریف Interceptors سفارشی برای آن است. به کمک Interceptors می‌توان به تمامی درخواست‌های HTTP ارسالی و پاسخ‌های دریافتی از سرور گوش فرا داد و در این بین منطق ویژه‌ای را پیاده سازی کرد؛ مانندcaching ،logging و غیره. در مطلب جاری قصد داریم از این قابلیت برای نمایش یک loading bar مشخص کننده‌ی شروع و پایان تمامی درخواست‌های HTTP استفاده کنیم.


دریافت و نصب پیشنیازها

برای نمایش loading bar در بالای صفحه، از کامپوننت ng2-slim-loading-bar استفاده خواهیم کرد. دمویی از آن‌را در اینجا می‌توانید مشاهده کنید.
برای نصب آن، ابتدا دستور ذیل را در ریشه‌ی پروژه اجرا کنید:
 npm install ng2-slim-loading-bar --save
پس از آن فایل angular-cli.json. را گشوده و فایل css آن‌را تعریف نمائید:
"styles": [
   "../node_modules/ng2-slim-loading-bar/style.css",
   "styles.css"
],
در ادامه به فایل app.module.ts مراجعه کرد و ماژول آن‌را نیز به نحو ذیل به برنامه معرفی کنید:
import {SlimLoadingBarModule} from 'ng2-slim-loading-bar';

@NgModule({
imports: [
     //...
     SlimLoadingBarModule.forRoot()
  ]
})
export class AppModule {
}

پس از این مقدمات، هرجایی که نیاز به استفاده‌ی از آن با کدنویسی باشد، ابتدا سرویس SlimLoadingBarService آن‌را به سازنده‌ی کلاس کامپوننت مدنظر تزریق می‌کنیم و سپس از متدهای start ،stop و complete آن می‌توان استفاده کرد.
 import {SlimLoadingBarService} from 'ng2-slim-loading-bar';
همچنین برای نمایش این کامپوننت باید selector آن را در فایل app.component.html در بالای صفحه اضافه کرد:
  <ng2-slim-loading-bar></ng2-slim-loading-bar>


تدارک یک LoaderInterceptor برای استفاده از ng2-slim-loading-bar

در ادامه می‌خواهیم هر زمانیکه در سراسر برنامه، درخواست HTTP ایی شروع شد، این کامپوننت نمایش داده شود و در پایان درخواست و یا درصورت بروز خطایی، پایان یابد و مخفی شود. برای این منظور یک Interceptor جدید را به صورت ذیل به پوشه‌ی Core برنامه اضافه می‌کنیم:
 ng g s core/interceptors/LoaderInterceptor --spec false
با این کدها:
import { Injectable } from "@angular/core";
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from "@angular/common/http";
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/do";
import { SlimLoadingBarService } from "ng2-slim-loading-bar";


@Injectable()
export class LoaderInterceptorService implements HttpInterceptor {

  constructor(private loadingBar: SlimLoadingBarService) {
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // start our loader here
    this.loadingBar.start();
    return next.handle(req).do(
      (event: HttpEvent<any>) => {
        if (event instanceof HttpResponse) {
          this.loadingBar.complete();
        }
      },
      (err: any) => {
        this.loadingBar.complete();
      });
  }
}
توضیحات:
- برای پیاده سازی یک interceptor جدید، نیاز است کلاس سرویسی را که HttpInterceptor را پیاده سازی می‌کند، ایجاد کنیم. برای تکمیل این پیاده سازی نیاز است متد intercept را با امضایی که مشاهده می‌کنید، تعریف کنیم:
 intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
- در اینجا پارامتر req به خود درخواست Http اشاره می‌کند.
- کار پارامتر next تنظیم و بازگشت یک HttpEvent observable توسط متد handle آن است.

برای نمونه در اینجا ابتدا سرویس SlimLoadingBarService به سازنده‌ی کلاس interceptor‌  تزریق شده‌است. سپس توسط آن می‌توان به متدهای start و complete این کامپوننت دسترسی یافت. برای مثال در ابتدای کار گوش فرادادن به درخواست جاری، متد start فراخوانی شده‌است و سپس زمانیکه پاسخی از سرور دریافت شده و یا خطایی رخ‌داده، متد complete آن فراخوانی شده‌است.


ثبت و معرفی LoaderInterceptorService به سیستم

برای معرفی interceptor تهیه شده، به فایل app.module.ts مراجعه کرد و قسمت providers آن‌را به نحو ذیل تکمیل می‌کنیم: 
 @NgModule({
  providers: [
   { provide: HTTP_INTERCEPTORS, useClass: LoaderInterceptorService, multi: true }
  ]
})
export class AppModule {}


آزمایش برنامه

اکنون اگر قسمت‌های مختلف برنامه را که با HttpClient جدید کار می‌کنند بررسی کنید، متوجه خواهید شد که با شروع هر درخواست، loading bar قرمزی در بالای صفحه ظاهر می‌شود و در پایان درخواست، به صورت خودکار مخفی می‌گردد. نکته‌ی مهم این روش عدم نیاز به تغییری در قسمت‌های مختلف برنامه است. این interceptor سراسری است و به صورت یکسانی بر روی کل برنامه تاثیر می‌گذارد.
مطالب
استفاده از مسیرهای مطلق در حین import ماژول‌ها در برنامه‌های مبتنی بر TypeScript
در حین import ماژول‌های TypeScript ایی پس از مدتی به یک چنین کدهایی خواهیم رسید:
import { SpecialCollection } from "../../special";
import { LoginComponent } from "../login";
import { TextUtils } from ".../../utils/text";
import { Router } from "../../../core/router";
در این حالت، در یک پوشه برای import ماژولی مشخص، چنین import ایی را خواهیم داشت:
import { Data } from '../data';
و در پوشه‌ی تو در توی دیگری، این تعریف به صورت زیر تغییر می‌کند:
import { Data } from '../../../data';
و در آخر برنامه پر می‌شود از مسیرهای نسبی ‘../../../..’ مانند. به این ترتیب جابجا کردن فایل‌ها و Refactoring آن‌ها، مشکل می‌شود.
خوشبختانه کامپایلر TypeScript به همراه تنظیمات baseUrl و paths است که توسط آن‌ها می‌توان این مسیرهای نسبی را به مسیرهای مطلق تبدیل کرد و در این حالت اهمیتی ندارد که ماژول مدنظر از چه سطحی و درون چه پوشه‌ی تو در تویی فراخوانی می‌شود، این مسیر import همواره ثابت خواهد بود.


تنظیمات فایل tsconfig.json برای معرفی مسیرهای مطلق ماژول‌ها

فرض کنید می‌خواهید از یکی از سرویس‌های Core Module استفاده کنید:


بسته به عمق پوشه‌ی استفاده کننده، به یک چنین تعریفی خواهید رسید:
import { BrowserStorageService } from "./../../core/browser-storage.service";
برای بهبود این وضعیت، فایل tsconfig.json و یا همان تنظیمات کامپایلر TypeScript را به نحو ذیل تکمیل می‌کنیم:
{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      "@app/*": [
        "app/*"
      ],
      "@app/core/*": [
        "app/core/*"
      ],
      "@app/shared/*": [
        "app/shared/*"
      ],
      "@env/*": [
        "environments/*"
      ]
    }
  }
}
در اینجا baseUrl به پوشه‌ی src برنامه اشاره می‌کند و مسیرهای بعدی بر این اساس محاسبه می‌شوند. در ادامه در قسمت paths، ابتدا یک نام مستعار ذکر می‌شود و سپس مسیری که ارائه دهنده‌ی آن است. ذکر @ در اینجا اختیاری است؛ اما ذکر */‌ها اجباری است.
پس از این تغییرات، اکنون افزونه‌ی پیشنهاد دهنده‌ی imports، هر دو حالت استفاده‌ی از مسیر مطلق بر اساس نام مستعار تعریف شده:
 import { BrowserStorageService } from "@app/core/browser-storage.service";
و یا استفاده‌ی از مسیر نسبی را نیز پیشنهاد می‌دهد:
 import { BrowserStorageService } from "./../../core/browser-storage.service";


برای مثال اگر دقت کرده باشید، روش import اجزای خود Angular به صورت زیر است:
 import { Component } from '@angular/core';
علت اینجا است که Angular از تعریف مشابهی به صورت زیر برای نگاشت پوشه‌ی node_modules آن به angular@ استفاده می‌کند:
"paths": {
    "@angular/*": ["node_modules/@angular/*"]
},
و ذکر @ اختیاری هم از اینجا اقتباس شده‌است.


یک نکته‌ی مهم: تنظیمات فوق بدون تنظیمات معادل webpack ناقص هستند

اگر از برنامه‌ی Angular CLI استفاده می‌کنید، تنظیمات ذکر شده، تا همینجا به پایان می‌رسند؛ چون webpack جزئی از Angular CLI است و تنظیمات پیش فرض آن، قسمت baseUrl و paths فایل tsconfig.json را به صورت خودکار پردازش می‌کند. اما اگر از TypeScript در محیط‌های دیگری استفاده می‌کنید که از webpack به صورت مجزایی استفاده می‌کنند، نیاز است قسمت resolve.alias فایل webpack.config.js را نیز جهت معرفی این تغییرات، اصلاح کنید. از این جهت که کامپایلر TypeScript این مسیرهای مطلق را در حین تولید فایل‌های نهایی JavaScript ایی معادل، به مسیرهای نسبی بازنویسی نمی‌کند و در این حالت webpack نمی‌داند که چطور باید این ماژول‌ها را یافته و یکی کند.
resolve: {
  extensions: ['*', '.js', '.ts'],
  modules: [
    rootDir,
    path.join(rootDir, 'node_modules')
  ],
  alias: {
    '@app': 'src/app'
  }
},


کوتاه کردن مسیرهای مطلق با معرفی فایل ویژه‌ی index.ts

تا اینجا بجای ذکر مسیر
import { BrowserStorageService } from "./../../core/browser-storage.service";
به مسیر مطلق زیر رسیدیم (صرفنظر از محل قرارگیری ماژولی که قرار است آن‌را import کند):
import { BrowserStorageService } from "@app/core/browser-storage.service";
این را هم می‌خواهیم به صورت زیر کوتاه‌تر کنیم:
import { BrowserStorageService } from "@app/core";
یعنی فقط app/core@ را ذکر کنیم.

برای اینکار نیاز است فایل ویژه‌ای را به نام index.ts، در ریشه‌ی پوشه‌ی core ایجاد کنیم (src\app\core\index.ts)، با این محتوا:
export * from "./browser-storage.service";
export * from "./app-config.service";
export * from "./seo-service";
در اینجا تمام ماژول‌هایی که توسط Core Module ارائه می‌شوند را یکبار export می‌کنیم.
برای نمونه اگر به پوشه‌ی node_modules\@angular خود مجموعه‌ی Angular هم مراجعه کنید، هر پوشه‌ی src آن به همراه یک فایل index.d.ts شبیه تعاریف فوق نیز هست.

پس از افزودن فایل index.ts به ریشه‌ی پوشه‌ی مدنظر، اکنون در لیست پیشنهادات، ذکر app/core@ تنها نیز ظاهر شده و استفاده‌ی از آن مجاز است:

مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت سوم - ورود به سیستم
پس از ایجاد AuthService در قسمت قبل، اکنون می‌خواهیم از آن برای تکمیل صفحه‌ی ورود به سیستم و همچنین تغییر منوی بالای برنامه یا همان کامپوننت header استفاده کنیم.


ایجاد ماژول Dashboard و تعریف کامپوننت صفحه‌ی محافظت شده

قصد داریم پس از لاگین موفق، کاربر را به یک صفحه‌ی محافظت شده هدایت کنیم. به همین جهت ماژول جدید Dashboard را به همراه کامپوننت یاد شده، به برنامه اضافه می‌کنیم:
>ng g m Dashboard -m app.module --routing
>ng g c Dashboard/ProtectedPage
پس از اجرای این دستورات، ابتدا به فایل app.module.ts مراجعه کرده و تعریف این ماژول را که به صورت خودکار به قسمت imports اضافه شده‌است، به قبل از AppRoutingModule منتقل می‌کنیم تا مسیریابی‌های آن توسط catch all ماژول AppRouting لغو نشوند:
import { DashboardModule } from "./dashboard/dashboard.module";

@NgModule({
  imports: [
    //...
    DashboardModule,
    AppRoutingModule
  ]
})
export class AppModule { }
همچنین به فایل dashboard-routing.module.ts ایجاد شده مراجعه کرده و مسیریابی کامپوننت جدید ProtectedPage را اضافه می‌کنیم:
import { ProtectedPageComponent } from "./protected-page/protected-page.component";

const routes: Routes = [
  { path: "protectedPage", component: ProtectedPageComponent }
];


ایجاد صفحه‌ی ورود به سیستم

در قسمت اول این سری، کارهای «ایجاد ماژول Authentication و تعریف کامپوننت لاگین» انجام شدند. اکنون می‌خواهیم کامپوننت خالی لاگین را به نحو ذیل تکمیل کنیم:
export class LoginComponent implements OnInit {

  model: Credentials = { username: "", password: "", rememberMe: false };
  error = "";
  returnUrl: string;
مدل login را همان اینترفیس Credentials تعریف شده‌ی در قسمت قبل درنظر گرفته‌ایم. به همراه دو خاصیت error جهت نمایش خطاها در ذیل قسمت لاگین و همچنین ذخیره سازی returnUrl در صورت وجود:
  constructor(
    private authService: AuthService,
    private router: Router,
    private route: ActivatedRoute) { }

  ngOnInit() {
    // reset the login status
    this.authService.logout(false);

    // get the return url from route parameters
    this.returnUrl = this.route.snapshot.queryParams["returnUrl"];
  }
AuthService را به سازنده‌ی کامپوننت لاگین تزریق کرده‌ایم تا بتوان از متدهای login و Logout آن استفاده کرد. همچنین از سرویس Router برای استفاده‌ی از متد navigate آن استفاده می‌کنیم و از سرویس ActivatedRoute برای دریافت کوئری استرینگ returnUrl، در صورت وجود، کمک خواهیم گرفت.
اکنون متد ارسال این فرم چنین شکلی را پیدا می‌کند:
  submitForm(form: NgForm) {
    this.error = "";
    this.authService.login(this.model)
      .subscribe(isLoggedIn => {
        if (isLoggedIn) {
          if (this.returnUrl) {
            this.router.navigate([this.returnUrl]);
          } else {
            this.router.navigate(["/protectedPage"]);
          }
        }
      },
      (error: HttpErrorResponse) => {
        console.log("Login error", error);
        if (error.status === 401) {
          this.error = "Invalid User name or Password. Please try again.";
        } else {
          this.error = `${error.statusText}: ${error.message}`;
        }
      });
  }
در اینجا توسط وهله‌ی تزریق شده‌ی this.authService، کار فراخوانی متد login و ارسال شیء Credentials به سمت سرور صورت می‌گیرد. خروجی متد login یک Observable از نوع boolean است. بنابراین در صورت true بودن این مقدار و یا موفقیت آمیز بودن عملیات لاگین، کاربر را به یکی از دو صفحه‌ی protectedPage و یا this.returnUrl (در صورت وجود)، هدایت خواهیم کرد.
صفحه‌ی خالی protectedPage را در ابتدای بحث، در ذیل ماژول Dashboard ایجاد کردیم.
در سمت سرور هم در صورت شکست اعتبارسنجی کاربر، یک return Unauthorized صورت می‌گیرد که معادل error.status === 401 کدهای فوق است و در اینجا در قسمت خطای عملیات بررسی شده‌است.

قالب این کامپوننت نیز به صورت ذیل به model از نوع Credentials آن متصل شده‌است:
<div class="panel panel-default">
  <div class="panel-heading">
    <h2 class="panel-title">Login</h2>
  </div>
  <div class="panel-body">
    <form #form="ngForm" (submit)="submitForm(form)" novalidate>
      <div class="form-group" [class.has-error]="username.invalid && username.touched">
        <label for="username">User name</label>
        <input id="username" type="text" required name="username" [(ngModel)]="model.username"
          #username="ngModel" class="form-control" placeholder="User name">
        <div *ngIf="username.invalid && username.touched">
          <div class="alert alert-danger"  *ngIf="username.errors['required']">
            Name is required.
          </div>
        </div>
      </div>

      <div class="form-group" [class.has-error]="password.invalid && password.touched">
        <label for="password">Password</label>
        <input id="password" type="password" required name="password" [(ngModel)]="model.password"
          #password="ngModel" class="form-control" placeholder="Password">
        <div *ngIf="password.invalid && password.touched">
          <div class="alert alert-danger"  *ngIf="password.errors['required']">
            Password is required.
          </div>
        </div>
      </div>

      <div class="checkbox">
        <label>
          <input type="checkbox" name="rememberMe" [(ngModel)]="model.rememberMe"> Remember me
        </label>
      </div>

      <div class="form-group">
        <button type="submit" class="btn btn-primary" [disabled]="form.invalid ">Login</button>
      </div>

      <div *ngIf="error" class="alert alert-danger " role="alert ">
        {{error}}
      </div>
    </form>
  </div>
</div>
در اینجا خاصیت‌های نام کاربری، کلمه‌ی عبور و همچنین rememberMe مدل، از کاربر دریافت می‌شوند؛ به همراه بررسی اعتبارسنجی سمت کلاینت آن‌ها؛ با این شکل:


برای آزمایش برنامه، نام کاربری Vahid و کلمه‌ی عبور 1234 را وارد کنید.


تکمیل کامپوننت Header برنامه

در ادامه، پس از لاگین موفق شخص، می‌خواهیم صفحه‌ی protectedPage را نمایش دهیم:


در این صفحه، Login از منوی سایت حذف شده‌است و بجای آن Logout به همراه «نام نمایشی کاربر» ظاهر شده‌اند. همچنین توکن decode شده به همراه تاریخ انقضای آن نمایش داده شده‌اند.
برای پیاده سازی این موارد، ابتدا از کامپوننت Header شروع می‌کنیم:
export class HeaderComponent implements OnInit, OnDestroy {

  title = "Angular.Jwt.Core";

  isLoggedIn: boolean;
  subscription: Subscription;
  displayName: string;

  constructor(private authService: AuthService) { }
این کامپوننت وضعیت گزارش شده‌ی ورود شخص را توسط خاصیت isLoggedIn در اختیار قالب خود قرار می‌دهد. برای این منظور به سرویس AuthService تزریق شده‌ی در سازنده‌ی خود نیاز دارد.
اکنون در روال رخ‌دادگردان ngOnInit، مشترک authStatus می‌شود که یک BehaviorSubject است و از آن جهت صدور رخ‌دادهای authService به تمام کامپوننت‌های مشترک به آن استفاده کرده‌ایم:
  ngOnInit() {
    this.subscription = this.authService.authStatus$.subscribe(status => {
      this.isLoggedIn = status;
      if (status) {
        this.displayName = this.authService.getDisplayName();
      }
    });
  }
Status بازگشت داده شده‌ی توسط آن از نوع boolean است و در صورت true بودن، خاصیت isLoggedIn را نیز true می‌کند که از آن در قالب این کامپوننت برای نمایش و یا مخفی کردن لینک‌های login و logout استفاده خواهیم کرد.
همچنین اگر این وضعیت true باشد، مقدار DisplayName کاربر را نیز از سرویس authService دریافت کرده و توسط خاصیت this.displayName در اختیار قالب Header قرار می‌دهیم.
در آخر برای جلوگیری از نشتی حافظه، ضروری است اشتراک به authStatus، در روال رخ‌دادگردان ngOnDestroy لغو شود:
  ngOnDestroy() {
    // prevent memory leak when component is destroyed
    this.subscription.unsubscribe();
  }

همچنین در قالب Header، مدیریت دکمه‌ی Logout را نیز انجام خواهیم داد:
  logout() {
    this.authService.logout(true);
  }

با این مقدمات، قالب Header اکنون به صورت ذیل تغییر می‌کند:
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <div class="navbar-header">
      <a class="navbar-brand" [routerLink]="['/']">{{title}}</a>
    </div>
    <ul class="nav navbar-nav">
      <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">
        <a class="nav-link" [routerLink]="['/welcome']">Home</a>
      </li>
      <li *ngIf="!isLoggedIn" class="nav-item" routerLinkActive="active">
        <a class="nav-link" queryParamsHandling="merge" [routerLink]="['/login']">Login</a>
      </li>
      <li *ngIf="isLoggedIn" class="nav-item" routerLinkActive="active">
        <a class="nav-link" (click)="logout()">Logoff [{{displayName}}]</a>
      </li>
      <li *ngIf="isLoggedIn" class="nav-item" routerLinkActive="active">
        <a class="nav-link" [routerLink]="['/protectedPage']">Protected Page</a>
      </li>
    </ul>
  </div>
</nav>
در اینجا توسط ngIfها وضعیت خاصیت isLoggedIn این کامپوننت را بررسی می‌کنیم. اگر true باشد، Logoff به همراه نام نمایشی کاربر را در منوی راهبری سایت ظاهر خواهیم کرد و در غیراینصورت لینک به صفحه‌ی Login را نمایش می‌دهیم.


تکمیل کامپوننت صفحه‌ی محافظت شده

در تصویر قبل، نمایش توکن decode شده را نیز مشاهده کردید. این نمایش توسط کامپوننت صفحه‌ی محافظت شده، مدیریت می‌شود:
import { Component, OnInit } from "@angular/core";
import { AuthService } from "../../core/services/auth.service";

@Component({
  selector: "app-protected-page",
  templateUrl: "./protected-page.component.html",
  styleUrls: ["./protected-page.component.css"]
})
export class ProtectedPageComponent implements OnInit {

  decodedAccessToken: any = {};
  accessTokenExpirationDate: Date = null;

  constructor(private authService: AuthService) { }

  ngOnInit() {
    this.decodedAccessToken = this.authService.getDecodedAccessToken();
    this.accessTokenExpirationDate = this.authService.getAccessTokenExpirationDate();
  }
}
در اینجا به کمک سرویس تزریقی AuthService، یکبار با استفاده از متد getDecodedAccessToken آن، مقدار اصلی توکن را و بار دیگر توسط متد getAccessTokenExpirationDate آن، تاریخ انقضای توکن دریافتی از سمت سرور را نمایش می‌دهیم؛ با این قالب:
<h1>
  Decoded Access Token
</h1>

<div class="alert alert-info">
  <label> Access Token Expiration Date:</label> {{accessTokenExpirationDate}}
</div>

<div>
  <pre>{{decodedAccessToken | json}}</pre>
</div>


کدهای کامل این سری را از اینجا می‌توانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه‌ی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
نظرات مطالب
شروع کار با Angular Material ۲
از نسخه 2.0.beta-11، ماژول  MaterialModule حذف شده است. این ماژول تمامی کامپوننتهای پیاده سازی شده را جهت استفاده وارد می‌کرد. از این نسخه به بعد بایستی کامپوننتهای مورد استفاده یکی یکی وارد شوند. به عنوان مثال اگر در برنامه خود فقط از کامپوننت دکمه و چک باکس استفاده می‌کنیم، به شکل زیر عمل میکنیم. 
import {
  MatButtonModule,
  MatCheckboxModule
} from "@angular/material";
به جهت جلوگیری از شلوغی ماژول اصلی برنامه بهتر است ماژول دیگری به شکل زیر تعریف کنیم که کامپوننتهای مورد استفاده در برنامه را مدیریت میکند و ماژول اصلی برنامه تنها از این ماژول استفاده خواهد کرد. (این روش در راهنمای استفاده از Angular Material Design به عنوان یک روش مطرح شده است.)
import {MatButtonModule, MatCheckboxModule} from '@angular/material';

@NgModule({
  imports: [MatButtonModule, MatCheckboxModule],
  exports: [MatButtonModule, MatCheckboxModule],
})
export class MyOwnCustomMaterialModule { }
در این صورت کافی است در AppModule فقط MyOwnCustomMaterialModule را در قسمت imports ذکر کنیم.
مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت پنجم - محافظت از مسیرها
در قسمت سوم، کار ورود به سیستم و امکان مشاهده‌ی صفحه‌ی محافظت شده‌ی پس از لاگین را پیاده سازی کردیم. در این قسمت می‌خواهیم امکان دسترسی به مسیر http://localhost:4200/protectedPage را کنترل کنیم. تا اینجا اگر کاربر بدون لاگین کردن نیز این مسیر را درخواست کند، می‌تواند حداقل ساختار ابتدایی آن‌را مشاهده کند که باید مدیریت شود و این مدیریت دسترسی می‌تواند بر اساس وضعیت لاگین کاربر و همچنین نقش‌های او در سیستم باشد:


طراحی بخش‌هایی از این قسمت، از پروژه‌ی «کنترل دسترسی‌ها در Angular با استفاده از Ng2Permission» ایده گرفته شده‌اند.


استخراج اطلاعات کاربر وارد شده‌ی به سیستم از توکن دسترسی او

یکی از روش‌های دسترسی به اطلاعات کاربر در سمت کلاینت، مانند نقش‌های او، تدارک متدی در سمت سرور و بازگشت Claims او به سمت کلاینت است:
public IActionResult Get()
{ 
    var user = this.User.Identity as ClaimsIdentity; 
    var config = new 
    { 
        userName = user.Name, 
        roles = user.Claims.Where(x => x.Type == ClaimTypes.Role).Select(x => x.Value).ToList() 
    }; 
    return Ok(config); 
}
 اما با توجه به اینکه در زمان لاگین، نقش‌های او (و سایر Claims دلخواه) نیز به توکن دسترسی نهایی اضافه می‌شوند، می‌توان این کوئری گرفتن مدام از سرور را تبدیل به روش بسیار سریعتر استخراج آن‌ها از توکنی که هم اکنون در کش مرورگر ذخیره شده‌است، کرد.
همچنین باید دقت داشت چون این توکن دارای امضای دیجیتال است، کوچکترین تغییری در آن در سمت کاربر، سبب برگشت خوردن خودکار درخواست ارسالی به سمت سرور می‌شود (یکی از مراحل اعتبارسنجی کاربر در سمت سرور، اعتبارسنجی توکن دریافتی (قسمت cfg.TokenValidationParameters) و همچنین بررسی خودکار امضای دیجیتال آن است). بنابراین نگرانی از این بابت وجود ندارد.
اگر اطلاعات کاربر در سمت سرور تغییر کنند، با اولین درخواست ارسالی به سمت سرور، رخ‌داد OnTokenValidated وارد عمل شده و درخواست ارسالی را برگشت می‌زند (در مورد پیاده سازی سمت سرور این مورد، در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» بیشتر بحث شده‌است). در این حالت کاربر مجبور به لاگین مجدد خواهد شد که این مورد سبب به روز رسانی خودکار اطلاعات توکن‌های ذخیره شده‌ی او در مرورگر نیز می‌شود.

اگر از قسمت دوم این سری به‌خاطر داشته باشید، توکن decode شده‌ی برنامه، چنین شکلی را دارد:
{
  "jti": "d1272eb5-1061-45bd-9209-3ccbc6ddcf0a",
  "iss": "http://localhost/",
  "iat": 1513070340,
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid",
  "DisplayName": "وحید",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber": "709b64868a1d4d108ee58369f5c3c1f3",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [
    "Admin",
    "User"
  ],
  "nbf": 1513070340,
  "exp": 1513070460,
  "aud": "Any"
}
به همین جهت متدی را برای تبدیل این اطلاعات به شیء کاربر، ایجاد خواهیم کرد و در سراسر برنامه از این اطلاعات آماده، برای تعیین دسترسی او به قسمت‌های مختلف برنامه‌ی سمت کلاینت، استفاده می‌کنیم.
برای این منظور اینترفیس src\app\core\models\auth-user.ts را به صورت ذیل ایجاد می‌کنیم:
export interface AuthUser {
  userId: string;
  userName: string;
  displayName: string;
  roles: string[];
}
پس از آن متد getAuthUser را جهت استخراج خواص ویژه‌ی توکن دسترسی decode شده‌ی فوق، به صورت ذیل به سرویس Auth اضافه می‌کنیم:
  getAuthUser(): AuthUser {
    if (!this.isLoggedIn()) {
      return null;
    }

    const decodedToken = this.getDecodedAccessToken();
    let roles = decodedToken["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"];
    if (roles) {
      roles = roles.map(role => role.toLowerCase());
    }
    return Object.freeze({
      userId: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"],
      userName: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
      displayName: decodedToken["DisplayName"],
      roles: roles
    });
  }
کار با این متد بسیار سریع است و نیازی به رفت و برگشتی به سمت سرور ندارد؛ چون تمام اطلاعات استخراجی توسط آن هم اکنون در کش سریع مرورگر کلاینت موجود هستند. استفاده‌ی از متد Object.freeze هم سبب read-only شدن این خروجی می‌شود.
همچنین در اینجا تمام نقش‌های دریافتی، تبدیل به LowerCase شده‌اند. با اینکار مقایسه‌ی بعدی آن‌ها با نقش‌های درخواستی، حساس به بزرگی و کوچکی حروف نخواهد بود.


تعریف نقش‌های دسترسی به مسیرهای مختلف سمت کلاینت

مرسوم است اطلاعات اضافی مرتبط با هر مسیر را به خاصیت data آن route انتساب می‌دهند. به همین جهت به فایل dashboard-routing.module.ts مراجعه کرده و نقش‌های مجاز به دسترسی به مسیر protectedPage را به خاصیت data آن به صورت ذیل اضافه می‌کنیم:
import { ProtectedPageComponent } from "./protected-page/protected-page.component";
import { AuthGuardPermission } from "../core/models/auth-guard-permission";

const routes: Routes = [
  {
    path: "protectedPage",
    component: ProtectedPageComponent,
    data: {
      permission: {
        permittedRoles: ["Admin"],
        deniedRoles: null
      } as AuthGuardPermission
    }
  }
];
که در اینجا ساختار اینترفیس AuthGuardPermission، در فایل جدید app\core\models\auth-guard-permission.ts به صورت ذیل تعریف شده‌است:
export interface AuthGuardPermission {
  permittedRoles?: string[];
  deniedRoles?: string[];
}
به این ترتیب هر قسمتی از برنامه که نیاز به اطلاعات سطوح دسترسی مسیری را داشت، ابتدا به دنبال route.data["permission"] خواهد گشت (کلیدی به نام permission در خاصیت data یک مسیر) و سپس اطلاعات آن‌را بر اساس ساختار اینترفیس AuthGuardPermission، تحلیل می‌کند.
در اینجا تنها باید یکی از خواص permittedRoles  (نقش‌های مجاز به دسترسی/صدور دسترسی فقط برای این نقش‌های مشخص، منهای مابقی) و یا deniedRoles (نقش‌های غیرمجاز به دسترسی/دسترسی همه‌ی نقش‌های ممکن، منهای این نقش‌های تعیین شده)، مقدار دهی شوند.


افزودن کامپوننت «دسترسی ندارید» به ماژول Authentication

در ادامه می‌خواهیم اگر کاربری به مسیری دسترسی نداشت، به صورت خودکار به صفحه‌ی «دسترسی ندارید» هدایت شود. به همین جهت این کامپوننت را به صورت ذیل به ماژول authentication اضافه می‌کنیم:
>ng g c Authentication/AccessDenied
با این خروجی که سبب درج خودکار آن در قسمت declaration فایل authentication.module نیز می‌شود:
 AccessDenied
  create src/app/Authentication/access-denied/access-denied.component.html (32 bytes)
  create src/app/Authentication/access-denied/access-denied.component.ts (296 bytes)
  create src/app/Authentication/access-denied/access-denied.component.css (0 bytes)
  update src/app/Authentication/authentication.module.ts (550 bytes)
سپس به فایل authentication-routing.module.ts مراجعه کرده و مسیریابی آن‌را نیز اضافه می‌کنیم:
import { LoginComponent } from "./login/login.component";
import { AccessDeniedComponent } from "./access-denied/access-denied.component";

const routes: Routes = [
  { path: "login", component: LoginComponent },
  { path: "accessDenied", component: AccessDeniedComponent }
];
قالب access-denied.component.html را نیز به صورت ذیل تکمیل می‌کنیم:
<h1 class="text-danger">
  <span class="glyphicon glyphicon-ban-circle"></span> Access Denied
</h1>
<p>Sorry! You don't have access to this page.</p>
<button class="btn btn-default" (click)="goBack()">
  <span class="glyphicon glyphicon-arrow-left"></span> Back
</button>

<button *ngIf="!isAuthenticated" class="btn btn-success" [routerLink]="['/login']"
  queryParamsHandling="merge">
  Login
</button>
که دکمه‌ی Back آن به کمک سرویس Location، صورت ذیل پیاده سازی شده‌است:
import { Component, OnInit } from "@angular/core";
import { Location } from "@angular/common";

import { AuthService } from "../../core/services/auth.service";

@Component({
  selector: "app-access-denied",
  templateUrl: "./access-denied.component.html",
  styleUrls: ["./access-denied.component.css"]
})
export class AccessDeniedComponent implements OnInit {

  isAuthenticated = false;

  constructor(
    private location: Location,
    private authService: AuthService
  ) {
  }

  ngOnInit() {
    this.isAuthenticated = this.authService.isLoggedIn();
  }

  goBack() {
    this.location.back(); // <-- go back to previous location on cancel
  }
}


در اینجا اگر کاربر به سیستم وارد نشده باشد، دکمه‌ی لاگین نیز به او نمایش داده می‌شود. همچنین وجود "queryParamsHandling="merge در لینک مراجعه‌ی به صفحه‌ی لاگین، سبب خواهد شد تا query string موجود در صفحه نیز حفظ شود و به صفحه‌ی لاگین انتقال پیدا کند. در صفحه‌ی لاگین نیز جهت پردازش این نوع کوئری استرینگ‌ها، تمهیدات لازم درنظر گرفته شده‌اند.
دکمه‌ی back آن نیز توسط سرویس توکار Location واقع در مسیر angular/common@ پیاده سازی شده‌است.


ایجاد یک محافظ مسیر سمت کلاینت برای بررسی وضعیت کاربر جاری و همچنین نقش‌های او

پس از تعریف متد getAuthUser و استخراج اطلاعات کاربر از توکن دسترسی دریافتی که شامل نقش‌های او نیز می‌شود، اکنون می‌توان متد بررسی این نقش‌ها را نیز به سرویس Auth اضافه کرد:
  isAuthUserInRoles(requiredRoles: string[]): boolean {
    const user = this.getAuthUser();
    if (!user || !user.roles) {
      return false;
    }
    return requiredRoles.some(requiredRole => user.roles.indexOf(requiredRole.toLowerCase()) >= 0);
  }

  isAuthUserInRole(requiredRole: string): boolean {
    return this.isAuthUserInRoles([requiredRole]);
  }
متد some در جاوا اسکریپت شبیه به متد Any در C# LINQ عمل می‌کند. همچنین در مقایسه‌ی صورت گرفته، با توجه به اینکه user.roles را پیشتر به LowerCase تبدیل کرد‌یم، حساسیتی بین نقش Admin و admin و کلا کوچکی و بزرگی نام نقش‌ها وجود ندارد.
اکنون در هر قسمتی از برنامه که نیاز به بررسی امکان دسترسی یک کاربر به نقش یا نقش‌هایی خاص وجود داشته باشد، می‌توان AuthService را به سازنده‌ی ‌آن تزریق و سپس از متد فوق جهت بررسی نهایی، استفاده کرد.

در ادامه یک Route Guard جدید را در مسیر app\core\services\auth.guard.ts ایجاد می‌کنیم. کار آن بررسی خودکار امکان دسترسی به یک مسیر درخواستی است:
import { Injectable } from "@angular/core";
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";

import { AuthService } from "./auth.service";
import { AuthGuardPermission } from "../models/auth-guard-permission";

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) { }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {

    if (!this.authService.isLoggedIn()) {
      this.showAccessDenied(state);
      return false;
    }

    const permissionData = route.data["permission"] as AuthGuardPermission;
    if (!permissionData) {
      return true;
    }

    if (Array.isArray(permissionData.deniedRoles) && Array.isArray(permissionData.permittedRoles)) {
      throw new Error("Don't set both 'deniedRoles' and 'permittedRoles' in route data.");
    }

    if (Array.isArray(permissionData.permittedRoles)) {
      const isInRole = this.authService.isAuthUserInRoles(permissionData.permittedRoles);
      if (isInRole) {
        return true;
      }

      this.showAccessDenied(state);
      return false;
    }

    if (Array.isArray(permissionData.deniedRoles)) {
      const isInRole = this.authService.isAuthUserInRoles(permissionData.deniedRoles);
      if (!isInRole) {
        return true;
      }

      this.showAccessDenied(state);
      return false;
    }
  }

  private showAccessDenied(state: RouterStateSnapshot) {
    this.router.navigate(["/accessDenied"], { queryParams: { returnUrl: state.url } });
  }
}
در اینجا در ابتدا وضعیت لاگین کاربر بررسی می‌گردد. این وضعیت نیز از طریق سرویس Auth که به سازنده‌ی کلاس تزریق شده‌است، تامین می‌شود. اگر کاربر هنوز لاگین نکرده باشد، به صفحه‌ی عدم دسترسی هدایت خواهد شد.
سپس خاصیت permission اطلاعات مسیر استخراج می‌شود. اگر چنین مقداری وجود نداشت، همینجا کار با موفقیت خاتمه پیدا می‌کند.
در آخر وضعیت دسترسی به نقش‌های استخراجی deniedRoles و permittedRoles که از اطلاعات مسیر دریافت شدند، توسط متد isAuthUserInRoles سرویس Auth بررسی می‌شوند.

در متد showAccessDenied کار ارسال آدرس درخواستی (state.url) به صورت یک کوئری استرینگ (queryParams) با کلید returnUrl به صفحه‌ی accessDenied صورت می‌گیرد. در این صفحه نیز دکمه‌ی لاگین به همراه "queryParamsHandling="merge است. یعنی کامپوننت لاگین برنامه، کوئری استرینگ returnUrl را دریافت می‌کند:
 this.returnUrl = this.route.snapshot.queryParams["returnUrl"];
 و پس از لاگین موفق، در صورت وجود این کوئری استرینگ، هدایت خودکار کاربر، به مسیر returnUrl پیشین صورت خواهد گرفت:
if (this.returnUrl) {
   this.router.navigate([this.returnUrl]);
} else {
   this.router.navigate(["/protectedPage"]);
}

محل معرفی این AuthGuard جدید که در حقیقت یک سرویس است، در ماژول Core، در قسمت providers آن، به صورت ذیل می‌باشد:
import { AuthGuard } from "./services/auth.guard";

@NgModule({
  providers: [
    AuthGuard
  ]
})
export class CoreModule {}
در آخر برای اعمال این Guard جدید، به فایل dashboard-routing.module.ts مراجعه کرده و خاصیت canActivate را مقدار دهی می‌کنیم:
import { ProtectedPageComponent } from "./protected-page/protected-page.component";
import { AuthGuardPermission } from "../core/models/auth-guard-permission";
import { AuthGuard } from "../core/services/auth.guard";

const routes: Routes = [
  {
    path: "protectedPage",
    component: ProtectedPageComponent,
    data: {
      permission: {
        permittedRoles: ["Admin"],
        deniedRoles: null
      } as AuthGuardPermission
    },
    canActivate: [AuthGuard]
  }
];
به این ترتیب با درخواست این مسیر، پیش از فعالسازی و نمایش آن، توسط AuthGuard معرفی شده‌ی به آن، کار بررسی وضعیت کاربر و نقش‌های او که از خاصیت permission خاصیت data دریافت می‌شوند، صورت گرفته و اگر عملیات تعیین اعتبار اطلاعات با موفقیت به پایان رسید، آنگاه کاربر مجوز دسترسی به این قسمت از برنامه را خواهد یافت.

اگر قصد آزمایش آن‌را داشتید، فقط کافی است بجای نقش Admin، مثلا Admin1 را در permittedRoles مقدار دهی کنید، تا صفحه‌ی access denied را در صورت درخواست مسیر protectedPage، بتوان مشاهده کرد.




کدهای کامل این سری را از اینجا می‌توانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه‌ی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
مطالب
شروع کار با webpack - قسمت چهارم
در سه قسمت قبلی با مفاهیم گوناگونی از وبپک آشنا شدیم که می‌توان به ساخت باندل‌های ساده و پیشرفته، استفاده از لودرها، فایل‌های پیکربندی  و ... اشاره کرد.

بارگذاری فایل‌های css به کمک وبپک

همان طور که قبلا اشاره شد، هسته‌ی وبپک به خودی خود بجز باندل کردن اسکریپت‌های خام جاوا اسکریپت و در نهایت Minify کردن آنها، قادر به انجام کار دیگری نیست. همین طور ذکر شد، برای اینکه وبپک فوت و فن جدیدی را یاد بگیرد، از Loader‌ها استفاده می‌کنیم. وارد کردن فایل‌های css به باندل نیز به کمک Loader‌ها خواهد بود. برای اینکار به دو Loader احتیاج داریم و به کمک npm آنها را به عنوان وابستگی برای توسعه‌ی پروژه نصب خواهیم کرد .
// نصب لودر‌های مورد نظر
npm install css-loader style-loader -D
لودر css وظیفه‌ی بارگذاری فایل‌های css و همچنین وابستگی آنها ( import هایی که در این فایل‌ها انجام شده) را دارد و در نهایت css نهایی را بر می‌گرداند.
لودر style نیز وظیفه‌ی اضافه کردن ماژول اکسپورت شده را به DOM، به عنوان یک style خواهد داشت.
در ساختار پروژه نیز تغییراتی انجام شده و فایل‌ها و لودر‌های اضافی حذف شده‌اند تا تمرکز بر روی مطلب بیشتر باشد. مانند قسمت اول، یک تک صفحه‌ی index.html را خواهیم داشت که دارای محتوای زیر است:
//index.html file
<html>
  <head>
    <title>webpack part 4</title>

  </head>
  <body>
    <h1>webpack is awesome</h1>
    <p>part 4 of tutorial</p>
    <div>i have a background</div>
    <h1>تست فونت !</h1>

    <script src="/assets/js/bundle.js">
    </script>
  </body>
</html>
اگر دقت کنید، در این فایل لینکی به فایل‌های استایل، مانند css وجود ندارد و هدف این است که این فایل‌ها توسط وبپک پردازش شوند.
برای شروع به فایل پیکربندی وبپک مراجعه کرده و دو Loader ی را که برای این کار، بالاتر در پروژه نصب کردیم، به وبپک معرفی می‌کنیم.
//webpack.config.js
var path = require("path");
var webpack = require("webpack");

module.exports = {
    context: path.resolve("js"),
    entry: ['./main.js']
    , output: {
        path: path.resolve("build/js"),
        publicPath: "assets/js",
        filename: 'bundle.js'
    },
    devServer: {
        contentBase: "assets"
    }
    , watch: true
    , module: {
        loaders: [
            {
                test: /\.css$/
                , exclude: /node_modules/
                , loader: 'style-loader!css-loader'
            }
        ]
    }
}
در قطعه کد بالا محتوای فایل پیکربندی قابل مشاهده است. مشخص است که لودر جدیدی را به وبپک معرفی کرده‌ایم که به دنبال فایل‌هایی با پسوند css در پروژه می‌گردد. ولی در کلید loader که وظیفه‌ی فراخوانی لودر مورد نظر را در مواجهه با این فایل‌ها دارد، کمی با قبل تفاوت وجود دارد.
loader:'style-loader!css-loader'
در قطعه کد بالا، نشانگر ! در مواقعی استفاده می‌شود که قصد داریم لودر‌ها را به صورت زنجیره‌ای استفاده کنیم. مثلن در اینجا به وبپک می‌گوییم که برای فایل‌های css ابتدا لودر css را فراخوانی کن و سپس با خروجی که از این لودر می‌گیری، لودر style را صدا بزن و منتظر جواب آن باش. ترتیب فراخوانی نیز از آخر به اول می‌باشد (اول لودر css سپس لودر style).
در مطالب قبلی ذکر شد که دو حالت برای معرفی فایل‌ها به وبپک وجود دارد؛ یکی معرفی آنها در کلید entry فایل پیکربندی و دیگری استفاده از تابع require در اسکریپت‌ها، برای بارگذاری پویای ماژول‌های دیگر. استفاده از استایل‌های css نیز به همین صورت است. برای بارگذاری فایل‌های استایل، از روش پویا استفاده کرده و در فایل main.js استایل مورد نظر را با کمک require وارد می‌کنیم.
محتوای فایل main.js بدین صورت است:
// main.js file
require("./../assets/main.css");
console.log(`i'm bundled by webpack`);
محتوای فایل main.css نیز بدین شکل می‌باشد:
// main.css
body{
    background-color: #DAA520;
}
با راه اندازی وبپک و باز کردن صفحه‌ی index می‌توان دید که فایل استایل ما به همراه باندل وارد شده است:

در تصویر بالا مشخص است که در تگ Head صفحه، یک تگ جدید style، توسط وبپک ایجاد شده و استایل ما به صفحه تزریق شده‌است. همچنین اگر وبپک را به حالت Minify کردن باندل ببریم (در مطلب قبلی نحوه‌ی این کار ذکر شد)، باندل نهایی برای فایل‌های css نیز Minify خواهد شد.


استفاده از Sass با کمک وبپک 


روش استفاده از Sass نیز تفاوتی با css نخواهد داشت و فقط کافی است Loader آن را در پروژه نصب کنیم و در نهایت آن را در فایل پیکربندی، به وبپک معرفی کنیم. با دستور زیر لودر Sass را در پروژه وارد می‌کنیم:

// نصب لودر sass
npm install -D sass-loader node-sass

( node-sass به عنوان وابستگی لودر sass، در کنار آن نصب شده است)

حال به فایل پیکربندی می‌رویم و لودر جدید را به قسمت لودر‌ها اضافه می‌کنیم:

// webpack.config.js 
module: {
        loaders: [
            {
                test: /\.css$/
                , exclude: /node_modules/
                , loader: 'style-loader!css-loader'
            }
            ,{
                test:/\.scss$/
                ,exclude:/node_modules/
                ,loader:'style-loader!css-loader!sass-loader'
            }
        ]
    }

در پوشه‌ی assets نیز فایل جدیدی را با عنوان main.scss ساخته و محتوای زیر را در آن وارد می‌کنیم:

// main.scss
$background-color:#DAA520;

body{
    background-color: $background-color;
}

سپس در فایل main.js به جای وارد کردن فایل css قبلی، فایل scss جدید را با کمک require وارد می‌کنیم و در ادامه وبپک را اجرا می‌کنیم. خواهیم دید که مانند قبل بدون مشکلی وبپک اجرا شده، فایل scss را به css ترجمه کرده و سپس به کمک بقیه لودر‌ها، به باندل اضافه می‌کند. استفاده از بقیه‌ی فریمورک‌های css مانند Less و ... نیز با کمک لودر آنها به همین صورت قابل انجام است.


استفاده از Autoprefixer 

همان طور که تمامی قابلیت‌های نسخه‌ی جدید جاوااسکریپت در همه‌ی مرورگرها به صورت سراسری پشتیبانی نمی‌شود، برای css نیز چنین مشکل مشابهی وجود دارد و برای استفاده‌ی بهینه‌ی از برخی قابلیت‌ها نیاز داریم تا prefix‌های مورد نیاز مرورگرهای مختلف را به فایل‌های css مان اضافه کنیم. می‌توانیم این روند را با کمک یک لودر وبپک، ساده و به صورت خودکار کرد. برای نصب این لودر دستور زیر را وارد می‌کنیم:

npm install -D autoprefixer-loader

و بعد از نصب شدن آن، در فایل پیکربندی وبپک به لودرهایی که برای فایل‌های css و scss اضافه کرده بودیم، این لودر را نیز به صورت زنجیر وار اضافه می‌کنیم:

//webpack.config.js
module: {
        loaders: [
            {
                test: /\.css$/
                , exclude: /node_modules/
                , loader: 'style-loader!css-loader!autoprefixer-loader'
            }
            ,{
                test:/\.scss$/
                ,exclude:/node_modules/
                ,loader:'style-loader!css-loader!autoprefixer-loader!sass-loader'
            }
        ]
    }

در هر دو لودری که برای css و scss ساخته بودیم، از لودر autoprefixer استفاده کردیم. برای تست اینکه این لودر بدون مشکل کار می‌کند، در فایل main.scss تغییر زیر را ایجاد می‌کنیم:

//main.scss
$background-color:#DAA520;

body{
    background-color: $background-color;
    display: flex;
}

حال با اجرای وبپک خواهیم دید که prefix‌های مورد نیاز توسط لودر اضافه شده اند ( این لودر از کتابخانه‌ی postcss کمک می‌گیرد).




باندل کردن تصاویر و فونت‌ها با کمک وبپک


تا اینجا با نحوه‌ی وارد کردن فایل‌های استایل، مانند css و ... به باندل آشنا شدیم. در ادامه قصد داریم که تصاویر و فونت‌ها را نیز وارد باندل کنیم. روند کار شبیه به گذشته است و این کار نیز به کمک لودر‌های وبپک انجام خواهد شد.

جهت باندل کردن تصاویر و فونت‌ها، به لودر جدیدی با نام url-loader احتیاج داریم. قبل از هر چیزی این لودر را در پروژه با کمک npm نصب می‌کنیم:

npm install -D url-loader file-loader

(لودر file-loader به عنوان وابستگی مورد نیاز است)

روند همچنان مثل گذشته است و پس از نصب لودر، وارد فایل پیکربندی شده و لودر جدید را به وبپک معرفی می‌کنیم:

//webpack.config.js file
module: {
        loaders: [
            {
                test: /\.css$/
                , exclude: /node_modules/
                , loader: 'style-loader!css-loader!autoprefixer-loader'
            }
            ,{
                test:/\.scss$/
                ,exclude:/node_modules/
                ,loader:'style-loader!css-loader!autoprefixer-loader!sass-loader'
            },{
                test:/\.(png|jpg|ttf|eot)$/
                ,exclude:/node_modules/
                ,loader:'url-loader?limit=100000'
            }
        ]
    }

در لودر اضافه شده، پسوند فایلهایی را که قصد داریم به باندل وارد شوند، معرفی می‌کنیم. در اینجا فرمت‌های png , jpg ,ttf, eot ذکر شده‌اند.

تنها نکته‌ی جدید، در مشخص کردن نام لودر وجود دارد و آن نیز قسمت پس از علامت ؟ می‌باشد. هنگام مشخص کردن اینکه از چه لودری قصد استفاده داریم، می‌توانیم با استفاده از ؟ پارامترهایی را به لودر مورد نظر ارسال کنیم. در اینجا به پارامتر limit، مقدار 100000 را داده‌ایم که برای این لودر به این معناست که اگر حجم فایل در حال پردازش، حجمی بیشتر از این مقدار را داشت، این فایل را به صورت یک لینک جدا از باندل قرار بده. ولی اگر حجمی کمتر از این مقدار داشت، لودر به صورت خودکار فایل را به فرمت Base64 انکود می‌کند و در درون باندل قرار می‌دهد.


برای تست اینکه آیا این لودر به درستی کار می‌کند یا نه، یک تصویر نمونه را در فولدر assets قرار می‌دهیم و سپس در فایل main.scss تغییرات زیر را انجام می‌دهیم.

حجم عکس قرار داده شده نزدیک به 400 کیلوبایت است و با مقدار محدودیت مشخص شده، تصویر مورد نظر از باندل توسط وبپک خارج می‌شود و به صورت جداگانه در بیلد نهایی قرار می‌گیرد. در تصویر زیر مشخص است که مرورگر درخواست جداگانه ای برای تصویر ارسال کرده است:



حال محدودیت حجم فایل را بالا می‌بریم و می‌توان دید که تصویر در باندل نهایی به صورت انکود شده قرار گرفته است .


قطعا انجام این کار برای تصاویری با حجم بالا مناسب نخواهد بود و برنامه نویس بسته به نیاز بایستی مقدار محدودیت حجم را برای لودر مشخص کند.


در تعریف بالا دیدیم که فرمت‌های مورد نیاز برای وارد کردن فونت را نیز علاوه بر تصاویر، برای وبپک مشخص کرده‌ایم. روند وارد کردن فونت‌ها به باندل نیز تفاوتی با تصاویر ندارد و کافی است تعاریف مورد نیاز را در فایل‌های css داشته باشیم.

برای مثال فونت ساحل در پوشه‌ی assets قرار داده شده و در فایل main.scss تغییرات زیر انجام شده‌اند:

// main.scss

$background-color:#DAA520;
 
div{
    background-image: url("galaxy.jpg");
}

@font-face {
  font-family: Sahel;
  src: url('Sahel.eot');
  src: url('Sahel.eot?#iefix') format('embedded-opentype'),
       url('Sahel.woff') format('woff'),
       url('Sahel.ttf') format('truetype');
  font-weight: normal;
}

@font-face {
  font-family: Sahel;
  src: url('Sahel-Bold.eot');
  src: url('Sahel-Bold.eot?#iefix') format('embedded-opentype'),
       url('Sahel-Bold.woff') format('woff'),
       url('Sahel-Bold.ttf') format('truetype');
  font-weight: bold;
}

@font-face {
  font-family: Sahel;
  src: url('Sahel-Black.eot');
  src: url('Sahel-Black.eot?#iefix') format('embedded-opentype'),
       url('Sahel-Black.woff') format('woff');
    
  font-weight: 900;
} 

body{
    background-color: $background-color;
    font-family: 'Sahel';
    display: flex;
}

تصویر زیر، نتیجه‌ی اجرای وبپک برای تولید باندل است. در تصویر می‌توان دید که هم فونت‌ها و هم فایل‌های تصاویر، توسط وبپک شناسایی شده و وارد باندل شده‌اند:



روش دیگری برای وارد کردن تصاویر نیز موجود است؛ به این صورت که به فرض مثال یک تگ img در اسکریپت ساخته و سپس پروپرتی src آن را با کمک require برابر با آدرس تصویر مورد نظر قرار می‌دهیم. این روش نیز برای وبپک قابل فهم بوده و فایل وارد باندل می‌شود. در ادامه مثالی از این روش آورده شده است:

var img = document.createElement("img");
img.width="200px";
img.height="200px";
img.src= require("path to some image");


چند نکته‌ی پایانی :

1. در فایل پیکربندی همیشه پسوند فایل‌هایی را که در کلید entry قرار داشتند، مشخص کردیم:

entry:['./main.js','./shared.ts']

با کلیدی با نام resolve در فایل پیکربندی می‌توان مشخص کرد در صورتیکه پسوند فایلی مشخص نبود، به ترتیب مشخص شده به دنبال آن بگردد. به طور مثال:

// webpack.config.js
resolve:{
            extensions:['','.js','.ts']
  }

در تعریف بالا ذکر می‌شود در صورتیکه پسوند فایل ورودی مشخص نبود، ابتدا به دنبال فایل بدون پسوند، سپس فایل‌هایی با پسوند js و در نهایت به دنبال فایل‌هایی با پسوند ts بگرد. توجه داشته باشید که ترتیب مشخص کردن پسوند فایل‌ها مهم است و وبپک بر اساس این ترتیب به دنبال فایل مورد نظر خواهد گشت.

حال می‌توان مقدار کلید entry را اینطور تعریف کرد:

entry:['./main','./shared']


2.استفاده از فایل‌های css ی که در درونشان فونت‌های مورد نیاز لینک شده‌اند تنها با استفاده از لودر css قابل انجام نیست. به طور مثال استفاده از کتابخانه‌ی بوت استرپ تنها با این لودر ممکن نیست و بایستی لودر url-loader نیز در پروژه نصب شده باشد تا در هنگامیکه وبپک به فونت‌ها برخورد کرد، بتواند آنها را وابسته به شرایط، وارد باندل نهایی کند.


فایل‌های پروژه: dntwebpack-part4.zip