اگر به فایل css آن مراجعه کنید، یک چنین عرضهایی قابل مشاهده است:
.span12 { width: 940px; } .span1 { width: 60px; }
.row-fluid .span12 { width: 100%; *width: 99.94680851063829%; }
@gridColumns: 12; @gridColumnWidth: 60px; @gridGutterWidth: 20px;
Features
public class Page { public int Id { get; set; } public string Path { get; set; } public string Content { get; set; } }
public class CustomVirtualPathProvider : VirtualPathProvider { public override bool FileExists(string virtualPath) { return base.FileExists(virtualPath) || FileExistsInDatabase(virtualPath); } public override VirtualFile GetFile(string virtualPath) { return base.FileExists(virtualPath) ? base.GetFile(virtualPath) : new CustomVirtualFile(virtualPath, GetFileFromDatabase(virtualPath)); } private bool FileExistsInDatabase(string virtualPath) { virtualPath = virtualPath.Replace("~", ""); return new DatabaseContext().Pages.Any(v => v.Path == virtualPath); } private byte[] GetFileFromDatabase(string virtualPath) { virtualPath = virtualPath.Replace("~", ""); return Encoding.UTF8.GetBytes(new DatabaseContext().Pages.First(v => v.Path == virtualPath).Content); } }
public class CustomVirtualFile : VirtualFile { private readonly byte[] _content; public CustomVirtualFile(string virtualPath, byte[] content) : base(virtualPath) { _content = content; } public override Stream Open() { return new MemoryStream(_content); } }
حال نوبت ثبت کلاس CustomVirtualPathProvider جهت استفادهی خودکار از آن میباشد. برای این کار در تابع Application_Start موجود در فایل Global.asax.cs دستور زیر را اضافه مینماییم:
protected void Application_Start() { HostingEnvironment.RegisterVirtualPathProvider(new CustomVirtualPathProvider()); //... }
داخل یک Image چیست؟
در قسمت قبل دریافتیم که یک Image، از کل User Space مورد نیاز جهت اجرای یک برنامه تشکیل میشود. در ادامه میخواهیم بررسی کنیم، این ادعا تا چه حد صحت دارد و یک Image، واقعا از چه اجزائی تشکیل میشود.
با استفاده از دستور docker images میتوان لیست imageهای دریافت شده را به همراه tagهای مرتبط با هر کدام، مشاهده کرد. سپس با استفاده از دستور docker save میتوان image ای خاص را export کرد؛ برای مثال:
docker save microsoft/iis:nanoserver -o iis.tar
البته ویندوز در حالت استاندارد آن، قابلیت کار با فایلهای tar را ندارد. هرچند میتوان از نرم افزارهای ثالثی مانند win-rar و یا 7zip برای انجام اینکار استفاده کرد، اما تمام Linux Containers، به همراه ابزار کمکی tar، برای استخراج محتوای این نوع فایلها نیز هستند. بنابراین نیاز است از حالت Windows Containers به Linux Containers سوئیچ کرد. برای اینکار فقط کافی بر روی آیکن Docker در قسمت Tray Icons ویندوز کلیک راست نمود و گزینهی Switch to Linux Containers را انتخاب کرد. اکنون اگر دستور docker images را صادر کنیم، تنها لیست imageهای لینوکسی از پیش دریافت شده را میتوان مشاهده کرد.
در ادامه، image یکی از سبکترین توزیعهای لینوکسی را به نام alpine، دریافت میکنیم که فقط 5 مگابایت حجم دارد؛ چون آنچنان فایلی در user space آن وجود ندارد. به همین جهت برای دریافت آن، دستور docker pull alpine را صادر میکنیم. اکنون اگر دستور docker images را صادر کنیم، این image جدید در لیست خروجی آن قابل مشاهدهاست.
سپس چون میخواهیم یک shell را در داخل این container اجرا کنیم (مانند اجرای کنسول PowerShell قسمت قبل)، نیاز است دستور docker run آنرا با سوئیچ it اجرا کنیم:
docker run -it alpine sh
اکنون میخواهیم به فایل iis.tar ای که export کردیم از اینجا دسترسی پیدا کنیم. این فایل نیز بر روی فایل سیستم ویندوز 10 تولید شدهاست. به همین جهت نیاز است بتوان به این فایل خارج از container جاری دسترسی پیدا کرد.
روش به اشتراک گذاری فایل سیستم میزبان با کانتینرها
برای اینکه از داخل یک container به فایلی در خارج از آن دسترسی پیدا کنیم، باید درایو آن فایل خارجی را اصطلاحا mount کرد؛ دقیقا مانند اتصال یک USB Stick به سیستم و اضافه شدن آن به صورت یک درایو جدید. در Docker، اینکار توسط مفهومی به نام Volumes انجام میشود. برای این منظور بر روی آیکن Docker در قسمت Tray Icons ویندوز کلیک راست کرده و گزینهی Settings را انتخاب کنید. البته این تصویر را در حالت کار با Linux Containers مشاهده میکنید:
در اینجا درایو C را انتخاب و Apply کنید و سپس نام کاربری و کلمهی عبور ویندوز خود را وارد نمائید، تا مجوز دسترسی به این درایو، به این Container داده شود.
سپس دستور زیر را اجرا کنید:
docker run --rm -v c:/Users:/data alpine ls /data
- عبارت c:/Users:/data به این معنا است که پوشهی C:\users ویندوز را به مسیر data/ داخل container لینوکسی نگاشت کن. به همین جهت بین این دو قسمت یک : وجود دارد و مسیرهای لینوکسی فاقد نام درایو خاصی هستند.
در این دستور میتوان پوشهای خاص مانند c:/Users/Vahid:/data و یا فایلی خاص را مانند c:/Users/Vahid/iis.tar:/data نیز نگاشت کرد. مزیت آن کاهش سطح دسترسی Container به فایلهای سیستم میزبان است.
- این دستور با اجرای دستور ls، محتوای پوشهی C:\users را لیست میکند.
کار با فایلهای سیستم میزبان از داخل یک Container
در ادامه دستور فوق را به صورت زیر اجرا میکنیم که در آن فایل iis.tar میزبان، در داخل container در مسیر data/ آن قابل دسترسی شدهاست و همچنین بجای ls، دستور sh صادر شدهاست تا به shell آن دسترسی پیدا کنیم. به همین جهت نیاز به سوئیچ it نیز وجود دارد:
docker run --rm -it -v c:/Users/vahid/iis.tar:/data alpine sh
tar -tf /data/iis.tar
در اینجا حداقل هفت پوشه را مشاهده میکنید که داخل هر کدام فایلی به نام layer.tar قرار دارد؛ لایههایی که این image را ایجاد میکنند.
پس از این، اگر دستور استخراج این فایلها را صادر کنیم (t برای نمایش لیست محتویات است و x برای استخراج آنها):
mkdir /data/extract tar -xf /data/iis.tar -C /data/extract
انتقال فایلها از Container به سیستم میزبان
البته نکتهی مهم اینجا است که این پوشهی /data/extract/ صرفا داخل دیسک مجازی این container ایجاد شدهاست و در سمت ویندوز قابل دسترسی و مشاهده نیست.
برای رفع این مشکل، اینبار دستور tar -xf را به صورت زیر اجرا میکنیم:
docker run --rm -it -v c:/Users/vahid/:/data alpine tar -xf /data/iis.tar -C /data/iis
در اینجا اگر دستور tar را بر روی این لایهها اجرا کنیم، در نهایت به یک چنین خروجیهایی خواهیم رسید:
که بسیار شبیه به درایو C یک سیستم ویندوزی است. بنابراین این لایه، همان OS Apps & Libs را به همراه دارد که در قسمت قبل با آن آشنا شدیم.
یک مثال دیگر: اجرای ffmpeg توسط docker
اگر خاطرتان باشد بحث docker را در قسمت اول با معرفی ffmpeg و سخت بودن اجرای آن آغاز کردیم. در ادامه میخواهیم image آنرا دریافت و سپس با تبدیل آن به یک container، کار تبدیل فرمتهای ویدیویی را انجام دهیم:
docker run --rm --volume ${pwd}:/output jrottenberg/ffmpeg -i http://site.com/file.mp4 /output/file.gif
- سوئیچ rm کار حذف container ایجاد شده را پس از اتمام کار آن انجام میدهد.
- سپس مسیرجاری (پوشهی جاری در سیستم میزبان که دستور فوق را اجرا میکند) به مسیر output/ کانتینر نگاشت شدهاست.
- در ادامه مخزن dockerhub ای که ffmpeg قرار است از آن دریافت و اجرا شود، مشخص شدهاست.
- در آخر پارامترهای کار با ffmpeg برای دریافت یک فایل mp4 آنلاین و تبدیل آن به یک فایل animated gif محلی، ذکر شدهاند. این فایل در مسیر output کانتینر ایجاد میشود که در نهایت با میزبان، به اشتراک گذاشته خواهد شد.
- خروجی نهایی تولید شده را در سیستم میزبان در همان پوشهای که دستور فوق را صادر کردهاید، مشاهده خواهید کرد.
تکمیل فرم ثبت نام کاربران
در ادامه کدهای کامل کامپوننت فرم ثبت نام کاربران را مشاهده میکنید:
@page "/registration" @inject IClientAuthenticationService AuthenticationService @inject NavigationManager NavigationManager <EditForm Model="UserForRegistration" OnValidSubmit="RegisterUser" class="pt-4"> <DataAnnotationsValidator /> <div class="py-4"> <div class=" row form-group "> <div class="col-6 offset-3 "> <div class="card border"> <div class="card-body px-lg-5 pt-4"> <h3 class="col-12 text-success text-center py-2"> <strong>Sign Up</strong> </h3> @if (ShowRegistrationErrors) { <div> @foreach (var error in Errors) { <p class="text-danger text-center">@error</p> } </div> } <hr style="background-color:aliceblue" /> <div class="py-2"> <InputText @bind-Value="UserForRegistration.Name" class="form-control" placeholder="Name..." /> <ValidationMessage For="(()=>UserForRegistration.Name)" /> </div> <div class="py-2"> <InputText @bind-Value="UserForRegistration.Email" class="form-control" placeholder="Email..." /> <ValidationMessage For="(()=>UserForRegistration.Email)" /> </div> <div class="py-2 input-group"> <div class="input-group-prepend"> <span class="input-group-text"> +1</span> </div> <InputText @bind-Value="UserForRegistration.PhoneNo" class="form-control" placeholder="Phone number..." /> <ValidationMessage For="(()=>UserForRegistration.PhoneNo)" /> </div> <div class="form-row py-2"> <div class="col"> <InputText @bind-Value="UserForRegistration.Password" type="password" id="password" placeholder="Password..." class="form-control" /> <ValidationMessage For="(()=>UserForRegistration.Password)" /> </div> <div class="col"> <InputText @bind-Value="UserForRegistration.ConfirmPassword" type="password" id="confirm" class="form-control" placeholder="Confirm Password..." /> <ValidationMessage For="(()=>UserForRegistration.ConfirmPassword)" /> </div> </div> <hr style="background-color:aliceblue" /> <div class="py-2"> @if (IsProcessing) { <button type="submit" class="btn btn-success btn-block disabled"><i class="fas fa-sign-in-alt"></i> Please Wait...</button> } else { <button type="submit" class="btn btn-success btn-block"><i class="fas fa-sign-in-alt"></i> Register</button> } </div> </div> </div> </div> </div> </div> </EditForm> @code{ UserRequestDTO UserForRegistration = new UserRequestDTO(); bool IsProcessing; bool ShowRegistrationErrors; IEnumerable<string> Errors; private async Task RegisterUser() { ShowRegistrationErrors = false; IsProcessing = true; var result = await AuthenticationService.RegisterUserAsync(UserForRegistration); if (result.IsRegistrationSuccessful) { IsProcessing = false; NavigationManager.NavigateTo("/login"); } else { IsProcessing = false; Errors = result.Errors; ShowRegistrationErrors = true; } } }
- مدل این فرم بر اساس UserRequestDTO تشکیل شدهاست که همان شیءای است که اکشن متد ثبت نام سمت Web API انتظار دارد.
- در این کامپوننت به کمک سرویس IClientAuthenticationService که آنرا در قسمت قبل تهیه کردیم، شیء نهایی متصل به فرم، به سمت Web API Endpoint ثبت نام ارسال میشود.
- در اینجا روشی را جهت غیرفعال کردن یک دکمه، پس از کلیک بر روی آن مشاهده میکنید. میتوان پس از کلیک بر روی دکمهی ثبت نام، با true کردن یک فیلد مانند IsProcessing، بلافاصله دکمهی جاری را برای مثال با ویژگی disabled در صفحه درج کرد و یا حتی آنرا از صفحه حذف کرد. این روش، یکی از روشهای جلوگیری از کلیک چندبارهی کاربر، بر روی یک دکمهاست.
- فرم جاری، خطاهای اعتبارسنجی مخصوص Identity سمت سرور را نیز نمایش میدهد که حاصل از ارسال آنها توسط اکشن متد ثبت نام است:
- پس از پایان موفقیت آمیز ثبت نام، کاربر را به سمت فرم لاگین هدایت میکنیم.
تکمیل فرم ورود به سیستم کاربران
در ادامه کدهای کامل کامپوننت فرم ثبت نام کاربران را مشاهده میکنید:
@page "/login" @inject IClientAuthenticationService AuthenticationService @inject NavigationManager NavigationManager <div id="logreg-forms"> <h1 class="h3 mb-3 pt-3 font-weight-normal text-primary" style="text-align:center;">Sign In</h1> <EditForm Model="UserForAuthentication" OnValidSubmit="LoginUser"> <DataAnnotationsValidator /> @if (ShowAuthenticationErrors) { <p class="text-center text-danger">@Errors</p> } <InputText @bind-Value="UserForAuthentication.UserName" id="email" placeholder="Email..." class="form-control mb-2" /> <ValidationMessage For="(()=>UserForAuthentication.UserName)"></ValidationMessage> <InputText @bind-Value="UserForAuthentication.Password" type="password" placeholder="Password..." id="password" class="form-control mb-2" /> <ValidationMessage For="(()=>UserForAuthentication.Password)"></ValidationMessage> @if (IsProcessing) { <button type="submit" class="btn btn-success btn-block disabled"><i class="fas fa-sign-in-alt"></i> Please Wait...</button> } else { <button type="submit" class="btn btn-success btn-block"><i class="fas fa-sign-in-alt"></i> Sign in</button> } <a href="/registration" class="btn btn-primary text-white mt-3"><i class="fas fa-user-plus"></i> Register as a new user</a> </EditForm> </div> @code { AuthenticationDTO UserForAuthentication = new AuthenticationDTO(); bool IsProcessing = false; bool ShowAuthenticationErrors; string Errors; string ReturnUrl; private async Task LoginUser() { ShowAuthenticationErrors = false; IsProcessing = true; var result = await AuthenticationService.LoginAsync(UserForAuthentication); if (result.IsAuthSuccessful) { IsProcessing = false; var absoluteUri = new Uri(NavigationManager.Uri); var queryParam = HttpUtility.ParseQueryString(absoluteUri.Query); ReturnUrl = queryParam["returnUrl"]; if (string.IsNullOrEmpty(ReturnUrl)) { NavigationManager.NavigateTo("/"); } else { NavigationManager.NavigateTo("/" + ReturnUrl); } } else { IsProcessing = false; Errors = result.ErrorMessage; ShowAuthenticationErrors = true; } } }
- مدل این فرم بر اساس AuthenticationDTO تشکیل شدهاست که همان شیءای است که اکشن متد لاگین سمت Web API انتظار دارد.
- در این کامپوننت به کمک سرویس IClientAuthenticationService که آنرا در قسمت قبل تهیه کردیم، شیء نهایی متصل به فرم، به سمت Web API Endpoint ثبت نام ارسال میشود.
- در اینجا نیز همانند فرم ثبت نام، پس از کلیک بر روی دکمهی ورود به سیستم، با true کردن یک فیلد مانند IsProcessing، بلافاصله دکمهی جاری را با ویژگی disabled در صفحه درج کردهایم تا از کلیک چندبارهی کاربر، جلوگیری شود.
- این فرم، خطاهای اعتبارسنجی مخصوص Identity سمت سرور را نیز نمایش میدهد که حاصل از ارسال آنها توسط اکشن متد لاگین است:
همانطور که مشاهده میکنید، مقدار JWT تولید شدهی پس از لاگین و همچنین مشخصات کاربر دریافتی از Web Api، جهت استفادههای بعدی، در Local Storage مرورگر درج شدهاند.
تغییر منوی راهبری سایت، بر اساس وضعیت لاگین شخص
تا اینجا قسمتهای ثبت نام و ورود به سیستم را تکمیل کردیم. در ادامه نیاز داریم تا منوی سایت را هم بر اساس وضعیت اعتبارسنجی شخص، تغییر دهیم. برای مثال اگر شخصی به سیستم وارد شدهاست، باید در منوی سایت، لینک خروج و نام خودش را مشاهده کند و نه مجددا لینکهای ثبتنام و لاگین را. جهت تغییر منوی راهبری سایت، کامپوننت Shared\NavMenu.razor را گشوده و لینکهای قبلی ثبتنام و لاگین را با محتوای زیر جایگزین میکنیم:
<AuthorizeView> <Authorized> <li class="nav-item p-0"> <NavLink class="nav-link" href="#"> <span class="p-2"> Hello, @context.User.Identity.Name! </span> </NavLink> </li> <li class="nav-item p-0"> <NavLink class="nav-link" href="logout"> <span class="p-2"> Logout </span> </NavLink> </li> </Authorized> <NotAuthorized> <li class="nav-item p-0"> <NavLink class="nav-link" href="registration"> <span class="p-2"> Register </span> </NavLink> </li> <li class="nav-item p-0"> <NavLink class="nav-link" href="login"> <span class="p-2"> Login </span> </NavLink> </li> </NotAuthorized> </AuthorizeView>
تکمیل قسمت خروج از سیستم
اکنون که لینک logout را در منوی سایت، پس از ورود به سیستم نمایش میدهیم، میتوان کدهای کامپوننت آنرا (Pages\Authentication\Logout.razor) به صورت زیر تکمیل کرد:
@page "/logout" @inject IClientAuthenticationService AuthenticationService @inject NavigationManager NavigationManager @code { protected async override Task OnInitializedAsync() { await AuthenticationService.LogoutAsync(); NavigationManager.NavigateTo("/"); } }
مشکل! پس از کلیک بر روی logout، هرچند میتوان مشاهده کرد که اطلاعات Local Storage به درستی حذف شدهاند، اما ... پس از هدایت به صفحهی اصلی برنامه، هنوز هم لینک logout و نام کاربری شخص نمایان هستند و به نظر هیچ اتفاقی رخ ندادهاست!
علت اینجا است که AuthenticationStateProvider سفارشی را که تهیه کردیم، فاقد اطلاع رسانی تغییر وضعیت است:
namespace BlazorWasm.Client.Services { public class AuthStateProvider : AuthenticationStateProvider { // ... public void NotifyUserLoggedIn(string token) { var authenticatedUser = new ClaimsPrincipal( new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType") ); var authState = Task.FromResult(new AuthenticationState(authenticatedUser)); base.NotifyAuthenticationStateChanged(authState); } public void NotifyUserLogout() { var authState = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); base.NotifyAuthenticationStateChanged(authState); } } }
namespace BlazorWasm.Client.Services { public class ClientAuthenticationService : IClientAuthenticationService { private readonly HttpClient _client; private readonly ILocalStorageService _localStorage; private readonly AuthenticationStateProvider _authStateProvider; public ClientAuthenticationService( HttpClient client, ILocalStorageService localStorage, AuthenticationStateProvider authStateProvider) { _client = client; _localStorage = localStorage; _authStateProvider = authStateProvider; } public async Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication) { // ... if (response.IsSuccessStatusCode) { //... ((AuthStateProvider)_authStateProvider).NotifyUserLoggedIn(result.Token); return new AuthenticationResponseDTO { IsAuthSuccessful = true }; } //... } public async Task LogoutAsync() { //... ((AuthStateProvider)_authStateProvider).NotifyUserLogout(); } } }
- ابتدا AuthenticationStateProvider به سازندهی کلاس تزریق شدهاست.
- سپس در حین لاگین موفق، متد NotifyUserLoggedIn آن فراخوانی شدهاست.
- در آخر پس از خروج از سیستم، متد NotifyUserLogout فراخوانی شدهاست.
پس از این تغییرات اگر بر روی لینک logout کلیک کنیم، این گزینه به درستی عمل کرده و اینبار شاهد نمایش مجدد لینکهای لاگین و ثبت نام خواهیم بود.
محدود کردن دسترسی به صفحات برنامه بر اساس نقشهای کاربران
پس از ورود کاربر به سیستم و تامین AuthenticationState، اکنون میخواهیم تنها این نوع کاربران اعتبارسنجی شده بتوانند جزئیات اتاقها (برای شروع رزرو) و یا صفحهی نمایش نتیجهی پرداخت را مشاهده کنند. البته نمیخواهیم صفحهی نمایش لیست اتاقها را محدود کنیم. برای این منظور ویژگی Authorize را به ابتدای تعاریف کامپوننتهای PaymentResult.razor و RoomDetails.razor، اضافه میکنیم:
@attribute [Authorize(Roles = ConstantRoles.Customer)]
@using Microsoft.AspNetCore.Authorization
تکمیل مشخصات هویتی شخصی که قرار است اتاقی را رزرو کند
پیشتر در فرم RoomDetails.razor، اطلاعات ابتدایی کاربر را مانند نام او، دریافت میکردیم. اکنون با توجه به محدود شدن این کامپوننت به کاربران لاگین کرده، میتوان اطلاعات کاربر وارد شدهی به سیستم را نیز به صورت خودکار بارگذاری و تکمیل کرد:
@page "/hotel-room-details/{Id:int}" // ... @code { // ... protected override async Task OnInitializedAsync() { try { HotelBooking.OrderDetails = new RoomOrderDetailsDTO(); if (Id != null) { // ... if (await LocalStorage.GetItemAsync<UserDTO>(ConstantKeys.LocalUserDetails) != null) { var userInfo = await LocalStorage.GetItemAsync<UserDTO>(ConstantKeys.LocalUserDetails); HotelBooking.OrderDetails.UserId = userInfo.Id; HotelBooking.OrderDetails.Name = userInfo.Name; HotelBooking.OrderDetails.Email = userInfo.Email; HotelBooking.OrderDetails.Phone = userInfo.PhoneNo; } } } catch (Exception e) { await JsRuntime.ToastrError(e.Message); } }
public async Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details) { // details.UserId = "unknown user!";
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-32.zip
در این مقاله قصد داریم با استفاده از جاوااسکریپت خالص، یک برنامهی ساده را با الگوی MVC انجام دهیم. این برنامه، عملیات CRUD را پیاده سازی میکند و تنها به سه فایل index.html , script.js , style.css نیاز دارد و از هیچ کتابخانه یا فریم ورک دیگری در آن استفاده نمیکنیم.
- M مخفف Model میباشد و کار مدیریت دادهها را بر عهده دارد.
- V مخفف View میباشد و وظیفهی نمایش دادهها به کاربر را بر عهده دارد.
- C مخفف Controller میباشد و پل ارتباطی بین Model و View میباشد و مدیریت درخواستها را بر عهده دارد.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>الگوی MVC در جاوااسکریپت</title> <link rel="stylesheet" href="style.css"> </head> <body> <div id="root"></div> <script src="script.js"></script> </body> </html>
*, *::before, *::after { box-sizing: border-box } html { color: #444; } #root { max-width: 450px; margin: 2rem auto; padding: 0 1rem; } form { display: flex; margin-bottom: 2rem; } [type="text"], button { display: inline-block; -webkit-appearance: none; padding: .5rem 1rem; border: 2px solid #ccc; border-radius: 4px; } button { cursor: pointer; background: #007bff; color: white; border: 2px solid #007bff; margin: 0 .5rem; } [type="text"] { width: 100%; } [type="text"]:active, [type="text"]:focus { outline: 0; border: 2px solid #007bff; } [type="checkbox"] { margin-right: 1rem; } h1 { color: #222; } ul { padding: 0; } li { display: flex; align-items: center; padding: 1rem; margin-bottom: 1rem; background: #f4f4f4; border-radius: 4px; } li span { display: inline-block; padding: .5rem; width: 250px; border-radius: 4px; border: 2px solid transparent; } li span:hover { background: rgba(179, 215, 255, 0.52); } li span:focus { outline: 0; border: 2px solid #007bff; background: rgba(179, 207, 255, 0.52) }
class Model { constructor() {} } class View { constructor() {} } class Controller { constructor(model, view) { this.model = model this.view = view } } const app = new Controller(new Model(), new View())
class Model { constructor() { // یک آرایه از اطلاعات پیش فرض this.todos = [{ id: 1, text: 'Run a marathon', complete: false }, { id: 2, text: 'Plant a garden', complete: false }, ] } // متدی برای افزودن آیتم جدید به آرایه addTodo(todoText) { const todo = { id: this.todos.length > 0 ? this.todos[this.todos.length - 1].id + 1 : 1, text: todoText, complete: false, } this.todos.push(todo) } // متدی برای بروزسانی آیتم مورد نظر editTodo(id, updatedText) { this.todos = this.todos.map(todo => todo.id === id ? { id: todo.id, text: updatedText, complete: todo.complete } : todo ) } // انجام میدهد filter با استفاده از متد id تابعی که عملیات حذف را بوسیله فیلد deleteTodo(id) { this.todos = this.todos.filter(todo => todo.id !== id) } // متدی که در آن مشخص میکنیم کار مد نظرانجام شده یا خیر toggleTodo(id) { this.todos = this.todos.map(todo => todo.id === id ? { id: todo.id, text: todo.text, complete: !todo.complete } : todo ) } }
app.model.addTodo('Take a nap') console.log(app.model.todos)
در حال حاضر با هر بار reload شدن صفحه، فقط اطلاعات پیش فرض، درون آرایه todos قرار میگیرد؛ ولی در ادامه آن را در local storage ذخیره میکنیم.
برای ساختن قسمت View، از جاوااسکریپت استفاده میکنیم و DOM را تغییر میدهیم. البته اینکار را بدون استفاده از JSX و یا یک templating language انجام خواهیم داد. قسمتهای دیگر برنامه مانند Controller و Model نباید درگیر تغییرات DOM یا CSS یا عناصر HTML باشند و تمام این موارد توسط View هندل میشود. کد View به نحو زیر خواهد بود:
class View { constructor() {} // ایجاد یک المنت با کلاسهای استایل دلخواه createElement(tag, className) { const element = document.createElement(tag) if (className) element.classList.add(className) return element } // DOM انتخاب و گرفتن آیتمی خاص از getElement(selector) { const element = document.querySelector(selector) return element } }
سپس قسمت سازنده کلاس View را تغییر میدهیم و تمام المنتهای مورد نیاز را در آن ایجاد میکنیم:
- ارجاعی به المنتی با آیدی root
- تگ h1 برای عنوان
- یک form، input و دکمهای برای افزودن آیتمی جدید به آرایهی todos
- یک المنت ul برای نمایش آیتمهای todos
constructor() { // root ارجاعی به المنتی با آیدی this.app = this.getElement('#root') // عنوان برنامه this.title = this.createElement('h1') this.title.textContent = 'Todos' // فرم ، اینپوت ورودی و دکمه this.form = this.createElement('form') this.input = this.createElement('input') this.input.type = 'text' this.input.placeholder = 'Add todo' this.input.name = 'todo' this.submitButton = this.createElement('button') this.submitButton.textContent = 'Submit' // برای نمایش عناط آرایه یا همان لیست کارها this.todoList = this.createElement('ul', 'todo-list') // افزودن اینپوت ورودی و دکمه به فرم this.form.append(this.input, this.submitButton) // ایجاد شده است app که اینجا ارجاعی به آن بنام root اضافه کردن تمام آیتمهای بالا در المنتی با آیدی this.app.append(this.title, this.form, this.todoList) }
get _todoText() { return this.input.value } _resetInput() { this.input.value = '' }
displayTodos(todos){ //... }
متد displayTodos یک المنت ul و liهایی را به تعداد عناصر todos ایجاد میکند و آنها را نمایش میدهد. هر زمانکه تغییراتی مانند اضافه شدن، حذف و ویرایش در todos صورت گیرد، این متد دوباره فراخوانی میشود و لیست جدید را نمایش میدهد. محتوای متد dispayTodos به شکل زیر خواهد بود:
displayTodos(todos) { // حذف تمام نودها while (this.todoList.firstChild) { this.todoList.removeChild(this.todoList.firstChild) } // اگر هیچ آیتمی در آرایه نبود این پاراگراف با متن پیش فرض نمایش داده میشود if (todos.length === 0) { const p = this.createElement('p') p.textContent = 'Nothing to do! Add a task?' this.todoList.append(p) } else { // وعناصرمربوطه را ایجاد میکند liاگه درون آرایه آیتمی قرار دارد پس به ازای آن یک عنصر todos.forEach(todo => { const li = this.createElement('li') li.id = todo.id const checkbox = this.createElement('input') checkbox.type = 'checkbox' checkbox.checked = todo.complete const span = this.createElement('span') span.contentEditable = true span.classList.add('editable') if (todo.complete) { const strike = this.createElement('s') strike.textContent = todo.text span.append(strike) } else { span.textContent = todo.text } const deleteButton = this.createElement('button', 'delete') deleteButton.textContent = 'Delete' li.append(checkbox, span, deleteButton) // نود ایجاد شده به لیست اضافه میکند this.todoList.append(li) }) } // برای خطایابی و نمایش در کنسول console.log(todos) }
در نهایت قسمت Controller را که پل ارتباطی بین View و Model میباشد، کامل میکنیم. اولین تغییراتی که در کلاس Controller ایجاد میکنیم، استفاده از متد displayTodos در سازندهی این کلاس میباشد و با هر بار تغییر این متد، دوباره فراخوانی میشود:
class Controller { constructor(model, view) { this.model = model this.view = view // نمایش اطلاعات پیش فرض this.onTodoListChanged(this.model.todos) } onTodoListChanged = todos => { this.view.displayTodos(todos) } }
چهار تابعی را که در قسمت Model ایجاد نمودیم و کار ویرایش، حذف، افزودن و اتمام کار را انجام میدادند، در کلاس کنترلر آنها را هندل میکنیم و زمانیکه کاربر دکمهای را برای افزودن یا تیک حذف آیتمی، زد، تابع مربوطه توسط کنترلر در Model فراخوانی شود:
handleAddTodo = todoText => { this.model.addTodo(todoText) } handleEditTodo = (id, todoText) => { this.model.editTodo(id, todoText) } handleDeleteTodo = id => { this.model.deleteTodo(id) } handleToggleTodo = id => { this.model.toggleTodo(id) }
چون کنترلر نمیتواند بصورت مستقیم فراخوانی شود و این توابع باید درون DOM تنظیم شوند تا به ازای رخدادهایی همچون click و change، فراخوانی شوند. پس از این توابع در قسمت View استفاده میکنیم و به کلاس View، موارد زیر را اضافه میکنیم:
bindAddTodo(handler) { this.form.addEventListener('submit', event => { event.preventDefault() if (this._todoText) { handler(this._todoText) this._resetInput() } }) } bindDeleteTodo(handler) { this.todoList.addEventListener('click', event => { if (event.target.className === 'delete') { const id = parseInt(event.target.parentElement.id) handler(id) } }) } bindToggleTodo(handler) { this.todoList.addEventListener('change', event => { if (event.target.type === 'checkbox') { const id = parseInt(event.target.parentElement.id) handler(id) } }) }
برای bind کردن این متدها در کلاس Controller، کدهای زیر را اضافه میکنیم:
this.view.bindAddTodo(this.handleAddTodo) this.view.bindDeleteTodo(this.handleDeleteTodo) this.view.bindToggleTodo(this.handleToggleTodo)
برای ذخیره اطلاعات در local storage، در سازنده کلاس Model، کد زیر را اضافه میکنیم:
this.todos = JSON.parse(localStorage.getItem('todos')) || []
متد دیگری هم در کلاس Model برای بهروز رسانی مقادیر local storage قرار میدهیم:
_commit(todos) { this.onTodoListChanged(todos) localStorage.setItem('todos', JSON.stringify(todos)) }
متدی هم برای تغییراتی که هر زمان بر روی todos اتفاق میافتد، فراخوانی شود:
deleteTodo(id) { this.todos = this.todos.filter(todo => todo.id !== id) this._commit(this.todos) }
این مقاله صرفا جهت آشنایی و نمونه کدی از پیاده سازی الگوی MVC در جاوااسکریپت میباشد.
معرفی List Patterns Matching در C# 11
در C# 11 میتوان یک رشتهی ثابت را به صورت <ReadOnlySpan<char تعریف کرد و سپس بر روی آن عملیات patterns matching را انجام داد:
ReadOnlySpan<char> strSpan = "Vahid"; if (strSpan is "Vahid") { Console.WriteLine("Hey, Vahid"); }
در اینجا قابلیتهای List Patterns Matching ذکر شدهی در بحث جاری هم قابل استفاده هستند. برای مثال میتوان بررسی کرد که آیا رشتهی ثابت تعریف شده، با حرف خاصی شروع میشود (صرفنظر از مابقی حروف آن) و یا خیر:
if (strSpan is ['V', ..]) { Console.WriteLine("The name starts with V"); }
EXCEPTION: TypeError: Cannot read property 'isSkipSelf' of null
var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); };
ضمن اینکه در اینجا قرار نیست فایلی یا فایلهایی با همان نامهای اصلی ارسال شوند. به این دلیل که هدف اصلی این کار bundling، کم کردن تعداد درخواستها به سرور نیز هست. برای مثال اگر سایت شما از 8 فایل اسکریپت استفاده میکند، در اینجا تبدیل به یک فایل خواهد شد که سبب کم شدن تعداد درخواستها و سریعتر شدن نمایش صفحات میشود.