ASP.NET Core Razor生成Html静态文件
一、前言
最近做项目的时候,使用Util进行开发,使用Razor写前端页面。初次使用感觉还是不大习惯,之前都是前后端分离的方式开发的,但是使用Util封装后的Angular后,感觉开发效率还是杠杠滴。
二、问题
在发布代码的时候,Webpack打包异常,提示是缺少了某些Html文件,我看了下相应的目录,发现目录缺少了部分Html文件,然后就问了何镇汐大大,给出的解决方案是,每个页面都需要访问一下才能生成相应的Html静态文件。这时候就产生了疑虑,是否有一种方式能获取所有路由,然后只需访问一次即可生成所有的Html页面。
三、解决方案
3.1 每次访问生成Html
解决方案思路:
- 继承
ActionFilterAttribute特性,重写执行方法 - 访问的时候判断访问的
Result是否ViewResult,如果是方可生成Html - 从
RazorViewEngine中查找到View后进行渲染
/// <summary>
/// 生成Html静态文件
/// </summary>
public class HtmlAttribute : ActionFilterAttribute {
/// <summary>
/// 生成路径,相对根路径,范例:/Typings/app/app.component.html
/// </summary>
public string Path { get; set; }
/// <summary>
/// 路径模板,范例:Typings/app/{area}/{controller}/{controller}-{action}.component.html
/// </summary>
public string Template { get; set; }
/// <summary>
/// 执行生成
/// </summary>
public override async Task OnResultExecutionAsync( ResultExecutingContext context, ResultExecutionDelegate next ) {
await WriteViewToFileAsync( context );
await base.OnResultExecutionAsync( context, next );
}
/// <summary>
/// 将视图写入html文件
/// </summary>
private async Task WriteViewToFileAsync( ResultExecutingContext context ) {
try {
var html = await RenderToStringAsync( context );
if( string.IsNullOrWhiteSpace( html ) )
return;
var path = Util.Helpers.Common.GetPhysicalPath( string.IsNullOrWhiteSpace( Path ) ? GetPath( context ) : Path );
var directory = System.IO.Path.GetDirectoryName( path );
if( string.IsNullOrWhiteSpace( directory ) )
return;
if( Directory.Exists( directory ) == false )
Directory.CreateDirectory( directory );
File.WriteAllText( path, html );
}
catch( Exception ex ) {
ex.Log( Log.GetLog().Caption( "生成html静态文件失败" ) );
}
}
/// <summary>
/// 渲染视图
/// </summary>
protected async Task<string> RenderToStringAsync( ResultExecutingContext context ) {
string viewName = "";
object model = null;
if( context.Result is ViewResult result ) {
viewName = result.ViewName;
viewName = string.IsNullOrWhiteSpace( viewName ) ? context.RouteData.Values["action"].SafeString() : viewName;
model = result.Model;
}
var razorViewEngine = Ioc.Create<IRazorViewEngine>();
var tempDataProvider = Ioc.Create<ITempDataProvider>();
var serviceProvider = Ioc.Create<IServiceProvider>();
var httpContext = new DefaultHttpContext { RequestServices = serviceProvider };
var actionContext = new ActionContext( httpContext, context.RouteData, new ActionDescriptor() );
using( var stringWriter = new StringWriter() ) {
var viewResult = razorViewEngine.FindView( actionContext, viewName, true );
if( viewResult.View == null )
throw new ArgumentNullException( $"未找到视图: {viewName}" );
var viewDictionary = new ViewDataDictionary( new EmptyModelMetadataProvider(), new ModelStateDictionary() ) { Model = model };
var viewContext = new ViewContext( actionContext, viewResult.View, viewDictionary, new TempDataDictionary( actionContext.HttpContext, tempDataProvider ), stringWriter, new HtmlHelperOptions() );
await viewResult.View.RenderAsync( viewContext );
return stringWriter.ToString();
}
}
/// <summary>
/// 获取Html默认生成路径
/// </summary>
protected virtual string GetPath( ResultExecutingContext context ) {
var area = context.RouteData.Values["area"].SafeString();
var controller = context.RouteData.Values["controller"].SafeString();
var action = context.RouteData.Values["action"].SafeString();
var path = Template.Replace( "{area}", area ).Replace( "{controller}", controller ).Replace( "{action}", action );
return path.ToLower();
}
}
3.2 一次访问生成所有Html
解决方案思路:
- 获取所有已注册的路由
- 获取使用
RazorHtml自定义特性的路由 - 忽略Api接口的路由
- 构建
RouteData信息,用于在RazorViewEngine中查找到相应的视图 - 构建
ViewContext用于渲染出Html字符串 - 将渲染得到的Html字符串写入文件
获取所有注册的路由,此处是比较重要的,其他地方也可以用到。
/// <summary>
/// 获取所有路由信息
/// </summary>
/// <returns></returns>
public IEnumerable<RouteInformation> GetAllRouteInformations()
{
List<RouteInformation> list = new List<RouteInformation>();
var actionDescriptors = this._actionDescriptorCollectionProvider.ActionDescriptors.Items;
foreach (var actionDescriptor in actionDescriptors)
{
RouteInformation info = new RouteInformation();
if (actionDescriptor.RouteValues.ContainsKey("area"))
{
info.AreaName = actionDescriptor.RouteValues["area"];
}
// Razor页面路径以及调用
if (actionDescriptor is PageActionDescriptor pageActionDescriptor)
{
info.Path = pageActionDescriptor.ViewEnginePath;
info.Invocation = pageActionDescriptor.RelativePath;
}
// 路由属性路径
if (actionDescriptor.AttributeRouteInfo != null)
{
info.Path = $"/{actionDescriptor.AttributeRouteInfo.Template}";
}
// Controller/Action 的路径以及调用
if (actionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
{
if (info.Path.IsEmpty())
{
info.Path =
$"/{controllerActionDescriptor.ControllerName}/{controllerActionDescriptor.ActionName}";
}
var controllerHtmlAttribute = controllerActionDescriptor.ControllerTypeInfo.GetCustomAttribute<RazorHtmlAttribute>();
if (controllerHtmlAttribute != null)
{
info.FilePath = controllerHtmlAttribute.Path;
info.TemplatePath = controllerHtmlAttribute.Template;
}
var htmlAttribute = controllerActionDescriptor.MethodInfo.GetCustomAttribute<RazorHtmlAttribute>();
if (htmlAttribute != null)
{
info.FilePath = htmlAttribute.Path;
info.TemplatePath = htmlAttribute.Template;
}
info.ControllerName = controllerActionDescriptor.ControllerName;
info.ActionName = controllerActionDescriptor.ActionName;
info.Invocation = $"{controllerActionDescriptor.ControllerName}Controller.{controllerActionDescriptor.ActionName}";
}
info.Invocation += $"({actionDescriptor.DisplayName})";
list.Add(info);
}
return list;
}
生成Html静态文件
/// <summary>
/// 生成Html文件
/// </summary>
/// <returns></returns>
public async Task Generate()
{
foreach (var routeInformation in _routeAnalyzer.GetAllRouteInformations())
{
// 跳过API的处理
if (routeInformation.Path.StartsWith("/api"))
{
continue;
}
await WriteViewToFileAsync(routeInformation);
}
}
/// <summary>
/// 渲染视图为字符串
/// </summary>
/// <param name="info">路由信息</param>
/// <returns></returns>
public async Task<string> RenderToStringAsync(RouteInformation info)
{
var razorViewEngine = Ioc.Create<IRazorViewEngine>();
var tempDataProvider = Ioc.Create<ITempDataProvider>();
var serviceProvider = Ioc.Create<IServiceProvider>();
var routeData = new RouteData();
if (!info.AreaName.IsEmpty())
{
routeData.Values.Add("area", info.AreaName);
}
if (!info.ControllerName.IsEmpty())
{
routeData.Values.Add("controller", info.ControllerName);
}
if (!info.ActionName.IsEmpty())
{
routeData.Values.Add("action", info.ActionName);
}
var httpContext = new DefaultHttpContext { RequestServices = serviceProvider };
var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor());
var viewResult = razorViewEngine.FindView(actionContext, info.ActionName, true);
if (!viewResult.Success)
{
throw new InvalidOperationException($"找不到视图模板 {info.ActionName}");
}
using (var stringWriter = new StringWriter())
{
var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
var viewContext = new ViewContext(actionContext, viewResult.View, viewDictionary, new TempDataDictionary(actionContext.HttpContext, tempDataProvider), stringWriter, new HtmlHelperOptions());
await viewResult.View.RenderAsync(viewContext);
return stringWriter.ToString();
}
}
/// <summary>
/// 将视图写入文件
/// </summary>
/// <param name="info">路由信息</param>
/// <returns></returns>
public async Task WriteViewToFileAsync(RouteInformation info)
{
try
{
var html = await RenderToStringAsync(info);
if (string.IsNullOrWhiteSpace(html))
return;
var path = Utils.Helpers.Common.GetPhysicalPath(string.IsNullOrWhiteSpace(info.FilePath) ? GetPath(info) : info.FilePath);
var directory = System.IO.Path.GetDirectoryName(path);
if (string.IsNullOrWhiteSpace(directory))
return;
if (Directory.Exists(directory) == false)
Directory.CreateDirectory(directory);
File.WriteAllText(path, html);
}
catch (Exception ex)
{
ex.Log(Log.GetLog().Caption("生成html静态文件失败"));
}
}
protected virtual string GetPath(RouteInformation info)
{
var area = info.AreaName.SafeString();
var controller = info.ControllerName.SafeString();
var action = info.ActionName.SafeString();
var path = info.TemplatePath.Replace("{area}", area).Replace("{controller}", controller).Replace("{action}", action);
return path.ToLower();
}
四、使用方式
MVC控制器配置

