Koa.js是基于Node.js平台的下一代web开发框架

Gin是Golang的高性能web框架

Asp.net core是.net 的web框架

0.前言

编程语言都有所不同,各个语言解决同一类问题而设计的框架,确有共通之处,毕竟是解决同一类问题,面临的挑战大致相同,比如身份验证,api授权等等,鄙人对node.js,golang,.net core有所涉猎,对各自的web框架进行学习的过程中发现了确实有相似之处。下面即对node.js的koa、golang的gin与.net core的asp.net core三种不同的web后端框架的中间件做一个分析对比

1.Node-Koa.js

1.1 中间件分类

应用级中间件

//如果不写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=404
        ctx.body="这是一个404"
    }else{
        console.log(ctx.url)
    }
})

//3.匹配下面的路由
 router.get('/news',async(ctx)=>{
     console.log("this is news")
     ctx.body="这是一个新闻页面"
 })

1.2 第三方中间件

静态资源中间件为例:静态资源地址没有路由匹配,盲目引入静态资源,会报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'))

1.3 中间件执行顺序

洋葱执行:从上到下依次执行,匹配路由响应,再返回至中间件进行执行中间件,【先从外向内,然后再从内向外】

2.Golang-Gin

钩子(Hook)函数,中间件函数

2.1 定义中间件

package main

import(
	"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...")
}

2.2 注册中间件

全局注册-某个路由单独注册-路由组注册

package main

import(
	"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)

2.3 中间件执行顺序

与koa中间件执行顺序一致

2.4 中间件通常写法-闭包

func authMiddleware(doCheck bool) gin.HandlerFunc{
    //连接数据库
    //或准备工作
    return func(c *gin.Context){
        //是否登录判断
        //if是登录用户
        //c.Next()
        //else
        //c.Abort()
    }
}

2.5 中间件通信

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
}

2.6 中间件中使用goroutine

当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context) 必须使用其只读副本c.Copy(),否则会出现线程安全问题。

3.Net Core-Asp.net core

3.1 创建中间件管道

使用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的应用级中间件很像

3.2 创建中间件管道分支

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.");
        });
    }
}

3.3 内置中间件

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();
    });
}

3.4 自定义中间件

3.4.1 在Configure中直接写

//在Startup.Configure直接编码
public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            //做一些操作

            // Call the next delegate/middleware in the pipeline
            await next();
        });

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello world");
        });
    }

3.4.2 中间件类+中间件扩展方法+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 pipeline
            await _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}");
        });
    }
}

4.Net -Asp.Net

对于asp.net core的中间件与koa.js,gin中间件,实现形式略有不同,但是终极目标只有一个,就是AOP,面向切面编程,减少代码量,不至于在某一个路由匹配的方法中去编写同样的代码。

在asp.net core之前,还是asp.net的时候,也有类似的AOP实现,去继承各种FilterAttribute ,重写方法,如启用属性路由,创建自定义授权过滤器,创建自定义身份验证过滤器,模型验证过滤器。