مطالب
شروع کار با webpack - قسمت چهارم
در سه قسمت قبلی با مفاهیم گوناگونی از وبپک آشنا شدیم که می‌توان به ساخت باندل‌های ساده و پیشرفته، استفاده از لودرها، فایل‌های پیکربندی  و ... اشاره کرد.

بارگذاری فایل‌های css به کمک وبپک

همان طور که قبلا اشاره شد، هسته‌ی وبپک به خودی خود بجز باندل کردن اسکریپت‌های خام جاوا اسکریپت و در نهایت Minify کردن آنها، قادر به انجام کار دیگری نیست. همین طور ذکر شد، برای اینکه وبپک فوت و فن جدیدی را یاد بگیرد، از Loader‌ها استفاده می‌کنیم. وارد کردن فایل‌های css به باندل نیز به کمک Loader‌ها خواهد بود. برای اینکار به دو Loader احتیاج داریم و به کمک npm آنها را به عنوان وابستگی برای توسعه‌ی پروژه نصب خواهیم کرد .
// نصب لودر‌های مورد نظر
npm install css-loader style-loader -D
لودر css وظیفه‌ی بارگذاری فایل‌های css و همچنین وابستگی آنها ( import هایی که در این فایل‌ها انجام شده) را دارد و در نهایت css نهایی را بر می‌گرداند.
لودر style نیز وظیفه‌ی اضافه کردن ماژول اکسپورت شده را به DOM، به عنوان یک style خواهد داشت.
در ساختار پروژه نیز تغییراتی انجام شده و فایل‌ها و لودر‌های اضافی حذف شده‌اند تا تمرکز بر روی مطلب بیشتر باشد. مانند قسمت اول، یک تک صفحه‌ی index.html را خواهیم داشت که دارای محتوای زیر است:
//index.html file
<html>
  <head>
    <title>webpack part 4</title>

  </head>
  <body>
    <h1>webpack is awesome</h1>
    <p>part 4 of tutorial</p>
    <div>i have a background</div>
    <h1>تست فونت !</h1>

    <script src="/assets/js/bundle.js">
    </script>
  </body>
</html>
اگر دقت کنید، در این فایل لینکی به فایل‌های استایل، مانند css وجود ندارد و هدف این است که این فایل‌ها توسط وبپک پردازش شوند.
برای شروع به فایل پیکربندی وبپک مراجعه کرده و دو Loader ی را که برای این کار، بالاتر در پروژه نصب کردیم، به وبپک معرفی می‌کنیم.
//webpack.config.js
var path = require("path");
var webpack = require("webpack");

module.exports = {
    context: path.resolve("js"),
    entry: ['./main.js']
    , output: {
        path: path.resolve("build/js"),
        publicPath: "assets/js",
        filename: 'bundle.js'
    },
    devServer: {
        contentBase: "assets"
    }
    , watch: true
    , module: {
        loaders: [
            {
                test: /\.css$/
                , exclude: /node_modules/
                , loader: 'style-loader!css-loader'
            }
        ]
    }
}
در قطعه کد بالا محتوای فایل پیکربندی قابل مشاهده است. مشخص است که لودر جدیدی را به وبپک معرفی کرده‌ایم که به دنبال فایل‌هایی با پسوند css در پروژه می‌گردد. ولی در کلید loader که وظیفه‌ی فراخوانی لودر مورد نظر را در مواجهه با این فایل‌ها دارد، کمی با قبل تفاوت وجود دارد.
loader:'style-loader!css-loader'
در قطعه کد بالا، نشانگر ! در مواقعی استفاده می‌شود که قصد داریم لودر‌ها را به صورت زنجیره‌ای استفاده کنیم. مثلن در اینجا به وبپک می‌گوییم که برای فایل‌های css ابتدا لودر css را فراخوانی کن و سپس با خروجی که از این لودر می‌گیری، لودر style را صدا بزن و منتظر جواب آن باش. ترتیب فراخوانی نیز از آخر به اول می‌باشد (اول لودر css سپس لودر style).
در مطالب قبلی ذکر شد که دو حالت برای معرفی فایل‌ها به وبپک وجود دارد؛ یکی معرفی آنها در کلید entry فایل پیکربندی و دیگری استفاده از تابع require در اسکریپت‌ها، برای بارگذاری پویای ماژول‌های دیگر. استفاده از استایل‌های css نیز به همین صورت است. برای بارگذاری فایل‌های استایل، از روش پویا استفاده کرده و در فایل main.js استایل مورد نظر را با کمک require وارد می‌کنیم.
محتوای فایل main.js بدین صورت است:
// main.js file
require("./../assets/main.css");
console.log(`i'm bundled by webpack`);
محتوای فایل main.css نیز بدین شکل می‌باشد:
// main.css
body{
    background-color: #DAA520;
}
با راه اندازی وبپک و باز کردن صفحه‌ی index می‌توان دید که فایل استایل ما به همراه باندل وارد شده است:

در تصویر بالا مشخص است که در تگ Head صفحه، یک تگ جدید style، توسط وبپک ایجاد شده و استایل ما به صفحه تزریق شده‌است. همچنین اگر وبپک را به حالت Minify کردن باندل ببریم (در مطلب قبلی نحوه‌ی این کار ذکر شد)، باندل نهایی برای فایل‌های css نیز Minify خواهد شد.


استفاده از Sass با کمک وبپک 


روش استفاده از Sass نیز تفاوتی با css نخواهد داشت و فقط کافی است Loader آن را در پروژه نصب کنیم و در نهایت آن را در فایل پیکربندی، به وبپک معرفی کنیم. با دستور زیر لودر Sass را در پروژه وارد می‌کنیم:

// نصب لودر sass
npm install -D sass-loader node-sass

( node-sass به عنوان وابستگی لودر sass، در کنار آن نصب شده است)

حال به فایل پیکربندی می‌رویم و لودر جدید را به قسمت لودر‌ها اضافه می‌کنیم:

// webpack.config.js 
module: {
        loaders: [
            {
                test: /\.css$/
                , exclude: /node_modules/
                , loader: 'style-loader!css-loader'
            }
            ,{
                test:/\.scss$/
                ,exclude:/node_modules/
                ,loader:'style-loader!css-loader!sass-loader'
            }
        ]
    }

در پوشه‌ی assets نیز فایل جدیدی را با عنوان main.scss ساخته و محتوای زیر را در آن وارد می‌کنیم:

// main.scss
$background-color:#DAA520;

body{
    background-color: $background-color;
}

سپس در فایل main.js به جای وارد کردن فایل css قبلی، فایل scss جدید را با کمک require وارد می‌کنیم و در ادامه وبپک را اجرا می‌کنیم. خواهیم دید که مانند قبل بدون مشکلی وبپک اجرا شده، فایل scss را به css ترجمه کرده و سپس به کمک بقیه لودر‌ها، به باندل اضافه می‌کند. استفاده از بقیه‌ی فریمورک‌های css مانند Less و ... نیز با کمک لودر آنها به همین صورت قابل انجام است.


استفاده از Autoprefixer 

