تعیین نوع props و state در کامپوننتهای کلاسی
برای این منظور ابتدا فایل جدید src\components\BigC.tsx را ایجاد کرده و سپس توسط میانبر rcc، ساختار ابتدایی این کامپوننت را ایجاد میکنیم. در ادامه آنرا در کامپوننت src\App.tsx استفاده خواهیم کرد:
یکی از مزایای دیگر کار با تایپاسکریپت، فعال شدن intellisense مرتبط با افزونههایی مانند typescript hero و auto import است که نمونهای از آنرا در تصویر فوق مشاهده میکنید. فقط کافی است نام المان مرتبط را نوشت و سپس با استفاده از افزونههای یاد شده، به صورت خودکار import آنرا اضافه کرد.
پس از افزودن المان این کامپوننت، اگر سعی کنیم یک props را برای آن تعریف کنیم، بلافاصله خطای تعریف نشده بودن آنرا دریافت خواهیم کرد:
در اینجا نیز همانند کامپوننتهای تابعی، باید ابتدا نوع Props را تعیین کرد و تنها تفاوت آن، روش معرفی این type جدید به کامپوننت است:
import React, { Component } from "react"; type Props = { title: string; }; type State = { status: string; }; export default class BigC extends Component<Props, State> { render() { return ( <div> <h1>I'm in a class component</h1> </div> ); } }
مابقی نکات آن مانند props اختیاری و غیره، تفاوت خاصی را با کامپوننتهای تابعی که پیشتر بررسی کردیم، ندارند و یکی هستند.
مقایسهای بین Types و Interfaces
در این سری بیشتر از types استفاده شد، تا اینترفیسهای تایپاسکریپت. برای مثال بجای نوع زیر:
type State = { rValue: boolean; };
interface State { rValue: boolean; }
در تایپاسکریپت، از type بیشتر برای تعریف یک نوع جدید استفاده میشود، اما از اینترفیسها برای تعریف موجودیتها و entities. برای مثال از type میتوان برای تعریف نامی برای نوعهای primitive, union, intersection نیز استفاده کرد، اما اینترفیسها همواره برای تعیین نوع اشیاء مورد استفاده قرار میگیرند. نمیتوان دو نوع همنام را تعریف کرد، اما اگر دو اینترفیس هم نام را تعریف کنید، با هم یکی میشوند. نوعها برخلاف اینترفیسها قابلیت پیاده سازی نداشته و بیشتر جنبهی یک اعلان را دارند و ... در نهایت استفادهی از هر کدام بیشتر یک انتخاب شخصی است.
منابع تکمیلی
یکی از بهترین منابع تکمیلی استفادهی از TypeScript در React، مخزن کد react-typescript-cheatsheet است که به همراه مجموعهای از نکات مرتبط است.
Blazor 5x - قسمت 23 - احراز هویت و اعتبارسنجی کاربران Blazor Server - بخش 3 - کار با نقشهای کاربران
کار با Authentication State از طریق کدنویسی
فرض کنید در کامپوننت HotelRoomUpsert.razor نمیخواهیم دسترسیها را به کمک اعمال ویژگی attribute [Authorize]@ محدود کنیم؛ میخواهیم اینکار را از طریق کدنویسی مستقیم انجام دهیم:
// ... @*@attribute [Authorize]*@ @code { [CascadingParameter] public Task<AuthenticationState> AuthenticationState { get; set; } protected override async Task OnInitializedAsync() { var authenticationState = await AuthenticationState; if (!authenticationState.User.Identity.IsAuthenticated) { var uri = new Uri(NavigationManager.Uri); NavigationManager.NavigateTo($"/identity/account/login?returnUrl={uri.LocalPath}"); } // ...
- سپس یک پارامتر ویژه را از نوع CascadingParameter، به نام AuthenticationState تعریف کردیم. این خاصیت از طریق کامپوننت CascadingAuthenticationState که در قسمت قبل به فایل BlazorServer.App\App.razor اضافه کردیم، تامین میشود.
- در آخر در روال رویدادگردان OnInitializedAsync، بر اساس آن میتوان به اطلاعات User جاری وارد شدهی به سیستم دسترسی یافت و برای مثال اگر اعتبارسنجی نشده بود، با استفاده از NavigationManager، او را به صفحهی لاگین هدایت میکنیم.
- در اینجا روش ارسال آدرس صفحهی فعلی را نیز مشاهده میکنید. این امر سبب میشود تا پس از لاگین، کاربر مجددا به همین صفحه هدایت شود.
authenticationState، امکانات بیشتری را نیز در اختیار ما قرار میدهد؛ برای مثال با استفاده از متد ()authenticationState.User.IsInRole آن میتوان دسترسی به قسمتی را بر اساس نقشهای خاصی محدود کرد.
ثبت کاربر ادمین Identity
در ادامه میخواهیم دسترسی به کامپوننتهای مختلف را بر اساس نقشها، محدود کنیم. به همین جهت نیاز است تعدادی نقش و یک کاربر ادمین را به بانک اطلاعاتی برنامه اضافه کنیم. برای اینکار به پروژهی BlazorServer.Common مراجعه کرده و تعدادی نقش ثابت را تعریف میکنیم:
namespace BlazorServer.Common { public static class ConstantRoles { public const string Admin = nameof(Admin); public const string Customer = nameof(Customer); public const string Employee = nameof(Employee); } }
سپس در فایل BlazorServer.App\appsettings.json، مشخصات ابتدایی کاربر ادمین را ثبت میکنیم:
{ "AdminUserSeed": { "UserName": "vahid@dntips.ir", "Password": "123@456#Pass", "Email": "vahid@dntips.ir" } }
namespace BlazorServer.Models { public class AdminUserSeed { public string UserName { get; set; } public string Password { get; set; } public string Email { get; set; } } }
namespace BlazorServer.App { public class Startup { public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddOptions<AdminUserSeed>().Bind(Configuration.GetSection("AdminUserSeed")); // ...
using System; using System.Linq; using System.Threading.Tasks; using BlazorServer.Common; using BlazorServer.DataAccess; using BlazorServer.Models; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; namespace BlazorServer.Services { public class IdentityDbInitializer : IIdentityDbInitializer { private readonly ApplicationDbContext _dbContext; private readonly UserManager<IdentityUser> _userManager; private readonly RoleManager<IdentityRole> _roleManager; private readonly IOptions<AdminUserSeed> _adminUserSeedOptions; public IdentityDbInitializer( ApplicationDbContext dbContext, UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<AdminUserSeed> adminUserSeedOptions) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _roleManager = roleManager ?? throw new ArgumentNullException(nameof(roleManager)); _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); _adminUserSeedOptions = adminUserSeedOptions ?? throw new ArgumentNullException(nameof(adminUserSeedOptions)); } public async Task SeedDatabaseWithAdminUserAsync() { if (_dbContext.Roles.Any(role => role.Name == ConstantRoles.Admin)) { return; } await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Admin)); await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Customer)); await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Employee)); await _userManager.CreateAsync( new IdentityUser { UserName = _adminUserSeedOptions.Value.UserName, Email = _adminUserSeedOptions.Value.Email, EmailConfirmed = true }, _adminUserSeedOptions.Value.Password); var user = await _dbContext.Users.FirstAsync(u => u.Email == _adminUserSeedOptions.Value.Email); await _userManager.AddToRoleAsync(user, ConstantRoles.Admin); } } }
پس از تعریف این سرویس، نیاز است آنرا به سیستم تزریق وابستگیهای برنامه اضافه کرد:
namespace BlazorServer.App { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddScoped<IIdentityDbInitializer, IdentityDbInitializer>(); // ...
using System; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Polly; namespace BlazorServer.DataAccess.Utils { public static class MigrationHelpers { public static void MigrateDbContext<TContext>( this IServiceProvider serviceProvider, Action<IServiceProvider> postMigrationAction ) where TContext : DbContext { using var scope = serviceProvider.CreateScope(); var scopedServiceProvider = scope.ServiceProvider; var logger = scopedServiceProvider.GetRequiredService<ILogger<TContext>>(); using var context = scopedServiceProvider.GetService<TContext>(); logger.LogInformation($"Migrating the DB associated with the context {typeof(TContext).Name}"); var retry = Policy.Handle<Exception>().WaitAndRetry(new[] { TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(15) }); retry.Execute(() => { context.Database.Migrate(); postMigrationAction(scopedServiceProvider); }); logger.LogInformation($"Migrated the DB associated with the context {typeof(TContext).Name}"); } } }
کار متد الحاقی فوق، دریافت یک IServiceProvider است که به سرویسهای اصلی برنامه اشاره میکند. سپس بر اساس آن، یک Scoped ServiceProvider را ایجاد میکند تا درون آن بتوان با Context برنامه در طی مدت کوتاهی کار کرد و در پایان آن، سرویسهای ایجاد شده را Dispose کرد.
در این متد ابتدا Database.Migrate فراخوانی میشود تا اگر مرحلهای از Migrations برنامه هنوز به بانک اطلاعاتی اعمال نشده، کار اجرا و اعمال آن انجام شود. سپس یک متد سفارشی را از فراخوان دریافت کرده و اجرا میکند. برای مثال توسط آن میتوان IIdentityDbInitializer در فایل BlazorServer.App\Program.cs به صوت زیر فراخوانی کرد:
public static void Main(string[] args) { var host = CreateHostBuilder(args).Build(); host.Services.MigrateDbContext<ApplicationDbContext>( scopedServiceProvider => scopedServiceProvider.GetRequiredService<IIdentityDbInitializer>() .SeedDatabaseWithAdminUserAsync() .GetAwaiter() .GetResult() ); host.Run(); }
و همچنین کاربر پیشفرض سیستم را نیز میتوان مشاهده کرد:
که نقش ادمین و کاربر پیشفرض، به این صورت به هم مرتبط شدهاند (یک رابطهی many-to-many برقرار است):
محدود کردن دسترسی کاربران بر اساس نقشها
پس از ایجاد کاربر ادمین و تعریف نقشهای پیشفرض، اکنون محدود کردن دسترسی به کامپوننتهای برنامه بر اساس نقشها، سادهاست. برای این منظور فقط کافی است لیست نقشهای مدنظر را که میتوانند توسط کاما از هم جدا شوند، به ویژگی Authorize کامپوننتها معرفی کرد:
@attribute [Authorize(Roles = ConstantRoles.Admin)]
protected override async Task OnInitializedAsync() { var authenticationState = await AuthenticationState; if (!authenticationState.User.Identity.IsAuthenticated || !authenticationState.User.IsInRole(ConstantRoles.Admin)) { var uri = new Uri(NavigationManager.Uri); NavigationManager.NavigateTo($"/identity/account/login?returnUrl={uri.LocalPath}"); }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-23.zip
سایت iplocationtools بانک اطلاعاتی موقعیت مکانی IP های خود را که ماهیانه نیز به روز خواهد شد، به رایگان برای دریافت قرار داده است. فرمت تعاریف جداول و عبارت insert آن مطابق دستورات بانک اطلاعاتی MySQL است.
تنها بازیگر شاخص یازده سال قبل در زمینه توسعه برنامههای مبتنی بر QT ، برنامه KDevelop بوده است. این وضعیت با ارائه Qt Creator بهبود یافته است ...
- شرکت Red gate که پیشتر برنامهی معروف Reflector را در جهت اهداف تبلیغاتی خود تصاحب کرده بود، اکنون در ادامهی همین سیاست، کتاب Illustrated C# 2008 را نیز به رایگان ارائه داده است.
- سایت DelphiFeeds.com با طراحی جدید خود چند روزی است که متحول شده است. سایتی شبیه به سایت مهندس خودمان که بعد از عید معلوم نیست چه بلایی سرش آمده است و جای خالی آن احساس میشود، هر چند سایت IDevCenter نیز همین کار را به صورت تخصصیتری ارائه میدهد.
- Microsoft MED-V 1.0 در جهت توسعهی اهداف مجازی سازی مایکروسافت ارائه شد.
- 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
- «با HttpHandler بیشتر آشنا شوید»
یکی از بزرگترین تغییرات ASP.NET Core نسبت به نگارشهای قبلی آن، مدیریت HTTP pipeline آن است. به عنوان یک توسعه دهندهی ASP.NET به طور قطع با مفاهیمی مانند HttpHandler و HttpModules آشنایی دارید و ... هر دوی اینها با نگارش جدید ASP.NET حذف و با مفهوم جدیدی به نام Middleware جایگزین شدهاند.
- HttpHandlerها عموما مبتنی بر پسوندهای فایلها عمل میکنند. برای نمونه، رایجترین آنها ASP.NET page handler است که هرگاه درخواستی، ختم شدهی به پسوند aspx، به موتور ASP.NET Web forms وارد شد، به این page handler، جهت پردازش نهایی هدایت میشود. محل تنظیم آنها نیز در فایل web.config است.
- HttpModuleها مبتنی بر رخدادها عمل میکنند. HttpModuleها به عنوان جزئی از request pipeline عمل کرده و دسترسی کاملی دارند به رخدادهای طول عمر درخواست رسیده. آنها را میتوان توسط فایلهای global.asax و یا web.config تنظیم کرد.
- MiddleWareها را که جزئی از طراحی OWIN نیز هستند، میتوان به عنوان کامپوننتهای کوچکی از برنامه که قابلیت یکپارچه شدن با HTTP request pipeline را دارند، درنظر گرفت. عملکرد آنها ترکیبی است از هر دوی HttpHandler و HttpModuleها که در نگارشهای قبلی ASP.NET مورد استفاده بودند. از MiddleWareها میتوان برای پیاده سازی اعمال مختلفی جهت پردازش درخواستهای رسیده مانند اعتبارسنجی، کار با سشنها، ثبت وقایع سیستم، مسیریابی و غیره استفاده کرد. OWIN یا Open Web Interface for .NET به توسعه دهندهها امکان غیروابسته کردن برنامهی ASP.NET خود را از وب سرور مورد استفاده میدهد. به علاوه OWIN امکان پلاگین نویسی اجزای مختلف برنامه را بدون وابسته کردن آنها به یکدیگر فراهم میکند. برای مثال میتوان یک پلاگین logger را تهیه کرد تا اطلاعات مختلفی را از درخواستهای رسیده ثبت کند.
مواردی که با ارائهی ASP.NET Core 1.0 حذف شدهاند
- System.Web: یکی از اهداف اصلی OWIN، مستقل کردن برنامهی وب، از هاست آن است تا بتوان از وب سرورهای بیشتری استفاده کرد. System.Web انحصارا برای IIS طراحی شده بود و در این نگارش دیگر وجود خارجی ندارد.
- HttpModules: با Middlewareها جایگزین شدهاند.
- HttpHandlers: البته HttpHandlers از زمان ارائهی اولین نگارش ASP.NET MVC، در چندین سال قبل منسوخ شدند. زیرا پردازش صفحات وب در ASP.NET MVC برخلاف وب فرمها، از فایلهایی با پسوندهای خاص شروع نمیشوند و نقطهی آغازین آنها، اکشن متدهای کنترلرها است.
- فایل global.asax: با حذف شدن HttpModules، دیگر ضرورتی به وجود این فایل نیست.
شباهتهای بینMiddleware و HttpModuleها
همانند HttpModuleها، Middlewareها نیز به ازای هر درخواست رسیده اجرا میشوند و از هر دو میتوان جهت تولید response ایی خاص استفاده کرد.
تفاوتهای بینMiddleware و HttpModuleها
- عموما HttpModule از طریق web.config و یا فایل global.asax تنظیم میشوند. اما Middlewareها تنها از طریق کد و در فایل Startup.cs برنامههای ASP.NET Core 1.0 قابل معرفی هستند.
- به عنوان یک توسعه دهنده، کنترلی را بر روی ترتیب اجرای انواع و اقسام HttpModuleها نداریم. این مشکل در Middlewareها برطرف شده و اکنون ترتیب اجرای آنها، دقیقا مطابق ترتیب افزوده شدن و تعریف آنها در فایل Startup.cs است.
- ترتیب اجرای HttpModuleها هرچه که باشد، برای حالتهای request و response یکی است. اما ترتیب اجرای Middlewareهای مخصوص response، عکس ترتیب اجرای Middlewareهای مخصوص request هستند (تصویر ذیل).
- HttpModuleها را صرفا جهت اتصال کدهایی به رخدادهای طول عمر برنامه میتوان طراحی کرد؛ اما Middleware مستقل هستند از این رخدادها.
- HttpModuleها به System.Web وابستهاند؛ برخلاف Middlewareها که وابستگی به هاست خود ندارند.
Middlewareهای توکار ASP.NET Core 1.0
ASP.NET Core 1.0 به همراه تعدادی Middleware توکار است؛ مانند:
- Authentication: جهت پشتیبانی از اعتبارسنجی
- CORS: برای فعال سازی Cross-Origin Resource Sharing
- Routing: جهت تعریف و محدود سازی مسیریابیهای برنامه
- Session: برای پشتیبانی و مدیریت سشنهای کاربران
- Diagnostics: پشتیبانی از صفحات خطا و اطلاعات زمان اجرای برنامه
و مواردی دیگر که برای توزیع فایلهای استاتیک و یا مرور محتویات پوشهها و امثال آنها طراحی شدهاند که در طی این مطلب و همچنین مطالب آتی، آنها را بررسی خواهیم کرد.
یک مثال: اگر یک پروژهی خالی ASP.NET Core 1.0 را در ویژوال استودیو ایجاد کنید، این پروژه قادر نیست حتی فایلهای استاتیک مانند تصاویر، فایلهای پیش فرض مانند index.html و یا قابلیت مرور سادهی فایلهای موجود در یک پوشه را ارائه دهد (حتی اگر به آنها نیازی نداشته باشید).
طراحی این نگارش از ASP.NET، مبتنی است بر سبک وزن بودن و ماژولار بودن. هر قابلیتی را که نیاز دارید، باید middleware آنرا نیز خودتان به صورت صریح اضافه کنید و یا در غیر اینصورت فعال نیست. برخلاف نگارشهای قبلی ASP.NET که HTTP Moduleهای از سشن گرفته تا اعتبارسنجی و غیره، همگی به صورت پیش فرض در فایل Web.Config فعال بودند؛ مگر اینکه آنها را به صورت دستی حذف میکردید.
ثبت و فعال سازی اولین Middleware
در ادامهی تکمیل و معرفی برنامهی خالی ASP.NET Core 1.0، قصد داریم یک Middleware توکار را به نام Welcome Page، بجای نمایش سطر Hello world پیش فرض این برنامه، ثبت و فعال سازی کنیم. این Middleware ویژه، در اسمبلی به نام Microsoft.AspNetCore.Diagnostics قرار دارد. اگر فایل project.json پیش فرض این پروژه را باز کنید، این اسمبلی به صورت پیش فرض در آن ثبت و معرفی شدهاست:
{ "dependencies": { "Microsoft.NETCore.App": { "version": "1.0.0", "type": "platform" }, "Microsoft.AspNetCore.Diagnostics": "1.0.0", "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", "Microsoft.Extensions.Logging.Console": "1.0.0" },
پس از اطمینان حاصل کردن از نصب بستهی نیوگت Microsoft.AspNetCore.Diagnostics، اکنون جهت معرفی Middleware توکار Welcome Page ، فایل Startup.cs را گشوده و کدهای آنرا به نحو ذیل تغییر دهید:
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; namespace Core1RtmEmptyTest { public class Startup { public void ConfigureServices(IServiceCollection services) { } public void Configure(IApplicationBuilder app) { app.UseWelcomePage(); app.Run(async (context) => { await context.Response.WriteAsync("Hello DNT!"); }); } } }
به علاوه middleware دومی را نیز با متد الحاقی Run مشاهده میکنید. به این نوع middlewareهای خاص، اصطلاحا terminal middleware میگویند. از این جهت که درخواستی را دریافت و یک response را تولید میکنند و کار همینجا خاتمه مییابد و زنجیرهی پردازشی middlewareها ادامه نخواهد یافت. در اینجا پارامتر context آن از نوع HttpContext است و باید دقت داشت، زمانیکه کار نوشتن در Response، در اینجا انجام شد، اگر پس از متد Run یک Middleware دیگر را ثبت کنید، هیچگاه اجرا نخواهد شد.
در این حالت اگر برنامه را اجرا کنید، خروجی ذیل را مشاهده خواهید کرد:
welcome page نیز یک terminal middleware است. به همین جهت middleware بعدی ثبت شدهی در اینجا یا همان متد Run، دیگر اجرا نخواهد شد.
در قسمتهای بعدی، تعداد بیشتری از Middlewareهای توکار ASP.NET Core 1.0 را بررسی خواهیم کرد.
Native .NET Core Support
In the release 2018.1, we present a full-featured report generator, created using the cross-platform technology — .NET Core. A full set of Web components such as the report designer as well as additional tools for quick export and report printing is available. The .NET Core components are included in the product Stimulsoft Reports.Web and Stimulsoft Reports.Ultimate.
زمانیکه کاربری برنامهی تک صفحهای وب را در مرورگر باز میکند، ابتدا فایل index.html را در پاسخ دریافت خواهد کرد. این فایل تعاریف مداخل مورد نیاز برای رندر آنرا مانند فایلهای جاوا اسکریپت و CSS، به همراه دارد. سپس این فایلها توسط مرورگر از سرور دریافت میشوند. در این حالت با پردازش این فایلها، کامپوننت ریشهی سایت بارگذاری میشود. پس از پایان آن، قالب این کامپوننت به کاربر نمایش داده خواهد شد. بنابر سرعت دریافت فایلها توسط کاربر، این آغاز میتواند اندکی کند باشد. البته با رعایت نکات گفتهی شدهی در مطلب «Angular CLI - قسمت پنجم - ساخت و توزیع برنامه» میتوان این حجم را توسط AoT و Tree-Shaking به میزان قابل ملاحظهای کاهش داد. به علاوه با فعالسازی Lazy loading میتوان قسمتهای مختلف برنامه را تبدیل به یک سری Bundle کرد که در زمان درخواست، بارگذاری میشوند. به این ترتیب حجم فایلهای ابتدایی که باید از سرور دریافت شوند بسیار کمتر شده و به علاوه با کاهش این حجم، مرورگر نیز باید میزان کمتری از کدها را در جهت نمایش اولین کامپوننت، پردازش و اجرا کند. در این حالت زمانیکه کاربری شروع به پیمایش مسیر یک ماژول خاص را میکند، آنگاه فایلهای مرتبط با آن از سرور دریافت و در مرورگر پردازش میشوند. بنابراین اگر کاربری به قسمتی دسترسی ندارد، نیازی هم به دریافت فایلهای آن نخواهد داشت؛ چون کار به فعالسازی مسیریابی آن ماژول نمیرسد.
آماده شدن جهت Lazy loading
پیش از Lazy loading یک قسمت از برنامه (که به آن async routing هم میگویند)، این قسمت باید دارای شرایطی باشد:
- این قسمت از برنامه حتما باید در یک ماژول تعریف شده باشد. از این جهت که Lazy loading، لیست کامپوننتهای قید شدهی در تعریف یک ماژول را بارگذاری میکند.
- تمام مسیرهای این ماژول باید در ذیل یک مسیر والد، گروه بندی شده باشند. از این جهت که Lazy loading فقط بر روی مسیر ریشهی والد تنظیم و بارگذاری میشود.
- این ماژول نباید در هیچ ماژول دیگری import شده باشد. اگر این ماژول ارجاعی را در سایر ماژولها داشته باشد، هیچ راهی بجز دریافت و کامپایل کامل آن توسط Angular وجود نخواهد داشت.
در مثال جاری این سری:
- تمام ویژگیهای قسمت مدیریت محصولات، داخل ماژول product.module.ts تعریف شدهاند. بنابراین اولین شرط Lazy loading آن برقرار است.
- در فایل product-routing.module.ts، کار گروه بندی مسیریابیها ذیل یک والد مشخص انجام شدهاست (همان قسمت ششم این سری). بنابراین شرط دوم lazy loading این ماژول نیز پیشتر پیاده سازی شدهاست.
- اما اگر به فایل src\app\app.module.ts مراجعه کنیم، ارجاعی به این ماژول در قسمت imports آن وجود دارد. بنابراین باید این ارجاع را حذف کنیم. در غیراینصورت کار دریافت کامل آن به همراه سایر ماژولهای برنامه، در همان ابتدای کار صورت خواهد گرفت.
بنابراین در فایل src\app\app.module.ts، ابتدا import فایل آنرا از ابتدای ماژول حذف و سپس ارجاع به نام کلاس کامپوننت ProductModule را نیز حذف میکنیم. در این حالت اگر از طریق منوی سایت سعی در دسترسی به این مسیرها کنیم، خطای 404 را دریافت خواهیم کرد؛ چون اکنون برنامه اطلاعاتی را در مورد نحوهی مسیریابی قسمت محصولات برنامه، ندارد.
Lazy loading یک ماژول
برای بارگذاری غیرهمزمان یک ماژول و یا همان Lazy loading، میتوان از خاصیت loadChildren تنظیمات مسیریابی، استفاده کرد:
{ path: 'products', loadChildren:'app/product/product.module#ProductModule' },
با این تنظیم، زمانیکه مسیر ریشهی produtcs درخواست شد، کار بارگذاری ماژول آن صورت گرفته و تنظیمات مسیریابی آن به سیستم اضافه میشود. به علاوه کار فعالسازی و نمایش کامپوننت آن را نیز انجام خواهد داد.
به همین منظور فایل src\app\app-routing.module.ts را گشوده و تنظیم فوق را به آن اضافه میکنیم:
const routes: Routes = [ { path: 'home', component: WelcomeComponent }, { path: 'welcome', redirectTo: 'home', pathMatch: 'full' }, { path: 'products', loadChildren: 'app/product/product.module#ProductModule' }, { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ];
chunk {0} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 165 kB {4} [initial] chunk {1} main.bundle.js, main.bundle.js.map (main) 32.7 kB {3} [initial] [rendered] chunk {2} styles.bundle.js, styles.bundle.js.map (styles) 129 kB {4} [initial] chunk {3} vendor.bundle.js, vendor.bundle.js.map (vendor) 2.72 MB [initial] [rendered] chunk {4} inline.bundle.js, inline.bundle.js.map (inline) 0 bytes [entry] chunk {5} 5.chunk.js, 5.chunk.js.map 51.1 kB {1} [rendered]
به علاوه اگر در منوی سایت بر روی لینک نمایش لیست محصولات کلیک کنیم، هنوز خروجی نمایش داده نمیشود (هرچند خطای 404 را هم دریافت نمیکنیم). علت اینجا است که اگر به فایل src\app\product\product-routing.module.ts مراجعه کنیم، تعریف این مسیر ریشه، در این فایل نیز وجود دارد:
const routes: Routes = [ { path: 'products', canActivate: [ AuthGuard ], children: [ ] } ];
import { AuthGuard } from './user/auth.guard'; const routes: Routes = [ { path: 'home', component: WelcomeComponent }, { path: 'welcome', redirectTo: 'home', pathMatch: 'full' }, { path: 'products', loadChildren: 'app/product/product.module#ProductModule', canActivate: [AuthGuard] }, { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ];
یک نکته: اکنون تنظیمات مسیریابی فایل src\app\product\product-routing.module.ts چنین شکلی را پیدا کردهاست:
const routes: Routes = [ { path: '', component: ProductListComponent }, { path: ':id', component: ProductDetailComponent, resolve: { product: ProductResolverService } }, { path: ':id/edit', component: ProductEditComponent, resolve: { product: ProductResolverService }, canDeactivate: [ProductEditGuard], children: [ { path: '', redirectTo: 'info', pathMatch: 'full' }, { path: 'info', component: ProductEditInfoComponent }, { path: 'tags', component: ProductEditTagsComponent } ] } ];
در این حالت اگر مسیر نمایش لیست محصولات را درخواست دهیم، مشاهده خواهیم کرد فایل 5.chunk.js که حاوی اطلاعات این ماژول است، به صورت مجزایی بارگذاری شده (lazy loading) و سپس با فعال شدن محافظ مسیر آن، صفحهی لاگین نمایش داده میشود:
این بارگذاری با تاخیر و در صورت نیاز، به دو علت آغاز برنامه را سریعتر میکند:
الف) مرورگر اطلاعی از وجود فایل 5.chunk.js در ابتدای کار نداشته و آنرا بارگذاری نمیکند (دریافت حجم کمتر، در آغاز نمایش برنامه).
ب) چون حجم کمتری از کدهای جاوا اسکریپت توسط مرورگر در آغاز کار دریافت میشود، کار پردازش و اجرای آنها نیز بسیار سریعتر خواهد شد.
بررسی محافظ canLoad
تعدادی از محافظهای مسیرها را در قسمت قبل بررسی کردیم. هنگامیکه کامپوننتها به صورت lazy loading فعالسازی شده و قالب آنها نمایش داده میشوند، میتوان از محافظ مسیر دیگری به نام canLoad نیز استفاده کرد و هدف از آن، بررسی منطقی، پیش از فعالسازی یک مسیر غیرهمزمان است. بنابراین اگر این محافظ false را برگرداند، حتی فایلهای اسکریپت این ماژول، بارگذاری اولیه نیز نخواهد شد. به این ترتیب کسانیکه دسترسی به یک مسیر را نداشته باشند، فایلهای اسکریپت متناظر با آنرا نیز دریافت نخواهند کرد.
در مثال جاری، اگر به برگهی network ابزار developer مرورگر دقت کنید، با درخواست نمایش مسیر لیست محصولات، ابتدا فایل js آن دریافت میشود که حاوی اطلاعات تمام کامپوننتها و قالبهای مرتبط با این مسیر است و سپس صفحهی login نمایش داده خواهد شد. بنابراین اگر کاربر به این قسمت دسترسی نداشته باشد، فایل js آن بیجهت دریافت و بارگذاری شدهاست. برای بهبود این وضعیت میتوان نمایش لاگین را پیش از بارگذاری فایل js این ماژول فعالسازی کرد و این مورد هدف اصلی محافظ canLoad است.
در ادامه برای تکمیل مثال جاری، میتوان AuthGuard را طوری تنظیم کرد که علاوه بر پیاده سازی CanActivate، اینترفیس CanLoad را نیز پیاده سازی کند:
import { ActivatedRouteSnapshot, RouterStateSnapshot, CanActivate, Router, CanLoad, Route } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate, CanLoad { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { return this.checkLoggedIn(state.url); } canLoad(route: Route): boolean { return this.checkLoggedIn(route.path); } // … the same as before }
مرحلهی بعد، تغییر فایل src\app\app-routing.module.ts و جایگزین کردن تعریف فعلی canActivate با canLoad است:
{ path: 'products', loadChildren: 'app/product/product.module#ProductModule', canLoad: [AuthGuard] },
پیش بارگذاری ماژولها
با فعالسازی lazy loading، ماژولهای مورد نیاز کاربر دیگر به همراه فایلهای js ابتدایی برنامه که در فایل index.html ارجاع مستقیمی به آنها دارند، ارائه نمیشوند و تنها در صورت درخواست مشاهدهی مسیری، کار بارگذاری آنها توسط برنامه صورت خواهد گرفت. همین مساله میتواند در بار اول نمایش این ماژولها تاخیر کوتاهی را سبب شود. به همین جهت قابلیت پیش بارگذاری ماژولها نیز در سیستم مسیریاب Angular پیش بینی شدهاست. به این قابلیت preloading و یا eager lazy loading نیز میگویند. در این حالت برنامه در پشت صحنه، کار پیش واکشی ماژولها را انجام میدهد و زمانیکه کاربری مسیری را درخواست میدهد، آْن مسیر را بدون درنگ مشاهده خواهد کرد.
بدیهی است این قابلیت نباید برای ماژولهایی که قرار است توسط کاربرانی خاص مشاهده شوند فعال شود و هدف آن دسترسی سریع به ماژولهای پرکاربرد برنامهاست.
در اینجا سه استراتژی پیش بارگذاری ماژولها میسر است:
- No preloading که حالت پیش فرض است.
- Preload all سبب پیش بارگذاری تمام قسمتهای lazy load برنامه میشود.
- Custom که اجازهی تعریف یک استراتژی سفارشی را میدهد.
برای مثال برای فعالسازی حالت Preload all، باید به فایل src\app\app-routing.module.ts مراجعه کرده و تغییرات ذیل را اعمال کنیم:
import { Routes, RouterModule, PreloadAllModules } from '@angular/router'; @NgModule({ imports: [RouterModule.forRoot( routes, { enableTracing: true, preloadingStrategy: PreloadAllModules /*, useHash: true*/ } )],
یک نکته: وجود محافظ canLoad، هر نوع استراتژی prealoading را غیرفعال میکند. اما prealoading با سایر انواع محافظها کار میکند.
بنابراین برای آزمایش تنظیم preloadingStrategy: PreloadAllModules، تعریف canLoad را به canActivate تغییر دهید.
تعریف استراتژیهای سفارشی پیش بارگذاری ماژولها
اگر نیاز به یک استراتژی پیش بارگذاری بهتر از هیچ یا همه باشد، میتوان یک استراتژی سفارشی را نیز تدارک دید و ایجاد آن سه مرحلهی ایجاد سرویس مرتبط، ثبت آن سرویس در ماژول و در آخر تنظیم مسیریابی را به همراه دارد.
برای این منظور ابتدا دستور ذیل را صادر کنید تا قالب ابتدایی سرویس SelectiveStrategy ایجاد شود:
>ng g s SelectiveStrategy -m app.module
installing service create src\app\selective-strategy.service.spec.ts create src\app\selective-strategy.service.ts update src\app\app.module.ts
سپس کدهای SelectiveStrategyService را به نحو ذیل تغییر دهید:
import { Injectable } from '@angular/core'; import { Route, PreloadingStrategy } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/of'; @Injectable() export class SelectiveStrategyService implements PreloadingStrategy { preload(route: Route, load: Function): Observable<any> { if (route.data && route.data['preload']) { return load(); } return Observable.of(null); } }
- پارامتر اول این متد، اطلاعاتی را در مورد مسیر جاری در اختیار ما قرار میدهد و دومین پارامتر آن متدی است که کار preloading را انجام میدهد.
- در اینجا است که تصمیم میگیریم ماژولی را preload کنیم یا خیر. برای نمونه در اینجا از خاصیت data مسیریابی استفاده شدهاست. این خاصیت نیز به یک مقدار ثابت اشاره میکند (قسمت «ارسال اطلاعات ثابت به مسیرهای مختلف برنامه» قسمت چهارم). برای مثال نام دلخواه آنرا preload گذاشتهایم و اگر مقدار آن به true تنظیم شده بود، آنگاه این مسیر preload خواهد شد. فراخوانی متد load در اینجا به معنای preloading این مسیر است. در غیراینصورت null را بازگشت میدهیم.
در ادامه نیاز است در فایل src\app\app-routing.module.ts، بجای معرفی PreloadAllModules، این استراتژی سفارشی خود را معرفی کرد:
import { SelectiveStrategyService } from './selective-strategy.service'; @NgModule({ imports: [RouterModule.forRoot( routes, { enableTracing: true, preloadingStrategy: SelectiveStrategyService /*, preloadingStrategy: PreloadAllModules*/ /*, useHash: true*/ } )],
{ path: 'products', loadChildren: 'app/product/product.module#ProductModule', //canLoad: [AuthGuard] canActivate: [AuthGuard], data: { preload: true } },
برای آزمایش آن، برنامه را مجدا اجرا کرده و صفحه را refresh کنید. سپس برگهی network ابزار developers را نیز باز نگه دارید. مشاهده خواهید کرد که علاوه بر فایلهای js اصلی برنامه که در فایل index.html ارجاعی را دارند، فایل 5.chunk.js نیز پیش بارگذاری شدهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-09.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
ارائهی MapKit JS از اپل
Use of the library does involve having an Apple Developer account at $99 a year , and to use the beta, developers will need to set up core Maps identifiers, keys, and tokens via their developer account. The beta version limits usage to 250,000 instantiations and 25,000 service calls, per API key per day, compared to Google's limit of 100,000 map instantiations per month on the free tier.