web框架中间件对比
编程语言都有所不同,各个语言解决同一类问题而设计的框架,确有共通之处,毕竟是解决同一类问题,面临的挑战大致相同,比如身份验证,api授权等等,鄙人对node.js,golang,.net core有所涉猎,对各自的web框架进行学习的过程中发现了确实有相似之处。下面即对node.js的koa、golang的gin与.net core的asp.net core三种不同的web后端框架的中间件做一个分析对比
Node-Koa.js
应用级中间件
//如果不写next,就不会向下匹配--匹配任何一个路由
app.use(async(ctx,next)=>{console.log(new Date())await next();
})
路由级中间件
router.get('/news',async(ctx,next)=>{console.log("this is news")await next();})
错误处理中间件
app.use(async(ctx,next)=>{//应用级中间件 都需要执行/*1.执行若干代码*/next();//2.执行next() 匹配其他路由//4.再执行if(ctx.status==404){ctx.status=404ctx.body="这是一个404"}else{console.log(ctx.url)}
})//3.匹配下面的路由router.get('/news',async(ctx)=>{console.log("this is news")ctx.body="这是一个新闻页面"})
第三方中间件
静态资源中间件为例:静态资源地址没有路由匹配,盲目引入静态资源,会报404.
//安装
npm install koa-static --save//使用
//引入
const static=require('koa-static')
//使用
app.use(static('static')) //去static文件目录中将中找文件,如果能找到对应的文件,找不到就next()app.use(static(__dirname+'/static'))app.use(static(__dirname+'/public'))
中间件执行顺序
洋葱执行:从上到下依次执行,匹配路由响应,再返回至中间件进行执行中间件,【先从外向内,然后再从内向外】
Golang-Gin
钩子(Hook)函数,中间件函数
定义中间件
package mainimport("github.com/gin-gonic/gin"
)func main(){r:=gin.Default()r.GET("/index",func(c *gin.Context){//...})r.Run()
}func m1(c *gin.Context){fmt.Println("中间件m1")c.Next()//调用后续的处理函数//c.Abort()//阻止调用后续的处理函数fmt.Println("m1 out...")
}
注册中间件
全局注册-某个路由单独注册-路由组注册
package mainimport("github.com/gin-gonic/gin"
)func main(){r:=gin.Default()r.GET("/index",func(c *gin.Context){//...})//某个路由单独注册--也可以取名为路由级注册中间件r.GET("/test1",m1,func(c *gin.Context){//...})//路由组注册xxGroup:=r.Group("/xx",m1){xxGroup.GET("/index",func(c *gin.Context){//...}) }xx2Group:=r.Group("/xx2")xx2Group.Use(m1){xxGroup.GET("/index",func(c *gin.Context){//...}) }r.Run()r.GET("/index",m1)
}func m1(c *gin.Context){fmt.Println("中间件m1")c.Next()//调用后续的处理函数//c.Abort()//阻止调用后续的处理函数//return 连下方的fmt.Println都不执行了,立即返回fmt.Println("m1 out...")
}r.Use(m1)//全局注册//多个中间件注册
r.Use(m1,m2)
中间件执行顺序
与koa中间件执行顺序一致
中间件通常写法-闭包
func authMiddleware(doCheck bool) gin.HandlerFunc{//连接数据库//或准备工作return func(c *gin.Context){//是否登录判断//if是登录用户//c.Next()//else//c.Abort()}
}
中间件通信
func m1(c *gin.Context){fmt.Println("m1 in ...")start := time.Now()c.Next()cost:=time.Since(start)fmt.Printf("cost:%v\n",cost)fmt.Println("m1 out...")
}func m2(c *gin.Context){fmt.Println("m2 in...")//中间件存值c.Set("name","carfield")fmt.Println("m2 out...")//其他中间件取值// c.Get// c.MustGet
}
中间件中使用goroutine
当在中间件或handler中启动新的goroutine时,不能使用原始的上下文
(c *gin.Context)
必须使用其只读副本c.Copy()
,否则会出现线程安全问题。
.Net Core-Asp.net core
创建中间件管道
使用IApplicationBuilder 创建中间件管道
//Run public class Startup {public void Configure(IApplicationBuilder app){app.Run(async context =>{await context.Response.WriteAsync("Hello, World!");});} }//Use - Run public class Startup {public void Configure(IApplicationBuilder app){app.Use(async (context, next) =>{// Do work that doesn't write to the Response.await next.Invoke();// Do logging or other work that doesn't write to the Response.});app.Run(async context =>{await context.Response.WriteAsync("Hello from 2nd delegate.");});} }//这个Use是不是跟koa的应用级中间件很像
创建中间件管道分支
Map 扩展用作约定来创建管道分支。
Map
基于给定请求路径的匹配项来创建请求管道分支。如果请求路径以给定路径开头,则执行分支。koa和gin中路由匹配就是map这种,当不使用内置的mvc模板路由,我姑且称它为自定义路由
public class Startup
{private static void HandleMapTest1(IApplicationBuilder app){app.Run(async context =>{await context.Response.WriteAsync("Map Test 1");});}private static void HandleMapTest2(IApplicationBuilder app){app.Run(async context =>{await context.Response.WriteAsync("Map Test 2");});}public void Configure(IApplicationBuilder app){app.Map("/map1", HandleMapTest1);app.Map("/map2", HandleMapTest2);app.Run(async context =>{await context.Response.WriteAsync("Hello from non-Map delegate. <p>");});}//请求会匹配 map1...map2...没匹配到路由的统统会执行app.Run
}//像golang的gin一样,map也支持嵌套
app.Map("/level1", level1App => {level1App.Map("/level2a", level2AApp => {// "/level1/level2a" processing});level1App.Map("/level2b", level2BApp => {// "/level1/level2b" processing});
});public class Startup
{private static void HandleMultiSeg(IApplicationBuilder app){app.Run(async context =>{await context.Response.WriteAsync("Map multiple segments.");});}public void Configure(IApplicationBuilder app){app.Map("/map1/seg1", HandleMultiSeg);app.Run(async context =>{await context.Response.WriteAsync("Hello from non-Map delegate.");});}
}//MapWhen 基于给定谓词的结果创建请求管道分支。Func<HttpContext, bool> 类型的任何谓词均可用于将请求映射到管道的新分支。 在以下示例中,谓词用于检测查询字符串变量 branch 是否存在:public class Startup
{private static void HandleBranch(IApplicationBuilder app){app.Run(async context =>{var branchVer = context.Request.Query["branch"];await context.Response.WriteAsync($"Branch used = {branchVer}");});}public void Configure(IApplicationBuilder app){app.MapWhen(context => context.Request.Query.ContainsKey("branch"),HandleBranch);app.Run(async context =>{await context.Response.WriteAsync("Hello from non-Map delegate. <p>");});}
}
//UseWhen 也是基于给定谓词的结果创建请求管道分支。 与 MapWhen 不同的是,如果这个分支发生短路或包含终端中间件,则会重新加入主管道:
public class Startup
{private readonly ILogger<Startup> _logger;public Startup(ILogger<Startup> logger){_logger = logger;}private void HandleBranchAndRejoin(IApplicationBuilder app){app.Use(async (context, next) =>{var branchVer = context.Request.Query["branch"];_logger.LogInformation("Branch used = {branchVer}", branchVer);// Do work that doesn't write to the Response.await next();// Do other work that doesn't write to the Response.});}public void Configure(IApplicationBuilder app){app.UseWhen(context => context.Request.Query.ContainsKey("branch"),HandleBranchAndRejoin);app.Run(async context =>{await context.Response.WriteAsync("Hello from main pipeline.");});}
}
内置中间件
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{if (env.IsDevelopment()){//开发人员异常页中间件 报告应用运行时错误app.UseDeveloperExceptionPage();//数据库错误页中间件报告数据库运行时错误app.UseDatabaseErrorPage();}else{//异常处理程序中间件app.UseExceptionHandler("/Error");//http严格传输安全协议中间件app.UseHsts();}//HTTPS重定向中间件app.UseHttpsRedirection();//静态文件中间件app.UseStaticFiles();//Cookie策略中间件app.UseCookiePolicy();//路由中间件app.UseRouting();//身份验证中间件app.UseAuthentication();//授权中间件app.UseAuthorization();//会话中间件-如果使用session,就需要把cookie策略中间件先使用了,再引入session中间件,再引入mvc中间件,毕竟session是依赖cookie实现的app.UseSession();//终结点路由中间件app.UseEndpoints(endpoints =>{endpoints.MapRazorPages();});
}
自定义中间件
在Configure
中直接写
//在Startup.Configure直接编码
public void Configure(IApplicationBuilder app){app.Use(async (context, next) =>{//做一些操作// Call the next delegate/middleware in the pipelineawait next();});app.Run(async (context) =>{await context.Response.WriteAsync($"Hello world");});}
中间件类+中间件扩展方法+UseXX
在
Startup.Configure
直接编码,当定义多个中间件,代码难免变得臃肿,不利于维护,看看内置的中间件,app.UseAuthentication();
多简洁,查看asp.net core源码,内置的中间件都是一个中间件类xxMiddleware.cs
一个扩展方法xxMiddlewareExtensions.cs
然后在Startup.Configure
中使用扩展方法调用Usexx()
using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;namespace Culture
{public class RequestTestMiddleware{private readonly RequestDelegate _next;//具有类型为 RequestDelegate 的参数的公共构造函数public RequestTestMiddleware(RequestDelegate next){_next = next;}//名为 Invoke 或 InvokeAsync 的公共方法。 此方法必须://返回 Task。//接受类型 HttpContext 的第一个参数。public async Task InvokeAsync(HttpContext context){//做一些操作// Call the next delegate/middleware in the pipelineawait _next(context);}}
}//中间件扩展方法
using Microsoft.AspNetCore.Builder;namespace Culture
{public static class RequestTestMiddlewareExtensions{public static IApplicationBuilder UseRequestTest(this IApplicationBuilder app){if (app == null){throw new ArgumentNullException(nameof(app));}return app.UseMiddleware<RequestTestMiddleware>();}}
}//调用中间件
public class Startup
{public void Configure(IApplicationBuilder app){app.UseRequestTest();app.Run(async (context) =>{await context.Response.WriteAsync($"Hello {CultureInfo.CurrentCulture.DisplayName}");});}
}
.Net -Asp.Net
对于asp.net core的中间件与koa.js,gin中间件,实现形式略有不同,但是终极目标只有一个,就是AOP,面向切面编程,减少代码量,不至于在某一个路由匹配的方法中去编写同样的代码。在asp.net core之前,还是asp.net的时候,也有类似的AOP实现,去继承各种FilterAttribute ,重写方法,如启用属性路由,创建自定义授权过滤器,创建自定义身份验证过滤器,模型验证过滤器