اشتراکها
در دوره #F این سایت (^) با نحوه کد نویسی و مفاهیم و مزایای این زبان آشنا شده اید. اما دانستن syntax یک زبان برای پیاده سازی یک پروژه کافی نیست و باید با تکنیکهای مهم دیگر از این زبان آشنا شویم. همان طور که قبلا (فصل اول دوره #F) بیان شد Visual Studio به صورت Visual از پروژههای #F پشتیبانی نمیکند. یعنی امکان ایجاد یک پروژه WPF یا Windows Application یا حتی پروژههای تحت وب برای این زبان همانند زبان #C به صورت Visual در VS.Net تعبیه نشده است. حال چه باید کرد؟ آیا باید در این مواقع این گونه پروژهها را با یک زبان دیگر نظیر #C ایجاد کنیم و از زبان #F در حل برخی مسائل محاسباتی و الگوریتمی استفاده کنیم. این اولین راه حلی است که به نظر میرسد. اما در حال حاضر افزونه هایی، توسط سایر تیمهای برنامه نویسی تهیه شده اند که پیاده سازی و اجرای یک پروژه تحت ویندوز یا وب را به صورت کامل با زبان #F امکان پذیر میکنند. در این پست به بررسی یک مثال از پروژه WPF به کمک این افزونهها میپردازیم.
نکته : آشنایی با کد نویسی و مفاهیم #F برای درک بهتر مطالب توصیه میشود.
معرفی پروژه FSharpX
پروژه FSharpx یک پروژه متن باز است که توسط یک تیم بسیار قوی از برنامه نویسان #F در حال توسعه میباشد. این پروژه شامل چندین زیر پروژه و بخش است که هر بخش آن برای یکی از مباحث دات نت در #F تهیه و توسعه داده میشود.نکته : آشنایی با کد نویسی و مفاهیم #F برای درک بهتر مطالب توصیه میشود.
معرفی پروژه FSharpX
این قسمتها عبارتند از :
FSharpx.Core : شامل مجموعه ای کامل از توابع عمومی، پرکاربرد و ساختاری است که برای این زبان توسعه داده شده اند و با تمام زبانهای دات نت سازگاری دارند؛
FSharpx.Http : استفاده از #F در برنامه نویسی مدل Http؛
FSharpx.TypeProvider : این پروژه خود شامل چندین بخش است که در این جا چند مورد از آنها را عنوان میکنم:
- FSharpx.TypeProviders.AppSetting : متد خواندن و نوشتن (setter و getter) را برای فایلهای تنظیمان پروژه (Application Setting File) فراهم میکند.
- FSharpx.TypeProviders.Vector : برای محاسبات با ساختارهای برداری استفاده میشود.
- FSharpx.TypeProviders.Machine : برای دسترسی و اعمال تغییرات در رجیستری و فایلهای سیستمی استفاده میشود.
- FSharpx.TypeProviders.Xaml : با استفاده از این افزونه میتوانیم از فایلهای Xaml، در پروژههای #F استفاده کنیم و WPF Designer نرم افزار VS.Net هم برای این زبان قابل استفاده خواهد شد.
- FSharpx.TypeProviders.Regex : امکان استفاده از عبارات با قاعده را در این پروژه فراهم میکند.
یک مثال از عبارات با قاعده:
type PhoneRegex = Regex< @"(?<AreaCode>^\d{3})-(?<PhoneNumber>\d{3}-\d{4}$)"> PhoneRegex.IsMatch "425-123-2345" |> should equal true PhoneRegex().Match("425-123-2345").CompleteMatch.Value |> should equal "425-123-2345" PhoneRegex().Match("425-123-2345").PhoneNumber.Value |> should equal "123-2345"
ایتدا یک پروژه از نوع F# Console Application ایجاد کنید. از قسمت Project Properties (بر روی پروژه کلیک راست کنید و گزینه Properties را انتخاب کنید) نوع پروژه را به Windows Application تغییر دهید(قسمت Out Put Type). اسمبلیهای زیر را به پروژه ارجاع دهید:
- PresentationCore
- PresentationFramework
- WindowBase
- System.Xaml
با استفاده از پنجره Package Manager Console دستور نصب زیر را اجرا کنید(آخرین نسخه این پکیج 1.8.31 و حجم آن کمتر از یک مگابایت است):
PM> Install-Package FSharpx.TypeProviders.Xaml
حال یک فایل Xaml به پروژه اضافه کنید و کدهای زیر را در آن کپی کنید:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF F# Sample By Masoud Pakdel" Height="350" Width="525"> <Grid Name="MainGrid"> <StackPanel Name="StackPanel1" Margin="50"> <Button Name="Button1">Who are you?</Button> </StackPanel> </Grid> </Window>
open System open System.Windows open System.Windows.Controls open FSharpx type MainWindow = XAML<"MainWindow.xaml"> let loadWindow() = let window = MainWindow() window.Button1.Click.Add(fun _ -> MessageBox.Show("Masoud Pakdel") |> ignore) window.Root [<STAThread>] (new Application()).Run(loadWindow()) |> ignore
در تابع loadWindow یک نمونه از کلاس MainWindow ساخته میشود و برای button1 آن رویداد کلیک تعریف میکنیم. دستورات زیر معادل دستورات شروع برنامه در فایل program پروژههای #C است.
[<STAThread>] (new Application()).Run(loadWindow()) |> ignore
پروژه را اجرا کنید و بر روی تنهای Button موجود در صفحه، کلیک کنید و پیغام مورد نظر را مشاهده خواهید کرد. به صورت زیر:
مدیریت پردازش آپلود فایلها در ASP.NET Core نسبت به ASP.NET MVC 5.x به طور کامل تغییر کردهاست و اینبار بجای ذکر نوع System.Web.HttpPostedFileBase باید از اینترفیس جدید IFormFile واقع در فضای نام Microsoft.AspNetCore.Http کمک گرفت.
مراحل فعال سازی آپلود فایلها در ASP.NET Core
مرحلهی اول فعال سازی آپلود فایلها در ASP.NET Core، شامل افزودن ویژگی "enctype="multipart/form-data به یک فرم تعریف شدهاست:
در اینجا همچنین ذکر ویژگی multiple در input از نوع file، امکان ارسال چندین فایل با هم را نیز میسر میکند.
در سمت سرور، امضای اکشن متد دریافت کنندهی این فایلها به صورت ذیل خواهد بود:
در اینجا نام پارامتر تعریف شده، باید دقیقا مساوی نام input از نوع file باشد. همچنین از آنجائیکه ویژگی multiple را نیز در سمت کلاینت قید کردهایم، این پارامتر سمت سرور از نوع یک لیست، تعریف شدهاست. اگر ویژگی multiple را حذف کنیم میتوان آنرا به صورت سادهی IFormFile files نیز تعریف کرد.
یافتن جایگزینی برای Server.MapPath در ASP.NET Core
زمانیکه فایل ارسالی، در سمت سرور دریافت شد، مرحلهی بعد، ذخیره سازی آن بر روی سرور است و از آنجائیکه ما دقیقا نمیدانیم ریشهی سایت در کدام پوشهی سرور واقع شدهاست، میشد از متد Server.MapPath برای یافتن دقیق آن کمک گرفت. با حذف این متد در ASP.NET Core، روش یافتن ریشهی سایت یا همان پوشهی wwwroot در اینجا شامل مراحل ذیل است:
ابتدا اینترفیس توکار IHostingEnvironment را در سازندهی کلاس تزریق میکنیم. سرویس HostingEnvironment جزو سرویسهای از پیش تعریف شدهی ASP.NET Core است و نیازی به تنظیمات اضافهتری ندارد. همینقدر که ذکر شود، به صورت خودکار توسط ASP.NET Core مقدار دهی و تامین میگردد.
پس از آن خاصیت environment.WebRootPath_ به ریشهی پوشهی wwwroot برنامه، بر روی سرور اشاره میکند. به این ترتیب میتوان مسیر دقیقی را جهت ذخیره سازی فایلهای رسیده، مشخص کرد.
امکان ذخیره سازی async فایلها در ASP.NET Core
عملیات کار با فایلها، عملیاتی است که از مرزهای IO سیستم عبور میکند. به همین جهت یکی از بهترین مثالهای پیاده سازی async، جهت رها سازی تردهای برنامه و بالا بردن میزان پاسخدهی آن با بالا بردن تعداد تردهای آزاد بیشتر است. در ASP.NET Core، نوشتن async محتوای فایل رسیده در یک stream پشتیبانی میشود و این stream میتواند یک FileStream و یا MemoryStream باشد. در ذیل نحوهی کار async با یک FileStream را مشاهده میکنید:
در اینجا کدهای کامل متد دریافت فایلها را در سمت سرور مشاهده میکنید. ابتدا با استفاده از خاصیت environment.WebRootPath_، به مسیر ریشهی wwwroot دسترسی و سپس پوشهی uploads را در آن جهت ذخیره سازی فایلهای دریافتی، تعیین کردهایم.
چون برنامههای ASP.NET Core قابلیت اجرای بر روی لینوکس را نیز دارند، تا حد امکان باید از Path.Combine جهت جمع زدن اجزای مختلف یک میسر، استفاده کرد. از این جهت که در لینوکس، جداکنندهی اجزای مسیرها، / است بجای \ در ویندوز و متد Path.Combine به صورت خودکار این مسایل را لحاظ خواهد کرد.
در آخر با استفاده از متد file.CopyToAsync کار نوشتن غیرهمزمان محتوای فایل دریافتی در یک FileStream انجام میشود؛ به همین جهت در امضای متد فوق، <async Task<IActionResult را نیز ملاحظه میکنید.
پشتیبانی کامل از Model Binding آپلود فایلها در ASP.NET Core
در ASP.NET MVC 5.x اگر ویژگی Required را بر روی یک خاصیت از نوع HttpPostedFileBase قرار دهید ... کار نمیکند و در سمت کلاینت تاثیری را به همراه نخواهد داشت؛ مگر اینکه تنظیمات سمت کلاینت آنرا به صورت دستی انجام دهیم. این مشکلات در ASP.NET Core، کاملا برطرف شدهاند:
در اینجا یک خاصیت از نوع IFormFile، با دو ویژگی Required و DataType خاص آن در یک ViewModel تعریف شدهاند. فرم معادل آن در ASP.NET Core به صورت ذیل خواهد بود:
در اینجا ابتدا نوع مدل View تعیین شدهاست و سپس با استفاده از Tag Helpers، صرفا یک input را به خاصیت Photo مدل View جاری متصل کردهایم. همین اتصال سبب فعال سازی مباحث اعتبارسنجی سمت سرور و کاربر نیز میشود.
اینبار جهت فعال سازی و استفادهی از قابلیتهای Model Binding میتوان از ModelState نیز بهره گرفت:
اگر ModelState معتبر باشد، کار ذخیره سازی تک فایل رسیده را انجام میدهیم. سایر نکات این متد، با اکشن متد Index که پیشتر بررسی شد، یکی هستند.
بررسی پسوند فایلهای رسیدهی به سرور
ASP.NET Core دارای ویژگی است به نام FileExtensions که ... هیچ ارتباطی به خاصیتهایی از نوع IFormFile ندارد:
ویژگی FileExtensions صرفا جهت درج بر روی خواصی از نوع string طراحی شدهاست. بنابراین قرار دادن این ویژگی بر روی خاصیتهایی از نوع IFormFile، سبب فعال سازی اعتبارسنجی سمت سرور پسوندهای فایلهای رسیده، نخواهد شد.
در ادامه جهت بررسی پسوندهای فایلهای رسیده، میتوان یک ویژگی اعتبارسنجی سمت سرور جدید را طراحی کرد:
در اینجا با ارث بری از کلاس پایه ValidationAttribute و بازنویسی متد IsValid آن، کار اعتبارسنجی پسوند فایلها و یا فایل رسیده را انجام دادهایم. این ویژگی جدید اگر بر روی خاصیتی از نوع IFormFile قرار بگیرد، پارامتر object value متد IsValid آن حاوی اطلاعات فایل و یا فایلهای رسیده، خواهد بود. بر این اساس میتوان تصمیم گیری کرد که آیا پسوند این فایل، مجاز است یا خیر.
در اینجا روش استفادهی از این ویژگی اعتبارسنجی جدید را نیز با تکمیل ViewModel کاربر، مشاهده میکنید. پس از آن تنها بررسی if (ModelState.IsValid) در یک اکشن متد، نتیجهی دریافتی از اعتبارسنج جدید UploadFileExtensions را در اختیار ما قرار میدهد و بر این اساس میتوان تصمیمگیری کرد که آیا باید فایل رسیده را ذخیره کرد یا خیر.
مراحل فعال سازی آپلود فایلها در ASP.NET Core
مرحلهی اول فعال سازی آپلود فایلها در ASP.NET Core، شامل افزودن ویژگی "enctype="multipart/form-data به یک فرم تعریف شدهاست:
<form method="post" asp-action="Index" asp-controller="TestFileUpload" enctype="multipart/form-data"> <input type="file" name="files" multiple /> <input type="submit" value="Upload" /> </form>
در سمت سرور، امضای اکشن متد دریافت کنندهی این فایلها به صورت ذیل خواهد بود:
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Index(IList<IFormFile> files)
یافتن جایگزینی برای Server.MapPath در ASP.NET Core
زمانیکه فایل ارسالی، در سمت سرور دریافت شد، مرحلهی بعد، ذخیره سازی آن بر روی سرور است و از آنجائیکه ما دقیقا نمیدانیم ریشهی سایت در کدام پوشهی سرور واقع شدهاست، میشد از متد Server.MapPath برای یافتن دقیق آن کمک گرفت. با حذف این متد در ASP.NET Core، روش یافتن ریشهی سایت یا همان پوشهی wwwroot در اینجا شامل مراحل ذیل است:
public class TestFileUploadController : Controller { private readonly IHostingEnvironment _environment; public TestFileUploadController(IHostingEnvironment environment) { _environment = environment; }
پس از آن خاصیت environment.WebRootPath_ به ریشهی پوشهی wwwroot برنامه، بر روی سرور اشاره میکند. به این ترتیب میتوان مسیر دقیقی را جهت ذخیره سازی فایلهای رسیده، مشخص کرد.
امکان ذخیره سازی async فایلها در ASP.NET Core
عملیات کار با فایلها، عملیاتی است که از مرزهای IO سیستم عبور میکند. به همین جهت یکی از بهترین مثالهای پیاده سازی async، جهت رها سازی تردهای برنامه و بالا بردن میزان پاسخدهی آن با بالا بردن تعداد تردهای آزاد بیشتر است. در ASP.NET Core، نوشتن async محتوای فایل رسیده در یک stream پشتیبانی میشود و این stream میتواند یک FileStream و یا MemoryStream باشد. در ذیل نحوهی کار async با یک FileStream را مشاهده میکنید:
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Index(IList<IFormFile> files) { var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads"); if (!Directory.Exists(uploadsRootFolder)) { Directory.CreateDirectory(uploadsRootFolder); } foreach (var file in files) { if (file == null || file.Length == 0) { continue; } var filePath = Path.Combine(uploadsRootFolder, file.FileName); using (var fileStream = new FileStream(filePath, FileMode.Create)) { await file.CopyToAsync(fileStream).ConfigureAwait(false); } } return View(); }
چون برنامههای ASP.NET Core قابلیت اجرای بر روی لینوکس را نیز دارند، تا حد امکان باید از Path.Combine جهت جمع زدن اجزای مختلف یک میسر، استفاده کرد. از این جهت که در لینوکس، جداکنندهی اجزای مسیرها، / است بجای \ در ویندوز و متد Path.Combine به صورت خودکار این مسایل را لحاظ خواهد کرد.
در آخر با استفاده از متد file.CopyToAsync کار نوشتن غیرهمزمان محتوای فایل دریافتی در یک FileStream انجام میشود؛ به همین جهت در امضای متد فوق، <async Task<IActionResult را نیز ملاحظه میکنید.
پشتیبانی کامل از Model Binding آپلود فایلها در ASP.NET Core
در ASP.NET MVC 5.x اگر ویژگی Required را بر روی یک خاصیت از نوع HttpPostedFileBase قرار دهید ... کار نمیکند و در سمت کلاینت تاثیری را به همراه نخواهد داشت؛ مگر اینکه تنظیمات سمت کلاینت آنرا به صورت دستی انجام دهیم. این مشکلات در ASP.NET Core، کاملا برطرف شدهاند:
public class UserViewModel { [Required(ErrorMessage = "Please select a file.")] [DataType(DataType.Upload)] public IFormFile Photo { get; set; } }
@model UserViewModel <form method="post" asp-action="UploadPhoto" asp-controller="TestFileUpload" enctype="multipart/form-data"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <input asp-for="Photo" /> <span asp-validation-for="Photo" class="text-danger"></span> <input type="submit" value="Upload"/> </form>
اینبار جهت فعال سازی و استفادهی از قابلیتهای Model Binding میتوان از ModelState نیز بهره گرفت:
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> UploadPhoto(UserViewModel userViewModel) { if (ModelState.IsValid) { var formFile = userViewModel.Photo; if (formFile == null || formFile.Length == 0) { ModelState.AddModelError("", "Uploaded file is empty or null."); return View(viewName: "Index"); } var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads"); if (!Directory.Exists(uploadsRootFolder)) { Directory.CreateDirectory(uploadsRootFolder); } var filePath = Path.Combine(uploadsRootFolder, formFile.FileName); using (var fileStream = new FileStream(filePath, FileMode.Create)) { await formFile.CopyToAsync(fileStream).ConfigureAwait(false); } RedirectToAction("Index"); } return View(viewName: "Index"); }
بررسی پسوند فایلهای رسیدهی به سرور
ASP.NET Core دارای ویژگی است به نام FileExtensions که ... هیچ ارتباطی به خاصیتهایی از نوع IFormFile ندارد:
[FileExtensions(Extensions = ".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")]
در ادامه جهت بررسی پسوندهای فایلهای رسیده، میتوان یک ویژگی اعتبارسنجی سمت سرور جدید را طراحی کرد:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public class UploadFileExtensionsAttribute : ValidationAttribute { private readonly IList<string> _allowedExtensions; public UploadFileExtensionsAttribute(string fileExtensions) { _allowedExtensions = fileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); } public override bool IsValid(object value) { var file = value as IFormFile; if (file != null) { return isValidFile(file); } var files = value as IList<IFormFile>; if (files == null) { return false; } foreach (var postedFile in files) { if (!isValidFile(postedFile)) return false; } return true; } private bool isValidFile(IFormFile file) { if (file == null || file.Length == 0) { return false; } var fileExtension = Path.GetExtension(file.FileName); return !string.IsNullOrWhiteSpace(fileExtension) && _allowedExtensions.Any(ext => fileExtension.Equals(ext, StringComparison.OrdinalIgnoreCase)); } }
public class UserViewModel { [Required(ErrorMessage = "Please select a file.")] //`FileExtensions` needs to be applied to a string property. It doesn't work on IFormFile properties, and definitely not on IEnumerable<IFormFile> properties. //[FileExtensions(Extensions = ".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")] [UploadFileExtensions(".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")] [DataType(DataType.Upload)] public IFormFile Photo { get; set; } }
اشتراکها
دریافت کتاب Node.js in Action
جاوا اسکریپت سمت سرور؟ Node.js جاوا اسکریپت سمت سرور است که توانایی پشتیبانی از برنامههای تحت وب مقیاس پذیر و عملکرد بالا را دارد. با استفاده از I/O ناهمگام، سرور توانایی انجام بیش از یک کار در یک لحظه را دارد که این یک احتیاج کلیدی برای برنامههای بلادرنگ تحت وب به مانند چت، بازیها و آمارهای لحظه به لحظه است. همچنین چون زبان برنامه نویسی جاوا اسکریپت است، شما تا انتها هم از همین زبان برای برنامه نویسی استفاده میکنید.
مسیرراهها
آموزش زبان Rust
- زبان برنامه نویسی Rust چیست و چرا باید از آن استفاده کنیم؟
- نصب Rust و ایجاد یک پروژهی جدید
- متغیرها در زبان برنامه نویسی Rust
- انواع دادهها
- Constants and Statics در Rust
- Fuctions
- Control Flow
- Rust-Based CS Masterclass
- Memory Management در Rust از طریق Ownership
- مفهوم Borrowing در Rust
- Structs
- Implementation Blocks
- Enum
نظرات نظرسنجیها
اگر بخواهید کنار دات نت بر روی یک پلتفرم یا زبان دیگری نیز کار کنید کدام را انتخاب می کنید؟
خیلی از همسنای من (لااقل) برنامه نویسی رو با پاسکال و سی++ شروع کردن، و مطمئنا هم دوسش دارن، منتها به خاطر سختی مدیریت حافظه و درگیریهای زیادی که توی کار با اشاره گرها وجود داره از سی یا سی++ فاصله گرفتن. من فکر نمیکردم کسی که برنامه نویسی روی زبانهای تا این حد انتزاعی انجام میده دیگه حوصله کنه و برگرده به سی++، با اینحال QT رو به این دلیل نوشتم که در شبکههای اجتماعی مختلف دوستانی رو دیدم که باهاش کار میکنن و پروژههای خوبی زدن.
با استفاده از React Hooks که در نگارش 16.7.0 آن معرفی شدند، میتوان در کامپوننتهای تابعی «تا پیش از این» بدون حالت، به state و تمام قابلیتهای دیگر React، دسترسی یافت. جهت یادآوری، در قسمت 8 این سری، کامپوننتهای تابعی بدون حالت را معرفی کردیم. تا پیش از معرفی React Hooks، برای ردیابی تغییرات مقادیری خاص، میبایستی آنها را در خاصیت state کامپوننتهایی که به صورت کلاس تعریف شده بودند، قرار میدادیم. بنابراین class components، تنها نوع کامپوننتهایی در React بودند که دسترسی به state را داشتند. شبیه به همین مورد، برای life-cycle hooks معرفی شدهی در قسمت 9 برقرار بود. برای مثال متد componentDidMount، تنها در class components، جهت دسترسی به یک API خارجی و انجام اعمال Ajax ای، قابل تعریف بود و کار کامپوننتهای تابعی بدون حالت، بیشتر نمایش عناصر HTML و دریافت مقادیر خود از class components، توسط props بود. به این ترتیب امکان تجزیهی یک UI پیچیده، به یک component tree با خوانایی بیشتری، میسر میشد. اما با معرفی React v16.7.0، از لحاظ فنی دیگر الزاما نیازی به class components وجود ندارد و میتوان با استفاده از React Hooks، تمام قابلیتهایی را که پیشتر فقط توسط class components در اختیار داشتیم، اکنون با کامپوننتهای تابعی نیز پیاده سازی کنیم.
برپایی پیشنیازها
در اینجا برای بررسی React Hooks، یک پروژهی جدید React را ایجاد میکنیم:
در ادامه توئیتر بوت استرپ 4 را نیز نصب میکنیم. برای این منظور پس از باز کردن پوشهی اصلی برنامه توسط VSCode، دکمههای ctrl+` را فشرده (ctrl+back-tick) و دستور زیر را در ترمینال ظاهر شده وارد کنید:
سپس برای افزودن فایل bootstrap.css به پروژهی React خود، ابتدای فایل index.js را به نحو زیر ویرایش خواهیم کرد:
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام میدهد، مورد استفاده قرار میگیرد.
همچنین اگر به فایل package.json موجود در ریشهی پروژه دقت کنیم، برای کار با React-hooks، نیاز است نگارش بستههای React و React-dom، حداقل مساوی 16.7 باشند که در زمان نگارش این مطلب، نگارش 16.12.0 آن به صورت خودکار نصب میشود. بنابراین بدون مشکلی میتوانیم شروع به کار با React hooks کنیم.
معرفی useState Hook
در اینجا قصد داریم یک شمارشگر را به همراه یک دکمه، در صفحه نمایش دهیم؛ بطوریکه این شمارشگر، تعداد بار کلیک بر روی دکمه را ردیابی میکند. از پیش میدانیم که برای ردیابی مقدار تعداد بار کلیک شدن، باید متغیر آنرا درون state یک class component قرار داد:
در اینجا یک class component متداول را داریم که در آن دکمهای تعریف شدهاست. سپس برای شمارش تعداد بار کلیک بر روی آن، خاصیت count را به شیء منتسب به state، با مقدار اولیهی صفر، اضافه کردهایم. اکنون هربار که بر روی آن کلیک میشود، رویدادگردان incrementCount فراخوانی شده و توسط متد this.setState، مقدار پیشین خاصیت count، بر اساس مقدار فعلی آن، یک واحد افزایش مییابد. نتیجهی آن نیز در برچسب دکمه، نمایش داده میشود:
اکنون میخواهیم همین کامپوننت را توسط React hooks بازنویسی کنیم. در ابتدا، فایل app.js را به AppClass.js، تغییر نام میدهیم تا نگارش قبلی class component را برای مقایسه، در اختیار داشته باشیم. در ادامه فایل جدید AppFunction.js را برای بازنویسی آن توسط یک کامپوننت تابعی، توسط میانبرهای imrc و سپس sfc در VSCode، ایجاد میکنیم. البته این تغییر نام فایلها، نیاز به تغییر نام ماژولهای import شدهی در فایل index.js را نیز به صورت زیر دارد:
اولین سؤالی که اینجا مطرح میشود، این است: در این کامپوننت تابعی جدید، state را از کجا بدست بیاوریم؟
با React Hooks، بجای تعریف یک state به صورت خاصیت، آنرا صرفا use میکنیم و این دقیقا نام اولین React Hooks ای است که بررسی میکنیم؛ یا همان useState. بنابراین ابتدا این شیء را import خواهیم کرد:
useState یک تابع است و باید در ابتدای کامپوننت، مورد استفاده قرار گیرد. این متد برای شروع به کار، نیاز به یک state آغازین را دارد؛ دقیقا مانند همانکاری که در class component فوق انجام دادیم:
در اینجا عدد صفر، همان مقدار آغازین متغیر count است (شبیه به کاری که در state = { count: 0 } انجام دادیم). سپس اولین خروجی متد useState که داخل یک آرایه مشخص شدهاست (JavaScript array destructuring ؛ با مزیت امکان انتخاب نامهایی دلخواه، بدون نیاز به تعریف alias، برخلاف حالت object destructuring)، همان متغیر count است که توسط state ردیابی خواهد شد. اینبار بجای متد this.setState قبلی که یک متد عمومی بود، متدی اختصاصی را صرفا جهت تغییر مقدار همین متغیر count، به نام setCount به عنوان دومین خروجی متد useState، تعریف میکنیم. در حقیقت تا اینجا امضای متد جنریک useState استفاده شده، به صورت زیر تغییر کردهاست:
متد useState، یک initialState را دریافت میکند و سپس یک عدد را (در اینجا چون مقدار آغازین، عددی است)، به همراه یک متد، جهت تغییر state آن، بازگشت میدهد:
- پس از تعریف useState، متد رویدادگردان onClick را همانند قبل تعریف میکنیم؛ با یک تغییر مهم: چون این متد داخل یک کامپوننت تابعی تعریف شدهاست، باید با const شروع شود و همانند یک متغیر که به آن متدی انتساب داده شده، معرفی گردد. پیشتر incrementCount تعریف شدهی داخل یک class component، یک خاصیت بود که متدی به آن انتساب داده شده بود.
- همچنین در اینجا (داخل این متد) دیگر خبری از thisها نیست؛ onClick، مستقیما به متغیر incrementCount اشاره میکند و {count} نیز مستقیما از خروجی useState، که به مقدار جاری count اشاره میکند، تامین میشود.
- اکنون با هربار کلیک بر روی این دکمه، متد منتسب به متغیر incrementCount فراخوانی شده و در داخل آن، همان متد setCount را جهت به روز رسانی state، فراخوانی میکنیم (بجای فراخوانی this.setState عمومی قبلی). در اینجا ابتدا مقدار جاری متغیر count در state، دریافت شده و سپس یک واحد به آن اضافه میشود. امضای متد جنریک setCount به صورت زیر است:
استفاده از مقدار قبلی state توسط useState
زمانیکه متد this.setState فراخوانی میشود، اینکار سبب در صف قرار گرفتن رندر مجدد کامپوننت جاری خواهد شد. همچنین اعمال این متد نیز ممکن است در صف قرار گیرد. یعنی اگر پس از فراخوانی this.setState، سعی در خواندن state به روز شده را داشته باشیم، ممکن است مقدار اشتباهی را دریافت کنیم:
برای نمونه در فراخوانی فوق، منظور از this.state.count، مقدار جاری یا همان مقدار قبلی count است که قرار است یک واحد به آن اضافه شود. اما چون متد this.setState کار به روز رسانیهای state را نیز در صف قرار میدهد، دفعهی بعدی که بر روی آن کلیک شد، الزامی ندارد که this.state.count، حتما در همان لحظه به روز رسانی شده باشد. برای رفع این مشکل میتوان نوشت:
prevState در اینجا یعنی state قبلی و توسط متد setState، به صورت یک arrow function قابل دریافت است که در نهایت یک شیء را باز میگرداند. اکنون بر اساس state قبلی میتوان به روز رسانی دقیقی را انجام داد.
این نکته در مورد کامپوننتهای تابعی نیز وجود دارد:
در اینجا متغیر count، همانند خاصیت this.state.count کامپوننتهای کلاسی عمل میکند. بنابراین الزامی ندارد که با هر بار فراخوانی setCount، مقدار جاری متغیر count، دقیقا به مقدار قبلی تنظیم شدهی آن اشاره کند. برای انجام مشابه اینکار با کامپوننتهای تابعی که از useState استفاده میکنند، میتوان به صورت زیر عمل کرد:
در اینجا نیز امکان دسترسی به مقدار قبلی count، توسط یک arrow function وجود دارد که برخلاف حالت prevState، فقط یک مقدار عددی مرتبط با count را باز میگرداند.
به روز رسانی بیش از یک خاصیت در state
فرض کنید قصد داریم به مثال جاری، یک مربع را در صفحه اضافه کنیم که با کلیک بر روی آن، رنگش تغییر میکند (خاموش و روشن میشود):
در حالت AppClass یا کامپوننت کلاسی، کدهای برنامه به صورت زیر تغییر میکنند:
توضیحات:
- در متد رندر، نیاز است تا تنها یک child، بازگشت داده شود. به همین جهت میتوان از React.Fragment، برای محصور سازی المانهای تعریف شده، استفاده کرد. البته در React 16.7.0، دیگر نیازی به ذکر صریح React.Fragment نبوده و فقط میتوان نوشت </><> تا بیانگر یک فرگمنت باشد.
- سپس یک div تعریف شده که با استفاده از ویژگی style، یک سری شیوهنامهی ابتدایی، مانند طول و عرض و نوع اشارهگر ماوس آن، تعیین شدهاند.
- اکنون برای اینکه بتوان با کلیک بر روی این div، رنگ آنرا تغییر داد، نیاز است بتوان توسط متغیری، مقدار خاموش و روشن بودن را ردیابی کرد. به همین جهت خاصیت جدید isOn را به state اضافه میکنیم.
- در آخر، رویداد onClick این div را به متد رویدادگران toggleLight متصل میکنیم تا توسط آن و فراخوانی this.setState، بتوان مقدار قبلی isOn را در state، دریافت و سپس آنرا معکوس کرد و بجای مقدار جاری isOn در state درج کنیم. این فراخوانی، سبب رندر مجدد کامپوننت جاری شده و در نتیجهی آن، مقدار className را بر اساس مقدار this.state.isOn، به صورت پویا تغییر میدهد.
برای مشاهدهی خروجی برنامه در این حالت، نیاز است به index.js مراجعه و تغییر زیر را اعمال کرد تا کامپوننت App، از ماژول AppClass تامین شود:
اکنون قصد داریم دقیقا معادل همین قطعه کد را در کامپوننت تابعی خود پیاده سازی کنیم. به همین جهت به فایل src\AppFunction.js بازگشته و تغییرات زیر را اعمال میکنیم:
توضیحات:
- اگر دقت کنید، کلیات این کامپوننت تابعی، با کامپوننت کلاسی، آنچنان متفاوت نیست. متد رندر آن دقیقا همان markup را بازگشت میدهد؛ با یک تفاوت مهم: در اینجا دیگر نیازی به ذکر thisها نیست، چون تمام ارجاعات، به متغیرهای داخل تابع App انجام شدهاست و نه به متدها و یا خاصیتهای یک کلاس.
- مرحلهی بعد تغییر، جایگزینی this.state.isOn قبلی، با یک متغیر درون تابع App است. به همین جهت در اینجا یک useState دیگر را تعریف میکنیم. هر useState، تنها به قسمتی از state اشاره میکند و مانند خاصیت کلی state مربوط به یک کلاس نیست. همچنین در خاصیت state یک کلاس، مقدار آن همواره به یک شیء اشاره میکند؛ اما با useState چنین اجباری وجود ندارد و میتواند هر نوع مجاز جاوا اسکریپتی باشد. برای مثال در اینجا مقدار اولیهی آن به false تنظیم شدهاست. پس از آن، خروجی این متد، قسمتی از state را که کنترل میکند، به نام متغیر isOn و تنظیم کنندهی آنرا به نام متد setIsOn، معرفی خواهد کرد. متد useState، یک متد جنریک است. یعنی نوع خروجیهای آن بر اساس نوع مقدار اولیهای که به آن انتساب داده میشود، تعیین میشود. برای مثال اگر نوع مقدار اولیهی آن، Boolean باشد، به صورت خودکار نوع متغیر و پارامتر متد خروجی از آن نیز Boolean خواهند بود.
- در آخر خاصیت toggleLight کلاس کامپوننت، تبدیل به یک متغیر یا ثابت، در تابع جاری App میشود و بجای this.setState کلی قبلی، از متد اختصاصیتر setIsOn، برای تغییر مقدار state متناظر، کمک گرفته خواهد شد. در اینجا با استفاده از prevIsOn، به مقدار دقیق پیشین متغیر isOn در state، دسترسی یافته و سپس آنرا معکوس میکنیم.
برای مشاهدهی خروجی برنامه در این حالت، نیاز است به index.js مراجعه و تغییر زیر را اعمال کرد تا کامپوننت App، از ماژول AppFunction تامین شود:
معرفی useEffect Hook
فرض کنید قصد داریم برچسب دکمهی «I was clicked {this.state.count} times» را در المان Title صفحه، نمایش دهیم. برای انجام چنین کاری نیاز است با DOM API تعامل داشته باشیم؛ اما پیشتر یک چنین کاری را تنها با class components میشد انجام داد. برای رفع این محدودیت در کامپوننتهای تابعی، hook جدیدی به نام useEffect، ارائه شدهاست که باید import شود:
اکنون این سؤال مطرح میشود که در متد useEffect، واژهی Effect به چه چیزی اشاره میکند؟
در اینجا effect به معنای side effect و یا اثرات جانبی است؛ مانند: تعامل با یک API خارجی، کار با API مرورگر و کلا هر جائیکه در برنامه با دنیای خارج تعاملی وجود دارد، به عنوان یک side effect شناخته میشود. بنابراین با استفاده متد useEffect، میتوان در کامپوننتهای تابعی نیز با دنیای خارج، تعامل برقرار کرد.
متد useEffect، یک تابع را به عنوان ورودی دریافت کرده (effect function) و آنرا پس از هر بار رندر کامپوننت جاری، اجرا میکند. برای مثال با تغییر state، کار رندر کامپوننت جاری، در صف قرار میگیرد و پس از اتمام رندر، تابع effect ذکر شده، اجرا میشود. برای مثال در اینجا پس از هر بار رندر کامپوننت، document.title با عبارتی که به همراه تعداد بار کلیک کردن بر روی دکمهاست، به روز رسانی میشود:
در اولین بار اجرای برنامه، عبارت «You have clicked 0 times»، در عنوان صفحهی جاری، ظاهر میشود که از مقدار پیشفرض count، استفاده کردهاست. اکنون اگر بر روی دکمه، کلیک کنیم، پس از تغییر برچسب دکمه (یا همان رندر کامپوننت، پس از تغییری در state)، آنگاه عنوان نمایش داده شدهی در مرورگر نیز تغییر میکند.
یک نکته: چون useEffect دارای همان scope متغیر count است، نیاز به API خاصی برای خواندن مقدار آن در اینجا نیست.
سؤال: برای پیاده سازی چنین قابلیتی در یک کامپوننت کلاسی چه باید کرد؟ در این مثال، در ابتدای کار باید مقدار پیشفرض موجود در state را در عنوان صفحه مشاهده کرد و پس از هربار به روز رسانی state نیز باید این عنوان، تغییر کند.
برای پیاده سازی معادل متد useEffect ای که در یک کامپوننت تابعی استفاده شد، در اینجا باید از دو life-cycle hook متفاوت، به نامهای componentDidMount و componentDidUpdate، استفاده کنیم:
- برای به روز رسانی عنوان صفحه، در اولین بار نمایش آن، باید از متد componentDidMount استفاده کرد.
- همچنین چون میخواهیم به ازای هر تغییری در state نیز این عنوان تغییر کند (با هر بار کلیک بر روی دکمه)، باید از متد componentDidUpdate هم استفاده کنیم.
پاکسازی اثرات جانبی در useEffect Hook
فرض کنید قصد داریم موقعیت فعلی کرسر ماوس را در مرورگر نمایش دهیم. برای انجام اینکار در کامپوننت کلاسی، میتوان از متد componentDidMount جهت دسترسی به DOM API و استفاده از متد addEventListener آن، برای گوش فرا دادن به حرکات کرسر ماوس، استفاده کرد:
دومین پارامتر این متد، یک callback function است که این حرکات را ردیابی میکند:
در اینجا میتوان با استفاده از شیء رخداد دریافتی، موقعیت x و y کرسر ماوس را در صفحه دریافت کرد و سپس با فراخوانی متد this.setState، این خواص را در state کامپوننت، اضافه و یا به روز رسانی نمود. بنابراین در این حالت الزامی به تعریف این خواص در شیء منتسب به state نیست؛ اما حداقل با تعریف آنها میتوان مقدار اولیهای را برایشان درنظر گرفت:
در آخر برای نمایش این اطلاعات موجود در state، میتوان چنین المانهایی را به متد رندر کامپوننت، اضافه کرد:
بنابراین ردیابی تغییرات محل کرسر ماوس نیز یک side effect است که برای دسترسی به آن، نیاز است با window API کار کرد و این side effectها دو نوع هستند:
- تعدادی از آنها نیازی به پاکسازی و خارج شدن از حافظه را ندارند؛ مانند به روز رسانی عنوان صفحه در مرورگر. میتوان یک چنین side effect هایی را اجرا و سپس آنها را فراموش کرد.
- اما تعدادی از side effectها را حتما باید پاکسازی کرد؛ مانند mousemove listener تعریف شدهی در مثال فوق. در اینجا زمانیکه کامپوننت جاری mount میشود، این listener را تعریف میکنیم؛ اما با Unmount شدن آن، باید این listener را حذف کرد تا برنامه دچار نشتی حافظه نشود (اگر اینکار انجام نشود، در این مثال مرورگر هنگ خواهد کرد!). روش انجام اینکار در متد componentWillUnmount، به صورت زیر است:
سؤال: یک چنین قابلیتی را چگونه میتوان در یک کامپوننت تابعی تعریف کرد؟
در این حالت نیز میتوان از متد useEffect استفاده کرد. البته ابتدا باید state شیء ای را برای نگهداری اطلاعات به روز موقعیت مکانی کرسر ماوس، ایجاد کرد:
همانطور که عنوان شد، متد useState، برخلاف خاصیت کلی state یک کامپوننت کلاسی که فقط اشیاء را میپذیرد، هر نوع جاوا اسکریپتی را میتواند بپذیرد که در اینجا برای نمونه به یک شیء، تنظیم شدهاست.
سپس side effect خود را در قسمت effect function متد useEffect قرار میدهیم که آن نیز به متغیر handleMouseMove اشاره میکند:
به متغیر handleMouseMove، متدی انتساب داده شدهاست که با فراخوانی آن توسط mousemove listener، یک شیء evenet دریافت و سپس بر اساس خواص آن، خواص x و y شیء موجود در state، توسط متد setMousePosition، به روز رسانی میشود.
سپس برای نمایش x و y به روز رسانی شدهی در state، میتوان از markup زیر در متد رندر استفاده کرد.
اکنون سؤال اینجا است که معادل متد componentWillUnmount در اینجا چیست؟
اگر effect function تعریف شده، دارای یک خروجی (از نوع تابع) باشد، به این معنا است که این side effect، نیاز به پاکسازی دارد و این متد را در زمان Unmount آن فراخوانی میکند:
پیشتر آموختیم که effect function، تنها یکبار اجرا نمیشود؛ بلکه به ازای هر بار رندر، یکبار پس از آن اجرا میشود. یعنی خروجی آن نه فقط در زمان Unmount اجرا میشود، بلکه یکبار هم پیش از هر بار اجرای خود effect اجرا میگردد. به این ترتیب در اینجا فرصت پاکسازی اجرای قبلی را نیز خواهیم داد. بنابراین روش پاکسازی آن نسبت به متد componentWillUnmount کامپوننتهای تابعی، پیشرفتهتر است. یعنی توسط آن میتوان یک side effect خاص را پیش و پس از هر بار رندر، در صورت نیاز فراخوانی کرد.
سؤال: اگر بخواهیم از اجرای یک side effect، به ازای هر بار رندر جلوگیری کنیم، چه باید کرد؟
برای اینکار میتوان آرگومان دومی را به متد useEffect اضافه کرد که آرایهای از مقادیر است. توسط اعضای آن میتوان مقدار و یا مقادیری را مشخص کرد که side effect تعریف شده، به آن وابستهاست. اکنون اگر این مقدار تغییر کند، آنگاه side effect متناظر با آن نیز اجرا میشود:
در این مثال، چون پارامتر دوم را به صورت یک آرایهی خالی مقدار دهی کردهایم، effect function تعریف شده تنها در زمان mount و unmount اجرا میشود. البته اگر این تغییر را ذخیره کرده و برنامه را اجرا کنیم، تمام قسمتهای تعریف شدهی آن به خوبی کار میکنند (با کلیک بر روی دکمه، برچسب آن به روز رسانی میشود و یا با حرکت کرسر ماوس در صفحه، موقعیت آن گزارش داده میشود)، منهای قسمت به روز رسانی عنوان صفحه با هر بار کلیک. علت اینجا است که با تغییر معرفی شده، دیگر حالت componentDidUpdate توسط متد useEffect پوشش داده نمیشود و به این ترتیب با هر بار به روز رسانی state و رندر کامپوننت، دیگر کار به اجرای مجدد side effect آن نمیرسد.
برای رفع این مشکل، باید به useEffect اعلام کنیم که side effect تعریف شدهی در آن، وابستهاست به مقدار count و با تغییرات آن در state، نیاز است مجددا اجرا شود:
به همین جهت در آرایهی تعریف شده، مقداری را که به آن وابستگی وجود دارد، مشخص میکنیم. با این تغییر اگر بر روی دکمه کلیک کنیم، چون اکنون useEffect میداند که باید تغییرات count را ردیابی کند، با اجرای مجدد effect function خود، عنوان صفحه نیز به روز رسانی خواهد شد.
کار با چندین listener مختلف در متد useEffect
سؤال: آیا تنظیم یک وابستگی خاص در متد useEffect، امکان تنظیم event listenerهای دیگر را غیرممکن میکند؟
برای پاسخ به این سؤال، چند event listener دیگر را ثبت میکنیم. برای مثال یکی دیگر از APIهای مرورگر، navigator نام دارد که توسط آن میتوان وضعیت آنلاین و آفلاین بودن را به کمک خروجی خاصیت navigator.onLine آن، مشخص کرد. به کمک این API میخواهیم این وضعیت را نمایش دهیم. برای این منظور ابتدا state آنرا در کامپوننت تابعی، ایجاد میکنیم:
مقدار اولیهی این state را نیز توسط مقدار جاری خاصیت navigator.onLine، مشخص کردهایم.
اکنون برای گوش فرا دادن به تغییرات این خاصیت (online و یا offline شدن کاربر)، نیاز است دو event listener را به کمک متد addEventListener ثبت کنیم و همچنین این متدها نیاز به پاکسازی هم دارند:
پس از ثبت event listener ها، اکنون میتوان با هربار فراخوانی آنها توسط مرورگر، وضعیت state آنرا به true و یا false، تغییر داد:
و در آخر مقدار متغیر status را به نحو زیر در متد رندر، نمایش داد:
برای آزمایش حالت offline آن، فقط کافی است به ابزار توسعه دهندگان مرورگر مراجعه کرده و در برگهی network آن، حالت online را offline کنید:
در این حالت هم نمایش وضعیت آنلاین بودن کاربر به درستی کار میکند و هم سایر قسمتهایی که تاکنون اضافه کردهایم. به این معنا که هر چند توسط ذکر پارامتر [count]، وابستگی خاصی برای side effect ویژهای، مشخص شدهاست، اما ما را از تنظیم event listenerهای دیگری در قسمتهای mount و unmount محروم نمیکند.
پاکسازی اثرات جانبی در useEffect Hook، زمانیکه روشی برای آن وجود ندارد
در مثالی دیگر میخواهیم از API موقعیت جغرافیایی کاربر در مرورگر یا navigator.geolocation استفاده کنیم. توسط این API هم میتوان طول و عرض جغرافیایی را به دست آورد و هم تغییرات آنرا تحت نظر قرار داد. برای مثال با بررسی این تغییرات میتوان سرعت را نیز به دست آورد.
در این حالت نیز ابتدا با تعریف state مختص به آن شروع میکنیم و اینبار به عنوان مثال، مقدار اولیهی آنرا خارج از تابع جاری تنظیم میکنیم (جهت نمایش یک گزینهی مهیای دیگر):
سپس جهت کار با این API، مقدار اولیهی موقعیت کاربر و همچنین ردیابی تغییرات آنرا در متد useEffect، تنظیم میکنیم:
در اینجا متد getCurrentPosition، با متد addEventListener متفاوت است و تنها یک callback function را دریافت میکند که در آن میتوان setLocation را جهت به روز رسانی موقعیت جاری جغرافیایی کاربر، فراخوانی کرد. مشکلی که با این متد وجود دارد، نداشتن معادلی شبیه به متد removeEventListener است. یعنی API آن روشی را برای unmount آن فراهم نکردهاست. البته متد watchPosition که تغییرات موقعیت کاربر را ردیابی میکند، دارای API مخصوص پاکسازی آن نیز هست که در این کدها ذکر شدهاست.
یک روش برای حل این مشکل و غیرفعال کردن دستی listener متد getCurrentPosition پس از unmount، تعریف یک متغیر mounted پیش از متد useEffect است:
و آنرا در زمان پاکسازی useEffect، با false مقدار دهی میکنیم. اکنون زمانیکه میخواهیم رویدادگردان این Listener را تعریف کنیم، تنها بر اساس مقدار متغیر mounted عمل خواهیم کرد که از نشتی حافظهی آن جلوگیری میکند:
در آخر هم برای نمایش آن در متد رندر به صورت زیر عمل میکنیم:
سؤال: در اینجا شیء location چندین بار تکرار شدهاست. آیا میتوان مقادیر خواص آنرا توسط Object Destructuring نیز به دست آورد؟
پاسخ: بله. در اینجا هم Object Destructuring بر روی location state، کار میکند:
که پس از آن، دیگر نیازی به تکرار شیء location نخواهد بود:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-30-part-01.zip
برپایی پیشنیازها
در اینجا برای بررسی React Hooks، یک پروژهی جدید React را ایجاد میکنیم:
> npm i -g create-react-app > create-react-app sample-30 > cd sample-30 > npm start
> npm install --save bootstrap
import "bootstrap/dist/css/bootstrap.css";
همچنین اگر به فایل package.json موجود در ریشهی پروژه دقت کنیم، برای کار با React-hooks، نیاز است نگارش بستههای React و React-dom، حداقل مساوی 16.7 باشند که در زمان نگارش این مطلب، نگارش 16.12.0 آن به صورت خودکار نصب میشود. بنابراین بدون مشکلی میتوانیم شروع به کار با React hooks کنیم.
معرفی useState Hook
در اینجا قصد داریم یک شمارشگر را به همراه یک دکمه، در صفحه نمایش دهیم؛ بطوریکه این شمارشگر، تعداد بار کلیک بر روی دکمه را ردیابی میکند. از پیش میدانیم که برای ردیابی مقدار تعداد بار کلیک شدن، باید متغیر آنرا درون state یک class component قرار داد:
import "./App.css"; import React, { Component } from "react"; class App extends Component { state = { count: 0 }; incrementCount = () => { this.setState({ count: this.state.count + 1 }); }; render() { return ( <button onClick={this.incrementCount} className="btn btn-primary m-3"> I was clicked {this.state.count} times! </button> ); } } export default App;
اکنون میخواهیم همین کامپوننت را توسط React hooks بازنویسی کنیم. در ابتدا، فایل app.js را به AppClass.js، تغییر نام میدهیم تا نگارش قبلی class component را برای مقایسه، در اختیار داشته باشیم. در ادامه فایل جدید AppFunction.js را برای بازنویسی آن توسط یک کامپوننت تابعی، توسط میانبرهای imrc و سپس sfc در VSCode، ایجاد میکنیم. البته این تغییر نام فایلها، نیاز به تغییر نام ماژولهای import شدهی در فایل index.js را نیز به صورت زیر دارد:
//import App from "./AppClass"; import App from "./AppFunction";
اولین سؤالی که اینجا مطرح میشود، این است: در این کامپوننت تابعی جدید، state را از کجا بدست بیاوریم؟
با React Hooks، بجای تعریف یک state به صورت خاصیت، آنرا صرفا use میکنیم و این دقیقا نام اولین React Hooks ای است که بررسی میکنیم؛ یا همان useState. بنابراین ابتدا این شیء را import خواهیم کرد:
import React, { useState } from 'react';
const App = () => { const [count, setCount] = useState(0);
useState<number>(initialState: number | (() => number)): [number, React.Dispatch<React.SetStateAction<number>>]
import React, { useState } from "react"; const App = () => { const [count, setCount] = useState(0); const incrementCount = () => { setCount(count + 1); }; return ( <button onClick={incrementCount} className="btn btn-primary m-3"> I was clicked {count} times! </button> ); }; export default App;
- همچنین در اینجا (داخل این متد) دیگر خبری از thisها نیست؛ onClick، مستقیما به متغیر incrementCount اشاره میکند و {count} نیز مستقیما از خروجی useState، که به مقدار جاری count اشاره میکند، تامین میشود.
- اکنون با هربار کلیک بر روی این دکمه، متد منتسب به متغیر incrementCount فراخوانی شده و در داخل آن، همان متد setCount را جهت به روز رسانی state، فراخوانی میکنیم (بجای فراخوانی this.setState عمومی قبلی). در اینجا ابتدا مقدار جاری متغیر count در state، دریافت شده و سپس یک واحد به آن اضافه میشود. امضای متد جنریک setCount به صورت زیر است:
const setCount: (value: React.SetStateAction<number>) => void
استفاده از مقدار قبلی state توسط useState
زمانیکه متد this.setState فراخوانی میشود، اینکار سبب در صف قرار گرفتن رندر مجدد کامپوننت جاری خواهد شد. همچنین اعمال این متد نیز ممکن است در صف قرار گیرد. یعنی اگر پس از فراخوانی this.setState، سعی در خواندن state به روز شده را داشته باشیم، ممکن است مقدار اشتباهی را دریافت کنیم:
incrementCount = () => { this.setState({ count: this.state.count + 1 }); };
incrementCount = () => { this.setState(prevState => ({ count: prevState.count + 1 })); };
این نکته در مورد کامپوننتهای تابعی نیز وجود دارد:
const incrementCount = () => { setCount(count + 1); };
const incrementCount = () => { setCount(prevCount => prevCount + 1); };
به روز رسانی بیش از یک خاصیت در state
فرض کنید قصد داریم به مثال جاری، یک مربع را در صفحه اضافه کنیم که با کلیک بر روی آن، رنگش تغییر میکند (خاموش و روشن میشود):
در حالت AppClass یا کامپوننت کلاسی، کدهای برنامه به صورت زیر تغییر میکنند:
import "./App.css"; import React, { Component } from "react"; class App extends Component { state = { count: 0, isOn: false }; incrementCount = () => { this.setState(prevState => ({ count: prevState.count + 1 })); }; toggleLight = () => { this.setState(prevState => ({ isOn: !prevState.isOn })); }; render() { return ( <> <h1>App Class</h1> <h2>Counter</h2> <button onClick={this.incrementCount} className="btn btn-primary m-3"> I was clicked {this.state.count} times! </button> <h2>Toggle Light</h2> <div style={{ height: "50px", width: "50px", cursor: "pointer" }} className={ this.state.isOn ? "alert alert-info m-3" : "alert alert-warning m-3" } onClick={this.toggleLight} /> </> ); } } export default App;
- در متد رندر، نیاز است تا تنها یک child، بازگشت داده شود. به همین جهت میتوان از React.Fragment، برای محصور سازی المانهای تعریف شده، استفاده کرد. البته در React 16.7.0، دیگر نیازی به ذکر صریح React.Fragment نبوده و فقط میتوان نوشت </><> تا بیانگر یک فرگمنت باشد.
- سپس یک div تعریف شده که با استفاده از ویژگی style، یک سری شیوهنامهی ابتدایی، مانند طول و عرض و نوع اشارهگر ماوس آن، تعیین شدهاند.
- اکنون برای اینکه بتوان با کلیک بر روی این div، رنگ آنرا تغییر داد، نیاز است بتوان توسط متغیری، مقدار خاموش و روشن بودن را ردیابی کرد. به همین جهت خاصیت جدید isOn را به state اضافه میکنیم.
- در آخر، رویداد onClick این div را به متد رویدادگران toggleLight متصل میکنیم تا توسط آن و فراخوانی this.setState، بتوان مقدار قبلی isOn را در state، دریافت و سپس آنرا معکوس کرد و بجای مقدار جاری isOn در state درج کنیم. این فراخوانی، سبب رندر مجدد کامپوننت جاری شده و در نتیجهی آن، مقدار className را بر اساس مقدار this.state.isOn، به صورت پویا تغییر میدهد.
برای مشاهدهی خروجی برنامه در این حالت، نیاز است به index.js مراجعه و تغییر زیر را اعمال کرد تا کامپوننت App، از ماژول AppClass تامین شود:
import App from "./AppClass"; //import App from "./AppFunction";
اکنون قصد داریم دقیقا معادل همین قطعه کد را در کامپوننت تابعی خود پیاده سازی کنیم. به همین جهت به فایل src\AppFunction.js بازگشته و تغییرات زیر را اعمال میکنیم:
import React, { useState } from "react"; const App = () => { const [count, setCount] = useState(0); const [isOn, setIsOn] = useState(false); const incrementCount = () => { setCount(prevCount => prevCount + 1); }; const toggleLight = () => { setIsOn(prevIsOn => !prevIsOn); }; return ( <> <h1>App Function</h1> <h2>Counter</h2> <button onClick={incrementCount} className="btn btn-primary m-3"> I was clicked {count} times! </button> <h2>Toggle Light</h2> <div style={{ height: "50px", width: "50px", cursor: "pointer" }} className={isOn ? "alert alert-info m-3" : "alert alert-warning m-3"} onClick={toggleLight} /> </> ); }; export default App;
- اگر دقت کنید، کلیات این کامپوننت تابعی، با کامپوننت کلاسی، آنچنان متفاوت نیست. متد رندر آن دقیقا همان markup را بازگشت میدهد؛ با یک تفاوت مهم: در اینجا دیگر نیازی به ذکر thisها نیست، چون تمام ارجاعات، به متغیرهای داخل تابع App انجام شدهاست و نه به متدها و یا خاصیتهای یک کلاس.
- مرحلهی بعد تغییر، جایگزینی this.state.isOn قبلی، با یک متغیر درون تابع App است. به همین جهت در اینجا یک useState دیگر را تعریف میکنیم. هر useState، تنها به قسمتی از state اشاره میکند و مانند خاصیت کلی state مربوط به یک کلاس نیست. همچنین در خاصیت state یک کلاس، مقدار آن همواره به یک شیء اشاره میکند؛ اما با useState چنین اجباری وجود ندارد و میتواند هر نوع مجاز جاوا اسکریپتی باشد. برای مثال در اینجا مقدار اولیهی آن به false تنظیم شدهاست. پس از آن، خروجی این متد، قسمتی از state را که کنترل میکند، به نام متغیر isOn و تنظیم کنندهی آنرا به نام متد setIsOn، معرفی خواهد کرد. متد useState، یک متد جنریک است. یعنی نوع خروجیهای آن بر اساس نوع مقدار اولیهای که به آن انتساب داده میشود، تعیین میشود. برای مثال اگر نوع مقدار اولیهی آن، Boolean باشد، به صورت خودکار نوع متغیر و پارامتر متد خروجی از آن نیز Boolean خواهند بود.
- در آخر خاصیت toggleLight کلاس کامپوننت، تبدیل به یک متغیر یا ثابت، در تابع جاری App میشود و بجای this.setState کلی قبلی، از متد اختصاصیتر setIsOn، برای تغییر مقدار state متناظر، کمک گرفته خواهد شد. در اینجا با استفاده از prevIsOn، به مقدار دقیق پیشین متغیر isOn در state، دسترسی یافته و سپس آنرا معکوس میکنیم.
برای مشاهدهی خروجی برنامه در این حالت، نیاز است به index.js مراجعه و تغییر زیر را اعمال کرد تا کامپوننت App، از ماژول AppFunction تامین شود:
// import App from "./AppClass"; import App from "./AppFunction";
معرفی useEffect Hook
فرض کنید قصد داریم برچسب دکمهی «I was clicked {this.state.count} times» را در المان Title صفحه، نمایش دهیم. برای انجام چنین کاری نیاز است با DOM API تعامل داشته باشیم؛ اما پیشتر یک چنین کاری را تنها با class components میشد انجام داد. برای رفع این محدودیت در کامپوننتهای تابعی، hook جدیدی به نام useEffect، ارائه شدهاست که باید import شود:
import React, { useEffect, useState } from "react";
در اینجا effect به معنای side effect و یا اثرات جانبی است؛ مانند: تعامل با یک API خارجی، کار با API مرورگر و کلا هر جائیکه در برنامه با دنیای خارج تعاملی وجود دارد، به عنوان یک side effect شناخته میشود. بنابراین با استفاده متد useEffect، میتوان در کامپوننتهای تابعی نیز با دنیای خارج، تعامل برقرار کرد.
import React, { useEffect, useState } from "react"; const App = () => { const [count, setCount] = useState(0); useEffect(() => { document.title = `You have clicked ${count} times`; });
در اولین بار اجرای برنامه، عبارت «You have clicked 0 times»، در عنوان صفحهی جاری، ظاهر میشود که از مقدار پیشفرض count، استفاده کردهاست. اکنون اگر بر روی دکمه، کلیک کنیم، پس از تغییر برچسب دکمه (یا همان رندر کامپوننت، پس از تغییری در state)، آنگاه عنوان نمایش داده شدهی در مرورگر نیز تغییر میکند.
یک نکته: چون useEffect دارای همان scope متغیر count است، نیاز به API خاصی برای خواندن مقدار آن در اینجا نیست.
سؤال: برای پیاده سازی چنین قابلیتی در یک کامپوننت کلاسی چه باید کرد؟ در این مثال، در ابتدای کار باید مقدار پیشفرض موجود در state را در عنوان صفحه مشاهده کرد و پس از هربار به روز رسانی state نیز باید این عنوان، تغییر کند.
برای پیاده سازی معادل متد useEffect ای که در یک کامپوننت تابعی استفاده شد، در اینجا باید از دو life-cycle hook متفاوت، به نامهای componentDidMount و componentDidUpdate، استفاده کنیم:
class App extends Component { componentDidMount() { document.title = `You have been clicked ${this.state.count} times`; } componentDidUpdate() { document.title = `You have been clicked ${this.state.count} times`; }
- همچنین چون میخواهیم به ازای هر تغییری در state نیز این عنوان تغییر کند (با هر بار کلیک بر روی دکمه)، باید از متد componentDidUpdate هم استفاده کنیم.
پاکسازی اثرات جانبی در useEffect Hook
فرض کنید قصد داریم موقعیت فعلی کرسر ماوس را در مرورگر نمایش دهیم. برای انجام اینکار در کامپوننت کلاسی، میتوان از متد componentDidMount جهت دسترسی به DOM API و استفاده از متد addEventListener آن، برای گوش فرا دادن به حرکات کرسر ماوس، استفاده کرد:
class App extends Component { componentDidMount() { // ... window.addEventListener("mousemove", this.handleMouseMove); }
handleMouseMove = event => { this.setState({ x: event.pageX, y: event.pageY }); };
class App extends Component { state = { //... x: 0, y: 0 };
<h2>Mouse Position</h2> <p>X position: {this.state.x}</p> <p>Y position: {this.state.y}</p>
- تعدادی از آنها نیازی به پاکسازی و خارج شدن از حافظه را ندارند؛ مانند به روز رسانی عنوان صفحه در مرورگر. میتوان یک چنین side effect هایی را اجرا و سپس آنها را فراموش کرد.
- اما تعدادی از side effectها را حتما باید پاکسازی کرد؛ مانند mousemove listener تعریف شدهی در مثال فوق. در اینجا زمانیکه کامپوننت جاری mount میشود، این listener را تعریف میکنیم؛ اما با Unmount شدن آن، باید این listener را حذف کرد تا برنامه دچار نشتی حافظه نشود (اگر اینکار انجام نشود، در این مثال مرورگر هنگ خواهد کرد!). روش انجام اینکار در متد componentWillUnmount، به صورت زیر است:
componentWillUnmount() { window.removeEventListener("mousemove", this.handleMouseMove); }
در این حالت نیز میتوان از متد useEffect استفاده کرد. البته ابتدا باید state شیء ای را برای نگهداری اطلاعات به روز موقعیت مکانی کرسر ماوس، ایجاد کرد:
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
سپس side effect خود را در قسمت effect function متد useEffect قرار میدهیم که آن نیز به متغیر handleMouseMove اشاره میکند:
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); useEffect(() => { // ... window.addEventListener("mousemove", handleMouseMove); }); const handleMouseMove = event => { setMousePosition({ x: event.pageX, y: event.pageY }); };
سپس برای نمایش x و y به روز رسانی شدهی در state، میتوان از markup زیر در متد رندر استفاده کرد.
<h2>Mouse Position</h2> {JSON.stringify(mousePosition, null, 2)} <br />
اگر effect function تعریف شده، دارای یک خروجی (از نوع تابع) باشد، به این معنا است که این side effect، نیاز به پاکسازی دارد و این متد را در زمان Unmount آن فراخوانی میکند:
useEffect(() => { // … // componentDidMount & componentDidUpdate window.addEventListener("mousemove", handleMouseMove); // componentWillUnmount return () => { window.removeEventListener("mousemove", handleMouseMove); }; });
سؤال: اگر بخواهیم از اجرای یک side effect، به ازای هر بار رندر جلوگیری کنیم، چه باید کرد؟
برای اینکار میتوان آرگومان دومی را به متد useEffect اضافه کرد که آرایهای از مقادیر است. توسط اعضای آن میتوان مقدار و یا مقادیری را مشخص کرد که side effect تعریف شده، به آن وابستهاست. اکنون اگر این مقدار تغییر کند، آنگاه side effect متناظر با آن نیز اجرا میشود:
useEffect(() => { document.title = `You have clicked ${count} times`; window.addEventListener("mousemove", handleMouseMove); return () => { window.removeEventListener("mousemove", handleMouseMove); }; },[]);
برای رفع این مشکل، باید به useEffect اعلام کنیم که side effect تعریف شدهی در آن، وابستهاست به مقدار count و با تغییرات آن در state، نیاز است مجددا اجرا شود:
useEffect(() => { // ... },[count]);
کار با چندین listener مختلف در متد useEffect
سؤال: آیا تنظیم یک وابستگی خاص در متد useEffect، امکان تنظیم event listenerهای دیگر را غیرممکن میکند؟
برای پاسخ به این سؤال، چند event listener دیگر را ثبت میکنیم. برای مثال یکی دیگر از APIهای مرورگر، navigator نام دارد که توسط آن میتوان وضعیت آنلاین و آفلاین بودن را به کمک خروجی خاصیت navigator.onLine آن، مشخص کرد. به کمک این API میخواهیم این وضعیت را نمایش دهیم. برای این منظور ابتدا state آنرا در کامپوننت تابعی، ایجاد میکنیم:
const [status, setStatus] = useState(navigator.onLine);
اکنون برای گوش فرا دادن به تغییرات این خاصیت (online و یا offline شدن کاربر)، نیاز است دو event listener را به کمک متد addEventListener ثبت کنیم و همچنین این متدها نیاز به پاکسازی هم دارند:
useEffect(() => { // ... window.addEventListener("online", handleOnline); window.addEventListener("offline", handleOffline); return () => { // ... window.removeEventListener("online", handleOnline); window.removeEventListener("offline", handleOffline); }; }, [count]);
const handleOnline = () => { setStatus(true); }; const handleOffline = () => { setStatus(false); };
<h2>Network Status</h2> <p> You are <strong>{status ? "online" : "offline"}</strong> </p>
برای آزمایش حالت offline آن، فقط کافی است به ابزار توسعه دهندگان مرورگر مراجعه کرده و در برگهی network آن، حالت online را offline کنید:
در این حالت هم نمایش وضعیت آنلاین بودن کاربر به درستی کار میکند و هم سایر قسمتهایی که تاکنون اضافه کردهایم. به این معنا که هر چند توسط ذکر پارامتر [count]، وابستگی خاصی برای side effect ویژهای، مشخص شدهاست، اما ما را از تنظیم event listenerهای دیگری در قسمتهای mount و unmount محروم نمیکند.
پاکسازی اثرات جانبی در useEffect Hook، زمانیکه روشی برای آن وجود ندارد
در مثالی دیگر میخواهیم از API موقعیت جغرافیایی کاربر در مرورگر یا navigator.geolocation استفاده کنیم. توسط این API هم میتوان طول و عرض جغرافیایی را به دست آورد و هم تغییرات آنرا تحت نظر قرار داد. برای مثال با بررسی این تغییرات میتوان سرعت را نیز به دست آورد.
در این حالت نیز ابتدا با تعریف state مختص به آن شروع میکنیم و اینبار به عنوان مثال، مقدار اولیهی آنرا خارج از تابع جاری تنظیم میکنیم (جهت نمایش یک گزینهی مهیای دیگر):
const initialLocationState = { latitude: null, longitude: null, speed: null }; const App = () => { // ... const [location, setLocation] = useState(initialLocationState);
useEffect(() => { // ... navigator.geolocation.getCurrentPosition(handleGeolocation); const watchId = navigator.geolocation.watchPosition(handleGeolocation); return () => { // ... navigator.geolocation.clearWatch(watchId); }; }, [count]);
یک روش برای حل این مشکل و غیرفعال کردن دستی listener متد getCurrentPosition پس از unmount، تعریف یک متغیر mounted پیش از متد useEffect است:
let mounted = true; useEffect(() => { // ... return () => { // ... mounted = false; }; }, [count]);
const handleGeolocation = event => { if (mounted) { setLocation({ latitude: event.coords.latitude, longitude: event.coords.longitude, speed: event.coords.speed }); } };
<h2>Geolocation</h2> <p>Latitude is {location.latitude}</p> <p>Longitude is {location.longitude}</p> <p>Your speed is {location.speed ? location.speed : "0"}</p>
سؤال: در اینجا شیء location چندین بار تکرار شدهاست. آیا میتوان مقادیر خواص آنرا توسط Object Destructuring نیز به دست آورد؟
پاسخ: بله. در اینجا هم Object Destructuring بر روی location state، کار میکند:
const [{ latitude, longitude, speed }, setLocation] = useState( initialLocationState );
<h2>Geolocation</h2> <p>Latitude is {latitude}</p> <p>Longitude is {longitude}</p> <p>Your speed is {speed ? speed : "0"}</p>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-30-part-01.zip
در مطلب «بررسی روش آپلود فایلها از طریق یک برنامهی Angular به یک برنامهی ASP.NET Core» روش عمومی آپلود فایلها را بررسی کردیم. آن مطلب وابستگی به کامپوننت خاصی ندارد و عمومی است. در مطلب جاری میخواهیم روش دیگری را مبتنی بر کامپوننت ng2-file-upload بررسی کنیم که به همراه نمایش درصد پیشرفت ارسال فایلها، امکان انتخاب بهتر نوع فایلهای آپلودی و همچنین امکان مشاهدهی لیست کامل فایلهای انتخاب شده و امکان حذف مواردی از آن، پیش از ارسال نهایی است.
پیشنیازهای کار با کامپوننت ng2-file-upload
برای شروع به کار با کامپوننت ng2-file-upload، ابتدا نیاز است بستهی npm آنرا نصب کرد:
همچنین یک کامپوننت آزمایشی را هم به برنامه (دقیقا همان مثال مطلب قبلی) جهت اعمال آن اضافه میکنیم:
پس از آن نیاز است به ماژولی که این کامپوننت جدید در آن قرار دارد، مدخل FileUploadModule کامپوننت ng2-file-upload را افزود:
در غیراینصورت خطای شناخته نشدن خاصیت uploader را در حین اعمال این کامپوننت مشاهده خواهید کرد.
تکمیل Ng2FileUploadTestComponent جهت اعمال ng2-file-upload
اکنون به کلاس کامپوننت جدیدی که ایجاد کردیم، مراجعه کرده و تغییرات ذیل را اعمال میکنیم:
در اینجا یک خاصیت عمومی از نوع FileUploader تعریف شدهاست که در اختیار قالب این کامپوننت قرار خواهد گرفت. همچنین شیء مدل فرم نیز همانند مطلب «بررسی روش آپلود فایلها از طریق یک برنامهی Angular به یک برنامهی ASP.NET Core» تهیه شدهاست. هدف این است که بررسی کنیم علاوه بر ارسال فایلها، چگونه میتوان اطلاعات یک فرم را نیز به سمت سرور ارسال کرد.
وهله سازی از کامپوننت ng2-file-upload و انجام تنظیمات اولیهی آن
پس از تعریف خاصیت عمومی fileUploader، اکنون نوبت به وهله سازی آن است:
- در اینجا url، مسیر اکشن متدی را در سمت سرور مشخص میکند که قرار است فایلهای ارسالی را دریافت و ذخیره کند.
- اگر برنامه از نکات anti-forgery token استفاده میکند، این کامپوننت برخلاف روش مطرح شدهی در مطلب مشابه قبلی، هیچ هدری را به سمت سرور ارسال نمیکند. بنابراین نیاز است کوکی مرتبط را خودمان یافته و سپس به لیست هدرها اضافه کنیم. در اینجا روش استخراج یک کوکی را توسط کدهای جاوا اسکریپتی مشاهده میکنید:
- برای محدود سازی فایلهای ارسالی توسط این کامپوننت، دو روش وجود دارد:
الف) مشخص سازی مقدار خاصیت allowedMimeType
همانطور که مشاهده میکنید، در اینجا باید mime type فایلهای مجاز را مشخص کرد.
ب) مشخص سازی مقدار خاصیت allowedFileType
برخلاف تصور، در اینجا از پسوند فایلها استفاده نمیکند و از یک لیست از پیش مشخص که نمونهای از آنرا در اینجا مشاهده میکنید، کمک گرفته میشود. بنابراین اگر برای مثال تنها نیاز به ارسال تصاویر بود، مقدار image را نگه داشته و مابقی را از لیست حذف کنید.
- removeAfterUpload به این معنا است که آیا لیست نهایی که نمایش داده میشود، پس از آپلود باقی بماند یا خیر؟
- توسط خاصیت maxFileSize میتوان حداکثر اندازهی قابل قبول فایلهای ارسالی را مشخص کرد.
مدیریت رخدادهای کامپوننت ng2-file-upload
اکنون که وهلهای از این کامپوننت ساخته شدهاست، میتوان رخدادهای آنرا نیز مدیریت کرد. برای مثال:
الف) نحوهی ارسال اطلاعات اضافی به همراه یک فایل به سمت سرور
در اینجا شبیه به مطلب مشابه قبلی، مقادیر خواص شیء مدل، به صورت خودکار استخراج شده و به خاصیت form این کامپوننت که درحقیقت همان FormData ارسالی به سمت سرور است، اضافه میشوند.
ب) اطلاع یافتن از رخداد خاتمهی کار
رخداد onCompleteAll پس از ارسال تمام فایلها به سمت سرور فراخوانی میشود:
ج) در حین وهله سازی fileUploader، تعدادی محدودیت نیز قابل اعمال هستند. این محدودیتها سبب نمایش هیچگونه پیام خطایی نمیشوند. فقط زمانیکه کاربر فایلی را انتخاب میکند، این فایل در لیست ظاهر نمیشود. اگر علاقمند به مدیریت این وضعیت باشید، میتوان از رخداد onWhenAddingFileFailed استفاده کرد:
د) اگر ارسال فایلی به سمت سرور با شکست مواجه شود، در رخدادگردان onErrorItem میتوان به نام این فایل و اطلاعات بیشتری که از سمت سرور دریافت شدهاست، دسترسی یافت:
ه) اگر از سمت سرور اطلاعات JSON مانندی یا هر اطلاعات دیگری به سمت کلاینت پس از آپلود ارسال میشود، این اطلاعات را میتوان در رخدادگردان onSuccessItem دریافت کرد:
ارسال نهایی فرم و فایلها به سمت سرور
در پایان، با فراخوانی متد uploadAll شیء fileUploader جاری، میتوان اطلاعات فرم و تمام فایلهای آنرا به سمت سرور ارسال کرد:
فقط باید دقت داشت که این کامپوننت هر فایل را جداگانه به سمت سرور ارسال میکند و برخلاف روش مطلب قبلی، همه را یکجا و در طی یک درخواست به سمت سرور ارسال نمیکند. اما کدهای سمت سرور آن با مطلب مشابه قبلی دقیقا یکی است و تفاوتی نمیکند (همان نکات قسمت «دریافت فرم درخواست پشتیبانی در سمت سرور و ذخیرهی فایلهای آن» مطلب قبلی نیز در اینجا صادق است).
کدهای کامل کامپوننت ng2-file-upload-test.component.ts را در اینجا میتوانید مشاهده کنید.
تکمیل قالب کامپوننت Ng2FileUploadTestComponent
اکنون که کار تکمیل کامپوننت آزمایشی ارسال فایلها به سمت سرور به پایان رسید، نوبت به تکمیل قالب آن است.
افزودن فیلد اضافی توضیحات به فرم
هدف از این فیلد این است که شیء Ticket را وهله سازی و مقدار دهی کند. از مقدار آن در رخدادگردان onBuildItemForm که پیشتر توضیح داده شد، استفاده میشود.
تعریف ویژهی فیلد ارسال فایلها به سمت سرور
در اینجا ابتدا دایرکتیو ng2FileSelect ذکر میشود تا کامپوننت مرتبط فعالسازی شود. سپس خاصیت uploader این دایرکتیو توسط خاصیت fileUploader که پیشتر در کامپوننت، وهله سازی و تنظیم شد، مقدار دهی میشود.
ذکر ویژگی استاندارد multiple را نیز در اینجا مشاهده میکنید. وجود آن سبب خواهد شد تا کاربر بتواند چندین فایل را با هم انتخاب کند. اگر نیازی به ارسال چندین فایل نیست، این ویژگی را حذف کنید.
نمایش لیست فایلها و نمایش درصد پیشرفت آپلود آنها
جدولی را که در تصویر ابتدای بحث مشاهده کردید، به صورت ذیل شکل میگیرد (کدهای آن در همان صفحهی توضیحات کامپوننت نیز موجود هستند):
شیء fileUploader وهله سازی شدهی در کامپوننت این قالب، دارای خاصیت queue است. در این خاصیت، لیست فایلهای انتخابی توسط کاربر درج میشوند. برای مثال مقدار fileUploader?.queue?.length مساوی تعداد فایلهای انتخابی توسط کاربر است. بنابراین میتوان حلقهای را بر روی آن تشکیل داد و مشخصات این فایلها را در صفحه نمایش داد. همچنین هر آیتم آن دارای متد remove نیز هست. کار این متد، حذف این آیتم از لیست queue است و یا اگر متد fileUploader.clearQueue فراخوانی شود، تمام آیتمهای این لیست را حذف میکند.
در اینجا از progress-bar بوت استرپ برای نمایش درصد آپلود فایلها استفاده شدهاست:
این کامپوننت ارسال فایل، خاصیت item.progress هر فایل موجود در queue را مدام به روز رسانی میکند. به همین جهت میتوان از آن جهت تغییر عرض پیشرفت progress-bar بوت استرپ استفاده کرد.
غیرفعال کردن دکمهی ارسال، در صورت عدم انتخاب یک فایل
اگر بخواهیم انتخاب حداقل یک فایل را توسط کاربر اجباری کنیم، میتوان خاصیت disabled دکمهی ارسال را به طول صف یا fileUploader.queue.length نیز متصل کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
پیشنیازهای کار با کامپوننت ng2-file-upload
برای شروع به کار با کامپوننت ng2-file-upload، ابتدا نیاز است بستهی npm آنرا نصب کرد:
>npm install ng2-file-upload --save
همچنین یک کامپوننت آزمایشی را هم به برنامه (دقیقا همان مثال مطلب قبلی) جهت اعمال آن اضافه میکنیم:
>ng g c UploadFile/ng2-file-upload-test
پس از آن نیاز است به ماژولی که این کامپوننت جدید در آن قرار دارد، مدخل FileUploadModule کامپوننت ng2-file-upload را افزود:
import { FileUploadModule } from "ng2-file-upload"; @NgModule({ imports: [ FileUploadModule ]
تکمیل Ng2FileUploadTestComponent جهت اعمال ng2-file-upload
اکنون به کلاس کامپوننت جدیدی که ایجاد کردیم، مراجعه کرده و تغییرات ذیل را اعمال میکنیم:
import { FileUploader, FileUploaderOptions } from "ng2-file-upload"; import { Ticket } from "./../ticket"; export class Ng2FileUploadTestComponent implements OnInit { fileUploader: FileUploader; model = new Ticket();
وهله سازی از کامپوننت ng2-file-upload و انجام تنظیمات اولیهی آن
پس از تعریف خاصیت عمومی fileUploader، اکنون نوبت به وهله سازی آن است:
this.fileUploader = new FileUploader( <FileUploaderOptions>{ url: "api/SimpleUpload/SaveTicket", headers: [ { name: "X-XSRF-TOKEN", value: this.getCookie("XSRF-TOKEN") }, { name: "Accept", value: "application/json" } ], isHTML5: true, // allowedMimeType: ["image/jpeg", "image/png", "application/pdf", "application/msword", "application/zip"] allowedFileType: [ "application", "image", "video", "audio", "pdf", "compress", "doc", "xls", "ppt" ], removeAfterUpload: true, autoUpload: false, maxFileSize: 10 * 1024 * 1024 } );
- اگر برنامه از نکات anti-forgery token استفاده میکند، این کامپوننت برخلاف روش مطرح شدهی در مطلب مشابه قبلی، هیچ هدری را به سمت سرور ارسال نمیکند. بنابراین نیاز است کوکی مرتبط را خودمان یافته و سپس به لیست هدرها اضافه کنیم. در اینجا روش استخراج یک کوکی را توسط کدهای جاوا اسکریپتی مشاهده میکنید:
getCookie(name: string): string { const value = "; " + document.cookie; const parts = value.split("; " + name + "="); if (parts.length === 2) { return decodeURIComponent(parts.pop().split(";").shift()); } }
- برای محدود سازی فایلهای ارسالی توسط این کامپوننت، دو روش وجود دارد:
الف) مشخص سازی مقدار خاصیت allowedMimeType
همانطور که مشاهده میکنید، در اینجا باید mime type فایلهای مجاز را مشخص کرد.
ب) مشخص سازی مقدار خاصیت allowedFileType
برخلاف تصور، در اینجا از پسوند فایلها استفاده نمیکند و از یک لیست از پیش مشخص که نمونهای از آنرا در اینجا مشاهده میکنید، کمک گرفته میشود. بنابراین اگر برای مثال تنها نیاز به ارسال تصاویر بود، مقدار image را نگه داشته و مابقی را از لیست حذف کنید.
- removeAfterUpload به این معنا است که آیا لیست نهایی که نمایش داده میشود، پس از آپلود باقی بماند یا خیر؟
- توسط خاصیت maxFileSize میتوان حداکثر اندازهی قابل قبول فایلهای ارسالی را مشخص کرد.
مدیریت رخدادهای کامپوننت ng2-file-upload
اکنون که وهلهای از این کامپوننت ساخته شدهاست، میتوان رخدادهای آنرا نیز مدیریت کرد. برای مثال:
الف) نحوهی ارسال اطلاعات اضافی به همراه یک فایل به سمت سرور
this.fileUploader.onBuildItemForm = (fileItem, form) => { for (const key in this.model) { if (this.model.hasOwnProperty(key)) { form.append(key, this.model[key]); } } };
ب) اطلاع یافتن از رخداد خاتمهی کار
رخداد onCompleteAll پس از ارسال تمام فایلها به سمت سرور فراخوانی میشود:
this.fileUploader.onCompleteAll = () => { // clear the form // this.model = new Ticket(); };
ج) در حین وهله سازی fileUploader، تعدادی محدودیت نیز قابل اعمال هستند. این محدودیتها سبب نمایش هیچگونه پیام خطایی نمیشوند. فقط زمانیکه کاربر فایلی را انتخاب میکند، این فایل در لیست ظاهر نمیشود. اگر علاقمند به مدیریت این وضعیت باشید، میتوان از رخداد onWhenAddingFileFailed استفاده کرد:
this.fileUploader.onWhenAddingFileFailed = (item, filter, options) => { // msg: `You can't select ${item.name} file because of the ${filter.name} filter.` };
د) اگر ارسال فایلی به سمت سرور با شکست مواجه شود، در رخدادگردان onErrorItem میتوان به نام این فایل و اطلاعات بیشتری که از سمت سرور دریافت شدهاست، دسترسی یافت:
this.fileUploader.onErrorItem = (fileItem, response, status, headers) => { // };
ه) اگر از سمت سرور اطلاعات JSON مانندی یا هر اطلاعات دیگری به سمت کلاینت پس از آپلود ارسال میشود، این اطلاعات را میتوان در رخدادگردان onSuccessItem دریافت کرد:
this.fileUploader.onSuccessItem = (item, response, status, headers) => { if (response) { const ticket = JSON.parse(response); console.log(`ticket:`, ticket); } };
ارسال نهایی فرم و فایلها به سمت سرور
در پایان، با فراخوانی متد uploadAll شیء fileUploader جاری، میتوان اطلاعات فرم و تمام فایلهای آنرا به سمت سرور ارسال کرد:
submitForm(form: NgForm) { this.fileUploader.uploadAll(); // NOTE: Upload multiple files in one request -> https://github.com/valor-software/ng2-file-upload/issues/671 }
کدهای کامل کامپوننت ng2-file-upload-test.component.ts را در اینجا میتوانید مشاهده کنید.
تکمیل قالب کامپوننت Ng2FileUploadTestComponent
اکنون که کار تکمیل کامپوننت آزمایشی ارسال فایلها به سمت سرور به پایان رسید، نوبت به تکمیل قالب آن است.
افزودن فیلد اضافی توضیحات به فرم
<div class="container"> <h3>Support Form(ng2-file-upload)</h3> <form #form="ngForm" (submit)="submitForm(form)" novalidate> <div class="form-group" [class.has-error]="description.invalid && description.touched"> <label class="control-label">Description</label> <input #description="ngModel" required type="text" class="form-control" name="description" [(ngModel)]="model.description"> <div *ngIf="description.invalid && description.touched"> <div class="alert alert-danger" *ngIf="description.errors.required"> description is required. </div> </div> </div>
تعریف ویژهی فیلد ارسال فایلها به سمت سرور
<div class="form-group"> <label class="control-label">Screenshot(s)</label> <input required type="file" multiple ng2FileSelect [uploader]="fileUploader" class="form-control" name="screenshot"> </div>
ذکر ویژگی استاندارد multiple را نیز در اینجا مشاهده میکنید. وجود آن سبب خواهد شد تا کاربر بتواند چندین فایل را با هم انتخاب کند. اگر نیازی به ارسال چندین فایل نیست، این ویژگی را حذف کنید.
نمایش لیست فایلها و نمایش درصد پیشرفت آپلود آنها
جدولی را که در تصویر ابتدای بحث مشاهده کردید، به صورت ذیل شکل میگیرد (کدهای آن در همان صفحهی توضیحات کامپوننت نیز موجود هستند):
<div style="margin-bottom: 10px" *ngIf="fileUploader.queue.length"> <h3>Upload queue</h3> <p>Queue length: {{ fileUploader?.queue?.length }}</p> <table class="table"> <thead> <tr> <th width="50%">Name</th> <th>Size</th> <th>Progress</th> <th>Status</th> <th>Actions</th> </tr> </thead> <tbody> <tr *ngFor="let item of fileUploader.queue"> <td><strong>{{ item?.file?.name }}</strong></td> <td nowrap>{{ item?.file?.size/1024/1024 | number:'.2' }} MB</td> <td> <div class="progress" style="margin-bottom: 0;"> <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': item.progress + '%' }"></div> </div> </td> <td class="text-center"> <span *ngIf="item.isError"><i class="glyphicon glyphicon-remove"></i></span> </td> <td nowrap> <button type="button" class="btn btn-danger btn-xs" (click)="item.remove()"> <span class="glyphicon glyphicon-trash"></span> Remove </button> </td> </tr> </tbody> </table> <div> <div> Queue progress: <div class="progress"> <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': fileUploader.progress + '%' }"></div> </div> </div> <button type="button" class="btn btn-danger btn-s" (click)="fileUploader.clearQueue()" [disabled]="!fileUploader.queue.length"> <span class="glyphicon glyphicon-trash"></span> Remove all </button> </div> </div>
در اینجا از progress-bar بوت استرپ برای نمایش درصد آپلود فایلها استفاده شدهاست:
<div class="progress" style="margin-bottom: 0;"> <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': item.progress + '%' }"></div> </div>
غیرفعال کردن دکمهی ارسال، در صورت عدم انتخاب یک فایل
<button class="btn btn-primary" [disabled]="form.invalid || !fileUploader.queue.length" type="submit">Submit</button> </form>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
یک نکتهی تکمیلی: کار با Policyها در برنامههای Blazor WASM
در این مطلب، روشی را برای برقراری دسترسی نقش Admin، به تمام قسمتهای محافظت شدهی برنامه، با معرفی نقش آن به یک ویژگی Authorize سفارشی شده، مشاهده کردید. هرچند این روش کار میکند، اما روش جدیدتر برقراری یک چنین دسترسیهای ترکیبی در برنامههای ASP.NET Core و سایر فناوریهای مشتق شدهی از آن، کار با Policyها است که برای نمونه در مثال فوق، به صورت زیر قابل پیاده سازی است:
الف) تعریف Policyهای مشترک بین برنامههای Web API و WASM
Policyهای تعریف شده، باید قابلیت اعمال به اکشن متدهای کنترلرها و همچنین کامپوننتهای WASM را داشته باشند. به همین جهت آنها را در پروژهی اشتراکی BlazorServer.Common که در هر دو پروژه استفاده میشود، قرار میدهیم:
در اینجا یکسری Policy جدید را مشاهده میکنید که در آنها همواره نقش Admin حضور دارد و همچین روش or آنها را توسط policy.RequireAssertion مشاهده میکنید. این تعاریف، نیاز به نصب بستهی Microsoft.AspNetCore.Authorization را نیز دارند. با کمک Policyها میتوان ترکیبهای پیچیدهای از دسترسیهای موردنیاز را ساخت؛ بدون اینکه نیاز باشد مدام AuthorizeAttribute سفارشی را طراحی کرد.
ب) افزودن Policyهای تعریف شده به پروژههای Web API و WASM
پس از تعریف Policyهای مورد نیاز، اکنون نوبت به افزودن آنها به برنامههای Web API:
و همچنین WASM است:
به این ترتیب Policyهای یکدستی را بین برنامههای کلاینت و سرور، به اشتراک گذاشتهایم.
ج) استفاده از Policyهای تعریف شده در برنامهی WASM
اکنون که برنامه قابلیت کار با Policyها را پیدا کرده، میتوان فیلتر Roles سفارشی را حذف و با فیلتر Authorize پالیسی دار جایگزین کرد:
حتی میتوان از پالیسیها در حین تعریف AuthorizeViewها نیز استفاده کرد:
در این مطلب، روشی را برای برقراری دسترسی نقش Admin، به تمام قسمتهای محافظت شدهی برنامه، با معرفی نقش آن به یک ویژگی Authorize سفارشی شده، مشاهده کردید. هرچند این روش کار میکند، اما روش جدیدتر برقراری یک چنین دسترسیهای ترکیبی در برنامههای ASP.NET Core و سایر فناوریهای مشتق شدهی از آن، کار با Policyها است که برای نمونه در مثال فوق، به صورت زیر قابل پیاده سازی است:
الف) تعریف Policyهای مشترک بین برنامههای Web API و WASM
Policyهای تعریف شده، باید قابلیت اعمال به اکشن متدهای کنترلرها و همچنین کامپوننتهای WASM را داشته باشند. به همین جهت آنها را در پروژهی اشتراکی BlazorServer.Common که در هر دو پروژه استفاده میشود، قرار میدهیم:
using System.Security.Claims; using Microsoft.AspNetCore.Authorization; // dotnet add package Microsoft.AspNetCore.Authorization namespace BlazorServer.Common { public static class PolicyTypes { public const string RequireAdmin = nameof(RequireAdmin); public const string RequireCustomer = nameof(RequireCustomer); public const string RequireEmployee = nameof(RequireEmployee); public const string RequireEmployeeOrCustomer = nameof(RequireEmployeeOrCustomer); public static AuthorizationOptions AddAppPolicies(this AuthorizationOptions options) { options.AddPolicy(RequireAdmin, policy => policy.RequireRole(ConstantRoles.Admin)); options.AddPolicy(RequireCustomer, policy => policy.RequireAssertion(context => context.User.HasClaim(claim => claim.Type == ClaimTypes.Role && (claim.Value == ConstantRoles.Admin || claim.Value == ConstantRoles.Customer)) )); options.AddPolicy(RequireEmployee, policy => policy.RequireAssertion(context => context.User.HasClaim(claim => claim.Type == ClaimTypes.Role && (claim.Value == ConstantRoles.Admin || claim.Value == ConstantRoles.Employee)) )); options.AddPolicy(RequireEmployeeOrCustomer, policy => policy.RequireAssertion(context => context.User.HasClaim(claim => claim.Type == ClaimTypes.Role && (claim.Value == ConstantRoles.Admin || claim.Value == ConstantRoles.Employee || claim.Value == ConstantRoles.Customer)) )); return options; } } }
ب) افزودن Policyهای تعریف شده به پروژههای Web API و WASM
پس از تعریف Policyهای مورد نیاز، اکنون نوبت به افزودن آنها به برنامههای Web API:
namespace BlazorWasm.WebApi { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { // ... services.AddAuthorization(options => options.AddAppPolicies()); // ...
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { // ... builder.Services.AddAuthorizationCore(options => options.AddAppPolicies()); // ... } } }
ج) استفاده از Policyهای تعریف شده در برنامهی WASM
اکنون که برنامه قابلیت کار با Policyها را پیدا کرده، میتوان فیلتر Roles سفارشی را حذف و با فیلتر Authorize پالیسی دار جایگزین کرد:
@page "/hotel-room-details/{Id:int}" // ... @* @attribute [Roles(ConstantRoles.Customer, ConstantRoles.Employee)] *@ @attribute [Authorize(Policy = PolicyTypes.RequireEmployeeOrCustomer)]
حتی میتوان از پالیسیها در حین تعریف AuthorizeViewها نیز استفاده کرد:
<AuthorizeView Policy="@PolicyTypes.RequireEmployeeOrCustomer"> <p>You can only see this if you're an admin or an employee or a customer.</p> </AuthorizeView>
React یک کتابخانهی جاوا اسکریپتی، برای ساخت رابطهای کاربری سریع و تعاملی است. توسعهی آن از سال 2011 در فیسبوک شروع شد و در حال حاضر محبوبترین کتابخانهی جاوا اسکریپتی در این ردهاست:
به همین جهت اگر میخواهید رزومهی غنیتری را ارائه دهید، فراگیری 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 است، تشکیل میشود:
state در اینجا همان اطلاعاتی است که قرار است در زمان نمایش این کامپوننت، رندر شود. کار متد render نیز همانطور که از نام آن نیز مشخص است، بیان نحوهی تشکیل و رندر UI است. خروجی این متد، یک React Element است که در حقیقت یک شیء جاوا اسکریپتی خالص است و در نهایت به المانهای DOM، نگاشت میشود. یک React Element، یک DOM Element واقعی نیست؛ بلکه تنها یک شیء جاوا اسکریپتی بیانگر DOM Element، در حافظهاست. بنابراین یک برنامهی React تشکیل شدهاست از لیستی از React Elementها در حافظه که به آن Virtual DOM هم گفته میشود.
مزیت کارکردن با 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 قدیمی موجود بر روی سیستم را به روز رسانی میکند (اگر پیشتر یک node.js قدیمی را نصب و اکنون آنرا به روز رسانی کردهاید).
اگر هم خیلی پیشترها node.js را نصب کردهاید (برای مثال چند سال قبل!)، نصب نگارش جدید آن احتمالا کار نخواهد کرد. حتی عزل و نصب مجدد آن نیز کارساز نیست. در این حالت باید پس از عزل آن، پوشههای قدیمی آنرا یکی یکی یافته و دستی حذف کنید . سپس مجددا آنرا نصب کنید.
در ادامه در خط فرمان و توسط npm، قالب create-react-app را نصب خواهیم کرد:
در اینجا سوئیچ i به معنای install است و g یعنی نصب global و سراسری بستهی create-react-app. نصب سراسری یک بسته یعنی در هر پوشهای میتوان به امکانات آن دسترسی یافت و از آن استفاده کرد. اگر از سوئیچ g استفاده نمیشد، این بسته تنها در پوشهی جاری و با سطح دید مختص به آن، نصب و قابل استفاده میشد.
ابزار دیگری که در این سری از آن استفاده خواهیم کرد، ادیتور بسیار معروف و محبوب VSCode است. پس از دریافت و نصب آن، چند افزونهی زیر را نیز به آن اضافه خواهیم کرد:
برای نصب آنها، پنل extensions را در VSCode، از نوار ابزار کنار صفحهی آن، انتخاب کرده و نامهای فوق را در آن جستجو و سپس نصب کنید.
و یا میتوانید این فایل را اجرا کرده و تعدادی از افزونههای مفید VSCode را یکجا نصب کنید: install-addons.zip
همچنین قابلیت فرمتکردن پس از Save را نیز در VSCode فعال کنید تا پس از هربار Save، اعمال این افزونهها به صورت خودکار صورت گیرد. برای این منظور گزینهی file->preferences->settings را در VSCode انتخاب کرده و سپس save را جستجو کرده و Format On Save را انتخاب کنید:
علاوه بر اینها، جهت کار بهتر با VSCode، بهتر است بررسی کنندههای کدهای جاوا اسکریپتی (static code analyzers) را نیز با اجرای دستور زیر نصب کنید:
پس از این تغییرات، نیاز است یکبار VSCode را بسته و مجددا باز کنید. سپس مجددا گزینهی file->preferences->settings را در VSCode انتخاب کرده و ابتدا eslint را در اینجا جستجو کنید. در صفحهی نمایش تنظیمات آن، گزینهی Auto fix on save آنرا انتخاب نمائید. در آخر در همین قسمت settings، عبارت prettier را انتخاب کنید. در اینجا اگر گزینهی قدیمی یکپارچگی با eslint آن هنوز وجود دارد، آنرا از حالت انتخاب شده خارج کنید (به صورت قرمز و deprecated نمایش داده میشود) تا افزونهی prettier بدون مشکل و خطا کار کند (disable Prettier ESLint integration).
ایجاد قالب اولین برنامهی React
در ادامه برای ایجاد اولین برنامهی React، از بستهی create-react-app که پیشتر آنرا نصب کردیم، استفاده میکنیم. برای این منظور در خط فرمان دستور زیر را صادر کنید:
در اینجا sample-01 یک نام دلخواه است و در حین اجرای این دستور باید به اینترنت متصل باشید تا وابستگیهای مرتبط با پروژه را نیز دریافت کند. برای بار اول، اجرای آن ممکن است کمی طول بکشد. اما از دفعات آتی، چون بستههای مرتبط را در npm-cache سیستم نیز ذخیره میکند، اجرای آن بسیار سریع خواهد بود.
این قالب نه تنها React را نصب میکند، بلکه یک development server را برای اجرا و مشاهدهی سریع برنامه، webpack را برای یکی کردن فایلها (bundling & minification)، Babel را برای کامپایل کدهای فایلهای JSX و ... نیز نصب میکند. بنابراین به این ترتیب، یک پروژهی تنظیم شده و آمادهی استفاده و توسعه را شاهد خواهیم بود که نیازی به تنظیمات اولیهی آن نیست.
پس ایجاد برنامه، وارد پوشهی sample-01 شده و دستور npm start را صادر کنید:
به این ترتیب برنامه بر روی پورت 3000، قابل دسترسی و مشاهده میشود:
development server آن، تغییرات فایلهای برنامه را تحت نظر قرار میدهد و با هر تغییری، به صورت خودکار برنامه را در مرورگر بارگذاری مجدد خواهد کرد.
بررسی ساختار اولین پروژهی React ایجاد شده
ساختار پوشهها و فایلهای مثال اولیهی ایجاد شده توسط قالب create-react-app به صورت زیر است:
البته شما در این تصویر پوشهی node_modules را که در کنار این پوشهها قرار دارد، مشاهده نمیکنید. وجود یک چنین پوشهی سنگینی با هزاران فایل داخل آن، کار نمایشی IDEها را با مشکل مواجه میکند (مصرف حافظهی بالا، به همراه کند شدن شدید آن). اگر نمیخواهید این پوشه نمایش داده شود، در مسیر file->preferences->settings، عبارت npm را جستجو کرده و سپس در قسمت npm: exclude آن، بر روی لینک edit in settings.json کلیک کنید:
و سپس در فایل باز شده، یک چنین تنظیمی را میتوانید اضافه و یا ویرایش و تکمیل کنید:
در ادامه پوشهی public این پروژه را مشاهده میکنید. تمام فایلهایی که قرار است به صورت عمومی توسط برنامه ارائه شوند، مانند favicon.ico و غیره، در این پوشه قرار میگیرند.
در این پوشه بر روی فایل index.html آن کلیک کنید تا بتوان محتوای آنرا بهتر بررسی کرد. برای مثال در ابتدای آن، درج تعدادی متادیتا را که یکی از آنها ذکر manifest.json است، مشاهده میکنید. کار فایل manifest.json، ارائهی یک سری متادیتای خاص مخصوص دستگاههای موبایل است که در آنها بجای favicon.ico، میتوان از تصاویر و یا آیکنهای بزرگتری مانند فایلهای png موجود در پوشهی public، استفاده کرد. در ادامهی این فایل، به تنظیم زیر میرسیم:
div با id مساوی root، محل ارائهی کل برنامهی React ما است.
در پوشهی src و فایل App.js آن، شاهد یک کامپوننت ابتدایی هستید که کار رندر صفحهی مشکی پیشفرض این قالب را انجام میدهد. در این فایل، شاهد بازگشت یک چنین تگهایی هستیم:
احتمالا تابحال چنین return ای را در برنامههای جاوا اسکریپتی مشاهده نکردهاید؛ چون درج آنها در فایلهای js به این نحو، غیرمجاز است. این تگها نه رشتهای هستند و نه HTML خالص. به آن jsx گفته میشود که مخفف JavaScript XML میباشد. کار آن ارائهی ساختار UI ای است که قرار است رندر شود. یک چنین کدی برای اینکه قابل تفسیر و اجرا باشد، از درون کامپایلر ویژهای به نام Babel عبور میکند و تبدیل به کدهای جاوا اسکریپتی خالصی میشود که برای مرورگرها قابل درک و اجرا است.
برای درک بهتر آن به آدرس https://babeljs.io/repl مراجعه کنید. سپس در سمت چپ صفحه، یک قطعه کد jsx را به یک ثابت انتساب دهید:
همانطور که مشاهده میکنید، این قطعه کد jsx (که یک رشتهی معمولی نیست)، توسط Babel به یک قطعه کد کاملا جاوا اسکریپتی قابل درک برای مرورگر تبدیل شدهاست:
بدیهی است نوشتن کدهای jsx، سادهتر از نوشتن قطعه کد فوق است و درک آن نیز به علت شباهت آن به HTML، آسانتر است. به همین جهت در کدهای React، ما از jsx استفاده میکنیم و تفسیر آنرا به Babel واگذار خواهیم کرد.
در پوشهی src، فایل مهم دیگری که وجود دارد، index.js است. این فایل نقطهی آغازین برنامه را مشخص میکند. در قسمتهای بعدی، محتویات این فایل را بیشتر بررسی خواهیم کرد.
در اینجا فایل serviceWorker.js را نیز مشاهده میکنید. این فایل به صورت خودکار توسط قالب create-react-app ایجاد شدهاست و کار آن کمک به ارائهی محلی برنامه، توسط development server آن است. بنابراین ما کاری با این فایل نخواهیم داشت.
نوشتن اولین برنامهی React
به پوشهی src ایجاد شده مراجعه کرده و تمام فایلهای موجود و پیشفرض آنرا حذف کنید. در ادامه خودمان آنها را از صفر ایجاد خواهیم کرد. برای این منظور فایل جدید و خالی src\index.js را ایجاد میکنیم. در ابتدای کار نیاز است تعدادی ماژول React را import کنیم.
در اینجا شیء React از ماژول react دریافت شده و سپس یک ثابت را با یک عبارت jsx مقدار دهی کردهایم. چون از jsx استفاده میکنیم، ذکر import ابتدای فایل الزامی است؛ از این جهت که Babel به کمک آن است که میتواند معادل React.createElement را تولید کند.
اگر هنوز برنامه توسط دستور 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 اصلی، رندر کرده و نمایش دهیم:
برای این منظور نیاز است از متد ReactDOM.render استفاده کرد. این شیء در ماژول react-dom قرار دارد؛ به همین جهت در ابتدای فایل import شدهاست. سپس در متد render آن، ابتدا المانی که قرار است رندر شود ذکر خواهد شد و سپس محل درج آنرا مشخص میکنیم که دقیقا به همان div با id مساوی root در فایل public\index.html اشاره میکند.
اکنون پس از ذخیره سازی فایل index.js، اگر به مرورگر مراجعه کنید، عبارت Hello World! را مشاهده خواهید کرد:
همانطور که در این تصویر نیز مشخص است، المان h1 ما را داخل div ای با id مساوی root، درج کردهاست.
هدف از این مثال ساده، نمایش نحوهی کارکرد React، در پشت صحنه بود. در یک برنامهی واقعی، بجای رندر یک المان ساده در DOM، کار رندر App component را انجام خواهیم داد. کامپوننت App، کامپوننت ریشهای برنامه بوده و میتواند شامل درختی از کامپوننتها که UI نهایی را تشکیل میدهند، شود.
نگاهی به تنظیمات پروژهی ایجاد شده
اگر فایل package.json پروژه را باز کنید، یک چنین بستههایی در آن درج شدهاست:
در اینجا صرفا سه بستهی react، react-dom و react-scripts را در قسمت dependencies مشاهده میکنید که کل Importهای ما را تشکیل میدهند.
بستهی 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 را بررسی میکنیم.
به همین جهت اگر میخواهید رزومهی غنیتری را ارائه دهید، فراگیری 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 را بررسی میکنیم.