- Bar and Column
- Line and Vertical Line
- Area and Vertical Area
- Bullet
- Pie and Donut
- Scatter
- Scatter Line
- Bubble
-
Radar and Polar
برای رسم نمودار میتوانیم به صورت زیر عمل کنیم:
1- ابتدا باید استایلهای مربوط به Data Visualization را به صفحه اضافه کنیم:
<link href="Content/kendo.dataviz.min.css" rel="stylesheet" /> <link href="Content/kendo.dataviz.default.min.css" rel="stylesheet" />
<div id="chart"></div>
<div id="chart" style="width: 400px; height: 600px"></div>
$("#chart").kendoChart();
همانطور که مشاهده میکنید هیچ دادهایی را هنوز برای چارت تعیین نکردهایم؛ در نتیجه همانند تصویر فوق یک چارت خالی بر روی صفحه نمایش داده میشود. برای چارت فوق میتوانیم خواصی از قبیل عنوان و ... را تعیین کنیم:
$("#chart").kendoChart({ title: { text: "چارت آزمایشی" } });
نمایش دادهها بر روی چارت:
دادهها را میتوان هم به صورت local و هم به صورت remote دریافت و بر روی چارت نمایش داد. اینکار را میتوانیم توسط قسمت series انجام دهیم:
$("#chart").kendoChart({ title: { text: "عنوان چارت" }, series: [ { name: "دادههای چارت", data: [200, 450, 300, 125] } ] });
$("#chart").kendoChart({ title: { text: "عنوان چارت" }, series: [ { name: "دادههای چارت", data: [200, 450, 300, 125] } ], categoryAxis: { categories: [2000, 2001, 2002, 2003] } });
دریافت اطلاعات از سرور:
کدهای سمت سرور:
public class ProductsController : ApiController { public IEnumerable<ProductViewModel> Get() { var products = _productService.GetAllProducts(); var query = products.GroupBy(p => p.Name).Select(p => new ProductViewModel { Value = p.Key, Count = p.Count() }); return query; } } public class ProductViewModel { public string Value { get; set; } public int Count { get; set; } }
سپس برای دریافت اطلاعات از سمت سرور باید DataSource مربوط به چارت را مقداردهی کنیم:
var productsDataSource = new kendo.data.DataSource({ transport: { read: { url: "api/products", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET' } }, error: function (e) { alert(e.errorThrown.stack); }, pageSize: 5, sort: { field: "Id", dir: "desc" } }); $("#chart").kendoChart({ title: { text: "عنوان چارت" }, dataSource: productsDataSource, series: [ { field: "Count", categoryField: "Value", aggregate: "sum" } ] });
موارد فوق را میتوانیم به صورت یک افزونه نیز کپسوله کنیم.
کدهای افزونه jquery.ChartAjax:
(function($) { $.fn.ShowChart = function(options) { var defaults = { url: '/', text: 'نمودار دایره ایی', theme: 'blueOpal', font: '13px bbc-nassim-bold', legendPosition: 'left', seriesField: 'Count', seriesCategoryField: 'Value', titlePosition: 'top', chartWidth: 400, chartHeight: 400 }; var options = $.extend(defaults, options); return this.each(function() { var chartDataSource = new kendo.data.DataSource({ transport: { read: { url: options.url, dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET' } }, error: function (e) { // handle error } }); $(this).kendoChart({ chartArea: { height: options.chartHeight }, theme: options.theme, title: { text: options.text, font: options.font, position: options.titlePosition }, legend: { position: options.legendPosition, labels: { font: options.font } }, seriesDefaults: { labels: { visible: false, format: "{0}%" } }, dataSource: chartDataSource, series: [ { type: "pie", field: options.seriesField, categoryField: options.seriesCategoryField, aggregate: "sum", } ], tooltip: { visible: true, template: "${category}: ${value}", font: options.font } }); }); }; })(jQuery);
chartArea : جهت تعیین طول و عرض چارت
theme : جهت تعیین قالبهای از پیشتعریف شده:
- Black
- BlueOpal
- Bootstrap
- Default
- Flat
- HighContrast
- Material
- MaterialBlack
- Metro
- MetroBlack
- Moonlight
- Silver
-
Uniform
title : جهت تعیین عنوان چارت
legend : جهت تنظیم ویژگیهای قسمت گروهبندی چارت
tooltip : جهت تنظیم ویژگیهای مربوط به نمایش tooltip در هنگام hover بر روی چارت.
لازم به ذکر است در قسمت series میتوانید نوع چارت را تعیین کنید.
$('#chart').ShowChart({ url: "/Report/ByUnit", legendPosition: "bottom" });
دریافت سورس مثال جاری (KendoChart.zip)
> ng test --help
یک مثال: بررسی اجرای دستور ng test
یکی از مثالهای بررسی شدهی در این سری را انتخاب و یا حتی یک برنامهی جدید را توسط Angular CLI ایجاد کرده و سپس دستور ng test را در ریشهی این پروژه اجرا کنید. به این ترتیب برنامه به صورت خودکار کامپایل شده و سپس به صورت خودکار آزمونهای واحد آنرا که در فایلهای spec.ts.* قرار دارند، اجرا میکند. در آخر نتیجه را در مرورگر گزارش میدهد:
همانطور که مشخص است، 3 specs, 3 failures داریم. در اینجا میتوان بر روی لینک Spec List کلیک کرد و لیست آزمونهای واحد موجود را مشاهده نمود:
هر کدام از عناوین ذکر شده نیز به جزئیات مشکلات آنها، لینک شدهاند. برای مثال اگر بر روی اولین مورد کلیک کنیم، خطایی مانند «'alert' is not a known element» قابل مشاهدهاست. به این معنا که برای نمونه در قسمت قبل کامپوننت alert را به صفحه اضافه کردیم:
<alert type="success">Alert success!</alert>
import { NO_ERRORS_SCHEMA } from '@angular/core'; describe('AppComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); }));
پس از این تغییر، بلافاصله مجدد برنامه کامپایل شده و آزمونهای واحد آن با موفقیت اجرا میشوند (با این فرض که هنوز پنجرهی اجرا کنندهی دستور ng test را باز نگه داشتهاید):
تغییر افزودن schemas: [NO_ERRORS_SCHEMA] را باید در مورد تمام فایلهای spec موجود تکرار کرد.
گزینههای مختلف دستور ng test
دستور ng test به همراه گزینههای متعددی است که شرح آنها را در جدول ذیل مشاهده میکنید:
گزینه | مخفف | توضیح |
code-coverage-- | cc- | تولید گزارش code coverage که به صورت پیش فرض خاموش است. |
colors-- | به صورت پیش فرض فعال است و سبب نمایش رنگهای قرمز و سبز، برای آزمونهای شکست خورده و یا موفق میشود. | |
single-run-- | sr- | اجرای یکبارهی آزمونهای واحد، بدون فعال سازی گزینهی مشاهدهی مداوم تغییرات که به صورت پیش فرض خاموش است. |
progress-- | نمایش جزئیات کامپایل و اجرای آزمونهای واحد که به صورت پیش فرض فعال است. | |
sourcemaps-- | sm- | تولید فایلهای سورسمپ که به صورت پیش فرض فعال است. |
watch-- | w- | بررسی مداوم تغییرات فایلها و اجرای آزمونهای واحد به صورت خودکار که به صورت پیش فرض فعال است. |
بنابراین اجرا دستور ng test بدون ذکر هیچ گزینهای به معنای اجرای مداوم آزمونهای واحد، در صورت مشاهدهی تغییراتی در آنها، به کمک Karma است.
همچنین دو دستور ذیل نیز به یک معنا هستند و هر دو سبب یکبار اجرای آزمونهای واحد میشوند:
> ng test -sr > ng test -w false
اجرای بررسی میزان پوشش آزمونهای واحد
یکی از گزینههای ng test روشن کردن پرچم code-coverage است:
> ng test --code-coverage
> ng test -sr -cc
کار این آزمون بررسی قسمتهای مختلف برنامه و ارائه گزارشی است که مشخص میکند آیا آزمونهای واحد نوشته شده تمام انشعابات برنامه را پوشش میدهند یا خیر؟ برای مثال اگر در متدی if/else دارید، آیا فقط قسمت if را پوشش دادهاید و یا آیا قسمت else هم در آزمونهای واحد، بررسی شدهاست.
اجرای آزمونهای end to end
هدف از ساخت یک برنامه ... استفادهی از آن توسط دیگران است؛ اینجا است که آزمونهای end to end مفهوم پیدا میکنند. در آزمونهای e2e رفتار برنامه همانند حالتی که یک کاربر از آن استفاده میکند، بررسی میشود (برای مثال باز کردن مرورگر، لاگین و مرور صفحات). برای این منظور، Angular CLI در پشت صحنه از Protractor برای این نوع آزمونها استفاده میکند.
برای مشاهدهی راهنما و گزینههای مختلف مرتبط با آزمونهای e2e، میتوان دستور ذیل را صادر کرد:
>ng e2e --help
گزینه | مخفف | توضیح |
config-- | c- | به فایل کانفیگ آزمونهای e2e اشاره میکند که به صورت پیشفرض همان protractor.conf.js واقع در ریشهی پروژهاست. |
element-explorer-- | ee- | بررسی و دیباگ protractor از طریق خط فرمان |
serve-- | s- | کامپایل و توزیع برنامه بر روی پورتی اتفاقی (حالت پیش فرض آن true است) |
specs-- | sp- | پیش فرض آن بررسی تمام specهای موجود در پروژهاست. اگر نیاز به لغو آن باشد میتوان از این گزینه استفاده کرد. |
webdriver-update-- | wu- |
به روز رسانی web driver که به صورت پیش فرض فعال است. |
بنابراین زمانیکه دستور ng e2e صادر میشود، به معنای کامپایل، توزیع برنامه بر روی پورتی اتفاقی و اجرای آزمونها است.
از این جهت که این نوع آزمونها، وابستهی به جزئی خاص از برنامه نیستند، حالت عمومی داشته و فایلهای spec آنها را میتوان در پوشهی e2e واقع در ریشهی پروژه، یافت. برای مثال در قسمتی از آن کار یافتن متن نمایش داده شدهی در صفحهی اول سایت انجام میشود
getParagraphText() { return element(by.css('app-root h1')).getText(); }
expect(page.getParagraphText()).toEqual('app works!');
برای آزمایش آن دستور ng e2e را در ریشهی پروژه صادر کنید. همچنین دقت داشته باشید که در این حالت نیاز است به اینترنت نیز متصل باشد؛ چون از chromedriver api گوگل نیز استفاده میکند. در غیراینصورت خطای ذیل را دریافت خواهید کرد:
Error: getaddrinfo ENOTFOUND chromedriver.storage.googleapis.com chromedriver.storage.googleapis.com:443
اولین پروژه
معمولا برای شروع از تاریخچه و توضیحات دیگر استفاده میکنند اما روش آموزشی که در پیش خواهیم گرفت با انجام پروژههای عملی بوده و هر جا که نیاز به توضیح باشد ، بیان میکنیم ...
ایجاد اولین پروژه Win32 Console Application
ویژوال استادیو را اجرا نمایید و از گزینه File -> New -> Project و سپس طبق عکس زیر پروژه Win32 Console Application را انتخاب نمایید ، دقت کنید که زبان انتخاب شده ++Visual C باشد.
در این مرحله میتوانید محل ذخیره شده پروژه را در قسمت Location تنظیم نمایید و از قسمت Name میتوانید نام دلخواه را وارد کنید در حالت پیش فرض اگر اولین پروژه Win32 Console در مسیر تعین شدهی قسمت Location باشد ، نام ConsoleApplication1 قرار گرفته است . پس از تنظیمات Ok کنید .
در این مرحله Next را بزنید .
در این مرحله در قسمت Additional options تیک Empty project را بزنید ، همانند عکس فوق تنظیمات را انجام دهید .
پس از انجام مراحل فوق پروژه بصورت شکل زیر ظاهر میشود .
برای کد نویسی روی نام پروژه که در اینجا ConsoleApplication1 میباشد ، راست کلیک میکنیم و گزینه Add و سپس New Item را انتخاب میکنیم .
طبق عکس زیر فایل با پسوند cpp را انتخاب و Add میکنیم .
فایلی که اکنون به پروژه اضافه کردیم خالی و با نام پیش فرض Source.cpp میباشد ، دستورات زیر را در آن تایپ کنید .حال پروژه به شکل زیر خواهد بود .
#include<iostream> int main() { std::cout<<"Hello world ...\n"; return 0; }
برای اجرای پروژه کلید F5 را فشار دهید و اگر میخواهید نتیجه کار را مشاهده کنید کلید Ctrl + F5 را امتحان کنید .
شما اولین پروژه VC++ را اجرا نمودید ( آفرین ) .
اما توضیحات :
خط اول برنامه یک راهنمای پیش پردازنده است ، کاراکتر # که نشان میدهد این خط یک راهنمای پیش پردازنده است و بعد عبارت include و نام یک فایل کتابخانه ای که بین علامت <> قرار داده شده ، فایل سرآیند استفاده شده در اینجا iostream میباشد . (به فایلهای کتابخانه ای ، فایلهای سرآیند (Header Files) نیز گفته میشود. ) راهنمای پیش پردازنده خطی است که به کامپایلر اطلاع میدهد در برنامه موجودیتی است که تعریف آن را در فایل سرآیند مذکور جستجو کند . در این برنامه از std::cout استفاده شده ، که کامپایلر در مورد آن چیزی نمیداند لذا به فایل iostream مراجعه نموده ، تعریف آن را مییابد و آن را اجرا میکند .
خط 3 :
بخشی از هر برنامه تابع میباشد . پرانتزهای واقع پس از آن main نشان میدهند که main یک بلوک برنامه بنام تابع است. برنامهها میتوانند حاوی یک یا چندین تابع باشند، اما main تابع اصلی برنامه است که وجود آن الزامی میباشد . کلمه کلیدی int که در سمت چپ main قرار گرفته، بر این نکته دلالت دارد که main یک مقدار صحیح برمیگرداند.
خط 5 :
با استفاده از این دستور رشته ای را به خروجی استاندارد که معمولا صفحه نمایش باشد ارسال میکنیم .
خط 6 :
که ;0 return میباشد مقدار برگشتی تابع را مشخص میکند در حقیقت این خط که مقدار 0 را برمیگرادند نشان دهنده اتمام موفقیت آمیز برنامه میباشد .
به مرور زمان نسبت به موارد بالا بیشتر و مفصل صحبت خواهیم نمود .
npm install -g @angular/cli
ng new HowToKeepAngularSizeSmall
ng serve --open --prod
"@agm/core": "^1.0.0-beta.5", "@angular/flex-layout": "^7.0.0-beta.23", "@angular/material": "^7.3.3", "@angular/platform-browser": "~7.2.0", "@asymmetrik/ngx-leaflet": "^5.0.1", "@ngx-loading-bar/router": "1.3.1", "@ngx-translate/core": "^11.0.1", "@ngx-translate/http-loader": "^4.0.0", "@progress/kendo-angular-buttons": "^4.3.3", "@progress/kendo-angular-dateinputs": "^3.6.0", "@progress/kendo-angular-dialog": "^3.10.1", "@progress/kendo-angular-dropdowns": "^3.5.1", "@progress/kendo-angular-excel-export": "^2.3.0", "@progress/kendo-angular-grid": "^3.13.0", "@progress/kendo-angular-inputs": "^4.2.0", "@progress/kendo-angular-intl": "^1.7.0", "@progress/kendo-angular-l10n": "^1.3.0", "@progress/kendo-angular-layout": "^3.2.0", "@progress/kendo-data-query": "^1.5.0", "@progress/kendo-drawing": "^1.5.8", "@progress/kendo-theme-default": "^3.3.1", "@swimlane/ngx-datatable": "^14.0.0", "angular-calendar": "0.23.7", "angular-tree-component": "7.0.1", "bootstrap": "^3.4.0", "chart.js": "2.7.2", "d3": "4.13.0", "dragula": "3.7.2", "hammerjs": "2.0.8", "intl": "1.2.5", "leaflet": "1.3.1", "moment": "2.21.0", "ng2-charts": "1.6.0", "ng2-dragula": "1.5.0", "ng2-file-upload": "1.3.0", "ng2-validation": "4.2.0", "ngx-perfect-scrollbar": "5.3.5", "ngx-quill": "2.2.0", "screenfull": "3.3.2", "font-awesome": "^4.7.0", "jalali-moment": "^3.3.1", "jquery": "^3.3.1", "ng-snotify": "^4.3.1", "normalize.css": "^8.0.1",
@import "~bootstrap/dist/css/bootstrap.css"; @import "~@progress/kendo-theme-default/scss/all"; @import '@angular/material/prebuilt-themes/pink-bluegrey.css'; @import '~perfect-scrollbar/css/perfect-scrollbar.css'; @import "~ng-snotify/styles/material";
"scripts": [ "node_modules/jquery/dist/jquery.js", "node_modules/bootstrap/dist/js/bootstrap.min.js", "node_modules/hammerjs/hammer.min.js" ],
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; // Import Material import { MatFormFieldModule, MatInputModule, MatButtonModule, MatButtonToggleModule, MatDialogModule, MatIconModule, MatSelectModule, MatToolbarModule, MatDatepickerModule, DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatTableModule, MatCheckboxModule, MatRadioModule, MatCardModule, fadeInContent, MatListModule, MatProgressBarModule, MatTabsModule, MatSidenavModule, MatSlideToggleModule, MatMenuModule } from '@angular/material'; // Import kendo angular ui import { ButtonsModule } from '@progress/kendo-angular-buttons'; import { GridModule, ExcelModule } from '@progress/kendo-angular-grid'; import { DropDownsModule } from '@progress/kendo-angular-dropdowns'; import { InputsModule } from '@progress/kendo-angular-inputs'; import { DateInputsModule } from '@progress/kendo-angular-dateinputs'; import { DialogsModule } from '@progress/kendo-angular-dialog'; import { RTL } from '@progress/kendo-angular-l10n'; import { LayoutModule } from '@progress/kendo-angular-layout'; import { WindowService, WindowRef, WindowCloseResult, DialogService, DialogRef, DialogCloseResult } from '@progress/kendo-angular-dialog'; import { SnotifyModule, SnotifyService, SnotifyPosition, SnotifyToastConfig, ToastDefaults } from 'ng-snotify'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, // material MatSidenavModule, MatSlideToggleModule, MatInputModule, MatFormFieldModule, MatButtonModule, MatButtonToggleModule, MatDialogModule, MatIconModule, MatSelectModule, MatToolbarModule, MatDatepickerModule, MatCheckboxModule, MatRadioModule, MatCardModule, MatMenuModule, MatListModule, MatProgressBarModule, MatTabsModule, // kendo-angular ButtonsModule, GridModule, ExcelModule, DropDownsModule, InputsModule, DateInputsModule, DialogsModule, LayoutModule, SnotifyModule, ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
همانطور که میبینید بدون افزودن کامپوننت جدیدی، حجم خروجی از 222KB به 582KB رسیدهاست. معمولا در هر پروژه نیاز به تعدادی دایرکتیو و سرویس پایه نیز میباشد که کم کم به حجم خروجی صفحات میافزاید. در نظر بگیرید که هنوز هیچ قالب خاصی برای صفحه مورد نظرمان استفاده نشده و به حجم 582KB رسیدهایم. برای نمونه میتوانیم سری به سایت madewithangular.com بزنیم و حجم خروجی تعدادی از سایتهای نوشته شدهی با انگیولار را بررسی کنیم. سایتهای زیر خروجی بالای 1.5MB دارند. همچنین سایتی را که خودم تقریبا یک سال پیش شروع کرده بودم، حجم خروجی آن 2.7MB است:
دلیل بالا رفتن حجم خروجی، اضافه شدن فایلهای JavaScript و style-sheet به bundle اصلی پروژه است. برای مثال حجم فایل main.js را در نمونههای ذکر شده بررسی کنید.
در قسمتهای بعدی، به نحوهی کار صحیح با انگیولار میپردازیم و حجم صفحات را بررسی میکنیم.
برای این کار روش استاندارد و مستقیمی وجود ندارد؛ اما با استفاده از بعضی از روشها مانند ارسال عکس مخفی، برخی تگهایی که یک آدرس را فراخوانی میکنند (مانند bgsound)، کلیک روی لینک و یا ترکیبی از چند روش میتوان تا حدود زیادی به اهداف فوق رسید.
روشی که بررسی خواهیم کرد، مبتنی بر ارسال عکس بوده و برای شروع، به یک عکس بسیار کوچک (1X1) با حجم کم و غیرقابل دید (Transparent) نیاز داریم که میتوانید یکی از آنها را از این آدرس انتخاب کنید. در این روش به همراه ایمیل، این عکس را ارسال خواهیم کرد (در تگ img). عکس باید بگونهای باشد که ظاهر ایمیل را تحت تأثیر قرار ندهد و آدرس عکس باید طوری تنظیم شود که به برنامه ما یک درخواست را ارسال کند و برنامه با توجه به درخواست ارسال شده، عملیات لازم را انجام دهد.
مثالی از عکسی که باید ارسال شود:
<img src='http://www.example.com/1x1.gif' style='width:1px;height:1px;'>
private bool SendEmail(string mail_to, string mail_subject, string mail_body) { bool result = false; try { SmtpClient client = new SmtpClient("smtp.gmail.com"); client.UseDefaultCredentials = false; client.EnableSsl = true; client.Port = 587; client.Credentials = new NetworkCredential("from@mail.com", "your_gmail_password");
MailMessage mailMessage = new MailMessage(); mailMessage.From = new MailAddress("from@mail.com"); mailMessage.To.Add(mail_to); mailMessage.Body = mail_body; mailMessage.Subject = mail_subject; client.Send(mailMessage); result = true; } catch (Exception ex) { result = false; } return result; }
public IActionResult SendEmailWithTransparentImage() { var email_body = "your_email_body"; var imageUrl = Url.Action("ImageRequestFromEmail", "Home", new {user_id=12345}, protocol: Url.ActionContext.HttpContext.Request.Scheme); var imageTag = $"<img src='{imageUrl}' style='width:1px;height:1px;'>"; email_body += imageTag; var result = SendEmail("to@email.com", "test image", email_body); return View(result); }
[Route("1x1.gif")] public IActionResult ImageResponse() { //درخواست عکس ارسال شده و اینجا باید عملیات دلخواه را انجام دهیم var emailOpenDate = DateTime.Now; //سپس عکس را در جواب درخواست ارسال میکنیم byte[] imagegBytes = Convert.FromBase64String("R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAI="); return File(imagegBytes, "image/gif"); }
<img src='http://www.example.com/1x1.gif?user_id=12345' style='width:1px;height:1px;'>
var imageUrl = Url.Action("ImageResponse, "Home", new {user_id=12345},protocol: Url.ActionContext.HttpContext.Request.Scheme);
public IActionResult ImageResponse(int user_id)
حذف تگهای زاید دریافتی از متون MS-Word
- Observable state: در MobX نیز همانند Redux، کل شیء state به صورت یک شیء جاوا اسکریپتی ارائه میشود؛ با این تفاوت که در اینجا این شیء، یک Observable است که نمونهای از مفهوم آنرا در مثال قسمت قبل بررسی کردیم.
- Actions: متدهایی هستند که state را تغییر میدهند.
- Computed properties: در مورد مفهوم خواص محاسباتی در قسمت قبل بحث کردیم. این خواص، مقدار خود را بر اساس تغییرات سایر خواص Observable، به روز میکنند.
- Reactions: سبب بروز اثرات جانبی (side effects) میشوند؛ مانند تعامل با دنیای خارج. نمونهای از آن، متد autorun است که تغییرات Observableها را ردیابی میکند.
برای مثال خاصیت محاسباتی fullName، تغییرات سایر خواص Observable را احساس کرده و مقدار خودش را به روز میکند. سپس یک Reaction به آن، میتواند به روز رسانی DOM، جهت نمایش این تغییرات باشد و یا نمونهی دیگری که میتواند بسیاری از این مفاهیم را نمایش دهد، کلاس زیر است:
import { action, observable, computed } from 'mobx'; class PizzaCalculator { @observable numberOfPeople = 0; @observable slicesPerPerson = 2; @observable slicesPerPie = 8; @computed get slicesNeeded() { console.log('Getting slices needed'); return this.numberOfPeople * this.slicesPerPerson; } @computed get piesNeeded() { console.log('Getting pies needed'); return Math.ceil(this.slicesNeeded / this.slicesPerPie); } @action addGuest() { this.numberOfPeople!++; } }
- برای مثال زمانیکه تعریف observable numberOfPeople@ را داریم، به این معنا است که میخواهیم تغییرات تعداد افراد را تحت نظر قرار دهیم و اگر تغییری در مقدار آن صورت گرفت، آنگاه مقدار خواص محاسباتی که با computed@ مزین شدهاند، به صورت خودکار به روز رسانی شوند.
- action@ به این معنا است که متدی در اینجا، سبب بروز تغییری در state کلاس جاری میشود. MobX به همراه یک strict mode است که اگر فعال باشد، ذکر تزئین کنندهی action@ بر روی یک چنین متدهایی ضروری است، در غیراینصورت، الزامی به درج آن نیست.
در این قطعه کد تعدای console.log را هم ملاحظه میکنید. علت آن نمایش مفهوم کش کردن اطلاعات در MobX است. فرض کنید برای بار اول، مقدار یکی از خواصی را که به صورت observable تعریف شدهاند، تغییر میدهیم. در این حالت تمام خواص محاسباتی وابستهی به آنها، به صورت خودکار مجددا محاسبه شده و console.logها را نیز مشاهده خواهیم کرد. اگر برای بار دوم همین فراخوانی صورت گیرد و تغییری در مقادیر خواص observable صورت نگیرد، MobX از نگارش کش شدهی این خواص محاسباتی استفاده میکند و بیجهت سبب رندر مجدد UI نخواهد شد که در نهایت کارآیی بالایی را سبب خواهد شد. برای پیاده سازی یک چنین قابلیتی با Redux باید از مفهومی مانند React.memo و Memoization و کتابخانههای کمکی مانند Reselect استفاده کرد؛ اما در اینجا به صورت توکار و خودکار اعمال میشود.
ساختارهای دادهای که توسط MobX پشتیبانی میشوند
MobX از اکثر ساختارهای دادهای متداول در جاوا اسکریپت پشتیبانی میکند؛ برای مثال:
- اشیاء مانند ({})observable
- آرایهها مانند ([])observable
- Maps مانند observable(new Map())
چند نکته:
- همانطور که در قسمت قبل نیز ذکر شد، decorators در اصل یکسری تابع هستند و برای مثال میتوان observable را به صورت observable@ و یا به صورت یک تابع معمولی مورد استفاده قرار داد.
- اگر شیءای را به صورت ({})observable معرفی کنیم، با افزودن خواصی به آن پس از این فراخوانی، این خواص دیگر مورد ردیابی قرار نخواهند گرفت. علت آنرا هم در شبهکد زیر میتوان مشاهده کرد:
const extendObservable = (target, source) => { source.keys().forEach(key => { const wrappedInObservable = observable(source[key]); Object.defineProperty(target, key, { set: value.set. get: value.get }); }); };
برای رفع این مشکل میتوان از Map استفاده کرد. یعنی در اینجا اگر قرار است تعداد خواص اشیاء را به صورت پویا تغییر دهید، آنها را به صورت Map تعریف کنید؛ چون متد set آن توسط observableها ردیابی میشود.
استفاده از MobX با React توسط کتابخانهی mobx-react
تا اینجا MobX را به صورت متکی به خود مورد بررسی قرار دادیم. اکنون قصد داریم آنرا به یک برنامهی React متصل کنیم. برای اینکار کتابخانههای زیادی وجود دارند که در این قسمت کلیات روش کار با کتابخانهی mobx-react را در بین آنها بررسی میکنیم.
نصب کتابخانهی mobx-react
ابتدا نیاز است تا این کتابخانه را نصب کنیم:
> npm install --save mobx mobx-react
تحت نظر قرار دادن کامپوننتها
در ادامه پس از نصب کتابخانهی mobx-react، نیاز است کامپوننتها را تحت نظر MobX قرار دهیم که اینکار را میتوان توسط تزئین کنندهی observer آن انجام داد. همانطور که عنوان شد، تزئین کنندهها را میتوان به صورت معمولی observer@ به یک کلاس و یا به صورت فراخوانی تابع، برای مثال به یک کامپوننت تابعی اعمال کرد. برای نمونه کامپوننتهای کلاسی را به نحو زیر میتوان با observer@ مزین کرد:
import { observer } from "mobx-react"; @observer class Counter extends Component {
و یا کامپوننتهای تابعی را میتوان توسط متد observer به صورت زیر محصور کرد:
const Counter = observer(({ count }) => { return ( // ... ); });
class ContainerComponent extends Component () { componentDidMount() { this.stopListening = autorun(() => this.render()); } componentWillUnmount() { this.stopListening(); } render() { … } }
تعریف مخزن و اتصال آن به کامپوننتها
کار شیء Provider که بالاترین کامپوننت را در سلسله مراتب کامپوننتها محصور میکند، ارائهی store، به تمام کامپوننتهای فرزند است. در Redux فقط یک store را داریم که به شیء Provider آن ارسال میکنیم. اما در حین کار با MobX چنین محدودیتی وجود ندارد و میتوان چندین store را تعریف کرد و در اختیار برنامه قرار داد که شبهکد نحوهی تعریف آن به صورت زیر است:
import { Provider } from 'mobx-react'; import ItemStore from './store/ItemStore'; import Application from './components/Application'; const itemStore = new ItemStore(); ReactDOM.render( <Provider itemStore={itemStore}> <Application /> </Provider>, document.getElementById('root'), );
@inject('itemStore') class NewItem extends Component { // ...
const UnpackedItems = inject('itemStore')( observer(({ itemStore }) => ( // ... )), );
یک مثال: پیاده سازی مثال شمارشگر قسمت سوم این سری با mobx-react
در ادامه قصد داریم برنامهی شمارشگر ارائه شده در قسمت سوم بررسی redux را با mobx پیاده سازی کنیم. به همین جهت یک پروژهی جدید React را ایجاد میکنیم:
> create-react-app state-management-with-mobx-part2 > cd state-management-with-mobx-part2 > npm start
> npm install --save mobx mobx-react bootstrap
import "bootstrap/dist/css/bootstrap.css";
پس از آن فایل src\index.js را به صورت زیر تغییر میدهیم:
import "./index.css"; import "bootstrap/dist/css/bootstrap.css"; import { autorun, decorate, observable } from "mobx"; import React from "react"; import ReactDOM from "react-dom"; import Counter from "./components/Counter"; import * as serviceWorker from "./serviceWorker"; class Count { value = 0; increment = () => { this.value++; }; decrement = () => { this.value--; }; } decorate(Count, { value: observable }); const count = (window.count = new Count()); autorun(() => console.log("The count changed!", count.value)); ReactDOM.render( <main className="container"> <Counter count={count} /> </main>, document.getElementById("root") ); serviceWorker.unregister();
- در قسمت قبل، روش تحت نظر قرار دادن یک شیء متداول جاوا اسکریپتی را توسط متد observable مشاهده کردیم. در اینجا نگارش کلاسی آن مثال را بر اساس کلاس Count مشاهده میکنید. اگر نخواهیم از decorator ای مانند observable@ بر روی خاصیت value این کلاس استفاده کنیم، روش تابعی آنرا با فراخوانی متد decorate و ذکر نوع کلاس و سپس خاصیتی که باید به صورت observable تحت نظر قرار گیرد، در اینجا مشاهده میکنید. این هم یک روش کار با mobx است.
- پس از ایجاد کلاس Count که در اینجا نقش store را نیز بازی میکند، یک وهلهی جدید را از آن ساخته و به متغیر count در این ماژول و همچنین window.count انتساب میدهیم. انتساب window.count سبب میشود تا بتوان در کنسول توسعه دهندگان مرورگر، با نوشتن count و سپس enter، به محتویات این متغیر دسترسی یافت و یا حتی آنرا تغییر داد؛ مانند تصویر زیر که بلافاصله این تغییر، در UI برنامه نیز منعکس میشود:
- در اینجا تعریف شیء Provider را که پیشتر در مورد آن بحث کردیم، مشاهده نمیکنید؛ چون با تک کامپوننت Counter تعریف شدهی در این مثال، نیازی به آن نیست. میتوان این شیء store را به صورت مستقیم به props کامپوننت Counter ارسال کرد.
اکنون تعریف کامپوننت شمارشگر واقع در فایل src\components\Counter.jsx به صورت زیر خواهد بود که این کامپوننت، count را به صورت props دریافت میکند:
import { observer } from "mobx-react"; import React from "react"; const Counter = observer(({ count }) => { return ( <section className="card mt-5"> <div className="card-body text-center"> <span className="badge m-2 badge-primary">{count.value}</span> </div> <div className="card-footer"> <div className="d-flex justify-content-center align-items-center"> <button className="btn btn-secondary btn-sm" onClick={count.increment} > + </button> <button className="btn btn-secondary btn-sm m-2" onClick={count.decrement} > - </button> </div> </div> </section> ); }); export default Counter;
تا زمانیکه کامپوننت، با تابع observer محصور شدهاست، به props رسیده گوش فرا داده و خواص و اشیاء observable آنرا تشخیص میدهد و سبب رندر مجدد کامپوننت، با تغییری در آنها خواهد شد.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-with-mobx-part2.zip
سلام و ممنون
بنده از این روش استفاده کرده بودم و نهایتا برای خودکار سازی این اعمال از افزونه ExportTemplate(vsix).vsix ویژوال استودیو استفاده کردم
طریقه استفاده اون هم به این صورت هستش که پس از نصب گزینه Export Template as VSIX... در منوی فایل ظاهر میشه و با کلیک بر روی اون تمامی پروژههای موجود در Solution جاری رو لیست میکنه و میتونید انتخاب کنید و Export کنید
خطای واردکردن(Import) فایل کلید
- راه دوم:
دستور زیر را در خط فرمان VS.NET وارد کنید (البته run as admin فراموش نشود و فرمان باید در همان پوشهای که فایل pfx هست اجرا شود):
sn -i key.pfx VS_KEY_2B6952079A0469C6
افزونه VSCommands را نصب کنید. دقیقا جایی که خطای فوق ظاهر میشود، کلیک راست کرده و گزینه apply fix را انتخاب کنید. همان راه دوم را به صورت خودکار اعمال میکند.