اضافه کردن Indentation به ابتدای محتوای یک سلول
«ایجاد قالبهای سفارشی ستونها و فرمت شرطی اطلاعات در PdfReport»
یک مثال دیگر: نمایش مبلغ به صورت سفارشی
تهیه مقدمات سمت سرور
مدلی که در تصویر فوق نمایش داده شدهاست، در سمت سرور چنین ساختاری را دارد:
namespace AngularTemplateDrivenFormsLab.Models { public class Product { public int ProductId { set; get; } public string ProductName { set; get; } public decimal Price { set; get; } public bool IsAvailable { set; get; } } }
همچنین یک منبع ساده درون حافظهای را نیز جهت بازگشت 1500 محصول تهیه کردهایم. علت اینجا است که ساختار نهایی اطلاعات آن شبیه به ساختار اطلاعات حاصل از ORMها باشد و همچنین به سادگی قابلیت اجرا و بررسی را داشته باشد:
using System.Collections.Generic; namespace AngularTemplateDrivenFormsLab.Models { public static class ProductDataSource { private static readonly IList<Product> _cachedItems; static ProductDataSource() { _cachedItems = createProductsDataSource(); } public static IList<Product> LatestProducts { get { return _cachedItems; } } private static IList<Product> createProductsDataSource() { var list = new List<Product>(); for (var i = 0; i < 1500; i++) { list.Add(new Product { ProductId = i + 1, ProductName = "نام " + (i + 1), IsAvailable = (i % 2 == 0), Price = 1000 + i }); } return list; } } }
مشخص کردن قرارداد اطلاعات دریافتی از سمت کلاینت
زمانیکه کلاینت Angular برنامه، اطلاعاتی را به سمت سرور ارسال میکند، یک چنین ساختاری را دریافت خواهیم کرد:
http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7
بنابراین اینترفیسی را دقیقا بر اساس نام کلیدهای همین کوئری استرینگها تهیه میکنیم:
public interface IPagedQueryModel { string SortBy { get; set; } bool IsAscending { get; set; } int Page { get; set; } int PageSize { get; set; } }
کاهش کدهای تکراری صفحه بندی اطلاعات در سمت سرور
با تعریف این اینترفیس چند هدف را دنبال خواهیم کرد:
الف) استاندارد سازی نام خواصی که مدنظر هستند و اعمال یک دست آنها به ViewModelهایی که قرار است از سمت کلاینت دریافت شوند:
public class ProductQueryViewModel : IPagedQueryModel { // ... other properties ... public string SortBy { get; set; } public bool IsAscending { get; set; } public int Page { get; set; } public int PageSize { get; set; } }
ب) امکان استفادهی از این قرارداد در متدهای کمکی که نوشته خواهند شد:
public static class IQueryableExtensions { public static IQueryable<T> ApplyPaging<T>( this IQueryable<T> query, IPagedQueryModel model) { if (model.Page <= 0) { model.Page = 1; } if (model.PageSize <= 0) { model.PageSize = 10; } return query.Skip((model.Page - 1) * model.PageSize).Take(model.PageSize); } }
همچنین دراینجا بجای صدور استثناء در حین دریافت مقادیر غیرمعتبر شماره صفحه یا تعداد ردیفهای هر صفحه، از حالت «بخشنده» بجای حالت «تدافعی» استفاده شدهاست. برای مثال در حالت «بخشنده» اگر شماره صفحه منفی بود، همان صفحهی اول اطلاعات نمایش داده میشود؛ بجای صدور یک استثناء (یا حالت «تدافعی و defensive programming»).
کاهش کدهای تکراری مرتب سازی اطلاعات در سمت سرور
همانطور که عنوان شد، از سمت کلاینت، چنین لینکی را دریافت خواهیم کرد:
http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7
if(model.SortBy == "f1") { query = !model.IsAscending ? query.OrderByDescending(x => x.F1) : query.OrderBy(x => x.F1); }
اما در این حالت نیاز است به ازای تک تک فیلدها، یکبار if/else یافتن فیلد و سپس بررسی صعودی و نزولی بودن آنها صورت گیرد که در نهایت ظاهر خوشایندی را نخواهند داشت.
یک نمونه از مزیتهای تهیهی قرارداد IPagedQueryModel را در حین نوشتن متد ApplyPaging مشاهده کردید. نمونهی دیگر آن کاهش کدهای تکراری مرتب سازی اطلاعات است:
namespace AngularTemplateDrivenFormsLab.Utils { public static class IQueryableExtensions { public static IQueryable<T> ApplyOrdering<T>( this IQueryable<T> query, IPagedQueryModel model, IDictionary<string, Expression<Func<T, object>>> columnsMap) { if (string.IsNullOrWhiteSpace(model.SortBy) || !columnsMap.ContainsKey(model.SortBy)) { return query; } if (model.IsAscending) { return query.OrderBy(columnsMap[model.SortBy]); } else { return query.OrderByDescending(columnsMap[model.SortBy]); } } } }
var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>() { ["productId"] = p => p.ProductId, ["productName"] = p => p.ProductName, ["isAvailable"] = p => p.IsAvailable, ["price"] = p => p.Price }; query = query.ApplyOrdering(queryModel, columnsMap);
تهیه قرارداد ساختار اطلاعات بازگشتی از سمت سرور به سمت کلاینت
تا اینجا قرارداد اطلاعات دریافتی از سمت کلاینت را مشخص کردیم. همچنین از آن برای ساده سازی عملیات مرتب سازی و صفحه بندی اطلاعات کمک گرفتیم. در ادامه نیاز است مشخص کنیم چگونه میخواهیم این اطلاعات را به سمت کلاینت ارسال کنیم:
using System.Collections.Generic; namespace AngularTemplateDrivenFormsLab.Models { public class PagedQueryResult<T> { public int TotalItems { get; set; } public IEnumerable<T> Items { get; set; } } }
پایان کار بازگشت اطلاعات سمت سرور با تهیه اکشن متد GetPagedProducts
در اینجا اکشن متدی را مشاهده میکنید که اطلاعات نهایی مرتب سازی شده و صفحه بندی شده را بازگشت میدهد:
[Route("api/[controller]")] public class ProductController : Controller { [HttpGet("[action]")] public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel) { var pagedResult = new PagedQueryResult<Product>(); var query = ProductDataSource.LatestProducts .AsQueryable(); //TODO: Apply Filtering ... .where(p => p....) ... var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>() { ["productId"] = p => p.ProductId, ["productName"] = p => p.ProductName, ["isAvailable"] = p => p.IsAvailable, ["price"] = p => p.Price }; query = query.ApplyOrdering(queryModel, columnsMap); pagedResult.TotalItems = query.Count(); query = query.ApplyPaging(queryModel); pagedResult.Items = query.ToList(); return pagedResult; } }
امضای این اکشن متد، شامل دو مورد مهم است:
public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel)
ب) خروجی آن از نوع PagedQueryResult است که در مورد آن توضیح داده شد. بنابراین باید به همراه تعداد کل رکوردهای جدول محصولات و همچنین تنها آیتمهای صفحهی جاری درخواستی باشد.
در ابتدای کار، دسترسی به منبع دادهی درون حافظهای ابتدای برنامه را مشاهده میکنید. برای اینکه کارکرد آنرا شبیه به کوئریهای ORMها کنیم، یک AsQueryable نیز به انتهای آن اضافه شدهاست.
var query = ProductDataSource.LatestProducts .AsQueryable(); //TODO: Apply Filtering ... .where(p => p....) ...
پس از مشخص شدن منبع داده و فیلتر آن در صورت نیاز، اکنون نوبت به مرتب سازی اطلاعات است:
var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>() { ["productId"] = p => p.ProductId, ["productName"] = p => p.ProductName, ["isAvailable"] = p => p.IsAvailable, ["price"] = p => p.Price }; query = query.ApplyOrdering(queryModel, columnsMap);
در آخر مطابق ساختار PagedQueryResult بازگشتی، ابتدا تعداد کل آیتمهای منبع داده محاسبه شدهاست و سپس صفحه بندی به آن اعمال گردیدهاست. این ترتیب نیز مهم است و گرنه TotalItems دقیقا به همان تعداد ردیفهای صفحهی جاری محاسبه میشود:
var pagedResult = new PagedQueryResult<Product>(); pagedResult.TotalItems = query.Count(); query = query.ApplyPaging(queryModel); pagedResult.Items = query.ToList(); return pagedResult;
در قسمت بعد، نحوهی نمایش این اطلاعات را در سمت Angular بررسی خواهیم کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
به مستندات رسمی AngularJS 2.0، فصل جدیدی به نام «Introduction to Webpack» اضافه شدهاست. در اینجا میتوان Webpack را جایگزین Gulp کرد و نکتهی جالب آن، امکان نوشتن یک چنین کامپوننتهایی هستند:
import { Component } from '@angular/core'; import '../../public/css/styles.css'; @Component({ selector: 'my-app', template: require('./app.component.html'), styles: [require('./app.component.css')] }) export class AppComponent { }
آشنایی با الگوی طراحی Decorator
// ساخت کیک معمولی با روکش کاکائویی Cake c = new CommonCake(); c = new Chocolate(c); // ساخت کیک معمولی با روکش میوهای Cake c = new CommonCake(); c = new Fruity(c); // ساخت کیک معمولی مخلوط با روکش کاکائویی و روکش میوهای به صورت همزمان Cake c = new CommonCake(); c = new Chocolate(c); c = new Fruity(c); // ساخت کیک مخصوص مخلوط با روکش کاکائویی و روکش میوهای به صورت همزمان Cake c = new SpecialCake(); c = new Chocolate(c); c = new Fruity(c);
برای هر c میتوان متدهای اینترفیسش را اجرا کرد.
چرا به ابزارهای مدیریت حالت نیاز داریم؟
به محض رد شدن از مرز پیاده سازی امکانات اولیهی یک برنامه، نیاز به ابزارهای مدیریت حالت نمایان میشوند؛ خصوصا زمانیکه نیاز است با اطلاعات قابل توجهی سر و کار داشت. مهمترین دلیل استفادهی از یک ابزار مدیریت حالت، مدیریت منطق تجاری برنامه است. منطق نمایشی برنامه مرتبط است به نحوهی نمایش اجزای آن در صفحه؛ مانند نمایش یک صفحهی مودال، تغییر رنگ عناصر با عبور کرسر ماوس از روی آنها و در کل منطقی که مرتبط و یا وابستهی به هدف اصلی برنامه نیست. از سوی دیگر منطق تجاری برنامه مرتبط است با مدیریت، تغییر و ذخیره سازی اشیاء تجاری مورد نیاز آن؛ مانند اطلاعات حساب کاربری شخص و دریافت اطلاعات برنامه از یک API که مختص به برنامهی خاص ما است و به همین دلیل نیاز به ابزاری برای مدیریت بهینهی آن وجود دارد. برای مثال اینکه در کجا باید منطق تجاری و نمایشی را به هم متصل کرد، میتواند چالش بر انگیر باشد. چگونه باید اطلاعات کاربر را ذخیره کرد؟ چگونه React باید متوجه شود که اطلاعات ما تغییر کردهاست و در نتیجهی آن کامپوننتی را مجددا رندر کند؟ یک ابزار مدیریت حالت، تمام این مسایل را به نحو یکدستی در سراسر برنامه، مدیریت میکند.
اگر از یک ابزار مدیریت حالت استفاده نکنیم، مجبور خواهیم شد تمام اطلاعات منطق تجاری را در داخل state کامپوننتها ذخیره کنیم که توصیه نمیشود؛ چون مقیاس پذیر نیست. برای مثال فرض کنید قرار است تمام اطلاعات state را داخل یک کامپوننت ذخیره کنیم. هر زمانیکه بخواهیم این state را از طریق یک کامپوننت فرزند تغییر دهیم، نیاز خواهد بود این اطلاعات را به والد آن کامپوننت ارسال کنیم که اگر از تعداد زیادی کامپوننت تو در تو تشکیل شده باشد، زمانبر و به همراه کدهای تکراری زیادی خواهد بود. همچنین اینکار سبب رندر مجدد کل برنامه با هر تغییری در state آن میشود که غیرضروری بوده و کارآیی برنامه را کاهش میدهد. به علاوه در این بین مشخص نیست هر قسمت از state، از کدام کامپوننت تامین شدهاست. به همین جهت نیاز به روشی برای مدیریت حالت در بین کامپوننتهای برنامه وجود دارد.
داشتن تنها یک محل برای ذخیره سازی state در برنامه
همانطور که در قسمت 8 ترکیب کامپوننتها در سری React 16x بررسی کردیم، هر کامپوننت در React، دارای state خاص خودش است و این state از سایر کامپوننتها کاملا مستقل و ایزولهاست. این مورد با بزرگتر شدن برنامه و برقراری ارتباط بین کامپوننتها، مشکل ایجاد میکند. برای مثال اگر بخواهیم دکمهای را در صفحه قرار داده و توسط این دکمه درخواست صفر شدن مقدار هر کدام از شمارشگرها را صادر کنیم، با صفر کردن value هر کدام از این کامپوننتها، اتفاقی رخ نمیدهد. چون state محلی این کامپوننتها، با سایر اجزای صفحه به اشتراک گذاشته نمیشود و باید آنرا تبدیل به یک controlled component کرد، بطوریکه دارای local state خاص خودش نیست و تمام دادههای دریافتی را از طریق this.props دریافت میکند و هر زمانیکه قرار است دادهای تغییر کند، رخدادی را به والد خود صادر میکند. بنابراین این کامپوننت به طور کامل توسط والد آن کنترل میشود. تازه این روش در مورد کامپوننتهایی صدق میکند که رابطهی والد و فرزندی بین آنها وجود دارد. اگر چنین رابطهای وجود نداشت، باید state را به یک سطح بالاتر انتقال داد. برای مثال باید state کامپوننت Counters را به والد آن که کامپوننت App است، منتقل کرد. پس از آن چون کامپوننتهای ما، از کامپوننت App مشتق میشوند، اکنون میتوان این state را به تمام فرزندان App توسط props منتقل کرد و به اشتراک گذاشت. این مورد هم مانند مثال انتقال اطلاعات کاربر لاگین شدهی به سیستم، به تمام زیر قسمتهای برنامه، نیاز به ارسال اطلاعات از طریق props یک کامپوننت، به کامپوننت بعدی را دارد و به همین ترتیب برای مابقی که به props drilling مشهور است و روش پسندیدهای نیست.
Redux چیست؟ ذخیره سازی کل درخت state یک برنامه، در یک محل. به این ترتیب به یک شیء جاوا اسکریپتی بزرگ خواهیم رسید که در برگیرندهی تمام state برنامهاست. یکی از مزایای آن امکان serialize و deserialize کل این شیء، به سادگی است. برای مثال توسط متد JSON.stringify میتوان آنرا در جائی ذخیره کرد و سپس آنرا به صورت یک شیء جاو اسکریپتی در زمانی دیگر بازیابی کرد. یکی از مزایای آن، امکان بازیابی دقیق شرایط کاربری است که دچار مشکل شدهاست و سپس دیباگ و رفع مشکل او، در زمانی دیگر.
تاریخچهای از سیستمهای مدیریت حالت
همه چیز با AngularJS 1x شروع شد که از data binding دو طرفه پشتیبانی میکرد. هرچند این روش برای همگام نگه داشتن View و مدل برنامه، مفید است، اما در Viewهای پیچیده، برنامه را کند میکند. در همین زمان فیسبوک، روش مدیریت حالتی را به نام Flux ارائه داد که از data binding یک طرفه پشتیبانی میکرد. به این معنا که در این روش، همواره اطلاعات از View به مدل، جریان پیدا میکند. کار کردن با آن سادهاست؛ چون نیازی نیست حدس زده شود که اکنون جریان اطلاعات از کدام سمت است. اما مشکل آن عدم هماهنگی model و view، در بعضی از حالات است. Flux از این جهت به وجود آمد که مدیریت حالت در برنامههای React آن زمان، پیچیده بود و مقیاس پذیری کمی داشت (پیش از ارائهی Context و Hooks). در کل Flux صرفا یکسری الگوی مدیریت حالت را بیان میکند و یک کتابخانهی مجزا نیست. بر مبنای این الگوها و قراردادها، میتوان کتابخانههای مختلفی را ایجاد کرد. از این رو در سال 2015، کتابخانههای زیادی مانند Reflux, Flummox, MartyJS, Alt, Redux و غیره برای پیاده سازی آن پدید آمدند. در این بین، کتابخانهی Redux ماندگار شد و پیروز این نبرد بود!
توابع خالص و ناخالص (Pure & Impure Functions)
پیش از شروع بحث، نیاز است با یکسری از واژهها مانند توابع خالص و ناخالص آشنا شد. این نکات از این جهت مهم هستند که Redux فقط با توابع خالص کار میکند.
توابع خالص: تعدادی آرگومان را دریافت کرده و بر اساس آنها، مقداری را باز میگردانند.
// Pure const add = (a, b) => { return a + b; }
توابع ناخالص: این نوع توابع سبب تغییراتی در متغیرهایی خارج از میدان دید خود میشوند و یا به همراه یک سری اثرات جانبی (side effects) مانند تعامل با دنیای خارج (وجود یک console.log در آن تابع و یا دریافت اطلاعاتی از یک API خارجی) هستند.
// Impure const b; const add = (a) => { return a + b; }
// Impure const add = (a, b) => { console.log('lolololol'); return a + b; }
// Impure const add = (a, b) => { Api.post('/add', { a, b }, (response) => { // Do something. }); };
روشهایی برای جلوگیری از تغییرات در اشیاء در جاوا اسکریپت
ایجاد تغییرات در آرایهها و اشیاء (Mutating arrays and objects) نیز ناخالصی ایجاد میکند؛ از این جهت که سبب تغییراتی در دنیای خارج (خارج از میدان دید تابع) میشویم. به همین جهت نیاز به روشهایی وجود دارد که از این نوع تغییرات جلوگیری کرد:
// Copy object const original = { a: 1, b: 2 }; const copy = Object.assign({}, original);
برای مثال در React، برای انجام رندر نهایی، در پشت صحنه کار مقایسهی اشیاء صورت میگیرد. به همین جهت اگر همان شیءای را که ردیابی میکند تغییر دهیم، دیگر نمیتواند به صورت مؤثری فقط قسمتهای تغییر کردهی آنرا تشخیص داده و کار رندر را فقط بر اساس آنها انجام دهد و مجبور خواهد شد کل یک شیء را بارها و بارها رندر کند که اصلا بهینه نیست. به همین جهت، ایجاد تغییرات مستقیم در شیءای که به state آن انتساب داده میشود، مجاز نیست.
متد Object.assign، چندین شیء را نیز میتواند با هم یکی کند و شیء جدیدی را تشکیل دهد:
// Extend object const original = { a: 1, b: 2 }; const extension = { c: 3 }; const extended = Object.assign({}, original, extension);
// Copy object const original = { a: 1, b: 2 }; const copy = { ...original };
// Extend object const original = { a: 1, b: 2 }; const extension = { c: 3 }; const extended = { ...original, ...extension };
روشهایی برای جلوگیری از تغییرات در آرایهها در جاوا اسکریپت
متد slice آرایهها نیز بدون ذکر آرگومانی، یک کپی از آرایهی اصلی را ایجاد میکند:
// Copy array const original = [1, 2, 3]; const copy = [1, 2, 3].slice();
// Copy array const original = [1, 2, 3]; const copy = [ ...original ];
// Extend array const original = [1, 2, 3]; const extended = original.concat(4); const moreExtended = original.concat([4, 5]);
معادل قطعه کد فوق در ES-6 و به همراه spread operator آن به صورت زیر است:
// Extend array const original = [1, 2, 3]; const extended = [ ...original, 4 ]; const moreExtended = [ ...original, ...extended, 5 ];
مفاهیم ابتدایی Redux
در Redux برای ایجاد تغییرات در شیء کلی state، از مفهومی به نام dispatch actions استفاده میشود. action در اینجا به معنای رخدادن چیزی است؛ مانند کلیک بر روی یک دکمه و یا دریافت اطلاعاتی از یک API. در این حالت مقایسهای بین وضعیت قبلی state و وضعیت فعلی آن صورت میگیرد و تغییرات مورد نیاز جهت اعمال به UI، محاسبه خواهند شد.
اصلیترین جزء Redux، تابعی است به نام Reducer. این تابع، یک تابع خالص است و دو آرگومان را دریافت میکند:
تابع Reducer، بر اساس action و یا رخدادی، ابتدا کل state برنامه را دریافت میکند و سپس خروجی آن بر اساس منطق این تابع، یک state جدید خواهد بود. اکنون که این state جدید را داریم، برنامهی React ما میتواند به تغییرات آن گوش فرا داده و بر اساس آن، UI را به روز رسانی کند. به این ترتیب کار اصلی مدیریت state، به خارج از برنامهی React منتقل میشود.
در این تصویر، تابع action creator را هم ملاحظه میکند که کاملا اختیاری است. یک action میتواند یک رشته و یا یک عدد باشد. با پیچیده شدن برنامه، نیاز به ارسال یکسری متادیتا و یا اطلاعات بیشتری از اکشن رسیدهاست. کار action creator، ایجاد شیء action، به صورت یک دست و یکنواخت است تا دیگر نیازی به ایجاد دستی آن نباشد.
مزایای کار با Redux
- داشتن یک مکان مرکزی برای ذخیره سازی کلی حالت برنامه (به آن «source of truth» و یا store هم گفته میشود): به این ترتیب مشکل ارسال خواص در بین کامپوننتهای عمیق و چند سطحی، برطرف شده و هر زمانیکه نیاز بود، از آن اطلاعاتی را دریافت و یا با قالب خاصی، آنرا به روز رسانی میکنند.
- رسیدن به بهروز رسانیهای قابل پیش بینی state: هرچند در حالت کار با Redux، یک شیء بزرگ جاوا اسکریپتی، کل state برنامه را تشکیل میدهد، اما امکان کار مستقیم با آن و تغییرش وجود ندارد. به همین جهت است که برای کار با آن، باید رویدادی را از طریق actionها به تابع Reducer آن تحویل داد. چون Reducer یک تابع خالص است، با دریافت یک سری ورودی مشخص، همواره یک خروجی مشخص را نیز تولید میکند. به همین جهت قابلیت ضبط و تکرار را پیدا میکند؛ همان بحث serialize و deseriliaze، توسط ابزاری مانند: logrocket. به علاوه قابلیت undo و redo را نیز میتوان به این ترتیب پیاده سازی کرد (state جدید محاسبه شده، مشخص است، کل state قبلی را نیز داریم یا میتوان ذخیره کرد و سپس برای undo، آنرا جایگزین state جدید نمود). افزونهی redux dev tools نیز قابلیت import و export کل state را به همراه دارد.
- چون تابع Reducer، یک تابع خالص است و همواره خروجیهای مشخصی را به ازای ورودیهای مشخصی، تولید میکند، آزمایش کردن، پیاده سازی و حتی logging آن نیز سادهتر است. در این بین حتی یک افزونهی مخصوص نیز برای دیباگ آن تهیه شدهاست: redux-devtools-extension. تابع خالص، تابعی است که به همراه اثرات جانبی نیست (side effects)؛ به همین جهت عملکرد آن کاملا قابل پیش بینی بوده و آزمون پذیری آن به دلیل نداشتن وابستگیهای خارجی، بسیار بالا است.
Context API خود React چطور؟
در قسمت 33 سری React 16x، مفهوم React Context را بررسی کردیم. پس از معرفی آن با React 16.3، مقالات زیادی منتشر شدند که ... Redux مردهاست (!) و یا بجای Redux از React context استفاده کنید. اما واقعیت این است که React Redux در پشت صحنه از React context استفاده میکند و تابع connect آن دقیقا به همین زیر ساخت متصل میشود.
کار با Redux مزایایی مانند کارآیی بالاتر، با کاهش رندرهای مجدد کامپوننتها، دیباگ سادهتر با افزونههای اختصاصی و همچنین سفارشی سازی، مانند نوشتن میانافزارها را به همراه دارد. اما شاید واقعا نیازی به تمام این امکانات را هم نداشته باشید؛ اگر هدف، صرفا انتقال سادهتر اطلاعات بوده و برنامهی مدنظر نیز کوچک است. React Context برخلاف Redux، نگهدارندهی state نیست و بیشتر هدفش محلی برای ذخیره سازی اطلاعات مورد استفادهی در چندین و چند کامپوننت تو در تو است. هرچند شبیه به Redux میتوان اشارهگرهایی از متدها را به استفاده کنندگان از آن ارسال کرد تا سبب بروز رویدادها و اکشنهایی در کامپوننت تامین کنندهی Contrext شوند (یا یک کتابخانهی ابتدایی شبیه به Redux را توسط آن تهیه کرد). بنابراین برای انتخاب بین React Context و Redux باید به اندازهی برنامه، تعداد نفرات تیم، آشنایی آنها با مفاهیم Redux دقت داشت.
قبل از C# 3.0 فقط میشد یک کلاس را از طریق ارثبری از آن توسعه داد و به کلاس مهروموم شده یاSealed نیز نمیشد تابعی افزود که البته ممکن است بگویید که این کار قوانین شیءگرایی را نقض میکند اما در پاسخ باید گفت که توابع تعمیم یافته به اعضاء خصوصی کلاسی که تعمیم مییابد، دسترسی ندارند.
تعمیم یک کلاس در خارج از بدنه کلاس انجام میشود و این کار میتواند در فضای نام همان کلاس یا خارج از آن انجام شود و شکل کلی آن به صورت زیر است:
public static class ExtendingClassName { public static ReturnType MethodName(this ExtendedMethod arg) { //دستورات درون متد Return ReturnType; } }
- کلاس توسعهدهنده و تابع توسهدهنده باید استاتیک باشند.
- در داخل آرگومان تابع، کلمه کلیدی this استفاده میشود.
- بعد از this عنوان کلاسی که قصد توسعه آن را داریم، ذکر میکنیم.
- در هرجا که خواستیم از قابلیت تعمیم داده شده استفاده کنیم باید فضای نام مربوط به آن را ذکر کنیم.
- با
کلمه کلیدی static نمیتوان کلاسی با متدهایvirtual ، abstract و override را توسعه داد.
مثلا اگر قصد داریم به کلاس String تابع AddPrefix را اضافه کنیم به این شکل عمل میکنیم :
public static class ExtendingString { public static string AddPrefix(this string arg) { return String.Format("prefix{0}",arg); } }
که نحوه استفاده از آن به شکل زیر است:
string s = "Student"; Console.WriteLine(s.AddPrefix());
و خروجی آن نمایش prefixStudent است.
اگر بخواهیم عبارت پیشوند را از طریق آرگومان ارسال کنیم به این شکل عمل میکنیم:
public static class ExtendingString { public static string AddPrefix(this string arg, string prefix) { return String.Format("{0}{1}", prefix, arg); } }
که نحوه استفاده از آن به شکل زیر است:
var s = "Student"; Console.WriteLine(s.AddPrefix("tbl"));
و خروجی آن نمایش tblStudent است.
به عنوان مثال دوم کلاس زیر را در نظر بگیرید:
public class Test { public int AddOne(int val) { return val + 1; } }
اگر بخواهیم کلاس فوق را توسعه داده و متد دیگری مثلا با عنوان AddTwo اضافه کنیم، کلاس توسعه دهنده را به این شکل مینویسیم:
public static class ExtendingTest { public static int AddTwo(this Test arg, int val) { return val + 2; } }
و روش استفاده از آن بصورت زیر است:
static void Main(string[] args) { var x = new Test(); Console.WriteLine(x.AddOne(10)); Console.WriteLine(x.AddTwo(10)); Console.Read(); }
Local Storage
با استفاده از Local Storage قادر خواهیم بود تا دادههایی را در سمت کلاینت ذخیره کنیم؛ بدون اینکه بر عملکرد سایت تاثیر بگذارد. منظور از تاثیر بر روی عملکرد این است که در هر رفت به سمت سرور لازم نیست با درخواست ارسالی، دادههای اضافهای ارسال شوند.
با استفاده از Local Storage قادر خواهیم بود حداقل 5 مگابایت داده را ذخیره کنیم.
استفاده از Local Storage با استفاده از دو شیء زیر امکان پذیر میباشد:
- window.localStorage : اطلاعات ذخیره شده در آن فاقد تاریخ انقضاء
میباشند و تا زمانی که اقدام به حذف آن ننمایید، دادههای آن معتبر
میباشند.
- window.sessionStorage : اطلاعات ذخیره شده در آن تا زمان بسته شدن تب مرورگر، معتبر میباشند. با بسته شدن تب فعلی مرورگر، تمامی اطلاعات ذخیره شده از بین خواهند رفت.
نحوه استفاده:
- نحوه افزودن داده در Local Storage به صورت کلید/ مقدار میباشد.
ابتدا نگاهی داشته باشیم به اینترفیس Storage:
interface Storage { readonly attribute unsigned long length; DOMString? key(unsigned long index); getter DOMString? getItem(DOMString key); setter void setItem(DOMString key, DOMString value); deleter void removeItem(DOMString key); void clear(); }
if (typeof(Storage) !== "undefined") { // do ... }
شیء window.localStorage
همانطور که بیان کردیم، در این روش داده دارای تاریخ انقضاء نمیباشد.
() SetItem :
متد setItem برای ذخیره اطلاعات است و نحوه استفاده از آن به صورت زیر میباشد:
localStorage.setItem("lastpost", "localstorage");
localStorage.setItem("visitorCount",15 ); localStorage.setItem("visitorCount", 16);
() getItem :
برای دسترسی به مقدار ذخیره شده میتوانیم از متد getItem به همراه مقدار کلید استفاده کنیم. در صورت موجود نبودن مقدار، null برگردانده خواهد شد.
localStorage.getItem("lastpost");
localStorage.lastpost = "localstorage"; document.getElementById("result").innerHTML = localStorage.lastpost;
و برای حذف اطلاعات ذخیره شده:
localStorage.removeItem("lastpost");
خصوصیت length :
تعداد جفت کلید / مقدارهای جاری را بر میگرداند.
متد Key :
این متد مقداری را از ورودی دریافت کرده و اسم کلید مورد نظر را بر میگرداند. ایندکس آن از صفر شروع خواهد شد. قطعه کد زیر باعث برگرداندن مقدار lastname میشود:
localStorage.setItem("name","uthman" ); localStorage.setItem("lastname","24" ); alert(localStorage.key(1));
این متد باعث پاک شدن تمامی دادههای ذخیره شده خواهد شد.
شی sessionStorage :
از نظر syntax دستوری همانند localStorage میباشد و تفاوت آن فقط در زمان ذخیره سازی است؛ با بسته شدن تب مرورگر، تمامی دادههای ذخیره شده پاک خواهند شد.
کتابخانههای مرتبط :
Locker :
با استفاده از این کتابخانه ، قادر خواهید بود تا اطلاعات را با فرمتهای مورد نظر، بدون convert آنها ذخیره نمایید. نمونهای از آن به صورت زیر خواهد بود:
Lockr.set('website', 'SitePoint'); // string Lockr.set('categories', 8); // number Lockr.set('users', [{ name: 'John Doe', age: 18 }, { name: 'Jane Doe', age: 19 }]);
secStore.js :
با استفاده از کتابخانه SJCL امنیت دادههای ذخیره شده، بالا میروند.
var storage = new secStore , options = { encrypt: true, data: { key: 'some data that is somewhat private' } }; storage.set(options, function(err, results){ if (err) throw err; console.log(results); });
و سایر کتابخانههای مرتبط :
public enum Grade { Failing = 5, BelowAverage = 10, Average = BelowAverage + 5, // = 15 VeryGood = 18, Excellent = 20 }
با در نظر گرفتن مثال قبل، یک Custom Attribute به نوع داده شمارشی اضافه میکنیم. برای این منظور بصورت زیر عمل میکنیم.
class Description : Attribute { public string Text; public Description(string text) { Text = text; } }
public enum Grade { [Description("Mardood")] Failing = 5, [Description("Ajab Shansi")] BelowAverage = 10, [Description("Bad Nabood")] Average = BelowAverage + 5, [Description("Khoob Bood")] VeryGood = 18, [Description("Gol Kashti")] Excellent = 20 }
public static class ExtensionMethodCls { public static string GetDescription(this Enum enu) { Type type = enu.GetType(); MemberInfo[] memInfo = type.GetMember(enu.ToString()); if (memInfo != null && memInfo.Length > 0) { object[] attrs = memInfo[0].GetCustomAttributes(typeof(Description), false); if (attrs != null && attrs.Length > 0) return ((Description)attrs[0]).Text; } return enu.ToString(); } }
Console.WriteLine(grade.GetDescription()); // Print Bad Nabood
using System; using System.Reflection; namespace CSharpEnum { class Description : Attribute { public string Text; public Description(string text) { Text = text; } } public enum Grade { [Description("Mardood")] Failing = 5, [Description("Ajab Shansi")] BelowAverage = 10, [Description("Bad Nabood")] Average = BelowAverage + 5, [Description("Khoob Bood")] VeryGood = 18, [Description("Gol Kashti")] Excellent = 20 } public static class ExtensionMethodCls { public static string GetDescription(this Enum enu) { Type type = enu.GetType(); MemberInfo[] memInfo = type.GetMember(enu.ToString()); if (memInfo != null && memInfo.Length > 0) { object[] attrs = memInfo[0].GetCustomAttributes(typeof(Description), false); if (attrs != null && attrs.Length > 0) return ((Description)attrs[0]).Text; } return enu.ToString(); } } class Program { static void Main(string[] args) { const Grade grade = Grade.Average; Console.WriteLine("Underlying type: {0}", Enum.GetUnderlyingType(grade.GetType())); Console.WriteLine("Type Code : {0}", grade.GetTypeCode()); Console.WriteLine("Value : {0}", (int)grade); Console.WriteLine("--------------------------------------"); Console.WriteLine(grade.ToString()); // name of the constant Console.WriteLine(grade.ToString("G")); // name of the constant Console.WriteLine(grade.ToString("F")); // name of the constant Console.WriteLine(grade.ToString("x")); // value is hex Console.WriteLine(grade.ToString("D")); // value in decimal Console.WriteLine("--------------------------------------"); Console.WriteLine(grade.GetDescription()); // Print Bad Nabood Console.ReadKey(); } } }
Symbols در ES 6
Symbol یک primitive data type جدید در ES 6 است؛ دقیقا مانند اعداد، Boolean، رشتهها و امثال آنها. دو نکتهی مهم در مورد Symbols وجود دارد:
الف) منحصربفرد و immutable (غیرقابل تغییر) هستند.
ب) میتوان از آنها به عنوان کلیدهایی جهت افزودن خواص جدید به اشیاء استفاده کرد.
ایجاد یک Symbol باید بدون استفاده از کلمهی new انجام شود (چون یک primitive data type است):
let s = Symbol();
let s1 = Symbol("some description");
let firstName = Symbol(); let person = { lastName: "Vahid", [firstName]: "N", }; // person.lastName = "Vahid" // person[firstName] = "N"
در ES 5 (نگارش فعلی جاوا اسکریپت)، کتابخانههای مختلف از time stamp و یا اعداد اتفاقی برای شبیه سازی چنین قابلیتی استفاده میکنند اما در ES 6 یک راه حل استاندارد به نام Symbols برای این مساله ارائه شدهاست.
چند نکته
- زمانیکه خاصیتی با کلیدی از نوع Symbol تعریف میشود، دیگر در حلقههای for in قدیمی ظاهر نخواهد شد.
let names = []; for(var p in person) { names.push(p); }
سؤال: ES 6 چگونه از Symbols جهت تعریف Iterators استفاده میکند؟
مطابق استاندارد ES 6 اگر متد خاصی با نام iterator@@ در شیءایی ظاهر شود، این شیء قابل پیمایش بوده و به عنوان منبع حلقهی for of قابل استفادهاست.
خوب، اکنون چگونه میتوان بررسی کرد که آیا شیءایی دارای متد ویژهی iterator@@ است؟ برای این منظور باید بررسی کرد که آیا این شیء دارای عضو Symbol.iterator هست یا خیر؟ خاصیت iterator متصل به متد Symbol، یکی از سمبلهای پیش فرض ES 6 است.
برای مثال آرایهی ذیل را درنظر بگیرید:
var numbers = [1, 2, 3];
numbers[Symbol.iterator];
همانطور که در تصویر مشاهده میکنید، آرایه و یا رشتهی تعریف شده، دارای Iterator هستند؛ اما عدد تعریف شده، خیر.
و یا اگر بخواهیم همان مثال while دار مطلب بررسی Iterators را با Symbol.iterator بازسازی کنیم، به مثال زیر خواهیم رسید:
var numbersIterator = numbers[Symbol.iterator](); numbersIterator.next(); // Result: Object {value: 1, done: false} numbersIterator.next(); // Result: Object {value: 2, done: false} numbersIterator.next(); // Result: Object {value: 3, done: false} numbersIterator.next(); // Result: Object {value: undefined, done: true}
ایجاد یک Iterator سفارشی با استفاده از Symbol.iterator
در این مثال قصد داریم یک پیمایشگر سفارشی را بر روی یک رشتهی دریافتی، ایجاد کنیم. ابتدا ایجاد سازندهی شیء:
function Words(str) { this._str = str; }
Words.prototype[Symbol.iterator] = function() { var re = /\S+/g; var str = this._str; return { next: function() { var match = re.exec(str); if (match) { return {value: match[0], done: false}; } return {value: undefined, done: true}; } } };
var helloWorld = new Words("Hello world"); for (var word of helloWorld) { console.log(word); } // Result: "Hello" // Result: "world"