مدفون سازی فایلهای CSS و JS هر افزونه درون فایل DLL آن
به solution جاری، یک class library جدید را به نام MvcPluginMasterApp.Common اضافه کنید. از آن جهت قرار دادن کلاسهای عمومی و مشترک بین افزونهها استفاده خواهیم کرد. برای مثال قصد نداریم کلاسهای سفارشی و عمومی ذیل را هربار به صورت مستقیم در افزونهای جدید کپی کنیم. کتابخانهی Common، امکان استفادهی مجدد از یک سری کدهای تکراری را در بین افزونهها میسر میکند.
این پروژه برای کامپایل شدن نیاز به بستهی نیوگت ذیل دارد:
PM> install-package Microsoft.AspNet.Web.Optimization
پس از این مقدمات، کلاس ذیل را به این پروژهی class library جدید اضافه کنید:
using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text; using System.Web.Optimization; namespace MvcPluginMasterApp.Common.WebToolkit { public class EmbeddedResourceTransform : IBundleTransform { private readonly IList<string> _resourceFiles; private readonly string _contentType; private readonly Assembly _assembly; public EmbeddedResourceTransform(IList<string> resourceFiles, string contentType, Assembly assembly) { _resourceFiles = resourceFiles; _contentType = contentType; _assembly = assembly; } public void Process(BundleContext context, BundleResponse response) { var result = new StringBuilder(); foreach (var resource in _resourceFiles) { using (var stream = _assembly.GetManifestResourceStream(resource)) { if (stream == null) { throw new KeyNotFoundException(string.Format("Embedded resource key: '{0}' not found in the '{1}' assembly.", resource, _assembly.FullName)); } using (var reader = new StreamReader(stream)) { result.Append(reader.ReadToEnd()); } } } response.ContentType = _contentType; response.Content = result.ToString(); } } }
کلاس فوق در اسمبلی معرفی شده به آن، توسط متد GetManifestResourceStream به دنبال فایلها و منابع مدفون شده گشته و سپس محتوای آنها را بازگشت میدهد.
اکنون برای استفادهی از آن، به پروژهی MvcPluginMasterApp.Plugin1 مراجعه کرده و ارجاعی را به پروژهی MvcPluginMasterApp.Common فوق اضافه نمائید. سپس در فایل Plugin1.cs، متد RegisterBundles آنرا به نحو ذیل تکمیل کنید:
namespace MvcPluginMasterApp.Plugin1 { public class Plugin1 : IPlugin { public EfBootstrapper GetEfBootstrapper() { return null; } public MenuItem GetMenuItem(RequestContext requestContext) { return new MenuItem { Name = "Plugin 1", Url = new UrlHelper(requestContext).Action("Index", "Home", new { area = "NewsArea" }) }; } public void RegisterBundles(BundleCollection bundles) { var executingAssembly = Assembly.GetExecutingAssembly(); // Mostly the default namespace and assembly name are the same var assemblyNameSpace = executingAssembly.GetName().Name; var scriptsBundle = new Bundle("~/Plugin1/Scripts", new EmbeddedResourceTransform(new List<string> { assemblyNameSpace + ".Scripts.test1.js" }, "application/javascript", executingAssembly)); if (!HttpContext.Current.IsDebuggingEnabled) { scriptsBundle.Transforms.Add(new JsMinify()); } bundles.Add(scriptsBundle); var cssBundle = new Bundle("~/Plugin1/Content", new EmbeddedResourceTransform(new List<string> { assemblyNameSpace + ".Content.test1.css" }, "text/css", executingAssembly)); if (!HttpContext.Current.IsDebuggingEnabled) { cssBundle.Transforms.Add(new CssMinify()); } bundles.Add(cssBundle); BundleTable.EnableOptimizations = true; } public void RegisterRoutes(RouteCollection routes) { } public void RegisterServices(IContainer container) { } } }
این فایلها به صورت ذیل در پروژه تعریف گردیدهاند:
همانطور که مشاهده میکنید، باید به خواص هر کدام مراجعه کرد و سپس Build action آنها را به embedded resource تغییر داد، تا در حین کامپایل، به صورت خودکار در قسمت منابع اسمبلی ذخیره شوند.
یک نکتهی مهم
اینبار برای مسیردهی منابع، باید بجای / فایل سیستم، از «نقطه» استفاده کرد. زیرا منابع با نامهایی مانند namespace.folder.name در قسمت resources یک اسمبلی ذخیره میشوند:
مدفون سازی تصاویر ثابت هر افزونه درون فایل DLL آن
مجددا به اسمبلی مشترک MvcPluginMasterApp.Common مراجعه کرده و اینبار کلاس جدید ذیل را به آن اضافه کنید:
using System; using System.Collections.Generic; using System.Reflection; using System.Web; using System.Web.Routing; namespace MvcPluginMasterApp.Common.WebToolkit { public class EmbeddedResourceRouteHandler : IRouteHandler { private readonly Assembly _assembly; private readonly string _resourcePath; private readonly TimeSpan _cacheDuration; public EmbeddedResourceRouteHandler(Assembly assembly, string resourcePath, TimeSpan cacheDuration) { _assembly = assembly; _resourcePath = resourcePath; _cacheDuration = cacheDuration; } IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) { return new EmbeddedResourceHttpHandler(requestContext.RouteData, _assembly, _resourcePath, _cacheDuration); } } public class EmbeddedResourceHttpHandler : IHttpHandler { private readonly RouteData _routeData; private readonly Assembly _assembly; private readonly string _resourcePath; private readonly TimeSpan _cacheDuration; public EmbeddedResourceHttpHandler( RouteData routeData, Assembly assembly, string resourcePath, TimeSpan cacheDuration) { _routeData = routeData; _assembly = assembly; _resourcePath = resourcePath; _cacheDuration = cacheDuration; } public bool IsReusable { get { return false; } } public void ProcessRequest(HttpContext context) { var routeDataValues = _routeData.Values; var fileName = routeDataValues["file"].ToString(); var fileExtension = routeDataValues["extension"].ToString(); var manifestResourceName = string.Format("{0}.{1}.{2}", _resourcePath, fileName, fileExtension); var stream = _assembly.GetManifestResourceStream(manifestResourceName); if (stream == null) { throw new KeyNotFoundException(string.Format("Embedded resource key: '{0}' not found in the '{1}' assembly.", manifestResourceName, _assembly.FullName)); } context.Response.Clear(); context.Response.ContentType = "application/octet-stream"; cacheIt(context.Response, _cacheDuration); stream.CopyTo(context.Response.OutputStream); } private static void cacheIt(HttpResponse response, TimeSpan duration) { var cache = response.Cache; var maxAgeField = cache.GetType().GetField("_maxAge", BindingFlags.Instance | BindingFlags.NonPublic); if (maxAgeField != null) maxAgeField.SetValue(cache, duration); cache.SetCacheability(HttpCacheability.Public); cache.SetExpires(DateTime.Now.Add(duration)); cache.SetMaxAge(duration); cache.AppendCacheExtension("must-revalidate, proxy-revalidate"); } } }
این IRouteHandler، نام و پسوند فایل را دریافت کرده و سپس به قسمت منابع اسمبلی رجوع، فایل مرتبط را استخراج و سپس بازگشت میدهد. همچنین برای کاهش سربار سیستم، امکان کش شدن منابع استاتیک نیز در آن درنظر گرفته شدهاست و هدرهای خاص caching را به صورت خودکار اضافه میکند.
سیستم bundling نیز هدرهای کش کردن را به صورت خودکار و توکار اضافه میکند.
اکنون به تعاریف Plugin1 مراجعه کنید و سپس این IRouteHandler سفارشی را به نحو ذیل به آن معرفی نمائید:
namespace MvcPluginMasterApp.Plugin1 { public class Plugin1 : IPlugin { public void RegisterRoutes(RouteCollection routes) { //todo: add custom routes. var assembly = Assembly.GetExecutingAssembly(); // Mostly the default namespace and assembly name are the same var nameSpace = assembly.GetName().Name; var resourcePath = string.Format("{0}.Images", nameSpace); routes.Insert(0, new Route("NewsArea/Images/{file}.{extension}", new RouteValueDictionary(new { }), new RouteValueDictionary(new { extension = "png|jpg" }), new EmbeddedResourceRouteHandler(assembly, resourcePath, cacheDuration: TimeSpan.FromDays(30)) )); } } }
مطابق تعریف آن، file و extension به صورت خودکار جدا شده و توسط routeData.Values در متد ProcessRequest کلاس EmbeddedResourceHttpHandler قابل دسترسی خواهند شد.
پسوندهایی که توسط آن بررسی میشوند از نوع png یا jpg تعریف شدهاند. همچنین مدت زمان کش کردن هر منبع استاتیک تصویری به یک ماه تنظیم شدهاست.
استفادهی نهایی از تنظیمات فوق در یک View افزونه
پس از اینکه تصاویر و فایلهای css و js را به صورت embedded resource تعریف کردیم و همچنین تنظیمات مسیریابی و bundling خاص آنها را نیز مشخص نمودیم، اکنون نوبت به استفادهی از آنها در یک View است:
@{ ViewBag.Title = "From Plugin 1"; } @Styles.Render("~/Plugin1/Content") <h2>@ViewBag.Message</h2> <div class="row"> Embedded image: <img src="@Url.Content("~/NewsArea/Images/chart.png")" alt="clock" /> </div> @section scripts { @Scripts.Render("~/Plugin1/Scripts") }
همچنین مسیر تصویر مشخص شدهی در آن، اینبار یک NewsArea اضافهتر دارد. فایل اصلی تصویر، در مسیر Images/chart.png قرار گرفتهاست اما میخواهیم این درخواستها را به مسیریابی جدید {NewsArea/Images/{file}.{extension هدایت کنیم. بنابراین نیاز است به این نکته نیز دقت داشت.
اینبار اگر برنامه را اجرا کنیم، میتوان به سه نکته در آن دقت داشت:
الف) alert اجرا شده از فایل js مدفون شده خوانده شدهاست.
ب) رنگ قرمز متن (تگ h2) از فایل css مدفون شده، گرفته شدهاست.
ج) تصویر نمایش داده شده، همان تصویر مدفون شدهی در فایل DLL برنامه است.
و هیچکدام از این فایلها، به پوشههای پروژهی اصلی برنامه، کپی نشدهاند.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
MvcPluginMasterApp-Part2.zip