XMLHttpRequest رابطی است که به شما امکان نقل و انتقالات را از سمت کاربر، به سمت سرور و سپس دریافت پاسخ آن را میدهد. این رابط طوری طراحی شدهاست که دیگر برای این جابجایی نیازی به بارگزاری مجدد کل صفحه نباشد و قسمتی از اطلاعات صفحات به روز شوند، مزاحمتی برای کاربر ایجاد نشود. به همین دلیل از این رابط، در پشت صحنههای عملیات ایجکسی استفاده زیادی میشود. در این مقاله با استفاده از خصوصیتی به نام request.IsAjax بررسی میشود که آیا درخواست رسیده به سرور از نوع ایجکسی است یا خیر. اگر به سورس نوشته شده این متد نگاه دقیقتری بیندازیم، متوجه میشویم کاری که این متد انجام میدهد، در واقع در یک خط خلاصه میشود و آن بررسی هدری برای وجود درخواست از نوع XMLHttpRequest است:
return request.Headers["X-Requested-With"] == "XMLHttpRequest";
یکی از متدهای این رابط، متد ارسال آن (send) میباشد که میتواند رابطی به نام formData را انتقال دهد و این رابط از نوع مجموعهای از کلید و مقدارهاست. این رابط زمانی به کار گرفته میشود که انکدینگ فرم خود را بر روی multipart/form-data قرار داده باشید. این ساختار میتواند توسط دستور for of بررسی گردد. برای آشنایی بیشتر با متدهای آن این صفحه را مطالعه فرمایید.
هنگام ارسال فایل در حالت postback، ما فرم را بر روی multipart قرار میدهیم تا امکان ارسال آن توسط formData مهیا شود. ولی از آنجاکه ما از ایجکس استفاده میکنیم، بهتر است که خودمان مستقیما از این ساختار استفاده کنیم.
بخشی از فرم Html
<div> <label>تصویر</label> <div> <input id="picture" type="file" data-buttonText="انتخاب تصویر"> </div> </div> <div> <label>کد ملی</label> <div> <input id="txtNationalCode" required="" maxlength="10" type="text"> </div> </div> <div> <label>نام</label> <div> <input id="txtName" type="text" maxlength="50" required=""> </div> </div> <div class="form-group"> <div class="col-sm-4 col-sm-offset-2"> <button class="btn btn-primary" id="btnSubmit" type="submit">ذخیره</button> <button class="btn btn-white" id="btnClear" type="submit">لغو</button> </div> </div>
سپس کد جی کوئری زیر را مینویسیم:
var formData = new FormData(); formData.append('FirstName', $("#txtName").val()); formData.append('NationalCode', $("#txtNationalCode").val()); jQuery.each($('#picture')[0].files, function (i, file) { formData.append('picture-'+i, file); }); $.ajax({ type: "POST", dataType: "json", url: address, data: formData, success: function (data) { //..... }, error: function (data) { //...... } });
توجه به این نکته ضروری است و با توجه کدهایی که در نت دیدم و بسیاری از آن حتی به عنوان پاسخ صحیح در نظر گرفته شده بودند این است که شیء FormData شامل هیچ سازندهای نیست و باید با استفاده از متد append آنها را اضافه کنید.
الف) تبدیلگر HTML به XAML نوشته شده توسط خود مایکروسافت
ب) پروژه فوق العاده جالب HTML Renderer
اجازه بدهید قبل از هر چیزی به دو مفهوم اصلی در IIS بپردزیم :
1. Worker Process
2. Application Pool
پروسههای کارگر w3wp.exe وظیفهی اجرای برنامههای asp.net را در IIS ، به عهده دارند. این پروسهها مسئولیت پردازش تمامی درخواست و پاسخها از/به کلاینت را دارند. هر کاری که باید در asp.net انجام بشود، توسط اینها صورت میگیرد. به بیان سادهتر این پروسهها قلب برنامههای ASP.Net بر روی IIS هستند .
Application Pool:این پولها در واقع ظرفی یا در برگیرنده ای برای پروسههای کارگر به حساب میآیند. این پولها پروسههای کارگر را از هم جدا و دسته بندی میکنند تا قابلیت اعتماد، امنیت و در دسترس بودن بدهند. موقعی که یک پروسه یا حتی یک پول دچار مشکل میشود، این اطمینان داده میشود که تاثیری بر دیگر پولها یا پروسههای کارگر، ندارد. یعنی موقعی که یک web application دچار مشکل شود، هیچ تاثیری بر اجرای web applicationهای دیگر ندارد. به یک application pool با چند پروسه کارگر web garden میگویند.
اصلا این WWW Service چه کاری انجام میدهد و به چه دردی میخورد؟
- HTTP administration and configuration
- Performance monitoring
- Process management
HTTP Administration and Configuration
در نسخههای جدیدتر IIS چکاری بر عهده WWW Service است؟
WAS در قسمت سوم این مقاله توضیح داده خواهد شد.
کار کردن با مسیریابی برای یک پروژه ساده ، نیاز به طراحی پیچیده ندارد. مسیریابی پیش فرض موجود در فایل RoutConfig.cs برای کارهای ابتدایی کافیست. اما اگر کمی کار پیچیده شود و صفحات مختلفی با منطقهای متفاوتی ایجاد کنیم، ممکن است با مشکل روبرو شویم. در MVC5 به کمک دخالت ویژگیها در مسیریابی، کار ساده شده است اما در MVC4 و قبل از آن چه باید کرد؟ پیش از بسط مساله، ابتدا این سوال را پاسخ میدهیم که چگونه صفحهی start پروژه انتخاب میشود؟
مسیریابی پیش فرض یک پروژه MVC به شکل زیر است :
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } );
وقتی یک پروژه MVC را بررسی کنید، مشاهده میکنید که در شاخهی اصلی آن، فایل index یا default وجود ندارد و اصولا منطق کار با اکشنها و صدا زدن آنها و دیدن پاسخها توسط View، این اتفاق را توجیه میکند. پس وقتی درخواستی به سمت شاخهی اصلی ما فرستاده میشود، مسیریابی وارد عمل میشود. وقتی پروژه را اجرا میکنید، متد RegisterRoutes از شیءایی که از کلاس RoutingConfig.cs ساخته شده (به فایل global.asax نگاه کنید) فراخوانی میشود و شیء Routes از " قالب آدرس " هایی که ما تعیین کرده ایم پُر میشود. بخشی از فایل RoutConfig.cs را در تصویر زیر میبینیم.
به Url کد فوق نگاه کنید که در حقیقت آدرسی نسبی است. آدرس ما به طور کامل به شکل زیر قابل تعریف است:
http://mydomain.com/{controller}/{action}/{id}
واضح است که درخواست اولیه به سایت ما دارای بخشهای controller و action و بخش اختیاری id نیست. پس به شکل پیشفرض بر اساس آنچه در جلوی خصوصیت defaults نوشته شده است، فراخوانی خواهد شد. یعنی اکشن Index از داخل کنترلر Home صدا زده میشود و چون id نداریم، هیچ نوع id به متد (اکشن) index ارسال نخواهد شد.
یک روتینگ دیگر به شکل زیر در بالای کد فوق اضافه کنید :
routes.MapRoute( name: "Default1", url: "{controller}/{action}/{id}", defaults: new { controller = "News", action = "Index", id = UrlParameter.Optional } );
حتما متوجه شدید که اینبار با اجرای پروژه، متد(اکشن) Index از کنترلر News فراخوانی خواهد شد. در حقیقت اولین روتینگ ساخته شده همان چیزیست که Asp.NET MVC پس از دریافت ریکوئست به آن رجوع میکند. دقت کنید که مقدار name در دو MapRouting فوق متفاوت است.
اما این اسمها(name ها) به چه دردی میخورند؟
قبل از توضیح در این مورد، default1 را از پروژه حذف میکنیم و با همان MapRouting پیش فرض به کار خود ادامه میدهیم. ابتدا مروری بر عملکرد MapRouting انجام میدهیم.
همانطور که میدانید برای ایجاد یک لینک از طریق ویوی Razor از چیزی شبیه دستور زیر میتوانیم استفاده کنیم:
<a href="@Url.Action("post", "News", new { id = @item.PostName }, null)"> @item.PostTitle </a>
فرض کنید یک سایت خبری داریم و گروههای مختلف آن شامل مطالب متفاوتی هستند. کنترلری به نام News ایجاد کردهایم، که دارای یک اکشن به نام post است که نام مطلب را دریافت کرده و یک View به ما برمیگرداند. بخش هایی از صفحه اول را در تصاویر زیر میبینید. تصویر آخر، بخش پایین صفحه اخبار است، که بوسیله لینک (لینک های) موجود صفحات جابجا میشوند.
وقتی کد فوق در یک صفحه فراخوانی میشود، کالکشن Routes بررسی میشود و لینک مورد نظر، با توجه به ورودیهای Url.Action ساخته میشود. ما یک MapRouting به نام Default در کالکشن Routes داریم، پس Url.Action لینک زیر را میسازد:
http://mydomain.com/news/post/نام-مقاله
استفاده از MapRouting مثل یک جعبه است که ورودی آن مقادیر موجود در Url.Action میباشد و آنچه به ما بر میگرداند یک آدرس برای فراخوانی اکشن مورد نیاز است. (آخرین پارامتر، null قرار داده شده که برای تعیین نوع پروتکل استفاده میشود که http یا https میتواند باشد.)
برای فراخوانی صفحهی اخبار (لیست اخبار) میتوانید دستور زیر را بنویسید :
@Html.ActionLink("اخبار", "Index", "News", null, new { @class="btn btn-success"})
این دستور تگ anchor را نیز میسازد و به کمک MapRouting آدرسی شبیه آدرس زیر را به ما بر میگرداند. (به تفاوت دو دستور دقت کنید):
<a href="/News">اخبار</a>
واضح است که چون MapRouting یک حالت پیش فرض درونی دارد که دارای اکشن دیفالت index است، پس ActionLink اگر ببیند لینکش در صفحه قرار است به شکل /news/index تعریف شود خود بخود بخش index را حذف میکند.
فرض کنید بعد از کلیک روی لینک فوق، اکشن index ، یک آبجکتِ لیستی با 10 خبر آخر، ایجاد میکند. پس ما میخواهیم قابلیت صفحه بندی را برای لیست اخبار فعال کنیم و در هر صفحه 10 مطلب را نمایش دهیم. مشکل از همینجا آغاز میشود. MapRouting فعلی جوابگوی ما نخواهد بود و آدرس را به شکل زیر نمایش میدهد.
<a href="/News/index?pid=2">صفحه بعد</a>
و آنچه ما در View نوشتهایم چیزی شبیه کد زیر است :
@Html.ActionLink("صفحه بعد", "index", "News", new { pid = …. }, null)
مشکلی در ارجاع به صفحات وجود ندارد و با کلیک روی لینک "صفحه بعد" مقدار عدد 2 به اکشن index ارسال میشود و اگر کد نویسی را برای take و skip کردن لیست، درست انجام شده باشد، نتیجه مورد نظر نمایش داده خواهد شد. اما آدرس فوق آدرس زیبایی نیست. اولین فکری که به ذهن برنامه نویس میرسد، ایجاد یک مسیریابی دیگر است. فکر درستیست؛ اما اگر چند بار دیگر این اتفاق بیفتد و در بخش هایی از برنامه نیاز به روتینگ پیدا کنید و روتینگهای جدید ایجاد کنید متوجه خواهید شد که مدیریت این MapRouting ها کار خسته کننده و طاقت فرسایی خواهد شد، مخصوصا اگر بدانید که فقط مجاز به استفاده از یک پارامتر optional در هر MapRouting هستید! دست شما کاملا بسته است. لینکهای بالای سایت را اصلاح میکنید ولی لینکهای پایین سایت خراب میشوند و بالعکس.
به هر حال MapRouting زیر را به RoutConfig.cs اضافه میکنیم :
اینکار بستگی به پروژهی شما دارد، مپ روتینگ فوق این مزیت را دارد که با مپ روتینگهای دیگر به سختی قاطی میشود! یعنی حتا اگر قبل از مپ روتینگ دیفالت نوشته شود برنامه با مشکل مواجه نخواهد شد، چون اصلا شکل درخواست اولیه به سایت، چیزی شبیه این آدرس نیست. اما خاص بودن آن و همچنین نوع بهره گیری از آن با کمک Action یا ActioLink شاید شما را سردرگم خواهد کند.- اسم این MapRouting ، دیگر Default نیست.routes.MapRoute("PostPaging", "{controller}/{action}/{id}/{pid}", defaults: new { controller = "News", action = "Index", id = "all", pid = UrlParameter.Optional } );
- یک پارامتر pid اضافهتر از MapRouting اولی دارد.
- pid به عنوان یک پارامتر اختیاری تعریف شده است، پس "قالب آدرس" بسیار شبیه مپ روتینگ قبلی است.
- مقدار id اختیاری نیست، چون قرار است در آینده بتوانیم گروههای مختلف موجود در بخش اخبار را صفحه بندی کنیم و قرار نیست پشت سر هم MapRouting ایجاد کنیم و کافیست به جای id اسم گروه را بنویسیم. در حالتیکه اسمی از گروه درلینکهایمان نبرده باشیم به شکل پیشفرض all قرار داده میشود که یعنی کل اخبار مد نظر است. (در اکشن مربوطه باید این تصمیمات را لحاظ کنیم)
- حتما این MapRouting را بعد از MapRouting اولیه بنویسید، کمی پیشتر، علت این امر توضیح داده شد و گفته شد اولین چیزی که MVC پس از درخواست ما میبیند به عنوان Routing بررسی میکند (درخواست اولیه) و چون ساختار MapRouting فوق تا اندازه ای شبیه ساختار Default MapRouting است ممکن است با فراخوانی سایت مشکل ایجاد شود.
- میتوانید MapRouting را کمی خاصتر هم بنویسیم :
routes.MapRoute("NewsPaging", "News/index/{id}/{pid}", defaults: new { controller = "News", action = "Index", id = "all", pid = UrlParameter.Optional } );
اما مشکل این MapRouting ها چیست؟
درخواست به سایت آمده و قرار است سایت بارگذاری شود؛ ترتیب زیر در شیء routes ثبت شده است :
در صفحه اول ما لینکی به شکل زیر گذاشته ایم :
@Html.ActionLink("اخبار", "Index", "News", null, new { @class="btn btn-success"})
در صفحه اخبار، لینک "صفحه بعد" وجود دارد ، آیا این لینک به شکل صحیح نمایش داده میشود؟ خیر! نتیجه کار را ببینید :
<a href="/News/index?pid=2">صفحه بعد</a>
به نظر میرسد یک طرح دقیق برای آدرس دهی شاید این مسائل را حل کند ولی نه وقت داریم و نه اعصاب.
1. ساخته شدن لینکها توسط هلپرها (که از مپ روتینگهای ثبت شده ما پیروی میکند)
2. واکنش پروژه به درخواستهای دریافتی و هدایت آن به اکشنهای مورد نظر که کاملا به ترتیب ثبت مپ روتینگها در کالکشن Routes بستگی دارد .
@Html.RouteLink("صفحه بعد", "PostPaging", new { action = "cat", controller = "News", id =. .., pid = ...})
نتیجه برای لینک موجود در صفحه اخبار چیزی شبیه شکل زیر خواهد شد :
<a href="/News/cat/all/2">صفحه بعد</a>
@Html.RouteLink("اخبار", "Default", new { controller = "news" }) @Html.RouteLink("درباره ما", "pages", new RouteValueDictionary(new { controller="Page", action="Index", pagename="درباره-ما"}), new Dictionary<string, Object> { { "data-toggle", "popover" }, { "data-placement", "top" } })
همانطور که میبینید در RoutLink
اولی، اخبار را به کمک MapRouting
با نام default
بازنویسی میکنیم و نتیجه چیزی شبیه کد زیر خواهد شد :
<a href="/News">اخبار</a>
در RoutLink دومی اولا از یک RoutValueDictionary به جای یک آبجکت ساده استفاده کرده ایم و مقادیر را به شکل فوق به کنترلر و اکشن و ...نسبت داده ایم ثانیا برای بخش HTML نیز پراپرتیها را به کمک یک دیکشنری ارسال میکنیم، به خاطر وجود "-" در یکی از خواص، راه دیگری غیر از اینکار نداریم.
اما دقت کنید که از یک MapRouting جدید استفاده کردیم که نامش pages است،
این MapRoutnig را قبل از دیگر Routing ها مینویسیم؟ وسط دو MapRouting قبلی مینویسیم؟ آخر MapRouting ها مینویسیم؟ آیا فرقی میکند؟ اگر سریع بگوییم خــیر! اشتباه کرده ایم. واقعا فرق میکند.
دقت کنید موضوع MapRouting فقط ایجاد یک لینکتر و تمیز نیست؛ RoutLink یک لینک تمیز بر اساس مپ روتینگی که نامش برده شده ایجاد میکند
اما تضمین نمیکند که با کلیک بر روی لینک به هدف برسیم و به خطای 404 برخورد نکنیم! اگر روی لینک کلیک کنید آدرس شروع به تفسیر شدن
میکند و این تفسیر اصلا ربطی به نامی که به RoutLink داده ایم ندارد و ترتیب موجود در کالکشن ایجاد شده در RoutConfig تعیین کننده است.(آبجکت Routes ) اگر MapRouting فوق را در انتهای بقیه بگذاریم صفحه اول لود میشود ولی با کلیک
روی "درباره ما" صفحه پیغام خطا خواهد داد.
باید به یاد
داشته باشیم برای اجرای درخواست (کلیک روی لینک)، آنچه برای ASP.NET MVC اهمیت دارد، ترتیب قرار گیری MapRouting ها در RouteRegister است و
ما به کمک RoutLink تنها
مشکل ساخت لینکها بر اساس قالب MapRouting مورد
نظرمان را حل کردیم و این به ما تضمینی برای هدایت آن لینک به مکان درست را نخواهد
داد.
اگر ترتیب به شکل زیر باشد :
1باشد. درخواست اولیه برای بالا آمدن سایت به مشکل برخورد نمیکند چون همان مپ روتینگ 1 اجرا میشود. اما مشکل فوق به وجود خواهد آمد و خطای 404 با کلیک بر روی "درباره ما" نمایش داده خواهد شد چون با کلیک روی "درباره ما" مپ روتینگ شماره 1 وارد عمل میشود.
2
3
اگر ترتیب به شکل زیر باشد :
3آیا اصلا صفحه اول سالم لود خواهد شد؟ خیر! درخواست نسبی " / " (یا به طور کامل http://mydomain.com ) شماره 3 را به خیر پشت سر میگذارد، چون اصلا چیزی به نام page در آدرس وجود ندارد که از این MapRouting بخواهد پیروی کند. اما در شماره 2 گیر میافتد چون این فرمت را حفظ کرده است :
2
1
"{controller}/{action}/{id}/{pid}"
اگر ترتیب به شکل زیر باشد :
3این همان چیزیست که مد نظر ماست. اولا 1 قبل از 2 است و صفحه اول برای لود شدن به مشکل برخورد نمیکند.
1
2
ثانیا وقتی روی "درباره ما" کلیک میکنیم همان شماره 3 فراخوانی میشود و بقیه مپ روتینگها اعمال نمیشوند.
انتقال خودکار Data Annotations از مدلها به ViewModelهای ASP.NET MVC به کمک AutoMapper
public static class ModelValidatorProviders { private static readonly ModelValidatorProviderCollection _providers = new ModelValidatorProviderCollection() { new DataAnnotationsModelValidatorProvider(), new DataErrorInfoModelValidatorProvider(), new ClientDataTypeModelValidatorProvider() }; public static ModelValidatorProviderCollection Providers { get { return _providers; } } }
در این قسمت با تب Console آشنا خواهیم شد .
در قسمت قبل در مورد این تب گفتیم :
در این تب دو بخش وجود دارد :
در بخش Log هشدارها ، پیغامها ، درخواستهای XHR و ... نمایش داده میشوند .
بخش دیگر هم که در سمت راست قرار دارد ، مخصوص اجرای کدهای جاوا اسکریپت میباشد .
پس یک قسمت داریم برای نوشتن کدهای جاوا اسکریپت و یک قسمت هم برای مشاهدهی رویدادها .
اکنون 2 سوال مطرح میشود :
- برای اجرای یک کد در حالت معمول چگونه عمل میکنیم ؟
پاسخ : کدهای مورد نظر را بین تگ باز و بستهی script قرار میدهیم و صفحه مورد نظر را در مرورگر باز میکنیم و مرورگر کد را اجرا میکند . - در صورتی که کدهای ما خطا داشته باشند ، چگونه خطایابی میکنیم ؟
پاسخ : در بین کدهای نوشته شده چند alert قرار میدهیم و سعی میکنیم مشکل را پیدا کنیم .
همین 2 سوال اهمیت و قدرت فایرباگ و قسمت Console آن را برای ما آشکار میکند ، زیرا ما میتوانیم بدون Reload کردن صفحه ، کدهایمان را اجرا و نتیجه را مشاهده کنیم یا بوسیله توابع موجود خیلی ساده کدهایمان را خطایابی کنیم .
چگونه کدهای نوشته شده را سریع اجرا کنم ؟ کلید میانبر ( HotKey ) اجرای کدها چیست ؟
پاسخ : فشردن کلید CTRL + Enter
امتحان کنید :
کد زیر را در بخش کدنویسی تایپ کنید و بوسیله کلید میانبر گفته شده ، آن را اجرا کنید .
document.getElementsByTagName("div");
تصویر ذیل نتیجه اجرای کدی هست که اجرا کردیم .
چند نکته :
- قسمت هایی که با رنگ قرمز و یک دات ( . ) بعد از نام تگ قرار گرفته اند ، کلاسهای CSS ای هستند که المنت دارد .
- قسمت هایی که با رنگ آبی تیره و یک شارپ ( # ) بعد از نام تگ قرار گرفته اند ، ID تگها هستند .
- قسمت هایی که کمرنگ هستند ، المنت هایی هستند که در صفحه قابل نمایش نیستند .
- اگر یک تگ چند کلاس داشته باشد ، فقط اولین کلاس نمایش داده میشود .
دکمهها و حالتهای نمایش بخش کد نویسی
4 دکمه در قسمت کدنویسی وجود دارد :
- Run : اجرای کد
- Clear : خالی کردن بخش کد نویسی
- Copy : کپی کردن کد موجود در بخش کد نویسی در حافظه
- History : کدهای نوشته شده در نشست ( Session ) فعلی مرورگر
بخش کد نویسی میتواند به 2 شکل نمایش داده شود :
- بصورت جعبه چند خطی ( Command Editor )
- بصورت تک خطی ( Command Line )
9 دکمه هم در بالای بخش log وجود دارد ( به ترتیب از چپ به راست ) :
- دکمه ای با عنوان “Break On All Errors” . زمانی فعال شود ، در اولین اجرای یک کد از داخل صفحه ، به تب Script منتقل میشود و در خطی که کد در حال اجرا است توقف میکند .
- Clear : بخش log را خالی میکند .
- Persist : فعال بودن این دکمه باعث میشود که محتویات بخش Console در بارگزاری مجدد صفحه حفظ شود .
- Profile : بوسیله این گزینه میتوانید کدهای اجرایی خود در مدت زمان فعال بودن این دکمه ، تحت نظر بگیرید .
به این صورت که پس از غیر فعال کردن این دکمه ( کلیک مجدد بروی آن ) میتوانید تابعهای اجرا شده ، تعداد فراخوانی آنها ، مدت زمان اجرای هر یک ، میانگین زمان اجرای هر بار یک تابع و ... را مشاهده کنید . - 5 دکمهی بعدی هم برای فیلتر کردن Log هستند .
نمایش تعداد خطاهای اتفاق افتاده ، در نوار وضعیت ( Status Bar ) :
اگر در زمان فعال بودن فایرباگ ، در صفحه خطایی رخ دهد ، در نوار وضعیت عدد 1 را نمایش میدهد و به ازای هر خطای جدید ، یکی به تعداد خطاها اضافه میکند .
البته اگر از ورژنهای جدید فایرفاکس استفاده میکنید ، نوار وضعیت را نخواهید داشت و تعداد خطاها را در کنار آیکون فایرباگ خواهید داشت .
در کنار همه این امکانات ، فایرباگ یک مجموعه کامل از توابع کاربردی برای توسعه جاوا اسکریپت و خطایابی جاوا اسکریپت در اختیار ما میگذارد .
چند متد کمکی برای نوشتن Logهای مختلف در Console :
console.debug('This is a Debug message'); console.info('This is an Information'); console.warn('This is a Warning message'); console.error('This is an Error message');
در قسمت بعدی توابع مربوط به توسعه جاوا اسکریپت ( Command Line API & Console API ) در فایرباگ را بررسی خواهیم کرد .
تشخیص قسمتهایی که قابلیت استخراج از کامپوننت لاگین را دارند
قصد داریم قسمتهایی از کامپوننت لاگین فعلی را استخراج کرده و آنها را درون یک کامپوننت با قابلیت استفادهی مجدد قرار دهیم:
- خاصیت state: میخواهیم تمام فرمهایی را که تعریف میکنیم، دارای خاصیت errors باشند. بنابراین این خاصیت قابلیت استفادهی مجدد را دارد.
- خاصیت schema: قابلیت استفادهی مجدد را ندارد و مختص فرم لاگین تعریف شدهاست. این منطق از هر فرمی با فرم دیگر، متفاوت است.
- متد validate: در این متد، هیچ نوع وابستگی از آن به مفهوم لاگین وجود ندارد و کاملا قابلیت استفادهی مجدد را دارد. تنها this.state.account آن وابستهی به کامپوننت لاگین است و بدیهی است شیء account را در سایر فرمها نخواهیم داشت و ممکن است نام آن movie یا customer باشد. بنابراین قاعدهای را در اینجا تعریف میکنیم، بر این مبنا که از این پس، تمام فرمهای ما دارای خاصیتی به نام data خواهند بود که بیانگر اطلاعات آن فرم میباشد. با این تغییر، برای مثال در فرم لاگین، data به شیء account تنظیم میشود و در فرمی دیگر به شیء customer.
- متد validateProperty: همانند متد validate است و کاملا قابلیت استفادهی مجدد را دارد.
- متد handleSubmit: قسمت ابتدایی این متد که شامل غیرفعال کردن post back به سرور و اعتبارسنجی فرم است، قابلیت استفادهی مجدد را دارد. اما قسمت دوم آن مانند ارسال فرم به سرور و یا هر عملیات دیگری، از یک فرم به فرم دیگر میتواند متفاوت باشد.
- متد handleChange: این متد نیز قابلیت استفادهی مجدد را دارد؛ چون میخواهیم در تمام فرمها در حین تایپ اطلاعات، کار اعتبارسنجی ورودیها صورت گیرد. این متد نیز به this.state.account وابستهاست که قاعدهی تعریف خاصیت data در state، میتواند این مشکل را حل کند.
- متد رندر: طراحی آن کاملا وابستهاست به نوع فرمی که مدنظر میباشد؛ اما دکمهی submit آن خیر. بجز برچسب دکمهی submit، مابقی قسمتهای آن مانند کلاسهای CSS و منطق فعالسازی و غیرفعالسازی آن، قابلیت استفادهی مجدد را دارند.
بنابراین در ادامه کار، refactoring کامپوننت فرم لاگین را برای استخراج قسمتهای با قابلیت استفادهی مجدد آن، انجام خواهیم داد.
تبدیل قسمتهای با قابلیت استفادهی مجدد کامپوننت لاگین، به یک کامپوننت عمومی
ابتدا کامپوننت عمومی Form را که قابلیت استفادهی مجدد دارد، در فایل جدید src\components\common\form.jsx تعریف کرده و سپس کامپوننت فرم لاگین را طوری تغییر میدهیم که از آن، بجای کلاس پیشفرض Component، ارث بری کند. به این ترتیب تمام متدهای تعریف شدهی در این کامپوننت با قابلیت استفادهی مجدد، در کامپوننتهای مشتق شدهی از آن، در دسترس خواهند بود.
1- در ادامه همانطور که عنوان شد، خاصیت state فرمها باید دارای شیء data و شیء errors باشند تا توسط آنها بتوان اطلاعات کل فرم و اطلاعات خطاهای اعتبارسنجی را ذخیره کرد:
import React, { Component } from "react"; class Form extends Component { state = { data:{}, errors:{} }
2- در ادامه، کاری با خاصیت schema تعریف شدهی در کامپوننت لاگین نداریم؛ چون کاملا مختص به آن است. اما متدهای validate و validateProperty آنرا طور کامل cut کرده و به کامپوننت Form، منتقل میکنیم. با این انتقال، چون این متدها از کتابخانهی Joi استفاده میکنند، باید import آنرا نیز به ابتدای ماژول جدید فرم، اضافه کرد:
import Joi from "@hapi/joi";
3- سپس متد رندر کامپوننت Form را کاملا حذف میکنیم؛ چون این کامپوننت قرار نیست چیزی را رندر کند.
4- در قسمت دوم متد handleSubmit، برای مثال قرار است ارسال دادهها به سرور صورت گیرد. به همین جهت آنرا تبدیل به متدی مانند doSubmit کرده و سپس کل متد handleSubmit را نیز به کامپوننت Form منتقل میکنیم.
doSubmit = () => { // call the server console.log("Submitted!"); };
5- متد handleChange را نیز از کامپوننت فرم لاگین cut کرده و به کامپوننت Form منتقل میکنیم.
6- پس از این نقل و انتقالات، کار ارث بری از کامپوننت فرم را در کامپوننت فرم لاگین انجام میدهیم:
import Form from "./common/form"; // ... class LoginForm extends Form {
اکنون اگر برنامه را ذخیره کرده و اجرا کنیم، همانند قبل و آنچیزی که در انتهای قسمت قبلی به آن رسیدیم، بدون مشکل کار میکند؛ اما کدهای کامپوننت فرم لاگین به شدت کاهش یافته و ساده شدهاست. همچنین اگر دفعهی بعد، نیاز به ایجاد فرمی وجود داشت، دیگر نیازی به تکرار این حجم از کد نیست. تنها نیاز خواهیم داشت تا state را تعریف کرده و schema را اضافه کنیم و همچنین نیاز است متد doSumbit را پیاده سازی کنیم تا مشخص شود پس از تکمیل فرم و اعتبارسنجی آن، قرار است چه رخدادی واقع شود.
کدهای کامل کامپوننت فرم را از پیوست انتهای بحث میتوانید دریافت کنید؛ البته تمام متدهای آنرا در قسمت قبل تکمیل کرده بودیم و در اینجا صرفا یکسری cut/paste صورت گرفتند.
ساده کردن و بهبود پیاده سازی متد رندر
1- در متد رندر فعلی کامپوننت فرم لاگین، اگر به دکمهی submit آن دقت کنیم، بجز برچسب آن، مابقی قسمتهای آن در تمام فرمهای دیگری که تعریف خواهیم کرد، یکسان خواهند بود. به همین جهت این قسمت را میتوان تبدیل به یک متد کمکی در کلاس Form کرد:
renderButton(label) { return ( <button disabled={this.validate()} className="btn btn-primary"> {label} </button> ); }
{this.renderButton("Login")}
2- در قسمتهای قبل، برچسب، فیلدهای ورودی و تگها و کلاسهای بوت استرپی را به کامپوننت Input منتقل کردیم، تا به یک فرم سادهتر و با قابلیت نگهداری بالاتری برسیم. هرچند این هدف حاصل شده، اما باز هم تعاریف المانهای Input قرارگرفتهی در متد رندر کامپوننت لاگین، دارای الگوی تکراری ذکر یک خاصیت مشخص، تعریف رویدادگردانهای مشخص و اطلاعات اعتبارسنجی کاملا مشخصی هستند. به همین جهت تعریف المان Input را هم مانند متد renderButton فوق میتوان به کلاس پایه Form انتقال داد:
import Input from "./input"; //... renderInput(name, label) { const { data, errors } = this.state; return ( <Input name={name} label={label} value={data[name]} onChange={this.handleChange} error={errors[name]} /> );
render() { return ( <form onSubmit={this.handleSubmit}> {this.renderInput("username", "Username")} {this.renderInput("password", "Password")} {this.renderButton("Login")} </form> ); }
3- تا اینجا فرم لاگین تعریف شده، یک مشکل کوچک را دارد: فیلد پسورد آن، از نوع text تعریف شده و اطلاعات وارد شده را همانند یک textbox معمولی نمایش میدهد. برای رفع این مشکل، پارامتر type را با یک مقدار پیشفرض پر استفاده، تعریف کرده و به المان Input اعمال میکنیم:
renderInput(name, label, type = "text") { const { data, errors } = this.state; return ( <Input name={name} type={type} label={label} value={data[name]} onChange={this.handleChange} error={errors[name]} /> ); }
سپس این type را در قسمتی که المان مرتبط را رندر میکنیم، با password مقدار دهی خواهیم کرد:
render() { return ( <form onSubmit={this.handleSubmit}> {this.renderInput("username", "Username")} {this.renderInput("password", "Password", "password")} {this.renderButton("Login")} </form> ); }
البته این تغییرات تا به اینجا کار نخواهند کرد؛ چون هنوز کلاس المان Input را جهت پذیرش ویژگی جدید type، ویرایش نکردهایم. بنابراین به فایل src\components\common\input.jsx مراجعه کرده و type را به آن اعمال میکنیم:
import React from "react"; const Input = ({ name, type, label, value, error, onChange }) => { return ( <div className="form-group"> <label htmlFor={name}>{label}</label> <input value={value} onChange={onChange} id={name} name={name} type={type} className="form-control" /> {error && <div className="alert alert-danger">{error}</div>} </div> ); }; export default Input;
4- مشکل! آیا باید به ازای هر ویژگی جدیدی که قرار است به این input اعمال کنیم، مانند type در اینجا، نیاز است یک پارامتر جدید را تعریف و سپس از آن استفاده کرد؟ در این حالت اینترفیس این کامپوننت از کنترل خارج میشود و همچنین هربار باید آنرا ویرایش کرد و تغییر داد. به علاوه اگر به تعریف این input دقت کنیم، نام 4 ویژگی آن، با مقادیری که دریافت میکنند، هم نام هستند (ویژگی value با مقدار value و ...):
<input value={value} name={name} type={type} onChange={onChange} id={name} className="form-control" />
import React from "react"; const Input = ({ name, label, error, ...rest }) => { return ( <div className="form-group"> <label htmlFor={name}>{label}</label> <input {...rest} name={name} id={name} className="form-control" /> {error && <div className="alert alert-danger">{error}</div>} </div> ); }; export default Input;
با این تغییر در کامپوننت Input، سایر قسمتهای برنامه نیازی به تغییر ندارند. برای مثال در متد renderInput، سه ویژگی name، label و error تبدیل به سه پارامتر دریافتی از props میشوند (ترتیب ذکر آنها اهمیتی ندارد). مابقی ویژگیهای تعریف شدهی در آن، به صورت خودکار در قسمت input {...rest} درج خواهند شد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-20.zip
JQuery 1.3 ارائه شد
این Jquery انقدر جالبه که من که برنامه نویس نیستم، علاقه مند شدم... اولین بار هم از طریق وبلاگ شما آشنا شدم. خیلی دوست دارم که در موردش بیشتر بدونم... خیلی زیاد تو سایت jquery.com گشتم و چیز های جالبی هم پیدا کردم. اگر امکانش باشه، می خواستم خواهش کنم یه سری مطالب با تمرکز بیشتر در این مورد بنویسید. مثلا این توصیه هاست روی گوگل جالب بود. حالا نمی دونم اگر روی هاست داخل ایران باشه، آیا سرعت لودشدنش در ایران بیشتر میشه یا گوگل؟! فعلا دارم دنبال پلاگین های جالب Jquery می گردم... خیلی متشکرم از مطالب خیلی جالبتون.