Startup配置

一次性生成方式,调用一次接口即可

五、源码地址
Util
Bing.NetCore
Razor生成静态Html文件:https://github.com/dotnetcore/Util/tree/master/src/Util.Webs/Razors 或者 https://github.com/bing-framework/Bing.NetCore/tree/master/src/Bing.Webs/Razors
六、参考
获取所有已注册的路由:https://github.com/kobake/AspNetCore.RouteAnalyzer
ASP.NET Core Razor生成Html静态文件的更多相关文章
- 学习ASP.NET Core Razor 编程系列十三——文件上传功能(一)
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- 《ASP.NET Core 高性能系列》静态文件中间件
一.概述 静态文件(如 HTML.CSS.图片和 JavaScript等文件)是 Web程序直接提供给客户端的直接加载的文件. 较比于程序动态交互的代码而言,其实原理都一样(走Http协议), ASP ...
- 学习ASP.NET Core Razor 编程系列十四——文件上传功能(二)
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- 学习ASP.NET Core Razor 编程系列十五——文件上传功能(三)
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- 学习ASP.NET Core Razor 编程系列十八——并发解决方案
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- 学习ASP.NET Core Razor 编程系列十六——排序
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- 学习ASP.NET Core Razor 编程系列十九——分页
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- 学习ASP.NET Core Razor 编程系列十七——分组
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- C#编译器优化那点事 c# 如果一个对象的值为null,那么它调用扩展方法时为甚么不报错 webAPI 控制器(Controller)太多怎么办? .NET MVC项目设置包含Areas中的页面为默认启动页 (五)Net Core使用静态文件 学习ASP.NET Core Razor 编程系列八——并发处理
C#编译器优化那点事 使用C#编写程序,给最终用户的程序,是需要使用release配置的,而release配置和debug配置,有一个关键区别,就是release的编译器优化默认是启用的.优化代码 ...
随机推荐
- 全网最详细的U盘被损坏导致一般性的软件无法修复的解决办法(必须可以)(图文详解)
不多说,直接上干货! 问题详情 一般,在不正当地操作U盘时,容易出现如下的情况: 解决办法: 本人,在尝试多款U盘修复工具软件后,发现: 成功几率很大,博文本人亲自尝试,并强烈推荐. 欢迎大家,加入我 ...
- 分步理解 Promise 的实现
一个 Promise 的运用: var firstPromise = new Promise(function(resolve,reject){ setTimeout(function(){ var ...
- MySQL Cluster 日常维护
在前面几篇文章已经详细介绍了MySQL Cluster的搭建,配置讲解.而且相信大家都掌握了基本用法.现在我们来看看Cluster的日常维护.熟悉日常维护,将有助于工作中更好的管理和使用Cluster ...
- 解决wamp 3.0.6 访问路径出现 403 错误
<VirtualHost *:80> ServerName localhost DocumentRoot e:/wamp64/www #your local dir path <Di ...
- 软件架构设计学习总结(19):详解分布式系统中的session同步问题
几周前,有个盆友问老王,说现在有多台服务器,怎么样来解决这些服务器间的session同步问题?老王一下就来精神了,因为在n年以前,老王还在学校和几个同学一起所谓创业的时候,也遇到了类似的问题.当时查了 ...
- 熟悉DAO模式的用法
今天主要是使用DAO模式. DAO模式通过对业务层提供数据抽象层接口,实现了以下目标: 1. 数据存储逻辑的分离 通过对数据访问逻辑进行抽象,为上层机构提供抽象化的数据访问接口.业务层无需关心具体的s ...
- nginx 代理之修改header 的HOST,实现代理转代理
现有一个需求,需要从nginx跳转到k8s的traefik代理上,从而实现服务的访问,用于测试.直接修改proxy_set_header的HOST,修改为traefuk代理的域名,proxy_pass ...
- 压测:celey backend为rabbitmq pk redis
使用celery的backend异步获取结果,本文使用rabbitmq 和 redis分别作为backend,代码对比如下 from celery import Celery, platforms i ...
- GCD之定时器dispatch_source_t(转载暂时未完全理解)
#import "ViewController.h" @interface ViewController (){ IBOutlet UIButton *l_timeButton; ...
- RadioButtonList根据值触发OnSelectedIndexChanged事件
Insus.NET有使用Iframe来处理另外一个站点的enter form,由于需要自动循环填入数据,免去人手操作.但是原来的Enter from有RadioButtonList控件以及OnSele ...