همان طور که تمامی قابلیت‌های نسخه‌ی جدید جاوااسکریپت در همه‌ی مرورگرها به صورت سراسری پشتیبانی نمی‌شود، برای css نیز چنین مشکل مشابهی وجود دارد و برای استفاده‌ی بهینه‌ی از برخی قابلیت‌ها نیاز داریم تا prefix‌های مورد نیاز مرورگرهای مختلف را به فایل‌های css مان اضافه کنیم. می‌توانیم این روند را با کمک یک لودر وبپک، ساده و به صورت خودکار کرد. برای نصب این لودر دستور زیر را وارد می‌کنیم:

npm install -D autoprefixer-loader

و بعد از نصب شدن آن، در فایل پیکربندی وبپک به لودرهایی که برای فایل‌های css و scss اضافه کرده بودیم، این لودر را نیز به صورت زنجیر وار اضافه می‌کنیم:

//webpack.config.js
module: {
        loaders: [
            {
                test: /\.css$/
                , exclude: /node_modules/
                , loader: 'style-loader!css-loader!autoprefixer-loader'
            }
            ,{
                test:/\.scss$/
                ,exclude:/node_modules/
                ,loader:'style-loader!css-loader!autoprefixer-loader!sass-loader'
            }
        ]
    }

در هر دو لودری که برای css و scss ساخته بودیم، از لودر autoprefixer استفاده کردیم. برای تست اینکه این لودر بدون مشکل کار می‌کند، در فایل main.scss تغییر زیر را ایجاد می‌کنیم:

//main.scss
$background-color:#DAA520;

body{
    background-color: $background-color;
    display: flex;
}

حال با اجرای وبپک خواهیم دید که prefix‌های مورد نیاز توسط لودر اضافه شده اند ( این لودر از کتابخانه‌ی postcss کمک می‌گیرد).




باندل کردن تصاویر و فونت‌ها با کمک وبپک


تا اینجا با نحوه‌ی وارد کردن فایل‌های استایل، مانند css و ... به باندل آشنا شدیم. در ادامه قصد داریم که تصاویر و فونت‌ها را نیز وارد باندل کنیم. روند کار شبیه به گذشته است و این کار نیز به کمک لودر‌های وبپک انجام خواهد شد.

جهت باندل کردن تصاویر و فونت‌ها، به لودر جدیدی با نام url-loader احتیاج داریم. قبل از هر چیزی این لودر را در پروژه با کمک npm نصب می‌کنیم:

npm install -D url-loader file-loader

(لودر file-loader به عنوان وابستگی مورد نیاز است)

روند همچنان مثل گذشته است و پس از نصب لودر، وارد فایل پیکربندی شده و لودر جدید را به وبپک معرفی می‌کنیم:

//webpack.config.js file
module: {
        loaders: [
            {
                test: /\.css$/
                , exclude: /node_modules/
                , loader: 'style-loader!css-loader!autoprefixer-loader'
            }
            ,{
                test:/\.scss$/
                ,exclude:/node_modules/
                ,loader:'style-loader!css-loader!autoprefixer-loader!sass-loader'
            },{
                test:/\.(png|jpg|ttf|eot)$/
                ,exclude:/node_modules/
                ,loader:'url-loader?limit=100000'
            }
        ]
    }

در لودر اضافه شده، پسوند فایلهایی را که قصد داریم به باندل وارد شوند، معرفی می‌کنیم. در اینجا فرمت‌های png , jpg ,ttf, eot ذکر شده‌اند.

تنها نکته‌ی جدید، در مشخص کردن نام لودر وجود دارد و آن نیز قسمت پس از علامت ؟ می‌باشد. هنگام مشخص کردن اینکه از چه لودری قصد استفاده داریم، می‌توانیم با استفاده از ؟ پارامترهایی را به لودر مورد نظر ارسال کنیم. در اینجا به پارامتر limit، مقدار 100000 را داده‌ایم که برای این لودر به این معناست که اگر حجم فایل در حال پردازش، حجمی بیشتر از این مقدار را داشت، این فایل را به صورت یک لینک جدا از باندل قرار بده. ولی اگر حجمی کمتر از این مقدار داشت، لودر به صورت خودکار فایل را به فرمت Base64 انکود می‌کند و در درون باندل قرار می‌دهد.


برای تست اینکه آیا این لودر به درستی کار می‌کند یا نه، یک تصویر نمونه را در فولدر assets قرار می‌دهیم و سپس در فایل main.scss تغییرات زیر را انجام می‌دهیم.

حجم عکس قرار داده شده نزدیک به 400 کیلوبایت است و با مقدار محدودیت مشخص شده، تصویر مورد نظر از باندل توسط وبپک خارج می‌شود و به صورت جداگانه در بیلد نهایی قرار می‌گیرد. در تصویر زیر مشخص است که مرورگر درخواست جداگانه ای برای تصویر ارسال کرده است:



حال محدودیت حجم فایل را بالا می‌بریم و می‌توان دید که تصویر در باندل نهایی به صورت انکود شده قرار گرفته است .


قطعا انجام این کار برای تصاویری با حجم بالا مناسب نخواهد بود و برنامه نویس بسته به نیاز بایستی مقدار محدودیت حجم را برای لودر مشخص کند.


در تعریف بالا دیدیم که فرمت‌های مورد نیاز برای وارد کردن فونت را نیز علاوه بر تصاویر، برای وبپک مشخص کرده‌ایم. روند وارد کردن فونت‌ها به باندل نیز تفاوتی با تصاویر ندارد و کافی است تعاریف مورد نیاز را در فایل‌های css داشته باشیم.

برای مثال فونت ساحل در پوشه‌ی assets قرار داده شده و در فایل main.scss تغییرات زیر انجام شده‌اند:

// main.scss

$background-color:#DAA520;
 
div{
    background-image: url("galaxy.jpg");
}

@font-face {
  font-family: Sahel;
  src: url('Sahel.eot');
  src: url('Sahel.eot?#iefix') format('embedded-opentype'),
       url('Sahel.woff') format('woff'),
       url('Sahel.ttf') format('truetype');
  font-weight: normal;
}

@font-face {
  font-family: Sahel;
  src: url('Sahel-Bold.eot');
  src: url('Sahel-Bold.eot?#iefix') format('embedded-opentype'),
       url('Sahel-Bold.woff') format('woff'),
       url('Sahel-Bold.ttf') format('truetype');
  font-weight: bold;
}

@font-face {
  font-family: Sahel;
  src: url('Sahel-Black.eot');
  src: url('Sahel-Black.eot?#iefix') format('embedded-opentype'),
       url('Sahel-Black.woff') format('woff');
    
  font-weight: 900;
} 

body{
    background-color: $background-color;
    font-family: 'Sahel';
    display: flex;
}

تصویر زیر، نتیجه‌ی اجرای وبپک برای تولید باندل است. در تصویر می‌توان دید که هم فونت‌ها و هم فایل‌های تصاویر، توسط وبپک شناسایی شده و وارد باندل شده‌اند:



روش دیگری برای وارد کردن تصاویر نیز موجود است؛ به این صورت که به فرض مثال یک تگ img در اسکریپت ساخته و سپس پروپرتی src آن را با کمک require برابر با آدرس تصویر مورد نظر قرار می‌دهیم. این روش نیز برای وبپک قابل فهم بوده و فایل وارد باندل می‌شود. در ادامه مثالی از این روش آورده شده است:

