龙空技术网

「复杂系统迁移 .NET Core平台系列」之静态文件

日行四善 294

前言:

此时我们对“net网站静态化”都比较讲究,大家都想要学习一些“net网站静态化”的相关知识。那么小编同时在网络上收集了一些对于“net网站静态化””的相关知识,希望看官们能喜欢,咱们一起来学习一下吧!

源宝导读:微软跨平台技术框架—.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 本代码由代码生成器自动生成,请不要手工调整

*

*/

define("Mysoft.PubPlatform.Organization.AppServices.OrganizationAppService", function (require, exports, module) {

var utility = require("utility");

//业务逻辑代码

})

有了确定好的格式以后,我们会在编译前端代码时候扫描OrganizationAppService.js的路径和 define中定义的模块名,生成如下json结构:

{

"Mysoft.PubPlatform.Organization.AppServices.OrganizationAppService": {

"type": "product",

"product": {

"path": "PubPlatform/Organization/AppService/OrganizationAppService.js",

"description": "\r\n\r\nAppService代理类\r\n@author本代码由代码生成器自动生成,请不要手工调整\r\nhttp:localhost:4602scriptMysoft.PubPlatform.Organization.AppServices.OrganizationAppServiceproxy\r\n\r\n",

"mtime": 1581492465639

}

}

}

根据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网站静态化