.NET Core之UI的类库(swagger/hangfire)
一、前言
在构建系统架构,开发功能的时候,我们会想到比如定时任务、接口测试工具功能可以引入swagger、hangfire、Quartz.NET等开源库来实现特定功能。我们通过在ASP.NET Core中第一步、引入类库,第二步、注入服务,第三步、启用中间,第四步、设置配置项,然后运行后会发现这些类库竟然提供了UI界面操作,这对于开发人员来说,界面可操作性、可视化极大提高了效率。由此引发了好奇,类库是如何把这个UI封装到类库中,引入到应用是如何进行访问的,其中涉及到的知识点包括哪些,后续实践这个带类库的UI。所以通过本篇内容来实践Demo,探索总结,通过研究基于Quartz.NET进行补充UI的开源类库GZY.Quartz.MUI,开源代码在GZY.Quartz.MUI。
二、简单实现
在swagger、hangfire、Quartz.NET中,引入组件完成四步,然后运行应用进行访问。通过指定的地址如/swagger、/hangfire、/Quartz返回组件的UI页面,所以主要实现的原理是在应用程序中对该路由地址进行拦截,获取类库的UI文件(html、js、css)返回文件流。其实不管是ASP.NET、还是ASP.NET Core等动态应用,最终都是给浏览器返回html、js、css等文件渲染成网页,而JSP,ASP,WebForm,模板引擎,前后端分离都是技术更新,前后端工程化的结果,本质是没有变化的。
第一步、创建一个ASP.NET Core应用,假设我们的UI类库是给这个应用引用的。
第二步、创建一个Source文件,新建htmpage.html,jquery.min.js,styleSheet.css文件,主要对应UI的html、js、css。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title></title> <!--引用Js--> <script type="text/javascript" src="/lib/jq.js"></script> <!--引用Css--> <link href="/lib/style.css" rel="stylesheet" /> <script type="text/javascript"> $(function () { $("#btnOk").click(function () { alert("ok"); }); }); </script> </head> <body> <h3>测试一下包含类库UI</h3> <button id="btnOk">Click</button> </body> </html>
body {
}
button {
background-color:coral;
border-radius:10px;
color: white;
width:150px;
padding:10px;
}
第三步、编写在ASP.NET Core中加载上述文件的类,如下HtmlHandler.cs。
namespace DemoUI { public class HtmlHandler { /// <summary> /// jquery.min.js /// </summary> /// <param name="app"></param> public void HandleMapJs(IApplicationBuilder app) { app.Run(async context => { var js = await GetResourceAsStringAsync("DemoUI.Source.jquery.min.js"); context.Response.ContentType = "application/javascript"; await context.Response.WriteAsync(js); await context.Response.CompleteAsync(); }); } /// <summary> /// htmlpage.html /// </summary> /// <param name="app"></param> public void HandleMapHtml(IApplicationBuilder app) { app.Run(async context => { var html = await GetResourceAsStringAsync("DemoUI.Source.htmlpage.html"); context.Response.ContentType = "text/html; charset=utf-8"; await context.Response.WriteAsync(html); await context.Response.CompleteAsync(); }); } /// <summary> /// styleSheet.css /// </summary> /// <param name="app"></param> public void HandleMapCss(IApplicationBuilder app) { app.Run(async context => { var html = await GetResourceAsStringAsync("DemoUI.Source.styleSheet.css"); context.Response.ContentType = "text/css; charset=utf-8"; await context.Response.WriteAsync(html); await context.Response.CompleteAsync(); }); } /// <summary> /// 获取资源文件内容 /// </summary> /// <param name="name"></param> /// <returns></returns> private async Task<string> GetResourceAsStringAsync(string name) { using (var stream = typeof(Program).Assembly.GetManifestResourceStream(name)) { if (stream != null) { var buffer = new byte[stream.Length]; await stream.ReadAsync(buffer, 0, buffer.Length); var content = System.Text.Encoding.UTF8.GetString(buffer); return content; } else { return string.Empty; } } } } }
通过上述代码,主要使用中间件run方法,Assembly类的GetManifestResourceStream获取文件流,context上下文的请求结果对象Response写回文件流。
注意,在获取js、css、html的时候的文件格式,比如text/css、text/html、application/javascript;在使用GetManifestResourceStream方法时候传入的是,GetManifestResourceStream中的资源清单,资源清单格式:项目命名空间.资源文件所在文件夹名.资源文件名。否则无法获取文件流,返回NULL值。并且要将上述的js、css、html文件在文件属性中生成操作设置成“嵌入资源”才能生效!!
第四步、在program文件中使用map方法,设置路由地址与HtmlHandler方法对应,获取文件流。如下所示
using DemoUI; namespace DemoUI { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorPages(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } else { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); HtmlHandler htmlHandler = new HtmlHandler(); //jquery.min.js app.Map("/lib/jq.js", htmlHandler.HandleMapJs); //htmlpage.html app.Map("/lib/index.html", htmlHandler.HandleMapHtml); //styleSheet.css app.Map("/lib/style.css", htmlHandler.HandleMapCss); app.UseAuthorization(); app.MapRazorPages(); app.Run(); } } }
通过app.Map("/lib/jq.js", htmlHandler.HandleMapJs);app.Map("/lib/index.html", htmlHandler.HandleMapHtml);app.Map("/lib/style.css", htmlHandler.HandleMapCss);方法返回了页面所需内容,浏览器渲染构造页面。
通过访问预设定的路由地址/lib/index.html,拦截请求,返回UI信息,这个只是简单的Demo,并且没有进行封装,生成nuget包,但是主要实现原理基本一致的。
三、分析源码
上述通过基本原理实现一个简单的Demo应用,实际的场景不仅仅是一个静态的UI界面,还可能是动态涉及业务逻辑,数据库等,我们基于GZY.Quartz.MUI开源定时任务调到UI框架来学习复杂的带UI的类库是怎么实现的,源码主要内容是什么?先介绍源码项目的目录层级。
其中GZY.Quartz.MUI是类库项目,主要实现类库UI,动态功能,Quartz.Test是该类库提供的测试项目,为了方便不进行nuget打包,直接在Quartz.Test项目中引用该类库,进行测试。我们从GZY.Quartz.MUI的目录文件夹出发,然后关注关键代码块,由整体层次到关键细节的解读源码内容,形成如何构建一个成熟的带UI的类库的实践。
1、GZY.Quartz.MUI创建一个Web应用,然后修改输出类型为类库,对该项目进行打包,然后上传nuget服务于其它项目中,主要文件夹①wwwroot包含css/js/vue等前端静态文件;②Areas包含前端html页面,使用RazorPages的方式动态加载内容,返回给浏览器;③BaseJobs、BaseService,BaseService定义定时任务的接口,然后在BaseJobs文件夹实现该接口,通过使用dll组件,HTTP请求实现接口,定义定时任务执行可以选择两种方式。④EFContext新增.NET Core的EF数据库上下文,定时任务涉及到相关数据的持久化功能。⑤Enum主要定义一个任务的枚举类型,标识任务的状态信息。⑥Extensions包含扩展类,扩展程序启动类中服务注入,中间件的启动等方法。⑦Middleware文件中定义一个中间件,ASP.NET Core的请求是通过中间件形成的管道进行处理的,这个定时任务要引入ASP.NET Core中一般要注入服务,启用中间件拦截请求来实现这个UI效果。⑧Model文件夹主要定义一下模型对象,表对象。⑨Service文件是核心类文件,主要涉及接口,数据库创建服务,文件日志服务,操作任务的服务等。⑨Tools包含工具类。在项目通过文件夹对各个类进行归类,划分层级,一个比较清晰的项目结构,简单实用。
2、关于定义中间件,提供中间件的调用,在项目定义一个中间件类QuartzUIBasicAuthMiddleware,基于中间件定义,实现一个委托,如果不是终结点则传递下一个中间件处理逻辑中,否则返回,构成一个管道,这个一样按照这个实现。
public class QuartzUIBasicAuthMiddleware { private readonly RequestDelegate next; private readonly IConfiguration _configuration; public QuartzUIBasicAuthMiddleware(RequestDelegate next, IConfiguration configuration) { this.next = next; _configuration = configuration; } public async Task InvokeAsync(HttpContext context) { //拦截QuartzUI开头的访问 if (context.Request.Path.StartsWithSegments("/QuartzUI")) { string authHeader = context.Request.Headers["Authorization"]; if (authHeader != null && authHeader.StartsWith("Basic ")) { //帐户密码读取并解码 var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim(); var decodedUsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword)); var username = decodedUsernamePassword.Split(':', 2)[0]; var password = decodedUsernamePassword.Split(':', 2)[1]; if (IsAuthorized(username, password)) { await next.Invoke(context); return; } } context.Response.Headers["WWW-Authenticate"] = "Basic"; context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; } else { await next.Invoke(context); } } /// <summary> /// 设置密码 /// </summary> /// <param name="username"></param> /// <param name="password"></param> /// <returns></returns> public bool IsAuthorized(string username, string password) { // 从配置读取帐户密码,否则默认 var Username = _configuration["QuartzUI:UserName"] ?? "Admin"; var Pwd = _configuration["QuartzUI:Pwd"] ?? "123456"; return username.Equals(Username, StringComparison.InvariantCultureIgnoreCase) && password.Equals(Pwd); } }
上述代码中,对当前请求路由进行拦截,判断是否包含/QuartzUI,如果是则表示当前项目访问了UI类型,在请求内部进行了Basic的权限验证,弹出验证输入框输入,验证成功则执行下一步await next.Invoke(context);return;这里完成Invoke方法后会return,就是相当于终节点总结请求返回请求结果,而请求结果就是定时任务的UI信息,验证失败返回权限验证失败的Code信息,否则使用await next.Invoke(context);流转下一个中间件,不做任何处理,返回正常请求的业务流程。使用_configuration配置项时,该配置项在引用项目中json文件进行配置读取数据。
3、关于定义服务,在Service文件中,创建了IQuartzLogService、IQuartzService、HttpResultfulJob、ClassLibraryJob、ISchedulerFactory、IJobFactory、IQuartzHandle,在ASP.NET Core框架中提供一个重要基础组件就是实现了依赖注入(DI)实现创建对象的解耦,所以上述的类库要使用的服务一样要进行服务的注入。
4、关于服务的注入,中间件的使用,通过QuartzUIExtension静态类,对IServiceCollection、IApplicationBuilder对象扩展实现服务注入、中间件的启用。
public static class QuartzUIExtension { public static IServiceCollection AddQuartzUI(this IServiceCollection services, DbContextOptions<QuarzEFContext> option = null) { services.AddRazorPages(); services.AddHttpClient(); services.AddHttpContextAccessor(); if (option != null) { services.AddSingleton<DbContextOptions<QuarzEFContext>>(a => { return option; }); services.AddDbContext<QuarzEFContext>(); services.AddScoped<IQuartzLogService, EFQuartzLogService>(); services.AddScoped<IQuartzService, EFQuartzService>(); } else { services.AddScoped<QuartzFileHelper>(); services.AddScoped<IQuartzLogService, FileQuartzLogService>(); services.AddScoped<IQuartzService, FileQuartzService>(); } services.AddScoped<HttpResultfulJob>(); services.AddScoped<ClassLibraryJob>(); services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>(); services.AddSingleton<IJobFactory, ASPDIJobFactory>(); services.AddScoped<IQuartzHandle, QuartzHandle>(); return services; } /// <summary> /// 自动注入定时任务类 /// </summary> /// <param name="services"></param> /// <returns></returns> public static IServiceCollection AddQuartzClassJobs(this IServiceCollection services) { var baseType = typeof(IJobService); var path = AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory; var referencedAssemblies = Directory.GetFiles(path, "*.dll"); List<Type> typelist = new List<Type>(); foreach (var item in referencedAssemblies) { try { var assembly = Assembly.LoadFrom(item); Type[] ts = assembly.GetTypes(); typelist.AddRange(ts.ToList()); } catch (Exception) { continue; } } var types = typelist .Where(x => x != baseType && baseType.IsAssignableFrom(x)).ToArray(); var implementTypes = types.Where(x => x.IsClass).ToArray(); var interfaceTypes = types.Where(x => x.IsInterface).ToArray(); foreach (var implementType in implementTypes) { var interfaceType = implementType.GetInterfaces().First(); services.AddScoped(interfaceType, implementType); ClassJobsFactory.ClassJobs.Add(implementType.Name); } return services; } /// <summary> /// 启用中间 /// </summary> /// <param name="builder"></param> /// <returns></returns> public static IApplicationBuilder UseQuartz(this IApplicationBuilder builder) { builder.UseRouting(); builder.UseStaticFiles(); builder.UseEndpoints(endpoints => { endpoints.MapRazorPages(); }); IServiceProvider services = builder.ApplicationServices; using (var serviceScope = services.CreateScope()) { var dd = serviceScope.ServiceProvider.GetService<IQuartzHandle>(); dd.InitJobs().GetAwaiter().GetResult(); } return builder; }
/// <summary> /// 注入授权信息 /// </summary> /// <param name="builder"></param> /// <returns></returns> public static IApplicationBuilder UseQuartzUIBasicAuthorized(this IApplicationBuilder builder) { return builder.UseMiddleware<QuartzUIBasicAuthMiddleware>(); } }
在该类中实现AddQuartzUI、AddQuartzClassJobs、UseQuartz、UseQuartzUIBasicAuthorized四个方法,两个服务的添加,两个中间件的使用,其中UseQuartzUIBasicAuthorized扩展方法就是通过UseMiddleware加载中间件类,只是使用Use方式扩展使用方式。AddQuartzUI方法中注入很多服务,包括这个项目自定义服务接口,RazorPages、数据库服务、在定时任务接口请求方式要使用HttpClient对象等。在实际引入这个组件就是通过上述扩展方法,在program中添加服务,中间件。
5、关于如何路由到page页面Main.cshtml中?上述服务与中间件是对请求的接收与定时任务功能处理,思考一个问题,系统如何将/QuartzUI请求地址路由到Main页面?主要是通过如下所示:
关键点在于这个@page "/QuartzUI"中,我们现在比较多是使用MVC或者前后端分离模式开发Web应用,但是Page的模式在一些场景下还是挺好使用,路由请求对应的页面明了,然后在页面.cs文件中完善业务逻辑内容
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using GZY.Quartz.MUI.Enum; using GZY.Quartz.MUI.Model; using GZY.Quartz.MUI.Service; using GZY.Quartz.MUI.Tools; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Newtonsoft.Json; namespace GZY.Quartz.MUI.Areas.MyFeature.Pages { public class MainModel : PageModel { private IQuartzHandle _quartzHandle; private IQuartzLogService _logService; public MainModel(IQuartzHandle quartzHandle, IQuartzLogService logService) { _quartzHandle = quartzHandle; _logService = logService; } [BindProperty] public tab_quarz_task Input { get; set; } /// <summary> /// 获取任务列表 /// </summary> /// <returns></returns> public async Task<IActionResult> OnGetSelectJob() { var jobs = await _quartzHandle.GetJobs(); return new JsonDataResult(jobs); } /// <summary> /// 新建任务 /// </summary> /// <returns></returns> public async Task<IActionResult> OnPostAddJob() { var date = await _quartzHandle.AddJob(Input); Input.Status = Convert.ToInt32(JobState.暂停); return new JsonDataResult(date); } /// <summary> /// 暂停任务 /// </summary> /// <returns></returns> public async Task<IActionResult> OnPostPauseJob() { var date = await _quartzHandle.Pause(Input); return new JsonDataResult(date); } /// <summary> /// 开启任务 /// </summary> /// <returns></returns> public async Task<IActionResult> OnPostStartJob() { var date = await _quartzHandle.Start(Input); return new JsonDataResult(date); } /// <summary> /// 立即执行任务 /// </summary> /// <returns></returns> public async Task<IActionResult> OnPostRunJob() { var date = await _quartzHandle.Run(Input); return new JsonDataResult(date); } /// <summary> /// 修改任务 /// </summary> /// <returns></returns> public async Task<IActionResult> OnPostUpdateJob() { var date = await _quartzHandle.Update(Input); return new JsonDataResult(date); } /// <summary> /// 删除任务 /// </summary> /// <returns></returns> public async Task<IActionResult> OnPostDeleteJob() { var date = await _quartzHandle.Remove(Input); return new JsonDataResult(date); } /// <summary> /// 获取任务执行记录 /// </summary> /// <returns></returns> public async Task<IActionResult> OnPostJobRecord(string taskName, string groupName, int current, int size) { var date = await _logService.GetLogs(taskName,groupName, current, size); return new JsonDataResult(date); } /// <summary> /// 获取已注入的任务类 /// </summary> /// <returns></returns> public IActionResult OnGetSelectClassJob() { var date = ClassJobsFactory.ClassJobs; return new JsonDataResult(date); } public void OnGet() { } } }
所以,分分合合,不一定前后端拆分就是最好的实践,有些时候使用一些这种Page的方式可能带来更好的体验或者收益!!
3、如何使用这个组件,对项目类库输出打包成package包上传至nuget中私有或者共有的包服务器中,这里创建一个测试项目,直接解决方案下引入项目。
public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddRazorPages(); builder.Services.AddQuartzClassJobs(); builder.Services.AddQuartzUI(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseAuthorization(); app.UseQuartz(); app.UseQuartzUIBasicAuthorized(); app.MapControllers(); app.Run(); } }
上述使用中,builder.Services.AddQuartzClassJobs();builder.Services.AddQuartzUI();是加入扩展类中注入的文件服务,app.UseQuartz();app.UseQuartzUIBasicAuthorized();则是启用中间件,加入管道,拦截请求,返回UI信息页面。然后启动这个测试项目,如下所示:
五、总结
上述内容主要介绍如何是实现一个带UI的类库,类似hanfire、swagger,然后基于开源的GZY.Quartz.MUI项目系统学习,分析怎么构建一个成熟可提供给系统使用的带UI的组件,从基本的原理出发明白其中本质内容,然后通过一个超级简单的Demo实现相关效果,最后结合具体框架ASP.NET Core,编写一套完整实现方式学习这快内容。所以要勤于实践,善于总结,从原理入手理解根本性内容,着手于小Demo运行效果,理解成熟的架构方案。并且多阅读优先的源码学习架构设计,层次设计。。。。。。
参考:https://blog.csdn.net/xiaoxionglove/article/details/102847302,https://gitcode.com/l2999019/GZY.Quartz.MUI/overview
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?