var img = document.createElement("img");
img.width="200px";
img.height="200px";
img.src= require("path to some image");


چند نکته‌ی پایانی :

1. در فایل پیکربندی همیشه پسوند فایل‌هایی را که در کلید entry قرار داشتند، مشخص کردیم:

entry:['./main.js','./shared.ts']

با کلیدی با نام resolve در فایل پیکربندی می‌توان مشخص کرد در صورتیکه پسوند فایلی مشخص نبود، به ترتیب مشخص شده به دنبال آن بگردد. به طور مثال:

// webpack.config.js
resolve:{
            extensions:['','.js','.ts']
  }

در تعریف بالا ذکر می‌شود در صورتیکه پسوند فایل ورودی مشخص نبود، ابتدا به دنبال فایل بدون پسوند، سپس فایل‌هایی با پسوند js و در نهایت به دنبال فایل‌هایی با پسوند ts بگرد. توجه داشته باشید که ترتیب مشخص کردن پسوند فایل‌ها مهم است و وبپک بر اساس این ترتیب به دنبال فایل مورد نظر خواهد گشت.

حال می‌توان مقدار کلید entry را اینطور تعریف کرد:

entry:['./main','./shared']


2.استفاده از فایل‌های css ی که در درونشان فونت‌های مورد نیاز لینک شده‌اند تنها با استفاده از لودر css قابل انجام نیست. به طور مثال استفاده از کتابخانه‌ی بوت استرپ تنها با این لودر ممکن نیست و بایستی لودر url-loader نیز در پروژه نصب شده باشد تا در هنگامیکه وبپک به فونت‌ها برخورد کرد، بتواند آنها را وابسته به شرایط، وارد باندل نهایی کند.


فایل‌های پروژه: dntwebpack-part4.zip 

مطالب
تغییرات بوجود آمده در Mobile Features-MVC4
یکی دیگه از امکاناتی که به MVC4 اضافه شده و برام جالب بود پشتیبانی توکار از مرورگرهای موبایل و تبلت‌ها است به این صورت که اگر به عنوان مثال یک فایل  Layout.cshtml  داشته باشیم و یک فایل   Layout.Mobile.cshtml  بسازیم MVC به صورت خودکار در زمانی که کاربر به وسیله موبایل یا تبلت به سایت ما وارد میشود تشخیص داده و  Layout  مربوط به موبایل را که  Layout.Mobile.cshtml   اعمال میکند.
در این رابطه کتابخانه  JQuery  افزونه بسیار قوی را ارائه داده که به راحتی میتوان از آن در برنامه خود استفاده کرد.
این افزونه فقط شامل چند فایل عکس, جاوا اسکریپ یا CSS نیست بلکه با پشتیانی کامل از صفحات لمسی ,تبلت‌ها , Smart Phone‌ها ویژگی قدرتمندی را به برنامه نویس میدهد.
در ادامه قصد دارم شما را با یک صفحه ساده ساخته شده توسط این کتابخانه قذرتمند آشنا کنم.
ابتدا یک پروژه خالی MVC4 ایجاد کنید.(هدف ما بیشتر برای آشنایی با کتابخانه JQuery Mobile است پس میتوان از یک صفحه Html ساده نیز استفاده نمود).
سپس در  کنسول Nuget برای نصب JQuery Mobile عبارت زیر را تایپ کنید.

PM> Install-Package jquery.mobile

حال پس از نصب آن شاهد اضافه شدن  فایلهای  عکس, جاوا اسکریپ و CSS هستید.
نکته ای که باید توجه کرد این است که اگر از MVC4  استفاده میکنید این فایلها چون در پوشه Content ودر Root این پوشه ایجاد میشود امکان دارد ظاهر اصلی سایت را بهم بزند و شاید هم بعضی از فایلهای جاوا اسکریپت شما اجرا نشود و این به علت ویژگی Bundling است که کل فایل هایی که در Root فولدر Content , Script قرار دارد را Bundle  میکند وامکان تداخل در فایلهای CSS و جاوااسکریپت وجود دراد.که میتوان فایلهای مربوط به JQuery Mobile  را در فولدر‌های جداگانه نگهداری کرد.(بازهم میگم ممکن است)
نکته دیگر این است که شما زمانی که به وسیله تبلت یا مویایل خود سایت را مشاهده میکنید ممکن است سایت را خیلی ریز ببینبد که با اضافه کردن یک متا تگ به شکل زیر قابل حل است.
 <meta name="viewport" content="width=device-width">
حال یک صفحه HTML خالی را باز کرده و کدهای زیر را وارد کنید:
<head>
    <meta name="viewport" content="width=device-width,initial-sclae=1" />
    <link href="Content/jquery.mobile-1.1.0.css" rel="stylesheet" type="text/css" />
    <script src="Scripts/jquery-1.6.4.js" type="text/javascript"></script>
    <script src="Scripts/jquery.mobile-1.1.0.js" type="text/javascript"></script>

    <title></title>
</head>
<body>
    <div data-role="page">
        <div data-role="header" data-theme="b">
            <h1>this is a test
            </h1>
        </div>
        <div data-role="conent">
            <ul data-role="listview" data-filter="true" data-inset="true" data-theme="e">
                <li><a href="#">Water</a></li>
                <li><a href="#">Pepsi</a></li>
                <li><a href="#">Diet Pepsi</a></li>
                <li><a href="#">Beer</a></li>
                <a href="#" data-role="button" data-theme="b">Click ME</a>
            </ul>
        </div>
        <div data-role="footer" data-theme="b" data-position="fixed">
            <h1>footer
            </h1>
        </div>
    </div>
</body> 
توجه داشته باشید که ترتیب اضافه کردن script  ها به صفحه مهم است.
توضیح کد بالا:
 data-role="page"
مشخص کننده محدوده صفحه است. 
" data-role="header
مشخص کننده هدر صفحه است.
"data-theme="e
مشخص کننده تم  صفحه است.برای اطلاعات بیشتر در باره این تنظیم به این سایت JQueryMobile مراجع نمایید.
"data-role="listview
همانطور که از اسمش پیداست برای مشخص کردن listview است.وباقی کد نیز مشخص است.
 "data-filter="true
توسط ویژگی بالا یک فیلترینگ زیبا بر روی آیتم های listview خواهیم داشت.
" data-inset="true"
واگر مقدار true باشد لبه‌های  listview  به صورت گرد در خواهند آمد.

MVC Mobile app

در قسمت‌های بعدی توضیحات کاملتری ارائه خواهم داد.
در ضمن اگر قلت املاعی دارم به بزرگی خودتون ببخشید.;)

مطالب
مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت سوم - روش اتصال Redux به برنامه‌های React
پس از بررسی ساختار کتابخانه‌ی 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
به علاوه در ادامه توئیتر بوت استرپ 4 را نیز نصب می‌کنیم:
> npm install --save bootstrap
سپس برای افزودن فایل bootstrap.css به پروژه‌ی React خود، ابتدای فایل index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام می‌دهد، مورد استفاده قرار می‌گیرد.


معرفی ساختار ابتدایی برنامه

