【复杂系统迁移 .NET Core平台系列】之静态文件
源寶導讀:微軟跨平臺技術框架—.NET Core已經日趨成熟,已經具備了支撐大型系統穩定運行的條件。本文將介紹明源云ERP平臺從.NET Framework向.NET Core遷移過程中的實踐經驗。
一、背景
? ? 隨著ERP的產品線越來越多,業務關聯也日益復雜,應用間依賴關系也變得錯綜復雜,單體架構的弱點日趨明顯。19年初,由于平臺底層支持了分應用部署模式,將ERP從應用子系統層面進行了切割分離,邁出了從單體架構向微服務架構轉型的堅實一步。不久的將來,ERP會進一步將各業務拆分成眾多的微服務,而微服務勢必需要進行容器化部署和運行管理,這就要求ERP技術底層必須支持跨平臺,所以將現有ERP系統從.NET Framework遷移到 .NET Core平臺勢在必行。
? ? 上一篇我們講述了Erp在改造.Net?Core頁面的處理,這一篇我們將講述在靜態文件改造過程中遇到的問題和解決思路。
二、靜態文件擴展需求
? ? 在ERP中,不僅僅要支持后端的擴展需求,還要支撐前端的擴展,ERP常見的擴展就是項目擴展標準產品的功能,其中包含JS和CSS的擴展
sea.js的擴展
? ? ERP采用sea.js的同步js加載機制,sea.js中是通過sea-config.js的配置來通過模塊名映射到正確的js路徑,而ERP中sea-config.js因為屏蔽掉手動配置,所以采取動態生成機制,通過如下幾步完成前端sea-config.js的加載。
我們需要先了解下sea.js的編碼規范,以下是js文件的一個示例:
//OrganizationAppService.js文件 /** * AppService代理類 * @author 本代碼由代碼生成器自動生成,請不要手工調整 * http://localhost:4602/script/Mysoft.PubPlatform.Organization.AppServices.OrganizationAppService/proxy */ define("Mysoft.PubPlatform.Organization.AppServices.OrganizationAppService", function (require, exports, module) {var utility = require("utility");//業務邏輯代碼})? ? 根據ERP的擴展要求,會有如下三個文件:? ??
平臺js文件 _frontend_build\platform-module.json。
產品js文件 _frontend_build\product-module.json。
二開js文件 _frontend\Customize{app}\dist\sea-config.json(app代表系統簡稱例如cbxt)。
? ? 接下來就是在代碼中生成如下的sea-config.js內容,因為上述json內容是一個結構化的內容,通過程序反序列化加載如上三個文件到內存中,再轉換成如下標準的sea-config.js的標準內容:
var __lang = !!window.Mysoft ? window.Mysoft.Map6.UI.page.lang : ""; seajs.config({"vars": {"lang": "zh-cn"},"alias": {"RptUtility": "Report/Common/RptUtility.js","Mysoft.Report.Preview.RptDownload": "Report/Preview/RptDownload.js",},"map": [[/^.*$/, function (url) { return url + "?_t=31634695680000000&lang=" + __lang; }]],"base": "/","debug": true });? ? 這樣就完成了整個js文件的sea.js的整個代碼編寫到配置加載的整個環路。最后在在Framework的ERP中是通過配置在webconfig的SeaConfigHandler進行加載的。
多語言js的擴展
? ? ERP的js支持多語言的擴展,而多語言都存儲在后端,所以js文件的加載是配置在webconfig中配置的StaticFileHandler來擴展功能,在加載中讀取物理文件內容時候,進行多語言替換之后返回到前端。
css文件擴展需求
? ? css文件加載為了產品進行二次開發擴展也是通過StaticFileHandler的配置來加載,首先加載產品的css文件,然后加載項目的css文件,將兩個文件內容進行合并后返回到前端。
靜態文件緩存的需求
? ? 在上述的靜態文件加載過程中,都增加了瀏覽器靜態文件緩存的功能,通過max-age 和etag的http頭進行標記,讓瀏覽器知道要緩存靜態文件,從而減少對后端服務器的請求數據。
? ? 上一篇文章中我們提到的TagHelper中,靜態文件也可以通過在生成的url中,拼接文件的的 SHA256的哈希值來直接使用瀏覽器自帶的緩存功能。
二、.Net Core靜態文件加載原理
? ? 前面我們講述了靜態文件的擴展需求,這部分在Core的改造中是需要做兼容的,整個的業務邏輯是不變的,但是Core中沒有HttpHanler的處理機制,所有的請求都是通過Middleware來進行處理,Core中對應靜態文件的處理全部是放在StaticFileMiddleware 中,通過IApplicationBuilder.UseStaticFiles進行引用。
? ? 在Core中提供的默認靜態文件加載主要是如下幾個類型:
FileExtensionContentTypeProvider:用來確定哪些請求是需要靜態文件StaticFileMiddleware 進行處理的。
PhysicalFileInfo:繼承自IFileInfo,用來讀取物理文件內容
PhysicalFileProvider:繼承自IFileProvider,用來映射物理路徑到FileInfo和監聽文件是否修改。
? ? Core靜態文件的處理流程如下:
首先通過FileExtensionContentTypeProvider確定是否需要處理請求。
通過PhysicalFileProvider的GetFileInfo返回IFileInfo對象(實際類型是PhysicalFileInfo,如果文件不存在返回NotFoundFileInfo)。
通過IFileInfo的Exists判斷文件是否存在,如果存在通過CreateReadStream方法返回文件內容。
第一次返回給客戶端的ETag和Last-Modified的值,會在第二次請求返回給服務端,如果文件沒有更改則這兩個值在生成規則一樣的情況下不會改變,這時候返回給客戶端304。
三、.Net Core中靜態文件改造
? ? 在講述了ERP的靜態文件擴展需求和.Net Core中靜態文件加載原理之后,接下來就是在.Net Core提供的靜態文件功能基礎上進行擴展來接入ERP的擴展需求了,這里我們采用了兩個適配器模式來講解決這個問題,首先我們看看整體的類圖:
說明:
PhysicalFileProviderAdapter 和PhysicalFileProvider 是組合關系,通過重寫GetFileInfo來返回IFileHandler對象,代碼如下:
public?IFileInfo?GetFileInfo(string?subpath){var?handlers?=?IocManager.ServiceProvider.GetServices<IFileHandler>();var?handler?=?handlers.OrderBy(item?=>?item.Order)//?優先處理特殊的,默認的優先級最小.ToList().First(item?=>{item.InitFile(_provider,?_provider.GetFileInfo(subpath));return?item.CanHandler();});return?handler; }為了兼容默認靜態文件處理行為,默認的DefaultHandler是可以處理所有的需要處理的請求,所以DefaultHandler的Order最大,SeaConfigJsFileHandler因為需要在JsLangeHandler之前處理所以Order最小。
BaseHandler 主要處理通用邏輯,并且可以做一些子類的公用代碼封裝,例如通過如下代碼,確定那種文件類型是這個類需要處理的,這樣在JSLangHandler和CssLangeHandler中就可以少些一些代碼。
// BaseHandler protected BaseFileHandler(IHttpContextAccessor accessor) {_accessor = accessor;ContentTypeProvider = new FileExtensionContentTypeProvider(); }protected virtual string HandlerContentType { get; } public virtual bool CanHandler() {//通過頭和物理路徑兩種條件來獲取ContentType,如果都獲取不到則不處理if (!ContentTypeProvider.TryGetContentType(_accessor.HttpContext.Request.Path, out var contentType)||!ContentTypeProvider.TryGetContentType(FileInfo.PhysicalPath, out contentType))return false;if (contentType != HandlerContentType)return false;return true; }//CssMergeFileHandler protected override string HandlerContentType => "text/css";public override bool CanHandler() {//忽略swagger的文件return base.CanHandler()&& FileInfo.Name != "swagger-ui.css"; }//JsLangFileHandler protected override string HandlerContentType => "application/javascript";public override bool CanHandler() {//忽略swagger的文件return base.CanHandler()&& FileInfo.Name != "sea-config.js"&& !(FileInfo.Name == "swagger-ui-standalone-preset.js"|| FileInfo.Name == "swagger-ui-bundle.js"); }JsLangFileHandler,CssMergeFileHandler和SeaConfigJsFileHandler都是處理后的內容寫入MemoryStream然后通過CreateReadStream 返回給Core的靜態文件處理。
在BaseHandler中提供InitFile的模板方法,子類重寫InnerInit來實現業務邏輯,AppendHeader用來處理瀏覽器緩存的邏輯。
| //模板方法,所有的初始化邏輯都在此處 public virtual void InitFile([NotNull] IFileProvider provider,[NotNull] IFileInfo fileInfo) {FileProvider=provider?? throw new ArgumentNullException(nameof(provider));FileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo));if (!CanHandler())return;InnerInit();AppendHeader(); }//留給子類來出來各自的業務邏輯 protected virtual void InnerInit() { } //給返回的靜態文件添加緩存 protected virtual void AppendHeader() {string language = LanguageResourceManager.GetCurrentTopCulture();_accessor.HttpContext.Response.Headers.Append("X-Language", language);string fileVer = _accessor.HttpContext.Request.GetQueryValue("_t");if (string.IsNullOrEmpty(fileVer) ){//沒有版本管理的文件默認添加緩存時間為30*60秒var staticFileExpiresMinutes = GetStaticFileExpires();_accessor.HttpContext.Response.Headers.Append("Cache-Control", $"public,max-age={staticFileExpiresMinutes*60}");_accessor.HttpContext.Response.Headers.Append("X-StaticFileHandler", $"{staticFileExpiresMinutes}minute");}else{//如果有版本管理默認添加一年,因為版本會隨著文件更改而更改_accessor.HttpContext.Response.Headers.Append("Cache-Control", $"public,max-age={TimeSpan.FromDays(365).Seconds}");_accessor.HttpContext.Response.Headers.Append("X-StaticFileHandler",?$"1year");????} } |
四、總結
? ? 相比于Framework散落的靜態文件處理,.NET Core的靜態文件處理職責更加明確,點更加集中。基于適配器的擴展之后,將職責更加明確,每個FileHandler只有一個職責,并且在以后需要類似的靜態文件功能時候增加一個FileHandler即可,更加易于擴展。
? ? 由于依賴于.Net Core 中Ioc容器提供的獲取實現列表的功能,IEnumerable<T> GetServices<T>(this IServiceProvider provider) 方法,所以這里簡單的采用遍歷判斷的方法,如果只有獲取單個實現的方法的話,這里可以調整為責任鏈模式,有興趣的可以嘗試一下。
------ END ------
作者簡介
熊同學:?研發工程師,目前負責ERP運行平臺的設計與開發工作。
也許您還想看
【復雜系統遷移 .NET Core平臺系列】之遷移項目工程
【復雜系統遷移 .NET Core平臺系列】之界面層
招商城科走進武漢研發中心,現場編碼解鎖平臺內核技術
如何解決大批量數據保存的性能問題
【2019總結篇】談談數字化時代,ERP如何坐穩數字化底座
總結
以上是生活随笔為你收集整理的【复杂系统迁移 .NET Core平台系列】之静态文件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: WTM系列视频教程:WebApi
- 下一篇: [Abp vNext 源码分析] - 1