مسیرراهها
بازخوردهای دوره
بوت استرپ (نگارش 3) چیست؟
- همانطور که عنوان شد، در سایت اصلی دریافت بوت استرپ، امکان سفارشی سازی و کار پویا با نگارش LESS آن وجود دارد. برای مثال قسمت تغییر پویای پیش فرضهای navbar در اینجا
- اگر خودتان مستقیما میخواهید این موارد را تغییر دهید، کلیه فایلهای LESS آن در مخزن اصلی کدهای بوت استرپ موجود هستند. در اینجا . برای نمونه فایل navbar.less آن.
اگر به فایلهای less آن دقت کنید، تمام آنها پارامتری هستند و هیچکدام دارای مقدار پیش فرضی نیستند. مقادیر پیش فرض مورد استفاده در فایل variables.less تعریف شدهاند. برای تغییر کلی فایل CSS نهایی فقط کافی است که مقادیر این فایل را تغییر دهید.
فایل نهایی که سبب کامپایل تمام تغییرات خواهد شد، فایل bootstrap.less است.
- استفاده از فایلهای LESS در نگارشهای جدید VS.NET سادهاست و به صورت توکار و خودکار، تغییرات را کامپایل کرده و فایل CSS نهایی را تولید میکند:
روی فایل bootstrap.less دوبار کلیک کنید (تمام فایلهای پوشه less سورس بوت استرپ به VS.NET اضافه شدهاند). اندکی صبر کنید تا کار کامپایل تمام شود. در صفحهای که باز میشود، دو ستون قابل مشاهده است. ستون سمت چپ، فایل less اصلی است و ستون سمت راست، خروجی CSS کامپایل شده نهایی.
- اگر خودتان مستقیما میخواهید این موارد را تغییر دهید، کلیه فایلهای LESS آن در مخزن اصلی کدهای بوت استرپ موجود هستند. در اینجا . برای نمونه فایل navbar.less آن.
اگر به فایلهای less آن دقت کنید، تمام آنها پارامتری هستند و هیچکدام دارای مقدار پیش فرضی نیستند. مقادیر پیش فرض مورد استفاده در فایل variables.less تعریف شدهاند. برای تغییر کلی فایل CSS نهایی فقط کافی است که مقادیر این فایل را تغییر دهید.
فایل نهایی که سبب کامپایل تمام تغییرات خواهد شد، فایل bootstrap.less است.
- استفاده از فایلهای LESS در نگارشهای جدید VS.NET سادهاست و به صورت توکار و خودکار، تغییرات را کامپایل کرده و فایل CSS نهایی را تولید میکند:
روی فایل bootstrap.less دوبار کلیک کنید (تمام فایلهای پوشه less سورس بوت استرپ به VS.NET اضافه شدهاند). اندکی صبر کنید تا کار کامپایل تمام شود. در صفحهای که باز میشود، دو ستون قابل مشاهده است. ستون سمت چپ، فایل less اصلی است و ستون سمت راست، خروجی CSS کامپایل شده نهایی.
فایلهای تصویر که روی سایت آپلود میشوند، با مشکلات متعددی ممکن است روبرو شوند. آپلود فایلهایی با پسوند یا محتوای غیرمجاز، آپلود فایلهایی با حجم غیرمجاز و آپلود فایلهایی با ابعاد غیرمجاز از جمله مشکلاتی هستند که بسیاری مواقع در هنگام طراحی سایت و برنامهنویسی وب با آن روبرو هستیم.
اعتبارسنجی فایلهای تصویر که کاربر به سایت ارسال میکند، لازم است تا در سمت سرور انجام شود. ولی برای افزایش کارایی و بهبود تجربه کاربری، میتواند بخشی از اعتبارسنجی را به صورت موازی در سمت مشتری یا فرانتاند نیز به انجام رسانید.
اعتبارسنجی فایلهای تصویر که کاربر به سایت ارسال میکند، لازم است تا در سمت سرور انجام شود. ولی برای افزایش کارایی و بهبود تجربه کاربری، میتواند بخشی از اعتبارسنجی را به صورت موازی در سمت مشتری یا فرانتاند نیز به انجام رسانید.
در ادامه قسمت اول به برخی دیگر از قابلیتهای جدید VisualStudio.NET 2012 میپردازیم.
بهبود ویرایشگر JavaScript:
ارتقاء ویرایشگر CSS:
ویرایشگر CSS هم در نوع خود بسیار بهتر از پیش عمل میکند. از قابلیتهای جدید آن میتوان به Color-Picker اشاره داشت. همچنین امکان Comment و Un-Comment کردن کدها با اضافه شدن دکمه هایی در نوار ابزار آسانتر شده است.
انتخاب مرورگر، این بار در Toolbar:
اضافه شدن امکان انتخاب مرورگر در نوار ابزار هم در نوع خود جالب است و موجب خوش دستی بیش از پیش VS.NET شده است:
مطالب
AngularJS #3
در این مقاله مفاهیم انقیاد داده (Data Binding)، تزریق وابستگی (Dependency Injection)،هدایت گرها (Directives) و سرویسها را بررسی خواهیم کرد و از مقالهی آینده، به بررسی ویژگیها و امکانات AngularJS در قالب مثال خواهیم پرداخت.
انقیاد داده (Data Binding)
سناریو هایی وجود دارد که در آنها باید اطلاعات قسمتی از صفحه به صورت نامتقارن (Asynchronous) با دادههای دریافتی جدید به روز رسانی شود. روش معمول برای انجام چنین کاری؛ دریافت دادهها از سرور است که عموما به فرم HTML میباشند و جایگزینی آن با بخشی از صفحه که قرار است به روز رسانی شود، اما حالتی را در نظر بگیرید که با داده هایی از جنس JSON طرف هستید و اطلاعات صفحه را با این دادهها باید به روز رسانی کنید. معمولا برای حل چنین مشکلی مجبور به نوشتن مقدار زیادی کد هستید تا بتوانید به خوبی اطلاعات View را به روز رسانی کنید. حتما با خودتان فکر کرده اید که قطعا راهی وجود دارد تا بدون نوشتن کدی، قسمتی از View را به Model متناظر خود نگاشت کرده و این دو به صورت بلادرنگ از تغییرات یکدیگر آگاه شوند. این عمل عموما به مفهوم انقیاد داده شناخته میشود و Angular هم به خوبی از انقیاد داده دوطرفه پشتیبانی میکند.
برای مشاهده این ویژگی در Angular، مثال مقالهی قبل را به کدهای زیر تغییر دهید تا پیغام به صورت پویا توسط کاربر وارد شود:
<!DOCTYPE html> <html ng-app> <head> <title>Sample2</title> </head> <body> <div> <input type="text" ng-model="greeting.text" /> <p>{{greeting.text}}, World!</p> </div> <script src="../Scripts/angular.js"></script> </body> </html>
بدون نیاز به حتی یک خط کد نویسی! با مشخص کردن input به عنوان Model از طریق ng-model، خاصیت greeting.text که در داخل {{ }} مشخص شده را به متن داخل textbox مقید (bind) کردیم. نتیجه میگیریم که جفت آکلود {{ }} برای اعمال Data Binding استفاده میشود.
حال یک دکمه نیز بر روی فرم قرار میدهیم که با کلیک کردن بر روی آن، متن داخل textbox را نمایش دهد.
<!DOCTYPE html> <html ng-app> <head> <title>Sample2</title> </head> <body> <div ng-controller="GreetingController"> <input type="text" ng-model="greeting.text" /> <p>{{greeting.text}}, World!</p> <button ng-click="showData()">Show</button> </div> <script src="../Scripts/angular.js"></script> <script> var GreetingController = function ($scope, $window) { $scope.greeting = { text: "Hello" }; $scope.showData = function () { $window.alert($scope.greeting.text); }; }; </script> </body> </html>
به کمک ng-click، تابع showData به هنگام کلیک شدن، فراخوانی میشود. window$ نیز به عنوان پارامتر کلاس GreetingController مشخص شده است. window$ نیز یکی از سرویسهای پیش فرض تعریف شده توسط Angular است و ما در اینجا در سازندهی کلاس آن را به عنوان وابستگی درخواست کرده ایم تا توسط سیستم تزریق وابستگی توکار، نمونهی مناسب آن در اختیار ما بگذارد. window$ نیز تقریبا معادل شی window است و یکی از دلایل استفاده از آن سادهتر شدن نوشتن آزمونهای واحداست.
حال متنی را داخل textbox نوشته و دکمهی show را فشار دهید. متن نوشته شده را به صورت یک popup مشاهده خواهید کرد.
همچنین شی scope$ نیز نمونهی مناسب آن توسط سیستم تزریق وابستگی Angular، در اختیار Controller قرار میگیرد و نمونهی در اختیار قرارگرفته، برای ارتباط با View Model و سیستم انقیاد داده استفاده میشود.
معمولا انقیاد داده در الگوی طراحی (ModelView-ViewModel(MVVM مطرح است و به این دلیل که این الگوی طراحی به خوبی با الگوی طراحی MVC سازگار است، این امکان در Angular گنجانده شده است.
تزریق وابستگی (Dependency Injection)
تا به این جای کار قطعن بارها و بارها اسم آن را خوانده اید. در مثال فوق، پارامتری با نام scope$ را برای سازندهی کنترلر خود در نظر گرفتیم و ما بدون انجام هیچ کاری نمونهی مناسب آن را که برای انجام اعمال انقیاد داده با viewmodel استفاده میشود را دریافت کردیم. به عنوان مثال، window$ را نیز در سازندهی کلاس کنترلر خود به عنوان یک وابستگی تعریف کردیم و تزریق نمونهی مناسب آن توسط سیستم تزریق وابستگی توکار Angular صورت میگرفت.
اگر با IOC Containerها در زبانی مثل #C کار کرده باشید، قطعا با IOC Container فراهم شده توسط Angular هم مشکلی نخواهید داشت.
اما یک مشکل! در زبانی مثل #C که همهی متغیرهای دارای نوع هستند، IOC Container با استفاده از Reflection، نوع پارامترهای درخواستی توسط سازندهی کلاس را بررسی کرده و با توجه به اطلاعاتی که ما از قبل در دسترس آن قرار داده بودیم، نمونهی مناسب آن را در اختیار در خواست کننده میگذارد.
اما در زبان جاوا اسکریپت که متغیرها دارای نوع نیستند، این کار به چه شکل انجام میگیرد؟
Angular برای این کار از نام پارامترها استفاده میکند. برای مثال Angular از نام پارامتر scope$ میفهمد که باید چه نمونه ای را به کلاس تزریق کند. پس نام پارامترها در سیستم تزریق وابستگی Angular نقش مهمی را ایفا میکنند.
اما در زبان جاوا اسکریپت، به طور پیش فرض امکانی برای به دست آوردن نام پارامترهای یک تابع وجود ندارد؛ پس Angular چگونه نام پارامترها را به دست میآورد؟ جواب در سورس کد Angular و در تابعی به نام annotate نهفته است که اساس کار این تابع استفاده از چهار عبارت با قاعده (Regular Expression) زیر است.
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
تابع annotate تابعی را به عنوان پارامتر دریافت میکند و سپس با فراخواندن متد toString آن، کدهای آن تابع را به شکل یک رشته در میآورد. حال کدهای تابع را که اکنون به شکل یک رشته در دسترس است را با استفاده از عبارات با قاعدهی فوق پردازش میکند تا نام پارامترها را به دست آورد. در ابتدا کامنتهای موجود در تابع را حذف میکند، سپس نام پارامترها را استخراج میکند و با استفاده از "," آنها را جدا میکند و در نهایت نام پارامترها را در یک آرایه باز میگرداند.
استفاده از تزریق وابستگی، امکان نوشتن کدهایی با قابلیت استفاده مجدد و نوشتن سادهتر آزمونهای واحد را فراهم میکند. به خصوص کدهایی که با سرور ارتباط برقرار میکنند را میتوان به یک سرویس انتقال داد و از طریق تزریق وابستگی، از آن در کنترلر استفاده کرد. سپس در آزمونهای واحد میتوان قسمت ارتباط با سرور را با یک نمونه فرضی جایگزین کرد تا برای تست، احتیاجی به راه اندازی یک وب سرور واقعی و یا مرورگر نباشد.
Directives
یکی از مزیتهای Angular این است که قالبها را میتوان با HTML نوشت و این را باید مدیون موتور قدرتمند تبدیل گر DOM بدانیم که در آن گنجانده شده است و به شما این امکان را میدهد تا گرامر HTML را گسترش دهید.
تا به این جای کار با attributeهای زیادی در قالب HTML روبرو شدید که متعلق به HTML نیست. به طور مثال: جفت آکولادها که برای انقیاد داده به کار برده میشود، ng-app که برای مشخص کردن بخشی که باید توسط Angular کامپایل شود، ng-controller که برای مشخص کردن این که کدام بخش از View متعلق به کدام Controller است و ... تمامی Directiveهای پیش فرض Angular هستند.
با استفاده از Directiveها میتوانید عناصر و خاصیتها و حتی رویدادهای سفارشی برای HTML بنویسید؛ اما واقعا چه احتیاجی به تعریف عنصر سفارشی و توسعه گرامر HTML وجود دارد؟
HTML یک زبان طراحی است که در ابتدا برای تولید اسناد ایستا به وجود آمد و هیچ وقت هدفش تولید وب سایتهای امروزی که کاملا پویا هستند نبود. این امر تا جایی پیش رفته است که HTML را از یک زبان طراحی تبدیل به یک زبان برنامه نویسی کرده است و احتیاج به چنین زبانی کاملا مشهود است. به همین دلیل جامعهی وب مفهومی را به نام Web Components مطرح کرده است. Web Components به شما امکان تعریف عناصر HTML سفارشی را میدهد. برای مثال شما یک تگ سفارشی به نام datepicker مینویسید که دارای رفتار و ویژگیهای خاص خود است و به راحتی عناصر HTML رابا استفاده از آن توسعه میدهید. مطمئنا آیندهی وب این گونه است، اما هنوز خیلی از مرورگرها از این ویژگی پشتیبانی نمیکنند.
یکی دیگر از معادلهای Web Componentهای امروز را میتوان ویجتهای jQuery UI دانست. اگر بخواهم تعریفی از ویجت ارائه دهم به این گونه است که یک ویجت؛ کدهای HTML، CSS و javascript مرتبط به هم را کپسوله کرده است. مهمترین مزیت ویجت ها، قابلیت استفادهی مجدد آنهاست، به این دلیل که تمام منطق مورد نیاز را در خود کپسوله کرده است؛ برای مثال ویجت datepicker که به راحتی در برنامههای مختلف بدون احتیاج به نوشتن کدی قابل استفاده است.
خب، متاسفانه Web Componentها هنوز در دنیای وب امروزی رایج نشده اند و ویجتها هم آنچنان قدرت Web Componentها را ندارند. خب Angular با استفاده از امکان تعریف Directiveهای سفارشی به صورت cross-browser امکان تعریف عناصر سفارشیه همانند web Componentها را به شما میدهد. حتی به عقیدهی عده ای Directiveها بسیار قدرتمندتر از Web Components عمل میکنند و راحتی کار با آنها بیشتر است.
با استفاده از Directiveها میتوانید عنصر HTML سفارشی مثل </ datepicker>، خاصیت سفارشی مثل ng-controller، رویداد سفارشی مثل ng-click را تعریف کنید و یا حتی حالت و اتفاقات رخ داده در برنامه را زیر نظر بگیرید.
و این یکی از دلایلی است که میگویند Angular دارای ویژگی forward-thinking است.
البته Directiveها یکی از قدرتمندترین امکانات فریم ورک AngularJS است و در آینده به صورت مفصل بر روی آن بحث خواهد شد.
سرویسها در AngularJS
حتما این جمله را در هنگام نوشتن برنامهها با الگوی طراحی MVC بارها و بارها شنیده اید که در Controllerها نباید منطق تجاری و پیچیده ای را پیاده سازی کرد و باید به قسمتهای دیگری به نام سرویسها منتقل شوند و سپس در سازندهی کلاس کنترلر به عنوان پارامتر تعریف شوند تا توسط Angular نمونهی مناسب آن به کنترلر تزریق شود. Controllerها نباید پیاده کنندهی هیچ منطق تجاری و یا اصطلاحا business برنامه باشد و باید از لایهی سرویس استفاده کنند و تنها وظیفهی کنترلر باید مشخص کردن انقیاد داده و حالت برنامه باشد.
دلیل استفاده از سرویسها در کنترلر ها، نوشتن سادهتر آزمونهای واحد و استفادهی مجدد از سرویسها در قسمتهای مختلف پروژه و یا حتی پروژههای دیگر است.
معمولا اعمال مرتبط در ارتباط با سرور را در سرویسها پیاده سازی میکنند تا بتوان در موقع نوشتن آزمونهای واحد یک نمونهی فرضی را خودمان ساخته و آن را به عنوان وابستگی به کنترلری که در حال تست آن هستیم تزریق کنیم، در غیر این صورت احتیاج به راه اندازی یک وب سرور واقعی برای نوشتن آزمونهای واحد و در نتیجه کند شدن انجام آزمون را در بر دارد. قابلیت استفادهی مجدد سرویس هم به این معناست که منطق پیاده سازی شده در آن نباید ربطی به رابط کاربری و ... داشته باشد. برای مثال یک سرویس به نام userService باید دارای متد هایی مثل دریافت لیست کاربران، افزودن کاربر و ... باشد و بدیهی است که از این سرویسها میشود در قسمتهای مختلف برنامه استفاده کرد. همچنین سرویسها در Angular به صورت Singleton در اختیار کنترلرها قرار میگیرند و این بدین معناست که یک نمونه از هر سرویس ایجاد شده و به بخشهای مختلف برنامه تزریق میشود.
مفاهیم پایه ای AngularJs به پایان رسید. در مقاله بعدی یک مثال تقریبا کامل را نوشته و با اجزای مختلف Angular بیشتر آشنا میشویم.
با تشکر از مهدی محزونی برای بازبینی مطلب
در مطلب «طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت اول» با ساختار کلی یک پروژهی افزونهی پذیر ASP.NET MVC آشنا شدیم. پس از راه اندازی آن و مدتی کار کردن با این نوع پروژهها، این سؤال پیش خواهد آمد که ... خوب، اگر هر افزونه تصاویر یا فایلهای CSS و JS اختصاصی خودش را بخواهد داشته باشد، چطور؟ موارد عمومی مانند بوت استرپ و جیکوئری را میتوان در پروژهی پایه قرار داد تا تمام افزونهها به صورت یکسانی از آنها استفاده کنند، اما هدف، ماژولار شدن برنامه است و جدا کردن فایلهای ویژهی هر پروژه، از پروژهای دیگر و همچنین بالا بردن سهولت کار تیمی، با شکستن اجزای یک پروژه به صورت افزونههایی مختلف، بین اعضای یک تیم. در این قسمت نحوهی مدفون سازی انواع فایلهای استاتیک افزونهها را درون فایلهای DLL آنها بررسی خواهیم کرد. به این ترتیب دیگر نیازی به ارائهی مجزای آنها و یا کپی کردن آنها در پوشههای پروژهی اصلی نخواهد بود.
مدفون سازی فایلهای CSS و JS هر افزونه درون فایل DLL آن
به solution جاری، یک class library جدید را به نام MvcPluginMasterApp.Common اضافه کنید. از آن جهت قرار دادن کلاسهای عمومی و مشترک بین افزونهها استفاده خواهیم کرد. برای مثال قصد نداریم کلاسهای سفارشی و عمومی ذیل را هربار به صورت مستقیم در افزونهای جدید کپی کنیم. کتابخانهی Common، امکان استفادهی مجدد از یک سری کدهای تکراری را در بین افزونهها میسر میکند.
این پروژه برای کامپایل شدن نیاز به بستهی نیوگت ذیل دارد:
همچنین باید به صورت دستی، در قسمت ارجاعات پروژه، ارجاعی را به اسمبلی استاندارد System.Web نیز به آن اضافه نمائید.
پس از این مقدمات، کلاس ذیل را به این پروژهی class library جدید اضافه کنید:
اگر با سیستم bundling & minification کار کرده باشید، با تعاریفی مانند ("new Bundle("~/Plugin1/Scripts آشنا هستید. سازندهی کلاس Bundle، پارامتر دومی را نیز میپذیرد که از نوع IBundleTransform است. با پیاده سازی اینترفیس IBundleTransform میتوان محل ارائهی فایلهای استاتیک CSS و JS را بجای فایل سیستم متداول و پیش فرض، به منابع مدفون شدهی در اسمبلی جاری هدایت و تنظیم کرد.
کلاس فوق در اسمبلی معرفی شده به آن، توسط متد GetManifestResourceStream به دنبال فایلها و منابع مدفون شده گشته و سپس محتوای آنها را بازگشت میدهد.
اکنون برای استفادهی از آن، به پروژهی MvcPluginMasterApp.Plugin1 مراجعه کرده و ارجاعی را به پروژهی MvcPluginMasterApp.Common فوق اضافه نمائید. سپس در فایل Plugin1.cs، متد RegisterBundles آنرا به نحو ذیل تکمیل کنید:
در اینجا نحوهی کار با کلاس سفارشی EmbeddedResourceTransform را مشاهده میکنید.
ابتدا فایلهای js و سپس فایلهای css برنامه به سیستم Bundling برنامه اضافه شدهاند.
این فایلها به صورت ذیل در پروژه تعریف گردیدهاند:
همانطور که مشاهده میکنید، باید به خواص هر کدام مراجعه کرد و سپس Build action آنها را به embedded resource تغییر داد، تا در حین کامپایل، به صورت خودکار در قسمت منابع اسمبلی ذخیره شوند.
یک نکتهی مهم
اینبار برای مسیردهی منابع، باید بجای / فایل سیستم، از «نقطه» استفاده کرد. زیرا منابع با نامهایی مانند namespace.folder.name در قسمت resources یک اسمبلی ذخیره میشوند:
مدفون سازی تصاویر ثابت هر افزونه درون فایل DLL آن
مجددا به اسمبلی مشترک MvcPluginMasterApp.Common مراجعه کرده و اینبار کلاس جدید ذیل را به آن اضافه کنید:
تصاویر پروژهی افزونه نیز به صورت embedded resource در اسمبلی آن قرار خواهند گرفت. به همین جهت باید سیستم مسیریابی را پس درخواست رسیدهی جهت نمایش تصاویر، به منابع ذخیره شدهی در اسمبلی آن هدایت نمود. اینکار را با پیاده سازی یک IRouteHandler سفارشی، میتوان به نحو فوق مدیریت کرد.
این IRouteHandler، نام و پسوند فایل را دریافت کرده و سپس به قسمت منابع اسمبلی رجوع، فایل مرتبط را استخراج و سپس بازگشت میدهد. همچنین برای کاهش سربار سیستم، امکان کش شدن منابع استاتیک نیز در آن درنظر گرفته شدهاست و هدرهای خاص caching را به صورت خودکار اضافه میکند.
سیستم bundling نیز هدرهای کش کردن را به صورت خودکار و توکار اضافه میکند.
اکنون به تعاریف Plugin1 مراجعه کنید و سپس این IRouteHandler سفارشی را به نحو ذیل به آن معرفی نمائید:
در مسیریابی تعریف شده، تمام درخواستهای رسیدهی به مسیر NewsArea/Images به EmbeddedResourceRouteHandler هدایت میشوند.
مطابق تعریف آن، file و extension به صورت خودکار جدا شده و توسط routeData.Values در متد ProcessRequest کلاس EmbeddedResourceHttpHandler قابل دسترسی خواهند شد.
پسوندهایی که توسط آن بررسی میشوند از نوع png یا jpg تعریف شدهاند. همچنین مدت زمان کش کردن هر منبع استاتیک تصویری به یک ماه تنظیم شدهاست.
استفادهی نهایی از تنظیمات فوق در یک View افزونه
پس از اینکه تصاویر و فایلهای css و js را به صورت embedded resource تعریف کردیم و همچنین تنظیمات مسیریابی و bundling خاص آنها را نیز مشخص نمودیم، اکنون نوبت به استفادهی از آنها در یک View است:
در اینجا نحوهی تعریف فایلهای CSS و JS ارائه شدهی توسط سیستم Bundling را مشاهده میکنید.
همچنین مسیر تصویر مشخص شدهی در آن، اینبار یک NewsArea اضافهتر دارد. فایل اصلی تصویر، در مسیر Images/chart.png قرار گرفتهاست اما میخواهیم این درخواستها را به مسیریابی جدید {NewsArea/Images/{file}.{extension هدایت کنیم. بنابراین نیاز است به این نکته نیز دقت داشت.
اینبار اگر برنامه را اجرا کنیم، میتوان به سه نکته در آن دقت داشت:
الف) alert اجرا شده از فایل js مدفون شده خوانده شدهاست.
ب) رنگ قرمز متن (تگ h2) از فایل css مدفون شده، گرفته شدهاست.
ج) تصویر نمایش داده شده، همان تصویر مدفون شدهی در فایل DLL برنامه است.
و هیچکدام از این فایلها، به پوشههای پروژهی اصلی برنامه، کپی نشدهاند.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
MvcPluginMasterApp-Part2.zip
مدفون سازی فایلهای CSS و JS هر افزونه درون فایل DLL آن
به solution جاری، یک class library جدید را به نام MvcPluginMasterApp.Common اضافه کنید. از آن جهت قرار دادن کلاسهای عمومی و مشترک بین افزونهها استفاده خواهیم کرد. برای مثال قصد نداریم کلاسهای سفارشی و عمومی ذیل را هربار به صورت مستقیم در افزونهای جدید کپی کنیم. کتابخانهی Common، امکان استفادهی مجدد از یک سری کدهای تکراری را در بین افزونهها میسر میکند.
این پروژه برای کامپایل شدن نیاز به بستهی نیوگت ذیل دارد:
PM> install-package Microsoft.AspNet.Web.Optimization
پس از این مقدمات، کلاس ذیل را به این پروژهی class library جدید اضافه کنید:
using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text; using System.Web.Optimization; namespace MvcPluginMasterApp.Common.WebToolkit { public class EmbeddedResourceTransform : IBundleTransform { private readonly IList<string> _resourceFiles; private readonly string _contentType; private readonly Assembly _assembly; public EmbeddedResourceTransform(IList<string> resourceFiles, string contentType, Assembly assembly) { _resourceFiles = resourceFiles; _contentType = contentType; _assembly = assembly; } public void Process(BundleContext context, BundleResponse response) { var result = new StringBuilder(); foreach (var resource in _resourceFiles) { using (var stream = _assembly.GetManifestResourceStream(resource)) { if (stream == null) { throw new KeyNotFoundException(string.Format("Embedded resource key: '{0}' not found in the '{1}' assembly.", resource, _assembly.FullName)); } using (var reader = new StreamReader(stream)) { result.Append(reader.ReadToEnd()); } } } response.ContentType = _contentType; response.Content = result.ToString(); } } }
کلاس فوق در اسمبلی معرفی شده به آن، توسط متد GetManifestResourceStream به دنبال فایلها و منابع مدفون شده گشته و سپس محتوای آنها را بازگشت میدهد.
اکنون برای استفادهی از آن، به پروژهی MvcPluginMasterApp.Plugin1 مراجعه کرده و ارجاعی را به پروژهی MvcPluginMasterApp.Common فوق اضافه نمائید. سپس در فایل Plugin1.cs، متد RegisterBundles آنرا به نحو ذیل تکمیل کنید:
namespace MvcPluginMasterApp.Plugin1 { public class Plugin1 : IPlugin { public EfBootstrapper GetEfBootstrapper() { return null; } public MenuItem GetMenuItem(RequestContext requestContext) { return new MenuItem { Name = "Plugin 1", Url = new UrlHelper(requestContext).Action("Index", "Home", new { area = "NewsArea" }) }; } public void RegisterBundles(BundleCollection bundles) { var executingAssembly = Assembly.GetExecutingAssembly(); // Mostly the default namespace and assembly name are the same var assemblyNameSpace = executingAssembly.GetName().Name; var scriptsBundle = new Bundle("~/Plugin1/Scripts", new EmbeddedResourceTransform(new List<string> { assemblyNameSpace + ".Scripts.test1.js" }, "application/javascript", executingAssembly)); if (!HttpContext.Current.IsDebuggingEnabled) { scriptsBundle.Transforms.Add(new JsMinify()); } bundles.Add(scriptsBundle); var cssBundle = new Bundle("~/Plugin1/Content", new EmbeddedResourceTransform(new List<string> { assemblyNameSpace + ".Content.test1.css" }, "text/css", executingAssembly)); if (!HttpContext.Current.IsDebuggingEnabled) { cssBundle.Transforms.Add(new CssMinify()); } bundles.Add(cssBundle); BundleTable.EnableOptimizations = true; } public void RegisterRoutes(RouteCollection routes) { } public void RegisterServices(IContainer container) { } } }
این فایلها به صورت ذیل در پروژه تعریف گردیدهاند:
همانطور که مشاهده میکنید، باید به خواص هر کدام مراجعه کرد و سپس Build action آنها را به embedded resource تغییر داد، تا در حین کامپایل، به صورت خودکار در قسمت منابع اسمبلی ذخیره شوند.
یک نکتهی مهم
اینبار برای مسیردهی منابع، باید بجای / فایل سیستم، از «نقطه» استفاده کرد. زیرا منابع با نامهایی مانند namespace.folder.name در قسمت resources یک اسمبلی ذخیره میشوند:
مدفون سازی تصاویر ثابت هر افزونه درون فایل DLL آن
مجددا به اسمبلی مشترک MvcPluginMasterApp.Common مراجعه کرده و اینبار کلاس جدید ذیل را به آن اضافه کنید:
using System; using System.Collections.Generic; using System.Reflection; using System.Web; using System.Web.Routing; namespace MvcPluginMasterApp.Common.WebToolkit { public class EmbeddedResourceRouteHandler : IRouteHandler { private readonly Assembly _assembly; private readonly string _resourcePath; private readonly TimeSpan _cacheDuration; public EmbeddedResourceRouteHandler(Assembly assembly, string resourcePath, TimeSpan cacheDuration) { _assembly = assembly; _resourcePath = resourcePath; _cacheDuration = cacheDuration; } IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) { return new EmbeddedResourceHttpHandler(requestContext.RouteData, _assembly, _resourcePath, _cacheDuration); } } public class EmbeddedResourceHttpHandler : IHttpHandler { private readonly RouteData _routeData; private readonly Assembly _assembly; private readonly string _resourcePath; private readonly TimeSpan _cacheDuration; public EmbeddedResourceHttpHandler( RouteData routeData, Assembly assembly, string resourcePath, TimeSpan cacheDuration) { _routeData = routeData; _assembly = assembly; _resourcePath = resourcePath; _cacheDuration = cacheDuration; } public bool IsReusable { get { return false; } } public void ProcessRequest(HttpContext context) { var routeDataValues = _routeData.Values; var fileName = routeDataValues["file"].ToString(); var fileExtension = routeDataValues["extension"].ToString(); var manifestResourceName = string.Format("{0}.{1}.{2}", _resourcePath, fileName, fileExtension); var stream = _assembly.GetManifestResourceStream(manifestResourceName); if (stream == null) { throw new KeyNotFoundException(string.Format("Embedded resource key: '{0}' not found in the '{1}' assembly.", manifestResourceName, _assembly.FullName)); } context.Response.Clear(); context.Response.ContentType = "application/octet-stream"; cacheIt(context.Response, _cacheDuration); stream.CopyTo(context.Response.OutputStream); } private static void cacheIt(HttpResponse response, TimeSpan duration) { var cache = response.Cache; var maxAgeField = cache.GetType().GetField("_maxAge", BindingFlags.Instance | BindingFlags.NonPublic); if (maxAgeField != null) maxAgeField.SetValue(cache, duration); cache.SetCacheability(HttpCacheability.Public); cache.SetExpires(DateTime.Now.Add(duration)); cache.SetMaxAge(duration); cache.AppendCacheExtension("must-revalidate, proxy-revalidate"); } } }
این IRouteHandler، نام و پسوند فایل را دریافت کرده و سپس به قسمت منابع اسمبلی رجوع، فایل مرتبط را استخراج و سپس بازگشت میدهد. همچنین برای کاهش سربار سیستم، امکان کش شدن منابع استاتیک نیز در آن درنظر گرفته شدهاست و هدرهای خاص caching را به صورت خودکار اضافه میکند.
سیستم bundling نیز هدرهای کش کردن را به صورت خودکار و توکار اضافه میکند.
اکنون به تعاریف Plugin1 مراجعه کنید و سپس این IRouteHandler سفارشی را به نحو ذیل به آن معرفی نمائید:
namespace MvcPluginMasterApp.Plugin1 { public class Plugin1 : IPlugin { public void RegisterRoutes(RouteCollection routes) { //todo: add custom routes. var assembly = Assembly.GetExecutingAssembly(); // Mostly the default namespace and assembly name are the same var nameSpace = assembly.GetName().Name; var resourcePath = string.Format("{0}.Images", nameSpace); routes.Insert(0, new Route("NewsArea/Images/{file}.{extension}", new RouteValueDictionary(new { }), new RouteValueDictionary(new { extension = "png|jpg" }), new EmbeddedResourceRouteHandler(assembly, resourcePath, cacheDuration: TimeSpan.FromDays(30)) )); } } }
مطابق تعریف آن، file و extension به صورت خودکار جدا شده و توسط routeData.Values در متد ProcessRequest کلاس EmbeddedResourceHttpHandler قابل دسترسی خواهند شد.
پسوندهایی که توسط آن بررسی میشوند از نوع png یا jpg تعریف شدهاند. همچنین مدت زمان کش کردن هر منبع استاتیک تصویری به یک ماه تنظیم شدهاست.
استفادهی نهایی از تنظیمات فوق در یک View افزونه
پس از اینکه تصاویر و فایلهای css و js را به صورت embedded resource تعریف کردیم و همچنین تنظیمات مسیریابی و bundling خاص آنها را نیز مشخص نمودیم، اکنون نوبت به استفادهی از آنها در یک View است:
@{ ViewBag.Title = "From Plugin 1"; } @Styles.Render("~/Plugin1/Content") <h2>@ViewBag.Message</h2> <div class="row"> Embedded image: <img src="@Url.Content("~/NewsArea/Images/chart.png")" alt="clock" /> </div> @section scripts { @Scripts.Render("~/Plugin1/Scripts") }
همچنین مسیر تصویر مشخص شدهی در آن، اینبار یک NewsArea اضافهتر دارد. فایل اصلی تصویر، در مسیر Images/chart.png قرار گرفتهاست اما میخواهیم این درخواستها را به مسیریابی جدید {NewsArea/Images/{file}.{extension هدایت کنیم. بنابراین نیاز است به این نکته نیز دقت داشت.
اینبار اگر برنامه را اجرا کنیم، میتوان به سه نکته در آن دقت داشت:
الف) alert اجرا شده از فایل js مدفون شده خوانده شدهاست.
ب) رنگ قرمز متن (تگ h2) از فایل css مدفون شده، گرفته شدهاست.
ج) تصویر نمایش داده شده، همان تصویر مدفون شدهی در فایل DLL برنامه است.
و هیچکدام از این فایلها، به پوشههای پروژهی اصلی برنامه، کپی نشدهاند.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
MvcPluginMasterApp-Part2.zip
یک نکتهی تکمیلی در مورد فونتها
عموما فونتها در بستههای اصلی یک چنین مسیرهایی را دارند:
و در حالت پیشفرض این ابزار آنها را به صورت زیر در فایل نهایی تولیدی بازنویسی میکند (بر اساس مسیر نسبی قرارگیری آن در پروژه):
به همین جهت در حین اجرای برنامه پیام یافت نشدن آنها را مشاهده میکنید.
برای غیرفعال کردن این بازنویسی مسیر (بدون نیاز به عمومی کردن مسیر node_modules در کلاس آغازین برنامه)، باید در اینجا adjustRelativePaths را به false تنظیم کنید:
عموما فونتها در بستههای اصلی یک چنین مسیرهایی را دارند:
src: url("../webfonts/fa-brands-400.eot");
src: url("../node_modules/components-font-awesome/webfonts/fa-brands-400.eot");
برای غیرفعال کردن این بازنویسی مسیر (بدون نیاز به عمومی کردن مسیر node_modules در کلاس آغازین برنامه)، باید در اینجا adjustRelativePaths را به false تنظیم کنید:
{ "outputFileName": "wwwroot/css/site.min.css", "inputFiles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "node_modules/bootstrap-rtl/dist/css/bootstrap-rtl.min.css", "node_modules/components-font-awesome/css/fa-solid.min.css", "node_modules/components-font-awesome/css/fontawesome.min.css", "content/custom.css" ], "minify": { "enabled": true, "renameLocals": false, "adjustRelativePaths": false }, "sourceMap": false },
اشتراکها
ASP.NET MVC 5.2.3 Beta منتشر شد
یک نکتهی تکمیلی: امکان نوشتن فایلهای CSS اختصاصی کامپوننتها در برنامههای Blazor
در نکتهی قبلی، در مورد JavaScript Isolation بحث شد؛ یک چنین قابلیتی در مورد فایلهای css. هم وجود دارد و جزو تازههای Blazor 5x است. در این حالت (CSS Isolation) میتوان شیوهنامهای را تهیه کرد که تنها مختص به یک کامپوننت است و بر روی سایر کامپوننتها تاثیری ندارد. این قابلیت نیاز به کدنویسی خاصی نداشته و به سادگی قابل استفادهاست: در این حالت اگر کامپوننتی برای مثال Counter.razor نام دارد، برای ایجاد فایل css مختص به آن، تنها کافی است در کنار فایل آن، فایل Counter.razor.css را ایجاد و تکمیل کرد. به این ترتیب هر شیوهنامهای که در این فایل تنظیم شود، فقط بر روی کامپوننت Counter تاثیرگذار خواهد بود؛ حتی اگر از این کامپوننت در کامپوننتهای دیگر نیز استفاده شود.
برای نمونه شیوهنامهی فوق تنها به المان h1 کامپوننت Counter اعمال میشود و هیچ تاثیری را بر روی سایر کامپوننتهای برنامه نخواهد داشت:
همانطور که مشاهده میکنید، در زمان build برنامه، تمام این شیوهنامههای اختصاصی کامپوننتهای مختلف برنامه، تبدیل به یک فایل خواهند شد (با نام project_name.styles.css) که توسط ویژگیهای HTML ای که به صورت خودکار تولید میشوند، از یکدیگر متمایز خواهند شد.
اگر علاقمند باشیم تا این شیوهنامه به فرزندان کامپوننت Counter نیز به ارث برسد، باید از روش زیر استفاده کرد (استفاده از deep combinator):
در نکتهی قبلی، در مورد JavaScript Isolation بحث شد؛ یک چنین قابلیتی در مورد فایلهای css. هم وجود دارد و جزو تازههای Blazor 5x است. در این حالت (CSS Isolation) میتوان شیوهنامهای را تهیه کرد که تنها مختص به یک کامپوننت است و بر روی سایر کامپوننتها تاثیری ندارد. این قابلیت نیاز به کدنویسی خاصی نداشته و به سادگی قابل استفادهاست: در این حالت اگر کامپوننتی برای مثال Counter.razor نام دارد، برای ایجاد فایل css مختص به آن، تنها کافی است در کنار فایل آن، فایل Counter.razor.css را ایجاد و تکمیل کرد. به این ترتیب هر شیوهنامهای که در این فایل تنظیم شود، فقط بر روی کامپوننت Counter تاثیرگذار خواهد بود؛ حتی اگر از این کامپوننت در کامپوننتهای دیگر نیز استفاده شود.
h1 { color:red; }
همانطور که مشاهده میکنید، در زمان build برنامه، تمام این شیوهنامههای اختصاصی کامپوننتهای مختلف برنامه، تبدیل به یک فایل خواهند شد (با نام project_name.styles.css) که توسط ویژگیهای HTML ای که به صورت خودکار تولید میشوند، از یکدیگر متمایز خواهند شد.
اگر علاقمند باشیم تا این شیوهنامه به فرزندان کامپوننت Counter نیز به ارث برسد، باید از روش زیر استفاده کرد (استفاده از deep combinator):
::deep h1 { color:red; }