EF Code First #11
اینکه پوشه درست کنید، class library درست کنید. اسمهای متفاوت استفاده کنید. همه اینها خوب است.
باز هم در طراحی خودتون اومدید ORM رو مخفی کردید. کل بحث جاری این است که اینکار اتلاف وقت است. «فقط با یک Config ساده در DI این موضوع به طور کامل حل میشود» : .... نمیشود. خیر! قصد ندارم مواردی رو که عنوان کردم تکرار کنم. مدتی با یک ORM با قابلیتهای بالا کار کنید، این مطلب رو دیگر عنوان نخواهید کرد.
به نظر در مورد اسامی کمی تداخل اینجا هست. مفهومی رو که از Repository دنبال میکنید، همان چیزی است که در لایه سرویس من به آن اشاره کردم.
اگر علاقمند بودید، به پیاده سازی generic repository که لینک دادم مراجعه کنید.
واژههای مورد استفاده رو اگر یکسان کنیم شاید خیلی از سؤ تفاهمها برطرف شود.
برای انجام عملیات پرس و جوی LINQ با استفاده از روش پردازش موازی به راحتی میتوان الحاقیه AsParallel را به هر دادهای از نوع IEnumerable<T> افزود:
var data = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // پرس و جوی عادی var q1 = from i in data select i; // پرس و جو به شیوه موازی var q2 = from i in data.AsParallel() select i;
الحاقیه .AsParallel() در پرس و جویq2 نسخه موازی LINQ را بر روی متغیر data اجرا میکند و اگر همه چیز به صورت صحیح انجام شود هر دو پرس و جو باید نتایج یکسانی داشته باشند، اما نتایج عبارتند از :
//نتیجه اجرای q1 0 1 2 3 4 5 6 7 8 9 10 //نتیجه اجرای q2 0 6 1 7 2 8 3 9 4 10 5
همانطور که ملاحظه میکنید ترتیب واقعی نتایج اجرای پرس و جوها با یکدیگر متفاوتاند و نکته جالبتر آنکه با هر بار اجرای برنامه نتیجه اجرای پرس و جوی q2 با نتیجه سری قبل خودش متفاوت است که این تفاوت به چگونگی تقسیم بندی انجام کار میان هستههای سی پی یو، بستگی دارد. نکته بسیار مهم آن است که عملیات پردازش موازی خود را ملزم به حفظ ترتیب دادهها نمیداند مگر آنکه مجبورش کنیم و این رفتار پردازش موازی به دلیل بالا بردن راندمان عملیات است در نتیجه انجام پرس و جوهای موازی توسط الحاقیه .AsParallel() خیلی هم ساده نیست و ممکن است منجر به تولید نتایج ناخواسته شود.
حال اگر چگونگی ترتیب دادهها، برایمان مهم است به دو روش میتوانیم آن را انجام دهیم:
1- افزودن عبارت orderby به پرس و جو
2- استفاده از الحاقیه AsOrdered
var q3 = from i in data.AsParallel() orderby i select i; var q4 = from i in data.AsParallel().AsOrdered() select i;
که نتیجه انجام هر دو پرس و جوی بالا یکی خواهد بود. حال مسأله دیگر این است که آیا همیشه استفاده از پردازش موازی مفید خواهد بود یا خیر؟پاسخ این سؤال وابسته است به نوع مسأله و حجم داده مورد نظر و مشخصات سیستمی که قرار است از آن کد استفاده کند. چگونگی اندازه سرعت و مقدار مصرف حافظه در اجرای چهار پرس و جوی فوق در کامپیوتر من با پردازنده Intel Q9550 به شکل زیر است:
چند نکته کاربردی درباره Entity Framework
نکته: دقت کنید که تنها یک فولدر App_GlobalResources به هر پروزه میتوان افزود. همچنین در ریشه هر مسیر موجود در پروژه تنها میتوان یک فولدر Appp_LocalResources داشت. پس از افزودن هر یک از این فولدرهای مخصوص، منوی فوق به صورت زیر در خواهد آمد:
protected object GetLocalResourceObject(string resourceKey) protected object GetLocalResourceObject(string resourceKey, Type objType, string propName)
txtTest.Text = GetLocalResourceObject("txtTest.Text") as string;
protected object GetGlobalResourceObject(string className, string resourceKey) protected object GetGlobalResourceObject(string className, string resourceKey, Type objType, string propName)
TextBox1.Text = GetGlobalResourceObject("Resource1", "String1") as string;
public static object GetLocalResourceObject(string virtualPath, string resourceKey) public static object GetLocalResourceObject(string virtualPath, string resourceKey, CultureInfo culture)
txtTest.Text = HttpContext.GetLocalResourceObject("~/Default.aspx", "txtTest.Text") as string;
public static object GetGlobalResourceObject(string classKey, string resourceKey) public static object GetGlobalResourceObject(string classKey, string resourceKey, CultureInfo culture)
TextBox1.Text = HttpContext.GetGlobalResourceObject("Resource1", "String1") as string;
//------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. // Runtime Version:4.0.30319.17626 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> //------------------------------------------------------------------------------ namespace Resources { using System; /// <summary> /// A strongly-typed resource class, for looking up localized strings, etc. /// </summary> // This class was auto-generated by the StronglyTypedResourceBuilder // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option or rebuild the Visual Studio project. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Web.Application.StronglyTypedResourceProxyBuilder", "10.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resource1 { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Resource1() { } /// <summary> /// Returns the cached ResourceManager instance used by this class. /// </summary> [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Resources.Resource1", global::System.Reflection.Assembly.Load("App_GlobalResources")); resourceMan = temp; } return resourceMan; } } /// <summary> /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// </summary> [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } set { resourceCulture = value; } } /// <summary> /// Looks up a localized string similar to String1. /// </summary> internal static string String1 { get { return ResourceManager.GetString("String1", resourceCulture); } } } }
TextBox1.Text = Resources.Resource1.String1;
امکان تعریف HTML Forms استاندارد در Blazor 8x
فرمهای استاندارد HTML، پیش از ظهور جاوااسکریپت و SPAها وجود داشتند (دقیقا همان زمانیکه که فقط مفهوم SSR وجود خارجی داشت) و هنوز هم جزء مهمی از اغلب برنامههای وب را تشکیل میدهند. با ارائهی دات نت 8 و قابلیت server side rendering آن، کامپوننتهای برنامه، فقط یکبار در سمت سرور رندر شده و HTML سادهی آنها به سمت مرورگر کاربر بازگشت داده میشود. در این حالت، فرمهای استاندارد HTML، امکان دریافت ورودیهای کاربر و ارسال دادههای آنها را به سمت سرور میسر میکنند (چون دیگر خبری از اتصال دائم SignalR نیست و باید اطلاعات را به همان نحو استاندارد پروتکل HTTP، به سمت سرور Post کرد). در دات نت 8، دو راهحل برای کار با فرمها در برنامههای Blazor وجود دارد: استفاده از EditForm خود Blazor و یا استفاده از HTML forms استاندارد و ساده، به همان نحوی که بوده و هست.
روش کار با EditForm در برنامههای Blazor SSR
البته ما قصد استفاده از فرمهای سادهی HTML را در اینجا نداریم و ترجیح میدهیم که از همان EditForm استفاده کنیم. EditForms در Blazor بسیار مفید بوده و امکان بایند خواص یک مدل را به اجزای مختلف ورودیهای تعریف شدهی در آن میسر میکند و همچنین قابلیتهایی مانند اعتبارسنجی و امثال آنرا نیز به همراه دارد (اطلاعات بیشتر). اما چگونه میتوان از این امکان در برنامههای Blazor SSR نیز استفاده کرد؟
برای این منظور، ابتدا مثالی را به صورت زیر تکمیل میکنیم (که بر اساس قالب dotnet new blazor --interactivity Server تهیه شده) و سپس توضیحات آن ارائه خواهد شد:
الف) تهیه یک مدل برای تعریف محلهای مرتبط با یک سفارش در فایل Models/OrderPlace.cs
using System.ComponentModel.DataAnnotations; namespace Models; public record OrderPlace { public Address BillingAddress { get; set; } = new(); public Address ShippingAddress { get; set; } = new(); } public class Address { [Required] public string Name { get; set; } = default!; public string? AddressLine1 { get; set; } public string? AddressLine2 { get; set; } public string? City { get; set; } [Required] public string PostCode { get; set; } = default!; }
ب) تهیهی یک کامپوننت Editor برای دریافت اطلاعات آدرس فوق در فایل Components\Pages\Chekout\AddressEntry.razor
@inherits Editor<Models.Address> <div> <label>Name</label> <InputText @bind-Value="Value.Name"/> </div> <div> <label>Address 1</label> <InputText @bind-Value="Value.AddressLine1"/> </div> <div> <label>Address 2</label> <InputText @bind-Value="Value.AddressLine2"/> </div> <div> <label>City</label> <InputText @bind-Value="Value.City"/> </div> <div> <label>Post Code</label> <InputText @bind-Value="Value.PostCode"/> </div>
ج) استفاده از مدل و ادیتور فوق در یک EditForm تغییر یافته برای کار با برنامههای Blazor SSR در فایل Components\Pages\Chekout\Checkout.razor
@page "/checkout" @using Models @if (!_submitted && PlaceModel != null) { <EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout"> <DataAnnotationsValidator/> <h4>Bill To:</h4> <AddressEntry @bind-Value="PlaceModel.BillingAddress"/> <h4>Ship To:</h4> <AddressEntry @bind-Value="PlaceModel.ShippingAddress"/> <button type="submit">Submit</button> <ValidationSummary/> </EditForm> } @if (_submitted && PlaceModel != null) { <div> <h2>Order Summary</h2> <h3>Shipping To:</h3> <dl> <dt>Name</dt> <dd>@PlaceModel.BillingAddress.Name</dd> <dt>Address 1</dt> <dd>@PlaceModel.BillingAddress.AddressLine1</dd> <dt>Address 2</dt> <dd>@PlaceModel.BillingAddress.AddressLine2</dd> <dt>City</dt> <dd>@PlaceModel.BillingAddress.City</dd> <dt>Post Code</dt> <dd>@PlaceModel.BillingAddress.PostCode</dd> </dl> </div> } @code { bool _submitted; [SupplyParameterFromForm] public OrderPlace? PlaceModel { get; set; } protected override void OnInitialized() { PlaceModel ??= GetOrderPlace(); } private void SubmitOrder() { _submitted = true; } private static OrderPlace GetOrderPlace() => new() { BillingAddress = new Address { PostCode = "12345", Name = "Test 1", }, ShippingAddress = new Address { PostCode = "67890", Name = "Test 2", }, }; }
باید بخاطر داشت که این فرم بر اساس حالت Server Side Rendering در اختیار مرورگر کاربر قرار میگیرد. یعنی برای بار اول، یک HTML خالص، در سمت سرور بر اساس اطلاعات آن تهیه شده و بازگشت داده میشود و زمانیکه به کاربر نمایش داده شد، دیگر برخلاف Blazor Server پیشین، اتصال SignalR ای وجود ندارد تا قابلیتهای تعاملی آنرا مدیریت کند. در این حالت اگر به view source صفحهی جاری رجوع کنیم، چنین خروجی قابل مشاهدهاست:
<form method="post"> <input type="hidden" name="_handler" value="checkout" /> <input type="hidden" name="__RequestVerificationToken" value="CfDxxx" /> . . . <button type="submit">Submit</button> </form>
این EditForm تعریف شده، دو قسمت اضافهتر را نسبت به EditFormهای نگارشهای قبلی Blazor دارد:
<EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout">
همانطور که در نحوهی تعریف فرم HTML ای فوق مشخص است، فیلد مخفی handler_، کار متمایز ساختن این فرم را به عهده داشته و از مقدار آن در سمت سرور جهت یافتن کامپوننت متناظر، استفاده خواهد شد.
همچنین برای دریافت و پردازش این اطلاعات در سمت سرور، تنها کافی است خاصیت مرتبط با آنرا با ویژگی SupplyParameterFromForm مزین کنیم:
[SupplyParameterFromForm] public OrderPlace? PlaceModel { get; set; }
جریان کاری این فرم به صورت خلاصه به نحو زیر است (که در آن متد OnInitialized دوبار فراخوانی میشود و باید به آن دقت داشت):
- در بار اول نمایش این صفحه (با فراخوانی مسیر /checkout در مرورگر)، متد OnInitialized فراخوانی شده و در آن، مقدار شیء PlaceModel نال است.
- بنابراین به متد GetOrderPlace مراجعه کرده و اطلاعاتی را دریافت میکند؛ برای مثال، این اطلاعات را از سرویسی میخواند.
- پس از پایان هر روال رخدادگردانی در Blazor، در پشت صحنه به صورت خودکار، متد تغییر حالت جاری کامپوننت (متد StateHasChanged) هم فراخوانی میشود. این فراخوانی خودکار، باعث رندر مجدد UI آن بر اساس اطلاعات جدید خواهد شد. یعنی قسمتهای نمایش فرم و نمایش اطلاعات ارسالی، یکبار ارزیابی شده و در صورت برقراری شرطها، نمایش داده میشوند.
- در ادامه، کاربر فرم را پر کرده و به سمت سرور POST میکند.
- پیش از هر رخدادی، خواص شیء PlaceModel به علت مزین بودن به ویژگی SupplyParameterFromForm، بر اساس اطلاعات ارسالی به سرور، مقدار دهی میشوند.
- سپس متد OnInitialized فراخوانی شده و چون اینبار مقدار PlaceModel نال نیست، به متد GetOrderPlace جهت دریافت مقادیر ابتدایی خود مراجعه نمیکند. سطر تعریف شدهی در متد OnInitialized فقط زمانی سبب مقدار دهی شیء PlaceModel میشود که مقدار این شیء، نال باشد (یعنی فقط در اولین بار نمایش صفحه)؛ اما اگر این مقدار توسط پارامتر مزین شدهی به SupplyParameterFromForm به علت ارسال دادههای فرم به سرور، مقدار دهی شده باشد، دیگر به منبع دادهی ابتدایی رجوع نمیکند.
- چون متد رخدادگردان OnInitialized فراخوانی شده، پس از پایان آن (و فراخوانی خودکار متد StateHasChanged در انتهای آن)، یکبار دیگر کار رندر UI فرم جاری بر اساس اطلاعات جدید، انجام خواهد شد.
- اکنون است که پس از طی این رخدادها، متد رویدادگردان SubmitOrder فراخوانی میشود. یعنی زمانیکه این متد فراخوانی میشود، شیء PlaceModel بر اساس اطلاعات رسیدهی از طرف کاربر، مقدار دهی شده و آمادهی استفاده است (برای مثال آمادهی ذخیره سازی در بانک اطلاعاتی؛ با فراخوانی سرویسی در اینجا).
- پس از پایان فراخوانی متد رویدادگردان SubmitOrder، به علت تغییر حالت کامپوننت (و فراخوانی خودکار متد StateHasChanged در انتهای آن)، یکبار دیگر نیز کار رندر UI فرم جاری بر اساس اطلاعات جدید انجام خواهد شد. یعنی اینبار قسمت Order Summary نمایش داده میشود.
مدیریت تداخل نامهای HTML Forms در Blazor 8x SSR
تمام فرمهایی که به این صورت در برنامههای Blazor SSR مدیریت میشوند، باید دارای نام منحصربفردی که توسط خاصیت FormName مشخص میشود، باشند. برای جلوگیری از این تداخل نامها، کامپوننت جدیدی به نام FormMappingScope معرفی شدهاست که نمونهای از آنرا در فایل فرضی Components\Pages\Chekout\CheckoutForm.razor تعریف شدهی به صورت زیر مشاهده میکنید:
@page "/checkout" <FormMappingScope Name="store-checkout"> <CheckoutForm /> </FormMappingScope>
اکنون اگر برنامه را اجرا کرده و خروجی HTML آنرا بررسی کنیم، به فرم زیر خواهیم رسید:
<form method="post"> <input type="hidden" name="_handler" value="[store-checkout]checkout" /> <input type="hidden" name="__RequestVerificationToken" value="CfDxxxxx" /> . . . <button type="submit">Submit</button> </form>
یک نکته: اگر به تگهای فرم HTML ای فوق دقت کنید، به همراه یک anti-forgery token نیز هست که کار تولید و مدیریت آن، به صورت خودکار صورت میگیرد و میانافزاری نیز برای آن طراحی شده که در فایل Program.cs برنامه، به صورت app.UseAntiforgery بکارگرفته شدهاست.
یک نکته: در Blazor 8x SSR میتوان بجای EditForm، از همان HTML form متداول هم استفاده کرد
اگر بخواهیم بجای استفاده از EditForm، از فرمهای استاندارد HTML هم در حالت SSR استفاده کنیم، این کار میسر بوده و روش کار به صورت زیر است:
<form method="post" @onsubmit="SaveData" @formname="MyFormName"> <AntiforgeryToken /> <InputText @bind-Value="Name" /> <button>Submit</button> </form>
پردازش فرمهای GET در Blazor 8x
در حالتیکه از فرمهای استاندارد HTML ای استفاده میشود، ممکن است method فرم، بجای post، حالت get باشد که نتایج آن به صورت کوئری استرینگ در نوار آدرس مرورگر ظاهر میشوند؛ مانند جستجوی گوگل که اشخاص میتوانند کوئری استرینگ و لینک نهایی را به اشتراک بگذارند. روش پردازش یک چنین فرمهایی به صورت زیر است:
@page "/" <form method="GET"> <input type="text" name="q"/> <button type="submit">Search</button> </form> @code { [SupplyParameterFromQuery(Name="q")] public string SearchTerm { get; set; } protected override async Task OnInitializedAsync() { // do something with the search term } }
یک ابتکار! تعاملی کردن قسمتی از صفحه بدون فعالسازی کامل Blazor Server و یا Blazor WASM کامل
این دکمهی قرار گرفتهی در یک صفحهی SSR را ملاحظه کنید:
<button class="nav-link border-0" @onclick="BeginSignOut">Log out</button>
<EditForm Context="ctx" FormName="LogoutForm" method="post" Model="@Foo" OnValidSubmit="BeginSignOut"> <button type="submit" class="nav-link border-0">Log out</button> </EditForm> @code{ [SupplyParameterFromForm(Name = "LogoutForm")] public string? Foo { get; set; } protected override void OnInitialized() => Foo = ""; async Task BeginSignOut() { // TODO: SignOutAsync(); // TODO: NavigateTo("/authentication/logout"); } }
یک نکته: میتوان حالت post-back مانند فرمهای تعاملی Blazor 8x را تغییر داد.
به همراه ویژگیهای جدید مرتبط با صفحات SSR، ویژگی هدایت بهبودیافته هم وجود دارد که جزئیات بیشتر آنرا در قسمتهای بعدی این سری بررسی میکنیم. برای نمونه اگر مثال این قسمت را اجرا کنید، فرم آن به همراه یک post-back مانند به سمت سرور است که کاملا قابل احساس است؛ این رفتار هرچند استاندارد است، اما بیشباهت به برنامههای MVC ، Razor pages و یا وبفرمها نیست و با فرمهای بیصدا و سریع نگارشهای قبلی Blazor متفاوت است. در Blazor8x میتوان این نوع ارسال اطلاعات را Ajax ای هم کرد که به آن enhanced navigation میگویند. برای اینکار فقط کافی است ویژگی Enhance را به تگ EditForm اضافه کرد و یا ویژگی جدید data-enhance را به تگهای فرمهای استاندارد HTML ای افزود. پس از آن اگر برنامه را اجرا کنیم، دیگر یک post-back استاندارد وبفرمها مشاهده نمیشود و رفتار این صفحه بسیار سریع، نرم و روان خواهد بود.
<EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout" Enhance>
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: Blazor8x-Server-Normal.zip
نشان دادن گزارش قبل از ذخیره یا چاپ
بعد مراجعه کنید به پوشه مثالهای آن. در اینجا میتونید مثالهای نحوه نمایش فایل گزارش تهیه شده را در برنامههای WinForms، WPF، سیلورلایت، وب ASP.NET و غیره ملاحظه کنید.
+ توضیحات بیشتر
آموزش های تکمیلی سیلورلایت
npm install -D jsHint jsHint-loader
module.exports = { entry:['./shared.js','./main.ts'] ,output:{ filename:'bundle.js' } ,watch :true ,module:{ preLoaders:[ { test:/\.js$/ ,exclude:/node_modules/ ,loader:'jshint-loader' } ], loaders:[ { test:/\.ts$/ ,exclude:/node_modules/ ,loader:'ts-loader' } ] } }
همچنین برای تکمیل قابل ذکر است که وبپک دارای postLoaders نیز میباشد که پس از Loaderهای اصلی اجرا میشوند.
// در حالتی که به صورت محلی وبپک نصب شده است npm run webpack -- -p // درحالتی که وبپک به صورت سراسری اجرا میشود webpack -p
npm install -D strip-loader
// webpack.prod.config.js //تنظیمات قبلی را میخوانیم var devConfig = require("./webpack.config.js"); // لودری که وظیفهی حذف کردن دارد را وارد میکنیم var stripLoader = require("strip-loader"); // مانند قبل یک آبجکت با موارد مورد نظر برای لودر میسازیم var stripLoaderConfig = { test:[/\.js$/,/\.ts$/], exclude :/node_modules/ ,loader:stripLoader.loader("console.log") } // اضافه کردن به لیست لودرهای قبلی devConfig.module.loaders.push(stripLoaderConfig); // و در آخر اکسپورت کردن تنظیمات جدید وقبلی module.exports = devConfig;
loader:stripLoader.loader("console.log")
// در حالتی که وبپک به صورت محلی نصب شده است npm run webpack -- --config webpack.prod.config.js -p // در حالتی که وبپک به صورت سراسری نصب شده باشد webpack --config webpack.prod.config.js -p
// زمانی که وبپک به صورت محلی در پروژه نصب شده است npm run webpackserver -- --config webpack.prod.config.js -p //در حالتی که وبپک به صورت گلوبال ( سراسری ) نصب میباشد webpack-dev-server --config webpack.prod.config.js -p
// new webpack.config.js file //ماژول توکار نود جی اس var path = require("path"); module.exports = { // مشخص کردن زمینه برای فایلهای ورودی context:path.resolve("js"), entry:['./shared.js','./main.ts'] ,output:{ // مشخص کردن محل قرارگیری باندل ساخته شده path:path.resolve("build/js"), // درخواست از سمت چه مسیری برای باندل خواهد آمد ؟ publicPath:"assets/js", filename:'bundle.js' } , devServer:{ //راهنمایی برای وب سرور جهت اینکه فایلها را از چه محلی سرو کند contentBase:"assets" } ,watch :true ,module:{ loaders:[ { test:/\.ts$/ ,exclude:/node_modules/ ,loader:'ts-loader' } ] } }
context:path.resolve("js")
// تغییرات در شی output // این کلید جدید مسیر قرار گیری جدید باندل را به وبپک اطلاع میدهد path:path.resolve("build/js"), // راهنما برای وب سرور وبپک جهت میزبانی مسیر زیر از کلید بالا publicPath:"assets/js",
//index.html <html> <head> first part of webpack tut! </head> <body> <h1>webpack is awesome !</h1> <script src="assets/js/bundle.js"></script> </body> </html>
حال با اجرا کردن وب سرور وبپک میتوان مشاهده کرد که مسیرهای جدید، بدون مشکل توسط وبپک پیدا شده و فایلها سرو میشوند. قابل توجه است که این نوع چینش پروژه قابل تغییر و شخصی سازی برای پروژههای گوناگون میباشد.
ساخت فایلهای سورس مپ (source map)
سادهترین راه جهت ساخت فایلهای سورس مپ با استفاده از یک پرچم در هنگام فراخوانی وبپک به صورت زیر میباشد.
// فعال کردن ساخت سورس مپها npm run webpack -- -d // یا در هنگام نصب گلوبال webpack -d // جهت استفاده به همراه وب سرور npm run webpackserver -- -d // یا به صورت نصب گلوبال webpack-dev-server -d
// webpack.config.js // کلید جدید اضافه شده در فایل پیکربندی devtool:"#source-map"
با اجرای وب سرور وبپک خواهید دید که سورس مپها در منوی توسعه دهندهی مرورگر قابل دستیابی میباشند.
ساخت چندین باندل گوناگون
قصد داریم به پروژه، دو صفحهی دیگر را نیز با نامهای aboutme و contact اضافه کنیم. هر یک از این صفحات اسکریپت مخصوص به خود را خواهد داشت و باندل نهایی نیز شامل تمامی آنها خواهد شد. در صورتی که این خروجی مطلوب ما نباشد و به طور مثال بخواهیم مکانیزمی شبیه به lazy loading اسکریپتها را داشته باشیم و فقط زمانی اسکریپتها بارگذاری شوند که به آنها احتیاج باشد، برای انجام این کار با وبپک به صورت زیر عمل خواهیم کرد.
دو صفحهی html جدید را با عناوین ذکر شدهی بالا به پوشهی assets اضافه میکنیم و برای هریک نیز اسکریپتی با همان نام خواهیم ساخت و در پوشهی js قرار میدهیم.
محتوای صفحات بدین شکل میباشد.
// index.html file <html> <head> <title> third part of webpack tut! </title> </head> <body> <nav> <a href="aboutme.html">about me</a> <a href="contact.html">contact</a> </nav> <h1>webpack is awesome !</h1> <script src="assets/js/shared.js"></script> <script src="assets/js/index.js"></script> </body> </html> // aboutme.html file <html> <head> <title> about me page ! </title> </head> <body> <nav> <a href="index.html">index</a> <a href="contact.html">contact</a> </nav> <h1>webpack is awesome !</h1> <script src="assets/js/shared.js"></script> <script src="assets/js/aboutme.js"></script> </body> </html> // contact.html file <html> <head> <title> contact me page ! </title> </head> <body> <nav> <a href="index.html">index</a> <a href="aboutme.html">about me</a> </nav> <h1>webpack is awesome !</h1> <script src="assets/js/shared.js"></script> <script src="assets/js/contact.js"></script> </body> </html>
var path = require("path"); var webpack = require("webpack"); // وارد کردن پلاگینی از وب پک برای ساخت تکههای مختلف اسکریپتها // معرفی اسکریپت shared.js var commonChunkPlugin = new webpack.optimize.CommonsChunkPlugin("shared.js"); module.exports = { context:path.resolve("js"), //entry:['./shared.js','./main.ts'] // معرفی اسکریپتهای جدید به وبپک entry:{ index:"./main.js", aboutme:"./aboutme.js", contact:"./contact.js" } ,output:{ path:path.resolve("build/js"), publicPath:"assets/js", // filename:'bundle.js' // به جای یک باندل کلی از وبپک میخاهیم برای هر ورودی باندلی جدید بسازد filename:"[name].js" } // رجیستر کردن پلاگین ,plugins:[commonChunkPlugin] , devServer:{ contentBase:"assets" } //,devtool:"#source-map" ,watch :true ,module:{... } }
حال با اجرای وبپک میتوان دید که سه باندل ساخته شده که همگی به اسکریپت shared.js وابستگی دارند و اگر این اسکریپت را از صفحات HTML حذف کنید، با خطا رو به رو خواهید شد. این پلاگین قدرت ساخت باندلهایی با خاصیت مشخص کردن وابستگیها و همچنین تو در توییها خاص را نیز دارد. برای مطالعهی بیشتر میتوانید به اینجا مراجعه کنید: پلاگین commonsChunk
در قسمت بعدی با استفاده از وبپک فایلهای css، فونتها و تصاویر را نیز باندل خواهیم کرد.
فایلهای مطلب:
سورس تا قبل از قسمت ایجاد تغییرات در ساختار فایلهای پروژه :dntwebpack-part3-beforeFileAndFolderManagment.zip
سورس برای بعد از ایجاد تغییرات در ساختار فایلهای پروژه : dntwebpack-part3AfterFileOrganization.zip
معماری لایه بندی نرم افزار #1
ممنون بابت به اشتراک گذاری این مطلب.
میخواستم بدونم چرا ما باید Presentation Layer رو ایجاد کنیم ؟ شما در Presentation Layer اومدید MVP Pattern رو پیاده سازی کردید ، مثلا اگر من از MVC استفاده کنم مگه هر دوی اونها ( یعنی MVP و MVC ) یک presentation pattern نیستند. پس چرا باید از هردو استفاده کنیم ؟
فرمودید برای آزمون واحد راحتر هستیم وقتی Presentation Layer رو ایجاد کنیم ، مگر MVC این قابلت رو نداره ؟