20 بسته الگوریتم در Nuget
20 NuGet packages برای Dapper
NuGet چیست؟
روش متداول استفاده از کتابخانههای موجود دات نتی در Visual studio عموما به این صورت است: مراجعه به سایت مربوطه، دریافت بسته مورد نظر، باز کردن آن و سپس افزودن ارجاعی به اسمبلیهای آن کتابخانه. در این حالت زمانیکه نسخهی جدیدی از کتابخانهی مورد استفاده ارائه شود (و عموما تا مدتها شاید از آن بیاطلاع باشیم) تمام این مراحل باید از ابتدا تکرار شوند و همینطور الی آخر.
برای رفع این نقیصه، تیم ASP.NET، افزونهای سورس باز و رایگان را به نام NuGet جهت VS.Net 2010 طراحی کردهاند که کار مدیریت بستههای کتابخانههای مورد استفاده را بسیار ساده کرده است. امکانات این افزونه پس از نصب، در دو حالت استفاده از رابط گرافیکی کاربری آن و یا با استفاده از خط فرمان PowerShell ویندوز در دسترس خواهد بود. این افزونه در زمان بارگذاری، با مراجعه به فید سایت مرکزی خود، لیست بستههای مهیا را در اختیار علاقمندان قرار میدهد. درب این سایت مرکزی به روی تمام توسعه دهندهها جهت افزودن بستههای خود باز است.
و ... فراگیری کار با NuGet برای تمام برنامه نویسان دات نت لازم و ضروری است! از این جهت که پیغام "این بسته تنها برای NuGet عرضه شده است" کم کم در حال متداول شدن میباشد و دیگر سایتهای مرتبط، لینک مستقیمی را جهت دریافت کتابخانههای خود ارائه نمیدهند. حتی خبر به روز شدن محصولات خود را هم شاید دیگر به صورت منظم ارائه ندهند؛ زیرا NuGet کار مدیریت آنها را به عهده خواهد داشت.
دریافت و نصب NuGet
NuGet را حداقل به سه طریق میتوان دریافت و نصب کرد:
الف) با مراجعه به سایت CodePlex : (+)
ب) دریافت آن از سایت گالریهای آن : (+)
ج) با استفاده از امکانات VS.NET
هر سه روش فوق به دریافت و نصب فایل NuGet.Tools.vsix منتهی میشوند. برای مثال در روش (ج) باید به منوی Tools و گزینهی Extension Manager مراجعه کنید. سپس برگهی Online Gallery را گشوده و اندکی صبر کنید تا اطلاعات آن دریافت و نمایش داده شود. سپس NuGet را در Search box بالای صفحه نوشته و NuGet Package manager ظاهر شده را انتخاب و نصب کنید.
نحوه استفاده از NuGet
فرض کنید یک پروژه جدید ASP.NET را ایجاد کردهاید و نیاز است تا کتابخانهی ELMAH به آن اضافه شود. روش انجام اینکار را به کمک NuGet در ادامه بررسی خواهیم کرد (کمتر از یک دقیقه زمان خواهد برد):
الف) با کمک امکانات رابط گرافیکی کاربر آن
سادهترین روش استفاده از NuGet ، کلیک راست بر روی پوشه References در Solution explorer و سپس انتخاب گزینهی Add Library Package Reference میباشد:
در صفحهی باز شده، برگهی Online را باز کنید و مدتی صبر نمائید تا اطلاعات لازم دریافت گردد (در زمان نگارش این مطلب، 1135 بسته در این مخزن موجود است):
سپس در جعبهی جستجوی سمت راست بالای صفحه، نام کتابخانهی مورد نظر را نوشته و اندکی صبر کنید تا اطلاعات آن نمایش داده شود:
اکنون با کلیک بر روی دکمه Install ، بسته مرتبط با این کتابخانه دریافت شده و سپس به صورت خودکار ارجاعی به آن نیز افزوده خواهد شد. همچنین تنظیمات مرتبط با فایل Config برنامه هم اضافه میشوند.
روش دیگر ظاهر کردن این صفحه، مراجعه به منوی Tools و گزینهی Library Package Manager میباشد:
جهت دریافت به روز رسانیهای بستههای نصب شده تنها کافی است به برگهی Updates این صفحه مراجعه کرده و موارد لیست شده را نصب نمائیم:
نکته: NuGet در SharpDevelop 4.1 به بعد هم پشتیبانی میشود:
ب) با استفاده از امکانات خط فرمان PowerShell ویندوز
برای استفاده از امکانات پاورشل ویندوز نیاز است تا پاورشل نگارش 2 بر روی سیستم شما نصب باشد (نیاز به Windows XP with Service Pack 3 به بعد دارد). سپس به منوی Tools ، قسمت Library Package Manager ، گزینهی Package Manager Console آن جهت فعال سازی کنسول پاور شل در VS.NET مراجعه نمائید:
نکته: در تصویر فوق پس از نوشتن el ، دکمه tab فشرده شده است. در این حالت منوی پکیجهای مهیای شروع شده با el، از سایت مرکزی NuGet ظاهر گردیده است.
فرامین مهمی که در اینجا در دسترس هستند شامل: List-Package ، Uninstall-Package ، Update-Package و Get-Package میباشند. برای مثال اگر قصد جستجو در بین بستههای موجود را داشته باشید Get-Package بسیار مفید است:
برای مثال جهت یافتن بستههای مرتبط با wpf و silverlight به صورت زیر میتوان عمل کرد:
PM> get-package -remote -filter wpf
PM> get-package -remote -filter silverlight
نکته: روش دیگر جستجو در بین بستههای مهیا، مراجعه به سایت گالری آن است : (+) . در اینجا دستور پاورشل نصب هر بستهی یافت شده نیز نمایش داده میشود.
ج) استفاده از برنامه NuGet.exe
برنامه NuGet.exe از سایت CodePlex قابل دریافت است. این روش هم جهت علاقمندان به خط فرمان تدارک دیده شده است!
پس از دریافت آن فرض کنید میخواهیم تمام بستههای شروع شده با nhi را لیست کنیم. برای این منظور دستور خط فرمان ذیل را صادر کنید:
D:\Test>nuget list nhi
D:\Test>nuget install NHibernate
به این صورت کتابخانه NHibernate به همراه تمام وابستگیهای آن دریافت خواهد شد.
به روز رسانی خودکار NuGet
برای به روز رسانی برنامه nuget.exe دستور زیر را میتوان صادر نمود:
D:\Test>NuGet.exe u
Tools | Options, then Environment | Extension Manager and click "Automatically check for updates to installed extensions."
ادامه دارد ...
به این صورت مدلهای خود را تعریف کرده و طبق مقالهی قبلی، Controllerهای هر یک را پیاده سازی نمایید:
public class Supplier { [Key] public string Key {get; set; } public string Name { get; set; } } public class Category { public int ID { get; set; } public string Name { get; set; } public virtual ICollection<Product> Products { get; set; } } public class Product { public int ID { get; set; } public string Name { get; set; } public decimal Price { get; set; } [ForeignKey("Category")] public int CategoryId { get; set; } public Category Category { get; set; } [ForeignKey("Supplier")] public string SupplierId { get; set; } public virtual Supplier Supplier { get; set; } }
پکیج Microsoft.AspNet.OData به تازگی ورژن 6 آن به صورت رسمی منتشر شده و شامل تغییراتی نسبت به نسخهی قبلی آن است. اولین نکتهی حائز اهمیت، Config آن است که به صورت زیر تغییر کرده و باید Optionهای مورد نیاز، کانفیگ شوند. در این نسخه DI نیز به Odata اضافه شده است:
public static void Register(HttpConfiguration config) { ODataModelBuilder odataModelBuilder = new ODataConventionModelBuilder(); var product = odataModelBuilder.EntitySet<Product>("Products"); var category = odataModelBuilder.EntitySet<Category>("Categories"); var supplier = odataModelBuilder.EntitySet<Supplier>("Suppliers"); var edmModel = odataModelBuilder.GetEdmModel(); supplier.EntityType.Ignore(c => c.Name); config.Select(System.Web.OData.Query.QueryOptionSetting.Allowed); config.MaxTop(25); config.OrderBy(System.Web.OData.Query.QueryOptionSetting.Allowed); config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed); config.Expand(System.Web.OData.Query.QueryOptionSetting.Allowed); config.Filter(System.Web.OData.Query.QueryOptionSetting.Allowed); config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed); //config.MapODataServiceRoute("ODataRoute", "odata", edmModel); // کانفیگ به صورت معمولی config.MapODataServiceRoute("ODataRoute", "odata", builder => { builder.AddService(ServiceLifetime.Singleton, sp => edmModel); builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault()); }); }
[EnableQuery] public IQueryable<Product> Get() { return new List<Product> { new Product { Id = 1, Name = "name 1", Price = 11, Category = new Category {Id =1, Name = "Cat1" } }, new Product { Id = 2, Name = "name 2", Price = 12, Category = new Category {Id =2, Name = "Cat2" } }, new Product { Id = 3, Name = "name 3", Price = 13, Category = new Category {Id =3, Name = "Cat3" } }, new Product { Id = 4, Name = "name 4", Price = 14, Category = new Category {Id =4, Name = "Cat4" } }, }.AsQueryable(); ; }
Description | Option |
بسط دادن موجودیت مرتبط | $expand |
فیلتر کردن نتیجه، بر اساس شرطهای Boolean ی | $filter |
فرمان به سرور که تعداد رکوردهای بازگشتی را نیز نمایش دهد(مناسب برای پیاده سازی server-side pagging ) | $count |
مرتب کردن نتیجهی بازگشتی | $orderby |
select زدن روی پراپرتیهای درخواستی | $select |
پرش کردن از اولین رکورد به اندازهی n عدد | $skip |
فقط بازگرداندن n رکورد اول | $top |
/odata/Products?$select=Id,Name
/odata/Products?$top=3&$skip=2
/odata/Products?$count=true
{@odata.context: "http://localhost:4516/odata/$metadata#Products", @odata.count: 4,…} @odata.context:"http://localhost:4516/odata/$metadata#Products" @odata.count:4 value:[{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0},…] 0:{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0} 1:{Id: 2, Name: "name 2", Price: 12, SupplierId: 0, CategoryId: 0} 2:{Id: 3, Name: "name 3", Price: 13, SupplierId: 0, CategoryId: 0} 3:{Id: 4, Name: "name 4", Price: 14, SupplierId: 0, CategoryId: 0}
/odata/Products?$filter=Id eq 1
/odata/Products?$filter=Id gt 1 and Id lt 3
/odata/Products?$orderby=Id desc
/odata/Products?$expand=Category
/odata/Products?$expand=Category,Supplier
/odata/Categories(1)?$expand=Products/Supplier
[EnableQuery(PageSize = 10)]
محدود کردن Query Options
[EnableQuery (AllowedQueryOptions= AllowedQueryOptions.Skip | AllowedQueryOptions.Top)]
[EnableQuery(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]
var product = odataModelBuilder.EntitySet<Product>("Products"); product.EntityType.Ignore(e => e.Price);
[EnableQuery(AllowedOrderByProperties = "Id,Name")]
public System.Web.Http.IHttpActionResult GetName(int key) { Product product = Get().Single(c => c.Id == key); return Ok(product.Name); }
/odata/Products(1)/Name/$value
HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 DataServiceVersion: 3.0 Content-Length: 3 Ali
Attribute Convention هایی هم برای اعتبارسنجی پراپرتیها موجود است که نام آنها واضح تعریف شدهاند:
Description | Attribute |
اجازهی فیلتر زدن بر روی آن پراپرتی داده نخواهد شد | NotFilterable |
اجازهی مرتب کردن بر روی آن پراپرتی داده نخواهد شد | NotSortable |
اجازهی select زدن بر روی آن پراپرتی داده نمیشود | NotNavigable |
اجازهی شمارش دهی بر روی آن Collection داده نمیشود | NotCountable |
اجازهی بسط دادن آن Collection داده نمیشود | NotExpandable |
و همچنین [AutoExpand] به صورت اتوماتیک آن موجودیت مورد نظر را بسط میدهد.
بطور مثال کدهای زیر را در مدل خود میتوانید مشاهده نمائید:
public class Product { public int Id { get; set; } public string Name { get; set; } [NotFilterable, NotSortable] public decimal Price { get; set; } [ForeignKey(nameof(SupplierId))] [NotNavigable] public virtual Supplier Supplier { get; set; } public int SupplierId { get; set; } [ForeignKey(nameof(CategoryId))] public virtual Category Category { get; set; } public int CategoryId { get; set; } [NotExpandable] public virtual ICollection<TestEntity> TestEnities { get; set; } }
فرض کنید پراپرتی زیر را به مدل خود اضافه کرده اید
public DateTimeOffset CreatedOn { get; set; }
حال کوئری زیر را برای فیلتر زدن، بر روی آن در اختیار داریم:
/odata/Products?$filter=year(CreatedOn) eq 2016
در اینجا فقط Product هایی بازگردانده میشوند که در سال 2016 ثبت شدهاند:
/odata/Products?$filter=CreatedOn lt cast(2017-04-01T04:11:31%2B08:00,Edm.DateTimeOffset)
کوئری فوق تاریخ مورد نظر را Cast کرده و همهی Product هایی را که قبل از این تاریخ ثبت شدهاند، باز میگرداند.
Nested Filter In Expand
/odata/Categories?$expand=Products($filter=Id gt 1 and Id lt 5)
همهی Categoryها به علاوه بسط دادن Product هایشان، در صورتیکه Id آنها بیشتر از 1 باشد
و یا حتی بر روی موجودیت بسط داده شده، select زده شود:
/odata/Categories?$expand=Products($select=Id,Name)
Custom Attribute
ضمنا به سادگی میتوان اتریبیوت سفارشی نوشت:
public class MyEnableQueryAttribute : EnableQueryAttribute { public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions) { // Don't apply Skip and Top. var ignoreQueryOptions = AllowedQueryOptions.Skip | AllowedQueryOptions.Top; return queryOptions.ApplyTo(queryable, ignoreQueryOptions); } }
روی هر متدی از کنترلر خود که اتریبیوت [MyEnableQuery] را قرار دهید، دیگر قابلیت Skip, Top را نخواهد داشت.
Dependency Injection در آخرین نسخهی OData اضافه شده است. بطور پیشفرض OData بصورت case-sensitive رفتار میکند. برای تغییر دادن آن در نسخههای قدیمی، Extension Methodی به نام EnableCaseSensitive وجود داشت. اما در نسخهی جدید شما میتوانید پیاده سازی خاص خود را از هر کدام از بخشهای OData داشته باشید و با استفاده از تزریق وابستگی، آن را به config برنامهی خود اضافه کنید؛ برای مثال:
public class CaseInsensitiveResolver : ODataUriResolver { private bool _enableCaseInsensitive; public override bool EnableCaseInsensitive { get { return true; } set { _enableCaseInsensitive = value; } } }
اینجا پیاده سازی از ODataUriResolver انجام شده و متد EnableCaseInsensitive به صورت جدیدی override و در حالت default مقدار true را برمیگرداند.
حال به صورت زیر آن را میتوان به وابستگیهای config برنامه، اضافه نمود:
config.MapODataServiceRoute("ODataRoute", "odata", builder => { builder.AddService(ServiceLifetime.Singleton, sp => edmModel); builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault()); builder.AddService<ODataUriResolver>(ServiceLifetime.Singleton, sp => new CaseInsensitiveResolver()); // how enable case sensitive });
در قسمت بعدی به Actionها و Functionها در OData میپردازیم.
این افزونه جزو موفقترین کتابخانههای دات نتی مایکروسافت در سالهای اخیر به شما میرود؛ تا حدی که معادلهای بسیاری از آن برای زبانهای دیگر مانند Java، JavaScript، Python، CPP و غیره نیز تهیه شدهاند.
استفاده از Rx به همراه یک کوئری LINQ
یک برنامهی کنسول جدید را ایجاد کنید. سپس برای نصب کتابخانهی Rx، دستور ذیل را در کنسول پاورشل نیوگت اجرا نمائید:
PM> Install-Package Rx-Main
<?xml version="1.0" encoding="utf-8"?> <packages> <package id="Rx-Core" version="2.2.4" targetFramework="net45" /> <package id="Rx-Interfaces" version="2.2.4" targetFramework="net45" /> <package id="Rx-Linq" version="2.2.4" targetFramework="net45" /> <package id="Rx-Main" version="2.2.4" targetFramework="net45" /> <package id="Rx-PlatformServices" version="2.2.4" targetFramework="net45" /> </packages>
using System; using System.Linq; namespace Rx01 { class Program { static void Main(string[] args) { var query = Enumerable.Range(1, 5).Select(number => number); foreach (var number in query) { Console.WriteLine(number); } finished(); } private static void finished() { Console.WriteLine("Done!"); } } }
اکنون اگر بخواهیم همین عملیات را توسط Rx انجام دهیم، به شکل زیر خواهد بود:
using System; using System.Linq; using System.Reactive.Linq; namespace Rx01 { class Program { static void Main(string[] args) { var query = Enumerable.Range(1, 5).Select(number => number); var observableQuery = query.ToObservable(); observableQuery.Subscribe(onNext: number => Console.WriteLine(number), onCompleted: () => finished()); } private static void finished() { Console.WriteLine("Done!"); } } }
1 2 3 4 5 Done!
observableQuery.Subscribe(Console.WriteLine, finished);
در این مثال ساده صرفا یک Syntax دیگر را نسبت به حلقهی foreach متداول مشاهده کردیم که اندکی فشردهتر است. در هر دو حالت نیز عملیات انجام شده در تردجاری صورت گرفتهاند. اما قابلیتها و ارزشهای واقعی Rx زمانی آشکار خواهند شد که پردازش موازی و پردازش در تردهای دیگر را در آن فعال کنیم.
الگوی Observer
Rx پیاده سازی کنندهی الگوی طراحی شیءگرایی به نام Observer است. برای توضیح آن یک لامپ و سوئیچ برق را درنظر بگیرید. زمانیکه لامپ مشاهده میکند سوئیچ برق در حالت روشن قرار گرفتهاست، روشن خواهد شد و برعکس. در اینجا به سوئیچ، subject و به لامپ، observer گفته میشود. هر زمان که حالت سوئیچ تغییر میکند، از طریق یک callback، وضعیت خود را به observer اعلام خواهد کرد. علت استفاده از callbackها، ارائه راهحلهای عمومی است تا بتواند با انواع و اقسام اشیاء کار کند. به این ترتیب هر بار که شیء observer از نوع متفاوتی تعریف میشود (مثلا بجای لامپ یک خودرو قرار گیرد)، نیازی نخواهد بود تا subject را تغییر داد.
در Rx دو اینترفیس معادل observer و subject تعریف شدهاند. در اینجا اینترفیس IObserver معادل observer است و اینترفیس IObservable معادل subject میباشد:
class Subject : IObservable<int> { public IDisposable Subscribe(IObserver<int> observer) { } }
class Observer : IObserver<int> { public void OnCompleted() { } public void OnError(Exception error) { } public void OnNext(int value) { } }
مجموعههای Observable کلید کار با Rx هستند. در مثال قبل ملاحظه کردیم که با استفاده از متد الحاقی ToObservable بر روی یک کوئری LINQ و یا هر نوع IEnumerable ایی، میتوان یک مجموعهی Observable را ایجاد کرد. خروجی کوئری حاصل از آن به صورت خودکار اینترفیس IObservable را پیاده سازی میکند که دارای یک متد به نام Subscribe است.
در متد Subscribe کاری که به صورت خودکار صورت خواهد گرفت، ایجاد یک حلقهی foreach بر روی مجموعهی مورد آنالیز و سپس فراخوانی متد OnNext کلاس پیاده سازی کنندهی IObserver به ازای هر آیتم موجود در مجموعه است (فراخوانی observer.OnNext). در پایان کار هم فقط return this در اینجا صورت خواهد گرفت. در حین پردازش حلقه، اگر خطایی رخ دهد، متد observer.OnError انجام میشود.
در مثال قبل،کوئری LINQ نوشته شده، خروجی از نوع IObservable ندارد. به کمک متد الحاقی ToObservable:
public static System.IObservable<TSource> ToObservable<TSource>( this System.Collections.Generic.IEnumerable<TSource> source, System.Reactive.Concurrency.IScheduler scheduler)
البته استفاده از متد Subscribe به نحوی که در مثال قبل ذکر شد، خلاصه شدهی الگوی Observer است. اگر بخواهیم دقیقا مانند الگو عمل کنیم، چنین شکلی را خواهد داشت:
var query = Enumerable.Range(1, 5).Select(number => number); var observableQuery = query.ToObservable(); var observer = Observer.Create<int>(onNext: number => Console.WriteLine(number)); observableQuery.Subscribe(observer);
پردازش نتایج یک کوئری LINQ در تردی دیگر توسط Rx
برای اجرای نتایج متد Subscribe در یک ترد جدید، میتوان پارامتر scheduler متد ToObservable را مقدار دهی کرد:
using System; using System.Linq; using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Threading; namespace Rx01 { class Program { static void Main(string[] args) { Console.WriteLine("Thread-Id: {0}", Thread.CurrentThread.ManagedThreadId); var query = Enumerable.Range(1, 5).Select(number => number); var observableQuery = query.ToObservable(scheduler: NewThreadScheduler.Default); observableQuery.Subscribe(onNext: number => { Console.WriteLine("number: {0}, on Thread-id: {1}", number, Thread.CurrentThread.ManagedThreadId); }, onCompleted: () => finished()); } private static void finished() { Console.WriteLine("Done!"); } } }
Thread-Id: 1 number: 1, on Thread-id: 3 number: 2, on Thread-id: 3 number: 3, on Thread-id: 3 number: 4, on Thread-id: 3 number: 5, on Thread-id: 3 Done!
NewThreadScheduler.Default در فضای نام System.Reactive.Concurrency واقع شدهاست.
یک نکته
در نگارشهای آغازین Rx، مقدار scheduler را میشد معادل Scheduler.NewThread نیز قرار داد که در نگارشهای جدید منسوخ شده درنظر گرفته شده و به زودی حذف خواهد شد. معادلهای جدید آن اکنون NewThreadScheduler.Default، ThreadPoolScheduler.Default و امثال آن هستند.
مدیریت خاتمهی اعمال انجام شدهی در تردهای دیگر توسط Rx
یکی از مواردی که حین اجرای نتیجهی callbackهای پردازش شدهی در تردهای دیگر نیاز است بدانیم، زمان خاتمهی کار آنها است. برای نمونه در مثال قبل، نمایش Done پس از پایان تمام callbacks انجام شدهاست. فرض کنید، callback پایان عملیات را حذف کرده و متد finished را پس از فراخوانی متد observableQuery.Subscribe قرار دهیم:
observableQuery.Subscribe(onNext: number => { Console.WriteLine("number: {0}, on Thread-id: {1}", number, Thread.CurrentThread.ManagedThreadId); }/*, onCompleted: () => finished()*/); finished();
Thread-Id: 1 number: 1, on Thread-id: 3 Done! number: 2, on Thread-id: 3 number: 3, on Thread-id: 3 number: 4, on Thread-id: 3 number: 5, on Thread-id: 3
مدیریت استثناهای رخ داده در حین پردازش مجموعههای واکنشگرا
متد Subscribe دارای چندین overload است. تا اینجا نمونهای که دارای پارامترهای onNext و onCompleted بودند را بررسی کردیم. اگر بخواهیم مدیریت استثناءها را نیز در اینجا اضافه کنیم، فقط کافی است از overload دیگر آن که دارای پارامتر onError است، استفاده نمائیم:
observableQuery.Subscribe( onNext: number => Console.WriteLine(number), onError: exception => Console.WriteLine(exception.Message), onCompleted: () => finished());
مدیریت ترد اجرای نتایج حاصل از Rx در یک برنامهی دسکتاپ WPF یا WinForms
تا اینجا مشاهده کردیم که اجرای callbackهای observer در یک ترد دیگر، به سادگی تنظیم پارامتر scheduler متد ToObservable است. اما در برنامههای دسکتاپ برای به روز رسانی عناصر رابط کاربری، حتما باید در تردی قرار داشته باشیم که آن رابط کاربری در آن ایجاد شدهاست یا به عبارتی در ترد اصلی برنامه؛ در غیر اینصورت برنامه کرش خواهد کرد. مدیریت این مساله نیز در Rx بسیار سادهاست. ابتدا نیاز است بستهی Rx-WPF را نصب کرد:
PM> Install-Package Rx-WPF
observableQuery.ObserveOn(DispatcherScheduler.Current).Subscribe(...)
observableQuery.ObserveOnDispatcher().Subscribe(...)
و یا اگر از WinForms استفاده میکنید، ابتدا بستهی Rx خاص آنرا نصب کنید:
PM> Install-Package Rx-WinForms
observableQuery.ObserveOn(SynchronizationContext.Current).Subscribe(...)
یک نکته
در Rx فرض میشود که کوئری شما زمانبر است و callbackهای مشاهدهگر سریع عمل میکنند. بنابراین هدف از callbackهای آن، پردازشهای سنگین نیست. جهت آزمایش این مساله، اینبار query ابتدایی برنامه را به شکل ذیل تغییر دهید که در آن بازگشت زمانبر یک سری داده شبیه سازی شدهاند.
var query = Enumerable.Range(1, 5).Select(number => { Thread.Sleep(250); return number; });
به همین جهت اگر میخواهید رزومهی غنیتری را ارائه دهید، فراگیری React میتواند موقعیتهای شغلی بیشتری را نصیب شما کند.
ساختار کلی یک برنامهی React
کامپوننتها (جزئی از یک رابط کاربری) قلب هر برنامهی React ای را تشکیل میدهند. برای ساخت یک برنامهی React، تعدادی کامپوننت مستقل را تهیه و با هم ترکیب میکنیم تا به رابط کاربری نهایی برسیم.
هر برنامهی React، حداقل از یک کامپوننت تشکیل میشود که به آن Root component هم میگویند. این کامپوننت بیانگر کل برنامهاست و دربرگیرندهی مابقی Child components برنامه است. بنابراین ساختار هر برنامهی React، شبیه به درختی از کامپوننتها است. اگر با Angular 2 به بعد کار کرده باشید، این مفهوم برای شما آشنا است.
یک مثال: فرض کنید میخواهیم UI برنامهای را به مانند رابط کاربری Twitter، ایجاد کنیم. هر قسمت یک صفحهی توئیتر، به کامپوننتهایی شکسته میشود؛ مانند منوی راهبری، نمایش پروفایل شخص، نمایش لیست آخرین اخبار مورد علاقهی شخص و نمایش فید. اگر بخواهیم این ساختار را توسط یک برنامهی React شبیه سازی کنیم، در بالاترین سطح، کامپوننت root را خواهیم داشت که کار ترکیب و نمایش سایر کامپوننتهای برنامه مانند nav bar ، trends ، profile و feed را انجام میدهد. اکنون در این ساختار ایجاد شده، برای مثال کامپوننت feed نیز میتواند از چندین کامپوننت مجزا تشکیل شود؛ مانند کامپوننتهای tweet و like.
بنابراین هر کامپوننت، قسمتی از UI را تشکیل میدهد. هر کدام از آنها به صورت مجزای از دیگری ساخته شده و سپس در کنار هم قرار میگیرند تا UI نهایی را شکل دهند:
هر کامپوننت در React به صورت یک کلاس ES6، با ساختاری که دارای یک شیء state و متد render است، تشکیل میشود:
class Tweet { state = {}; render() { } }
مزیت کارکردن با Virtual DOM، سادگی ایجاد، تغییر و به روز رسانی آن در مقایسه با DOM واقعی است که در نهایت کار رندر عناصر UI را در مرورگر انجام میدهد. زمانیکه در state کامپوننتی تغییری رخ میدهد، یک React Element جدید تولید میشود. سپس React این شیء جدید را با نمونهی قبلی آن مقایسه کرده و تغییرات رخداده را محاسبه میکند. در آخر این تغییرات را به DOM واقعی اعمال میکند تا با Virtual DOM موجود هماهنگ شود.
بنابراین در حین کار با React، دیگر همانند کار با جاوا اسکریپت خالص و یا jQuery، مستقیما عناصر UI و DOM واقعی را تغییر نمیدهیم. در اینجا فقط state یک کامپوننت را تغییر میدهیم و سپس React، کار ایجاد شیء UI درون حافظهای متناظر با آن و سپس اعمال آنرا به UI نهایی قابل مشاهدهی در مرورگر، انجام میدهد. به همین جهت به این کتابخانه React میگویند! چون به تغییرات state کامپوننتها واکنش نشان میدهد و سپس DOM واقعی را به روز میکند.
Angular یا React؟!
هر دوی React و Angular از لحاظ طراحی کامپوننتها بسیار شبیه به هم هستند؛ اما Angular یک فریمورک است و React تنها یک کتابخانه. تنها کاری را که React انجام میدهد، رندر View است و هماهنگ نگه داشتن آن با state کامپوننتها. این تمام کاری است که React انجام میدهد؛ نه بیشتر و نه کمتر! بنابراین یادگیری React، بسیار سریعتر و سادهتر از Angular است. بدیهی است یک برنامهی تک صفحهای وب، از اجزای دیگری مانند مسیریابی و یا کار با سرویسهای HTTP نیز تشکیل میشود. در React شما مختار هستید که کتابخانههای جانبی فراهم شدهی برای آنرا خودتان انتخاب کرده و استفاده کنید؛ برخلاف روشی که در Angular مرسوم است و به صورت مشخص و ثابتی به همراه این فریمورک ارائه میشوند.
برپایی محیط توسعهی React
اولین برنامهای را که برای کار با React باید نصب کنید، node.js است. البته ما در این سری قرار نیست با node.js کار کنیم؛ اما از یکی از اجزای آن به نام node package manager یا npm، برای نصب کتابخانهی جاوا اسکریپتی ثالث، زیاد استفاده خواهیم کرد. پس از نصب آن، به خط فرمان مراجعه کرد و دستور زیر را صادر کنید:
> npm install -g npm@latest
اگر هم خیلی پیشترها node.js را نصب کردهاید (برای مثال چند سال قبل!)، نصب نگارش جدید آن احتمالا کار نخواهد کرد. حتی عزل و نصب مجدد آن نیز کارساز نیست. در این حالت باید پس از عزل آن، پوشههای قدیمی آنرا یکی یکی یافته و دستی حذف کنید . سپس مجددا آنرا نصب کنید.
در ادامه در خط فرمان و توسط npm، قالب create-react-app را نصب خواهیم کرد:
> npm i -g create-react-app
ابزار دیگری که در این سری از آن استفاده خواهیم کرد، ادیتور بسیار معروف و محبوب VSCode است. پس از دریافت و نصب آن، چند افزونهی زیر را نیز به آن اضافه خواهیم کرد:
برای نصب آنها، پنل extensions را در VSCode، از نوار ابزار کنار صفحهی آن، انتخاب کرده و نامهای فوق را در آن جستجو و سپس نصب کنید.
و یا میتوانید این فایل را اجرا کرده و تعدادی از افزونههای مفید VSCode را یکجا نصب کنید: install-addons.zip
همچنین قابلیت فرمتکردن پس از Save را نیز در VSCode فعال کنید تا پس از هربار Save، اعمال این افزونهها به صورت خودکار صورت گیرد. برای این منظور گزینهی file->preferences->settings را در VSCode انتخاب کرده و سپس save را جستجو کرده و Format On Save را انتخاب کنید:
علاوه بر اینها، جهت کار بهتر با VSCode، بهتر است بررسی کنندههای کدهای جاوا اسکریپتی (static code analyzers) را نیز با اجرای دستور زیر نصب کنید:
> npm i -g typescript eslint tslint eslint-plugin-react-hooks
پس از این تغییرات، نیاز است یکبار VSCode را بسته و مجددا باز کنید. سپس مجددا گزینهی file->preferences->settings را در VSCode انتخاب کرده و ابتدا eslint را در اینجا جستجو کنید. در صفحهی نمایش تنظیمات آن، گزینهی Auto fix on save آنرا انتخاب نمائید. در آخر در همین قسمت settings، عبارت prettier را انتخاب کنید. در اینجا اگر گزینهی قدیمی یکپارچگی با eslint آن هنوز وجود دارد، آنرا از حالت انتخاب شده خارج کنید (به صورت قرمز و deprecated نمایش داده میشود) تا افزونهی prettier بدون مشکل و خطا کار کند (disable Prettier ESLint integration).
ایجاد قالب اولین برنامهی React
در ادامه برای ایجاد اولین برنامهی React، از بستهی create-react-app که پیشتر آنرا نصب کردیم، استفاده میکنیم. برای این منظور در خط فرمان دستور زیر را صادر کنید:
> create-react-app sample-01
این قالب نه تنها React را نصب میکند، بلکه یک development server را برای اجرا و مشاهدهی سریع برنامه، webpack را برای یکی کردن فایلها (bundling & minification)، Babel را برای کامپایل کدهای فایلهای JSX و ... نیز نصب میکند. بنابراین به این ترتیب، یک پروژهی تنظیم شده و آمادهی استفاده و توسعه را شاهد خواهیم بود که نیازی به تنظیمات اولیهی آن نیست.
پس ایجاد برنامه، وارد پوشهی sample-01 شده و دستور npm start را صادر کنید:
> cd sample-01 > npm start
development server آن، تغییرات فایلهای برنامه را تحت نظر قرار میدهد و با هر تغییری، به صورت خودکار برنامه را در مرورگر بارگذاری مجدد خواهد کرد.
بررسی ساختار اولین پروژهی React ایجاد شده
ساختار پوشهها و فایلهای مثال اولیهی ایجاد شده توسط قالب create-react-app به صورت زیر است:
البته شما در این تصویر پوشهی node_modules را که در کنار این پوشهها قرار دارد، مشاهده نمیکنید. وجود یک چنین پوشهی سنگینی با هزاران فایل داخل آن، کار نمایشی IDEها را با مشکل مواجه میکند (مصرف حافظهی بالا، به همراه کند شدن شدید آن). اگر نمیخواهید این پوشه نمایش داده شود، در مسیر file->preferences->settings، عبارت npm را جستجو کرده و سپس در قسمت npm: exclude آن، بر روی لینک edit in settings.json کلیک کنید:
و سپس در فایل باز شده، یک چنین تنظیمی را میتوانید اضافه و یا ویرایش و تکمیل کنید:
"files.exclude": { "**/.git": true, "**/.svn": true, "**/.hg": true, "**/CVS": true, "**/.DS_Store": true, "**/node_modules": true, "**/wwwroot": true, "**/bower_components": true, "**/**/bin": true, "**/**/obj": true, "**/packages": true },
در ادامه پوشهی public این پروژه را مشاهده میکنید. تمام فایلهایی که قرار است به صورت عمومی توسط برنامه ارائه شوند، مانند favicon.ico و غیره، در این پوشه قرار میگیرند.
در این پوشه بر روی فایل index.html آن کلیک کنید تا بتوان محتوای آنرا بهتر بررسی کرد. برای مثال در ابتدای آن، درج تعدادی متادیتا را که یکی از آنها ذکر manifest.json است، مشاهده میکنید. کار فایل manifest.json، ارائهی یک سری متادیتای خاص مخصوص دستگاههای موبایل است که در آنها بجای favicon.ico، میتوان از تصاویر و یا آیکنهای بزرگتری مانند فایلهای png موجود در پوشهی public، استفاده کرد. در ادامهی این فایل، به تنظیم زیر میرسیم:
<body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div>
در پوشهی src و فایل App.js آن، شاهد یک کامپوننت ابتدایی هستید که کار رندر صفحهی مشکی پیشفرض این قالب را انجام میدهد. در این فایل، شاهد بازگشت یک چنین تگهایی هستیم:
return ( <div className="App"> <header className="App-header"> ... </header> </div> );
برای درک بهتر آن به آدرس https://babeljs.io/repl مراجعه کنید. سپس در سمت چپ صفحه، یک قطعه کد jsx را به یک ثابت انتساب دهید:
const element = <h1>Hello World!</h1>;
همانطور که مشاهده میکنید، این قطعه کد jsx (که یک رشتهی معمولی نیست)، توسط Babel به یک قطعه کد کاملا جاوا اسکریپتی قابل درک برای مرورگر تبدیل شدهاست:
"use strict"; var element = React.createElement("h1", null, "Hello World!");
بدیهی است نوشتن کدهای jsx، سادهتر از نوشتن قطعه کد فوق است و درک آن نیز به علت شباهت آن به HTML، آسانتر است. به همین جهت در کدهای React، ما از jsx استفاده میکنیم و تفسیر آنرا به Babel واگذار خواهیم کرد.
در پوشهی src، فایل مهم دیگری که وجود دارد، index.js است. این فایل نقطهی آغازین برنامه را مشخص میکند. در قسمتهای بعدی، محتویات این فایل را بیشتر بررسی خواهیم کرد.
در اینجا فایل serviceWorker.js را نیز مشاهده میکنید. این فایل به صورت خودکار توسط قالب create-react-app ایجاد شدهاست و کار آن کمک به ارائهی محلی برنامه، توسط development server آن است. بنابراین ما کاری با این فایل نخواهیم داشت.
نوشتن اولین برنامهی React
به پوشهی src ایجاد شده مراجعه کرده و تمام فایلهای موجود و پیشفرض آنرا حذف کنید. در ادامه خودمان آنها را از صفر ایجاد خواهیم کرد. برای این منظور فایل جدید و خالی src\index.js را ایجاد میکنیم. در ابتدای کار نیاز است تعدادی ماژول React را import کنیم.
import React from "react"; const element = <h1>Hello World!</h1>; console.log(element);
اگر هنوز برنامه توسط دستور npm start در حال اجرا است، هر بار که فایل index.js را ذخیره میکنیم، خروجی نهایی را در مرورگر نمایش میدهد (اگر هم آنرا بستهاید، یکبار از طریق خط فرمان، دستور npm start را در ریشهی پروژه، صادر کنید). به این قابلیت hot module reloading هم گفته میشود.
در این حالت اگر به مرورگر مراجعه کنید، یک صفحهی سفید را مشاهده خواهید کرد. اکنون دکمهی F12 را فشرده (و یا ctrl+shift+i) و developer console مرورگر را باز کنید.
شیءای را که در اینجا مشاهده میکنید، همان حاصل console.log کدهای فوق است؛ به عبارتی Babel، عبارت jsx ما را تبدیل به یک شیء جاوا اسکریپتی قابل فهم برای مرورگر کردهاست که از دیدگاه React، جزئی از همان Virtual DOM ای است که پیشتر معرفی شد (نمایش درون حافظهای DOM مختص React، جهت محاسبهی تغییرات، با تغییر state هر کامپوننت و سپس اعمال آنها به DOM اصلی در مرورگر).
اکنون میخواهیم این المان را در DOM اصلی، رندر کرده و نمایش دهیم:
import React from "react"; import ReactDOM from "react-dom"; const element = <h1>Hello World!</h1>; console.log(element); ReactDOM.render(element, document.getElementById("root"));
اکنون پس از ذخیره سازی فایل index.js، اگر به مرورگر مراجعه کنید، عبارت Hello World! را مشاهده خواهید کرد:
همانطور که در این تصویر نیز مشخص است، المان h1 ما را داخل div ای با id مساوی root، درج کردهاست.
هدف از این مثال ساده، نمایش نحوهی کارکرد React، در پشت صحنه بود. در یک برنامهی واقعی، بجای رندر یک المان ساده در DOM، کار رندر App component را انجام خواهیم داد. کامپوننت App، کامپوننت ریشهای برنامه بوده و میتواند شامل درختی از کامپوننتها که UI نهایی را تشکیل میدهند، شود.
نگاهی به تنظیمات پروژهی ایجاد شده
اگر فایل package.json پروژه را باز کنید، یک چنین بستههایی در آن درج شدهاست:
{ "name": "sample-01", "version": "0.1.0", "private": true, "dependencies": { "react": "^16.11.0", "react-dom": "^16.11.0", "react-scripts": "3.2.0" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } }
بستهی react-scripts است که کار مدیریت چهار جزء قسمت scripts این فایل را انجام میدهد. برای نمونه دستور npm start ای که در اینجا تعریف شده، سبب اجرای react-scripts start میشود. در ادامه اگر دستور npm run build را اجرا کنیم، یک بستهی نهایی بهینه سازی شده را تولید میکند.
آخرین دستور آن eject است. اگر دستور npm run eject را اجرا کنید، امکان سفارشی سازی پشت صحنهی create-react-app را خواهید داشت؛ اما در نهایت به یک فایل package.json بسیار شلوغ خواهیم رسید (اینبار ارجاعات به Babel، Webpack و تمام ابزارهای دیگر نیز ظاهر میشوند). همچنین این عملیات نیز یک طرفهاست. یعنی از این پس قرار است کنترل تمام این پشت صحنه، در اختیار ما باشد و به روز رسانیهای بعدی create-react-app را با مشکل مواجه میکند. این گزینه صرفا مختص توسعه دهندگان پیشرفتهی React است.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-01.zip
در قسمت بعد، پیشنیازهای جاوا اسکریپتی شروع به کار با React را بررسی میکنیم.