چرا TypeScript؟
- TypeScript زبان توصیه شدهی توسعهی برنامههای AngularJS 2 است و همچنین با سایر کتابخانههای معروف جاوا اسکریپتی مانند ReactJS و jQuery نیز سازگاری دارد. بنابراین اگر قصد دارید به AngularJS 2 مهاجرت کنید، اکنون فرصت خوبی است تا زبان TypeScript را نیز بیاموزید. همچنین WinJS نیز با TypeScript نوشته شدهاست.
- superset زبان JavaScript بودن به این معنا است که تمام کدهای جاوا اسکریپتی موجود، به عنوان کد معتبر TypeScript نیز شناخته میشوند و همین مساله مهاجرت به آنرا سادهتر میکند. زبانهای دیگری مانند Dart و یا CoffeeScript ، نسبت به JavaScript بسیار متفاوت به نظر میرسند؛ اما Syntax زبان TypeScript شباهت بسیار زیادی به جاوا اسکریپت و خصوصا ES 6 دارد. در اینجا تنها کافی است پسوند فایلهای js را به ts تغییر دهید و از آنها به عنوان کدهای معتبر TypeScript استفاده کنید.
- strong typing و معرفی نوعها، کدهای نهایی نوشته شده را امنتر میکنند. به این ترتیب کامپایلر، پیش از اینکه کدهای شما در زمان اجرا به خطا بر بخورند، در زمان کامپایل، مشکلات موجود را گوشزد میکند. همچنین وجود نوعها، سرعت توسعه را با بهبود ابزارهای مرتبط با برنامه نویسی، افزایش میدهند؛ از این جهت که مفهوم مهمی مانند Intellisense، با وجود نوعها، پیشنهادهای بهتر و دقیقتری را ارائه میدهد. همچنین ابزارهای Refactoring نیز در صورت وجود نوعها بهتر و دقیقتر عمل میکنند. این موارد مهمترین دلایل طراحی TypeScript جهت توسعه و نگهداری برنامههای بزرگ نوشته شدهی با JavaScript هستند.
- Syntax زبان TypeScript به شدت الهام گرفته شده از زبان سیشارپ است. به همین جهت اگر با این زبان آشنایی دارید، درک مفاهیم TypeScript برایتان بسیار ساده خواهد بود.
- بهترین قسمت TypeScript، کامپایل شدن آن به ES 5 است (به این عملیات Transpile هم میگویند). در زبان TypeScript به تمام امکانات پیشرفتهی ES 6 مانند کلاسها و ماژولها دسترسی دارید، اما کد نهایی را که تولید میکند، میتواند ES 5 ایی باشد که هم اکنون تمام مرورگرهای عمده آنرا پشتیبانی میکنند. با تنظیمات کامپایلر TypeScript، امکان تولید کدهای ES 3 تا ES 5 و همچنین ES 6 نیز وجود دارد. نمونهی آنلاین این ترجمه را در TypeScript playground میتوانید مشاهده کنید.
- TypeScript چندسکویی است. امکانات و کامپایلر این زبان، برای ویندوز، مک و لینوکس طراحی شدهاند.
- TypeScript سورس باز است. طراحان اصلی آن، همان طراحان زبان سیشارپ در مایکروسافت هستند و هم اکنون این زبان به صورت سورس باز توسط این شرکت توسعه داده شده و در GitHub نگهداری میشود.
آماده سازی محیطهای کار با TypeScript
برای کار با TypeScript، یک ادیتور متنی ساده، به همراه کامپایلر آن کفایت میکند. اما همانطور که عنوان شد، یکی از مهمترین دلایل وجودی TypeScript، بهبود ابزارهای برنامه نویسی مرتبط با JavaScript است و اگر قرار باشد صرفا از یک ادیتور متنی ساده استفاده شود، فلسفهی وجودی آن زیر سؤال میرود.
نصب TypeScript در ویژوال استودیو
در نگارشهای جدید ویژوال استودیو، از VS 2013 Update 2 به بعد، قسمت ویژهی TypeScript نیز قابل مشاهدهاست. البته این قسمت با به روز رسانیهای TypeScript، نیاز به به روز رسانی دارد. به همین جهت به سایت رسمی آن مراجعه کرده و بستههای جدید مخصوص VS 2013 و یا 2015 آنرا دریافت و نصب کنید.
همچنین افزونهی Web Essentials نیز امکانات بیشتری را جهت کار با TypeScript به همراه دارد و امکان مشاهدهی خروجی جاوا اسکریپت تولیدی را در حین کار با فایل TypeScript فعلی میسر میکند. در سمت چپ صفحه TypeScript را خواهید نوشت و در سمت راست، خروجی JavaScript نهایی را بلافاصله مشاهده میکنید.
تصویر فوق مربوط به VS 2015 است. همچنین گزینهی افزودن یک فایل و آیتم جدید نیز امکان افزودن فایلهای TS را به همراه دارد.
نصب و تنظیم TypeScript در ویژوال استودیو کد
ویژوال استودیو کد، نگارش رایگان، سورس باز و چندسکویی ویژوال استودیو است که بر روی ویندوز، مک و لینوکس قابل اجرا است. ویژوال استودیو کد نیز به همراه پشتیبانی بسیار خوبی از TypeScript است، تا حدی که تمام ارائههای معرفی Anugular 2 توسط تیم مربوطهی آن از گوگل، توسط ویژوال استودیو کد و یکپارچگی آن با TypeScript انجام شدند.
ویژوال استودیو کد بر مبنای فولدرها کار میکند و با گشودن یک پوشه در آن (با کلیک بر روی دکمهی open folder آن)، امکان کار کردن با آن پوشه و فایلهای موجود در آن را خواهیم یافت.
نکتهی مهم اینجا است که پس از نصب VS Code، برای فایلهای با پسوند ts بلافاصله Intellisense مرتبط نیز مهیا است و نیاز به هیچگونه تنظیم اضافهتری ندارد. همچنین قابلیتهای type safety این زبان نیز در این ادیتور به نحو واضحی مشخص هستند:
در ادامه ابتدا یک پوشهی جدید خالی را ایجاد کنید و سپس این پوشه را در VS Code باز نمائید (از طریق منوی فایل، گزینهی گشودن پوشه). سپس ماوس را بر روی نام این پوشه حرکت دهید:
همانطور که مشاهده میکنید، دکمهی new file ظاهر میشود. در اینجا میتوانید فایل جدیدی را به نام test.ts اضافه کنید.
در ادامه با فشردن دکمههای ctrl+shift+p، امکان انتخاب یک task runner را جهت کامپایل فایلهای ts خواهیم داشت:
در اینجا ابتدا عبارت task< را وارد کنید و سپس از منوی باز شده، گزینهی rub build task را انتخاب کنید:
پس از آن، در بالای صفحه مشاهده خواهید کرد که عنوان شده: «هنوز هیچ task runner ایی برای اینکار تنظیم نشدهاست»
برای این منظور بر روی دکمهی configure task runner تصویر فوق که با رنگ آبی مشخص شدهاست، کلیک کنید. به این ترتیب یک فایل جدید به نام task.json ایجاد میشود که در پوشهای به نام vscode. در ریشهی پروژه (یا همان پوشهی جاری) قرار میگیرد:
فایل task.json دارای تعاریفی است که کامپایلر TypeScript یا همان tsc را فعال میکند:
{ "version": "0.1.0", // The command is tsc. Assumes that tsc has been installed using npm install -g typescript "command": "tsc", // The command is a shell script "isShellCommand": true, // Show the output window only if unrecognized errors occur. "showOutput": "silent", // args is the HelloWorld program to compile. "args": ["HelloWorld.ts"], // use the standard tsc problem matcher to find compile problems // in the output. "problemMatcher": "$tsc" }
در اینجا قسمتی که نیاز به تنظیم دارد، خاصیت args است. مقادیر آن، پارامترهایی هستند که به کامپایلر typescript ارسال میشوند. برای نمونه آنرا به صورت ذیل تغییر دهید:
"args": [ "--target", "ES5", "--outdir", "js", "--sourceMap", "--watch", "test.ts" ],
برای اجرای کامپایلر، ابتدا از منوی view گزینهی toggle output را انتخاب کنید تا بتوان خروجی نهایی کامپایلر را مشاهده کرد. سپس گزینهی view->command pallet و اجرا tasks< را انتخاب کنید. در ادامه همانند مرحلهی قبل، یعنی گزینهی run build task را اجرا کنید (که خلاصهی این عملیات ctrl+shift+B است).
به این ترتیب پوشهی js که در خاصیت args مشخص کردیم، تولید میشود:
البته این خطا هم در قسمت output نمایش داده میشود:
error TS5023: Unknown option 'watch' Use the '--help' flag to see options.
علت اینجا است که در تنظیمات فوق، خاصیت command به tsc تنظیم شدهاست و همانطور که در کامنت آن عنوان شدهاست، کامپایلر typescript را از طریق دستور npm install -g typescript دریافت میکند و نیازی به ذکر مسیر آن در اینجا نیست. بنابراین لازم است تا با npm و نصب typescript از طریق آن آشنا شد و به این ترتیب کامپایلر آنرا به روز کرد تا دستور watch را شناسایی کند.
نصب TypeScript از طریق npm
همانطور که عنوان شد، TypeScript چندسکویی است و این مورد را از طریق npm یا NodeJS package manager انجام میدهد. برای این منظور به آدرس https://nodejs.org/en مراجعه کرده و فایل نصاب آنرا مخصوص سیستم عامل خود دریافت و سپس نصب کنید. Node.js یک runtime سمت سرور اجرای برنامههای جاوا اسکریپتی است. از آنجائیکه TypeScript در نهایت به JavaScript تبدیل میشود، استفاده از node.js انتخاب مناسبی جهت اجرا و توزیع آن در تمام سیستم عاملها بودهاست.
پس از نصب node.js، از package manager آن که npm نام دارد، جهت نصب TypeScript استفاده میشود. چون node.js به Path و مسیرهای اصلی ویندوز اضافه میشود، تنها کافی است دستور npm install -g typescript را در خط فرمان صادر کنید. در اینجا سوئیچ g به معنای global و دسترسی عمومی است.
همانطور که در این تصویر مشخص است، پس از صدور دستور نصب TypeScript، نگارش 1.8.9 آن نصب شدهاست. اما زمانیکه کامپایلر tsc را با پارامتر version اجرا میکنیم، شماره نگارش قدیمی 1.0.3.0 را نمایش میدهد. برای رفع این مشکل به مسیر C:\Program Files (x86)\Microsoft SDKs\TypeScript مراجعه کرده و پوشهی 1.0 را به 1.0-old تغییر نام دهید.
اکنون اگر مجددا بررسی کنیم، نگارش صحیح قابل مشاهده است:
پس از این تغییرات اگر مجددا به VS Code باز گردیم و ctrl+shift+B را صادر کنیم (جهت اجرای مجدد task runner و اجرای tsc تنظیم شده) ، پیام ذیل مشاهده میشود:
15:33:52 - Compilation complete. Watching for file changes.
در اینجا چون پارامتر watch فعال شدهاست، هر تغییری که در فایل ts داده شود، بلافاصله کامپایل شده و در فایل js منعکس خواهد شد.
تنظیم VS Code جهت دیباگ کدهای TypeScript
در نوار ابزار کنار صفحهی VS Code، بر روی دکمهی دیباگ کلیک کنید:
سپس بر روی دکمهی چرخدندهی موجود که کار انجام تنظیمات را توسط آن میتوان ادامه داد، کلیک کنید. بلافاصله منویی ظاهر میشود که درخواست انتخاب محیط دیباگ را دارد:
در اینجا node.js را انتخاب کنید. با اینکار فایل جدیدی دیگری به نام launch.json به پوشهی vscode. اضافه میشود. اگر به این فایل دقت کنید دو خاصیت name به نامهای Launch و Attach در آن موجود هستند. این نامها در یک دراپ داون، در کنار دکمهی start دیباگ نیز ظاهر میشوند:
- در فایل launch.json، باید خاصیت "program": "${workspaceRoot}/app.js" را ویرایش کرد و app.js آنرا به test.ts مثال جاری تغییر داد.
- سپس خاصیت "sourceMaps" آن نیز باید تغییر کرده و جهت استفادهی از source mapهای تولیدی به true تنظیم شود.
- در آخر باید مسیر پوشهی خروجی js را نیز تنظیم کرد: "outDir": "${workspaceRoot}/js"
همچنین باید دقت داشت چون externalConsole به false تنظیم شدهاست، خروجی این کنسول به output ویژوال استودیوکد منتقل میشود.
اکنون اگر بر روی دکمهی سبز رنگ start کلیک کنید (دکمهی F5)، امکان دیباگ سطر به سطر کد TypeScript را خواهید یافت:
فایلهای نهایی json یاد شدهی در متن را از اینجا میتوانید دریافت کنید:
VSCodeTypeScript.zip
<script src="~/Scripts/jquery.validate.min.js" type="text/javascript"></script> <script src="~/Scripts/jquery.validate.unobtrusive.min.js" type="text/javascript"></script>
عدم سازگاری پیش فرض jquery.validate با بعضی از ویجتهای Kendo UI
در حالت استفاده از Kendo UI، این سیستم هنوز هم کار میکند؛ اما با یک مشکل. اگر برای مثال از kendoComboBox استفاده کنید، اعتبارسنجیهای تعریف شده در برنامه، توسط jquery.validate دیده نخواهند شد. برای مثال فرض کنید یک چنین مدلی در اختیار View برنامه قرار گرفته است:
public class OrderDetailViewModel { [StringLength(15)] [Required] public string Destination { get; set; } }
@model Mvc4TestViewModel.Models.OrderDetailViewModel @using (Ajax.BeginForm(actionName: "Index", controllerName: "Home", ajaxOptions:new AjaxOptions(), htmlAttributes: new { id = "Form1", name ="Form1" }, routeValues: new { } )) { @Html.AntiForgeryToken() @Html.ValidationSummary(true) <fieldset> <legend>OrderDetail</legend> <div class="editor-label"> @Html.LabelFor(model => model.Destination) </div> <div class="editor-field"> @Html.TextBoxFor(model => model.Destination, new { @class = "k-textbox" }) @Html.ValidationMessageFor(model => model.Destination) </div> <p> <button class="k-button" type="submit" title="Sumbit"> Sumbit </button> </p> </fieldset> } @section JavaScript { <script type="text/javascript"> $(function () { $("#Destination").kendoComboBox({ dataSource: [ "loc 1", "loc 2" ] }); </script> }
همانطور که در تصویر مشاهده میکنید، با اتصال kendoComboBox به یک فیلد، این فیلد در حالت مخفی قرار میگیرد و ویجت کندو یو آی بجای آن نمایش داده خواهد شد. در این حالت چون در فایل jquery.validate.js چنین تنظیمی وجود دارد:
$.extend( $.validator, { defaults: { //… ignore: ":hidden",
راه حل آن نیز سادهاست. تنها باید خاصیت ignore را بازنویسی کرد و تغییر داد:
<script type="text/javascript"> $(function () { var form = $('#Form1'); form.data('validator').settings.ignore = ''; // default is ":hidden". }); </script>
<script type="text/javascript"> $.validator.setDefaults({ ignore: "" }); </script>
یکپارچه کردن سیستم اعتبارسنجی Kendo UI با سیستم اعتبارسنجی ASP.NET MVC
در مطلب «اعتبار سنجی ورودیهای کاربر در Kendo UI» با زیرساخت اعتبارسنجی Kendo UI آشنا شدید. برای اینکه بتوان این سیستم را با ASP.NET MVC یکپارچه کرد، نیاز است دو کار صورت گیرد:
الف) تعریف فایل kendo.aspnetmvc.js به صفحه اضافه شود:
<script src="~/Scripts/kendo.aspnetmvc.js" type="text/javascript"></script>
<script type="text/javascript"> $(function () { $("form").kendoValidator(); }); </script>
فایل kendo.aspnetmvc.js که در بستهی مخصوص Kendo UI تهیه شده برای ASP.NET MVC موجود است (در پوشهی js آن)، عملکردی مشابه فایل jquery.validate.unobtrusive مایکروسافت دارد. کار آن وفق دادن و ترجمهی اعتبارسنجی unobtrusive به روش Kendo UI است.
این فایل را از اینجا میتوانید دریافت کنید:
kendo.mvc.zip
البته باید دقت داشت که در حال حاضر فقط ویژگیهای ذیل از ASP.NET MVC توسط kendo.aspnetmvc.js پشتیبانی میشوند:
Required StringLength Range RegularExpression
در این قسمت میتوانید هر کدام از موارد را که نیاز دارید، نصب کنید. هر کدام از عنوانها در آینده آموزش داده خواهند شد و پیشنهاد میشود آنها را نصب کنید؛ در غیر این صورت فقط گزینهی اول کافی میباشد.
در صورت کلیک بر روی Options، با صفحهی زیر روبرو میشوید که در آن با انتخاب گزینهی Administrative، میتوانید تنظیمات بیشتری را در زمان نصب، انجام دهید و همچنین با انتخاب All users ، نرم افزار برای تمام کاربران سیستم در دسترس خواهد بود.
بعد از اتمام نصب، فایلهای نرم افزار در آدرس زیر در دسترس میباشند:
%LOCALAPPDATA%\JetBrains\Installations
اکنون اگر Visual studio را باز کنید، Resharper به قسمت Extensionsها اضافه شدهاست که در ادامهی آموزش به بررسی آن میپردازیم.
زبانهای پشتیبانی شده
ریشارپر از زبان های C# , VB.NET , TypeScript , JavaScript , C++ , CSS پشتیبانی میکند.
افزایش سرعت Reshaper
یکی از مشکلاتی که بیشتر افراد بعد از نصب این افزونه دارند، مخصوصا اگر سیستم آنها خیلی قوی نباشد، کند شدن Visual Studio میباشد. برای سریعتر کردن این افزونه راه هایی موجود میباشد که به آنها میپردازیم.
- خود ریشارپر پیشنهادهایی را برای بهبود سرعت میکند که آنها در مسیر زیر در دسترس میباشند:
ReSharper | Options | Environment | Performance Guide
- یکی از امکانات ریشارپر، نشان دادن تمام خطاهای موجود در برنامه به صورت یک لیست میباشد که نام این ویژگی، solution-wide analysis است و البته این امکان باعث سنگینی زیاد Visual studio میشود. برای غیر فعال کردن آن میتوانید به مسیر زیر بروید:
ReSharper | Options | Code Inspection | Settings
- راه دیگر انجام اینکار از قسمت تنظیمات خود Visual studio است. برای این کار به مسیر زیر بروید و گزینههای گفته شده را غیر فعال کنید.
Environment | General
Automatically adjust visual experience based on client performance Enable rich client visual experience
- همچنین این گزینه را نیز فعال کنید تا جلوی لگ در UI گرفته شود.
Use hardware graphics acceleration if available
- اگر پروژهی بزرگی را دارید، میتوانید گزینهی زیر را نیز غیر فعال کنید. البته با غیر فعال کردن این گزینه در صورت کرش نرم افزار، کدهای ذخیره نشده ازدست میروند.
Environment | AutoRecover
Save AutoRecover information
- اگر با تعداد فایلهای زیادی کار میکنید، امکان Track changes باعث کندی برنامه میشود. برای غیر فعال کردن این گزینه، به مسیر زیر بروید.
Text Editor | General
Track changes
- خود Visual studio گزینههایی را مانند خطاها در Scroll Bar نشان میدهد که این امکانات در ریشارپر هم موجود میباشد. برای غیر فعال کردن این امکان در Visual Studio برای جلوگیری از دو بار نشان دادن اطلاعات، به مسیر زیر بروید و گزینهی گفته شده را غیر فعال کنید.
Text Editor | All Languages | Scroll Bars
Show annotations over vertical scroll bar
- یکی دیگر از امکانات Visual Studio گزینهای به اسم CodeLens می باشد که یکی از کارهای آن، نشان دادن تمام رفرنسهای توابع یک فایل، در بالای تابع میباشد. این امکان نیز باعث کندی بسیار زیاد برنامه میشود. برای غیر فعال کردن آن میتوانید به مسیر زیر بروید.
Text Editor | All Languages | CodeLens
- هم Visual Studio و هم Reshaper کدهای شما را Format میکنند. پس برای جلوگیری از دوبار انجام شدن این کار، به مسیر زیر بروید و گزینهی گفته شده را غیر فعال کنید.
Text Editor | [Language] | Formatting
auto-formatting
- اگر از تمام امکانات Reshaper نمیخواهید استفاده کنید، میتوانید آنها را از آدرس زیر غیر فعال کنید.
Environment | Products & Features
- اگر در زمان تایپ کردن، برنامه کند میباشد، میتوانید بعضی از امکانات Resharper را از آدرس زیر غیر فعال کنید.
Environment | IntelliSense
Completion Appearance ReSharper's IntelliSense for specific languages
روشهای زیادی برای مدیریت این مساله وجود دارند؛ مانند استفاده از ماژولهای URL Rewrite برای بازنویسی آدرسهای نهایی صفحهی در حال رندر و یا ... به روز رسانی مستقیم بانک اطلاعاتی، یافتن تمام فیلدهای رشتهای ممکن در تمام جداول موجود و سپس اعمال تغییرات.
یافتن لیست تمام جداول قابل مدیریت توسط Entity framework
در ابتدا میخواهیم لیست پویای تمام جداول مدیریت شدهی توسط EF را پیدا کنیم. از این جهت که نمیخواهیم به ازای هر کدام یک کوئری جداگانه بنویسیم.
using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using EFReplaceAll.Models; namespace EFReplaceAll.Config { public class DbSetInfo { public IQueryable<object> DbSet { set; get; } public Type DbSetType { set; get; } } public class MyContext : DbContext { public DbSet<Product> Products { set; get; } public DbSet<Category> Categories { set; get; } public DbSet<User> Users { set; get; } public MyContext() : base("Connection1") { this.Database.Log = sql => Console.Write(sql); } public IList<DbSetInfo> GetAllDbSets() { return this.GetType() .GetProperties() .Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)) .Select(p => new DbSetInfo { DbSet = (IQueryable<object>)p.GetValue(this, null), DbSetType = p.PropertyType.GetGenericArguments().First() }) .ToList(); } } }
یافتن فیلدهای رشتهای رکوردهای تمام جداول و سپس به روز رسانی آنها
میخواهیم متدی را طراحی کنیم که در آن لیستی از یافتنها و جایگزینیها قابل تعیین باشد. به همین جهت مدل زیر را تعریف میکنیم:
using System; namespace EFReplaceAll.Utils { public class ReplaceOp { public string ToFind { set; get; } public string ToReplace { set; get; } public StringComparison Comparison { set; get; } } }
سپس متدی که کار یافتن تمام فیلدهای رشتهای و سپس جایگزین کردن آنها را انجام میدهد به صورت زیر خواهد بود:
using System.Collections.Generic; using System.Linq; using EFReplaceAll.Config; namespace EFReplaceAll.Utils { public static class UpdateDbContextContents { public static void ReplaceAllStringsAcrossTables(IList<ReplaceOp> replaceOps) { int dbSetsCount; using (var uow = new MyContext()) { dbSetsCount = uow.GetAllDbSets().Count; } for (var i = 0; i < dbSetsCount; i++) { using (var uow = new MyContext()) // using a new context each time to free resources quickly. { var dbSetResult = uow.GetAllDbSets()[i]; var stringProperties = dbSetResult.DbSetType.GetProperties() .Where(p => p.PropertyType == typeof(string)) .ToList(); var dbSetEntities = dbSetResult.DbSet; var haveChanges = false; foreach (var entity in dbSetEntities) { foreach (var stringProperty in stringProperties) { var oldPropertyValue = stringProperty.GetValue(entity, null) as string; if (string.IsNullOrWhiteSpace(oldPropertyValue)) { continue; } var newPropertyValue = oldPropertyValue; foreach (var replaceOp in replaceOps) { newPropertyValue = newPropertyValue.ReplaceString(replaceOp.ToFind, replaceOp.ToReplace, replaceOp.Comparison); } if (oldPropertyValue != newPropertyValue) { stringProperty.SetValue(entity, newPropertyValue, null); haveChanges = true; } } } if (haveChanges) { uow.SaveChanges(); } } } } } }
- در اینجا using (var uow = new MyContext()) را زیاد مشاهده میکنید. علت اینجا است که اگر تنها با یک Context کار کنیم، EF تمام تغییرات و تمام رکوردهای وارد شدهی به آنرا کش میکند و مصرف حافظهی برنامه با توجه به خواندن تمام رکوردهای بانک اطلاعاتی توسط آن، ممکن است به چند گیگابایت برسد. به همین جهت از Contextهایی با طول عمر کوتاه استفاده شدهاست تا میزان مصرف RAM این متد سبب کرش برنامه نشود.
- در ابتدای کار توسط متد GetAllDbSets که به Context اضافه کردیم، تعداد DbSetهای موجود را پیدا میکنیم تا بتوان بر روی آنها حلقهای را تشکیل داد و به ازای هر کدام یک (()using (var uow = new MyContext را تشکیل داد.
- سپس با استفاده از نوع DbSet که توسط خاصیت dbSetResult.DbSetType در دسترس است، خواص رشتهای ممکن این DbSet یافت میشوند.
- در ادامه dbSetResult.DbSet یک Data Reader را به صورت پویا بر روی DbSet جاری باز کرده و تمام رکوردهای این DbSet را یک به یک بازگشت میدهد.
- در اینجا با استفاده از Reflection، از رکورد جاری، مقادیر خواص رشتهای آن دریافت شده و سپس کار جستجو و جایگزینی انجام میشود.
- در آخر هم فراخوانی uow.SaveChanges کار ثبت تغییرات صورت گرفته را انجام میدهد.
متدی برای جایگزینی غیرحساس به حروف بزرگ و کوچک
متد استاندارد Replace رشتهها، حساس به حروف بزرگ و کوچک است. یک نمونهی عمومیتر را که در آن بتوان StringComparison.OrdinalIgnoreCase را تعیین کرد، در ذیل مشاهده میکنید که از آن در متد ReplaceAllStringsAcrossTables فوق استفاده شدهاست:
using System; using System.Text; namespace EFReplaceAll.Utils { public static class StringExtensions { public static string ReplaceString(this string src, string oldValue, string newValue, StringComparison comparison) { if (string.IsNullOrWhiteSpace(src)) { return src; } if (string.Compare(oldValue, newValue, comparison) == 0) { return src; } var sb = new StringBuilder(); var previousIndex = 0; var index = src.IndexOf(oldValue, comparison); while (index != -1) { sb.Append(src.Substring(previousIndex, index - previousIndex)); sb.Append(newValue); index += oldValue.Length; previousIndex = index; index = src.IndexOf(oldValue, index, comparison); } sb.Append(src.Substring(previousIndex)); return sb.ToString(); } } }
UpdateDbContextContents.ReplaceAllStringsAcrossTables( new[] { new ReplaceOp { ToFind = "https://www.dntips.ir", ToReplace = "https://www.dntips.ir", Comparison = StringComparison.OrdinalIgnoreCase }, new ReplaceOp { ToFind = "https://www.dntips.ir", ToReplace = "https://www.dntips.ir", Comparison = StringComparison.OrdinalIgnoreCase } });
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: EFReplaceAll.zip
استفاده از Tag Helpers ویژهی ASP.NET Core برای مدیریت محیطهای توسعه و تولید
فایلهای برنامهی تک صفحهای تولید شدهی توسط Angular CLI، در نهایت یک چنین شکلی را خواهند داشت:
این فایلها نیز در حالت توسعه تهیه شدهاند. در یک برنامهی واقعی، صفحهی سادهی index.html تولیدی آن، تنها میتواند یک قالب شروع به کار باشد و نه فایل نهایی که قرار است ارائه شود. نیاز است به این فایل تگهای بیشتری را اضافه کرد و سفارشی سازیهای خاصی را به آن اعمال نمود. در این حالت با توجه به بازنویسی و تولید مجدد این فایل در هر بار ساخت برنامه، میتوان از فایل Layout پروژهی ASP.NET Core جاری استفاده کرد. به این ترتیب از مزایای Razor و تمام زیرساختی که در اختیار داریم نیز محروم نخواهیم شد.
بنابراین تنها کاری را که باید انجام دهیم، کپی ساختار فایل index.html تولیدی به فایل Layout برنامه است.
مشکل! در حالت توسعه، نام فایلهای تولید شده به همین سادگی است که ملاحظه میکنید. اما در حالت ارائهی نهایی، این فایلها به همراه یک هش نیز تولید میشوند (پیاده سازی مفهوم cache busting و اجبار به بهروز رسانی کش مرورگر، باتوجه به تغییر آدرس فایلها)؛ مانند vendor.ea3f8329096dbf5632af.bundle.js
راه حل اول: تولید فایلهای نهایی بدون هش
ng build -prod --output-hashing=none
درکل بهتر است از این روش استفاده نشود، چون با وجود پروکسیهای کش کردن اطلاعات در بین راه، احتمال اینکه کاربران نگارشهای قدیمی برنامه را مشاهده کنند، بسیار زیاد است.
راه حل دوم: تگ Script در ASP.NET Core اجازهی ذکر تمام فایلهای اسکریپت یک پوشه را نیز میدهد
<script type="text/javascript" asp-src-include="*.js"></script>
راه حل واقعی
در اینجا کدهای کامل فایل Views\Shared\_Layout.cshtml را که میتواند جایگزین فایل index.html تولیدی توسط Angular CLI باشد، ملاحظه میکنید:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="icon" type="image/x-icon" href="favicon.ico"> <title>ng2-lab</title> <base href="/"> <environment names="Development"> </environment> <environment names="Staging,Production"> <link rel="stylesheet" asp-href-include="~/styles*.css" /> </environment> </head> <body> @RenderBody() <app-root></app-root> <environment names="Development"> <script type="text/javascript" src="/inline.bundle.js"></script> <script type="text/javascript" src="/polyfills.bundle.js"></script> <script type="text/javascript" src="/scripts.bundle.js"></script> <script type="text/javascript" src="/styles.bundle.js"></script> <script type="text/javascript" src="/vendor.bundle.js"></script> <script type="text/javascript" src="/main.bundle.js"></script> </environment> <environment names="Production,Staging"> <script type="text/javascript" asp-src-include="~/inline*.js"></script> <script type="text/javascript" asp-src-include="~/polyfills*.js"></script> <script type="text/javascript" asp-src-include="~/scripts*.js"></script> <script type="text/javascript" asp-src-include="~/vendor*.js"></script> <script type="text/javascript" asp-src-include="~/main*.js"></script> </environment> </body> </html>
[name].[hash].bundle.js
همچنین باید دقت داشت که در حالت توسعه، تمام شیوه نامههای برنامه در فایل styles.bundle.js قرار میگیرند. اما در حالت ارائهی نهایی، این فایل وجود نداشته و با نام کلی styles*.css تولید میشود که باید در head صفحه قرار گیرد (مانند تنظیمات حالت تولید در Layout فوق).
اصلاح قسمت URL Rewrite برنامه
در حالت کار با برنامههای تک صفحهای وب، در اولین درخواست رسیدهی به برنامه ممکن است آدرسی درخواست شود که معادل کنترلر و اکشن متدی را در برنامهی سمت سرور نداشته باشد. در این حالت کاربر را به همان صفحهی index.html هدایت میکنیم تا سیستم مسیریابی سمت کلاینت، کار نمایش آن صفحه را انجام دهد:
app.Use(async (context, next) => { await next(); var path = context.Request.Path.Value; if (path != null && context.Response.StatusCode == 404 && !Path.HasExtension(path) && !path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase)) { context.Request.Path = "/index.html"; await next(); } });
//context.Request.Path = "/index.html"; context.Request.Path = "/"; // since we are using views/shared/_layout.cshtml now.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
علت اینجا است که با فعال سازی wysiwyg-editorهای موجود، المان HTML پیش فرض مانند یک TextArea حالت مخفی پیدا میکند:
و اگر به سورس کد فایل jquery.validate.js مراجعه کنید، یک چنین تعریف پیش فرضی در آن موجود است:
$.extend( $.validator, { defaults: { ignore: ":hidden",
برای رفع این مشکل کافی است بنویسیم:
jQuery.validator.setDefaults({ ignore: ":hidden:not(textarea)" });
اگر میخواهید کلا از چیزی صرفنظر نشود، مقدار آنرا به "" تنظیم کنید.
مشکل دوم! متد required پیش فرض، فقط به رشتههای خالی یا نال واکنش نشان میدهد. اما اگر کاربری در اینجا <p><br/></p> را وارد کرد، چطور؟
در این حالت نیاز است متد rqeuired پیش فرض jQuery validator را به نحو ذیل بازنویسی کنیم:
<script type="text/javascript"> // حذف تمام تگهای یک قطعه متن function removeAllTagsAndTrim(html) { return !html ? "" : jQuery.trim(html.replace(/(<([^>]+)>)/ig, "")); } // این تنظیم برای پردازش ادیتور مخفی وب لازم است jQuery.validator.setDefaults({ ignore: ":hidden:not(textarea)" }); // متد اصلی اعتبارسنجی را ابتدا ذخیره میکنیم jQuery.validator.methods.originalRequired = jQuery.validator.methods.required; // نحوه بازنویسی متد توکار اعتبار سنجی جهت استفاده از یک متد سفارشی jQuery.validator.addMethod("required", function (value, element, param) { value = removeAllTagsAndTrim(value); if (!value) { return false; } // فراخوانی متد اصلی اعتبار سنجی در صورت شکست تابع سفارشی return jQuery.validator.methods.originalRequired.call(this, value, element, param); }, jQuery.validator.messages.required); </script>
برای اینکار نیاز است متد اصلی required را در جایی ذخیره کنیم تا در صورت شکست اعتبارسنجی سفارشی، همان متد اصلی فراخوانی گردد. در ادامه با فراخوانی jQuery.validator.addMethod و بکارگیری نام required، دقیقا همان متد اصلی required موجود بازنویسی خواهد شد. در ابتدای کار تگهای ورودی پاک شده و سپس اعتبارسنجی میشوند.
در ادامه فراخوانی متد اصلی required را ملاحظه میکنید. همچنین با استفاده از jQuery.validator.messages.required اصل پیام خطای تنظیم شده به کاربر نمایش داده خواهد شد.
مدیریت پیشرفتهی حالت در React با Redux و Mobx - قسمت سوم - روش اتصال Redux به برنامههای React
نصب پیشنیازها
میتوان همانند قسمت قبل، تمام کارها را با کتابخانهی redux انجام داد و یا میتوان قسمت به روز رسانی UI آنرا و همچنین مدیریت state را به کتابخانهی ساده کنندهی دیگری به نام react-redux واگذار کرد. به همین جهت در ادامهی همان برنامهی قسمت قبل، دو کتابخانهی redux و همچنین react-redux را به همراه types آن نصب میکنیم (نصب types، سبب ارائهی intellisense بهتری در VSCode میشود؛ حتی اگر نخواهیم با TypeScript کار کنیم).
برای این منظور پس از باز کردن پوشهی اصلی برنامه توسط VSCode، دکمههای ctrl+` را فشرده (ctrl+back-tick) و دستورات زیر را در ترمینال ظاهر شده وارد کنید:
> npm install --save redux react-redux > npm install --save-dev @types/react-redux
> npm install --save bootstrap
import "bootstrap/dist/css/bootstrap.css";
معرفی ساختار ابتدایی برنامه
برنامهای را که در این قسمت بررسی میکنیم، ساختار بسیار سادهای را داشته و به همراه دو دکمهی افزایش و کاهش مقدار یک شمارشگر است؛ به همراه دکمهی برای به حالت اول در آوردن آن. هدف اصلی دنبال شدهی در اینجا نیز نحوهی برپایی redux و همچنین react-redux و اتصال آنها به برنامهی React جاری است:
به همین جهت ابتدا کامپوننت جدید src\components\counter.jsx را به نحو زیر تشکیل میدهیم تا markup ابتدایی فوق را به همراه سه دکمه و یک span، برای نمایش مقدار شمارشگر، رندر کند:
import React, { Component } from "react"; class Counter extends Component { render() { return ( <section className="card mt-5"> <div className="card-body text-center"> <span className="badge m-2 badge-primary">0</span> </div> <div className="card-footer"> <div className="d-flex justify-content-center align-items-center"> <button className="btn btn-secondary btn-sm">+</button> <button className="btn btn-secondary btn-sm m-2">-</button> <button className="btn btn-danger btn-sm">Reset</button> </div> </div> </section> ); } } export default Counter;
import "./App.css"; import React from "react"; import Counter from "./components/counter"; function App() { return ( <main className="container"> <Counter /> </main> ); } export default App;
پوشه بندی مخصوص برنامههای مبتنی بر Redux
هدف ما در ادامه ایجاد یک store مخصوص redux است و سپس اتصال آن به کامپوننت شمارشگر برنامه. به همین جهت نیاز به 4 پوشهی جدید، برای مدیریت بهتر برنامه خواهیم داشت:
- پوشه constants: برای اینکه نام رشتهای نوع اکشنهای مختلف را بتوانیم در قسمتهای مختلف برنامه استفاده کنیم، بهتر است فایل جدید src\actions\index.js را ایجاد کرده و این ثوابت را داخل آن export کنیم.
- پوشهی actions: در فایل جدید src\actions\index.js، تمام متدهای ایجاد کنندهی شیء خاص action، که در قسمت قبل در مورد آن بحث شد، قرار میگیرند. نمونهی آن، متد createAddAction قسمت قبل است.
- پوشهی reducers: تمام توابع reducer برنامه را در فایلهای مجزایی در پوشهی reducers قرار میدهیم. سپس در فایل src\reducers\index.js با استفاده از متد combineReducer آنها را یکی کرده و به متد createStore ارسال میکنیم.
- پوشهی containers: این پوشه جائی است که کار فراخوانی متد connect کتابخانهی react-redux به ازای هر کامپوننت استفاده کنندهی از redux store، صورت میگیرد.
ایجاد نام نوع اکشن متناظر با دکمهی افزودن مقدار
میخواهیم با کلیک بر روی دکمهی +، مقدار شمارشگر افزایش یابد. به همین جهت نیاز به یک نام وجود دارد تا در تابع Reducer متناظر و قسمتهای دیگر برنامه، بتوان بر اساس آن، این اکشن خاص را شناسایی کرد و سپس عکس العمل نشان داد. به همین جهت فایل جدید src\constants\ActionTypes.js را ایجاد کرده و به صورت زیر تکمیل میکنیم:
export const Increment = "Increment";
ایجاد متد Action Creator
در قسمت قبل مشاهده کردیم که شیء ارسالی به یک reducer از طریق dispatch یک action خاص، دارای فرمت ویژهی زیر است:
{ type: "ADD", payload: { amount // = amount: amount }, meta: {} }
import * as types from "../constants/ActionTypes"; export const incrementValue = () => ({ type: types.Increment });
ایجاد تابع reducer مخصوص افزودن مقدار
ابتدا فایل جدید src\reducers\counter.js را با محتوای زیر ایجاد میکنیم:
import * as types from "../constants/ActionTypes"; const initialState = { count: 0 }; export default function counterReducer(state = initialState, action) { if (action.type === types.Increment) { return { count: state.count + 1 }; } return state; }
- سپس میخواهیم رویداد کلیک بر روی دکمه + را مدیریت کنیم. به همین جهت نیاز به یک اکشن جدید به نام Increment داریم که توسط مقدار ثابت رشتهای types.Increment، از فایل مجزای src\constants\ActionTypes.js، تامین میشود.
- پس از مشخص کردن نوع action ای که قرار است مدیریت شود و همچنین ایجاد متدی برای تولید شیء حاوی اطلاعات آن که در فایل src\actions\index.js قرار دارد، اکنون میتوان متد reducer را که state و action را دریافت میکند و سپس state جدیدی را بر اساس action.type دریافتی و در صورت نیاز بازگشت میدهد، ایجاد کرد. این متد بررسی میکند که آیا action.type رسیده همان ثابت Increment است؟ اگر بله، بجای تغییر مستقیم state.count، یک شیء جدید را بازگشت میدهد. البته روش صحیحتر اینکار را در قسمت اول این سری با معرفی روشهایی برای کپی اشیاء و آرایهها، بررسی کردیم. در اینجا جهت سادگی بیشتر، یک شیء کاملا جدید را دستی ایجاد میکنیم. در آخر اگر action.type رسیده قابل پردازش نبود، همان state ابتدایی دریافتی را بازگشت میدهیم تا در صورت وجود چندین reducer تعریف شدهی در سیستم، زنجیرهی آنها قابل پردازش باشد. این مورد را در قسمت قبل، ذیل عنوان «بررسی تابع combineReducers با یک مثال» بیشتر بررسی کردیم.
پس از ایجاد reducer اختصاصی عمل افزودن مقدار شمارشگر، فایل جدید src\reducers\index.js را نیز با محتوای زیر ایجاد میکنیم:
import { combineReducers } from "redux"; import counterReducer from "./counter"; const rootReducer = combineReducers({ counterReducer }); export default rootReducer;
ایجاد store مخصوص Redux
تا اینجا رسیدیم به یک rootReducer متشکل از تمام reducerهای سفارشی برنامه. اکنون بر اساس آن در فایل src\index.js، یک store جدید را ایجاد میکنیم:
import { createStore } from "redux"; import reducer from "./reducers"; //... const store = createStore( reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ); //...
نکته 2: در اینجا روش فعالسازی افزونهی redux-devtools را نیز ملاحظه میکنید. ابتدا بررسی میشود که آیا متد ویژهی فراخوانی این افزونه وجود دارد یا خیر؟ اگر بله، فراخوانی میشود. بدون این پارامتر دوم، افزونهی redex dev tools، هیچ خروجی را نمایش نخواهد داد.
اتصال React به Redux
کتابخانهی react-redux تنها به همراه دو شیء مهم connect و Provider است. شیء Provider آن شبیه به Context API خود React است و هدف آن، ارسال ارجاعی از store ایجاد شده، به برنامهی React است. پس از ایجاد store در فایل src\index.js، اکنون نوبت به اتصال آن به برنامهی React ای جاری است. به همین جهت در بالاترین سطح برنامه، ابتدا شیء کامپوننت App را با شیء Provider محصور میکنیم:
import { Provider } from "react-redux"; import { createStore } from "redux"; import reducer from "./reducers"; // ... const store = createStore( reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") );
تامین state کامپوننت شمارشگر از طریق props
همانطور که عنوان شد، کامپوننت Counter به همراه state نیست و ما قصد نداریم در آن از state خود React استفاده کنیم؛ البته فلسفهی آنرا در قسمت اول این سری بررسی کردیم و همچنین اگر کامپوننتی نیاز به اشتراک گذاری اطلاعات خودش را با لایههای زیرین یا بالاتر از خود ندارد، شاید اصلا نیازی به Redux نداشته باشد و همان state استاندارد React برای آن کافی است. بنابراین میتوان برنامهای را داشت که ترکیبی از state استاندارد React، در کامپوننتهای متکی به خود و Redux، در کامپوننتهایی که باید اطلاعاتی را با هم به اشتراک بگذارند، باشد. برای مثال، کامپوننت مثال جاری، واقعا نیازی را به Redux، برای مدیریت حالت خود، ندارد؛ هدف ما در اینجا بررسی نحوهی برقراری ارتباطات یک سیستم مبتنی بر Redux، در برنامههای React است.
بنابراین در اینجا و کامپوننتی که قرار است از Redux برای مدیریت حالت خود استفاده کند، هر اطلاعاتی که به آن از طریق react-redux store وارد میشود، از طریق props به آن ارسال خواهد شد. برای مثال در اینجا مقدار count، از طریق props خوانده میشود و همچنین امکان ارسال action ای خاص به متد reducer تعریف شده نیز باید تعریف شود. بنابراین در ادامه نیاز داریم تا یک کامپوننت React را به redux store متصل کنیم. برای این منظور فایل جدید src\containers\Counter.js را با محتوای زیر ایجاد میکنیم:
import { connect } from "react-redux"; import { incrementValue } from "../actions"; import Counter from "../components/counter"; const mapStateToProps = state => { return state; }; const mapDispatchToProps = dispatch => { return { increment() { dispatch(incrementValue()); } }; }; export default connect(mapStateToProps, mapDispatchToProps)(Counter);
زمانیکه در مورد store در redux صحبت میشود، داخل آن یک شیء بزرگ state قرار گرفتهاست که حاوی کل state برنامهاست. اما شاید هر کامپوننت به تمام آن نیازی نداشته باشد. برای مثال شاید کامپوننت شمارشگر، اهمیتی به اطلاعات خطاهای سیستم و یا کاربر وارد شدهی به سیستم که در شیء کلی state موجود در store وجود دارند، ندهد. به همین جهت متد mapStateToProps، کل state برنامه را دریافت کرده و به ما اجازه میدهد تا تنها اطلاعاتی را که از آن نیاز داریم، به صورت props دریافت کنیم. به این ترتیب از رندر مجدد این کامپوننت نیز جلوگیری خواهد شد؛ چون این کامپوننت دیگر وابستهی به تغییرات سایر اجزای کل state برنامه، نخواهد بود و اگر آنها تغییر کردند، این کامپوننت رندر مجدد نخواهد شد.
بنابراین میتوان متد mapStateToProps را به صورت کلی زیر نیز تعریف کرد:
const mapStateToProps = (state) => { return state };
یک نکته: اگر کامپوننتی نیاز به تامین state خود را از طریق props نداشت و فقط کارش صدور رخدادها است، میتوان پارامتر اول متد connect را نال وارد کرد.
پارامتر dispatch متد mapDispatchToProps، به متد store.dispatch اشاره میکند. بنابراین توسط آن امکان ارسال actions را میسر کرده و میتوان state را توسط reducerهای تعریف شده، تغییر داد که در نتیجهی آن props جدیدی به کامپوننت منتقل میشوند. این تابع نیز یک شیء را باز میگرداند. این شیء را فعلا با یک متد دلخواه مقدار دهی میکنیم که توسط پارامتر dispatch رسیدهی به آن، متد action creator تعریف شدهی در فایل src\actions\index.js را به نام incrementValue، فراخوانی میکند؛ دقیقا عملی شبیه به فراخوانی store.dispatch(createAddAction(2)) در قسمت قبل که از آن برای ارسال یک اکشن، به reducer متناظری استفاده شد.
یک نکته: اگر کامپوننتی کار صدور رخدادها را انجام نمیدهد، میتوان پارامتر دوم متد connect را بطور کامل حذف کرد و قید نکرد.
استفاده از کامپوننت جدید خروجی متد connect، جهت تامین props کامپوننت شمارشگر
در انتهای فایل src\components\counter.jsx، چنین سطری درج شدهاست:
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
import "./App.css"; import React from "react"; import CounterContainer from "./containers/Counter"; function App() { return ( <main className="container"> <CounterContainer /> </main> ); } export default App;
اکنون کامپوننت شمارشگر src\components\counter.jsx، دو شیء را از طریق props دریافت میکند؛ یکی کل state است که خاصیت count داخل آن قرار دارد و از طریق mapStateToProps تامین میشود. دیگری متد increment ای است که در متد mapDispatchToProps تعریف کردیم و کار صدور رخدادی را به reducer متناظر، انجام میدهد. به همین جهت تغییرات ذیل را در کامپوننت Counter اعمال میکنیم:
import React, { Component } from "react"; class Counter extends Component { render() { console.log("props", this.props); const { counterReducer: { count }, increment } = this.props; return ( <section className="card mt-5"> <div className="card-body text-center"> <span className="badge m-2 badge-primary">{count}</span> </div> <div className="card-footer"> <div className="d-flex justify-content-center align-items-center"> <button className="btn btn-secondary btn-sm" onClick={increment}> + </button> <button className="btn btn-secondary btn-sm m-2">-</button> <button className="btn btn-danger btn-sm">Reset</button> </div> </div> </section> ); } } export default Counter;
به همین جهت، خاصیت تو در توی this.props.counterReducer.count و همچنین اشارهگر به متد increment، توسط Object Destructuring به صورت زیر از this.props دریافتی، تجزیه شدهاند:
const { counterReducer: { count }, increment } = this.props;
جزئیات کار با Redux store را نیز میتوان در افزونهی redux dev tools مشاهده کرد:
این افزونه در نوار ابزار پایین آن، امکان export کل state و سپس import و بازیابی آنرا نیز به همراه دارد.
دریافت props از طریق کامپوننت دربرگیرنده و ارسال آن به کامپوننت اصلی
فرض کنید نیاز باشد تا اطلاعاتی را به صورت متداول React از طریق props، به کامپوننت دربرگیرندهی کامپوننت شمارشگر ارسال کرد:
function App() { const prop1 = 123 return ( <main className="container"> <CounterContainer prop1={prop1} /> </main> ); }
const mapStateToProps = (state, ownProps) => { console.log("mapStateToProps", { state, ownProps }); return state; };
پیاده سازی دکمهی کاهش مقدار شمارشگر
پس از آشنایی با روش کلی برقراری اتصالات سیستم react-redux، پیاده سازی دکمهی کاهش مقدار شمارشگر بسیار سادهاست و شامل مراحل زیر است:
1) ایجاد نام نوع اکشن متناظر با دکمهی کاهش مقدار
به فایل src\constants\ActionTypes.js، نوع جدید کاهشی را اضافه میکنیم:
export const Decrement = "Decrement";
در فایل src\actions\index.js، متد ایجاد کنندهی شیء اکشن ارسالی به reducer متناظری را تعریف میکنیم تا بتوان بر اساس نوع آن در reducer کاهشی، منطق کاهش را پیاده سازی کرد:
export const decrementValue = () => ({ type: types.Decrement });
اکنون در فایل src\reducers\counter.js، بر اساس نوع شیء رسیده، تصمیم به کاهش یا افزایش مقدار موجود در state گرفته میشود:
export default function counterReducer(state = initialState, action) { // ... if (action.type === types.Decrement) { return { count: state.count - 1 }; } return state; }
در ادامه نیاز است بتوان اکشن کاهش را به این reducer ارسال کرد. به همین جهت به کامپوننت دربرگیرندهی کامپوننت شمارشگر در فایل src\containers\Counter.js مراجعه کرده و به شیء خروجی متد mapDispatchToProps، متد کاهش را اضافه میکنیم:
import { decrementValue, incrementValue } from "../actions"; // ... const mapDispatchToProps = dispatch => { return { // ... decrement() { dispatch(decrementValue()); } }; };
در آخر به فایل src\components\counter.jsx مراجعه کرده و اشارهگر به متد decrement را از طریق this.props دریافت میکنیم:
const { // ... decrement } = this.props;
<button className="btn btn-secondary btn-sm m-2" onClick={decrement} > - </button>
به عنوان تمرین، پیاده سازی دکمهی Reset را نیز انجام دهید که جزئیات آن بسیار شبیه به دو مثال قبلی افزودن و کاهش مقدار شمارشگر است.
بهبود کیفیت کدهای کامپوننت دربرگیرندهی کامپوننت Counter
متد mapDispatchToProps فایل src\containers\Counter.js اکنون چنین شکلی را پیدا کردهاست:
const mapDispatchToProps = dispatch => { return { increment() { dispatch(incrementValue()); }, decrement() { dispatch(decrementValue()); } }; };
import { bindActionCreators } from "redux"; // ... const mapDispatchToProps = dispatch => { return bindActionCreators( { incrementValue, decrementValue }, dispatch ); };
به همین جهت نیاز است در متد رندر کامپوننت src\components\counter.jsx، نامهایی را که به متدهای action creator اشاره میکنند، به صورت زیر تغییر داد:
const { counterReducer: { count }, incrementValue, decrementValue } = this.props;
روش دوم: در نگارشهای اخیر react-redux میتوان متد mapDispatchToProps را به صورت زیر نیز خلاصه و تعریف کرد که بسیار سادهتر است:
const mapDispatchToProps = { incrementValue, decrementValue };
همچنین بجای بازگشت کل state در متد mapStateToProps، میتوان تنها خواص مدنظر را بازگشت داد:
const mapStateToProps = state => { //return state; return { count: state.counterReducer.count }; };
بنابراین باید در متد رندر کامپوننت شمارشگر، خاصیت count را به صورت معمولی دریافت کرد:
const { //counterReducer: { count }, count, incrementValue, decrementValue } = this.props;
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: state-management-redux-mobx-part03.zip
آموزش TypeScript #4
در این پست به مفاهیم شی گرایی در این زبان میپردازیم.
module MyModule1 { module MyModule2 { } }
کلاس ها:
برای تعریف یک کلاس میتوانیم همانند دات نت از کلمه کلیدی class استفاده کنیم. بعد از تعریف کلاس میتوانیم متغیرها و توابع مورد نظر را در این کلاس قرار داده و تعریف کنیم.
module Utilities { export class Logger { log(message: string): void{ if(typeofwindow.console !== 'undefined') { window.console.log(message); } } } }
تابع log که در کلاس بالا تعریف کردیم به صورت پیش فرض public یا عمومی است و نیاز به استفاده export نیست.
برای استفاده از کلاس بالا باید این کلمه کلیدی new استفاده کنیم.
window.onload = function() { varlogger = new Utilities.Logger(); logger.log('Logger is loaded'); };
export class Logger{ constructor(private num: number) { }
اگر به تابع log دقت کنید خواهید دید که یک پارامتر ورودی به نام message دارد که نوع آن string است. در ضمن Typescript از پارامترهای اختیاری( پارامتر با مقدار پیش فرض) نیز پشتیبانی میکند. مثال:
pad(num: number, len: number= 2, char: string= '0')
منظور از پارامترهای Rest یعنی در هنگام فراخوانی توابع محدودیتی برای تعداد پارامترها نیست که معادل params در دات نت است. برای تعریف این گونه پارامترهاکافیست به جای params از ... استفاده نماییم.
function addManyNumbers(...numbers: number[]) { var sum = 0; for(var i = 0; i < numbers.length; i++) { sum += numbers[i]; } returnsum; } var result = addManyNumbers(1,2,3,5,6,7,8,9);
در TypeScript امکان توابع خصوصی با کلمه کلیدی private امکان پذیر است. همانند دات نت با استفاده از کلمه کلیدی private میتوانیم کلاسی تعریف کنیم که فقط برای همان کلاس قابل دسترس باشد(به صورت پیش فرض توابع به صورت عمومی هستند).
module Utilities { Export class Logger { log(message: string): void{ if(typeofwindow.console !== 'undefined') { window.console.log(this.getTimeStamp() + ' -'+ message); window.console.log(this.getTimeStamp() + ' -'+ message); } } private getTimeStamp(): string{ var now = newDate(); return now.getHours() + ':'+ now.getMinutes() + ':'+ now.getSeconds() + ':'+ now.getMilliseconds(); } } }
یک نکته مهم این است که کلمه private فقط برای توابع و متغیرها قابل استفاده است.
تعریف توابع static:
در TypeScript امکان تعریف توابع static وجود دارد. همانند دات نت باید از کلمه کلیدی static استفاده کنیم.
classFormatter { static pad(num: number, len: number, char: string): string{ var output = num.toString(); while(output.length < len) { output = char + output; } returnoutput; } } }
Formatter.pad(now.getSeconds(), 2, '0') +
همان گونه که در دات نت امکان overload کردن توابع میسر است در TypeScript هم این امکان وجود دارد.
static pad(num: number, len?: number, char?: string); static pad(num: string, len?: number, char?: string); static pad(num: any, len: number= 2, char: string= '0') { var output = num.toString(); while(output.length < len) { output = char + output; } returnoutput; }
ادامه دارد...
آیا میدانید CSS سایت شما تا چه اندازه مفید و مورد مصرف بوده و کدامیک از selector های آن بدون مصرف باقیماندهاند؟
خوشبختانه افزونه مفیدی برای فایرفاکس به نام Dust-Me Selectors موجود است که خروجی سایت را بررسی کرده و اضافات را گوشزد خواهد کرد. این افزونه را از آدرس زیر میتوانید دریافت کنید:
https://addons.mozilla.org/en-US/firefox/addon/5392
پس از نصب، یک آیکون جارو به status bar فایرفاکس اضافه خواهد شد که با کلیک بر روی آن، صفحه جاری آنالیز شده و css selectors بدون استفاده در آن گوشزد خواهند شد.
همچنین مورد دیگری که عموما ردیابی آن مشکل است، تشخیص تصاویر مفقود یک صفحه است. کدامیک از عناصری که در فایل HTML نهایی به آنها ارجاعی وجود دارد واقعا در سایت ما موجود است و از قلم نیفتاده است؟
برای این منظور ابتدا Firebug را نصب کنید. سپس افزونه Yslow آنرا نیز باید نصب نمود.
زمانیکه یک صفحه درحال بارگذاری است، بر روی آیکون Yslow در status bar فایرفاکس کلیک کرده و پس از نمایان شدن آن، بر روی Performance کلیک کنید تا کار آنالیز عناصر صفحه آغاز شود. پس از پایان کار، بر روی دکمه components کلیک نمائید تا علاوه بر مشاهده تاثیر عناصر مختلف صفحه بر نحوه بارگذاری و سرعت سایت شما، عناصر مفقود را با رنگ قرمز نمایان سازد.