HTTP Error 403.14 - Forbidden The Web server is configured to not list the contents of this directory. Most likely causes: A default document is not configured for the requested URL, and directory browsing is not enabled on the server. Things you can try: If you do not want to enable directory browsing, ensure that a default document is configured and that the file exists. Enable directory browsing using IIS Manager. Open IIS Manager. In the Features view, double-click Directory Browsing. On the Directory Browsing page, in the Actions pane, click Enable. Verify that the configuration/system.webServer/directoryBrowse@enabled attribute is set to true in the site or application configuration file. Detailed Error Information: Module DirectoryListingModule Notification ExecuteRequestHandler Handler StaticFile Error Code 0x00000000 Requested URL http://localhost:80/3724/ Physical Path C:\inetpub\wwwroot\3724\ Logon Method Anonymous Logon User Anonymous
ایجاد کامپوننت جدید ReducerButtons
برای توضیح مفاهیم این قسمت، فایل جدید src\components\ReducerButtons.tsx را به همراه محتوای زیر ایجاد میکنیم:
import React, { useReducer } from "react"; const initialState = { rValue: true }; type State = { rValue: boolean; }; type Action = { type: string; }; function reducer(state: State, action: Action) { switch (action.type) { case "one": return { rValue: true }; case "two": return { rValue: false }; default: return state; } } export const ReducerButtons = () => { const [state, dispatch] = useReducer(reducer, initialState); return ( <div> {state?.rValue && <h1>Visible</h1>} <button onClick={() => dispatch({ type: "one" })}>Action One</button> <button onClick={() => dispatch({ type: "two" })}>Action Two</button> </div> ); };
- این کامپوننت دو دکمه را رندر میکند تا توسط آنها بتوان چندین Action مختلف را جهت مدیریت حالت، سبب شد؛ مانند Action One و Two.
- initialState را در این مثال، یک شیء ساده در نظر گرفتهایم که تنها دارای یک مقدار boolean است.
- سپس نیاز به یک تابع ویژه را به نام reducer، داریم که مقدار state جاری و یک action را دریافت میکند. این تابع بر اساس نوع Action ای که به آن میرسد، state جدیدی را بازگشت میدهد و در قسمت رندر آن، با بررسی state?.rValue، سبب نمایش عبارتی خواهیم شد.
- در حین تعریف هوک useReducer، قسمت dispatch آن، تابعی است که یک Action را اجرا میکند. فراخوانی آنرا در رویداد onClick دو دکمهی تعریف شده مشاهده میکنید. این تابع یک شیء را دریافت میکند که type اکشن ارسالی به تابع reducer را مقدار دهی میکند.
توسط useReducer برای ایجاد تغییرات در شیء state کامپوننت جاری، از مفهومی به نام dispatch actions استفاده میشود. action در اینجا به معنای رخدادن چیزی است؛ مانند کلیک بر روی یک دکمه و یا دریافت اطلاعاتی از یک API. در این حالت مقایسهای بین وضعیت قبلی state و وضعیت فعلی آن صورت میگیرد و تغییرات مورد نیاز جهت اعمال به UI، محاسبه خواهند شد. اصلیترین جزء آن، تابعی است به نام Reducer. این تابع، یک تابع خالص است و دو آرگومان را دریافت میکند. تابع Reducer، بر اساس action و یا رخدادی، ابتدا کل state برنامه را دریافت میکند و سپس خروجی آن بر اساس منطق این تابع، یک state جدید خواهد بود. اکنون که این state جدید را داریم، برنامهی React ما میتواند به تغییرات آن گوش فرا داده و بر اساس آن، UI را به روز رسانی کند.
اولین قسمتی را که در حین کار با useReducer توسط TypeScript به آن برخورد خواهیم کرد، نمایش خطاهایی در مورد نوعهای پارامترهای state و action تابع reducer است. در حالت متداول جاوا اسکریپتی آن، این پارامترها، بدون نوع ذکر میشوند که در اینجا به any تفسیر خواهند شد و یک چنین تعاریفی با توجه به strict بودن تنظیمات tsconfig.json برنامه، مجاز نیست. به همین جهت نیاز به تعریف دو type جدید به نامهای State و Action در اینجا وجود دارد تا خطاهای بدون نوع بودن پارامترهای تابع reducer برطرف شوند. نوع Action به همراه حداقل یک خاصیت به نام action رشتهای است و نوع State بر اساس initialState ای که تعریف کردیم، دارای یک خاصیت متناظر boolean است.
نکتهی جالب دومی که در اینجا توسط TypeScript گوشزد میشود، الزام به ذکر حالت default مربوط به switch ای است که در تابع reducer تعریف کردهایم. اگر این حالت را حذف کنیم، بلافاصله خطای زیر را در قسمت تعریف useReducer نمایش میدهد:
عنوان میکند که خروجی تابع reducer، یک state جدید است؛ اما ممکن است از نوع never هم باشد که قابلیت ترجمهی به نوع state را ندارد.
بهبود تعاریف نوعهای useReducer
تا اینجا مهمترین تغییرات تایپاسکریپتی صورت گرفته، تعریف نوعهای پارامترهای تابع reducer است. اکنون فرض کنید میخواهیم، سومین Action را هم به کامپوننت جاری اضافه کنیم:
<button onClick={() => dispatch({ type: "three" })}>Action Three</button>
type Action = { type: "one" | "two" | "three"; };
و یا حتی اگر مقدار action.type ای را در تابع reducer به اشتباه وارد کردیم، باز هم برنامه کامپایل نمیشود:
import React, { useReducer } from "react"; const initialState = { rValue: true }; type State = { rValue: boolean; }; type Action = { type: "one" | "two" | "three"; }; function reducer(state: State, action: Action) { switch (action.type) { case "one": return { rValue: true }; case "two": return { rValue: false }; case "three": return { rValue: false }; } } export const ReducerButtons = () => { const [state, dispatch] = useReducer(reducer, initialState); return ( <div> {state?.rValue && <h1>Visible</h1>} <button onClick={() => dispatch({ type: "one" })}>Action One</button> <button onClick={() => dispatch({ type: "two" })}>Action Two</button> <button onClick={() => dispatch({ type: "three" })}>Action Three</button> </div> ); };
تا اینجای کار ساخت کامپوننتها را با React.createClass که تفاوتی با توسعه (ارث بری) از کلاس React.Component ندارد، انجام دادهایم. اما ساخت کامپوننتها به صورت یک تابع هم مزیتهایی را دارد. اول از همه باید بدانیم که ساخت کامپوننت توسط تابع، بدون وضعیت خواهد بود که به آن Stateless میگویند. به دلیل نداشتن وضعیت، کامپوننتهای تابعی را کمی بهتر میشود برای استفاده مجدد به کار برد. در کامپوننتهای غیر تابعی که Stateful هستند به دلیل احتمال وابستگی وضعیت کامپوننت به خارج از کلاس، مانند مثال قسمت چهارم که کامپوننت در انتظار کلیک یک دکمه خاص توسط کاربر بود، مدیریت استفاده مجدد ازکامپوننت چالش برانگیز خواهد شد.
اعتبارسنجی دادههای ورودی
برای مدیریت بهتر کامپوننتها جهت استفاده مجدد از آنها بهتر است ورودیهای کامپوننت را اعتبارسنجی کنیم. این ورودیها چه برای استفاده داخلی کامپوننت، یا جهت مشخص کردن وضعیت آن، بر رفتار کامپوننت تاثیر زیادی دارند. React مجموعهای از اعتبار سنجیها را دارد که میشود به کامپوننت اضافه کرد. باید توجه داشته باشیم که پیامهای خطای این اعتبارسنجیها فقط در حالت Development Mode قابل استفاده هستند. به زبان ساده اگر از react.min.js استفاده کنید، پیامهای خطا را نخواهید دید. باید فایلها را به نوع react.js تبدیل کنید. اعتبارسنجی React در زمان توسعه و برای توسعه دهندگان استفاده میشود.
مثال نوشیدنیها در قسمت چهارم میتوانست نام نوشیدنی و قیمت آن را نمایش دهد و همچنین میتوانستیم به لیست نوشیدنیها، موردی را اضافه کنیم. اگر ورودی قیمت، اعتبارسنجی نشود، میتوان رشتهای را بجای عدد به عنوان قیمت به کامپوننت ارسال کرد. نحوه اعتبارسنجی قیمت نوشیدنیها به صورت زیر است.
const MenuItem = props => ( <li className="list-group-item"> <span className="badge">{props.price}</span> <p>{props.item}</p> </li> ) MenuItem.propTypes = { price: React.PropTypes.number };
شیء propTypes را به کامپوننت اضافه کردهایم و در تنظیمات آن میتوانیم برای هر پارامتر ورودی یکی از اعضای PropTypes از React را که مناسب حال پارامتر است، انتخاب کنیم. در مثال بالا مشخص کردهایم که ورودی price باید عدد باشد و اگر مثلا رشتهای را بجای عدد ارسال کنیم، پیام خطای زیر را در Console خواهیم داشت.
Warning: Failed prop type: Invalid prop `price` of type `string` supplied to `MenuItem`, expected `number`. in MenuItem (created by Menu) in Menu
یا میتوانستیم از React.PropTypes.number.isRequired استفاده کنیم تا درج مقداری برای این ورودی الزامی باشد. اگر اعتبارسنجیهای React کافی نبودند میتوانیم اعتبارسنجیهای سفارشی خودمان را ایجاد کنیم. در مثال زیر میخواهیم ورودی price بیشتر از 15000 نباشد.
MenuItem.propTypes = { price: (props, price)=>{ if(props[price] > 15000){ return new Error("Too expensive!"); } } };
let MenuItem = React.createClass({ propTypes: { price: React.PropTypes.number } }); class MenuItem extends React.Component{ static propTypes = { price: React.PropTypes.number }; }
نوعهای دیگر برای اعتبارسنجی شامل موارد زیر هستند و البته مرجع تمام اعتبارسنجیهای React را میتوانید در اینجا بررسی کنید.
- React.PropTypes.array
- React.PropTypes.bool
- React.PropTypes.number
- React.PropTypes.object
- React.PropTypes.string
مقدار پیشفرض دادههای ورودی
یکی از امکانات مفید دیگر برای مدیریت مقدارهای ورودی، مشخص کردن مقدار پیشفرضی برای یک پارامتر است. برای مثال اگر برای قیمت یک نوشیدنی مقداری وارد نشد، یک حداقل قیمت برای آن در نظر بگیریم، هر چند که ایده و روشی اشتباه است!
MenuItem.defaultProps = { price: 1000 };
همانطور که میبینید روش کار مشابه با اعتبارسنجی است و برای مشخص کردن مقدار پیشفرض برای React.creatClass از متد getDefaultProps که عضوی از React است، استفاده میکنیم.
let MenuItem = React.createClass({ getDefaultProps() { return { price: 200 } }, render() { return ( <li className="list-group-item"> <span className="badge">{this.props.price}</span> <p>{this.props.item}</p> </li> ); } });
یک TextArea ساده را به صفحه اضافه کرده و این افزونه جیکوئری را بر روی آن اجرا میکنید. به این ترتیب TextArea به صورت خودکار تبدیل به یک ویرایشگر مطلوب خواهد شد. برای مثال:
@Html.TextAreaFor(model => model.ArticleBody, htmlAttributes: new { style = "width:98%; height:500px" }) <script type="text/javascript"> $('#ArticleBody').redactor({ autoformat: false, convertDivs: false }); </script>
یک سری مثال نوشته شده با PHP به همراه این ویرایشگر آنلاین هستند که برای ایده گرفتن بد نیستند (البته به این معنا نیست که این ویرایشگر نیازی به PHP دارد. تنها قسمت سمت سرور مثالهای آن با PHP است). برای مثال اگر در PHP از دستور echo برای ارائه یک نتیجه نهایی به ویرایشگر RedActor استفاده شده، معادل آن در ASP.NET MVC مساوی return Content است.
<script type="text/javascript"> $('#ArticleBody').redactor({ imageUpload: "@Url.Action(result: MVC.RedactorUpload.ImageUpload())", fileUpload: "@Url.Action(result: MVC.RedactorUpload.FileUpload())", linkFileUpload: "@Url.Action(result: MVC.RedactorUpload.FileLinkUpload())" , autoformat: false , convertDivs: false }); </script>
همانطور که ملاحظه میکنید از T4MVC برای مشخص سازی مسیرها استفاده شده. برای مثال MVC.RedactorUpload.ImageUpload به این معنا است که در کنترلری به نام RedactorUpload، اکشن متدی به نام ImageUpload پذیرای ارسال فایل ادیتور خواهد بود و به همین ترتیب در مورد سایر پارامترها.
RedactorUploadController هم ساختار بسیار سادهای دارد. برای مثال هر کدام از متدهای آپلود یاد شده یک چنین امضایی دارند:
[HttpPost] public virtual ActionResult ImageUpload(HttpPostedFileBase file) { }
[AllowUploadSpecialFilesOnly(".jpg,.gif,.png")]
در حالت imageUpload، محتوایی به شکل زیر باید بازگشت داده شود:
return Content("<img src='" + path + "' />");
return Content("<a href=" + path + ">" + someName + "</a>");
return Content(path);
همچنین باید دقت داشت که کار ارسال فایل به سرور توسط خود افزونه RedActor انجام میشود و نیازی به کدنویسی ندارد. فقط باید سمت سرور آنرا به نحوی که عنوان شد مدیریت کنید. ابتدا فایل را در سرور ذخیره کنید. سپس باید یک محتوای رشتهای را به نحو یاد شده، ساخت و توسط return Content بازگشت داد.
پ.ن.
قسمتی از مطالب متن فوق در نگارش جدید این ویرایشگر به نحو زیر تغییر کرده است.
با توجه به اینکه React یک سیستم متشکل از کامپوننتهای کوچک و بزرگ است و از JSX جهت کدنویسی استفاده میکند و یک قالب HTML، متشکل از تمام عناصر به صورت درهم ریخته میباشد و بخشهای مختلفی دارد، امکان استفادهی مستقیم از قالب HTML در آن وجود ندارد و باید با فرمت React همخوانی داشته باشد. من در اینجا از قالب رایگان و راستچین شده AdminLTE که بر پایه بوت استرپ 4 میباشد استفاده کردهام.
همانطور که میدانید React پوشهای را به نام public، مهیا کردهاست که برای استفادهی عمومی از فایلهای استاتیک ایجاد شدهاست. پس ابتدا فایلهای js,css، تصاویر و دیگر فایلهای استاتیک را به پوشهی public منتقل میکنیم. سپس فایل index قالب را باز کرده و به تگ header فایل مراجعه کنید. تگهای لینکهای معرفی فایلهای css و script ای را که در آن تعریف شدهاند، کپی کرده به هدر فایل index.html که در پوشهی public قرار دارد، منتقل کنید. همچنین از فایلهای اسکرپیت دیگر که در پایین تگ Body قرار گرفتهاند، غافل نگردید.
در اینجا باید بخشهای اساسی قالب، همانند navbar و sidebar را به صورت کامپوننت ایجاد کنیم.
پس ابتدا یک کامپوننت NavBar.jsx را ایجاد کرده و کدهای همین قسمت را در متد render قرار میدهیم:
import React, { Component } from "react";
class NavBar extends Component {
state = {};
render() {
return (
<React.Fragment>
<nav>
<!-- Left navbar links -->
<ul>
<li>
<a data-widget="pushmenu" href="#"><i></i></a>
</li>
<li>
<a href="index3.html">خانه</a>
</li>
<li>
<a href="#">تماس</a>
</li>
</ul>
<!-- SEARCH FORM -->
<form>
<div>
<input type="search" placeholder="جستجو" aria-label="Search">
<div>
<button type="submit">
<i></i>
</button>
</div>
</div>
</form>
...
</nav>
</React.Fragment>
);
}
}
export default NavBar;
- تمامی کامنتهای موجود در فایل را حذف کنید.
- تمام تگها که شامل خصوصیت class هستند را با استفاده از ابزار جستجو، یافته و با عبارت className جایگزین کنید.
- در صورتیکه روی تگها از خصوصیت style استفاده کردهاید، به شکل زیر ویرایش کرده و قالب jsx را روی آن پیاده کنید.
style="opacity:0.8;"
style={{ opacity: "0.8" }}
- در صورتیکه از تگهای img و یا input استفاده میکنید، حتما باید انتها تگها به شکل زیر بسته شده باشند:
<input type="search" placeholder="جستجو" aria-label="Search">
<input className="form-control form-control-navbar" type="search" placeholder="جستجو" aria-label="Search" />
import React, { Component } from "react"; class NavBar extends Component { state = {}; render() { return ( <React.Fragment> <nav className="main-header navbar navbar-expand bg-white navbar-light border-bottom"> <ul className="navbar-nav"> <li className="nav-item"> <a className="nav-link" data-widget="pushmenu" href="#"> <i className="fa fa-bars"></i> </a> </li> <li className="nav-item d-none d-sm-inline-block"> <a href="index3.html" className="nav-link"> خانه </a> </li> <li className="nav-item d-none d-sm-inline-block"> <a href="#" className="nav-link"> تماس </a> </li> </ul> <form className="form-inline ml-3"> <div className="input-group input-group-sm"> <input className="form-control form-control-navbar" type="search" placeholder="جستجو" aria-label="Search" /> <div className="input-group-append"> <button className="btn btn-navbar" type="submit"> <i className="fa fa-search"></i> </button> </div> </div> </form> ... </nav> </React.Fragment> ); } } export default NavBar;
return ( <React.Fragment> <NavBar /> <SideBar /> <div className="content-wrapper" style={{ marginTop: "20px" }}> <DomainList /> </div> <Footer /> </React.Fragment> )
تا به اینجا مثالهایی که زدهایم تاثیر کامپوننتهای React را بر روی UI، نشان دادند. در این بخش به رویدادهای سمت UI و ورودیهای کاربر میپردازیم.
رویدادهای ترکیبی React
React روش مدیریت رویدادهای خودش را دارد و به آنها رویدادهای Synthetic یا ترکیبی گفته میشود. در زیر مقایسهای داریم از رویدادهای معمول در JavaScript و رویدادهای React و تفاوتها را بررسی میکنیم.
<!-- HTML Buttons --> <button type="button" onclick="console.log('Button Clicked')">Click Me</button> // React Buttons <button type="button" onClick={console.log("Button Clicked")}>Click Me</button>
- باید نام رویدادها را بصورت camelCase تایپ کنیم.
- از جاوااسکریپت به طور مستقیم استفاده میکنیم؛ نه بین quotation markها.
- برای رویدادها از توابع استفاده میکنیم و بهتر است تابع اجرایی هر رویداد در خود کامپوننت ساخته شود.
- رویداد onClick در React به نوعی override شده رویداد onclick مرورگر است و به جای آن عمل میکند.
رفتار رویدادهای React در مرورگرهای مختلف یکسان است. برای مثال رویداد onChange هر تغییری را برای هر نوع تگ ورودی اعمال میکند. هر کلیدی که در یک input یا textarea زده شود، اگر یک check box را انتخاب یا از انتخاب خارج کنیم و یا اگر موردی را از یک drop-down انتخاب کنیم، React رویداد onChange را اجرا میکند. React اکثر رویدادهای مرسوم را پوشش میدهد و همچنین رویدادهایی را برای کار با کلیپبرد، رسانههای مختلف و تصاویر دارد. برای اطلاعات بیشتر به مستندات آن رجوع کنید.
وقتی با کتابخانه React کار میکنیم، همه چیز مجازی اتفاق میافتد؛ مانند ساخت تگ و نمایش آنها، همچنین مدیریت تگها و رویدادها. اما به این معنا نیست که ارتباط React با HTML DOM در مرورگر قطع است. اگر لازم باشد به HTML DOM در کامپوننتها دسترسی داشته باشیم میتوانیم از خاصیت ref در React استفاده کنیم. برای مثال فرض کنید یک ورودی را برای ایمیل بهصورت <input type="email" /> تعریف کردهایم. میخواهیم پیش از ذخیره بدانیم آیا داده وارد شده به فرمت ایمیل هست یا نه.
const EmailForm = React.createClass({ clickHandler() { if (this.inputEmail.checkValidity()) console.log("Email is OK to save it."); else console.log("Email is not in right format."); }, render() { return ( <div> <input type="email" ref={inputEmail => this.inputEmail = inputEmail} /> <button type="submit" onClick={this.clickHandler}>Save</button> </div> ) }
در مثال بالا clickHandler وظیفه مدیریت رویداد کلیک دکمه را به عهده دارد. در ادامه، وقتی از خاصیت ref در تگ input استفاده میکنیم و مقدار آن را یک تابع قرار میدهیم، React این تابع را زمانیکه کامپوننت به طور کامل در HTML DOM ساخته شد، اجرا میکند. React همچنین ارجاعی را به عنوان پارامتر این تابع به DOM همراه با تابع ارسال میکند (inputEmail). داخل تابع ref میتوانیم به نمونه ساخته شده از کامپوننت در DOM دسترسی داشته باشیم. inputEmail که به صورت ارجاع به تابع فرستاده شده، تگ ساخته شده input را برمیگرداند، در نتیجه میتوانیم در کامپوننت به آن دسترسی داشته باشیم.
تغییر وضعیت کامپوننت
اگر از کامپوننتهای Sateful که دارای وضعیت هستند استفاده میکنیم، میتوانیم وضعیت کامپوننت را بر اساس ورودیهای کاربر تغییر دهیم. مثال بالا را به این شکل تغییر میدهیم که در ابتدا وضعیت کامپوننت، یک ایمیل پیشفرض باشد و اگر کاربر آدرس متفاوتی را وارد کرد، آدرس جدید به عنوان وضعیت جدید کامپوننت در نظر گرفته شود.
const EmailForm = React.createClass({ getInitialState() { return { currentEmail: this.props.currentEmail } }, setCurrentEmailState(se) { this.setState({ currentEmail: se.target.value }); }, clickHandler() { if (this.inputEmail.checkValidity()) console.log("Email is OK to save it."); else console.log("Email is not in right format."); }, render() { return ( <div> <input type="email" ref={inputEmail => this.inputEmail = inputEmail} value={this.state.currentEmail} onChange={this.setCurrentEmailState} /> <button type="submit" onClick={this.clickHandler}>Save</button> </div> ) } })
در خط 20 از مثال بالا با قرار دادن مقدار value برابر با ایمیل جاری (وضعیت کامپوننت)، کاربر آدرس پیشفرض را در input میبیند، اما هیچ تغییری را نمیتواند در آن ایجاد کند و input عملا تبدیل به یک تگ فقط خواندنی میشود. علت این است که React دو وضعیت را ایجاد کرده، یکی در حافظه به عنوان وضعیت پیشفرض و دیگری وضعیتی که در DOM ساخته. وقتی در سطح DOM تغییری را ایجاد میکنیم، React به صورت خودکار متوجه آن نمیشود و ما باید با روشی React را در جریان این تغییرات قرار دهیم! برای این کار رویداد onChange را برای تگی که قرار است تغییر کند پیادهسازی میکنیم. در مثال بالا متد setCurrentEmailState و رویداد onChange برای همین منظور به کار گرفته شدهاند.
در قسمت بعد که آخرین قسمت است، به مسئله چرخه زندگی (Lifecycle) کامپوننتهای React میپردازیم.
برای استفاده سادهتر از ابزارهای unit testing در ویژوال استودیو افزونههای زیادی وجود دارند، از ری شارپر تا CodeRush تا حتی امکانات نسخهی کامل VS.NET که با MSTest یکپارچه است. اما اگر نخواهیم از MSTest استفاده کنیم و همچنین افزونهها را هم بخواهیم حذف کنیم (مثلا از نسخهی رایگان express استفاده کنیم)، چطور؟
برای حل این مشکل چندین روش وجود دارد. یا میشود از test runner اینها استفاده کرد که اصلا نیازی به IDE ندارند و مستقل است؛ یا میتوان به صورت زیر هم عمل کرد:
به خواص پروژه در VS.NET مراجعه کنید. برگهی Build events را باز کنید. در اینجا میخواهیم post-build event را مقدار دهی کنیم. به این معنا که پس از هر build موفق، لطفا این دستورات خط فرمان را اجرا کن.
NUnit به همراه test runner خط فرمان هم ارائه میشود و نام آن nunit-console.exe است. اگر به محل نصب آن مراجعه کنید، عموما در آدرس C:\Program Files\NUnit xyz\bin\nunit-console.exe قرار دارد. برای استفاده از آن تنها کافی است تنظیم زیر صورت گیرد:
c:\path\nunit-console.exe /nologo $(TargetPath)
TargetPath به صورت خودکار با نام اسمبلی جاری پروژه در زمان اجرا جایگزین میشود.
اکنون پس از هر Build، به صورت خودکار nunit-console.exe اجرا شده، اسمبلی برنامه که حاوی آزمونهای واحد است به آن ارسال گردیده و سپس خروجی کار در output window نمایش داده میشود. اگر خطایی هم رخ داده باشد در قسمت errors قابل مشاهده خواهد بود.
در اینجا حتی بجای برنامه کنسول یاده شده میتوان از برنامه nunit.exe هم استفاده کرد. در این حالت GUI اصلی پس از هر Build نمایش داده میشود:
c:\path\nunit.exe $(TargetPath)
چند نکته:
1- برنامه nunit-console.exe چون در حال حاضر برای دات نت 2 کامپایل شده امکان بارگذاری dll های دات نت 4 را ندارد. به همین منظور فایل nunit-console.exe.config را باز کرده و تنظیمات زیر را به آن اعمال کنید:
<configuration>
<startup>
<supportedRuntime version="v4.0.30319" />
</startup>
و همچنین:
<runtime>
<loadFromRemoteSources enabled="true" />
2- خروجی نتایج اجرای آزمونها را به صورت XML هم میتوان ذخیره کرد. مثلا:
c:\path\nunit-console.exe /xml:$(ProjectName)-tests.xml /nologo $(TargetPath)
3- از فایل xml ذکر شده میتوان گزارشات زیبایی تهیه کرد. برای مثال:
Generating Report for NUnit
NUnit2Report Task
جهت مطالعه بیشتر:
Setting up Visual C#2010 Express with NUnit
Use Visual Studio's Post-Build Events to Automate Unit Testing Running
3 Ways to Run NUnit From Visual Studio
کتابخانه Timesheet.js
- Observable state: در MobX نیز همانند Redux، کل شیء state به صورت یک شیء جاوا اسکریپتی ارائه میشود؛ با این تفاوت که در اینجا این شیء، یک Observable است که نمونهای از مفهوم آنرا در مثال قسمت قبل بررسی کردیم.
- Actions: متدهایی هستند که state را تغییر میدهند.
- Computed properties: در مورد مفهوم خواص محاسباتی در قسمت قبل بحث کردیم. این خواص، مقدار خود را بر اساس تغییرات سایر خواص Observable، به روز میکنند.
- Reactions: سبب بروز اثرات جانبی (side effects) میشوند؛ مانند تعامل با دنیای خارج. نمونهای از آن، متد autorun است که تغییرات Observableها را ردیابی میکند.
برای مثال خاصیت محاسباتی fullName، تغییرات سایر خواص Observable را احساس کرده و مقدار خودش را به روز میکند. سپس یک Reaction به آن، میتواند به روز رسانی DOM، جهت نمایش این تغییرات باشد و یا نمونهی دیگری که میتواند بسیاری از این مفاهیم را نمایش دهد، کلاس زیر است:
import { action, observable, computed } from 'mobx'; class PizzaCalculator { @observable numberOfPeople = 0; @observable slicesPerPerson = 2; @observable slicesPerPie = 8; @computed get slicesNeeded() { console.log('Getting slices needed'); return this.numberOfPeople * this.slicesPerPerson; } @computed get piesNeeded() { console.log('Getting pies needed'); return Math.ceil(this.slicesNeeded / this.slicesPerPie); } @action addGuest() { this.numberOfPeople!++; } }
- برای مثال زمانیکه تعریف observable numberOfPeople@ را داریم، به این معنا است که میخواهیم تغییرات تعداد افراد را تحت نظر قرار دهیم و اگر تغییری در مقدار آن صورت گرفت، آنگاه مقدار خواص محاسباتی که با computed@ مزین شدهاند، به صورت خودکار به روز رسانی شوند.
- action@ به این معنا است که متدی در اینجا، سبب بروز تغییری در state کلاس جاری میشود. MobX به همراه یک strict mode است که اگر فعال باشد، ذکر تزئین کنندهی action@ بر روی یک چنین متدهایی ضروری است، در غیراینصورت، الزامی به درج آن نیست.
در این قطعه کد تعدای console.log را هم ملاحظه میکنید. علت آن نمایش مفهوم کش کردن اطلاعات در MobX است. فرض کنید برای بار اول، مقدار یکی از خواصی را که به صورت observable تعریف شدهاند، تغییر میدهیم. در این حالت تمام خواص محاسباتی وابستهی به آنها، به صورت خودکار مجددا محاسبه شده و console.logها را نیز مشاهده خواهیم کرد. اگر برای بار دوم همین فراخوانی صورت گیرد و تغییری در مقادیر خواص observable صورت نگیرد، MobX از نگارش کش شدهی این خواص محاسباتی استفاده میکند و بیجهت سبب رندر مجدد UI نخواهد شد که در نهایت کارآیی بالایی را سبب خواهد شد. برای پیاده سازی یک چنین قابلیتی با Redux باید از مفهومی مانند React.memo و Memoization و کتابخانههای کمکی مانند Reselect استفاده کرد؛ اما در اینجا به صورت توکار و خودکار اعمال میشود.
ساختارهای دادهای که توسط MobX پشتیبانی میشوند
MobX از اکثر ساختارهای دادهای متداول در جاوا اسکریپت پشتیبانی میکند؛ برای مثال:
- اشیاء مانند ({})observable
- آرایهها مانند ([])observable
- Maps مانند observable(new Map())
چند نکته:
- همانطور که در قسمت قبل نیز ذکر شد، decorators در اصل یکسری تابع هستند و برای مثال میتوان observable را به صورت observable@ و یا به صورت یک تابع معمولی مورد استفاده قرار داد.
- اگر شیءای را به صورت ({})observable معرفی کنیم، با افزودن خواصی به آن پس از این فراخوانی، این خواص دیگر مورد ردیابی قرار نخواهند گرفت. علت آنرا هم در شبهکد زیر میتوان مشاهده کرد:
const extendObservable = (target, source) => { source.keys().forEach(key => { const wrappedInObservable = observable(source[key]); Object.defineProperty(target, key, { set: value.set. get: value.get }); }); };
برای رفع این مشکل میتوان از Map استفاده کرد. یعنی در اینجا اگر قرار است تعداد خواص اشیاء را به صورت پویا تغییر دهید، آنها را به صورت Map تعریف کنید؛ چون متد set آن توسط observableها ردیابی میشود.
استفاده از MobX با React توسط کتابخانهی mobx-react
تا اینجا MobX را به صورت متکی به خود مورد بررسی قرار دادیم. اکنون قصد داریم آنرا به یک برنامهی React متصل کنیم. برای اینکار کتابخانههای زیادی وجود دارند که در این قسمت کلیات روش کار با کتابخانهی mobx-react را در بین آنها بررسی میکنیم.
نصب کتابخانهی mobx-react
ابتدا نیاز است تا این کتابخانه را نصب کنیم:
> npm install --save mobx mobx-react
تحت نظر قرار دادن کامپوننتها
در ادامه پس از نصب کتابخانهی mobx-react، نیاز است کامپوننتها را تحت نظر MobX قرار دهیم که اینکار را میتوان توسط تزئین کنندهی observer آن انجام داد. همانطور که عنوان شد، تزئین کنندهها را میتوان به صورت معمولی observer@ به یک کلاس و یا به صورت فراخوانی تابع، برای مثال به یک کامپوننت تابعی اعمال کرد. برای نمونه کامپوننتهای کلاسی را به نحو زیر میتوان با observer@ مزین کرد:
import { observer } from "mobx-react"; @observer class Counter extends Component {
و یا کامپوننتهای تابعی را میتوان توسط متد observer به صورت زیر محصور کرد:
const Counter = observer(({ count }) => { return ( // ... ); });
class ContainerComponent extends Component () { componentDidMount() { this.stopListening = autorun(() => this.render()); } componentWillUnmount() { this.stopListening(); } render() { … } }
تعریف مخزن و اتصال آن به کامپوننتها
کار شیء Provider که بالاترین کامپوننت را در سلسله مراتب کامپوننتها محصور میکند، ارائهی store، به تمام کامپوننتهای فرزند است. در Redux فقط یک store را داریم که به شیء Provider آن ارسال میکنیم. اما در حین کار با MobX چنین محدودیتی وجود ندارد و میتوان چندین store را تعریف کرد و در اختیار برنامه قرار داد که شبهکد نحوهی تعریف آن به صورت زیر است:
import { Provider } from 'mobx-react'; import ItemStore from './store/ItemStore'; import Application from './components/Application'; const itemStore = new ItemStore(); ReactDOM.render( <Provider itemStore={itemStore}> <Application /> </Provider>, document.getElementById('root'), );
@inject('itemStore') class NewItem extends Component { // ...
const UnpackedItems = inject('itemStore')( observer(({ itemStore }) => ( // ... )), );
یک مثال: پیاده سازی مثال شمارشگر قسمت سوم این سری با mobx-react
در ادامه قصد داریم برنامهی شمارشگر ارائه شده در قسمت سوم بررسی redux را با mobx پیاده سازی کنیم. به همین جهت یک پروژهی جدید React را ایجاد میکنیم:
> create-react-app state-management-with-mobx-part2 > cd state-management-with-mobx-part2 > npm start
> npm install --save mobx mobx-react bootstrap
import "bootstrap/dist/css/bootstrap.css";
پس از آن فایل src\index.js را به صورت زیر تغییر میدهیم:
import "./index.css"; import "bootstrap/dist/css/bootstrap.css"; import { autorun, decorate, observable } from "mobx"; import React from "react"; import ReactDOM from "react-dom"; import Counter from "./components/Counter"; import * as serviceWorker from "./serviceWorker"; class Count { value = 0; increment = () => { this.value++; }; decrement = () => { this.value--; }; } decorate(Count, { value: observable }); const count = (window.count = new Count()); autorun(() => console.log("The count changed!", count.value)); ReactDOM.render( <main className="container"> <Counter count={count} /> </main>, document.getElementById("root") ); serviceWorker.unregister();
- در قسمت قبل، روش تحت نظر قرار دادن یک شیء متداول جاوا اسکریپتی را توسط متد observable مشاهده کردیم. در اینجا نگارش کلاسی آن مثال را بر اساس کلاس Count مشاهده میکنید. اگر نخواهیم از decorator ای مانند observable@ بر روی خاصیت value این کلاس استفاده کنیم، روش تابعی آنرا با فراخوانی متد decorate و ذکر نوع کلاس و سپس خاصیتی که باید به صورت observable تحت نظر قرار گیرد، در اینجا مشاهده میکنید. این هم یک روش کار با mobx است.
- پس از ایجاد کلاس Count که در اینجا نقش store را نیز بازی میکند، یک وهلهی جدید را از آن ساخته و به متغیر count در این ماژول و همچنین window.count انتساب میدهیم. انتساب window.count سبب میشود تا بتوان در کنسول توسعه دهندگان مرورگر، با نوشتن count و سپس enter، به محتویات این متغیر دسترسی یافت و یا حتی آنرا تغییر داد؛ مانند تصویر زیر که بلافاصله این تغییر، در UI برنامه نیز منعکس میشود:
- در اینجا تعریف شیء Provider را که پیشتر در مورد آن بحث کردیم، مشاهده نمیکنید؛ چون با تک کامپوننت Counter تعریف شدهی در این مثال، نیازی به آن نیست. میتوان این شیء store را به صورت مستقیم به props کامپوننت Counter ارسال کرد.
اکنون تعریف کامپوننت شمارشگر واقع در فایل src\components\Counter.jsx به صورت زیر خواهد بود که این کامپوننت، count را به صورت props دریافت میکند:
import { observer } from "mobx-react"; import React from "react"; const Counter = observer(({ count }) => { return ( <section className="card mt-5"> <div className="card-body text-center"> <span className="badge m-2 badge-primary">{count.value}</span> </div> <div className="card-footer"> <div className="d-flex justify-content-center align-items-center"> <button className="btn btn-secondary btn-sm" onClick={count.increment} > + </button> <button className="btn btn-secondary btn-sm m-2" onClick={count.decrement} > - </button> </div> </div> </section> ); }); export default Counter;
تا زمانیکه کامپوننت، با تابع observer محصور شدهاست، به props رسیده گوش فرا داده و خواص و اشیاء observable آنرا تشخیص میدهد و سبب رندر مجدد کامپوننت، با تغییری در آنها خواهد شد.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-with-mobx-part2.zip
مدیریت پیشرفتهی حالت در React با Redux و Mobx - قسمت دهم - MobX Hooks و اعمال Async در Mobx
const storeContext = React.createContext(null); export function StoreProvider({ children }) { const userStore = useLocalStore({ id: 1, name: 'User', }); const orderStore = useLocalStore({ id: 0, productId: 1, }); return <storeContext.Provider value={{ userStore, orderStore }}>{children}</storeContext.Provider> } export function useStores() { const stores = React.useContext(storeContext); if (!stores) { throw new Error('useStores must be used within a StoreProvider.'); } return stores; } const { userStore, orderStore } = useStores();