从 Dingo API 原理来看 Laravel 的 Http 请求处理过程

Dingo Api 是一个使用率比较高的包,通常用在 Web API 的开发中。对我来说这三个功能吸引力比较大:

  • 路由版本管理
  • Http Exception 处理
  • Response Transform

如果异常处理以及把结果转换成符合 JSON 标准的响应,都靠手工去实现,确实很烦琐,而 Dingo API 帮我们把这个功能实现了,来看看它的原理。

区分不同的请求

原生的路由系统在 Laravel 中维护了一个单例的 Router,创建的所有路由「Route」都是由 Router 来管理,并且把请求分发的对应的 Route 中。

我们知道 Dingo Api 的路由是可以和 Laravel 原生的路由共存,如果 Dingo 和 Laravel 使用同一个 Router,那么没办法实现路由的版本控制以及处理 Http Exception(实际上 5.4 版本的 Router 中已经提供了 version 接口,只是还没实现,后续应该会实现这个功能);如果 Dingo 维护一个新的 Router 的话,请求是在 Http 的 Kernel 中 handle 给 Router 的,在 Http Kernel 中 Dingo 根本没有接口把自己的 Router 注入到 Kernel 中并让 Kernel 使用 Dingo 的Router。

Dingo 确实自己维护了一个 Router,这个 Router 中实现了路由的版本管理。

区分不同的请求很简单,通过一个中间件来判断请求的 host 或 uri 就行。在使用 Dingo 的时候需要设置 prefix 或者 domain,这两个值就是 Dingo 用来区分 Dingo 路由和 Laravel 的路由的特征值。

关键是如何把中间件运用到路由中。

中间件注入的时机

中间件有四个级别的:一个是全局中间件,也就是 Kernel 中的 Middleware 数组,对所有的请求都有效;第二个是路由的中间件, 通过注册路由时添加中间件,这个只对当前路由有效;第三个是控制区中间件,可以细化到具体的某一个方法;最后一个是 Terminate 中间件,这个中间件是在响应发送之后作用的。

这四个中间件通过的时间顺序也不一样,最先的是全局中间件,然后路由中间件和控制器中间件,最后是 Terminate 中间件。

因为要接管所有请求并判断是否为 API 请求,所以中间件必须是全局中间件,并且请求(Request)要尽量最先通过这个中间件,因为不能保证其他中间件是否会有一些副作用。

在 Http Kernel 中提供了两个方法,prependMiddleware 以及 pushMiddleware,这两个方法可以向 middleware 数组中添加中间件,一个是添加到数组的开头,一个是追加到末尾。

什么时候添加呢?在 ServiceProvider 的 boot 方法中。

这个时候 Kernel 已经创建了,并且请求也 handle 到 Kernel 中了,接下来就是调用 sendRequestThroughRouter 这个方法交给 Router 来处理。在通过所有的中间件之前会调用所有的 ServiceProvider 的 boot 方法,这个时候可以添加你自定义的中间件,之后 Kernel 会使用管道来通过所有的中间件。

中间件的作用

中间件的作用很简单,通过前面说的 prefix 和 domain 等特征值,判断一个请求是否是 API 请求,如果不是的话,那么调用 $next($request) ,把处理交回给 Laravel。如果判断是 API 请求的话,则交给 Dingo 自定义的 sendRequestThroughRouter 来处理。

看到 Dingo 这样的处理方式,给我很大的启发。之前中间件基本上用来做一些有副作用的操作,根本没想到可以以这种方式来接管后面所有请求的操作,也正是这个方式,也让 Dingo 可以从这个阶段开始接管所有的异常处理。

在判断为 API 请求时,会出发一个事件,这个事件会让容器用 Dingo 的 Router 替换原生的 Router。

异常处理

在 Dingo 实现自己的 sendRequestThroughRouter 的时候,就可以在这个方法里面 catch 所有的异常,并针对不同的异常返回不同的 Json 响应。

在 Dingo 中创建了自定义的 Exception handle,这个 Handle 会返回对应异常的 JSON 响应,接着再把异常交给 Laravel 的 Exception Handle 来处理。

自定义的 Router 实现 Api 版本管理

Dingo 中的版本有两种不同的模式,严格模式以及宽松模式。严格模式就是需要传入指定的 Accept Header:Accept:application/x.SUBTYPE.v1+json,通过这个头来判断请求的版本,如果没有对应的请求头,则会抛出异常;宽松模式就是如果没有指定对应的版本,则使用设置中的默认版本。

现在 web API 的版本管理貌似用的比较多的就是两种,要不直接写在 uri 中,要不是用请求头来实现,之前看 Dingo 里面的「标准树」,完全搞不懂啥玩意,其实就是定义一个 MIME 类型,对整个功能来说应该没有影响,但是对一些标准啥的有规定吧。

通过这个头可以解析出请求的 API 版本以及响应的格式。

Dingo 定义了一个全新的 Router,Version 方法是添加了一个版本名称的 Group,这样在分发到请求对应的路由时会查找对应版本的 Group。

这种方式在我看来有利有弊,有利的一方面是由于它自定义了所有 Router 的工作方式,可以很方便的自定义一些操作,比如版本管理,比如可以介入响应(Response)的生成,通过这种方式可以做到一个很细化的程度;弊端呢,在我看来,Dingo 里面有大量的自定义的东西,包括请求,Router Route 等等,以及大量的 Adapter,这样做的结果是整个包臃肿了,逻辑更复杂了,而且兼容性可能也不是太好,比如可能 Laravel 升级了,然后对应的方法有可能 Dingo 没办法做到很快的同步性等。

我倒是有一个思路,比如可以创建一个 Router,继承自 Laravel 的 Router,版本管理很简单,判断路由注册时版本是否跟请求的版本一致,一致的话注册到 Router 中,不一致的话就用一个实现了 Router 接口的 Mock 类来注册,不会影响到路由的调用。

这样做的方法也有利有弊,有利的部分因为继承自原生的 Router,不用管兼容性问题,而且其他方法都是原生的 Router,不用去管那么复杂的逻辑,另外一方面由于没有加载其他版本的路由,在路由分发的时候应该性能会好一点。弊端呢,一方面自主性没有那么大,比如如何去生成指定的响应等等,另外一方面这种方式不知道在使用路由缓存时是否可以很好的处理。

在 Laravel 的 Router 中有 version 接口,看看后续官方是否会不会出这个功能吧。

再来看 Laravel 的生命周期

现在反过来看 Laravel 这个框架,真的觉得扩展性非常高。比如,通过添加中间件,可以在不同的阶段完成一些副作用的行为,甚至可以直接接管整个请求的处理过程。

再比如,只要找准了那个点,在这个组件起作用之前,通过容器的重新绑定,你可以替换任何默认的组件,而对整个框架无需做任何修改,只要通过 ServiceProvider。

这些看起来有些 Geek 的方式,在对 Laravel 框架足够熟悉的情况下,无疑是我们的另外一种选择。

Oustn

    原文作者:HTTP
    原文地址: https://juejin.im/entry/58fdc633b123db74d880f27c
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