【复杂系统迁移 .NET Core平台系列】之静态文件
源寶導讀:微軟跨平臺技術(shù)框架—.NET Core已經(jīng)日趨成熟,已經(jīng)具備了支撐大型系統(tǒng)穩(wěn)定運行的條件。本文將介紹明源云ERP平臺從.NET Framework向.NET Core遷移過程中的實踐經(jīng)驗。
一、背景
? ? 隨著ERP的產(chǎn)品線越來越多,業(yè)務關(guān)聯(lián)也日益復雜,應用間依賴關(guān)系也變得錯綜復雜,單體架構(gòu)的弱點日趨明顯。19年初,由于平臺底層支持了分應用部署模式,將ERP從應用子系統(tǒng)層面進行了切割分離,邁出了從單體架構(gòu)向微服務架構(gòu)轉(zhuǎn)型的堅實一步。不久的將來,ERP會進一步將各業(yè)務拆分成眾多的微服務,而微服務勢必需要進行容器化部署和運行管理,這就要求ERP技術(shù)底層必須支持跨平臺,所以將現(xiàn)有ERP系統(tǒng)從.NET Framework遷移到 .NET Core平臺勢在必行。
? ? 上一篇我們講述了Erp在改造.Net?Core頁面的處理,這一篇我們將講述在靜態(tài)文件改造過程中遇到的問題和解決思路。
二、靜態(tài)文件擴展需求
? ? 在ERP中,不僅僅要支持后端的擴展需求,還要支撐前端的擴展,ERP常見的擴展就是項目擴展標準產(chǎn)品的功能,其中包含JS和CSS的擴展
sea.js的擴展
? ? ERP采用sea.js的同步js加載機制,sea.js中是通過sea-config.js的配置來通過模塊名映射到正確的js路徑,而ERP中sea-config.js因為屏蔽掉手動配置,所以采取動態(tài)生成機制,通過如下幾步完成前端sea-config.js的加載。
我們需要先了解下sea.js的編碼規(guī)范,以下是js文件的一個示例:
//OrganizationAppService.js文件 /** * AppService代理類 * @author 本代碼由代碼生成器自動生成,請不要手工調(diào)整 * http://localhost:4602/script/Mysoft.PubPlatform.Organization.AppServices.OrganizationAppService/proxy */ define("Mysoft.PubPlatform.Organization.AppServices.OrganizationAppService", function (require, exports, module) {var utility = require("utility");//業(yè)務邏輯代碼})? ? 根據(jù)ERP的擴展要求,會有如下三個文件:? ??
平臺js文件 _frontend_build\platform-module.json。
產(chǎn)品js文件 _frontend_build\product-module.json。
二開js文件 _frontend\Customize{app}\dist\sea-config.json(app代表系統(tǒng)簡稱例如cbxt)。
? ? 接下來就是在代碼中生成如下的sea-config.js內(nèi)容,因為上述json內(nèi)容是一個結(jié)構(gòu)化的內(nèi)容,通過程序反序列化加載如上三個文件到內(nèi)存中,再轉(zhuǎn)換成如下標準的sea-config.js的標準內(nèi)容:
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的整個代碼編寫到配置加載的整個環(huán)路。最后在在Framework的ERP中是通過配置在webconfig的SeaConfigHandler進行加載的。
多語言js的擴展
? ? ERP的js支持多語言的擴展,而多語言都存儲在后端,所以js文件的加載是配置在webconfig中配置的StaticFileHandler來擴展功能,在加載中讀取物理文件內(nèi)容時候,進行多語言替換之后返回到前端。
css文件擴展需求
? ? css文件加載為了產(chǎn)品進行二次開發(fā)擴展也是通過StaticFileHandler的配置來加載,首先加載產(chǎn)品的css文件,然后加載項目的css文件,將兩個文件內(nèi)容進行合并后返回到前端。
靜態(tài)文件緩存的需求
? ? 在上述的靜態(tài)文件加載過程中,都增加了瀏覽器靜態(tài)文件緩存的功能,通過max-age 和etag的http頭進行標記,讓瀏覽器知道要緩存靜態(tài)文件,從而減少對后端服務器的請求數(shù)據(jù)。
? ? 上一篇文章中我們提到的TagHelper中,靜態(tài)文件也可以通過在生成的url中,拼接文件的的 SHA256的哈希值來直接使用瀏覽器自帶的緩存功能。
二、.Net Core靜態(tài)文件加載原理
? ? 前面我們講述了靜態(tài)文件的擴展需求,這部分在Core的改造中是需要做兼容的,整個的業(yè)務邏輯是不變的,但是Core中沒有HttpHanler的處理機制,所有的請求都是通過Middleware來進行處理,Core中對應靜態(tài)文件的處理全部是放在StaticFileMiddleware 中,通過IApplicationBuilder.UseStaticFiles進行引用。
? ? 在Core中提供的默認靜態(tài)文件加載主要是如下幾個類型:
FileExtensionContentTypeProvider:用來確定哪些請求是需要靜態(tài)文件StaticFileMiddleware 進行處理的。
PhysicalFileInfo:繼承自IFileInfo,用來讀取物理文件內(nèi)容
PhysicalFileProvider:繼承自IFileProvider,用來映射物理路徑到FileInfo和監(jiān)聽文件是否修改。
? ? Core靜態(tài)文件的處理流程如下:
首先通過FileExtensionContentTypeProvider確定是否需要處理請求。
通過PhysicalFileProvider的GetFileInfo返回IFileInfo對象(實際類型是PhysicalFileInfo,如果文件不存在返回NotFoundFileInfo)。
通過IFileInfo的Exists判斷文件是否存在,如果存在通過CreateReadStream方法返回文件內(nèi)容。
第一次返回給客戶端的ETag和Last-Modified的值,會在第二次請求返回給服務端,如果文件沒有更改則這兩個值在生成規(guī)則一樣的情況下不會改變,這時候返回給客戶端304。
三、.Net Core中靜態(tài)文件改造
? ? 在講述了ERP的靜態(tài)文件擴展需求和.Net Core中靜態(tài)文件加載原理之后,接下來就是在.Net Core提供的靜態(tài)文件功能基礎(chǔ)上進行擴展來接入ERP的擴展需求了,這里我們采用了兩個適配器模式來講解決這個問題,首先我們看看整體的類圖:
說明:
PhysicalFileProviderAdapter 和PhysicalFileProvider 是組合關(guān)系,通過重寫GetFileInfo來返回IFileHandler對象,代碼如下:
public?IFileInfo?GetFileInfo(string?subpath){var?handlers?=?IocManager.ServiceProvider.GetServices<IFileHandler>();var?handler?=?handlers.OrderBy(item?=>?item.Order)//?優(yōu)先處理特殊的,默認的優(yōu)先級最小.ToList().First(item?=>{item.InitFile(_provider,?_provider.GetFileInfo(subpath));return?item.CanHandler();});return?handler; }為了兼容默認靜態(tài)文件處理行為,默認的DefaultHandler是可以處理所有的需要處理的請求,所以DefaultHandler的Order最大,SeaConfigJsFileHandler因為需要在JsLangeHandler之前處理所以O(shè)rder最小。
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都是處理后的內(nèi)容寫入MemoryStream然后通過CreateReadStream 返回給Core的靜態(tài)文件處理。
在BaseHandler中提供InitFile的模板方法,子類重寫InnerInit來實現(xiàn)業(yè)務邏輯,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(); }//留給子類來出來各自的業(yè)務邏輯 protected virtual void InnerInit() { } //給返回的靜態(tài)文件添加緩存 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");????} } |
四、總結(jié)
? ? 相比于Framework散落的靜態(tài)文件處理,.NET Core的靜態(tài)文件處理職責更加明確,點更加集中?;谶m配器的擴展之后,將職責更加明確,每個FileHandler只有一個職責,并且在以后需要類似的靜態(tài)文件功能時候增加一個FileHandler即可,更加易于擴展。
? ? 由于依賴于.Net Core 中Ioc容器提供的獲取實現(xiàn)列表的功能,IEnumerable<T> GetServices<T>(this IServiceProvider provider) 方法,所以這里簡單的采用遍歷判斷的方法,如果只有獲取單個實現(xiàn)的方法的話,這里可以調(diào)整為責任鏈模式,有興趣的可以嘗試一下。
------ END ------
作者簡介
熊同學:?研發(fā)工程師,目前負責ERP運行平臺的設(shè)計與開發(fā)工作。
也許您還想看
【復雜系統(tǒng)遷移 .NET Core平臺系列】之遷移項目工程
【復雜系統(tǒng)遷移 .NET Core平臺系列】之界面層
招商城科走進武漢研發(fā)中心,現(xiàn)場編碼解鎖平臺內(nèi)核技術(shù)
如何解決大批量數(shù)據(jù)保存的性能問題
【2019總結(jié)篇】談談數(shù)字化時代,ERP如何坐穩(wěn)數(shù)字化底座
總結(jié)
以上是生活随笔為你收集整理的【复杂系统迁移 .NET Core平台系列】之静态文件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: WTM系列视频教程:WebApi
- 下一篇: 疫情期间,千万级系统宕机N次,老板撂下狠