برنامه‌ای را که در این قسمت بررسی می‌کنیم، ساختار بسیار ساده‌ای را داشته و به همراه دو دکمه‌ی افزایش و کاهش مقدار یک شمارشگر است؛ به همراه دکمه‌ی برای به حالت اول در آوردن آن. هدف اصلی دنبال شده‌ی در اینجا نیز نحوه‌ی برپایی 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;
سپس المان آن‌را جهت نمایش در برنامه، به فایل src\App.js اضافه می‌کنیم:
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: {}
}
به همین جهت برای نظم بخشیدن به تعریف این نوع اشیاء و یک‌دست سازی آن‌ها، فایل جدید src\actions\index.js را ایجاد کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
import * as types from "../constants/ActionTypes";

export const incrementValue = () => ({ type: types.Increment });
همانطور که ملاحظه می‌کنید در این متد، فعلا فقط نام رشته‌ای نوع این اکشن، بیشتر مدنظر است تا بر اساس action.type رسیده در reducer متناظر با آن، عملی رخ دهد. بنابراین فقط قسمت type آن‌را مقدار دهی کرده‌ایم. مقدار ثابت رشته‌ای types.Increment نیز از فایل مجزای src\constants\ActionTypes.js که پیشتر تعریف کردیم، تامین شده‌است.


ایجاد تابع 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;
}
- اگر دقت کرده باشید، کامپوننت شمارشگر این قسمت، دارای state نیست و همچنین نمی‌خواهیم هم که دارای state باشد؛ چون قرار است توسط redux مدیریت شود. به همین جهت state اولیه را به صورت initialState که محتوای یک شیء با خاصیت count با مقدار اولیه‌ی صفر است، خارج از کلاس کامپوننت، ایجاد کرده‌ایم.
- سپس می‌خواهیم رویداد کلیک بر روی دکمه + را مدیریت کنیم. به همین جهت نیاز به یک اکشن جدید به نام 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;
کار این فایل، مدیریت مرکزی تمام reducerهای سفارشی تعریف شده‌ی در برنامه‌است. لیست آن‌ها را به متد combineReducers ارسال کرده و در نهایت یک 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__()
);

//...
نکته 1: چون شیء rootReducer در فایل src\reducers\index.js واقع شده‌است، دیگر در حین import، نیازی به ذکر نام فایل index آن نیست.
نکته 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")
);
کامپوننت Provider، از طریق props خود نیاز به دریافت store تعریف شده را دارد. به این ترتیب هر کامپوننتی که در درخت کامپوننت‌های App قرار می‌گیرد، می‌تواند با redux store کار کند.


تامین 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);
ابتدا متد connect را از react-redux دریافت می‌کنیم. connect خود متدی است که منتظر یک کامپوننت React است؛ مانند Counter. همچنین به عنوان پارامتر، توابعی را دریافت می‌کند که اطلاعات redux store را به کامپوننت، نگاشت می‌کنند؛ مانند props و actions. در اینجا دو تابع نگاشت state به props و همچنین dispatch به props را ملاحظه می‌کنید (توابع mapStateToProps و mapDispatchToProps)؛ هرچند الزامی نیست، ولی بهتر است از همین روش نامگذاری استفاده شود.

زمانیکه در مورد store در redux صحبت می‌شود، داخل آن یک شیء بزرگ state قرار گرفته‌است که حاوی کل state برنامه‌است. اما شاید هر کامپوننت به تمام آن نیازی نداشته باشد. برای مثال شاید کامپوننت شمارشگر، اهمیتی به اطلاعات خطاهای سیستم و یا کاربر وارد شده‌ی به سیستم که در شیء کلی state موجود در store وجود دارند، ندهد. به همین جهت متد mapStateToProps، کل state برنامه را دریافت کرده و به ما اجازه می‌دهد تا تنها اطلاعاتی را که از آن نیاز داریم، به صورت props دریافت کنیم. به این ترتیب از رندر مجدد این کامپوننت نیز جلوگیری خواهد شد؛ چون این کامپوننت دیگر وابسته‌ی به تغییرات سایر اجزای کل state برنامه، نخواهد بود و اگر آن‌ها تغییر کردند، این کامپوننت رندر مجدد نخواهد شد.
بنابراین می‌توان متد mapStateToProps را به صورت کلی زیر نیز تعریف کرد:
const mapStateToProps = (state) => { return state };
هرچند این روش در مثال ما بدون مشکل کار می‌کند، اما چون کل state را دریافت می‌کند، مشکل رندر مجدد کامپوننت را به ازای هر تغییری در 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);
این شیء حاصل، به خودی خود، سبب بروز تغییری در کامپوننت شمارشگر نمی‌شود. بلکه یک کامپوننت دربرگیرنده‌ی کامپوننت Counter را ایجاد می‌کند (به همین جهت آن‌را در پوشه‌ی containers یا دربرگیرنده‌ها قرار دادیم). بنابراین برای استفاده‌ی از آن، به کامپوننت src\App.js مراجعه کرده و جائیکه المان کامپوننت Counter قبلی درج شده، آن‌را به صورت زیر تغییر می‌دهیم:
import "./App.css";

import React from "react";

import CounterContainer from "./containers/Counter";

function App() {
  return (
    <main className="container">
      <CounterContainer />
    </main>
  );
}

export default App;
ابتدا کامپوننت جدید CounterContainer را که تبادل اطلاعات بین کامپوننت Counter اصلی و state و action مخزن redux را برقرار می‌کند، import کرده و سپس المان جدید آن‌را جایگزین المان کامپوننت شمارشگر اصلی می‌کنیم.

اکنون کامپوننت شمارشگر 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.pros این کامپوننت که اکنون توسط دربرگیرنده‌ی آن ارائه می‌شود، به صورت زیر است:


به همین جهت، خاصیت تو در توی this.props.counterReducer.count و همچنین اشاره‌گر به متد increment، توسط Object Destructuring به صورت زیر از this.props دریافتی، تجزیه شده‌اند:
    const {
      counterReducer: { count },
      increment
    } = this.props;
سپس مقدار count، توسط span نمایش داده و همچنین دکمه +  را به صورت onClick={increment} تکمیل کرده‌ایم تا با کلیک بر روی آن، متد increment که در حقیقت معادل فراخوانی store.dispatch(incrementValue()) است، اجرا شود. حاصل آن، افزایش مقدار شمارشگر است:


جزئیات کار با Redux store را نیز می‌توان در افزونه‌ی redux dev tools مشاهده کرد:


این افزونه در نوار ابزار پایین آن، امکان export کل state و سپس import و بازیابی آن‌را نیز به همراه دارد.


دریافت props از طریق کامپوننت دربرگیرنده و ارسال آن به کامپوننت اصلی

فرض کنید نیاز باشد تا اطلاعاتی را به صورت متداول React از طریق props، به کامپوننت دربرگیرنده‌ی کامپوننت شمارشگر ارسال کرد:
function App() {
  const prop1 = 123
  return (
    <main className="container">
      <CounterContainer prop1={prop1} />
    </main>
  );
}
برای دسترسی به آن، پارامتر دومی به متد mapStateToProps به نام ownProps اضافه می‌شود که حاوی props ارسالی به کامپوننت container است:
const mapStateToProps = (state, ownProps) => {
  console.log("mapStateToProps", { state, ownProps });
  return state;
};
در این حالت اگر نیاز به انتقال آن به کامپوننت اصلی بود، می‌توان شیء بازگشت داده شده‌ی از mapStateToProps را به همراه یک سری خواص سفارشی دریافتی از ownProps، تعریف کرد.


