.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静态类,对IServiceCollectionIApplicationBuilder对象扩展实现服务注入、中间件的启用。

复制代码
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

posted @   tuqunfu  阅读(188)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示