یک سرویس ویندوز ان تی با سی شارپ نوشتهام که کارش مراجعه به یک سری آدرس RSS و ذخیره سازی آنها به صورت آنالیز شده در یک دیتابیس SQL server است (این مورد ضعفی است که اکثر برنامههای فیدخوان دارند و پس از مدتی کار با آنها این احساس را دارید که اطلاعات گذشته را از دست دادهاید).
در طی آزمایش اولیه این سرویس، به مشکل عجیب timeout پس از باز کردن برای مثال سومین یا چهارمین thread همزمان برای دانلود کردن اطلاعات بر خوردم. همه چیز درست بود، از کلاسها، دریافت اطلاعات از وب و غیره، اما برنامه کار نمیکرد. این مشکل فقط هم با feedburner.com رخ میداد (همانطور که مطلع هستید feedburner.com سرویسی را جهت پیگیری آمار مشترکین فیدهای شما ارائه میدهد که بسیار جالب است. برای مثال چند نفر مشترک دارید، یا یک سری نمودار و غیره. به همین جهت رسم شده است که اکثر سایتها فیدهای خودشان را در این سایت نیز ثبت میکنند).
پس از مدتی جستجو به نکته جالب زیر برخوردم که شاید برای شما هم در آینده مفید باشد:
مطابق RFC2068 - Hypertext Transfer Protocol -- HTTP/1.1 ، شما تنها مجازید 2 کانکشن فعال به یک سایت باز کنید. این علت تایم آوت در سومین thread ایجاد شده بود. برای مثال IE این مورد را محترم میشمارد. در دات نت نیز به صورت پیش فرض این محدودیت قرار داده شده است که بهسادگی میتوان آنرا تغییر داد. برای این منظور باید یک فایل app.config به پروژه اضافه کرد و سپس خطوط زیر را به آن افزود:
بعد از این تغییر مشکل timeout برنامه حل شد.
برای مدیریت چندین ترد همزمان دانلود کننده و در صف قرار دادن آنها در این پروژه، از کتابخانه سورس باز زیر استفاده کردم:
http://www.codeplex.com/smartthreadpool
مآخذ:
http://msdn.microsoft.com/en-us/library/fb6y0fyc.aspx
http://www.faqs.org/rfcs/rfc2068.html
http://vahidnasiri.blogspot.com
http://odetocode.com/Blogs/scott/archive/2004/06/08/272.aspx
پ.ن.
برای اینکه در بلاگر بتوانید متون حاوی xml را ارسال کنید باید از سرویس زیر استفاده کنید
http://www.elliotswan.com/postable/
در طی آزمایش اولیه این سرویس، به مشکل عجیب timeout پس از باز کردن برای مثال سومین یا چهارمین thread همزمان برای دانلود کردن اطلاعات بر خوردم. همه چیز درست بود، از کلاسها، دریافت اطلاعات از وب و غیره، اما برنامه کار نمیکرد. این مشکل فقط هم با feedburner.com رخ میداد (همانطور که مطلع هستید feedburner.com سرویسی را جهت پیگیری آمار مشترکین فیدهای شما ارائه میدهد که بسیار جالب است. برای مثال چند نفر مشترک دارید، یا یک سری نمودار و غیره. به همین جهت رسم شده است که اکثر سایتها فیدهای خودشان را در این سایت نیز ثبت میکنند).
پس از مدتی جستجو به نکته جالب زیر برخوردم که شاید برای شما هم در آینده مفید باشد:
مطابق RFC2068 - Hypertext Transfer Protocol -- HTTP/1.1 ، شما تنها مجازید 2 کانکشن فعال به یک سایت باز کنید. این علت تایم آوت در سومین thread ایجاد شده بود. برای مثال IE این مورد را محترم میشمارد. در دات نت نیز به صورت پیش فرض این محدودیت قرار داده شده است که بهسادگی میتوان آنرا تغییر داد. برای این منظور باید یک فایل app.config به پروژه اضافه کرد و سپس خطوط زیر را به آن افزود:
<configuration>
<system.net>
<connectionManagement>
<add address="*" maxconnection="100" />
</connectionManagement>
</system.net>
</configuration>
بعد از این تغییر مشکل timeout برنامه حل شد.
برای مدیریت چندین ترد همزمان دانلود کننده و در صف قرار دادن آنها در این پروژه، از کتابخانه سورس باز زیر استفاده کردم:
http://www.codeplex.com/smartthreadpool
مآخذ:
http://msdn.microsoft.com/en-us/library/fb6y0fyc.aspx
http://www.faqs.org/rfcs/rfc2068.html
http://vahidnasiri.blogspot.com
http://odetocode.com/Blogs/scott/archive/2004/06/08/272.aspx
پ.ن.
برای اینکه در بلاگر بتوانید متون حاوی xml را ارسال کنید باید از سرویس زیر استفاده کنید
http://www.elliotswan.com/postable/
قبل از استفاده از بلاگر، در سایت wordpress وبلاگ داشتم، که بهدلایلی کنسل شد. تفاوت محسوسی را که اینجا مشاهده میکنم، نبود قسمت آمار سایت است. در سایت wordpress آمار مبسوطی را از بازدید کنندگان سایت میتوانید در کنترل پنل مدیریتی وبلاگ مشاهده کنید، اما در اینجا خیر.
به همین جهت اولین کاری را که انجام دادم استفاده از سرویس رایگان persianstat بود که انصافا هم با کیفیت است و قابل مقایسه با آماری که wordpress ارائه میدهد، میباشد.
جالب اینجا است که هر چند هاست اینجا، گوگل است اما استفادهی خودکار از ابزار Google analytics در آن مهیا نیست. احتمالا علت آن آماده نبودن API آن است که قرار است به زودی ارائه شود، بنابراین ارزش وقت گذاشتن را دارد.
برای استفاده از Google analytics ، پس از ثبت نام و ورود به آن، سایت مورد نظر را معرفی کرده (در قسمت Add Website Profile) و نهایتا یک کد جاوا اسکریپتی به شما خواهد داد که میتوانید آنرا به صفحات مورد نظر خود در سایت اضافه نمائید تا تحت کنترل آماری قرار گیرد. محدودیتی هم در مورد تعداد سایت وجود ندارد و با یک اکانت میتوانید چندین سایت را معرفی کرده و تحت کنترل قرار دهید.
اگر از ASP.Net استفاده میکنید، تنها کافی است به master page سایت مراجعه کنید و پیش از بسته شدن تگ body ، اسکریپت مربوط به Google analytics را اضافه کنید تا تمام سایت را تحت کنترل قرار دهید.
یا اگر علاقمند بودید که اینکار را به صورت "شیکتری" انجام دهید، میتوان از این http module استفاده کرد. به این صورت ابتدا تگ بسته شدن body به صورت خودکار پیدا شده و سپس اسکریپت به پیش از آن اضافه میشود.
این روش بار بزرگ تهیه آمار سایت را حذف خواهد کرد. عموما دیتابیس جمع آوری آمار سایت خیلی زود (برای مثال پس از گذشت 6 ماه) حجیم میشود و تاثیر مشهودی را بر روی کارآیی سایت خواهد گذاشت. بنابراین، این سؤال مطرح میشود که چرا گوگل اینکار را برای ما انجام ندهد؟! هزینه بانک اس کیوال سرور بر روی هاستهای اینترنتی بالا بوده و حجمی را هم که در اختیار قرار میدهند محدود است. در صورت نیاز به حجمهای بالاتر باید هزینه بیشتری را پرداخت کرد. بنابراین هم از لحاظ قیمت و هچنین کارآیی سایت، استفاده از این سرویس واقعا مقرون به صرفه است. بعلاوه از تنوع آماری که ارائه میدهد نیز نمیتوان چشم پوشی کرد. برای مثال کاربران چه واژههای کلیدی را در موتورهای جستجو وارد کردهاند تا به سایت شما رسیدهاند؟ چند درصد کاربر وفادار دارید؟! (کاربرهای وفادار، منظور افرادی هستند که به صورت منظم به سایت سر میزنند) و امثال این. انصافا تهیه چنین ماژولی برای یک سایت از لحاظ برنامه نویسی شاید با برنامه نویسی کل یک سایت برابری کند.
اگر هم نیاز به یک برنامه سورس باز داشتید که هر روز به اکانت Google analytics شما سر بزند و اطلاعات آنرا استخراج کرده و در یک بانک SQL server ذخیره کند، میتوانید به پروژه سی شارپ زیر مراجعه نمائید:
Google Analytics Data Extractor
البته باید دقت داشت که پس از ارائه API کامل Google analytics ، دیگر نیازی به این نوع روشهای ابتکاری وجود نداشته و استخراج داده از آن بسیار سادهتر خواهد شد.
تا اینجا دو مثالی را که از Mobx بررسی کردیم (مثال ورود متن و مثال کامپوننت شمارشگر)، به عمد به همراه decoratorهای @ دار آن نبودند. برای مثال در قسمت قبل، یک کلاس را با یک خاصیت ایجاد کردیم که روش مزین سازی خاصیت value آن را با observable decorator، توسط متد decorate انجام دادیم و این هم یک روش کار با MobX است؛ بدون اینکه نیاز به تنظیمات خاصی را داشته باشد:
اما اگر همان مثال بسیار سادهی ورود متن را بخواهیم توسط decoratorهای @ دار MobX پیاده سازی کنیم ... پروژهی استاندارد React ما کامپایل نخواهد شد که در این قسمت، روش رفع این مشکل را بررسی میکنیم.
بازنویسی مثال ورود متن و نمایش آن با Mobx decorators
در اینجا یک text-box، به همراه دو div در صفحه رندر خواهند شد که قرار است با ورود اطلاعاتی در text-box، یکی از آنها (text-display) این اطلاعات را به صورت معمولی و دیگری (text-display-uppercase) آنرا به صورت uppercase نمایش دهد. روش کار انجام شده هم مستقل از React است و به صورت مستقیم با استفاده از DOM API عمل شدهاست. این مثال را پیشتر در اولین قسمت بررسی MobX، ملاحظه کردید. اکنون اگر بخواهیم بجای شیءای که توسط متد observable کتابخانهی MobX محصور شدهاست:
از یک کلاس ES6 به همراه Mobx decorators استفاده کنیم، به یک چنین پروژهی جدیدی خواهیم رسید:
ابتدا یک پروژهی جدید React را ایجاد میکنیم:
در ادامه کتابخانهی mobx را نیز نصب میکنیم. برای این منظور پس از باز کردن پوشهی اصلی برنامه توسط VSCode، دکمههای ctrl+` را فشرده (ctrl+back-tick) و دستور زیر را در ترمینال ظاهر شده وارد کنید:
در ادامه، ابتدا فایل public\index.html را جهت نمایش دو div و یک text-box، ویرایش میکنیم:
سپس محتویات فایل src\index.js را نیز به نحو زیر تغییر میدهیم:
تنها تفاوت این نگارش با نگارش قبلی آن، استفاده از کلاس Text که یک کلاس ES6 به همراه MobX Decorators است، بجای یک شیء سادهی جاوا اسکریپتی میباشد. در اینجا خاصیت value به صورت observable@ تعریف شده و در نتیجهی تغییر مقدار آن در کدهای برنامه، خاصیت محاسباتی وابستهی به آن یا همان uppercase که با computed@ تزئین شده، به صورت خودکار به روز رسانی خواهد شد. متد autorun نیز به این تغییرات که حاصل فشرده شدن کلیدها هستند، واکنش نشان داده و متن دو div موجود در صفحه را به روز رسانی میکند.
اکنون اگر در همین حال، برنامه را با دستور npm start اجرا کنیم، با خطای زیر متوقف خواهیم شد:
راه حل اول: از Decorators استفاده نکنیم!
یک راه حل مشکل فوق این است که بدون هیچ تغییری در ساختار پروژهی React خود، اصلا از decorator syntax استفاده نکنیم. برای مثال اگر یک کلاس متداول MobX ای چنین شکلی را دارد:
میتوان آنرا بدون استفاده از decorator syntax، به صورت زیر نیز تعریف کرد:
نمونهی این روش را در قسمت قبل با تعریف شیء شمارشگر مشاهده کردهاید. در اینجا با توجه به اینکه Decorators در جاوا اسکریپت چیزی نیستند بجز بیان زیبای higher-order functions و higher-order functions هم توابعی هستند که توابع دیگر را با ارائهی قابلیتهای بیشتری، محصور میکنند، به همین جهت هر کاری را که بتوان با تزئین کنندهها انجام داد، همان را با توابع معمولی جاوا اسکریپتی نیز میتوان انجام داد. اینکار را در مثال فوق توسط متد decorate مشاهده میکنید. این متد ابتدا نوع کلاس خاصی را دریافت کرده و سپس در پارامتر دوم آن میتوان شیءای را تعریف کرد که خواص آن، همان خواص کلاس پارامتر اول است و مقادیر این خواص، تزئین کنندههایی هستند که قرار است برای آنها بکار گرفته شوند. مزیت این روش بدون تغییر باقی ماندن تعریف کلاس Timer در اینجا و همچنین انجام هیچگونه تغییری در ساختار پروژهی React، بدون نیاز به نصب بستههای کمکی اضافی است.
همچنین در این حالت بجای استفاده از کامپوننتهای کلاسی، باید از روش بکارگیری متد observer برای محصور کردن کامپوننت تابعی تعریف شده استفاده کرد (تا دیگر نیازی به ذکر observer class@ نباشد):
راه حل دوم: از تایپاسکریپت استفاده کنید!
create-react-app امکان ایجاد پروژههای React تایپاسکریپتی را با ذکر سوئیچ typescript نیز دارد:
پس از ایجاد پروژه، فایل tsconfig.json آنرا یافته و 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 دقیقا برای همین منظور طراحی شدهاست.
بنابراین ابتدا بستههای زیر را نصب میکنیم:
بستهی react-app-rewired، امکان بازنویسی تنظیمات webpack پروژهی react را بدون eject آن میسر میکند. customize-cra نیز با استفاده از امکانات همین بسته، نگارشهای جدیدتر create-react-app را پشتیبانی میکند.
پس از نصب این پیشنیازها، فایل جدید config-overrides.js را به ریشهی پروژه، جائیکه فایل package.json قرار گرفتهاست، با محتوای زیر اضافه کنید تا پشتیبانی ازlegacy" decorators spec" فعال شوند:
در ادامه فایل package.json را گشوده و قسمت scripts آنرا برای استفادهی از react-app-rewired، به صورت زیر بازنویسی کنید تا امکان تغییر تنظیمات webpack به صورت پویا در زمان اجرای برنامه، میسر شود:
پس از این تغییرات، نیاز است دستور npm start را یکبار دیگر از ابتدا اجرا کنید. اکنون برنامه بدون مشکل کامپایل شده و خروجی بدون خطایی در مرورگر نمایش داده خواهد شد.
تنظیمات ESLint مخصوص کار با decorators
فایل ویژهی eslintrc.json. که در ریشهی پروژه قرار میگیرد (این فایل بدون نام است و فقط از پسوند تشکیل شده)، برای پروژههای MobX، باید حداقل تنظیم زیر را داشته باشد تا ESLint بتواند legacyDecorators را نیز پردازش کند:
و یا یک نمونهی غنی شدهی فایل eslintrc.json. مخصوص برنامههای React به صورت زیر است:
البته برای اینکه این تنظیمات کار کند، باید افزونههای زیر را نیز به صورت محلی در ریشهی پروژهی جاری نصب کنید (این مورد از ESLint 6x به بعد اجباری است و از بستههای global استفاده نمیکند):
پس از آن میتوان فایل config-overrides.js را به صورت زیر نیز بر اساس تنظیمات فوق، بهبود بخشید:
رفع اخطار مرتبط با decorators در VSCode
تا اینجا کار تنظیم کامپایلر babel، جهت پردازش decorators انجام شد. اما خود VSCode نیز چنین اخطاری را در پروژههایی که از decorates استفاده میکنند، نمایش میدهد:
برای رفع آن، فایل جدید tsconfig.json را در ریشهی پروژه ایجاد کرده و آنرا به صورت زیر تکمیل کنید تا ادیتور تایپاسکریپتی VSCode، دیگر خطاهای مرتبط با decorators را نمایش ندهد:
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-with-mobx-part3.zip
import { decorate } from "mobx"; class Count { value = 0; } decorate(Count, { value: observable }); const count = new Count();
بازنویسی مثال ورود متن و نمایش آن با 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(); } });
ابتدا یک پروژهی جدید React را ایجاد میکنیم:
> create-react-app state-management-with-mobx-part3 > cd state-management-with-mobx-part3 > npm start
> npm install --save mobx
<!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>
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; });
اکنون اگر در همین حال، برنامه را با دستور 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(); } }
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 });
همچنین در این حالت بجای استفاده از کامپوننتهای کلاسی، باید از روش بکارگیری متد observer برای محصور کردن کامپوننت تابعی تعریف شده استفاده کرد (تا دیگر نیازی به ذکر observer class@ نباشد):
const Counter = observer(({ count }) => { return ( // ... ); });
راه حل دوم: از تایپاسکریپت استفاده کنید!
create-react-app امکان ایجاد پروژههای React تایپاسکریپتی را با ذکر سوئیچ typescript نیز دارد:
> create-react-app my-proj1 --typescript
{ "compilerOptions": { // ... "experimentalDecorators": true } }
فعالسازی 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
پس از نصب این پیشنیازها، فایل جدید 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() );
"scripts": { "start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-app-rewired eject" },
تنظیمات ESLint مخصوص کار با decorators
فایل ویژهی eslintrc.json. که در ریشهی پروژه قرار میگیرد (این فایل بدون نام است و فقط از پسوند تشکیل شده)، برای پروژههای MobX، باید حداقل تنظیم زیر را داشته باشد تا ESLint بتواند legacyDecorators را نیز پردازش کند:
{ "extends": "react-app", "parserOptions": { "ecmaFeatures": { "legacyDecorators": true } } }
{ "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 } }
>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
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)
{ "compilerOptions": { "experimentalDecorators": true, "allowJs": true } }
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-with-mobx-part3.zip
یکی از مشکلاتی را که حین کار با AngularJS 2.0 به کرات شاهدش خواهید بود، کش شدن تک اسکریپتهای ماژولهای آن است. برای مثال فایل ts ایی را تغییر میدهید؛ به فایل js معادل آن کامپایل میشود. چون برنامه ماژولار است و این ماژول پیشتر توسط مرورگر بارگذاری شدهاست، بار دیگر نسبت به دریافت مجدد آن اقدام نمیکند. همچنین با ارائهی نگارش RC، دیگر خبری از فایلهای bundle این مجموعه نیست و اینبار اگر تبادلات شبکهی بین سرور و برنامه را مرور کنید، به چند صد رفت و برگشت، برای دریافت فایلهای JS کتابخانههای مرتبط خواهید رسید که اصلا بهینه نیست. در این قسمت قصد داریم، یک Gulp Task را ایجاد کنیم تا تمام اسکریپتهای موجود را با هم یکی کرده و توزیع برنامه را سادهتر کند؛ به همراه بالا رفتن سرعت کار با این سیستم، بدون نیازی به توزیع تک تک فایلهای js نهایی، که شاید صدها فایل باشند.
نصب پیشنیازهای کار با Gulp و TypeScript
فایل package.json در قسمت اول این سری معرفی شد. دراینجا قسمت devDependencies آنرا به نحو ذیل تکمیل کنید:
به این ترتیب، پس از ذخیرهی فایل و یا کلیک راست بر روی نام فایل و انتخاب گزینهی restore packages، وابستگیهایی مانند gulp، gulp-typescript و یک سری فشرده ساز CSS و HTML دریافت خواهند شد.
نکتهی مهم آن systemjs-builder است. این کتابخانه کار کامپایل systemjs.config.js را به یک تک اسکریپت انجام میدهد. به این ترتیب مشکل صدها بار رفت و برگشت به سرور، برای دریافت وابستگیهای AngularJS 2.0، به طور کامل برطرف میشود.
افزودن فایل gulpfile.js به پروژه
یا یک فایل جدید جاوا اسکریپتی را به نام gulpfile.js به ریشهی پروژه اضافه کنید و یا از منوی project -> add new item نیز میتوانید گزینهی gulp configuration file را در VS 2015 انتخاب نمائید. محتوای این فایل را به نحو ذیل تغییر دهید:
توضیحات
در این فایل فرض شدهاست که خروجی نهایی برنامه قرار است در پوشهای به نام wwwroot کپی شود و پوشهی اصلی برنامه، همان پوشهای به نام app، در ریشهی پروژه است.
سپس در اینجا یک سری task کامپایل و کپی کردن فایلها تهیه شدهاند:
1) وظیفهی clean، کار تمیز کردن پوشهی نهایی خروجی برنامه را انجام میدهد (حذف تمام فایلهای آن).
2) وظیفهی shims، کار بسته بندی، یکی کردن و فشرده کردن سه اسکریپت es6-shim.js، zone.js و Reflect.js را انجام میدهد. سپس تک فایل حاصل را به نام shims.js، در پوشهی wwwroot/js کپی میکند.
3) وظیفهی tsc، یکبار دیگر کامپایلر TypeScript را اجرا میکند تا مطمئن شویم با آخرین نگارش فایلهای js برنامه کار میکنیم.
4) وظیفهی system-build، کار پردازش خودکار مداخل فایل systemjs.config.js را انجام میدهد. در آخرین نگارش ارائه شدهی AngularJS 2.0، بجای ذکر مداخل مورد نیاز آن، این تک فایل systemjs.config.js را به صفحه پیوست میکنیم تا اسکریپتهای لازم را (چند صد عدد)، به صورت خودکار بارگذاری کند. برای یکی کردن این چند صد عدد اسکریپت، از کتابخانهی SystemBuilder آن کمک گرفته و کار کامپایل نهایی را انجام میدهیم. خروجی تمام این فایلها، به همراه کلیه فایلهای js حاصل از کامپایل فایلهای TypeScript برنامه، در فایلی به نام bundle.js کپی شدهی در پوشهی wwwroot/js نوشته میشود. بنابراین دیگر نیازی نیست تا فایلهای js پوشهی app و همچنین فایلهای js وابستگیهای AngularJS 2.0 را توزیع کنیم. تک فایل bundle.js، حاوی تمام اینها است.
5) وظیفهی buildAndMinify کار اجرای وظیفهی system-bulder را به همراه فشرده سازی تک فایل bundle.js، به عهده دارد. به علاوه اگر در پوشهی css آن نیز فایل styles.css موجود باشد، آن را فشرده میکند.
6) در ادامه یک سری وظیفهی کپی کردن منابع برنامه را مشاهده میکنید. مانند favicon که کار کپی کردن این آیکن را به پوشهی wwwroot انجام میدهد. وظیفهی css، فایلهای css موجود در پوشههای برنامه را به wwwroot و زیر پوشههای آن کپی میکند. وظیفهی templates، کار کپی کردن فایلهای html قالبهای کامپوننتها را بر عهده دارد. وظیفهی assets، کار کپی کردن فایلهای png را انجام میدهد.
7) وظیفهی otherScriptsAndStyles یک سری css و js ثالث را به پوشهی wwwroot کپی میکند؛ مانند فایلهای بوت استرپ و جیکوئری.
8) وظیفهی default، کار اجرای تمام این وظایف را با هم به عهده دارد.
اکنون اگر بر روی gulpfile.js کلیک راست کنید، گزینهی task runner explorer ظاهر خواهد شد. آنرا انتخاب کنید:
بر روی وظیفهی default کلیک راست کرده و آنرا اجرا کنید. پس از مدتی پوشهی جدید wwwroot ساخته شده و فایلهای نهایی برنامه به آن کپی میشوند.
اصلاح فایل index.html و یا Views\Shared\_Layout.cshtml
اکنون که تمام فایلهای مورد نیاز پروژه در پوشهی wwwroot کپی شدهاند، نیاز است فایل index.html را به نحو ذیل تغییر داد:
همانطور که مشاهده میکنید، اینبار دیگر خبری از systemjs.config.js و وابستگیهای آن نیست.
اسکریپتهای shims که برای مرورگرهای قدیمیتر درنظر گرفته شدهاند، به تک فایل wwwroot/js/shims.js منتقل شدهاند.
تمام اسکریپتهای AngularJS 2.0 و وابستگیهای آن به همراه تمام اسکریپتهای برنامهی خودمان، به تک فایل wwwroot/js/bundle.js منتقل شدهاند.
اکنون اگر برنامه را اجرا کنید، سرعت آن با قبل قابل مقایسه نیست! اینبار دیگر نه نیازی به بارگذاری تمام وابستگیهای AngularJS 2.0 به صورت مجزا توسط systemjs.config.js وجود دارد و نه به ازای مشاهدهی هر صفحهای، یکبار قرار است فایل js کامپوننت آن بارگذاری شود. تمام اینها داخل فایل wwwroot/js/bundle.js قرار گرفتهاند و تنها یکبار بارگذاری میشوند.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part12.zip
خلاصهی بحث
با نوشتن یک Gulp Task جدید میتوان بر اساس فایل systemjs.config.js، تمام اسکریپتهای دخیل در اجرای برنامه را به صورت خودکار یافته و به صورت یک تک فایل نهایی، بسته بندی و توزیع کرد.
نصب پیشنیازهای کار با Gulp و TypeScript
فایل package.json در قسمت اول این سری معرفی شد. دراینجا قسمت devDependencies آنرا به نحو ذیل تکمیل کنید:
"devDependencies": { "typescript": "^1.8.10", "gulp": "^3.9.1", "path": "^0.12.7", "gulp-clean": "^0.3.2", "fs": "^0.0.2", "gulp-concat": "^2.6.0", "gulp-typescript": "^2.13.1", "gulp-tsc": "^1.1.5", "del": "^2.2.0", "gulp-autoprefixer": "^3.1.0", "gulp-cssnano": "^2.0.0", "gulp-html-replace": "^1.5.4", "gulp-htmlmin": "^1.0.5", "gulp-uglify": "^1.5.3", "merge-stream": "^1.0.0", "systemjs-builder": "^0.15.16", "typings": "^0.8.1" },
نکتهی مهم آن systemjs-builder است. این کتابخانه کار کامپایل systemjs.config.js را به یک تک اسکریپت انجام میدهد. به این ترتیب مشکل صدها بار رفت و برگشت به سرور، برای دریافت وابستگیهای AngularJS 2.0، به طور کامل برطرف میشود.
افزودن فایل gulpfile.js به پروژه
یا یک فایل جدید جاوا اسکریپتی را به نام gulpfile.js به ریشهی پروژه اضافه کنید و یا از منوی project -> add new item نیز میتوانید گزینهی gulp configuration file را در VS 2015 انتخاب نمائید. محتوای این فایل را به نحو ذیل تغییر دهید:
var gulp = require("gulp"), concat = require("gulp-concat"), tsc = require("gulp-typescript"), jsMinify = require("gulp-uglify"), cssPrefixer = require("gulp-autoprefixer"), cssMinify = require("gulp-cssnano"), del = require("del"), merge = require("merge-stream"), minifyHTML = require('gulp-htmlmin'), SystemBuilder = require("systemjs-builder"); var appFolder = "./app"; var outFolder = "wwwroot"; gulp.task("clean", () => { return del(outFolder); }); gulp.task("shims", () => { return gulp.src([ "node_modules/es6-shim/es6-shim.js", "node_modules/zone.js/dist/zone.js", "node_modules/reflect-metadata/Reflect.js" ]) .pipe(concat("shims.js")) .pipe(jsMinify()) .pipe(gulp.dest(outFolder + "/js/")); }); gulp.task("tsc", () => { var tsProject = tsc.createProject("./tsconfig.json"); var tsResult = gulp.src([ appFolder + "/**/*.ts" ]) .pipe(tsc(tsProject), undefined, tsc.reporter.fullReporter()); return tsResult.js.pipe(gulp.dest("build/")); }); gulp.task("system-build", ["tsc"], () => { var builder = new SystemBuilder(); return builder.loadConfig("systemjs.config.js") .then(() => builder.buildStatic(appFolder, outFolder + "/js/bundle.js")) .then(() => del("build")); }); gulp.task("buildAndMinify", ["system-build"], () => { var bundle = gulp.src(outFolder + "/js/bundle.js") .pipe(jsMinify()) .pipe(gulp.dest(outFolder + "/js/")); var css = gulp.src(outFolder + "/css/styles.css") .pipe(cssMinify()) .pipe(gulp.dest(outFolder + "/css/")); return merge(bundle, css); }); gulp.task("favicon", function () { return gulp.src("./app/favicon.ico") .pipe(gulp.dest(outFolder)); }); gulp.task("css", function () { return gulp.src(appFolder + "/**/*.css") .pipe(cssPrefixer()) .pipe(cssMinify()) .pipe(gulp.dest(outFolder)); }); gulp.task("templates", function () { return gulp.src(appFolder + "/**/*.html") .pipe(minifyHTML()) .pipe(gulp.dest(outFolder)); }); gulp.task("assets", ["templates", "css", "favicon"], function () { return gulp.src(appFolder + "/**/*.png") .pipe(gulp.dest(outFolder)); }); gulp.task("otherScriptsAndStyles", () => { gulp.src([ "jquery/dist/jquery.*js", "bootstrap/dist/js/bootstrap*.js" ], { cwd: "node_modules/**" }) .pipe(gulp.dest(outFolder + "/js/")); gulp.src([ "node_modules/bootstrap/dist/css/bootstrap.css" ]).pipe(cssMinify()).pipe(gulp.dest(outFolder + "/css/")); gulp.src([ "node_modules/bootstrap/fonts/*.*" ]).pipe(gulp.dest(outFolder + "/fonts/")); }); //gulp.task("watch.tsc", ["tsc"], function () { // return gulp.watch(appFolder + "/**/*.ts", ["tsc"]); //}); //gulp.task("watch", ["watch.tsc"]); gulp.task("default", [ "shims", "buildAndMinify", "assets", "otherScriptsAndStyles" //,"watch" ]);
در این فایل فرض شدهاست که خروجی نهایی برنامه قرار است در پوشهای به نام wwwroot کپی شود و پوشهی اصلی برنامه، همان پوشهای به نام app، در ریشهی پروژه است.
var appFolder = "./app"; var outFolder = "wwwroot";
1) وظیفهی clean، کار تمیز کردن پوشهی نهایی خروجی برنامه را انجام میدهد (حذف تمام فایلهای آن).
2) وظیفهی shims، کار بسته بندی، یکی کردن و فشرده کردن سه اسکریپت es6-shim.js، zone.js و Reflect.js را انجام میدهد. سپس تک فایل حاصل را به نام shims.js، در پوشهی wwwroot/js کپی میکند.
3) وظیفهی tsc، یکبار دیگر کامپایلر TypeScript را اجرا میکند تا مطمئن شویم با آخرین نگارش فایلهای js برنامه کار میکنیم.
4) وظیفهی system-build، کار پردازش خودکار مداخل فایل systemjs.config.js را انجام میدهد. در آخرین نگارش ارائه شدهی AngularJS 2.0، بجای ذکر مداخل مورد نیاز آن، این تک فایل systemjs.config.js را به صفحه پیوست میکنیم تا اسکریپتهای لازم را (چند صد عدد)، به صورت خودکار بارگذاری کند. برای یکی کردن این چند صد عدد اسکریپت، از کتابخانهی SystemBuilder آن کمک گرفته و کار کامپایل نهایی را انجام میدهیم. خروجی تمام این فایلها، به همراه کلیه فایلهای js حاصل از کامپایل فایلهای TypeScript برنامه، در فایلی به نام bundle.js کپی شدهی در پوشهی wwwroot/js نوشته میشود. بنابراین دیگر نیازی نیست تا فایلهای js پوشهی app و همچنین فایلهای js وابستگیهای AngularJS 2.0 را توزیع کنیم. تک فایل bundle.js، حاوی تمام اینها است.
5) وظیفهی buildAndMinify کار اجرای وظیفهی system-bulder را به همراه فشرده سازی تک فایل bundle.js، به عهده دارد. به علاوه اگر در پوشهی css آن نیز فایل styles.css موجود باشد، آن را فشرده میکند.
6) در ادامه یک سری وظیفهی کپی کردن منابع برنامه را مشاهده میکنید. مانند favicon که کار کپی کردن این آیکن را به پوشهی wwwroot انجام میدهد. وظیفهی css، فایلهای css موجود در پوشههای برنامه را به wwwroot و زیر پوشههای آن کپی میکند. وظیفهی templates، کار کپی کردن فایلهای html قالبهای کامپوننتها را بر عهده دارد. وظیفهی assets، کار کپی کردن فایلهای png را انجام میدهد.
7) وظیفهی otherScriptsAndStyles یک سری css و js ثالث را به پوشهی wwwroot کپی میکند؛ مانند فایلهای بوت استرپ و جیکوئری.
8) وظیفهی default، کار اجرای تمام این وظایف را با هم به عهده دارد.
اکنون اگر بر روی gulpfile.js کلیک راست کنید، گزینهی task runner explorer ظاهر خواهد شد. آنرا انتخاب کنید:
بر روی وظیفهی default کلیک راست کرده و آنرا اجرا کنید. پس از مدتی پوشهی جدید wwwroot ساخته شده و فایلهای نهایی برنامه به آن کپی میشوند.
اصلاح فایل index.html و یا Views\Shared\_Layout.cshtml
اکنون که تمام فایلهای مورد نیاز پروژه در پوشهی wwwroot کپی شدهاند، نیاز است فایل index.html را به نحو ذیل تغییر داد:
<!DOCTYPE html> <html> <head> <base href="/"> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - My ASP.NET Application</title> <link href="~/wwwroot/css/bootstrap.css" rel="stylesheet" /> <link href="~/wwwroot/app.component.css" rel="stylesheet" /> <link href="~/Content/Site.css" rel="stylesheet" type="text/css" /> <script src="~/wwwroot/js/shims.js"></script> </head> <body> <div> @RenderBody() <pm-app>Loading App...</pm-app> </div> <script src="~/wwwroot/js/jquery/dist/jquery.min.js"></script> <script src="~/wwwroot/js/bootstrap/dist/js/bootstrap.min.js"></script> <script src="~/wwwroot/js/bundle.js"></script> @RenderSection("Scripts", required: false) </body> </html>
اسکریپتهای shims که برای مرورگرهای قدیمیتر درنظر گرفته شدهاند، به تک فایل wwwroot/js/shims.js منتقل شدهاند.
تمام اسکریپتهای AngularJS 2.0 و وابستگیهای آن به همراه تمام اسکریپتهای برنامهی خودمان، به تک فایل wwwroot/js/bundle.js منتقل شدهاند.
اکنون اگر برنامه را اجرا کنید، سرعت آن با قبل قابل مقایسه نیست! اینبار دیگر نه نیازی به بارگذاری تمام وابستگیهای AngularJS 2.0 به صورت مجزا توسط systemjs.config.js وجود دارد و نه به ازای مشاهدهی هر صفحهای، یکبار قرار است فایل js کامپوننت آن بارگذاری شود. تمام اینها داخل فایل wwwroot/js/bundle.js قرار گرفتهاند و تنها یکبار بارگذاری میشوند.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part12.zip
خلاصهی بحث
با نوشتن یک Gulp Task جدید میتوان بر اساس فایل systemjs.config.js، تمام اسکریپتهای دخیل در اجرای برنامه را به صورت خودکار یافته و به صورت یک تک فایل نهایی، بسته بندی و توزیع کرد.
پس از آشنایی با «روش کار با فایلهای ایستا در برنامههای React»، اکنون اگر این فایلها ایستا نباشند و توسط یک برنامهی ASP.NET Core بازگشت داده شوند، چطور میتوان از آنها در برنامههای React استفاده کرد؟
برپایی پروژههای مورد نیاز
ابتدا یک پوشهی جدید را مانند DownloadFilesSample، ایجاد کرده و در داخل آن دستور زیر را اجرا میکنیم:
در مورد این قالب که امکان تجربهی توسعهی یکپارچهی ASP.NET Core و React را میسر میکند، در مطلب «روش یکی کردن پروژههای React و ASP.NET Core» بیشتر بحث کردیم.
سپس در این پوشه، پوشهی ClientApp پیشفرض آنرا حذف میکنیم؛ چون کمی قدیمی است. همچنین فایلهای کنترلر و سرویس آب و هوای پیشفرض آنرا به همراه پوشهی صفحات Razor آن، حذف میکنیم.
به علاوه بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
از تنظیم زیر استفاده کردهایم تا با هر بار تغییری در کدهای پروژهی ASP.NET، یکبار دیگر از صفر npm start اجرا نشود:
بدیهی است در این حالت باید از طریق خط فرمان به پوشهی clientApp وارد شد و دستور npm start را یکبار به صورت دستی اجرا کرد، تا این وب سرور بر روی پورت 3000، راه اندازی شود.
اکنون در ریشهی پروژهی ASP.NET Core ایجاد شده، دستور زیر را صادر میکنیم تا پروژهی کلاینت React را با فرمت جدید آن ایجاد کند:
سپس وارد این پوشهی جدید شده و بستههای زیر را نصب میکنیم:
توضیحات:
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود که برای افزودن فایل bootstrap.css آن به پروژهی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام میدهد، مورد استفاده قرار میگیرد.
- برای دریافت فایلها از سمت سرور، از کتابخانهی معروف axios استفاده خواهیم کرد.
کدهای سمت سرور دریافت فایلهای پویا
در اینجا کدهای سمت سرور برنامه، یک فایل PDF ساده را بازگشت میدهند. این محتوای باینری میتواند حاصل اجرای یک گزارش اکسل، PDF و یا کلا هر نوع فایلی باشد:
فایل بازگشتی فوق که در این مثال در مسیر wwwroot\app_data\sample.pdf برنامهی وب کپی شدهاست، در نهایت با آدرس api/Reports/GetPdfReport در سمت کلاینت قابل دسترسی خواهد بود.
روش دریافت محتوای باینری در برنامههای React
برای دریافت یک محتوای باینری از سرور توسط axios مانند تصاویر، فایلهای PDF و اکسل و غیره، مهمترین نکته، تنظیم ویژگی responseType آن به blob است:
ساخت URL برای دسترسی به اطلاعات باینری
تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی میکنند. این متد، شیء URL را از شیء window جاری دریافت میکند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید میکند. حاصل آن، یک URL ویژهاست مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در ادامه کدهای تبدیل blob دریافت شدهی از سرور را به این URL ویژه، مشاهده میکنید:
توضیحات:
- توسط useEffect Hook و بدون ذکر وابستگی خاصی در آن، سبب شبیه سازی رویداد componentDidUpdate شدهایم. به این معنا که متد getResults فراخوانی شدهی در آن، پس از رندر کامپوننت در DOM فراخوانی میشود و بهترین محلی است که از آن میتوان برای ارسال درخواستهای Ajaxای به سمت سرور و دریافت اطلاعات از backend، استفاده کرد و سپس setState را با اطلاعات جدید فراخوانی نمود. معادل setState در اینجا نیز، همان شیء حالتی است که توسط useState Hook و متد setBlobInfo آن تعریف کردهایم.
- پس از دریافت headers و data از سرور، با استفاده از متد createObjectURL، آنرا تبدیل به یک blob URL کردهایم.
- همچنین در سمت سرور، پارامتر fileDownloadName را نیز تنظیم کردهایم. این نام در سمت کلاینت، توسط هدری با کلید content-disposition ظاهر میشود:
بنابراین میتوان آنرا تجزیه کرد و سپس filename را از آن استخراج نمود.
- اکنون که نام فایل و URL دسترسی به دادهی فایل باینری دریافتی از سرور را استخراج و ایجاد کردهایم. با فراخوانی متد setBlobInfo، سبب تنظیم متغیر حالت blobInfo خواهیم شد. این مورد، رندر مجدد UI را سبب شده و توسط آن میتوان برای مثال فایل PDF دریافتی را نمایش داد.
نمایش فایل PDF دریافتی از سرور، به همراه دکمههای دریافت، چاپ و بازکردن آن در برگهای جدید
در ادامه کدهای کامل قسمت رندر این کامپوننت را مشاهده میکنید:
که چنین خروجی را ایجاد میکند:
در اینجا با انتساب مستقیم blob URL ایجاد شده، به خواص src و یا data اشیائی مانند iframe ،object و یا embed، میتوان سبب نمایش فایل pdf دریافتی از سرور شد. این نمایش نیز توسط قابلیتهای توکار مرورگر صورت میگیرد و نیاز به نصب افزونهی خاصی را ندارد.
در ادامه کدهای مرتبط با سه دکمهی چاپ، دریافت و بازکردن فایل دریافتی از سرور را مشاهده میکنید.
مدیریت دکمهی چاپ PDF
پس از اینکه به blobUrl دسترسی یافتیم، اکنون میتوان یک iframe مخفی را ایجاد کرد، سپس src آنرا به این آدرس ویژه تنظیم نمود و در آخر متد print آنرا فراخوانی کرد که سبب نمایش خودکار دیالوگ چاپ مرورگر میشود:
مدیریت دکمهی نمایش فایل PDF در یک برگهی جدید
اگر علاقمند بودید تا این فایل PDF را به صورت تمام صفحه و در برگهای جدید نمایش دهید، میتوان از متد window.open استفاده کرد:
مدیریت دکمهی دریافت فایل PDF
بجای نمایش فایل PDF میتوان دکمهای را بر روی صفحه قرار داد که با کلیک بر روی آن، این فایل توسط مرورگر به صورت متداولی جهت دریافت به کاربر ارائه شود:
در اینجا یک anchor جدید به صورت مخفی به صفحه اضافه میشود که href آن به blobUrl تنظیم شدهاست و همچنین از فایل fileName استخراجی نیز در اینجا جهت ارائهی نام اصلی فایل دریافتی از سرور، کمک گرفته شدهاست. سپس متد click آن فراخوانی خواهد شد. این روش در مورد تدارک دکمهی دریافت تمام blobهای دریافتی از سرور کاربرد دارد و منحصر به فایلهای PDF نیست.
اگر خواستید عملیات axios.get و دریافت فایل، با هم یکی شوند، میتوان متد handleDownloadPdf را پس از پایان کار await axios.get، فراخوانی کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: DownloadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگیهای سمت کلاینت نیز میشود، ابتدا به پوشهی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار میکند. سپس به پوشهی اصلی برنامهی ASP.NET Core بازگشته و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائهی برنامهی React بر روی پورت 5001 میشود.
برپایی پروژههای مورد نیاز
ابتدا یک پوشهی جدید را مانند DownloadFilesSample، ایجاد کرده و در داخل آن دستور زیر را اجرا میکنیم:
> dotnet new react
سپس در این پوشه، پوشهی ClientApp پیشفرض آنرا حذف میکنیم؛ چون کمی قدیمی است. همچنین فایلهای کنترلر و سرویس آب و هوای پیشفرض آنرا به همراه پوشهی صفحات Razor آن، حذف میکنیم.
به علاوه بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
اکنون در ریشهی پروژهی ASP.NET Core ایجاد شده، دستور زیر را صادر میکنیم تا پروژهی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
> cd clientapp > npm install --save bootstrap axios
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود که برای افزودن فایل bootstrap.css آن به پروژهی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
- برای دریافت فایلها از سمت سرور، از کتابخانهی معروف axios استفاده خواهیم کرد.
کدهای سمت سرور دریافت فایلهای پویا
در اینجا کدهای سمت سرور برنامه، یک فایل PDF ساده را بازگشت میدهند. این محتوای باینری میتواند حاصل اجرای یک گزارش اکسل، PDF و یا کلا هر نوع فایلی باشد:
using Microsoft.AspNetCore.Mvc; namespace DownloadFilesSample.Controllers { [Route("api/[controller]")] public class ReportsController : Controller { [HttpGet("[action]")] public IActionResult GetPdfReport() { return File(virtualPath: "~/app_data/sample.pdf", contentType: "application/pdf", fileDownloadName: "sample.pdf"); } } }
روش دریافت محتوای باینری در برنامههای React
برای دریافت یک محتوای باینری از سرور توسط axios مانند تصاویر، فایلهای PDF و اکسل و غیره، مهمترین نکته، تنظیم ویژگی responseType آن به blob است:
const getResults = async () => { const { headers, data } = await axios.get(apiUrl, { responseType: "blob" }); }
ساخت URL برای دسترسی به اطلاعات باینری
تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی میکنند. این متد، شیء URL را از شیء window جاری دریافت میکند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید میکند. حاصل آن، یک URL ویژهاست مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در ادامه کدهای تبدیل blob دریافت شدهی از سرور را به این URL ویژه، مشاهده میکنید:
import axios from "axios"; import React, { useEffect, useState } from "react"; export default function DisplayPdf() { const apiUrl = "https://localhost:5001/api/Reports/GetPdfReport"; const [blobInfo, setBlobInfo] = useState({ blobUrl: "", fileName: "" }); useEffect(() => { getResults(); }, []); const getResults = async () => { try { const { headers, data } = await axios.get(apiUrl, { responseType: "blob" }); console.log("headers", headers); const pdfBlobUrl = window.URL.createObjectURL(data); console.log("pdfBlobUrl", pdfBlobUrl); const fileName = headers["content-disposition"] .split(";") .find(n => n.includes("filename=")) .replace("filename=", "") .trim(); console.log("filename", fileName); setBlobInfo({ blobUrl: pdfBlobUrl, fileName: fileName }); } catch (error) { console.log(error); } };
- توسط useEffect Hook و بدون ذکر وابستگی خاصی در آن، سبب شبیه سازی رویداد componentDidUpdate شدهایم. به این معنا که متد getResults فراخوانی شدهی در آن، پس از رندر کامپوننت در DOM فراخوانی میشود و بهترین محلی است که از آن میتوان برای ارسال درخواستهای Ajaxای به سمت سرور و دریافت اطلاعات از backend، استفاده کرد و سپس setState را با اطلاعات جدید فراخوانی نمود. معادل setState در اینجا نیز، همان شیء حالتی است که توسط useState Hook و متد setBlobInfo آن تعریف کردهایم.
- پس از دریافت headers و data از سرور، با استفاده از متد createObjectURL، آنرا تبدیل به یک blob URL کردهایم.
- همچنین در سمت سرور، پارامتر fileDownloadName را نیز تنظیم کردهایم. این نام در سمت کلاینت، توسط هدری با کلید content-disposition ظاهر میشود:
ontent-disposition: "attachment; filename=sample.pdf; filename*=UTF-8''sample.pdf"
- اکنون که نام فایل و URL دسترسی به دادهی فایل باینری دریافتی از سرور را استخراج و ایجاد کردهایم. با فراخوانی متد setBlobInfo، سبب تنظیم متغیر حالت blobInfo خواهیم شد. این مورد، رندر مجدد UI را سبب شده و توسط آن میتوان برای مثال فایل PDF دریافتی را نمایش داد.
نمایش فایل PDF دریافتی از سرور، به همراه دکمههای دریافت، چاپ و بازکردن آن در برگهای جدید
در ادامه کدهای کامل قسمت رندر این کامپوننت را مشاهده میکنید:
import axios from "axios"; import React, { useEffect, useState } from "react"; export default function DisplayPdf() { // ... const { blobUrl } = blobInfo; return ( <> <h1>Display PDF Files</h1> <button className="btn btn-info" onClick={handlePrintPdf}> Print PDF </button> <button className="btn btn-primary ml-2" onClick={handleShowPdfInNewTab}> Show PDF in a new tab </button> <button className="btn btn-success ml-2" onClick={handleDownloadPdf}> Download PDF </button> <section className="card mb-5 mt-3"> <div className="card-header"> <h4>using iframe</h4> </div> <div className="card-body"> <iframe title="PDF Report" width="100%" height="600" src={blobUrl} type="application/pdf" ></iframe> </div> </section> <section className="card mb-5"> <div className="card-header"> <h4>using object</h4> </div> <div className="card-body"> <object data={blobUrl} aria-label="PDF Report" type="application/pdf" width="100%" height="100%" ></object> </div> </section> <section className="card mb-5"> <div className="card-header"> <h4>using embed</h4> </div> <div className="card-body"> <embed aria-label="PDF Report" src={blobUrl} type="application/pdf" width="100%" height="100%" ></embed> </div> </section> </> ); }
در اینجا با انتساب مستقیم blob URL ایجاد شده، به خواص src و یا data اشیائی مانند iframe ،object و یا embed، میتوان سبب نمایش فایل pdf دریافتی از سرور شد. این نمایش نیز توسط قابلیتهای توکار مرورگر صورت میگیرد و نیاز به نصب افزونهی خاصی را ندارد.
در ادامه کدهای مرتبط با سه دکمهی چاپ، دریافت و بازکردن فایل دریافتی از سرور را مشاهده میکنید.
مدیریت دکمهی چاپ PDF
پس از اینکه به blobUrl دسترسی یافتیم، اکنون میتوان یک iframe مخفی را ایجاد کرد، سپس src آنرا به این آدرس ویژه تنظیم نمود و در آخر متد print آنرا فراخوانی کرد که سبب نمایش خودکار دیالوگ چاپ مرورگر میشود:
const handlePrintPdf = () => { const { blobUrl } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } const iframe = document.createElement("iframe"); iframe.style.display = "none"; iframe.src = blobUrl; document.body.appendChild(iframe); if (iframe.contentWindow) { iframe.contentWindow.print(); } };
مدیریت دکمهی نمایش فایل PDF در یک برگهی جدید
اگر علاقمند بودید تا این فایل PDF را به صورت تمام صفحه و در برگهای جدید نمایش دهید، میتوان از متد window.open استفاده کرد:
const handleShowPdfInNewTab = () => { const { blobUrl } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } window.open(blobUrl); };
مدیریت دکمهی دریافت فایل PDF
بجای نمایش فایل PDF میتوان دکمهای را بر روی صفحه قرار داد که با کلیک بر روی آن، این فایل توسط مرورگر به صورت متداولی جهت دریافت به کاربر ارائه شود:
const handleDownloadPdf = () => { const { blobUrl, fileName } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } const anchor = document.createElement("a"); anchor.style.display = "none"; anchor.href = blobUrl; anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); };
اگر خواستید عملیات axios.get و دریافت فایل، با هم یکی شوند، میتوان متد handleDownloadPdf را پس از پایان کار await axios.get، فراخوانی کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: DownloadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگیهای سمت کلاینت نیز میشود، ابتدا به پوشهی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار میکند. سپس به پوشهی اصلی برنامهی ASP.NET Core بازگشته و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائهی برنامهی React بر روی پورت 5001 میشود.
مطالب دورهها
بوت استرپ (نگارش 3) چیست؟
بوت استرپ یک فریم ورک CSS واکنشگرا (responsive) است، که جهت ساخت سریع برنامههای استاتیک و همچنین پویای وب کاربرد دارد. در حال حاضر این پروژه جزو محبوبترین و فعالترین پروژههای سایت Github است. اگر علاقمند هستید که لیستی از سایتهای استفاده کننده از بوت استرپ را مشاهده کنید، به آدرسهای ذیل مراجعه نمائید:
تازههای بوت استرپ 3 کدامند؟
- بوت استرپ 3 جهت کار با صفحههای نمایش کوچک دستگاههای موبایل به شدت بهینه سازی شده است و به همین جهت به آن mobile-first CSS framework نیز میگویند.
- در نگارش 2 بوت استرپ، حداقل دو نوع گرید واکنشگرا و غیر واکنشگرا قابل تعریف بودند. در نگارش سوم آن، تنها یک نوع گرید جدید واکنشگرا در این فریم ورک وجود دارد که میتواند چهار نوع سایز از بزرگ تا کوچک را شامل شود.
- بوت استرپ 3 با IE7 به قبل و همچنین فایرفاکس 3.6 و پایینتر دیگر سازگار نیست. البته برای پشتیبانی از IE8، نیاز به اندکی تغییرات نیز وجود خواهد داشت که در قسمتهای بعد این جزئیات را بیشتر بررسی خواهیم کرد. به عبارت دیگر بدون این تغییرات، بوت استرپ 3 در حالت پیش فرض با IE9 به بعد سازگار است.
- در بوت استرپ 3 برخلاف نگارش قبلی آن که لیستی از آیکنهای خود را در قالب چند فایل PNG image sprite که آیکنها را به صورت فشرده در کنار هم قرار داده بود، اینبار تنها از Font icons استفاده میکند. به این ترتیب تغییر اندازه این آیکنها با توجه به برداری بودن نمایش قلمها و همچنین قابلیت اعمال رنگ به آنها نیز بسیار سادهتر میگردد.
سؤال: آیا نیاز است از یک فریم ورک CSS واکنشگرا استفاده شود؟
در سالهای قبل، عموما طراحی وب بر اساس تهیه یا خرید یک سری قالبهای از پیش آماده شده، شکیل صورت میگرفتهاست. این قالبها به سرعت با برنامه، یکپارچه شده و حداکثر قلم یا رنگهای آنهارا اندکی تغییر میدادیم و یا اینکه خودمان کل این مسیر را از صفر طی میکردیم. این پروسه سفارشی، بسیار سنگین بوده و مشکل مهم آن، عدم امکان استفاده مجدد از طراحیهای انجام شده میباشد که نهایتا در دراز مدت هزینهی بالایی را برای ما به همراه خواهند داشت. اما با استفاده از فریم ورکهای CSS واکنشگرا به این مزایا خواهیم رسید:
- قسمت عمدهای از کار پیشتر برای شما انجام شده است.
برای مثال نیازی نیست تا حتما برای طرحبندی صفحه، سیستم گرید خاص خودتان را طراحی کنید و یا اینکه مانند سالهای دور، به استفاده از HTML tables پناه ببرید.
- قابلیت سفارشی سازی بسیار بالایی دارند.
برای مثال با استفاده از فناوریهایی مانند less میتوان بوت استرپ را تا حد بسیار زیادی سفارشی سازی کرد. به این ترتیب دیگر یک سایت بوت استرپ، شبیه به بوت استرپ به نظر نخواهد رسید! شاید عدهای عنوان کنند که تمام سایتهای بوت استرپ یک شکل هستند، اما واقعیت این است که این سایتها تنها از قابلیتهای سفارشی سازی بوت استرپ و less استفاده نکردهاند.
دریافت بوت استرپ 3
سایت رسمی دریافت بوت استرپ، آدرس ذیل میباشد:
البته ما از این نگارش خام استفاده نخواهیم کرد و نیاز است برای کارهای سایتهای فارسی، از نگارش راست به چپ آن استفاده کنیم. بنابراین اگر از ویژوال استودیو استفاده میکنید، میتوانید به یکی از دو بسته نیوگت ذیل مراجعه نمائید:
اگر بوت استرپ اصل را از سایت اصلی آن دریافت کنید، شامل تعداد فایلها و پوشههای بسیار بیشتری است نسبت به نمونه RTL فوق. اما فایلهای نهایی آن که مورد استفاده قرار خواهند گرفت، درون پوشه dist یا توزیع آن قرار گرفتهاند و آنچنان تفاوتی با نگارش RTL ندارند. فقط در نگارش اصل، فایلهای min و فشرده شده نیز همراه این بسته هستند که در نگارش RTL لحاظ نشدهاند. این موضوع در آینده به نفع ما خواهد بود. از این لحاظ که اگر از سیستم bundling & minification مربوط بهASP.NET استفاده کنید (جهت تولید خودکار فایلهای min در زمان اجرا)، این سیستم به صورت پیش فرض از فایلهای min موجود استفاده میکند و ممکن است مدتی سردرگم باشید که چرا تغییراتی را که به فایل CSS بوت استرپ اعمال کردهام، در سایت اعمال نمیشوند. به علاوه امکان اعمال تغییرات و حتی دیباگ فایلهای غیرفشرده خصوصا جاوا اسکریپتی آن نیز بسیار سادهتر و مفهومتر است.
جهت مطالعه مباحث تکمیلی در مورد نحوه فشرده سازی فایلهای CSS یا JS میتوانید به مقالات ذیل، در سایت جاری مراجعه نمائید:
علاوه بر اینها در نگارش سوم بوت استرپ، تعدادی فایل CSS جدید به نام قالب یا theme نیز اضافه شدهاند که همراه نسخه RTL نیست. برای مثال اگر به پوشه bootstrap-3.0.0.zip\bootstrap-3.0.0\dist\css مراجعه کنید، فایل bootstrap-theme.css نیز قابل مشاهده است. به این ترتیب قالبی و لایهای بر روی مقادیر پیش فرض موجود در فایل bootstrap.css اعمال خواهند شد؛ برای مثال اعمال طراحی تخت یا flat مدرن آن به دکمهها و عناصر دیگر این مجموعه.
شروع یک فایل HTML با بوت استرپ
تا اینجا فرض بر این است که فایلهای بوت استرپ را دریافت کردهاید. در ادامه قصد داریم، نحوه معرفی این فایلها را در یک فایل ساده HTML بررسی کنیم.
صفحه آغازین کار با بوت استرپ 3 یک چنین شکلی را خواهد داشت و میتواند پایه تشکیل فایل masterpage یا layout برنامههای ASP.NET قرار گیرد. متا تگ viewport اضافه شده، جهت طراحیهای واکنشگرا اضافه شده است و در ادامه لینک شدن فایل CSS بوت استرپ 3 را ملاحظه میکنید.
اگر سایت شما از تعاریف CSS سفارشی دیگری نیز استفاده میکند، تعاریف آنها باید پس از بوت استرپ، ذکر گردند.
افزودن اسکریپتهای بوت استرپ 3
برای کار با اسکریپتهای بوت استرپ 3 نیاز است ابتدا jQuery را به صورت جداگانه دریافت کنیم. در حال حاضر اگر به سایت جیکوئری مراجعه کنید با دو نگارش 1.x و 2.x این کتابخانه مواجه خواهید شد. اگر نیاز به پشتیبانی از IE 8 را در محل کار خود دارید، باید از نگارش 1.x استفاده کنید. نگارش آخر 1.x کتابخانه جیکوئری را از طریق CDN آن همواره میتوان مورد استفاده قرار داد:
بهتر است تعاریف فایلهای جاوا اسکریپت را پیش از بسته شدن تگ body قرار دهید. یکی از مزایای مهم آن مشاهده نشدن یک فلش کوتاه مدت سفید رنگ در ابتدای بارگذاری صفحاتی با پس زمینه غیر روشن است. از این جهت که هر المانی که در head صفحه تعریف شود، حتما باید پیش از بارگذاری کل صفحه دریافت گردد. به این ترتیب با سرعتهای دریافت کمتر، این مساله سبب خالی ماندن صفحه برای مدتی کوتاه خواهد شد و همان فلش سفید رنگ عنوان شده را پدید میآورد؛ چون هنوز مابقی صفحه بارگذاری نشده و خالی است.
پس از تعریف جیکوئری، تعریف اسکریپتهای بوت استرپ قرار میگیرد (چون وابسته است به جیکوئری). فایل bootstrap-rtl.js شامل تمام زیر فایلهای مورد نیاز نیز میباشد:
برای سازگار سازی بوت استرپ 3 با IE8 نیاز به یک فایل اسکریپت دیگر نیز داریم. این فایل را از آدرس ذیل دریافت نمائید:
این فایل 4 کیلوبایتی را نیز باید به تعاریف اسکریپتهای مورد نیاز، اضافه کرد:
البته این اسکریپت خاص، مطابق توضیحات آن باید به head صفحه اضافه شود تا با IE8 بهتر کار کند.
تا اینجا ساختار صفحه HTML تهیه شده جهت استفاده از امکانات بوت استرپ 3، شکل زیر را خواهد داشت:
فایلهای نهایی این قسمت را از اینجا نیز میتوانید دریافت کنید:
bs3-sample01.zip
تازههای بوت استرپ 3 کدامند؟
- بوت استرپ 3 جهت کار با صفحههای نمایش کوچک دستگاههای موبایل به شدت بهینه سازی شده است و به همین جهت به آن mobile-first CSS framework نیز میگویند.
- در نگارش 2 بوت استرپ، حداقل دو نوع گرید واکنشگرا و غیر واکنشگرا قابل تعریف بودند. در نگارش سوم آن، تنها یک نوع گرید جدید واکنشگرا در این فریم ورک وجود دارد که میتواند چهار نوع سایز از بزرگ تا کوچک را شامل شود.
- بوت استرپ 3 با IE7 به قبل و همچنین فایرفاکس 3.6 و پایینتر دیگر سازگار نیست. البته برای پشتیبانی از IE8، نیاز به اندکی تغییرات نیز وجود خواهد داشت که در قسمتهای بعد این جزئیات را بیشتر بررسی خواهیم کرد. به عبارت دیگر بدون این تغییرات، بوت استرپ 3 در حالت پیش فرض با IE9 به بعد سازگار است.
- در بوت استرپ 3 برخلاف نگارش قبلی آن که لیستی از آیکنهای خود را در قالب چند فایل PNG image sprite که آیکنها را به صورت فشرده در کنار هم قرار داده بود، اینبار تنها از Font icons استفاده میکند. به این ترتیب تغییر اندازه این آیکنها با توجه به برداری بودن نمایش قلمها و همچنین قابلیت اعمال رنگ به آنها نیز بسیار سادهتر میگردد.
سؤال: آیا نیاز است از یک فریم ورک CSS واکنشگرا استفاده شود؟
در سالهای قبل، عموما طراحی وب بر اساس تهیه یا خرید یک سری قالبهای از پیش آماده شده، شکیل صورت میگرفتهاست. این قالبها به سرعت با برنامه، یکپارچه شده و حداکثر قلم یا رنگهای آنهارا اندکی تغییر میدادیم و یا اینکه خودمان کل این مسیر را از صفر طی میکردیم. این پروسه سفارشی، بسیار سنگین بوده و مشکل مهم آن، عدم امکان استفاده مجدد از طراحیهای انجام شده میباشد که نهایتا در دراز مدت هزینهی بالایی را برای ما به همراه خواهند داشت. اما با استفاده از فریم ورکهای CSS واکنشگرا به این مزایا خواهیم رسید:
- قسمت عمدهای از کار پیشتر برای شما انجام شده است.
برای مثال نیازی نیست تا حتما برای طرحبندی صفحه، سیستم گرید خاص خودتان را طراحی کنید و یا اینکه مانند سالهای دور، به استفاده از HTML tables پناه ببرید.
- قابلیت سفارشی سازی بسیار بالایی دارند.
برای مثال با استفاده از فناوریهایی مانند less میتوان بوت استرپ را تا حد بسیار زیادی سفارشی سازی کرد. به این ترتیب دیگر یک سایت بوت استرپ، شبیه به بوت استرپ به نظر نخواهد رسید! شاید عدهای عنوان کنند که تمام سایتهای بوت استرپ یک شکل هستند، اما واقعیت این است که این سایتها تنها از قابلیتهای سفارشی سازی بوت استرپ و less استفاده نکردهاند.
دریافت بوت استرپ 3
سایت رسمی دریافت بوت استرپ، آدرس ذیل میباشد:
البته ما از این نگارش خام استفاده نخواهیم کرد و نیاز است برای کارهای سایتهای فارسی، از نگارش راست به چپ آن استفاده کنیم. بنابراین اگر از ویژوال استودیو استفاده میکنید، میتوانید به یکی از دو بسته نیوگت ذیل مراجعه نمائید:
https://www.nuget.org/packages/Twitter.Bootstrap.RTL/3.0.0
https://www.nuget.org/packages/Twitter.Bootstrap.RTL.Less/3.0.0
و اگر میخواهید صرفا به فایلهای درون این بستهها دسترسی پیدا کنید، از دو آدرس ذیل استفاده کنید:https://www.nuget.org/packages/Twitter.Bootstrap.RTL.Less/3.0.0
https://www.nuget.org/api/v2/package/Twitter.Bootstrap.RTL/3.0.0
https://www.nuget.org/api/v2/package/Twitter.Bootstrap.RTL.Less/3.0.0
فایلهای دریافت شده با پسوند nupkg، در حقیقت یک فایل zip استاندارد هستند.https://www.nuget.org/api/v2/package/Twitter.Bootstrap.RTL.Less/3.0.0
اگر بوت استرپ اصل را از سایت اصلی آن دریافت کنید، شامل تعداد فایلها و پوشههای بسیار بیشتری است نسبت به نمونه RTL فوق. اما فایلهای نهایی آن که مورد استفاده قرار خواهند گرفت، درون پوشه dist یا توزیع آن قرار گرفتهاند و آنچنان تفاوتی با نگارش RTL ندارند. فقط در نگارش اصل، فایلهای min و فشرده شده نیز همراه این بسته هستند که در نگارش RTL لحاظ نشدهاند. این موضوع در آینده به نفع ما خواهد بود. از این لحاظ که اگر از سیستم bundling & minification مربوط بهASP.NET استفاده کنید (جهت تولید خودکار فایلهای min در زمان اجرا)، این سیستم به صورت پیش فرض از فایلهای min موجود استفاده میکند و ممکن است مدتی سردرگم باشید که چرا تغییراتی را که به فایل CSS بوت استرپ اعمال کردهام، در سایت اعمال نمیشوند. به علاوه امکان اعمال تغییرات و حتی دیباگ فایلهای غیرفشرده خصوصا جاوا اسکریپتی آن نیز بسیار سادهتر و مفهومتر است.
جهت مطالعه مباحث تکمیلی در مورد نحوه فشرده سازی فایلهای CSS یا JS میتوانید به مقالات ذیل، در سایت جاری مراجعه نمائید:
علاوه بر اینها در نگارش سوم بوت استرپ، تعدادی فایل CSS جدید به نام قالب یا theme نیز اضافه شدهاند که همراه نسخه RTL نیست. برای مثال اگر به پوشه bootstrap-3.0.0.zip\bootstrap-3.0.0\dist\css مراجعه کنید، فایل bootstrap-theme.css نیز قابل مشاهده است. به این ترتیب قالبی و لایهای بر روی مقادیر پیش فرض موجود در فایل bootstrap.css اعمال خواهند شد؛ برای مثال اعمال طراحی تخت یا flat مدرن آن به دکمهها و عناصر دیگر این مجموعه.
شروع یک فایل HTML با بوت استرپ
تا اینجا فرض بر این است که فایلهای بوت استرپ را دریافت کردهاید. در ادامه قصد داریم، نحوه معرفی این فایلها را در یک فایل ساده HTML بررسی کنیم.
<!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My Website</title> <link href="Content/css/bootstrap-rtl.css" rel="stylesheet"> <link href="Content/css/custom.css" rel="stylesheet"> </head> <body> </body> </html>
اگر سایت شما از تعاریف CSS سفارشی دیگری نیز استفاده میکند، تعاریف آنها باید پس از بوت استرپ، ذکر گردند.
افزودن اسکریپتهای بوت استرپ 3
برای کار با اسکریپتهای بوت استرپ 3 نیاز است ابتدا jQuery را به صورت جداگانه دریافت کنیم. در حال حاضر اگر به سایت جیکوئری مراجعه کنید با دو نگارش 1.x و 2.x این کتابخانه مواجه خواهید شد. اگر نیاز به پشتیبانی از IE 8 را در محل کار خود دارید، باید از نگارش 1.x استفاده کنید. نگارش آخر 1.x کتابخانه جیکوئری را از طریق CDN آن همواره میتوان مورد استفاده قرار داد:
<script src="http://code.jquery.com/jquery-latest.min.js"></script>
پس از تعریف جیکوئری، تعریف اسکریپتهای بوت استرپ قرار میگیرد (چون وابسته است به جیکوئری). فایل bootstrap-rtl.js شامل تمام زیر فایلهای مورد نیاز نیز میباشد:
<script src="Scripts/bootstrap-rtl.js"></script>
این فایل 4 کیلوبایتی را نیز باید به تعاریف اسکریپتهای مورد نیاز، اضافه کرد:
<script src="Scripts/respond.min.js"></script>
تا اینجا ساختار صفحه HTML تهیه شده جهت استفاده از امکانات بوت استرپ 3، شکل زیر را خواهد داشت:
<!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My Website</title> <link href="Content/css/bootstrap-rtl.css" rel="stylesheet"> <link href="Content/css/custom.css" rel="stylesheet"> <script src="Scripts/respond.min.js"></script> </head> <body> <script src="http://code.jquery.com/jquery-latest.min.js"></script> <script src="Scripts/bootstrap-rtl.js"></script> </body> </html>
فایلهای نهایی این قسمت را از اینجا نیز میتوانید دریافت کنید:
bs3-sample01.zip
اگر بازار هدف یک محصول شامل چندین کشور، منطقه یا زبان مختلف باشد، طراحی و پیاده سازی آن برای پشتیبانی از ویژگیهای چندزبانه یک فاکتور مهم به حساب میآید. یکی از بهترین روشهای پیاده سازی این ویژگی در دات نت استفاده از فایلهای Resource است. درواقع هدف اصلی استفاده از فایلهای Resource نیز Globalization است. Globalization برابر است با Internationalization + Localization که به اختصار به آن g11n میگویند. در تعریف، Internationalization (یا به اختصار i18n) به فرایند طراحی یک محصول برای پشتیبانی از فرهنگ(culture)ها و زبانهای مختلف و Localization (یا L10n) یا بومیسازی به شخصیسازی یک برنامه برای یک فرهنگ یا زبان خاص گفته میشود. (اطلاعات بیشتر در اینجا).
استفاده از این فایلها محدود به پیاده سازی ویژگی چندزبانه نیست. شما میتوانید از این فایلها برای نگهداری تمام رشتههای موردنیاز خود استفاده کنید. نکته دیگری که باید بدان اشاره کرد این است که تقرببا تمامی منابع مورد استفاده در یک محصول را میتوان درون این فایلها ذخیره کرد. این منابع در حالت کلی شامل موارد زیر است:
- انواع رشتههای مورد استفاده در برنامه چون لیبلها و پیغامها و یا مسیرها (مثلا نشانی تصاویر یا نام کنترلرها و اکشنها) و یا حتی برخی تنظیمات ویژه برنامه (که نمیخواهیم براحتی قابل نمایش یا تغییر باشد و یا اینکه بخواهیم با تغییر زبان تغییر کنند مثل direction و امثال آن)
- تصاویر و آیکونها و یا فایلهای صوتی و انواع دیگر فایل ها
- و ...
نحوه بهره برداری از فایلهای Resource در دات نت، پیاده سازی نسبتا آسانی را در اختیار برنامه نویس قرار میدهد. برای استفاده از این فایلها نیز روشهای متنوعی وجود دارد که در مطلب جاری به چگونگی استفاده از آنها در پروژههای ASP.NET MVC پرداخته میشود.
Globalization در دات نت
فرمت نام یک culture دات نت (که در کلاس CultureInfo پیاده شده است) بر اساس استاندارد RFC 4646 (^ و ^) است. (در اینجا اطلاعاتی راجع به RFC یا Request for Comments آورده شده است). در این استاندارد نام یک فرهنگ (کالچر) ترکیبی از نام زبان به همراه نام کشور یا منطقه مربوطه است. نام زبان برپایه استاندارد ISO 639 که یک عبارت دوحرفی با حروف کوچک برای معرفی زبان است مثل fa برای فارسی و en برای انگلیسی و نام کشور یا منطقه نیز برپایه استاندارد ISO 3166 که یه عبارت دوحرفی با حروف بزرگ برای معرفی یک کشور یا یک منطقه است مثل IR برای ایران یا US برای آمریکاست. برای نمونه میتوان به fa-IR برای زبان فارسی کشور ایران و یا en-US برای زبان انگلیسی آمریکایی اشاره کرد. البته در این روش نامگذاری یکی دو مورد استثنا هم وجود دارد (اطلاعات کامل کلیه زبانها: National Language Support (NLS) API Reference). یک فرهنگ خنثی (Neutral Culture) نیز تنها با استفاده از دو حرف نام زبان و بدون نام کشور یا منطقه معرفی میشود. مثل fa برای فارسی یا de برای آلمانی. در این بخش نیز دو استثنا وجود دارد (^).
در دات نت دو نوع culture وجود دارد: Culture و UICulture. هر دوی این مقادیر در هر Thread مقداری منحصربه فرد دارند. مقدار Culture بر روی توابع وابسته به فرهنگ (مثل فرمت رشتههای تاریخ و اعداد و پول) تاثیر میگذارد. اما مقدار UICulture تعیین میکند که سیستم مدیریت منابع دات نت (Resource Manager) از کدام فایل Resource برای بارگذاری دادهها استفاده کند. درواقع در دات نت با استفاده از پراپرتیهای موجود در کلاس استاتیک Thread برای ثرد جاری (که عبارتند از CurrentCulture و CurrentUICulture) برای فرمت کردن و یا انتخاب Resource مناسب تصمیم گیری میشود. برای تعیین کالچر جاری به صورت دستی میتوان بصورت زیر عمل کرد:
Thread.CurrentThread.CurrentUICulture = new CultureInfo("fa-IR"); Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture("fa-IR");
دراینجا باید اشاره کنم که کار انتخاب Resource مناسب با توجه به کالچر ثرد جاری توسط ResourceProviderFactory پیشفرض دات نت انجام میشود. در مطالب بعدی به نحوه تعریف یک پرووایدر شخصی سازی شده هم خواهم پرداخت.
پشتیبانی از زبانهای مختلف در MVC
برای استفاده از ویژگی چندزبانه در MVC دو روش کلی وجود دارد.
1. استفاده از فایلهای Resource برای تمامی رشتههای موجود
2. استفاده از Viewهای مختلف برای هر زبان
البته روش سومی هم که از ترکیب این دو روش استفاده میکند نیز وجود دارد. انتخاب روش مناسب کمی به سلیقهها و عادات برنامه نویسی بستگی دارد. اگر فکر میکنید که استفاده از ویوهای مختلف به دلیل جداسازی مفاهیم درگیر در کالچرها (مثل جانمایی اجزای مختلف ویوها یا بحث Direction) باعث مدیریت بهتر و کاهش هزینههای پشتیبانی میشود بهتر است از روش دوم یا ترکیبی از این دو روش استفاده کنید. خودم به شخصه سعی میکنم از روش اول استفاده کنم. چون معتقدم استفاده از ویوهای مختلف باعث افزایش بیش از اندازه حجم کار میشود. اما در برخی موارد استفاده از روش دوم یا ترکیبی از دو روش میتواند بهتر باشد.
تولید فایلهای Resource
بهترین مکان برای نگهداری فایلهای Resource در یک پروژه جداگانه است. در پروژههای از نوع وبسایت پوشههایی با نام App_GlobalResources یا App_LocalResources وجود دارد که میتوان از آنها برای نگهداری و مدیریت این نوع فایلها استفاده کرد. اما همانطور که در اینجا توضیح داده شده است این روش مناسب نیست. بنابراین ابتدا یک پروژه مخصوص نگهداری فایلهای Resource ایجاد کنید و سپس اقدام به تهیه این فایلها نمایید. سعی کنید که عنوان این پروژه به صورت زیر باشد. برای کسب اطلاعات بیشتر درباره نحوه نامگذاری اشیای مختلف در دات نت به این مطلب رجوع کنید.
<SolutionName>.Resources
برای افزودن فایلهای Resource به این پروژه ابتدا برای انتخاب زبان پیش فرض محصول خود تصمیم بگیرید. پیشنهاد میکنم که از زبان انگلیسی (en-US) برای اینکار استفاده کنید. ابتدا یک فایل Resource (با پسوند resx.) مثلا با نام Texts.resx به این پروژه اضافه کنید. با افزودن این فایل به پروژه، ویژوال استودیو به صورت خودکار یک فایل cs. حاوی کلاس متناظر با این فایل را به پروژه اضافه میکند. این کار توسط ابزار توکاری به نام ResXFileCodeGenerator انجام میشود. اگر به پراپرتیهای این فایل resx. رجوع کنید میتوانید این عنوان را در پراپرتی Custom Tool ببینید. البته ابزار دیگری برای تولید این کلاسها نیز وجود دارد. این ابزارهای توکار برای سطوح دسترسی مخنلف استفاده میشوند. ابزار پیش فرض در ویژوال استودیو یعنی همان ResXFileCodeGenerator، این کلاسها را با دسترسی internal تولید میکند که مناسب کار ما نیست. ابزار دیگری که برای اینکار درون ویژوال استودیو وجود دارد PublicResXFileCodeGenerator است و همانطور که از نامش پیداست از سطح دسترسی public استفاده میکند. برای تغییر این ابزار کافی است تا عنوان آن را دقیقا در پراپرتی Custom Tool تایپ کنید.
نکته: درباره پراپرتی مهم Build Action این فایلها در مطالب بعدی بیشتر بحث میشود.
برای تعیین سطح دسترسی Resource موردنظر به روشی دیگر، میتوانید فایل Resource را باز کرده و Access Modifier آن را به Public تغییر دهید.
سپس برای پشتیبانی از زبانی دیگر، یک فایل دیگر Resource به پروژه اضافه کنید. نام این فایل باید همنام فایل اصلی به همراه نام کالچر موردنظر باشد. مثلا برای زبان فارسی عنوان فایل باید Texts.fa-IR.resx یا به صورت سادهتر برای کالچر خنثی (بدون نام کشور) Texts.fa.resx باشد. دقت کنید اگر نام فایل را در همان پنجره افزودن فایل وارد کنید ویژوال استودیو این همنامی را به صورت هوشمند تشخیص داده و تغییراتی را در پراپرتیهای پیش فرض فایل Resource ایجاد میکند.
نکته: این هوشمندی مرتبه نسبتا بالایی دارد. بدین صورت که تنها درصورتیکه عبارت بعد از نام فایل اصلی Resource (رشته بعد از نقطه مثلا fa در اینجا) متعلق به یک کالچر معتبر باشد این تغییرات اعمال خواهد شد.
مهمترین این تغییرات این است که ابزاری را برای پراپرتی Custom Tool این فایلها انتخاب نمیکند! اگر به پراپرتی فایل Texts.fa.resx مراجعه کنید این مورد کاملا مشخص است. در نتیجه دیگر فایل cs. حاوی کلاسی جداگانه برای این فایل ساخته نمیشود. همچنین اگر فایل Resource جدید را باز کنید میبنید که برای Access Modifier آن گزینه No Code Generation انتخاب شده است.
در ادامه شروع به افزودن عناوین موردنظر در این دو فایل کنید. در اولی (بدون نام زبان) رشتههای مربوط به زبان انگلیسی و در دومی رشتههای مربوط به زبان فارسی را وارد کنید. سپس در هرجایی که یک لیبل یا یک رشته برای نمایش وجود دارد از این کلیدهای Resource استفاده کنید مثل:
<SolutionName>.Resources.Texts.Save
<SolutionName>.Resources.Texts.Cancel
استفاده از Resource در ویومدل ها
دو خاصیت معروفی که در ویومدلها استفاده میشوند عبارتند از: DisplayName و Required. پشتیبانی از کلیدهای Resource به صورت توکار در خاصیت Required وجود دارد. برای استفاده از آنها باید به صورت زیر عمل کرد:
[Required(ErrorMessageResourceName = "ResourceKeyName", ErrorMessageResourceType = typeof(<SolutionName>.Resources.<ResourceClassName>))]
در کد بالا باید از نام فایل Resource اصلی (فایل اول که بدون نام کالچر بوده و به عنوان منبع پیشفرض به همراه یک فایل cs. حاوی کلاس مربوطه نیز هست) برای معرفی ErrorMessageResourceType استفاده کرد. چون ابزار توکار ویژوال استودیو از نام این فایل برای تولید کلاس مربوطه استفاده میکند.
متاسفانه خاصیت DisplayName که در فضای نام System.ComponentModel (در فایل System.dll) قرار دارد قابلیت استفاده از کلیدهای Resource را به صورت توکار ندارد. در دات نت 4 خاصیت دیگری در فضای نام System.ComponentModel.DataAnnotations به نام Display (در فایل System.ComponentModel.DataAnnotations.dll) وجود دارد که این امکان را به صورت توکار دارد. اما قابلیت استفاده از این خاصیت تنها در MVC 3 وجود دارد. برای نسخههای قدیمیتر MVC امکان استفاده از این خاصیت حتی اگر نسخه فریمورک هدف 4 باشد وجود ندارد، چون هسته این نسخههای قدیمی امکان استفاده از ویژگیهای جدید فریمورک با نسخه بالاتر را ندارد. برای رفع این مشکل میتوان کلاس خاصیت DisplayName را برای استفاده از خاصیت Display به صورت زیر توسعه داد:
public class LocalizationDisplayNameAttribute : DisplayNameAttribute { private readonly DisplayAttribute _display; public LocalizationDisplayNameAttribute(string resourceName, Type resourceType) { _display = new DisplayAttribute { ResourceType = resourceType, Name = resourceName }; } public override string DisplayName { get { try { return _display.GetName(); } catch (Exception) { return _display.Name; } } } }
در این کلاس با ترکیب دو خاصیت نامبرده امکان استفاده از کلیدهای Resource فراهم شده است. در پیاده سازی این کلاس فرض شده است که نسخه فریمورک هدف حداقل برابر 4 است. اگر از نسخههای پایینتر استفاده میکنید در پیاده سازی این کلاس باید کاملا به صورت دستی کلید موردنظر را از Resource معرفی شده بدست آورید. مثلا به صورت زیر:
public class LocalizationDisplayNameAttribute : DisplayNameAttribute { private readonly PropertyInfo nameProperty; public LocalizationDisplayNameAttribute(string displayNameKey, Type resourceType = null) : base(displayNameKey) { if (resourceType != null) nameProperty = resourceType.GetProperty(base.DisplayName, BindingFlags.Static | BindingFlags.Public); } public override string DisplayName { get { if (nameProperty == null) base.DisplayName; return (string)nameProperty.GetValue(nameProperty.DeclaringType, null); } } }
برای استفاده از این خاصیت جدید میتوان به صورت زیر عمل کرد:
[LocalizationDisplayName("ResourceKeyName", typeof(<SolutionName>.Resources.<ResourceClassName>))]
البته بیشتر خواص متداول در ویومدلها از ویژگی موردبحث پشتیبانی میکنند.
نکته: به کار گیری این روش ممکن است در پروژههای بزرگ کمی گیج کننده و دردسرساز بوده و باعث پیچیدگی بیمورد کد و نیز افزایش بیش از حد حجم کدنویسی شود. در مقاله آقای فیل هک (Model Metadata and Validation Localization using Conventions) روش بهتر و تمیزتری برای مدیریت پیامهای این خاصیتها آورده شده است.
پشتیبانی از ویژگی چند زبانه
مرحله بعدی برای چندزبانه کردن پروژههای MVC تغییراتی است که برای مدیریت Culture جاری برنامه باید پیاده شوند. برای اینکار باید خاصیت CurrentUICulture در ثرد جاری کنترل و مدیریت شود. یکی از مکانهایی که برای نگهداری زبان جاری استفاده میشود کوکی است. معمولا برای اینکار از کوکیهای دارای تاریخ انقضای طولانی استفاده میشود. میتوان از تنظیمات موجود در فایل کانفیگ برای ذخیره زبان پیش فرض سیستم نیز استفاه کرد.
روشی که معمولا برای مدیریت زبان جاری میتوان از آن استفاده کرد پیاده سازی یک کلاس پایه برای تمام کنترلرها است. کد زیر راه حل نهایی را نشان میدهد:
public class BaseController : Controller { private const string LanguageCookieName = "MyLanguageCookieName"; protected override void ExecuteCore() { var cookie = HttpContext.Request.Cookies[LanguageCookieName]; string lang; if (cookie != null) { lang = cookie.Value; } else { lang = ConfigurationManager.AppSettings["DefaultCulture"] ?? "fa-IR"; var httpCookie = new HttpCookie(LanguageCookieName, lang) { Expires = DateTime.Now.AddYears(1) }; HttpContext.Response.SetCookie(httpCookie); } Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang); base.ExecuteCore(); } }
راه حل دیگر استفاده از یک ActionFilter است که نحوه پیاده سازی یک نمونه از آن در زیر آورده شده است:
public class LocalizationActionFilterAttribute : ActionFilterAttribute { private const string LanguageCookieName = "MyLanguageCookieName"; public override void OnActionExecuting(ActionExecutingContext filterContext) { var cookie = filterContext.HttpContext.Request.Cookies[LanguageCookieName]; string lang; if (cookie != null) { lang = cookie.Value; } else { lang = ConfigurationManager.AppSettings["DefaultCulture"] ?? "fa-IR"; var httpCookie = new HttpCookie(LanguageCookieName, lang) { Expires = DateTime.Now.AddYears(1) }; filterContext.HttpContext.Response.SetCookie(httpCookie); } Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang); base.OnActionExecuting(filterContext); } }
نکته مهم: تعیین زبان جاری (یعنی همان مقداردهی پراپرتی CurrentCulture ثرد جاری) در یک اکشن فیلتر بدرستی عمل نمیکند. برای بررسی بیشتر این مسئله ابتدا به تصویر زیر که ترتیب رخدادن رویدادهای مهم در ASP.NET MVC را نشان میدهد دقت کنید:
همانطور که در تصویر فوق مشاهده میکنید رویداد OnActionExecuting که در یک اکشن فیلتر به کار میرود بعد از عملیات مدل بایندینگ رخ میدهد. بنابراین قبل از تعیین کالچر جاری، عملیات validation و یافتن متن خطاها از فایلهای Resource انجام میشود که منجر به انتخاب کلیدهای مربوط به کالچر پیشفرض سرور (و نه آنچه که کاربر تنظیم کرده) خواهد شد. بنابراین استفاده از یک اکشن فیلتر برای تعیین کالچر جاری مناسب نیست. راه حل مناسب استفاده از همان کنترلر پایه است، زیرا متد ExecuteCore قبل از تمامی این عملیات صدا زده میشود. بنابرابن همیشه کالچر تنظیم شده توسط کاربر به عنوان مقدار جاری آن در ثرد ثبت میشود.
امکان تعیین/تغییر زبان توسط کاربر
برای تعیین یا تغییر زبان جاری سیستم نیز روشهای گوناگونی وجود دارد. استفاده از زبان تنظیم شده در مرورگر کاربر، استفاده از عنوان زبان در آدرس صفحات درخواستی و یا تعیین زبان توسط کاربر در تنظیمات برنامه/سایت و ذخیره آن در کوکی یا دیتابیس و مواردی از این دست روشهایی است که معمولا برای تعیین زبان جاری از آن استفاده میشود. در کدهای نمونه ای که در بخشهای قبل آورده شده است فرض شده است که زبان جاری سیستم درون یک کوکی ذخیره میشود بنابراین برای استفاده از این روش میتوان از قطعه کدی مشابه زیر (مثلا در فایل Layout.cshtml_) برای تعیین و تغییر زبان استفاه کرد:
<select id="langs" onchange="languageChanged()"> <option value="fa-IR">فارسی</option> <option value="en-US">انگلیسی</option> </select> <script type="text/javascript"> function languageChanged() { setCookie("MyLanguageCookieName", $('#langs').val(), 365); window.location.reload(); } document.ready = function () { $('#langs').val(getCookie("MyLanguageCookieName")); }; function setCookie(name, value, exdays, path) { var exdate = new Date(); exdate.setDate(exdate.getDate() + exdays); var newValue = escape(value) + ((exdays == null) ? "" : "; expires=" + exdate.toUTCString()) + ((path == null) ? "" : "; path=" + path) ; document.cookie = name + "=" + newValue; } function getCookie(name) { var i, x, y, cookies = document.cookie.split(";"); for (i = 0; i < cookies.length; i++) { x = cookies[i].substr(0, cookies[i].indexOf("=")); y = cookies[i].substr(cookies[i].indexOf("=") + 1); x = x.replace(/^\s+|\s+$/g, ""); if (x == name) { return unescape(y); } } } </script>
متدهای setCookie و getCookie جاوا اسکریپتی در کد بالا از اینجا گرفته شده اند البته پس از کمی تغییر.
نکته: مطلب Cookieها بحثی نسبتا مفصل است که در جای خودش باید به صورت کامل آورده شود. اما در اینجا تنها به همین نکته اشاره کنم که عدم توجه به پراپرتی path کوکیها در این مورد خاص برای خود من بسیار گیجکننده و دردسرساز بود.
به عنوان راهی دیگر میتوان به جای روش ساده استفاده از کوکی، تنظیماتی در اختیار کاربر قرار داد تا بتواند زبان تنظیم شده را درون یک فایل یا دیتابیس ذخیره کرد البته با درنظر گرفتن مسائل مربوط به کش کردن این تنظیمات.
راه حل بعدی میتواند استفاده از تنظیمات مرورگر کاربر برای دریافت زبان جاری تنظیم شده است. مرورگرها تنظیمات مربوط به زبان را در قسمت Accept-Languages در HTTP Header درخواست ارسالی به سمت سرور قرار میدهند. بصورت زیر:
GET https://www.dntips.ir HTTP/1.1 ... Accept-Language: fa-IR,en-US;q=0.5 ...
این هم تصویر مربوط به Fiddler آن:
نکته: پارامتر q در عبارت مشخص شده در تصویر فوق relative quality factor نام دارد و به نوعی مشخص کننده اولویت زبان مربوطه است. مقدار آن بین 0 و 1 است و مقدار پیش فرض آن 1 است. هرچه مقدار این پارامتر بیشتر باشد زبان مربوطه اولویت بالاتری دارد. مثلا عبارت زیر را درنظر بگیرید:
Accept-Language: fa-IR,fa;q=0.8,en-US;q=0.5,ar-BH;q=0.3
در این حالت اولویت زبان fa-IR برابر 1 و fa برابر 0.8 (fa;q=0.8) است. اولویت دیگر زبانهای تنظیم شده نیز همانطور که نشان داده شده است در مراتب بعدی قرار دارند. در تنظیم نمایش داده شده برای تغییر این تنظیمات در IE میتوان همانند تصویر زیر اقدام کرد:
در تصویر بالا زبان فارسی اولویت بالاتری نسبت به انگلیسی دارد. برای اینکه سیستم g11n دات نت به صورت خودکار از این مقادیر جهت زبان ثرد جاری استفاده کند میتوان از تنظیم زیر در فایل کانفیگ استفاده کرد:
<system.web> <globalization enableClientBasedCulture="true" uiCulture="auto" culture="auto"></globalization> </system.web>
در سمت سرور نیز برای دریافت این مقادیر تنظیم شده در مرورگر کاربر میتوان از کدهای زیر استفاه کرد. مثلا در یک اکشن فیلتر:
var langs = filterContext.HttpContext.Request.UserLanguages;
پراپرتی UserLanguages از کلاس Request حاوی آرایهای از استرینگ است. این آرایه درواقع از Split کردن مقدار Accept-Languages با کاراکتر ',' بدست میآید. بنابراین اعضای این آرایه رشتهای از نام زبان به همراه پارامتر q مربوطه خواهند بود (مثل "fa;q=0.8").
راه دیگر مدیریت زبانها استفاده از عنوان زبان در مسیر درخواستی صفحات است. مثلا آدرسی شبیه به www.MySite.com/fa/Employees نشان میدهد کاربر درخواست نسخه فارسی از صفحه Employees را دارد. نحوه استفاده از این عناوین و نیز موقعیت فیزیکی این عناوین در مسیر صفحات درخواستی کاملا به سلیقه برنامه نویس و یا کارفرما بستگی دارد. روش کلی بهره برداری از این روش در تمام موارد تقریبا یکسان است.
برای پیاده سازی این روش ابتدا باید یک route جدید در فایل Global.asax.cs اضافه کرد:
routes.MapRoute( "Localization", // Route name "{lang}/{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults );
دقت کنید که این route باید قبل از تمام routeهای دیگر ثبت شود. سپس باید کلاس پایه کنترلر را به صورت زیر پیاده سازی کرد:
public class BaseController : Controller { protected override void ExecuteCore() { var lang = RouteData.Values["lang"]; if (lang != null && !string.IsNullOrWhiteSpace(lang.ToString())) { Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang.ToString()); } base.ExecuteCore(); } }
این کار را در یک اکشن فیلتر هم میتوان انجام داد اما با توجه به توضیحاتی که در قسمت قبل داده شد استفاده از اکشن فیلتر برای تعیین زبان جاری کار مناسبی نیست.
نکته: به دلیل آوردن عنوان زبان در مسیر درخواستها باید کتترل دقیقتری بر کلیه مسیرهای موجود داشت!
استفاده از ویوهای جداگانه برای زبانهای مختلف
برای اینکار ابتدا ساختار مناسبی را برای نگهداری از ویوهای مختلف خود درنظر بگیرید. مثلا میتوانید همانند نامگذاری فایلهای Resource از نام زبان یا کالچر به عنوان بخشی از نام فایلهای ویو استفاده کنید و تمام ویوها را در یک مسیر ذخیره کنید. همانند تصویر زیر:
البته اینکار ممکن است به مدیریت این فایلها را کمی مشکل کند چون به مرور زمان تعداد فایلهای ویو در یک فولدر زیاد خواهد شد. روش دیگری که برای نگهداری این ویوها میتوان به کار برد استفاده از فولدرهای جداگانه با عناوین زبانهای موردنظر است. مانند تصویر زیر:
روش دیگری که برای نگهداری و مدیریت بهتر ویوهای زبانهای مختلف از آن استفاده میشود به شکل زیر است:
استفاه از هرکدام از این روشها کاملا به سلیقه و راحتی مدیریت فایلها برای برنامه نویس بستگی دارد. درهر صورت پس از انتخاب یکی از این روشها باید اپلیکشن خود را طوری تنظیم کنیم که با توجه به زبان جاری سیستم، ویوی مربوطه را جهت نمایش انتخاب کند.
مثلا برای روش اول نامگذاری ویوها میتوان از روش دستکاری متد OnActionExecuted در کلاس پایه کنترلر استفاده کرد:
public class BaseController : Controller { protected override void OnActionExecuted(ActionExecutedContext context) { var view = context.Result as ViewResultBase; if (view == null) return; // not a view var viewName = view.ViewName; view.ViewName = GetGlobalizationViewName(viewName, context); base.OnActionExecuted(context); } private static string GetGlobalizationViewName(string viewName, ControllerContext context) { var cultureName = Thread.CurrentThread.CurrentUICulture.Name; if (cultureName == "en-US") return viewName; // default culture if (string.IsNullOrEmpty(viewName)) return context.RouteData.Values["action"] + "." + cultureName; // "Index.fa" int i; if ((i = viewName.IndexOf('.')) > 0) // ex: Index.cshtml return viewName.Substring(0, i + 1) + cultureName + viewName.Substring(i); // "Index.fa.cshtml" return viewName + "." + cultureName; // "Index" ==> "Index.fa" } }
همانطور که قبلا نیز شرح داده شد، چون متد ExecuteCore قبل از OnActionExecuted صدا زده میشود بنابراین از تنظیم درست مقدار کالچر در ثرد جاری اطمینان داریم.
روش دیگری که برای مدیریت انتخاب ویوهای مناسب استفاده از یک ویوانجین شخصی سازی شده است. مثلا برای روش سوم نامگذاری ویوها میتوان از کد زیر استفاده کرد:
public sealed class RazorGlobalizationViewEngine : RazorViewEngine { protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) { return base.CreatePartialView(controllerContext, GetGlobalizationViewPath(controllerContext, partialPath)); } protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) { return base.CreateView(controllerContext, GetGlobalizationViewPath(controllerContext, viewPath), masterPath); } private static string GetGlobalizationViewPath(ControllerContext controllerContext, string viewPath) { //var controllerName = controllerContext.RouteData.GetRequiredString("controller"); var request = controllerContext.HttpContext.Request; var lang = request.Cookies["MyLanguageCookie"]; if (lang != null && !string.IsNullOrEmpty(lang.Value) && lang.Value != "en-US") { var localizedViewPath = Regex.Replace(viewPath, "^~/Views/", string.Format("~/Views/Globalization/{0}/", lang.Value)); if (File.Exists(request.MapPath(localizedViewPath))) viewPath = localizedViewPath; } return viewPath; }
و برای ثبت این ViewEngine در فایل Global.asax.cs خواهیم داشت:
protected void Application_Start() { ViewEngines.Engines.Clear(); ViewEngines.Engines.Add(new RazorGlobalizationViewEngine()); }
محتوای یک فایل Resource
ساختار یک فایل resx. به صورت XML استاندارد است. در زیر محتوای یک نمونه فایل Resource با پسوند resx. را مشاهده میکنید:
<?xml version="1.0" encoding="utf-8"?> <root> <!-- Microsoft ResX Schema ... --> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> ... </xsd:schema> <resheader name="resmimetype"> <value>text/microsoft-resx</value> </resheader> <resheader name="version"> <value>2.0</value> </resheader> <resheader name="reader"> <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <resheader name="writer"> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="RightToLeft" xml:space="preserve"> <value>false</value> <comment>RightToleft is false in English!</comment> </data> </root>
در قسمت ابتدایی تمام فایلهای resx. که توسط ویژوال استودیو تولید میشود کامنتی طولانی وجود دارد که به صورت خلاصه به شرح محتوا و ساختار یک فایل Resource میپردازد. در ادامه تگ نسبتا طولانی xsd:schema قرار دارد. از این قسمت برای معرفی ساختار داده ای فایلهای XML استفاده میشود. برای آشنایی بیشتر با XSD (یا XML Schema) به اینجا مراجعه کنید. به صورت خلاصه میتوان گفت که XSD برای تعیین ساختار دادهها یا تعیین نوع داده ای اطلاعات موجود در یک فایل XML به کار میرود. درواقع تگهای XSD به نوعی فایل XML ما را Strongly Typed میکند. با توجه به اطلاعات این قسمت، فایلهای resx. شامل 4 نوع گره اصلی هستند که عبارتند از: metadata و assembly و data و resheader. در تعریف هر یک از گرهها در این قسمت مشخصاتی چون نام زیر گرههای قابل تعریف در هر گره و نام و نوع خاصیتهای هر یک معرفی شده است.
بخش موردنظر ما در این مطلب قسمت انتهایی این فایلهاست (تگهای resheader و data). همانطور در بالا مشاهده میکنید تگهای reheader شامل تنظیمات مربوط به فایل resx. با ساختاری ساده به صورت name/value است. یکی از این تنظیمات resmimetype فایل resource را معرفی میکند که درواقع مشخص کننده نوع محتوای (Content Type) فایل XML است(^). برای فایلهای resx این مقدار برابر text/microsoft-resx است. تنظیم بعدی نسخه مربوط به فایل resx (یا Microsoft ResX Schema) را نشان میدهد. در حال حاضر نسخه جاری (در VS 2010) برابر 2.0 است. تنظیم بعدی مربوط به کلاسهای reader و writer تعریف شده برای استفاده از این فایلهاست. به نوع این کلاسهای خواننده و نویسنده فایلهای resx. و مکان فیزیکی و فضای نام آنها دقت کنید که در مطالب بعدی از آنها برای ویرایش و بروزرسانی فایلهای resource در زمان اجرا استفاده خواهیم کرد.
در پایان نیز تگهای data که برای نگهداری دادهها از آنها استفاده میشود. هر گره data شامل یک خاصیت نام (name) و یک زیرگره مقدار (value) است. البته امکان تعیین یک کامنت در زیرگره comment نیز وجود دارد که اختیاری است. هر گره data مینواند شامل خاصیت type و یا mimetype نیز باشد. خاصیت type مشخص کننده نوعی است که تبدیل text/value را با استفاده از ساختار TypeConverter پشتیبانی میکند. البته اگر در نوع مشخص شده این پشتیبانی وجود نداشته باشد، داده موردنظر پس از سریالایز شدن با فرمت مشخص شده در خاصیت mimetype ذخیره میشود. این mimetype اطلاعات موردنیاز را برای کلاس خواننده این فایلها (ResXResourceReader به صورت پیشفرض) جهت چگونگی بازیابی آبجکت موردنظر فراهم میکند. مشخص کردن این دو خاصیت برای انواع رشته ای نیاز نیست. انواع mimetype قابل استفاده عبارتند از:
- application/x-microsoft.net.object.binary.base64: آبجکت موردنظر باید با استفاده از کلاس System.Runtime.Serialization.Formatters.Binary.BinaryFormatter سریالایز شده و سپس با فرمت base64 به یک رشته انکد شود (راجع به انکدینگ base64 ^ و ^).
- application/x-microsoft.net.object.soap.base64: آبجکت موردنظر باید با استفاده از کلاس System.Runtime.Serialization.Formatters.Soap.SoapFormatter سریالایز شده و سپس با فرمت base64 به یک رشته انکد شود.
- application/x-microsoft.net.object.bytearray.base64: آبجکت ابتدا باید با استفاده از یک System.ComponentModel.TypeConverter به آرایه ای از بایت سریالایز شده و سپس با فرمت base64 به یک رشته انکد شود.
نکته: امکان جاسازی کردن (embed) فایلهای resx. در یک اسمبلی یا کامپایل مستقیم آن به یک سَتِلایت اسمبلی (ترجمه مناسبی برای satellite assembly پیدا نکردم، چیزی شبیه به اسمبلی قمری یا وابسته و از این قبیل ...) وجود ندارد. ابتدا باید این فایلهای resx. به فایلهای resources. تبدیل شوند. اینکار با استفاده از ابزار Resource File Generator (نام فایل اجرایی آن resgen.exe است) انجام میشود (^ و ^). سپس میتوان با استفاده از Assembly Linker ستلایت اسمبلی مربوطه را تولید کرد (^). کل این عملیات در ویژوال استودیو با استفاده از ابزار msbuild به صورت خودکار انجام میشود!
نحوه یافتن کلیدهای Resource در بین فایلهای مختلف Resx توسط پرووایدر پیش فرض در دات نت
عملیات ابتدا با بررسی خاصیت CurrentUICulture از ثرد جاری آغاز میشود. سپس با استفاده از عنوان استاندارد کالچر جاری، فایل مناسب Resource یافته میشود. در نهایت بهترین گزینه موجود برای کلید درخواستی از منابع موجود انتخاب میشود. مثلا اگر کالچر جاری fa-IR و کلید درخواستی از کلاس Texts باشد ابتدا جستجو برای یافتن فایل Texts.fa-IR.resx آغاز میشود و اگر فایل موردنظر یا کلید درخواستی در این فایل یافته نشد جستجو در فایل Texts.fa.resx ادامه مییابد. اگر باز هم یافته نشد درنهایت این عملیات جستجو در فایل resource اصلی خاتمه مییابد و مقدار کلید منبع پیش فرض به عنوان نتیجه برگشت داده میشود. یعنی در تمامی حالات سعی میشود تا دقیقترین و بهترین و نزدیکترین نتیجه انتخاب شود. البته درصورتیکه از یک پرووایدر شخصی سازی شده برای کار خود استفاده میکنید باید چنین الگوریتمی را جهت یافتن کلیدهای منابع خود از فایلهای Resource (یا هرمنبع دیگر مثل دیتابیس یا حتی یک وب سرویس) درنظر بگیرید.
Globalization در کلاینت (javascript g11n)
یکی دیگر از موارد استفاده g11n در برنامه نویسی سمت کلاینت است. با وجود استفاده گسترده از جاوا اسکریپت در برنامه نویسی سمت کلاینت در وب اپلیکیشنها، متاسفانه تا همین اواخر عملا ابزار یا کتابخانه مناسبی برای مدیریت g11n در این زمینه وجود نداشته است. یکی از اولین کتابخانههای تولید شده در این زمینه کتابخانه jQuery Globalization است که توسط مایکروسافت توسعه داده شده است (برای آشنایی بیشتر با این کتابخانه به ^ و ^ مراجعه کنید). این کتابخانه بعدا تغییر نام داده و اکنون با عنوان Globalize شناخته میشود. Globalize یک کتابخانه کاملا مستقل است که وابستگی به هیچ کتابخانه دیگر ندارد (یعنی برای استفاده از آن نیازی به jQuery نیست). این کتابخانه حاوی کالچرهای بسیاری است که عملیات مختلفی چون فرمت و parse انواع دادهها را نیز در سمت کلاینت مدیریت میکند. همچنین با فراهم کردن منابعی حاوی جفتهای key/culture میتوان از مزایایی مشابه مواردی که در این مطلب بحث شد در سمت کلاینت نیز بهره برد. نشانی این کتابخانه در github اینجا است. با اینکه خود این کتابخانه ابزار کاملی است اما در بین کالچرهای موجود در فایلهای آن متاسفانه پشتیبانی کاملی از زبان فارسی نشده است. ابزار دیگری که برای اینکار وجود دارد پلاگین jquery localize است که برای بحث g11n رشتهها پیادهسازی بهتر و کاملتری دارد.
در مطالب بعدی به مباحث تغییر مقادیر کلیدهای فایلهای resource در هنگام اجرا با استفاده از روش مستقیم تغییر محتوای فایلها و کامپایل دوباره توسط ابزار msbuild و نیز استفاده از یک ResourceProvider شخصی سازی شده به عنوان یک راه حل بهتر برای اینکار میپردازم.
در تهیه این مطلب از منابع زیر استفاده شده است:
تا اینجا الگوی Redux را در برنامههای React بررسی کردیم که شامل این موارد است:
- با استفاده از Redux، یک شیء سراسری state، کار مدیریت state تمام برنامه را به عهده میگیرد که به آن «single source of truth» نیز گفته میشود. البته هرچند میتوان کامپوننتهایی را هم در این بین داشت که state خاص خودشان را داشته باشند و آنرا در این شیء سراسری ذخیره نکنند.
- در حین کار با Redux، تنها راه تغییر شیء سراسری state آن، صدور رخدادهایی هستند که در اینجا اکشن نامیده میشوند. یک اکشن شیءای است که بیان میکند چه چیزی قرار است تغییر کند.
- برای ساده سازی ساخت این اشیاء میتوان متدهایی را به نام action creators ایجاد کرد.
- اگر این متدهای action creator را توسط متد store.dispatch فراخوانی کنیم، سبب dispatch شیء اکشن، به یک تابع Reducer متناظری خواهند شد. این تابع Reducer است که قسمتی از state را که متناظر با نوع اکشن رسیدهاست، تغییر میدهد. در این حالت اگر اکشن رسیده، نوع مدنظری را نداشته باشد، خروجی تابع Reducer، همان state اصلی و بدون تغییر خواهد بود.
- Reducerها توابعی خالص هستند و نباید به همراه اثرات جانبی باشند (هر نوع تعاملی با دنیای خارج از تابع جاری) و همچنین نباید شیء state را نیز مستقیما تغییر دهند. این توابع باید یک کپی تغییر یافتهی از state را در صورت نیاز بازگشت دهند.
- برای مدیریت بهتر برنامه میتوان چندین تابع Reducer را بر اساس نوعهای اکشنهای ویژهای، پیاده سازی کرد. سپس با ترکیب آنها، یک شیء rootReducer ایجاد میشود.
- در نهایت در الگوی Redux، یک مخزن یا store تعریف خواهد شد که تمام این اجزاء را مانند rootReducer و میانافزارهای تعریف شده مانند Thunk، در کنار هم قرار میدهد و امکان dispatch اکشنها را میسر میکند.
- اکنون برای استفادهی از Redux در یک برنامهی React، نیاز است کامپوننت ریشهی برنامه را توسط کامپوننت Provider آن محصور کرد تا قسمتهای مختلف برنامه بتوانند با امکانات مخزن Redux، کار کرده و با آن ارتباط برقرار کنند.
- قسمت آخر این اتصال جائی است که کامپوننتهای اصلی برنامه، توسط یک کامپوننت دربرگیرنده که Container نامیده میشود، توسط متد connect کتابخانهی react-redux محصور میشوند. به این ترتیب این کامپوننتها میتوانند state و خواص مورد نیاز خود را از طریق props دریافت کرده (mapStateToProps) و یا رویدادها را به سمت store، ارسال کنند (mapDispatchToProps).
از زمان React 16.8، مفهوم جدیدی به نام React Hooks معرفی شد که تعدادی از مهمترینهای آنها را در سری «React 16x» بررسی کردیم. توسط Hooks، کامپوننتهای تابعی React اکنون میتوانند به local state خود دسترسی پیدا کنند و یا با دنیای خارج ارتباط برقرار کنند. پس از آن سایر کتابخانههای نوشته شدهی برای React نیز شروع به انطباق خود با این الگوی جدید کردهاند؛ برای مثال کتابخانهی react-redux v1.7 نیز به همراه تعدادی Hook، جهت ساده سازی آخرین قسمتی است که در اینجا بیان شد، تا بتوانند راه حل دومی برای اتصال کامپوننتها و دربرگیری آنها باشند که در ادامه جزئیات آنها را بررسی خواهیم کرد.
بررسی useSelector Hook
useSelector Hook که توسط کتابخانهی react-redux ارائه میشود، معادل بسیار نزدیک تابع mapStateToProps مورد استفادهی در متد connect است. برای مثال در قسمت قبل، دربرگیرندهی کامپوننت Posts در فایل src\containers\Posts.js، یک چنین محتوایی را دارد:
اینبار اگر بخواهیم کل این container را حذف کرده و از useSelector Hook استفاده کنیم، به این ترتیب عمل خواهیم کرد:
پیشتر امضای کامپوننت تابعی Posts واقع در فایل src\components\Posts.jsx، به صورت زیر تعریف شده بود که سه خاصیت را از طریق props دریافت میکرد:
و این سه خاصیت دقیقا از متد mapStateToProps فوق که ملاحظه میکنید، تامین میشود. این متد خواص شیء state.postsReducer را به صورت props به کامپوننت Posts از طریق متد connect، ارسال میکند. کار postsReducer، فراهم آوردن و مدیریت سه خاصیت { loading: false, posts: [], error: null } است.
اکنون فایل جدید src\components\HooksPosts.jsx را ایجاد کرده و ابتدا و امضای کامپوننت تابعی Posts را به صورت زیر تغییر میدهیم:
متد useSelector، امکان دسترسی به state ذخیره شدهی در مخزن redux را میسر میکند. سپس باید همانند متد mapStateToProps، خواصی را که از آن نیاز داریم، دریافت کنیم که در اینجا کل خواص postsReducer دریافت شده (کل state دریافت شده و سپس خاصیت state.postsReducer آن بازگشت داده شدهاست) و در ادامه توسط Object Destructuring، به سه متغیری که پیشتر از طریق props تامین میشدند، انتساب داده میشود.
یک نکته: خروجی تابع mapStateToProps همواره باید یک شیء باشد، اما چنین محدودیتی در مورد تابع useSelector وجود ندارد و در صورت نیاز میتوان تنها مقدار یک خاصیت از یک شیء را نیز بازگشت داد.
این کامپوننت، هیچ تغییر دیگری را نیاز ندارد و اگر اکنون به فایل src\App.js مراجعه کنیم، میتوان دربرگیرندهی کامپوننت Posts را:
با کامپوننت جدید HooksPosts جایگزین کرد و دیگر نیازی به نوشتن متد connect و ساخت یک container مخصوص آن، نیست:
بررسی useDispatch Hook
تا اینجا موفق شدیم متد mapStateToProps را با useSelector Hook جایگزین کنیم. مرحلهی بعد، جایگزین کردن mapDispatchToProps با هوک دیگری به نام useDispatch است. برای مثال در قسمت قبل، دربرگیرندهی کامپوننت FetchPosts در فایل src\containers\FetchPosts.js، چنین تعریفی را دارد:
کار این تامین کننده، اتصال action creator ای به نام fetchPostsAsync به props کامپوننت FetchPosts است که در فایل src\components\FetchPosts.jsx به این صورت تعریف شدهاست:
اکنون برای جایگزین کردن mapDispatchToProps با useDispatch Hook، نگارش دیگری از این کامپوننت تابعی را به نام HooksFetchPosts در فایل src\components\HooksFetchPosts.jsx ایجاد میکنیم:
عملکر آن نیز بسیار سادهاست. متد useDispatch، به ما امکان دسترسی به متد store.dispatch را میدهد (ارجاعی به آنرا در اختیار ما قرار میدهد). اکنون اگر مانند رخداد onClick تعریف شده، سبب dispatch یک action creator به نام fetchPostsAsync شویم (که اینبار باید به صورت صریح از ماژول مربوطه import شود؛ چون دیگر از طریق props تامین نمیشود)، سبب ارسال نتیجهی آن به reducer متناظری میشود.
با این تغییر نیز میتوان به فایل src\App.js مراجعه کرد و المان قبلی FetchPostsContainer را که از ماژول containers/FetchPosts تامین میشد، به نحو متداولی با همان کامپوننت جدید HooksFetchPosts، تعویض کرد:
یک مثال تکمیلی: بازنویسی src\components\counter.jsx با redux hooks
کامپوننت شمارشگر را در قسمت سوم این سری بررسی و تکمیل کردیم. اکنون قصد داریم فایل تامین کنندهی آنرا که به صورت زیر در فایل src\containers\Counter.js تعریف شده:
حذف کرده و با redux hooks جایگزین کنیم. برای این منظور فایل جدید src\components\HooksCounter.jsx را ایجاد میکنیم و سپس در ابتدا برای جایگزین کردن قسمت دریافت اطلاعات از this.pros آن:
به صورت زیر عمل میکنیم:
- متغیر count را با استفاده از useSelector، از شیء state استخراج کرده و با نام خاصیت count بازگشت میدهیم.
- اینبار دو action creator مورد استفادهی در متدهای + و - را از ماژول action دریافت کردهایم تا توسط useDispatch مورد استفاده قرار گیرند.
- همچنین دیگر نیازی به ذکر (state, ownProps) نیست. مقدار ownProps، همان props معمولی است که به کامپوننت ارسال میشود که برای مثال اینبار نام prop1 را دارد؛ چون هنگامیکه المان کامپوننت HooksCounter را درج و معرفی میکنیم، توسط کامپوننت دیگری محصور نشدهاست. تامین آن نیز در فایل src\App.js با درج متداول نام المان کامپوننت HooksCounter و ذکر ویژگی سفارشی prop1 صورت میگیرد:
با این تغییرات، کدهای کامل src\components\HooksCounter.jsx به صورت زیر تکمیل میشود که قسمتهای استفاده از متغیر count و همچنین dispatch دو action creator دریافت شده، در آن مشخص هستند:
مشکل! با استفاده از useSelector، تعداد رندرهای مجدد کامپوننتهای برنامه افزایش یافتهاست!
برنامهی جاری را پس از این تغییرات اجرا کنید. با هر بار کلیک بر روی دکمهی fetch posts، حتی کامپوننت شمارشگر درج شدهی در صفحه که ربطی به آن ندارد نیز رندر مجدد میشود! چرا؟ (این مورد را با مشاهدهی کنسول توسعه دهندگان مرورگر میتوانید مشاهده کنید. در ابتدای متد رندر هر کدام از کامپوننتها، یک console.log قرار داده شدهاست)
زمانیکه اکشنی dispatch میشود، useSelector hook با استفاده از مقایسهی ارجاعات اشیاء (strict === reference check)، کار مقایسهی مقدار قبلی و مقدار جدید را انجام میدهد. اگر اینها متفاوت باشند، کامپوننت را مجبور به رندر مجدد میکند. این مورد مهمترین تفاوت بین useSelector hook و متد connect است. متد connect از روش shallow equality checks برای مقایسهی نتایج حاصل از mapStateToProps و تصمیم در مورد رندر مجدد استفاده میکند. اما این مقایسهها چه تفاوتی با هم دارند؟
در حالت mapStateToProps، مهم نیست که شیء بازگشت داده شده، دارای یک ارجاع جدید است یا خیر؟ shallow equality checks فقط به معنای مقایسهی خاصیت به خاصیت شیء بازگشت داده شده، با نمونهی قبلی است. اما زمانیکه از useSelector hook استفاده میکنیم، با بازگشت یک شیء جدید، یعنی یک ارجاع جدید را خواهیم داشت و ... این یعنی اجبار به رندر مجدد کامپوننتها. به همین جهت در این حالت تعداد بار رندر کامپوننتها افزایش یافتهاست، چون خروجی reducerهای تعریف شدهی در برنامه، همیشه یک شیء جدید را بازگشت میدهند.
برای رفع این مشکل میتوان از پارامتر دوم متد useSelector که روش مقایسهی اشیاء را مشخص میکند، استفاده کرد:
استفاده از shallowEqual در اینجا سبب خواهد شد تا بجای مقایسهی ارجاعات اشیاء (که همیشه متفاوت خواهند بود؛ چون هربار شیء جدیدی را بازگشت میدهیم)، مقادیر تک تک خواص آنها با هم مقایسه شوند.
با اضافه کردن پارامتر shallowEqual به کامپوننتهای HooksPosts و HooksCounter، دیگر با کلیک بر روی دکمهی fetch posts، کار رندر مجدد کامپوننت شمارشگر، رخ نمیدهد.
یک نکته: روش دیگر مشاهدهی تعداد بار رندر شدن کامپوننتها، استفاده از افزونهی react dev tools و مراجعه به برگهی profiler آن است. روی دکمهی record آن کلیک کرده و سپس اندکی با برنامه کار کنید. اکنون کار ضبط را متوقف نمائید، تا نتیجهی نهایی نمایش داده شود.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-redux-mobx-part05.zip
- با استفاده از Redux، یک شیء سراسری state، کار مدیریت state تمام برنامه را به عهده میگیرد که به آن «single source of truth» نیز گفته میشود. البته هرچند میتوان کامپوننتهایی را هم در این بین داشت که state خاص خودشان را داشته باشند و آنرا در این شیء سراسری ذخیره نکنند.
- در حین کار با Redux، تنها راه تغییر شیء سراسری state آن، صدور رخدادهایی هستند که در اینجا اکشن نامیده میشوند. یک اکشن شیءای است که بیان میکند چه چیزی قرار است تغییر کند.
- برای ساده سازی ساخت این اشیاء میتوان متدهایی را به نام action creators ایجاد کرد.
- اگر این متدهای action creator را توسط متد store.dispatch فراخوانی کنیم، سبب dispatch شیء اکشن، به یک تابع Reducer متناظری خواهند شد. این تابع Reducer است که قسمتی از state را که متناظر با نوع اکشن رسیدهاست، تغییر میدهد. در این حالت اگر اکشن رسیده، نوع مدنظری را نداشته باشد، خروجی تابع Reducer، همان state اصلی و بدون تغییر خواهد بود.
- Reducerها توابعی خالص هستند و نباید به همراه اثرات جانبی باشند (هر نوع تعاملی با دنیای خارج از تابع جاری) و همچنین نباید شیء state را نیز مستقیما تغییر دهند. این توابع باید یک کپی تغییر یافتهی از state را در صورت نیاز بازگشت دهند.
- برای مدیریت بهتر برنامه میتوان چندین تابع Reducer را بر اساس نوعهای اکشنهای ویژهای، پیاده سازی کرد. سپس با ترکیب آنها، یک شیء rootReducer ایجاد میشود.
- در نهایت در الگوی Redux، یک مخزن یا store تعریف خواهد شد که تمام این اجزاء را مانند rootReducer و میانافزارهای تعریف شده مانند Thunk، در کنار هم قرار میدهد و امکان dispatch اکشنها را میسر میکند.
- اکنون برای استفادهی از Redux در یک برنامهی React، نیاز است کامپوننت ریشهی برنامه را توسط کامپوننت Provider آن محصور کرد تا قسمتهای مختلف برنامه بتوانند با امکانات مخزن Redux، کار کرده و با آن ارتباط برقرار کنند.
- قسمت آخر این اتصال جائی است که کامپوننتهای اصلی برنامه، توسط یک کامپوننت دربرگیرنده که Container نامیده میشود، توسط متد connect کتابخانهی react-redux محصور میشوند. به این ترتیب این کامپوننتها میتوانند state و خواص مورد نیاز خود را از طریق props دریافت کرده (mapStateToProps) و یا رویدادها را به سمت store، ارسال کنند (mapDispatchToProps).
از زمان React 16.8، مفهوم جدیدی به نام React Hooks معرفی شد که تعدادی از مهمترینهای آنها را در سری «React 16x» بررسی کردیم. توسط Hooks، کامپوننتهای تابعی React اکنون میتوانند به local state خود دسترسی پیدا کنند و یا با دنیای خارج ارتباط برقرار کنند. پس از آن سایر کتابخانههای نوشته شدهی برای React نیز شروع به انطباق خود با این الگوی جدید کردهاند؛ برای مثال کتابخانهی react-redux v1.7 نیز به همراه تعدادی Hook، جهت ساده سازی آخرین قسمتی است که در اینجا بیان شد، تا بتوانند راه حل دومی برای اتصال کامپوننتها و دربرگیری آنها باشند که در ادامه جزئیات آنها را بررسی خواهیم کرد.
بررسی useSelector Hook
useSelector Hook که توسط کتابخانهی react-redux ارائه میشود، معادل بسیار نزدیک تابع mapStateToProps مورد استفادهی در متد connect است. برای مثال در قسمت قبل، دربرگیرندهی کامپوننت Posts در فایل src\containers\Posts.js، یک چنین محتوایی را دارد:
import { connect } from "react-redux"; import Posts from "../components/Posts"; const mapStateToProps = state => { console.log("PostsContainer->mapStateToProps", state); return { ...state.postsReducer }; }; export default connect(mapStateToProps)(Posts);
پیشتر امضای کامپوننت تابعی Posts واقع در فایل src\components\Posts.jsx، به صورت زیر تعریف شده بود که سه خاصیت را از طریق props دریافت میکرد:
const Posts = ({ posts, loading, error }) => { return ( // ...
اکنون فایل جدید src\components\HooksPosts.jsx را ایجاد کرده و ابتدا و امضای کامپوننت تابعی Posts را به صورت زیر تغییر میدهیم:
import { useSelector } from "react-redux"; // ... const HooksPosts = () => { const { posts, loading, error } = useSelector(state => state.postsReducer); return ( // ...
یک نکته: خروجی تابع mapStateToProps همواره باید یک شیء باشد، اما چنین محدودیتی در مورد تابع useSelector وجود ندارد و در صورت نیاز میتوان تنها مقدار یک خاصیت از یک شیء را نیز بازگشت داد.
این کامپوننت، هیچ تغییر دیگری را نیاز ندارد و اگر اکنون به فایل src\App.js مراجعه کنیم، میتوان دربرگیرندهی کامپوننت Posts را:
import PostsContainer from "./containers/Posts"; function App() { return ( <main className="container"> <PostsContainer /> </main> ); }
import HooksPosts from "./components/HooksPosts"; function App() { return ( <main className="container"> <HooksPosts /> </main> ); }
بررسی useDispatch Hook
تا اینجا موفق شدیم متد mapStateToProps را با useSelector Hook جایگزین کنیم. مرحلهی بعد، جایگزین کردن mapDispatchToProps با هوک دیگری به نام useDispatch است. برای مثال در قسمت قبل، دربرگیرندهی کامپوننت FetchPosts در فایل src\containers\FetchPosts.js، چنین تعریفی را دارد:
import { connect } from "react-redux"; import { fetchPostsAsync } from "../actions"; import FetchPosts from "../components/FetchPosts"; const mapDispatchToProps = { fetchPostsAsync }; export default connect(null, mapDispatchToProps)(FetchPosts);
const FetchPosts = ({ fetchPostsAsync }) => {
import React from "react"; import { useDispatch } from "react-redux"; import { fetchPostsAsync } from "../actions"; const HooksFetchPosts = () => { const dispatch = useDispatch(); return ( <section className="card mt-5"> <div className="card-header text-center"> <button className="btn btn-primary" onClick={() => dispatch(fetchPostsAsync())} > Fetch Posts </button> </div> </section> ); }; export default HooksFetchPosts;
با این تغییر نیز میتوان به فایل src\App.js مراجعه کرد و المان قبلی FetchPostsContainer را که از ماژول containers/FetchPosts تامین میشد، به نحو متداولی با همان کامپوننت جدید HooksFetchPosts، تعویض کرد:
import HooksFetchPosts from "./components/HooksFetchPosts"; import HooksPosts from "./components/HooksPosts"; // ... function App() { return ( <main className="container"> <HooksFetchPosts /> <HooksPosts /> </main> ); }
کامپوننت شمارشگر را در قسمت سوم این سری بررسی و تکمیل کردیم. اکنون قصد داریم فایل تامین کنندهی آنرا که به صورت زیر در فایل src\containers\Counter.js تعریف شده:
import { connect } from "react-redux"; import { decrementValue, incrementValue } from "../actions"; import Counter from "../components/counter"; const mapStateToProps = (state, ownProps) => { console.log("CounterContainer->mapStateToProps", { state, ownProps }); return { count: state.counterReducer.count }; }; const mapDispatchToProps = { incrementValue, decrementValue }; export default connect(mapStateToProps, mapDispatchToProps)(Counter);
class Counter extends Component { render() { console.log("Counter->props", this.props); const { //counterReducer: { count }, count, incrementValue, decrementValue } = this.props;
import React from "react"; import { useDispatch, useSelector } from "react-redux"; import { decrementValue, incrementValue } from "../actions"; const HooksCounter = ({ prop1 }) => { const { count } = useSelector(state => { console.log("HooksCounter->useSelector", { state, prop1 }); return { count: state.counterReducer.count }; }); const dispatch = useDispatch(); return ( // ...
- اینبار دو action creator مورد استفادهی در متدهای + و - را از ماژول action دریافت کردهایم تا توسط useDispatch مورد استفاده قرار گیرند.
- همچنین دیگر نیازی به ذکر (state, ownProps) نیست. مقدار ownProps، همان props معمولی است که به کامپوننت ارسال میشود که برای مثال اینبار نام prop1 را دارد؛ چون هنگامیکه المان کامپوننت HooksCounter را درج و معرفی میکنیم، توسط کامپوننت دیگری محصور نشدهاست. تامین آن نیز در فایل src\App.js با درج متداول نام المان کامپوننت HooksCounter و ذکر ویژگی سفارشی prop1 صورت میگیرد:
import HooksCounter from "./components/HooksCounter"; //... function App() { const prop1 = 123; return ( <main className="container"> <HooksCounter prop1={prop1} /> </main> ); }
import React from "react"; import { useDispatch, useSelector } from "react-redux"; import { decrementValue, incrementValue } from "../actions"; const HooksCounter = ({ prop1 }) => { const { count } = useSelector(state => { console.log("HooksCounter->useSelector", { state, prop1 }); return { count: state.counterReducer.count }; }); const dispatch = useDispatch(); 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={() => dispatch(incrementValue())} > + </button> <button className="btn btn-secondary btn-sm m-2" onClick={() => dispatch(decrementValue())} > - </button> <button className="btn btn-danger btn-sm">Reset</button> </div> </div> </section> ); }; export default HooksCounter;
مشکل! با استفاده از useSelector، تعداد رندرهای مجدد کامپوننتهای برنامه افزایش یافتهاست!
برنامهی جاری را پس از این تغییرات اجرا کنید. با هر بار کلیک بر روی دکمهی fetch posts، حتی کامپوننت شمارشگر درج شدهی در صفحه که ربطی به آن ندارد نیز رندر مجدد میشود! چرا؟ (این مورد را با مشاهدهی کنسول توسعه دهندگان مرورگر میتوانید مشاهده کنید. در ابتدای متد رندر هر کدام از کامپوننتها، یک console.log قرار داده شدهاست)
زمانیکه اکشنی dispatch میشود، useSelector hook با استفاده از مقایسهی ارجاعات اشیاء (strict === reference check)، کار مقایسهی مقدار قبلی و مقدار جدید را انجام میدهد. اگر اینها متفاوت باشند، کامپوننت را مجبور به رندر مجدد میکند. این مورد مهمترین تفاوت بین useSelector hook و متد connect است. متد connect از روش shallow equality checks برای مقایسهی نتایج حاصل از mapStateToProps و تصمیم در مورد رندر مجدد استفاده میکند. اما این مقایسهها چه تفاوتی با هم دارند؟
در حالت mapStateToProps، مهم نیست که شیء بازگشت داده شده، دارای یک ارجاع جدید است یا خیر؟ shallow equality checks فقط به معنای مقایسهی خاصیت به خاصیت شیء بازگشت داده شده، با نمونهی قبلی است. اما زمانیکه از useSelector hook استفاده میکنیم، با بازگشت یک شیء جدید، یعنی یک ارجاع جدید را خواهیم داشت و ... این یعنی اجبار به رندر مجدد کامپوننتها. به همین جهت در این حالت تعداد بار رندر کامپوننتها افزایش یافتهاست، چون خروجی reducerهای تعریف شدهی در برنامه، همیشه یک شیء جدید را بازگشت میدهند.
برای رفع این مشکل میتوان از پارامتر دوم متد useSelector که روش مقایسهی اشیاء را مشخص میکند، استفاده کرد:
import React from "react"; import { shallowEqual, useSelector } from "react-redux"; import Post from "./Post"; const HooksPosts = () => { const { posts, loading, error } = useSelector( state => state.postsReducer, shallowEqual ); console.log("render HooksPosts"); return ( // ...
با اضافه کردن پارامتر shallowEqual به کامپوننتهای HooksPosts و HooksCounter، دیگر با کلیک بر روی دکمهی fetch posts، کار رندر مجدد کامپوننت شمارشگر، رخ نمیدهد.
یک نکته: روش دیگر مشاهدهی تعداد بار رندر شدن کامپوننتها، استفاده از افزونهی react dev tools و مراجعه به برگهی profiler آن است. روی دکمهی record آن کلیک کرده و سپس اندکی با برنامه کار کنید. اکنون کار ضبط را متوقف نمائید، تا نتیجهی نهایی نمایش داده شود.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-redux-mobx-part05.zip
یک برنامهی Angular، از گروهی از کامپوننتها تشکیل میشود؛ برای مثال یک کامپوننت App وجود دارد که آن نیز از تعدادی کامپوننت مختلف تشکیل میشود. ماژولها کار سازماندهی و بسته بندی این کامپوننتها را انجام میدهند و با بزرگتر شدن برنامه میتوان قسمتهای مختلف را در ماژولهای متفاوتی قرار داد. مزایای این روش به شرح زیر هستند:
- بهبود کپسوله سازی قسمتهای مختلف برنامه با بسته بندی آنها در ماژولهای متفاوت
- فراهم آوردن امکان lazy loading و بهبود کارآیی برنامه
انواع ماژولهای توصیه شدهی در برنامههای Angular
منهای App Module پیشفرض یک برنامههای Angular، ایجاد سه نوع ماژول دیگر نیز در جهت سازماندهی اینگونه برنامهها توصیه میشوند:
- Core Module
هدف از آن فراهم آوردن سرویسهای Singleton اشتراکی بین کامپوننتها و ماژولهای مختلف برنامه است. علت اینجا است که سیستم تزریق وابستگیهای Angular، به ازای هر ماژولی که Lazy loaded باشد، سرویس تزریقی در آنها را مجددا وهله سازی میکند. به همین جهت نیاز است تک ماژول اختصاصی را برای مدیریت سرویسهایی که نیازی است تنها یکبار در طول عمر برنامه وهله سازی شوند، تدارک ببینیم و Core Module مکان مناسبی برای اینکار است.
همچنین Code Module باید شامل کامپوننتهایی در سطح برنامه باشد. دراینجا منظور از «در سطح برنامه»، کامپوننتهایی که قرار است در بین تمام ماژولها به اشتراک گذاشته شوند، نیست. منظور تنها کامپوننتهایی هستند که در App Component اصلی برنامه قرار است استفاده شوند؛ مانند منوی راهبری بالای سایت.
- Shared Module
هدف از آن مدیریت و بسته بندی کامپوننتها، دایرکتیوها و Pipes اشتراکی بین تمام اجزای برنامه است. برای مثال کامپوننت «لطفا منتظر بمانید ...» اگر قرار است در تمام قسمتهای برنامه استفاده شود، نیاز است در Shared Module تعریف شود. از این جهت که در یک برنامهی Angular نمیتوان یک کامپوننت را بین دو ماژول مختلف به اشتراک گذاشت. به همین جهت نیاز است یک مکان مرکزی برای تعریف این کامپوننتهای اشتراکی ایجاد شود و سپس این تک ماژول را در قسمتهای مختلف برنامه، بدون مشکل مورد استفاده قرار داد.
- Feature Module
این ماژولها به ازای هر ویژگی برنامه ایجاد شده و کامپوننتها، سرویسها، دایرکتیوها و Pipes اختصاصی آن ویژگی را بسته بندی میکنند.
ایجاد Core Module
فرض کنید میخواهید اطلاعات کاربر جاری لاگین شده را در طول عمر برنامه نگهداری کنید و از آن در تمام قسمتهای برنامه استفاده نمائید. یک چنین سرویسی نیاز است دارای طول عمر Singleton باشد و تنها یکبار وهله سازی شود تا اطلاعات کاربر جاری از دست نرود. به همین جهت بهترین مکان تعریف این سرویس، در Core Module است.
برای این منظور در ساختار برنامهی خود، پوشهی جدید src\app\core را ایجاد میکنیم. سپس فایل core.module.ts را به صورت ذیل در آن تعریف خواهیم کرد:
- CoreModule در ابتدا تنها CommonModule و RouterModule را در صورت نیاز import میکند.
- سپس سرویسهای اشتراکی و Singleton برنامه در قسمت providers آن قرار میگیرند.
- در اینجا همچنین دو کامپوننت منو که توسط app.component.ts مورد استفاده قرار میگیرند نیز import شدهاند.
- فایلهای account-menu.component.ts، nav-bar.component.ts و user-repository.service.ts نیز به درون پوشهی src\app\core منتقل خواهند شد (به همراه تمام فایلهای html و css متناظر با آنها).
- اگر دقت کنید، قسمت exports این ماژول نیز مقدار دهی شدهاست. چون این کامپوننتها قرار است خارج از این ماژول و در AppModule استفاده شوند، نیاز است آنها را به صورت خروجی نیز معرفی کنیم.
اکنون جهت استفادهی از این قابلیتها، تنها کافی است تعریف CoreModule را به AppModule در فایل app.module.ts اضافه کنیم:
ایجاد Shared Modules
در Shared Module اجزایی را قرار خواهیم داد که قرار است در بیش از یک ماژول مورد استفاده قرار گیرند. به همین جهت در ساختار برنامهی خود، پوشهی جدید src\app\shared را ایجاد میکنیم. سپس در آن، ماژول جدید shared.module.ts را ایجاد خواهیم کرد:
ساختار این ماژول نیز شبیه به Core Module است. ابتدای CommonModule به آن import شدهاست. سپس کامپوننتهایی که قرار است در بین سایر ماژولهای سایت به اشتراک گذاشته شوند (برای مثال یک کامپوننت Loading Spinner فرضی)، در هر دو قسمت declarations و exports این ماژول اشتراکی قرار میگیرند. همچنین فایل loading-spinner.component.ts و تمام اجزای وابستهی به آن نیز به پوشهی src\app\shared منتقل میشوند.
از این جهت که اجزای خروجی این ماژول قرار است در Feature Moduleها استفاده شوند، CommonModule مورد استفادهی در آنها نیز در قسمت exports ذکر شدهاست.
اکنون جهت استفادهی از این قابلیتها، تنها کافی است تعریف SharedModule را به AppModule در فایل app.module.ts اضافه کنیم:
ایجاد Feature Modules
این مورد نکتهی ویژهای را به همراه ندارد و همانند ایجاد سایر ماژولهای برنامهاست. برای مثال ویژگی مدیریت کاربران، به همراه تمام اجزای آن درون ماژول کاربران قرار میگیرد و به همین ترتیب برای سایر ویژگیهای دیگر برنامه. ایجاد و مدیریت اینگونه ماژولها توسط Angular CLI بسیار سادهاست:
دستور اول ایجاد ماژول جدید users، پوشهی مرتبط با آن و همچنین به روز رسانی فایل app.module را به صورت خودکار انجام میدهد.
دستور دوم نیز کامپوننتی را به این ماژول اضافه میکند؛ به همراه به روز رسانی تعاریف این ماژول.
فقط در اینجا SharedModule ایی را که پیشتر اضافه کردیم، به قسمت imports آن اضافه میکنیم:
- بهبود کپسوله سازی قسمتهای مختلف برنامه با بسته بندی آنها در ماژولهای متفاوت
- فراهم آوردن امکان lazy loading و بهبود کارآیی برنامه
انواع ماژولهای توصیه شدهی در برنامههای Angular
منهای App Module پیشفرض یک برنامههای Angular، ایجاد سه نوع ماژول دیگر نیز در جهت سازماندهی اینگونه برنامهها توصیه میشوند:
- Core Module
هدف از آن فراهم آوردن سرویسهای Singleton اشتراکی بین کامپوننتها و ماژولهای مختلف برنامه است. علت اینجا است که سیستم تزریق وابستگیهای Angular، به ازای هر ماژولی که Lazy loaded باشد، سرویس تزریقی در آنها را مجددا وهله سازی میکند. به همین جهت نیاز است تک ماژول اختصاصی را برای مدیریت سرویسهایی که نیازی است تنها یکبار در طول عمر برنامه وهله سازی شوند، تدارک ببینیم و Core Module مکان مناسبی برای اینکار است.
همچنین Code Module باید شامل کامپوننتهایی در سطح برنامه باشد. دراینجا منظور از «در سطح برنامه»، کامپوننتهایی که قرار است در بین تمام ماژولها به اشتراک گذاشته شوند، نیست. منظور تنها کامپوننتهایی هستند که در App Component اصلی برنامه قرار است استفاده شوند؛ مانند منوی راهبری بالای سایت.
- Shared Module
هدف از آن مدیریت و بسته بندی کامپوننتها، دایرکتیوها و Pipes اشتراکی بین تمام اجزای برنامه است. برای مثال کامپوننت «لطفا منتظر بمانید ...» اگر قرار است در تمام قسمتهای برنامه استفاده شود، نیاز است در Shared Module تعریف شود. از این جهت که در یک برنامهی Angular نمیتوان یک کامپوننت را بین دو ماژول مختلف به اشتراک گذاشت. به همین جهت نیاز است یک مکان مرکزی برای تعریف این کامپوننتهای اشتراکی ایجاد شود و سپس این تک ماژول را در قسمتهای مختلف برنامه، بدون مشکل مورد استفاده قرار داد.
- Feature Module
این ماژولها به ازای هر ویژگی برنامه ایجاد شده و کامپوننتها، سرویسها، دایرکتیوها و Pipes اختصاصی آن ویژگی را بسته بندی میکنند.
ایجاد Core Module
فرض کنید میخواهید اطلاعات کاربر جاری لاگین شده را در طول عمر برنامه نگهداری کنید و از آن در تمام قسمتهای برنامه استفاده نمائید. یک چنین سرویسی نیاز است دارای طول عمر Singleton باشد و تنها یکبار وهله سازی شود تا اطلاعات کاربر جاری از دست نرود. به همین جهت بهترین مکان تعریف این سرویس، در Core Module است.
برای این منظور در ساختار برنامهی خود، پوشهی جدید src\app\core را ایجاد میکنیم. سپس فایل core.module.ts را به صورت ذیل در آن تعریف خواهیم کرد:
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { UserRepositoryService } from './user-repository.service'; import { NavBarComponent } from './nav-bar.component'; import { AccountMenuComponent } from './account-menu.component'; @NgModule({ imports: [ CommonModule, RouterModule ], exports: [ NavBarComponent, AccountMenuComponent ], declarations: [ NavBarComponent, AccountMenuComponent ], providers: [ UserRepositoryService ] }) export class CoreModule { };
- سپس سرویسهای اشتراکی و Singleton برنامه در قسمت providers آن قرار میگیرند.
- در اینجا همچنین دو کامپوننت منو که توسط app.component.ts مورد استفاده قرار میگیرند نیز import شدهاند.
- فایلهای account-menu.component.ts، nav-bar.component.ts و user-repository.service.ts نیز به درون پوشهی src\app\core منتقل خواهند شد (به همراه تمام فایلهای html و css متناظر با آنها).
- اگر دقت کنید، قسمت exports این ماژول نیز مقدار دهی شدهاست. چون این کامپوننتها قرار است خارج از این ماژول و در AppModule استفاده شوند، نیاز است آنها را به صورت خروجی نیز معرفی کنیم.
اکنون جهت استفادهی از این قابلیتها، تنها کافی است تعریف CoreModule را به AppModule در فایل app.module.ts اضافه کنیم:
import { CoreModule } from "./core/core.module"; @NgModule({ imports: [ //... CoreModule, //... RouterModule.forRoot(appRoutes) ], //... }) export class AppModule { }
ایجاد Shared Modules
در Shared Module اجزایی را قرار خواهیم داد که قرار است در بیش از یک ماژول مورد استفاده قرار گیرند. به همین جهت در ساختار برنامهی خود، پوشهی جدید src\app\shared را ایجاد میکنیم. سپس در آن، ماژول جدید shared.module.ts را ایجاد خواهیم کرد:
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { LoadingSpinnerComponent } from './loading-spinner.component'; @NgModule({ imports: [ CommonModule ], declarations: [ LoadingSpinnerComponent ], exports: [ LoadingSpinnerComponent, CommonModule ], providers: [ ] }) export class SharedModule { };
از این جهت که اجزای خروجی این ماژول قرار است در Feature Moduleها استفاده شوند، CommonModule مورد استفادهی در آنها نیز در قسمت exports ذکر شدهاست.
اکنون جهت استفادهی از این قابلیتها، تنها کافی است تعریف SharedModule را به AppModule در فایل app.module.ts اضافه کنیم:
import { CoreModule } from "./core/core.module"; import { SharedModule } from "./shared/shared.module"; @NgModule({ imports: [ //... CoreModule, SharedModule, //... RouterModule.forRoot(appRoutes) ], //... }) export class AppModule { }
ایجاد Feature Modules
این مورد نکتهی ویژهای را به همراه ندارد و همانند ایجاد سایر ماژولهای برنامهاست. برای مثال ویژگی مدیریت کاربران، به همراه تمام اجزای آن درون ماژول کاربران قرار میگیرد و به همین ترتیب برای سایر ویژگیهای دیگر برنامه. ایجاد و مدیریت اینگونه ماژولها توسط Angular CLI بسیار سادهاست:
> ng g m users -m app.module --routing > ng g c users/users-list
دستور دوم نیز کامپوننتی را به این ماژول اضافه میکند؛ به همراه به روز رسانی تعاریف این ماژول.
فقط در اینجا SharedModule ایی را که پیشتر اضافه کردیم، به قسمت imports آن اضافه میکنیم:
import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { SharedModule } from '../shared/shared.module'; import { UsersListComponent } from './users-list.component'; @NgModule({ imports: [ RouterModule, SharedModule ], declarations: [ UsersListComponent ], exports: [ ], providers: [ ] }) export class UsersModule { };