پیاده سازی دکمه‌ی کاهش مقدار شمارشگر

پس از آشنایی با روش کلی برقراری اتصالات سیستم react-redux، پیاده سازی دکمه‌ی کاهش مقدار شمارشگر بسیار ساده‌است و شامل مراحل زیر است:
1)  ایجاد نام نوع اکشن متناظر با دکمه‌ی کاهش مقدار
به فایل src\constants\ActionTypes.js، نوع جدید کاهشی را اضافه می‌کنیم:
export const Decrement = "Decrement";
2) ایجاد متد Action Creator
در فایل src\actions\index.js، متد ایجاد کننده‌ی شیء اکشن ارسالی به reducer متناظری را تعریف می‌کنیم تا بتوان بر اساس نوع آن در reducer کاهشی، منطق کاهش را پیاده سازی کرد:
export const decrementValue = () => ({ type: types.Decrement });
3) ایجاد تابع reducer مخصوص کاهش مقدار
اکنون در فایل src\reducers\counter.js، بر اساس نوع شیء رسیده، تصمیم به کاهش یا افزایش مقدار موجود در state گرفته می‌شود:
export default function counterReducer(state = initialState, action) {

  // ...

  if (action.type === types.Decrement) {
    return {
      count: state.count - 1
    };
  }

  return state;
}
4) تامین state کامپوننت شمارشگر از طریق props
در ادامه نیاز است بتوان اکشن کاهش را به این reducer ارسال کرد. به همین جهت به کامپوننت دربرگیرنده‌ی کامپوننت شمارشگر در فایل src\containers\Counter.js مراجعه کرده و به شیء خروجی متد mapDispatchToProps، متد کاهش را اضافه می‌کنیم:
import { decrementValue, incrementValue } from "../actions";
// ...

const mapDispatchToProps = dispatch => {
  return {
    // ...
    decrement() {
      dispatch(decrementValue());
    }
  };
};
5) استفاده از نتایج دریافتی از props
در آخر به فایل src\components\counter.jsx مراجعه کرده و اشاره‌گر به متد decrement را از طریق this.props دریافت می‌کنیم:
const {
      // ...
      decrement
    } = this.props;
 سپس آن‌را به onClick دکمه‌ی کاهش، انتساب خواهیم داد:
<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());
    }
  };
};
می‌توان با استفاده از تابع bindActionCreators که در قسمت قبل در مورد آن بحث شد، تعریف آن‌را به صورت زیر خلاصه کرد:
import { bindActionCreators } from "redux";

// ...

const mapDispatchToProps = dispatch => {
  return bindActionCreators(
    {
      incrementValue,
      decrementValue
    },
    dispatch
  );
};
با استفاده از تابع bindActionCreators کتابخانه‌ی redux، می‌توان تمام action creators واقع در فایل src\actions\index.js را به صورت یک شیء به آن ارسال کرد و پارامتر دوم آن‌را نیز به store.dispatch یا در اینجا به همان dispatch دریافتی توسط پارامتر dispatch متد mapDispatchToProps، تنظیم کرد. البته در این حالت props دریافتی در کامپوننت شمارشگر به صورت زیر تغییر می‌کنند:


به همین جهت نیاز است در متد رندر کامپوننت src\components\counter.jsx، نام‌هایی را که به متدهای action creator اشاره می‌کنند، به صورت زیر تغییر داد:
const {
      counterReducer: { count },
      incrementValue,
      decrementValue
    } = this.props;
و همچنین نام‌های منتسب به onClickها را نیز بر این اساس، اصلاح کرد.

روش دوم: در نگارش‌های اخیر react-redux می‌توان متد mapDispatchToProps را به صورت زیر نیز خلاصه و تعریف کرد که بسیار ساده‌تر است:
const mapDispatchToProps = {
  incrementValue,
  decrementValue
};
البته در این حالت نیز مابقی آن که شامل تغییر نام‌ها می‌شود، یکسان است.

همچنین بجای بازگشت کل state در متد mapStateToProps، می‌توان تنها خواص مدنظر را بازگشت داد:
const mapStateToProps = state => {
  //return state;
  return {
    count: state.counterReducer.count
  };
};
در این حالت props ارسالی به کامپوننت یک چنین شکلی را پیدا می‌کنند:


بنابراین باید در متد رندر کامپوننت شمارشگر، خاصیت count را به صورت معمولی دریافت کرد:
const {
      //counterReducer: { count },
      count,
      incrementValue,
      decrementValue
    } = this.props;

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: state-management-redux-mobx-part03.zip
مطالب
مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت هشتم - تنظیمات پروژه‌های React برای کار با Mobx decorators
تا اینجا دو مثالی را که از Mobx بررسی کردیم (مثال ورود متن و مثال کامپوننت شمارشگر)، به عمد به همراه decoratorهای @ دار آن نبودند. برای مثال در قسمت قبل، یک کلاس را با یک خاصیت ایجاد کردیم که روش مزین سازی خاصیت value آن را با  observable decorator، توسط متد decorate انجام دادیم و این هم یک روش کار با MobX است؛ بدون اینکه نیاز به تنظیمات خاصی را داشته باشد:
import { decorate } from "mobx";

class Count {
  value = 0;
}

decorate(Count, { value: observable });
const count = new Count();
اما اگر همان مثال بسیار ساده‌ی ورود متن را بخواهیم توسط decoratorهای @ دار MobX پیاده سازی کنیم ... پروژه‌ی استاندارد React ما کامپایل نخواهد شد که در این قسمت، روش رفع این مشکل را بررسی می‌کنیم.


بازنویسی مثال ورود متن و نمایش آن با Mobx decorators

