نظرات مطالب
Want to deepen your understanding of promises, async
and await
? The course will guide you there through 47 live examples and exercises. To get started, just click through to the first lesson: Why async, anyway?.
اشتراکها
مطالب مفید در مورد CQRS
Welcome to the Edument CQRS and Intentful Testing Starter Kit project page. On this page, you will find links to our open sourced CQRS Starter Kit on GitHub as well as tutorials to get you started.
یکی از راحتترین راههای افزودن پکیجهای برنامه نویسی به پروژههای دات نت، از طریق Nuget
میباشد. این ابزار به قدری راحت است که من تصمیم گرفتم پکیجهای تیممان را
از طریق این سیستم دریافت کنیم. مزیت آن هم این است که بچههای تیم همیشه
به پکیجها دسترسی راحتتری دارند و هم اینکه در آینده به روز رسانی
سادهتری خواهند داشت. با توجه به اینکه سایت اصلی تنها پکیجهای عمومی را
پشتیبانی میکند و چیزی تحت عنوان پکیجهای شخصی ندارد، پس باید خودمان این
سرویس را راه اندازی کنیم. برای راه اندازی این سیستم میتوان آن را بر روی
سیستم شخصی قرار داد و یا اینکه به صورت اینترنتی بر روی یک سرور به آن دسترسی
داشته باشیم. در این مقاله این دو روش را بررسی میکنم:
نصب بر روی یک سیستم شخصی یا لوکال
در اولین قدم، شما باید یک دایرکتوری را در سیستم خود درست کنید تا پکیجهای خود را داخل آن قرار دهید. پنجرهی Package manager Settings را باز کنید و در آن گزینهی Package Sources را انتخاب کنید. سپس در کادر باز شده، بر روی دکمهی افزودن، در بالا کلیک کنید تا در پایین کادر، از شما نام محل توزیع بسته و آدرس آن را بپرسد:
بعد از ورود اطلاعات، بر روی دکمهی Update کلیک کنید. از این پس این دایرکتوری، منبع پکیجهای شماست و برای دریافت پکیجها از این آدرس میتوانید از طریق منوی کشویی موجود در کنسول، پکیج جدید خودتان را انتخاب کنید:
اگر میخواهید میتوانید این دایرکتوری را به اشتراک بگذارید تا دیگر افراد حاضر در شبکهی محلی هم بتوانند آن را به عنوان منبع توزیع خود به سیستم اضافه کنند.
مرحلهی بعدی این است که از طریق ابزار خط فرمان نیوگت نسخه 3.3 پکیجهایتان را به دایرکتوری مربوطه انتقال دهید. نحوهی صدا زدن این دستور به شکل زیر است:
این دستور تمام پکیجهایی را که شما در مسیر اولی قرار دادهاید، به داخل
دایرکتوری test منتقل میکند. ولی اگر قصد دارید که فقط یکی از پکیجها را
به این دایرکتوری انتقال دهید از دستور زیر استفاده کنید:
اینبار با کلمهی رزرو شدهی add و بعد از آن نام پکیچ، دایرکتوری منبع را به آن معرفی میکنیم.
ساخت منبع راه دور (اینترنت)
شما با استفاده از ویژوال استودیو و انجام چند عمل ساده میتوانید پکیجهای خودتان را مدیریت کنید. برای شروع، یک پروژهی تحت وب خام (Empty) را ایجاد کنید و در کنسول Nuget دستور زیر را وارد کنید:
شما با نصب این بسته و وابستگیهایش، به راحتی یک سیستم مدیریت بسته را دارید.
ممکن است مدتی برای نصب طول بکشد و در نهایت از شما بخواهد که فایل
web.config را رونویسی کند که شما اجازهی آن را صادر خواهید کرد. بعد از
اتمام نصب، فایل web.config را گشوده و در خط زیر، خصوصیت Value را به یک
دایرکتوری که دلخواه که مدنظر شماست تغییر دهید. این آدرس دهی میتواند به صورت مطلق
باشد، یا آدرس مجازی آن را وارد کنید؛ یا اگر هم خالی بگذارید به طور پیش
فرض دایرکتوری Packages را در نظر میگیرد:
حال فایلهای دایرکتوری محلی خود را به دایرکتوری Packages انتقال دهید و سپس سایت را بر روی IIS، هاست نمایید. از این پس منبع شما به صورت آنلاین مانند آدرس زیر در دسترس خواهد بود.
در صورتی که قصد دارید مستقیما بستهای را به سمت سرور push کنید، از یک رمز عبور قدرتمند که آن را میتوانید در web.config، بخش apiKey وارد نمایید، استفاده کنید. اگر هم نمیخواهید، میتوانید در تگ RequiredApiKey در خصوصیت Value، مقدار false را وارد نمایید.
برای اینکار میتوانید از دستور زیر استفاده کنید:
البته اگر قبل از دستور بالا، دستور زیر را وار کنید، دیگر نیازی نیست تا برای دستورات بعدی تا مدتی ApiKey را وارد نمایید.
پی نوشت: برای داشتن نیوگت شخصی سایتهای نظیر Nuget Server و Myget ( به همراه پشتیبانی از مخازن npm و Bower ) هم این سرویس را ارائه میکنند. ولی باید هزینهی آن را پرداخت کنید. البته سایت GemFury مخازن رایگان مختلفی چون Nuget را نیز پشتیبانی میکند.
نصب بر روی یک سیستم شخصی یا لوکال
در اولین قدم، شما باید یک دایرکتوری را در سیستم خود درست کنید تا پکیجهای خود را داخل آن قرار دهید. پنجرهی Package manager Settings را باز کنید و در آن گزینهی Package Sources را انتخاب کنید. سپس در کادر باز شده، بر روی دکمهی افزودن، در بالا کلیک کنید تا در پایین کادر، از شما نام محل توزیع بسته و آدرس آن را بپرسد:
بعد از ورود اطلاعات، بر روی دکمهی Update کلیک کنید. از این پس این دایرکتوری، منبع پکیجهای شماست و برای دریافت پکیجها از این آدرس میتوانید از طریق منوی کشویی موجود در کنسول، پکیج جدید خودتان را انتخاب کنید:
اگر میخواهید میتوانید این دایرکتوری را به اشتراک بگذارید تا دیگر افراد حاضر در شبکهی محلی هم بتوانند آن را به عنوان منبع توزیع خود به سیستم اضافه کنند.
مرحلهی بعدی این است که از طریق ابزار خط فرمان نیوگت نسخه 3.3 پکیجهایتان را به دایرکتوری مربوطه انتقال دهید. نحوهی صدا زدن این دستور به شکل زیر است:
nuget init e:\nuget\ e:\nuget\test
nuget add GMap.Net.1.0.1.nupkg -source e:\nuget\test
ساخت منبع راه دور (اینترنت)
شما با استفاده از ویژوال استودیو و انجام چند عمل ساده میتوانید پکیجهای خودتان را مدیریت کنید. برای شروع، یک پروژهی تحت وب خام (Empty) را ایجاد کنید و در کنسول Nuget دستور زیر را وارد کنید:
Install-Package NuGet.Server
<appSettings> <!-- Set the value here to specify your custom packages folder. --> <add key="packagesPath" value="×\Packages" /> </appSettings>
(هر محلی که نصب کنید طبق الگوی مسیریابی، آدرس nuget را در انتها وارد کنید)
Dotnettips.info/nuget
در صورتی که قصد دارید مستقیما بستهای را به سمت سرور push کنید، از یک رمز عبور قدرتمند که آن را میتوانید در web.config، بخش apiKey وارد نمایید، استفاده کنید. اگر هم نمیخواهید، میتوانید در تگ RequiredApiKey در خصوصیت Value، مقدار false را وارد نمایید.
برای اینکار میتوانید از دستور زیر استفاده کنید:
nuget push GMap.Net.1.0.1.nupkg -source http://Dotenettips.info/nuget (ApiKey)
nuget setapikey -source https://www.dntips.ir/Nuget (ApiKey)
در این مقاله هدف این است که GraphQL را در ASP.NET Core راه اندازی کنیم. از یک کتابخانه ثالث برای آسانتر کردن یکپارچگی استفاده میکنیم و همچنین با جزئیات، توضیح خواهیم داد که چگونه میتوان از elementهای مربوط به GraphQL مثل (Type ،Query و Schema) برای کامل کردن فرآیند یکپارچگی در ASP.NET Core استفاده کنیم.
GraphQL و تفاوتهای آن با REST
GraphQl یک query language میباشد که queryها را با استفاده از type systemهایی که ما برای دادهها تعریف میکنیم، اجرا میکند. GraphQL به هیچ زبان یا پایگاه داده مشخصی گره نخورده است.
- GraphQL نیازمند رفت و برگشتهای کمتری به server، به منظور بازیابی دادهها برای template یا view است. همراه با REST ما مجبور هستیم که چندیدن endpoint مثلا (... api/students, api/courses, api/instructors ) را برای گرفتن همه دادههای که برای template یا view نیاز داریم ملاقات کنیم؛ ولی این شرایط در GraphQL برقرار نیست. با GraphQL ما تنها یک query را ایجاد میکنیم که چندین تابع (resolver) را در سمت سرور فراخوانی میکند و همه دادهها را از منابع مختلفی، در یک درخواست برگشت میدهد.
- همراه با REST، همانطور که Application ما رشد میکند، تعداد endpointها هم زیاد میشوند که این نیازمند زمان بیشتری برای نگهداری میباشد. اما با GraphQL ما تنها یک endpoint داریم؛ همین!
- با استفاده از GraphQL، ما هرگز به مشکل گرفتن دادههایی کم یا زیاد از منبع روبرو نخواهیم شد. به این خاطر است که ما queryها را با فیلدهایی که چیزهایی را که نیاز داریم، نشان میدهند، تعریف میکنیم. در این صورت ما همیشه چیزهایی را که درخواست دادهایم، دریافت میکنیم.
بنابراین اگر یک query شبیه زیر را ارسال کنیم :
query OwnersQuery { owners { name account { type } } }
{ "data": { "owners": [ { "name": "John Doe", "accounts": [ { "type": "Cash" }, { "type": "Savings" } ] } ] } }
شروع کار
یک پروژه جدید را با استفاده از دستور زیر ایجاد میکنیم:
dotnet new api -n ASPCoreGraphQL
پوشه Contracts شامل واسطهای مورد نیاز برای repository logic میباشد:
namespace ASPCoreGraphQL.Contracts { public interface IOwnerRepository { } }
namespace ASPCoreGraphQL.Contracts { public interface IAccountRepository { } }
در پوشه Models، کلاسهای مدل را نگه داری میکنیم؛ به همراه یک کلاس context و کلاسهای configuration:
public class Owner { [Key] public Guid Id { get; set; } [Required(ErrorMessage = "Name is required")] public string Name { get; set; } public string Address { get; set; } public ICollection<Account> Accounts { get; set; } } public class Account { [Key] public Guid Id { get; set; } [Required(ErrorMessage = "Type is required")] public TypeOfAccount Type { get; set; } public string Description { get; set; } [ForeignKey("OwnerId")] public Guid OwnerId { get; set; } public Owner Owner { get; set; } } public enum TypeOfAccount { Cash, Savings, Expense, Income }
public class ApplicationContext : DbContext { public ApplicationContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { } public DbSet<Owner> Owners { get; set; } public DbSet<Account> Accounts { get; set; } }
در پوشه Repository، کلاسهای مرتبط با منطق بازیابی دادهها را داریم:
public class OwnerRepository : IOwnerRepository { private readonly ApplicationContext _context; public OwnerRepository(ApplicationContext context) { _context = context; } }
public class AccountRepository : IAccountRepository { private readonly ApplicationContext _context; public AccountRepository(ApplicationContext context) { _context = context; } }
repository logic، یک راه اندازی ابتدایی بدون هیچ لایه اضافهتری است.
کلاس context و کلاسهای repository، در فایل Startup.cs ثبت میشوند:
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationContext>(opt => opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddScoped<IOwnerRepository, OwnerRepository>(); services.AddScoped<IAccountRepository, AccountRepository>(); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2) .AddJsonOptions(options => options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore); }
Integration of GraphQL in ASP.NET Core
برای کار کردن با GraphQL در ابتدا نیاز است که کتابخانه GraphQL را نصب کنیم. به همین منظور در ترمینال مربوط به VS Code، دستور زیر را وارد میکنیم:
dotnet add package GraphQL
و همچنین کتابخانهی زیر که به ما کمک میکند تا GraphQL.NET را به عنوان یک وابستگی دریافت کنیم:
dotnet add package GraphQL.Server.Transports.AspNetCore
dotnet add package GraphQL.Server.Ui.Playground
(Creating GraphQL Specific Objects (Type, Query, Schema
کار را با ایجاد کردن یک پوشه جدید به نام GraphQL و سپس در آن ایجاد یک پوشه دیگر به نام GraphQLSchema، شروع میکنیم. در پوشه GraphQLSchema، یک کلاس را به نام AppSchema، ایجاد میکنیم.
کلاس AppSchema باید از کلاس Schema ارث بری کند تا در فضای نام GraphQL.Types قرار گیرد. در سازنده این کلاس، IDependencyResolver را تزریق میکنیم که قرار است به ما کمک کند تا اشیاء Query ،Mutation یا Subscription را resolve کنیم:
public class AppSchema : Schema { public AppSchema(IDependencyResolver resolver) :base(resolver) { } }
فعلا این کلاس را در همین حالت میگذاریم و سپس یک پوشه را به نام GraphQLTypes در پوشه GraphQL ایجاد میکنیم. در پوشه GraphQLTypes یک کلاس را به نام OwnerType ایجاد میکنیم:
public class OwnerType : ObjectGraphType<Owner> { public OwnerType() { Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the owner object."); Field(x => x.Name).Description("Name property from the owner object."); Field(x => x.Address).Description("Address property from the owner object."); } }
از کلاس OwnerType به عنوان یک جایگزین برای مدل Owner درون یک GraphQL API استفاده میکنیم. این کلاس از نوع جنریک ObjectGraphType ارث بری میکند. با متد Field، فیلدهایی را که بیانگر خصوصیات مدل Owner میباشند، مشخص میکنیم.
در ابتدا واسط IOwnerRepository و کلاس OwnerRepository را به حالت زیر ویرایش میکنیم:
public interface IOwnerRepository { IEnumerable<Owner> GetAll(); } public class OwnerRepository : IOwnerRepository { private readonly ApplicationContext _context; public OwnerRepository(ApplicationContext context) { _context = context; } public IEnumerable<Owner> GetAll() => _context.Owners.ToList(); }
در ادامه یک پوشه دیگر را به نام GraphQLQueries در پوشهی GraphQL ایجاد و سپس در آن یک کلاس را به نام AppQuery ایجاد و آن را به حالت زیر ویرایش میکنیم:
public class AppQuery : ObjectGraphType { public AppQuery(IOwnerRepository repository) { Field<ListGraphType<OwnerType>>( "owners", resolve: context => repository.GetAll() ); } }
توضیحات AppQuery
همانطور که میبینیم این کلاس از ObjectGraphType ارث بری میکند. در سازنده کلاس، IOwnerRepository را تزریق میکنیم و یک فیلد را به منظور برگشت دادن نتیجه برای یک Query مشخص، ایجاد میکنیم. در این کلاس، از نوع جنریک متد Field، استفاده کردهایم که تعدادی type را به عنوان یک پارامتر جنریک پذیرش میکند که این بیانگر GraphQL.NET برای type های معمول در NET. میباشد. علاوه بر ListGraphType، نوعهایی مثل IntGraphType و StringGraphType و ... وجود دارند (لیست کامل).
پارامتر owners نام فیلد میباشد (query مربوط به کلاینت باید با این نام مطابقت داشته باشد) و پارامتر دوم نتیجه میباشد.
بعد از انجام این مقدمات، اکنون کلاس AppSchema را باز میکنیم و به حالت زیر آن را ویرایش میکنیم:
public class AppSchema : Schema { public AppSchema(IDependencyResolver resolver) :base(resolver) { Query = resolver.Resolve<AppQuery>(); } }
Libraries and Schema Registration
در کلاس Startup نیاز است کتابخانههای نصب شده و هم چنین کلاس schema ایجاد شده را ثبت کنیم. این کار را با ویرایش کردن متد ConfigureServices و Configure انجام میدهیم:
public void ConfigureServices(IServiceCollection services) { ... services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService)); services.AddScoped<AppSchema>(); services.AddGraphQL(o => { o.ExposeExceptions = false; }) .AddGraphTypes(ServiceLifetime.Scoped); ... }
در نهایت در متد schema Configure را به خط لوله درخواستها (request’s pipeline) اضافه میکنیم و هم چنین ابزار Playground UI:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { ... app.UseGraphQL<AppSchema>(); app.UseGraphQLPlayground(options: new GraphQLPlaygroundOptions()); app.UseMvc(); }
برای آزمایش GraphQL API امان، از ابزار GraphQL.UI.Playground استفاده میکنیم. در ابتدا پروژه را با دستور زیر اجرا میکنیم:
dotnet run
و سپس در مرورگر به این آدرس میرویم:
https://localhost:5001/ui/playground
کدهای مربوط به این قسمت را از اینجا دریافت کنید .ASPCoreGraphQL.zip
یک روش کار کردن با پروژههای SPA، توسعهی مجزای قسمتهای front-end و back-end است. برای مثال پروژهی React را به صورت جداگانهای توسعه میدهیم، پروژهی ASP.NET Core را نیز به همین صورت. هنگام آزمایش برنامه، در یکی دستور npm start را اجرا میکنیم تا وب سرور آزمایشی React، آنرا در آدرس http://localhost:3000 قابل دسترسی کند و در دیگری دستور dotnet watch run را صادر میکنیم تا برنامهی وب ASP.NET Core را بر روی آدرس https://localhost:5001 مهیا کند. سپس برای اینکه از پورت 3000 بتوان با پورت 5001 کار کرد، نیاز خواهد بود تا CORS را در برنامهی ASP.NET Core فعالسازی کنیم. در حین ارائهی نهایی برنامه نیز هر کدام را به صورت مجزا publish کرده و بعد هم خروجی نهایی پروژهی SPA را در پوشهی wwwroot برنامهی وب کپی میکنیم تا قابل دسترسی و استفاده شود. روش دیگری نیز برای یکی/ساده سازی این تجربه وجود دارد که در این مطلب به آن خواهیم پرداخت.
پیشنیاز: ایجاد یک برنامهی خالی React و ASP.NET Core
یک پوشهی خالی را ایجاد کرده و در آن دستور dotnet new react را صادر کنید، تا قالب خاص پروژههای React یکی سازی شدهی با پروژههای ASP.NET Core، یک پروژهی جدید را ایجاد کند.
همانطور که در تصویر فوق نیز مشاهده میکنید، این پروژه از دو برنامه تشکیل شدهاست:
الف) برنامهی SPA که در پوشهی ClientApp قرار گرفتهاست و شامل کدهای کامل یک برنامهی React است.
ب) برنامهی سمت سرور ASP.NET Core که یک برنامهی متداول وب، به همراه فایل Startup.cs و سایر فایلهای مورد نیاز آن است.
در ادامه نکات ویژهی ساختار این پروژه را بررسی خواهیم کرد.
تجربهی توسعهی برنامهها توسط این قالب ویژه
اکنون اگر این پروژهی وب را برای مثال با فشردن دکمهی F5 و یا اجرای دستور dotnet run، اجرا کنیم، چه اتفاقی رخ میدهد؟
- به صورت خلاصه برنامهی ASP.NET Core شروع به کار کرده و سبب ارائه همزمان برنامهی SPA نیز خواهد شد.
- پورتی که برنامهی وب بر روی آن قرار دارد، با پورتی که برنامهی React بر روی روی آن ارائه میشود، یکی است. یعنی نیازی به تنظیمات CORS را ندارد.
- در این حالت اگر در برنامهی React تغییری را ایجاد کنیم (در هر قسمتی از آن)، hot reloading آن هنوز هم برقرار است و سبب بارگذاری مجدد برنامهی SPA در مرورگر خواهد شد و برای اینکار نیازی به توقف و راه اندازی مجدد برنامهی ASP.NET Core نیست.
اما این تجربهی روان کاربری و توسعه، چگونه حاصل شدهاست؟
بررسی ساختار فایل Startup.cs یک پروژهی مبتنی بر dotnet new react
برای درک نحوهی عملکرد این قالب ویژه، نیاز است از فایل Startup.cs آن شروع کرد.
در ابتدا تعریف فضای نام SpaServices را مشاهده میکنید. بستهی متناظر با آن در فایل csproj برنامه به صورت زیر ثبت شدهاست:
این بسته، همان بستهی جدید SpaServices است و در NET 5x. نیز پشتیبانی خواهد شد .
در متد ConfigureServices، ثبت سرویسهای مرتبط با فایلهای استاتیک پروژهی SPA، توسط متد AddSpaStaticFiles صورت گرفتهاست. در اینجا RootPath آن، به پوشهی ClientApp/build اشاره میکند. البته این پوشه هنوز در این ساختار، قابل مشاهده نیست؛ اما زمانیکه پروژهی ASP.NET Core را برای ارائهی نهایی، publish کردیم، به صورت خودکار ایجاد شده و حاوی فایلهای قابل ارائهی برنامهی React نیز خواهد بود.
قسمت مهم دیگر کلاس آغازین برنامه، متد Configure آن است:
در اینجا ثبت سه میان افزار جدید را مشاهده میکنید:
- متد UseSpaStaticFiles، سبب ثبت میانافزاری میشود که امکان دسترسی به فایلهای استاتیک پوشهی ClientApp حاوی برنامهی React را میسر میکند؛ مسیر این پوشه را در متد ConfigureServices تنظیم کردیم.
- متد UseSpa، سبب ثبت میانافزاری میشود که دو کار مهم را انجام میدهد:
1- کار اصلی آن، ثبت مسیریابی معروف catch all است تا مسیریابیهایی را که توسط کنترلرهای برنامهی ASP.NET Core مدیریت نمیشوند، به سمت برنامهی React هدایت کند. برای مثال مسیر https://localhost:5001/api/users به یک کنترلر API برنامهی سمت سرور ختم میشود، اما سایر مسیرها مانند https://localhost:5001/login قرار است صفحهی login برنامهی سمت کلاینت SPA را نمایش دهند و متناظر با اکشن متد خاصی در کنترلرهای برنامهی وب ما نیستند. در این حالت، کار این مسیریابی catch all، نمایش صفحهی پیشفرض برنامهی SPA است.
2- بررسی میکند که آیا شرایط IsDevelopment برقرار است؟ آیا در حال توسعهی برنامه هستیم؟ اگر بله، میانافزار دیگری را به نام UseReactDevelopmentServer، اجرا و ثبت میکند.
برای درک عملکرد میانافزار ReactDevelopmentServer نیاز است به سورس آن مراجعه کرد. این میانافزار بر اساس پارامتر start ای که دریافت میکند، سبب اجرای npm run start خواهد شد. به این ترتیب دیگر نیازی به اجرای جداگانهی این دستور نخواهد بود و همچنین این اجرا، به همراه تنظیمات proxy مخصوصی نیز هست تا پورت اجرایی برنامهی React و برنامهی ASP.NET Core یکی شده و دیگر نیازی به تنظیمات CORS مخصوص برنامههای React نباشد. بنابراین hot reloading ای که از آن صحبت شد، توسط ASP.NET Core مدیریت نمیشود. در پشت صحنه همان npm run start اصلی برنامههای React، در حال اجرای وب سرور آزمایشی React است که از hot reloading پشتیبانی میکند.
یک مشکل: با این تنظیم، هربار که برنامهی ASP.NET Core اجرا میشود (به علت تغییرات در کدها و فایلهای پروژه)، سبب اجرای مجدد و پشت صحنهی react development server نیز خواهد شد که ... آغاز برنامه را در حالت توسعه، کند میکند. برای رفع این مشکل میتوان این وب سرور توسعهی برنامههای React را به صورت جداگانهای اجرا کرد و فقط تنظیمات پروکسی آنرا در اینجا ذکر نمود:
در اینجا فقط کافی است سطر UseReactDevelopmentServer را با تنظیم UseProxyToSpaDevelopmentServer که به آدرس وب سرور توسعهی برنامههای React اشاره میکند، تنظیم کنیم. بدیهی است در اینجا حالت باید از طریق خط فرمان به پوشهی clientApp وارد شد و دستور npm start را یکبار به صورت دستی اجرا کرد، تا این وب سرور، راه اندازی شود.
تغییرات ویژهی فایل csproj برنامه
اگر به فایل csproj برنامه دقت کنیم، دو تغییر جدید نیز در آن قابل مشاهده هستند:
الف) نصب خودکار وابستگیهای برنامهی client
در این تنظیم، در حالت build و debug، ابتدا بررسی میکند که آیا پوشهی node_modules برنامهی SPA وجود دارد؟ اگر خیر، ابتدا مطمئن میشود که node.js بر روی سیستم نصب است و سپس دستور npm install را صادر میکند تا تمام وابستگیهای برنامهی client، دریافت و نصب شوند.
ب) یکی کردن تجربهی publish برنامهی ASP.NET Core با publish پروژههای React
میانافزار ReactDevelopmentServer کار اجرا و پروکسی دستور npm run start را در حالت توسعه انجام میدهد. اما در حالت ارائهی نهایی چطور؟ در اینجا نیاز است دستور npm run build اجرا شده و فایلهای مخصوص ارائهی نهایی برنامهی React تولید و سپس به پوشهی wwwroot، کپی شوند. تنظیم فوق، دقیقا همین کار را در حین publish برنامهی ASP.NET Core، به صورت خودکار انجام میدهد و شامل این مراحل است:
- ابتدا npm install را جهت اطمینان از به روز بودن وابستگیهای برنامه مجددا اجرا میکند.
- سپس npm run build را برای تولید فایلهای قابل ارائهی برنامهی React اجرا میکند.
- در آخر تمام فایلهای پوشهی ClientApp/build تولیدی را به بستهی نهایی توزیعی برنامهی ASP.NET Core، اضافه میکند.
پیشنیاز: ایجاد یک برنامهی خالی React و ASP.NET Core
یک پوشهی خالی را ایجاد کرده و در آن دستور dotnet new react را صادر کنید، تا قالب خاص پروژههای React یکی سازی شدهی با پروژههای ASP.NET Core، یک پروژهی جدید را ایجاد کند.
همانطور که در تصویر فوق نیز مشاهده میکنید، این پروژه از دو برنامه تشکیل شدهاست:
الف) برنامهی SPA که در پوشهی ClientApp قرار گرفتهاست و شامل کدهای کامل یک برنامهی React است.
ب) برنامهی سمت سرور ASP.NET Core که یک برنامهی متداول وب، به همراه فایل Startup.cs و سایر فایلهای مورد نیاز آن است.
در ادامه نکات ویژهی ساختار این پروژه را بررسی خواهیم کرد.
تجربهی توسعهی برنامهها توسط این قالب ویژه
اکنون اگر این پروژهی وب را برای مثال با فشردن دکمهی F5 و یا اجرای دستور dotnet run، اجرا کنیم، چه اتفاقی رخ میدهد؟
- به صورت خلاصه برنامهی ASP.NET Core شروع به کار کرده و سبب ارائه همزمان برنامهی SPA نیز خواهد شد.
- پورتی که برنامهی وب بر روی آن قرار دارد، با پورتی که برنامهی React بر روی روی آن ارائه میشود، یکی است. یعنی نیازی به تنظیمات CORS را ندارد.
- در این حالت اگر در برنامهی React تغییری را ایجاد کنیم (در هر قسمتی از آن)، hot reloading آن هنوز هم برقرار است و سبب بارگذاری مجدد برنامهی SPA در مرورگر خواهد شد و برای اینکار نیازی به توقف و راه اندازی مجدد برنامهی ASP.NET Core نیست.
اما این تجربهی روان کاربری و توسعه، چگونه حاصل شدهاست؟
بررسی ساختار فایل Startup.cs یک پروژهی مبتنی بر dotnet new react
برای درک نحوهی عملکرد این قالب ویژه، نیاز است از فایل Startup.cs آن شروع کرد.
// ... using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer; namespace dotnet_template_sample { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); // In production, the React files will be served from this directory services.AddSpaStaticFiles(configuration => { configuration.RootPath = "ClientApp/build"; }); }
<ItemGroup> <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.2" /> </ItemGroup>
در متد ConfigureServices، ثبت سرویسهای مرتبط با فایلهای استاتیک پروژهی SPA، توسط متد AddSpaStaticFiles صورت گرفتهاست. در اینجا RootPath آن، به پوشهی ClientApp/build اشاره میکند. البته این پوشه هنوز در این ساختار، قابل مشاهده نیست؛ اما زمانیکه پروژهی ASP.NET Core را برای ارائهی نهایی، publish کردیم، به صورت خودکار ایجاد شده و حاوی فایلهای قابل ارائهی برنامهی React نیز خواهد بود.
قسمت مهم دیگر کلاس آغازین برنامه، متد Configure آن است:
// ... using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer; namespace dotnet_template_sample { public class Startup { // ... public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ... app.UseStaticFiles(); app.UseSpaStaticFiles(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller}/{action=Index}/{id?}"); }); app.UseSpa(spa => { spa.Options.SourcePath = "ClientApp"; if (env.IsDevelopment()) { spa.UseReactDevelopmentServer(npmScript: "start"); } }); } } }
- متد UseSpaStaticFiles، سبب ثبت میانافزاری میشود که امکان دسترسی به فایلهای استاتیک پوشهی ClientApp حاوی برنامهی React را میسر میکند؛ مسیر این پوشه را در متد ConfigureServices تنظیم کردیم.
- متد UseSpa، سبب ثبت میانافزاری میشود که دو کار مهم را انجام میدهد:
1- کار اصلی آن، ثبت مسیریابی معروف catch all است تا مسیریابیهایی را که توسط کنترلرهای برنامهی ASP.NET Core مدیریت نمیشوند، به سمت برنامهی React هدایت کند. برای مثال مسیر https://localhost:5001/api/users به یک کنترلر API برنامهی سمت سرور ختم میشود، اما سایر مسیرها مانند https://localhost:5001/login قرار است صفحهی login برنامهی سمت کلاینت SPA را نمایش دهند و متناظر با اکشن متد خاصی در کنترلرهای برنامهی وب ما نیستند. در این حالت، کار این مسیریابی catch all، نمایش صفحهی پیشفرض برنامهی SPA است.
2- بررسی میکند که آیا شرایط IsDevelopment برقرار است؟ آیا در حال توسعهی برنامه هستیم؟ اگر بله، میانافزار دیگری را به نام UseReactDevelopmentServer، اجرا و ثبت میکند.
برای درک عملکرد میانافزار ReactDevelopmentServer نیاز است به سورس آن مراجعه کرد. این میانافزار بر اساس پارامتر start ای که دریافت میکند، سبب اجرای npm run start خواهد شد. به این ترتیب دیگر نیازی به اجرای جداگانهی این دستور نخواهد بود و همچنین این اجرا، به همراه تنظیمات proxy مخصوصی نیز هست تا پورت اجرایی برنامهی React و برنامهی ASP.NET Core یکی شده و دیگر نیازی به تنظیمات CORS مخصوص برنامههای React نباشد. بنابراین hot reloading ای که از آن صحبت شد، توسط ASP.NET Core مدیریت نمیشود. در پشت صحنه همان npm run start اصلی برنامههای React، در حال اجرای وب سرور آزمایشی React است که از hot reloading پشتیبانی میکند.
یک مشکل: با این تنظیم، هربار که برنامهی ASP.NET Core اجرا میشود (به علت تغییرات در کدها و فایلهای پروژه)، سبب اجرای مجدد و پشت صحنهی react development server نیز خواهد شد که ... آغاز برنامه را در حالت توسعه، کند میکند. برای رفع این مشکل میتوان این وب سرور توسعهی برنامههای React را به صورت جداگانهای اجرا کرد و فقط تنظیمات پروکسی آنرا در اینجا ذکر نمود:
// replace spa.UseReactDevelopmentServer(npmScript: "start"); // with spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
تغییرات ویژهی فایل csproj برنامه
اگر به فایل csproj برنامه دقت کنیم، دو تغییر جدید نیز در آن قابل مشاهده هستند:
الف) نصب خودکار وابستگیهای برنامهی client
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') "> <!-- Ensure Node.js is installed --> <Exec Command="node --version" ContinueOnError="true"> <Output TaskParameter="ExitCode" PropertyName="ErrorCode" /> </Exec> <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." /> <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." /> <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" /> </Target>
ب) یکی کردن تجربهی publish برنامهی ASP.NET Core با publish پروژههای React
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish"> <!-- As part of publishing, ensure the JS resources are freshly built in production mode --> <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" /> <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" /> <!-- Include the newly-built files in the publish output --> <ItemGroup> <DistFiles Include="$(SpaRoot)build\**" /> <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)"> <RelativePath>%(DistFiles.Identity)</RelativePath> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> <ExcludeFromSingleFile>true</ExcludeFromSingleFile> </ResolvedFileToPublish> </ItemGroup> </Target>
- ابتدا npm install را جهت اطمینان از به روز بودن وابستگیهای برنامه مجددا اجرا میکند.
- سپس npm run build را برای تولید فایلهای قابل ارائهی برنامهی React اجرا میکند.
- در آخر تمام فایلهای پوشهی ClientApp/build تولیدی را به بستهی نهایی توزیعی برنامهی ASP.NET Core، اضافه میکند.