گوشهای از Angular 4
const Dashboard = ({ match }) => { return ( <div> <h1>Admin Dashboard</h1> <div className="row"> <div className="col-3"> <SideBar /> </div> <div className="col"> <Route path="/admin/users" component={Users} /> <Route path="/admin/posts" component={Posts} /> </div> </div> </div> ); };
import 'bootstrap-v4-rtl/dist/css/bootstrap-rtl.css'
<div class="row"> <div className="col-md-3">۱</div> <div className="col-md-6">2</div> <div className="col-md-3">3</div> </div>
jQuery Tips #2
در این پست قصد دارم نحوهی کاربا Cookie را با استفاده از jQuery برسی کنم و در پست بعدی یک مثال عملی را برسی میکنیم.
همانطور که میدانید کوکی یکی از اشیاء بسیار مهم برای نگه داری دادهها در بحث وب میباشد که یک فایل متنی است که سمت Client ذخیره میشود. و ما زمانی که از کتابخانه jQuery استفاده میکنیم خیلی مهم است که بدانیم چگونه باید با Cookieها کار کرد.
برای کار با کوکیها در jQuery باید از Plugin های موجود استفاده کرد . برای ایجاد یک Cookie ابتدا فایل jQuery و سپس این کتابخانه را به صفحه مورد نظر اضافه نموده و کد زیر را برای ایجاد یک کوکی مینویسیم
<script src="jquery-1.7.1.min.js" type="text/javascript"></script> <script src="jquery.cookie.js" type="text/javascript"></script> <script type="text/javascript"> $(function () { $.cookie("TestCookie", "Test Cookie By Mohsen Bahrzadeh "); }); </script>
حال پروژه را اجرا میکنیم. و در تصویر زیر مشاهده میکنید که کوکی ما ایجاد شده است
یکی از آیتمهای بسیار مهم در کوکیها تعریف زمان انقضاء کوکی است برای ست کردن تاریخ از کد زیر استفاده میکنیم
$(function () { $.cookie("TestCookie", "Test Cookie By Mohsen Bahrzadeh ", { expires: 7 }); });
$(function () { alert($.cookie("TestCookie")); });
$(function () { $.cookie("TestCookie", null); });
با کلیک بر روی لینک منوی نمایش لیست محصولات، ابتدا قاب خالی لیست محصولات نمایش داده میشود:
سپس بعد از یک ثانیه، شاهد بارگذاری اطلاعات جدول لیست محصولات خواهید بود. این یک ثانیه تاخیر را نیز به عمد توسط منبع داده درون حافظهای برنامه ایجاد کردیم، تا بتوان شرایط دنیای واقعی را شبیه سازی کرد:
InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }),
ارسال اطلاعات ثابت به مسیرهای مختلف برنامه
روشهای متعددی برای ارسال اطلاعات به مسیرهای مختلف برنامه وجود دارند که تعدادی از آنها را مانند پارامترهای اختیاری، پارامترهای اجباری و پارامترهای کوئری، در قسمت قبل بررسی کردیم. روش دیگری را که در اینجا میتوان بکار برد، استفاده از خاصیت data تعاریف مسیریابی برنامه است:
{ path: 'products', component: ProductListComponent, data: { pageTitle: 'Product List'} },
برای خواندن این اطلاعات ثابت میتوان از شیء route.snapshot سرویس ActivatedRoute استفاده کرد:
this.pageTitle = this.route.snapshot.data['pageTitle'];
پیش بارگذاری اطلاعات پویای مسیرهای مختلف برنامه
زمانیکه به صفحهی جزئیات یک محصول مراجعه میکنیم، ابتدا این کامپوننت آغاز شده و قالب آن نمایش داده میشود. سپس در متد ngOnInit آن کار درخواست اطلاعات از سرور و نمایش آن صورت خواهد گرفت. در این بین، چون زمانی بین درخواست اطلاعات از سرور و دریافت آن صرف میشود، کاربر ابتدا شاهد قالب خالی کامپوننت، به همراه برچسبهای مختلف آن خواهد بود که فاقد اطلاعات هستند و پس از مدتی این اطلاعات نمایش داده میشوند.
برای حل این مشکل از سرویسی به نام Route Resolver استفاده میشود. در این حالت زمانیکه کاربر صفحهی جزئیات یک محصول را درخواست میکند، ابتدا مسیریابی آن فعال شده و سپس سرویس Route Resolver اجرا میشود که کار آن درخواست اطلاعات از وب سرور است. در این حالت پس از دریافت اطلاعات از سرور، کار فعالسازی کامپوننت صورت میگیرد. به این ترتیب قالب کاملا آمادهی کامپوننت، به همراه اطلاعات مرتبط با آن، به کاربر نمایش داده خواهد شد.
بدون استفادهی از Route Resolver، کامپوننت کلاس، پس از آغاز آن، اطلاعات را دریافت میکند. اما با بکارگیری Route Resolver، این سرویس ویژهاست که پیش از هر مرحلهی دیگری اطلاعات را دریافت میکند.
پیاده سازی یک Route Resolver شامل سه مرحلهاست:
الف) ایجاد و ثبت سرویس Route Resolver
ب) معرفی Route Resolver به تنظیمات مسیریابی
ج) خواندن اطلاعات دریافتی توسط Route Resolver به کمک سرویس ActivatedRoute
ایجاد سرویس Route Resolver
یک Route Resolver به صورت یک سرویس جدید ایجاد میشود:
> ng g s product/ProductResolver -m product/product.module installing service create src\app\product\product-resolver.service.spec.ts create src\app\product\product-resolver.service.ts update src\app\product\product.module.ts
providers: [ProductService, ProductResolverService]
فایل src\app\product\product-resolver.service.ts را به نحو ذیل تکمیل کنید:
import { Injectable } from '@angular/core'; import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/map'; import { ProductService } from './product.service'; import { IProduct } from './iproduct'; @Injectable() export class ProductResolverService implements Resolve<IProduct> { constructor(private productService: ProductService, private router: Router) { } resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IProduct> { let id = route.params['id']; if (isNaN(id)) { console.log(`Product id was not a number: ${id}`); this.router.navigate(['/products']); return Observable.of(null); } return this.productService.getProduct(+id) .map(product => { if (product) { return product; } console.log(`Product was not found: ${id}`); this.router.navigate(['/products']); return null; }) .catch(error => { console.log(`Retrieval error: ${error}`); this.router.navigate(['/products']); return Observable.of(null); }); } }
مرحلهی اول تعریف یک سرویس Route Resolver، پیاده سازی اینترفیس جنریک Resolve است:
export class ProductResolverService implements Resolve<IProduct> {
این اینترفیس پیاده سازی متد resolve را با امضایی که مشاهده میکنید، درخواست میکند:
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IProduct> {
RouterStateSnapshot وضعیت مسیریاب را در این لحظه در اختیار این سرویس قرار میدهد.
خروجی این متد یک Observable است؛ از نوع اطلاعاتی که دریافت میکند. زمانیکه مسیریابی فعال میشود، متد resolve را فراخوانی کرده و منتظر پایان کار Observable آن میشود. پس از آن است که کامپوننت این مسیریابی را فعالسازی خواهد کرد.
در پیاده سازی متد resolve، تعدادی اعتبارسنجی اطلاعات را نیز مشاهده میکنید. برای مثال اگر id وارد شده، عددی نباشد، در اینجا فرصت خواهیم داشت پیش از فعالسازی کامپوننت نمایش جزئیات یک محصول، کاربر را به صفحهای دیگر هدایت کنیم.
پس از آن نیاز به دریافت اطلاعات محصول درخواست شده، از REST Web API برنامه است. به همین جهت سرویس ProductService را که در قسمت قبل معرفی کردیم، به سازندهی کلاس تزریق کردهایم تا از طریق متد getProduct آن، کار دریافت اطلاعات یک محصول را انجام دهیم.
در اینجا متد getProduct(+id) به همراه عملگر + است تا id دریافتی را به عدد تبدیل کند. سپس بر روی این متد، عملگر map فراخوانی شدهاست. به این ترتیب میتوان به اطلاعات دریافتی از سرور، پیش از بازگشت آن به فراخوان متد resolve، دسترسی یافت. به این ترتیب در اینجا نیز میتوان یک سری اعتبارسنجی را انجام داد. برای مثال آیا id دریافتی، متناظر با محصولی در سمت سرور است یا خیر؟
map operator خروجی را به صورت یک observable بازگشت میدهد. به همین جهت در اینجا نیازی به ذکر return Observable.of نیست.
معرفی Route Resolver به تنظیمات مسیریابی
بعد از پیاده سازی سرویس Route Resolver، نیاز است آنرا به تنظیمات مسیریابی برنامه اضافه کنیم. به همین جهت فایل src\app\product\product-routing.module.ts را گشوده و تنظیمات آنرا به شکل زیر تغییر دهید:
import { ProductResolverService } from './product-resolver.service'; const routes: Routes = [ { path: 'products', component: ProductListComponent }, { path: 'products/:id', component: ProductDetailComponent, resolve: { product: ProductResolverService } }, { path: 'products/:id/edit', component: ProductEditComponent, resolve: { product: ProductResolverService } } ];
در اینجا هر تعداد Route Resolver مورد نیاز را میتوان تعریف کرد. برای مثال اگر مسیریابی خاصی، اطلاعات دیگری را نیز از سرویس خاصی دریافت میکند، میتوان یک جفت کلید/مقدار دیگر را نیز برای آن تعریف کرد. فقط باید دقت داشت که keyها باید منحصربفرد باشند.
به این ترتیب اطمینان حاصل خوهیم کرد که اطلاعات مورد نیاز این مسیریابیها، پیش از فعالسازی کامپوننت آنها، از REST Web API برنامه دریافت میشوند.
خواندن اطلاعات دریافتی توسط Route Resolver به کمک سرویس ActivatedRoute
پس از تعریف سرویس Route Resolver سفارشی خود و معرفی آن به تنظیمات مسیریابی برنامه، قسمت نهایی این عملیات، خواندن این اطلاعات پیش واکشی شدهاست. به همین جهت فایل src\app\product\product-detail\product-detail.component.ts را گشوده و محتوای آنرا به نحو ذیل اصلاح کنید:
constructor(private route: ActivatedRoute) { } ngOnInit(): void { this.product = this.route.snapshot.data['product']; }
- همانطور که مشاهده میکنید، دیگر در این کامپوننت نیازی به تزریق سرویس ProductService نبوده و قسمت دریافت اطلاعات آن از طریق این سرویس، حذف شدهاست.
برای آزمایش آن، لیست محصولات را مشاهده کرده و سپس بر روی لینک مشاهدهی جزئیات یک محصول کلیک کنید. البته در اینجا چون هنوز Route Resolver ایی را برای پیش دریافت لیست محصولات ایجاد نکردهایم، ابتدا قاب خالی لیست محصولات نمایش داده میشود و سپس لیست محصولات. اما دیگر صفحهی نمایش جزئیات یک محصول، این چنین نیست. ابتدا یک وقفهی یک ثانیهای را حس خواهید کرد و سپس صفحهی کامل جزئیات یک محصول نمایان میشود.
یک نکته: اگر یک سرویس Route Resolver، در دو کامپوننت مختلف استفاده شود، اطلاعات آن، بین این دو کامپوننت به اشتراک گذاشته خواهد شد.
مرحلهی بعد، ویرایش فایل src\app\product\product-edit\product-edit.component.ts است تا کامپوننت ویرایش جزئیات اطلاعات نیز بتواند از قابلیت پیش واکشی اطلاعات استفاده کند. در اینجا هنوز نیاز به سرویس ProductService است تا بتوان اطلاعات را ذخیره و یا حذف کرد. تنها قسمتی که باید تغییر کند، حذف متد getProduct و تغییر متد ngOnInit است:
ngOnInit(): void { this.route.data.subscribe(data => { this.onProductRetrieved(data['product']); }); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-03.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
اگر به جستجوی گوگل دقت کرده باشید، به صورت Ajax ایی پیاده سازی شدهاست، با این تفاوت که بعد از هر تغییر درجستجوی مورد نظر، Url صفحه نیز تغییر میکند (برای مثال بعد از جستجوی عبارت dotNetTips آدرس بار صفحه به شکل https://www.google.com/#q=dotNetTips&* تغییر میکند). برای پیاده سازی این ویژگی باید از تکنیکی به نام HashChange استفاده کرد. در نتیجه با این روش مشکل ارسال صفحهای خاص در یک گرید برای دیگران، به صورت Ajax ایی و بدون مشکل انجام میشود. از این رو با توجه به داشتن Urlهای منحصر به فرد برای هر صفحه، تا حدی مشکل سئو سایت را نیز برطرف میکنیم.
برای استفاده از این ویژگی در ادامه قصد داریم پیاده سازی کتابخانهی MvcAjaxPager را مورد بررسی قرار دهیم. ابتدا قبل از هر کاری، با استفاده از دستور زیر اقدام به نصب کتابخانه آن مینماییم:
Install-Package MvcAjaxPager
در ادامه نحوه پیاده سازی آن را به همراه مثالی، مورد بررسی قرار میدهیم:
ابتدا یک مدل فرضی را همانند زیر تهیه میکنیم :
public class Topic { public int Id; public string Title; public string Text; }
public class TopicService { public static IEnumerable<Topic> Topics = new List<Topic>() { new Topic{Id=1,Title="Title 1",Text= "Text 1"}, new Topic{Id=2,Title="Title 2",Text="Text 2"}, new Topic{Id=3,Title="Title 3",Text="Text 3"}, new Topic{Id=4,Title="Title 4",Text="Text 4"}, new Topic{Id=5,Title="Title 5",Text="Text 5"}, new Topic{Id=6,Title="Title 6",Text="Text 6"}, new Topic{Id=7,Title="Title 7",Text="Text 7"}, new Topic{Id=8,Title="Title 8",Text="Text 8"}, new Topic{Id=9,Title="Title 9",Text="Text 9"}, new Topic{Id=10,Title="Title 10",Text="Text 10"}, new Topic{Id=11,Title="Title 11",Text="Text 11"}, new Topic{Id=12,Title="Title 12",Text="Text 12"}, new Topic{Id=13,Title="Title 13",Text="Text 13"}, new Topic{Id=14,Title="Title 14",Text="Text 14"}, new Topic{Id=15,Title="Title 15",Text="Text 15"}, new Topic{Id=16,Title="Title 16",Text="Text 16"}, new Topic{Id=17,Title="Title 17",Text="Text 17"}, new Topic{Id=18,Title="Title 18",Text="Text 18"}, new Topic{Id=19,Title="Title 19",Text="Text 19"}, new Topic{Id=20,Title="Title 20",Text="Text 20"}, new Topic{Id=21,Title="Title 21",Text="Text 21"}, new Topic{Id=22,Title="Title 22",Text="Text 22"}, }; public static IEnumerable<Topic> GetAll() { return Topics.OrderBy(row => row.Id); } }
public class ListViewModel { public IEnumerable<Topic> Topics { get; set; } public int PageIndex { get; set; } public int TotalItemCount { get; set; } }
public ActionResult Index(int page = 1) { var topics = TopicService.GetAll (); int totalItemCount = topics.Count(); var model = new ListViewModel() { PageIndex = page, Topics = topics.OrderBy(p => p.Id).Skip((page - 1) * 10).Take(10).ToList(), TotalItemCount = totalItemCount }; if (!Request.IsAjaxRequest()) { return View(model); } return PartialView("_TopicList", model); }
و در Partial view مربوطه نیز داریم :
@using MvcAjaxPager @model ListViewModel @Html.AjaxPager(Model.TotalItemCount, 10, Model.PageIndex, "Index", "Home", null, new PagerOptions { ShowDisabledPagerItems = true, AlwaysShowFirstLastPageNumber = true, HorizontalAlign = "center", ShowFirstLast = false, CssClass = "NavigationBox", AjaxUpdateTargetId = "dvTopics", AjaxOnBegin = "AjaxStart", AjaxOnComplete = "AjaxStop" }, null, null) <table> <tr> <th> @Html.DisplayName("ID") </th> <th> @Html.DisplayName("Title") </th> <th> @Html.DisplayName("Text") </th> </tr> @foreach (var topic in Model.Topics) { <tr> <td> @topic.Id </td> <td> @topic.Title </td> <td> @topic.Text </td> </tr> } </table> @Html.AjaxPager(Model.TotalItemCount, 10, Model.PageIndex, "Index", "Home", null, new PagerOptions { ShowDisabledPagerItems = true, AlwaysShowFirstLastPageNumber = true, HorizontalAlign = "center", ShowFirstLast = true, FirstPageText = "اولین", LastPageText = "آخرین", MorePageText = "...", PrevPageText = "قبلی", NextPageText = "بعدی", CssClass = "NavigationBox", AjaxUpdateTargetId = "dvTopics", AjaxOnBegin = "AjaxStart", AjaxOnComplete = "AjaxStop" }, null, null)
حال برای استفاده از pager مورد نظر فقط کافیست متد AjaxPager آن را فراخوانی کنیم. این متد شامل 11 OverLoad مختلف هست.
در این قسمت TotalItemCount جمع کل رکوردها، PageSize تعداد رکوردهای هر صفحه و PageIndex آدرس صفحه جاری میباشد.
مهمترین بخش این pager که قابلیتهای زیادی را به کاربر میدهد، قسمت PagerOptions آن است و تعدادی از پارامترهای آن شامل AjaxOnBeginAjaxOnCompelte، AjaxOnSuccess ، AjaxOnFailure میتوان تعیین کرد تا بعد از شروع، وقوع خطا، موفقیت و یا خاتمه عملیات جاوا اسکریپتی، اجرا شود.
AlwaysShowFirstLastPageNumber جهت نمایش صفحه اول و آخر
FirstPageText جهت تعیین متن اولین صفحه
LastPageText جهت تعیین متن آخرین صفحه
CssClass ، Id جهت تعیین Id خاص
و در انتها، در view مربوطه داریم:
@using MvcAjaxPager @model ListViewModel @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> </head> <body> <div id="dvTopics"> @{ @Html.Partial("_TopicList", Model); } </div> <script type="text/javascript" src="@Url.Content("~/Scripts/jquery-1.7.2.min.js")"></script> <script type="text/javascript" src="@Url.Content("~/Scripts/path.min.js")"></script> <script type="text/javascript" src="@Url.Content("~/Scripts/jquery.pager-1.0.1.min.js")"></script> <script type="text/javascript"> $('.NavigationBox').pager(); //pagination before start function AjaxStart() { console.log('Start AJAX call. Loading message can be shown'); } // pagination - after request function AjaxStop() { console.log('Stop AJAX call. Loading message can be hidden'); }; </script> </body> </html>
نصب پیشنیازهای کار با 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، تمام اسکریپتهای دخیل در اجرای برنامه را به صورت خودکار یافته و به صورت یک تک فایل نهایی، بسته بندی و توزیع کرد.
- فعال سازی فشرده سازی
- انتقال لاگها به یک سیستم راه دور
- حذف لاگ فایلهای قدیمی از طریق اسکریپت نویسی
- حذف لاگ فایلهای قدیمی توسط IIS Log File Cleaner
فشرده سازی دایرکتوری لاگ فایل ها
%SystemDrive%\inetpub\logs\LogFiles
این روش سادهترین روش موجود برای مدیریت لاگ هاست ولی روش نهایی نیست و باز به مرور زمان این روش هم کارایی خودش را از دست خواهد داد. این روش بیشتر شبیه خرید زمان میباشد تا اینکه یک راه حل نهایی برای حل مشکل باشد. البته این را هم باید مدنظر داشت که موقع تیک زدن گزینه بالا عملیات فشرده سازی باعث کند شدن سرعت کامپیوتر در حین آغاز عمل ذخیره سازی لاگ فایلها هم خواهد شد. پس اگر قصد چنین کاری ذا دارید در ساعاتی که سرور کمترین فشار از طرف کاربران را دارد یا اصطلاحا پیک کاری آن پایین است انجامش دهید.
انتقال لاگ فایلها به یک سیستم راه دور
همانطور که در بالا اشاره کردیم محل پیش فرض ذخیره سازی لاگها درمسیر
%SystemDrive%\inetpub\logs\LogFiles
قرار دارد و این محل ذخیره سازی برای هر سرور یا حتی یک وب سایت خاص در صفحه تنظیمات Logging مشخص شده است و شما در میتوانید این لاگها را حتی برای کل سرور یا مربوط به یک سایت خاص، به سروری دیگر انتقال دهید. این امکان میتواند به امنیت سیستم هم کمک فراوانی کند تا اگر دیسک محلی Local Disk هم دچار مشکل شد، باز خواندن لاگ فایلها میسر باشد و با استفاده از ابزارهای تحلیل لاگ فایل ها، آنها را مورد بررسی قرار دهیم. برای تغییر محل ذخیره سازی لاگها به یک سیستم راه دور، راه حل زیر را طی کنید.
در IIS وب سایتی را که میخواهید لاگ آن انتقال یابد، انتخاب کنید؛ یا اگر لاگ کل سیستم IIS را میخواهید انتقال بدهید نام سرور را در لیست درختی انتخاب کنید و از ماژولهای سمت راست، ماژول Logging را انتخاب کنید و در قسمت Directory که محل ذخیره سازی فعلی لاگها را نوشته شده است، به صورت UNC آدرس دهی کنید. در آدرس زیر اولی نام سرور است Contoso-server1\\ و دومی هم Logs نام پوشهای که به اشتراک گذاشته شده است.
حذف لاگ فایلهای قدیمی با استفاده از اسکریپت
با این روش میتوانید لاگ فایل هایی را که بعد از مدتی معین که دلخواه شما هست، از سیستم حذف نمایید و اگر این اسکریپت را زمان بندی خودکار نمایید، میتوانید از مراقبت مداوم و ثابت این کار نیز رها شوید.
با ستفاده از VBScript بررسی میکنیم که اگر مثلا عمر لاگ فایل به 30 روز رسیده است، باید حذف شوند. خط دوم کد زیر نهایت عمر یک لاگ فایل را مشخص میکند:
sLogFolder = "c:\inetpub\logs\LogFiles" iMaxAge = 30 'in days Set objFSO = CreateObject("Scripting.FileSystemObject") set colFolder = objFSO.GetFolder(sLogFolder) For Each colSubfolder in colFolder.SubFolders Set objFolder = objFSO.GetFolder(colSubfolder.Path) Set colFiles = objFolder.Files For Each objFile in colFiles iFileAge = now-objFile.DateCreated if iFileAge > (iMaxAge+1) then objFSO.deletefile objFile, True end if Next Next
اسکریپت بالا تمامی subfolderها را برای همه سایتها بررسی کرده و لاگهای آنان را حذف میکند. ولی اگر دوست دارید این عملیات را تنها به یک وب سایت محدود کنید، باید مسیر را در خط اول دقیقتر مشخص کنید.
برای اجرای دستی اسکریپت در cmd تایپ کنید:
cscript.exe c:\scripts\retentionscript.vbs
ولی اگر میخواهید این اسکریپت در هر دورهی زمانی خاص اجرا شود، یا زمان بندی Scheduling گردد، دیگر مجبور نیستید هر بار به فکر نگهداری از لاگها باشید.
زمان بندی اجرای اسکریپت
server manager (قابل تست در ویندوزهای سرور) را باز کرده و از منوی Tools گزینه Task Scheduler را انتخاب کنید و در قسمت Actions گزینه Create Task را انتخاب نمایید. در کادر باز شده نام "Delete Log Files " را برای مثال برگزینید و در قسمت Security هم کاربری که اجازه اجرای اسکریپت را دارد مشخص کنید.
برگه Triggers را انتخاب کرده و گزینه New را انتخاب کنید و عملیات زمان بندی را تنظیم کنید و حتما بعد از زمان بندی مطمئن باشید که تیک Enabled فعال است.
در برگه Actions هم گزینه New را انتخاب کنید؛ در کادر باز شده از لیست Start a program را انتخاب کرده و در قسمت Program\script، دستور cscript را ذکر نمایید و به عنوان آرگومان ورودی Add arguments هم مسیر اسکریپت خود را ذکر نمایید و کادر را تایید کنید.
برای آغاز زمان بندی در لیست وظیفههای فعال active task pane، وظیفه ای که الان ساخته اید را اجرا کرده و به مسیر ذخیره لاگها رفته و میبینید که لاگهای مورد نظر حذف شدهاند؛ پس از صحت اجرای اسکریپت مطمئن میشویم. دوباره به لیست وظایف رفته و گزینه End را بزنید تا وظیفه، در حالت Ready قرار گیرد تا از همین الان فرایند زمان بندی اجرای اسکریپت آغاز شود.
حذف لاگ فایلها با استفاده از IIS Log Cleaner Tools
سادهترین ابزار برای مدیریت حذف لاگ فایل هاست که هر یک ساعت یکبار اجرا شده و لاگ فایلهای تاریخ گذشته را که زمانش را شما تعیین میکنید، به سمت سطل زباله که البته درستش بازیافت است Recycle Bin انتقال میدهد تا از ضرر از دست دادن لاگها جلوگیری کند که بعدا شما میتوانید آنها را به صورت دستی حذف کنید. همچنین عملیات خودکار حذف را نیز میتوان متوقف نمود.
ابتدا برنامه را از اینجا دانلود کنید. موقعیکه برنامه را اجرا کنید، در نوتیفیکیشن taskbar مینشیند و برنامه با یک پیغام به شما اعلام میکند، این اولین بار است که برنامه را باز کردهاید. پس یک سر به setting آن بزنید؛ با انتخاب گزینهی settings برنامه بسته شده و فایل Settings.txt برای شما باز میشود که مدت زمان عمر لاگ فایل و مسیر ذخیره آنها، از شما پرسیده میشود که مقدار عمر هر لاگ فایل به طور پیش فرض 30 روز و مسیر ذخیرهی لاگها همان مسیر پیش فرض IIS است که اگر شما دستی آن را تغییر داده اید، با پرسیدن آن، از محل لاگها اطمینان کسب میکند. در صورتی که قصد تغییری را در فایل، دارید آن را تغییر داده و ذخیره کنید و برنامه را مجددا اجرا کنید.
نکات نهایی در مورد این برنامه :
- اگر از ابزار IIS Cleaner Tool استفاده میکنید باید دستی سطل بازیافت را هم پاک کنید و هم اینکه میتوانید یک محدودیت حجمی برای Recycle Bin قرار دهید که اگر به یک حدی رسید، خودکار پاک کند تا مشکلی برای سیستم عامل ایجاد نشود که البته به طور پیش فرض چنین است.
- برنامه بالا به طور پیش فرض ریشهی لاگها را حذف میکند. پس اگر میخواهید فقط سایت خاصی را مد نظر داشته باشد، آدرس دایرکتوری آن را اضافه کنید. البته چون این برنامه فقط روی یک دایرکتوری کار میکند و شما چند وب سایت دارید و مثلا میخواهید سه تای آنها را پاکسازی کنید، چارهی جز استفاده از اسکریپتهای با زمان بندی ندارید.
- برنامهی بالا فقط فایل هایی با پسوند log را به سطل بازیافت انتقال میدهد.
- برنامهی بالا یک سرویس نیست و باید به طور دستی توسط کاربر اجرا گردد. پس اگر ریست هم شد باید دستی اجرا شود یا آن را به داخل پوشه startup بکشید.
- برنامه برای اجرایش نیاز به لاگین کاربر و مجوز نوشتن در آن پوشه را دارد تا به درستی کار کند.
نیاز به یک Dispatcher برای تعامل با بیش از یک مخزن حالت
در اینجا برای نمونه دو مخزن حالت تعریف شدهاند؛ اما روش تعامل با این مخازن حالت، دیگر مانند قبل نیست. برای نمونه در اثر تعامل یک کاربر با View ای خاص، رخدادی صادر شده و اینبار مدیریت این رخداد توسط یک Action (که عموما یک پیام رشتهای است)، به Dispatcher مرکزی ارسال میشود (و نه مستقیما به مخزن حالت خاصی). اکنون این Dispatcher، اکشن رسیده را به مخازن کد مشترک به آن ارسال میکند تا عمل متناسب با آن اکشن درخواستی را انجام دهند. مابقی آن همانند قبل است که پس از تغییر حالت در هر کدام از مخازن حالت، کار به روز رسانی UI، در کامپوننتهای مشترک صورت خواهد گرفت. بدیهی است در اینجا مخازن حالت، مجاز به صرفنظر کردن از یک اکشن خاص هستند و الزامی به پیاده سازی آن ندارند. هدف اصلی این است که اگر اکشنی قرار بود در تمام مخازن حالت پیاده سازی شود و حالتهای آنها را تغییر دهد، روشی را برای مدیریت آن داشته باشیم.
بنابراین اگر به این الگوی جدید دقت کنید، چیزی نیست بجز یک الگوی Observer دو سطحی:
الف) Dispatcher ای (Subject) که مشترکهایی را مانند مخازن حالت دارد (Observers).
ب) مخازن حالتی (Subjects) که مشترکهایی را مانند کامپوننتها دارند (Observers).
اگر پیشتر با React کار کرده باشید، این الگو را تحت عناوینی مانند Flux و یا Redux میشناسید و در اینجا میخواهیم پیاده سازی #C آنرا بررسی کنیم:
در الگوی Flux، در اثر تعامل یک کاربر با کامپوننتی، اکشنی به سمت یک Dispatcher ارسال میشود. سپس Dispatcher این اکشن را به مخزن حالتی جهت مدیریت آن ارسال میکند که در نهایت سبب تغییر حالت آن شده و به روز رسانی UI را در پی خواهد داشت.
پیاده سازی یک Dispatcher برای تعامل با بیش از یک مخزن حالت
پیش از هر کاری نیاز است قالب اکشنهای ارسالی را که قرار است توسط مخازن حالت مورد پردازش قرار گیرند، مشخص کنیم:
namespace BlazorStateManagement.Stores { public interface IAction { public string Name { get; } } }
namespace BlazorStateManagement.Stores.CounterStore { public class IncrementAction : IAction { public const string Increment = nameof(Increment); public string Name { get; } = Increment; } public class DecrementAction : IAction { public const string Decrement = nameof(Decrement); public string Name { get; } = Decrement; } }
پس از تعریف ساختار یک اکشن، اکنون نوبت به پیاده سازی راه حلی برای ارسال آن به تمام مخازن حالت برنامه است:
using System; namespace BlazorStateManagement.Stores { public interface IActionDispatcher { void Dispatch(IAction action); void Subscribe(Action<IAction> actionHandler); void Unsubscribe(Action<IAction> actionHandler); } public class ActionDispatcher : IActionDispatcher { private Action<IAction> _actionHandlers; public void Subscribe(Action<IAction> actionHandler) => _actionHandlers += actionHandler; public void Unsubscribe(Action<IAction> actionHandler) => _actionHandlers -= actionHandler; public void Dispatch(IAction action) => _actionHandlers?.Invoke(action); } }
این سرویس را نیز با طول عمر Scoped به سیستم تزریق وابستگیهای برنامه معرفی میکنیم که سبب میشود تا پایان عمر برنامه (بسته شدن مرورگر یا ریفرش کامل صفحهی جاری)، در حافظه باقی مانده و وهله سازی مجدد نشود. به همین جهت تزریق آن در مخازن حالت مختلف برنامه، دقیقا حالت یک Dispatcher اشتراکی را پیدا خواهد کرد.
namespace BlazorStateManagement.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddScoped<IActionDispatcher, ActionDispatcher>(); // ... } } }
استفاده از IActionDispatcher در مخازن حالت برنامه
در ادامه میخواهیم مخازن حالت برنامه را تحت کنترل سرویس IActionDispatcher قرار دهیم تا کاربر بتواند اکشنی را به Dispatcher ارسال کند و سپس Dispatcher این درخواست را به تمام مخازن حالت موجود، جهت بروز واکنشی (در صورت نیاز)، اطلاعات رسانی نماید.
برای این منظور سرویس ICounterStore قسمت قبل ، به صورت زیر تغییر میکند که اینترفیس IDisposable را پیاده سازی کرده و همچنین دیگر به همراه متدهای عمومی افزایش و یا کاهش مقدار نیست:
using System; namespace BlazorStateManagement.Stores.CounterStore { public interface ICounterStore : IDisposable { CounterState State { get; } void AddStateChangeListener(Action listener); void BroadcastStateChange(); void RemoveStateChangeListener(Action listener); } }
using System; namespace BlazorStateManagement.Stores.CounterStore { public class CounterStore : ICounterStore { private readonly CounterState _state = new(); private bool _isDisposed; private Action _listeners; private readonly IActionDispatcher _actionDispatcher; public CounterStore(IActionDispatcher actionDispatcher) { _actionDispatcher = actionDispatcher ?? throw new ArgumentNullException(nameof(actionDispatcher)); _actionDispatcher.Subscribe(HandleActions); } private void HandleActions(IAction action) { switch (action) { case IncrementAction: IncrementCount(); break; case DecrementAction: DecrementCount(); break; } } public CounterState State => _state; private void IncrementCount() { _state.Count++; BroadcastStateChange(); } private void DecrementCount() { _state.Count--; BroadcastStateChange(); } public void AddStateChangeListener(Action listener) => _listeners += listener; public void RemoveStateChangeListener(Action listener) => _listeners -= listener; public void BroadcastStateChange() => _listeners.Invoke(); public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_isDisposed) { try { if (disposing) { _actionDispatcher.Unsubscribe(HandleActions); } } finally { _isDisposed = true; } } } } }
- با توجه به اینکه CounterStore یک سرویس ثبت شدهی در سیستم است، میتواند از مزیت تزریق سایر سرویسها در سازندهی خودش بهرهمند شود؛ مانند تزریق سرویس جدید IActionDispatcher.
- پس از تزریق سرویس جدید IActionDispatcher، متدهای Subscribe آنرا در سازندهی کلاس و Unsubscribe آنرا در حین Dispose سرویس، فراخوانی میکنیم. البته فراخوانی و یا پیاده سازی Unsubscribe و Dispose در اینجا غیرضروری است؛ چون طول عمر این کلاس با طول عمر برنامه یکی است.
- بر اساس این الگوی جدید، هر اکشنی که به سمت Dispatcher مرکزی ارسال میشود، در نهایت به متد HandleActions یکی از مخازن حالت تعریف شده، خواهد رسید:
private void HandleActions(IAction action) { switch (action) { case IncrementAction: IncrementCount(); break; case DecrementAction: DecrementCount(); break; } }
@inject ICounterStore CounterStore @code { private void IncrementCount() { CounterStore.IncrementCount(); }
- ابتدا در انتهای فایل Client\_Imports.razor، فضای نام سرویس جدید IActionDispatcher را اضافه میکنیم:
@using BlazorStateManagement.Stores
// ... @inject IActionDispatcher ActionDispatcher @code { private void IncrementCount() { ActionDispatcher.Dispatch(new IncrementAction()); }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorStateManagement-Part-2.zip