در اینجا یک text-box، به همراه دو div در صفحه رندر خواهند شد که قرار است با ورود اطلاعاتی در text-box، یکی از آن‌ها (text-display) این اطلاعات را به صورت معمولی و دیگری (text-display-uppercase) آن‌را به صورت uppercase نمایش دهد. روش کار انجام شده هم مستقل از React است و به صورت مستقیم با استفاده از DOM API عمل شده‌است. این مثال را پیشتر در اولین قسمت بررسی MobX، ملاحظه کردید. اکنون اگر بخواهیم بجای شیءای که توسط متد observable کتابخانه‌ی MobX محصور شده‌است:
const text = observable({
  value: "Hello world!",
  get uppercase() {
    return this.value.toUpperCase();
  }
});
از یک کلاس ES6 به همراه Mobx decorators استفاده کنیم، به یک چنین پروژه‌ی جدیدی خواهیم رسید:
ابتدا یک پروژه‌ی جدید React را ایجاد می‌کنیم:
> create-react-app state-management-with-mobx-part3
> cd state-management-with-mobx-part3
> npm start
در ادامه کتابخانه‌ی mobx را نیز نصب می‌کنیم. برای این منظور پس از باز کردن پوشه‌ی اصلی برنامه توسط VSCode، دکمه‌های ctrl+` را فشرده (ctrl+back-tick) و دستور زیر را در ترمینال ظاهر شده وارد کنید:
> npm install --save mobx
در ادامه، ابتدا فایل public\index.html را جهت نمایش دو div و یک text-box، ویرایش می‌کنیم:
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>MobX Basics, part 3</title>
    <meta charset="UTF-8" />
    <link href="src/styles.css" />
  </head>

  <body>
    <main>
      <input id="text-input" />
      <p id="text-display"></p>
      <p id="text-display-uppercase"></p>
    </main>

    <script src="src/index.js"></script>
  </body>
</html>
سپس محتویات فایل src\index.js را نیز به نحو زیر تغییر می‌دهیم:
import { autorun, computed, observable } from "mobx";

const input = document.getElementById("text-input");
const textDisplay = document.getElementById("text-display");
const loudDisplay = document.getElementById("text-display-uppercase");

class Text {
  @observable value = "Hello World";
  @computed get uppercase() {
    return this.value.toUpperCase();
  }
}

const text = new Text();

input.addEventListener("keyup", event => {
  text.value = event.target.value;
});

autorun(() => {
  input.value = text.value;
  textDisplay.textContent = text.value;
  loudDisplay.textContent = text.uppercase;
});
تنها تفاوت این نگارش با نگارش قبلی آن، استفاده از کلاس Text که یک کلاس ES6 به همراه MobX Decorators است، بجای یک شیء ساده‌ی جاوا اسکریپتی می‌باشد. در اینجا خاصیت value به صورت observable@ تعریف شده و در نتیجه‌ی تغییر مقدار آن در کدهای برنامه، خاصیت محاسباتی وابسته‌ی به آن یا همان uppercase که با computed@ تزئین شده، به صورت خودکار به روز رسانی خواهد شد. متد autorun نیز به این تغییرات که حاصل فشرده شدن کلیدها هستند، واکنش نشان داده و متن دو div موجود در صفحه را به روز رسانی می‌کند.

اکنون اگر در همین حال، برنامه را با دستور npm start اجرا کنیم، با خطای زیر متوقف خواهیم شد:
./src/index.js
SyntaxError: \src\index.js: Support for the experimental syntax 'decorators-legacy' isn't currently enabled (8:3):

   6 | 
   7 | class Text {
>  8 |   @observable value = "Hello World";
     |   ^
   9 |   @computed get uppercase() {
  10 |     return this.value.toUpperCase();
  11 |   }

راه حل اول: از Decorators استفاده نکنیم!

یک راه حل مشکل فوق این است که بدون هیچ تغییری در ساختار پروژه‌ی React خود، اصلا از decorator syntax استفاده نکنیم. برای مثال اگر یک کلاس متداول MobX ای چنین شکلی را دارد:
import { observable, computed, action } from "mobx";

class Timer {
  @observable start = Date.now();
  @observable current = Date.now();

  @computed
  get elapsedTime() {
    return this.current - this.start + "milliseconds";
  }

  @action
  tick() {
    this.current = Date.now();
  }
}
می‌توان آن‌را بدون استفاده از decorator syntax، به صورت زیر نیز تعریف کرد:
import { observable, computed, action, decorate } from "mobx";

class Timer {
  start = Date.now();
  current = Date.now();

  get elapsedTime() {
    return this.current - this.start + "milliseconds";
  }

  tick() {
    this.current = Date.now();
  }
}

decorate(Timer, {
  start: observable,
  current: observable,
  elapsedTime: computed,
  tick: action
});
نمونه‌ی این روش را در قسمت قبل با تعریف شیء شمارشگر مشاهده کرده‌اید. در اینجا با توجه به اینکه Decorators در جاوا اسکریپت چیزی نیستند بجز بیان زیبای higher-order functions و higher-order functions هم توابعی هستند که توابع دیگر را با ارائه‌ی قابلیت‌های بیشتری، محصور می‌کنند، به همین جهت هر کاری را که بتوان با تزئین کننده‌ها انجام داد، همان را با توابع معمولی جاوا اسکریپتی نیز می‌توان انجام داد. اینکار را در مثال فوق توسط متد decorate مشاهده می‌کنید. این متد ابتدا نوع کلاس خاصی را دریافت کرده و سپس در پارامتر دوم آن می‌توان شیءای را تعریف کرد که خواص آن، همان خواص کلاس پارامتر اول است و مقادیر این خواص، تزئین کننده‌هایی هستند که قرار است برای آن‌ها بکار گرفته شوند. مزیت این روش بدون تغییر باقی ماندن تعریف کلاس Timer در اینجا و همچنین انجام هیچگونه تغییری در ساختار پروژه‌ی React، بدون نیاز به نصب بسته‌های کمکی اضافی است.

همچنین در این حالت بجای استفاده از کامپوننت‌های کلاسی، باید از روش بکارگیری متد observer برای محصور کردن کامپوننت تابعی تعریف شده استفاده کرد (تا دیگر نیازی به ذکر observer class@ نباشد):
const Counter = observer(({ count }) => {
  return (
   // ...
  );
});


راه حل دوم: از تایپ‌اسکریپت استفاده کنید!

create-react-app امکان ایجاد پروژه‌های React تایپ‌اسکریپتی را با ذکر سوئیچ typescript نیز دارد:
> create-react-app my-proj1 --typescript
پس از ایجاد پروژه، فایل tsconfig.json آن‌را یافته و experimentalDecorators آن‌را به true تنظیم کنید:
{
  "compilerOptions": {
    // ...
    "experimentalDecorators": true
  }
}
این تنها تغییری است که مورد نیاز می‌باشد و پس از آن برنامه‌ی React جاری، بدون مشکلی می‌تواند با decorators کار کند.


فعالسازی MobX Decorators در پروژه‌های استاندارد React مبتنی بر ES6

MobX از legacy" decorators spec" پشتیبانی می‌کند. یعنی اگر پروژه‌ای از spec جدید استفاده کند، دیگر نخواهد توانست با MobX فعلی کار کند. این هم مشکل MobX نیست. مشکل اینجا است که باید دانست کلا decorators در زبان جاوااسکریپت هنوز در مرحله‌ی آزمایشی قرار دارند و تکلیف spec نهایی و تائید شده‌ی آن مشخص نیست.
برای فعالسازی decorators در یک پروژه‌ی React استاندارد مبتنی بر ES6، شاید کمی جستجو کنید و به نتایجی مانند افزودن فایل babelrc. به ریشه‌ی پروژه و نصب افزونه‌هایی مانند babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties@ برسید. اما ... این‌ها بدون اجرای دستور npm run eject کار نمی‌کنند و اگر این دستور را اجرا کنیم، در نهایت به یک فایل package.json بسیار شلوغ خواهیم رسید (اینبار ارجاعات به Babel، Webpack و تمام ابزارهای دیگر نیز ظاهر می‌شوند). همچنین این عملیات نیز یک طرفه‌است. یعنی از این پس قرار است کنترل تمام این پشت صحنه، در اختیار ما باشد و به روز رسانی‌های بعدی create-react-app را با مشکل مواجه می‌کند. این گزینه صرفا مختص توسعه دهندگان پیشرفته‌ی React است. به همین جهت نیاز به روشی را داریم تا بتوانیم تنظیمات Webpack و کامپایلر Babel را بدون اجرای دستور npm run eject، تغییر دهیم تا در نتیجه، decorators را در آن فعال کنیم و خوشبختانه پروژه‌ی react-app-rewired دقیقا برای همین منظور طراحی شده‌است.

بنابراین ابتدا بسته‌های زیر را نصب می‌کنیم:
> npm i --save-dev customize-cra react-app-rewired
بسته‌ی react-app-rewired، امکان بازنویسی تنظیمات webpack پروژه‌ی react را بدون eject آن میسر می‌کند. customize-cra نیز با استفاده از امکانات همین بسته، نگارش‌های جدیدتر create-react-app را پشتیبانی می‌کند.

پس از نصب این پیشنیازها، فایل جدید config-overrides.js را به ریشه‌ی پروژه، جائیکه فایل package.json قرار گرفته‌است، با محتوای زیر اضافه کنید تا پشتیبانی ازlegacy" decorators spec" فعال شوند:
const {
  override,
  addDecoratorsLegacy,
  disableEsLint
} = require("customize-cra");

module.exports = override(
  // enable legacy decorators babel plugin
  addDecoratorsLegacy(),

  // disable eslint in webpack
  disableEsLint()
);
در ادامه فایل package.json را گشوده و قسمت scripts آن‌را برای استفاده‌ی از react-app-rewired، به صورت زیر بازنویسی کنید تا امکان تغییر تنظیمات webpack به صورت پویا در زمان اجرای برنامه، میسر شود:
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
  },
پس از این تغییرات، نیاز است دستور npm start را یکبار دیگر از ابتدا اجرا کنید. اکنون برنامه بدون مشکل کامپایل شده و خروجی بدون خطایی در مرورگر نمایش داده خواهد شد.


تنظیمات ESLint مخصوص کار با decorators

فایل ویژه‌ی eslintrc.json. که در ریشه‌ی پروژه قرار می‌گیرد (این فایل بدون نام است و فقط از پسوند تشکیل شده)، برای پروژه‌های MobX، باید حداقل تنظیم زیر را داشته باشد تا ESLint بتواند legacyDecorators را نیز پردازش کند:
{
  "extends": "react-app",
  "parserOptions": {
    "ecmaFeatures": {
      "legacyDecorators": true
    }
  }
}
و یا یک نمونه‌ی غنی شده‌ی فایل eslintrc.json. مخصوص برنامه‌های React به صورت زیر است:
{
    "env": {
        "node": true,
        "commonjs": true,
        "browser": true,
        "es6": true,
        "mocha": true
    },
    "settings": {
        "react": {
            "version": "detect"
        }
    },
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true,
            "legacyDecorators": true
        },
        "ecmaVersion": 2018,
        "sourceType": "module"
    },
    "plugins": [
        "babel",
        "react",
        "react-hooks",
        "react-redux",
        "no-async-without-await",
        "css-modules",
        "filenames",
        "simple-import-sort"
    ],
    "rules": {
        "no-const-assign": "warn",
        "no-this-before-super": "warn",
        "constructor-super": "warn",
        "strict": [
            "error",
            "safe"
        ],
        "no-debugger": "error",
        "brace-style": [
            "error",
            "1tbs",
            {
                "allowSingleLine": true
            }
        ],
        "no-trailing-spaces": "error",
        "keyword-spacing": "error",
        "space-before-function-paren": [
            "error",
            "never"
        ],
        "spaced-comment": [
            "error",
            "always"
        ],
        "vars-on-top": "error",
        "no-undef": "error",
        "no-undefined": "warn",
        "comma-dangle": [
            "error",
            "never"
        ],
        "quotes": [
            "error",
            "double"
        ],
        "semi": [
            "error",
            "always"
        ],
        "guard-for-in": "error",
        "no-eval": "error",
        "no-with": "error",
        "valid-typeof": "error",
        "no-unused-vars": "error",
        "no-continue": "warn",
        "no-extra-semi": "warn",
        "no-unreachable": "warn",
        "no-unused-expressions": "warn",
        "max-len": [
            "warn",
            80,
            4
        ],
        "react/prefer-es6-class": "warn",
        "react/jsx-boolean-value": "warn",
        "react-hooks/rules-of-hooks": "error",
        "react-hooks/exhaustive-deps": "warn",
        "react/prop-types": "off",
        "react-redux/mapDispatchToProps-returns-object": "off",
        "react-redux/prefer-separate-component-file": "off",
        "no-async-without-await/no-async-without-await": "warn",
        "css-modules/no-undef-class": "off",
        "filenames/match-regex": [
            "off",
            "^[a-zA-Z]+\\.*\\b(typescript|module|locale|validate|test|action|api|reducer|saga)?\\b$",
            true
        ],
        "filenames/match-exported": "off",
        "filenames/no-index": "off",
        "simple-import-sort/sort": "error"
    },
    "extends": [
        "react-app",
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:react-redux/recommended",
        "plugin:css-modules/recommended"
    ],
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly",
        "process": true
    }
}
البته برای اینکه این تنظیمات کار کند، باید افزونه‌های زیر را نیز به صورت محلی در ریشه‌ی پروژه‌ی جاری نصب کنید (این مورد از ESLint 6x به بعد اجباری است و از بسته‌های global استفاده نمی‌کند):
>npm i --save-dev eslint babel-eslint eslint-config-react-app eslint-loader eslint-plugin-babel eslint-plugin-react eslint-plugin-css-modules eslint-plugin-filenames eslint-plugin-flowtype eslint-plugin-import eslint-plugin-no-async-without-await eslint-plugin-react-hooks eslint-plugin-react-redux eslint-plugin-redux-saga eslint-plugin-simple-import-sort eslint-loader typescript
پس از آن می‌توان فایل config-overrides.js را به صورت زیر نیز بر اساس تنظیمات فوق، بهبود بخشید:
const {
  override,
  addDecoratorsLegacy,
  useEslintRc
} = require("customize-cra");

module.exports = override(
  addDecoratorsLegacy(),
  useEslintRc(".eslintrc.json")
);


رفع اخطار مرتبط با decorators در VSCode

تا اینجا کار تنظیم کامپایلر babel، جهت پردازش decorators انجام شد. اما خود VSCode نیز چنین اخطاری را در پروژه‌هایی که از decorates استفاده می‌کنند، نمایش می‌دهد:
Experimental support for decorators is a feature that is subject to change in a future release.
Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning.ts(1219)
برای رفع آن، فایل جدید tsconfig.json را در ریشه‌ی پروژه ایجاد کرده و آن‌را به صورت زیر تکمیل کنید تا ادیتور تایپ‌اسکریپتی VSCode، دیگر خطاهای مرتبط با decorators را نمایش ندهد:
{
    "compilerOptions": {
        "experimentalDecorators": true,
        "allowJs": true
    }
}

کدهای کامل این قسمت را می‌توانید از اینجا دریافت کنید: state-management-with-mobx-part3.zip
نظرات مطالب
Highlight کردن لینک صفحه جاری در ASP.NET MVC
سلام مهندس (تبریک سال نو با تاخیر)
روشی که گفتین به نظر من یه ایراد کوچک داره و اونم اینه که در صورتی لینک رنگی می‌شه که حتما آدرس مورد نظر با آدرس لینک برابر باشه، در صورتی که لینک موردنظر یک صفحه فرعی باشه و ان لینک در لیست منو وجود نداشته باشه هیچ لینکی رنگی نمی‌شه، مثلا در سایت دات نت تیپس در صورتی که کاربر روی لینک جزئیات یک پروژه کلیک کنه لینک پروژه‌ها رنگی میشه در صورتی که ادرسش توی منوها موجود نیست، من وقتی کدهای سایت مذکور بررسی کردم فهمیدم به جای اون شما از تابع startsWith (تعریف شده توسط شما) استفاده کردین، در این صورت لینکهایی که با قسمتی از آدرس مذکور شروع بشه تغییرات رنگ (استایل مورد نظر) رو اون اعمال میشه.
اگر اشتباه می‌کنم لطفا تصحیح بفرمایید.
 
مطالب
OpenCVSharp #9
تغییر اندازه، و چرخش تصاویر

در OpenCV با استفاده از مفهومی به نام affine transform، امکان تغییر اندازه و همچنین چرخش تصاویر میسر می‌شود. در اینجا، تصویر در یک ماتریس دو در سه ضرب می‌شود تا انتقالات یاد شده، انجام شوند.
private static void rotateImage(double angle, double scale, Mat src, Mat dst)
{
    var imageCenter = new Point2f(src.Cols / 2f, src.Rows / 2f);
    var rotationMat = Cv2.GetRotationMatrix2D(imageCenter, angle, scale);
    Cv2.WarpAffine(src, dst, rotationMat, src.Size());
}
متد فوق کار چرخش تصویر مبدا (src) را به تصویر مقصد (dst) انجام می‌دهد. این عملیات توسط متد WarpAffine مدیریت شده و مهم‌ترین پارامتر آن، پارامتر سوم آن است که ماتریس تعریف کننده‌ی انتقالات تعریف شده توسط متد GetRotationMatrix2D است. در اینجا مرکز مشخص شده، زاویه و مقیاس، نحوه‌ی چرخش را تعریف می‌کنند.
برای مشاهده‌ی بهتر تاثیر پارامترهای مختلف در اینجا، به مثال ذیل دقت کنید:
using OpenCvSharp;
using OpenCvSharp.CPlusPlus;
 
namespace OpenCVSharpSample09
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var src = new Mat(@"..\..\Images\Penguin.Png", LoadMode.AnyDepth | LoadMode.AnyColor))
            using (var dst = new Mat())
            {
                src.CopyTo(dst);
 
                using (var window = new Window("Resize/Rotate/Blur",
                                                image: dst, flags: WindowMode.AutoSize))
                {
                    var angle = 0.0;
                    var scale = 0.7;
 
                    var angleTrackbar = window.CreateTrackbar(
                        name: "Angle", value: 0, max: 180,
                        callback: pos =>
                        {
                            angle = pos;
                            rotateImage(angle, scale, src, dst);
                            window.Image = dst;
                        });
 
                    var scaleTrackbar = window.CreateTrackbar(
                        name: "Scale", value: 1, max: 10,
                        callback: pos =>
                        {
                            scale = pos / 10f;
                            rotateImage(angle, scale, src, dst);
                            window.Image = dst;
                        }); 
 
                    angleTrackbar.Callback.DynamicInvoke(0);
                    scaleTrackbar.Callback.DynamicInvoke(1);
 
                    Cv2.WaitKey();
                }
            }
        }
 
        private static void rotateImage(double angle, double scale, Mat src, Mat dst)
        {
            var imageCenter = new Point2f(src.Cols / 2f, src.Rows / 2f);
            var rotationMat = Cv2.GetRotationMatrix2D(imageCenter, angle, scale);
            Cv2.WarpAffine(src, dst, rotationMat, src.Size());
        }
    }
}
با این خروجی:


در این مثال، مانند مطلب قسمت قبل، ابتدا یک پنجره‌ی سازگار با C++ API ایجاد شده و سپس دو tracker به آن اضافه شده‌اند. این trackers کار دریافت ورودی اطلاعات را از کاربر به عهده دارند (دریافت مقادیر زاویه‌ی چرخش و مقیاس) و مقادیر دریافتی از آن‌ها، در نهایت به متد rotateImage ارسال می‌شوند. این متد کار چرخش و تغییر مقیاس تصویر اصلی را انجام داده و نتیجه را به تصویر dst کپی می‌کند. در آخر تصویر dst در پنجره به روز شده و نمایش داده می‌شود.


تغییر اندازه‌ی تصاویر

اگر صرفا قصد تغییر اندازه‌ی تصاویر را دارید (بدون چرخش آن‌ها)، متد ویژه‌ای به نام Resize برای این منظور تدارک دیده شده‌است:
var resizeTrackbar = window.CreateTrackbar(
    name: "Resize", value: 1, max: 100,
    callback: pos =>
    {
        Cv2.Resize(src, dst,
            new Size(src.Width + pos, src.Height + pos),
            interpolation: Interpolation.Cubic);
        window.Image = dst;
    });
در اینجا یک tracker دیگر به پنجره‌ی اصلی اضافه شده و توسط آن کار تعیین تغییر اندازه‌ی تصویر انجام می‌شود. نکته‌ی مهم این متد، امکان تعیین الگوریتم تغییر اندازه است که برای مثال در اینجا از Interpolation.Cubic استفاده شده‌است (احتمالا با این نام‌ها در برنامه‌های معروف کار با تصاویر، مانند فتوشاپ آشنایی دارید).

اگر می‌خواهید مقادیر پارامترهای چرخشی تصویر نیز در اینجا اعمال شوند، می‌توان به نحو ذیل عمل کرد:
var resizeTrackbar = window.CreateTrackbar(
    name: "Resize", value: 1, max: 100,
    callback: pos =>
    {
        rotateImage(angle, scale, src, dst);
        Cv2.Resize(dst, dst,
            new Size(src.Width + pos, src.Height + pos),
            interpolation: Interpolation.Cubic);
        window.Image = dst;
    });
در این کد ابتدا تصویر اصلی چرخش یافته و سپس در متد Resize از این تصویر چرخش یافته، به عنوان src استفاده می‌شود (هر دو پارامتر متد Resize به dst تنظیم شده‌اند).



مات کردن تصاویر

در OpenCV با استفاده از متدهای GaussianBlur و یا medianBlur ، می‌توان تصاویر را  مات کرد که نمونه‌ای از آن‌را در ادامه ملاحظه می‌کنید:
var blurTrackbar = window.CreateTrackbar(
   name: "Blur", value: 1, max: 100,
   callback: pos =>
   {
       if (pos % 2 == 0) pos++;
 
       rotateImage(angle, scale, src, dst);
       Cv2.GaussianBlur(dst, dst, new Size(pos, pos), sigmaX: 0);
       window.Image = dst;
   });
در اینجا ابتدا تصویر اصلی به متد چرخش تصویر ارسال شده و نتیجه‌ی آن در متد GaussianBlur استفاده خواهد شد. اندازه‌ی مشخص شده‌ی در این متد باید توسط اعداد فرد تعیین گردد. پارامتر sigmaX به معنای standard deviation در جهت x است و اگر صفر تعیین شود، برای محاسبه‌ی آن از پارامتر اندازه‌ی تعیین شده کمک گرفته خواهد شد.



کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.