一、mvc的基本使用
在iris中,还封装了mvc包,该包可以让开发者快速的搭建出基于mvc(model-view-controller)分层的业务系统。其基本使用如下:
package main
import (
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/mvc"
)
func main() {
app := iris.New()
// 基于app.Party("/")分组 初始化mvcApplication
mvcApplication := mvc.New(app.Party("/"))
// 注册controller
mvcApplication.Handle(new(testController))
app.Listen(":8080")
}
// 定义controller处理器
type testController struct {
Ctx *context.Context
}
func (c *testController) GetHome() {
c.Ctx.Writef(ctx.Method())
}
func (c *testController) Post() {
c.Ctx.Writef(ctx.Method())
}
这样,就能搭建一个最简单的controller处理器了。然后运行该服务,在浏览器中输入 http://localhost:8080/home
就能访问到testController
的GetHome
方法。
我们知道,在iris框架中,是需要注册路由的,即将路径对应到特定的context.Handler类型
(函数类型)的处理器,当用户访问对应的路径时,才能执行对应处理器上的函数体的逻辑的。那么,mvc包
是如何做到路径到controller
中函数的映射的呢?
二、mvc的实现原理
通过上面使用基于mvc包
的示例,我们可以了解到,mvc包
的操作实际上是基于mvc.Application
类型的实例进行的。在初始化mvc.Application实例的时候,传入参数是一个app.Party的对象,即一个路由组。因此,一个mvc.Application
实例本质上就是一个路由分组。
然后,通过mvc.Application.Handle
方法,将testController类型的对象进行注册。实际上是将controller
依赖的对象(model、service
等)注入到controller
中、将Controller
中的方法转换成路由、将controller
中执行的结果渲染到view
的过程。
2.1 从controller到ControllerActivator的转换
在第一部分的代码示例中,当初始化了mvc.Application对象后,就是通过如下这行代码对controller中的方法进行转换的:
mvcApplication.Handle(new(testController))
代码就一行,很简单。但就是这个Handle函数将testController中的方法转换成了路由,使用户可以通过对应的路径访问到controller中的方法。接下来我们看看该Handle方法中都做了哪些事情。
点击进入源代码,直到iris/mvc/mvc.go
文件中的handle
方法,如下图示所示。
该方法显示,首先将controller包装成了一个ControllerActivator对象c。其结构体如下:
各字段含义如下:
- injector:该controller中依赖的字段类型。即该controller结构体中有哪些字段。这个我们后面详细解释。
- Value:该字段即在一开始new出来的controller的对象值。比如new(testController)的值。
- Typ:该字段是controller的具体类型。比如上面示例中的testController类型。
- routes:controller中每个函数名对应的具体的路由。
然后该对象c做了3件事情:执行对象c的BeforeActivation方法(如果controller中定义了该方法)、activate方法和AfterActivation方法(如果定义了该方法)。我们先跳过BeforeActivation和AfterActivation方法,重点看下activate方法。
2.2 ControllerActivator的activate方法
c.activate
方法的源码如下图所示,主要是parseMethods方法,根据名字也能猜到是要解析方法了。
这里的逻辑也很简单,先是获取该类型(即controller
的结构体类型,例如示例中的testController
类型)的方法个数,然后依次遍历所有的方法,并对每个方法进行解析,也就是代码中的c.parseMethod(m)
方法。
接下来进入到c.parseMethod(m)
的代码逻辑中,看看该方法做了些什么。
这里看到,通过parseMethod
解析出来了请求的方法名称httpMethod(例如GET、POST等)、请求的路径httpPath。然后再通过c.Handle函数将对应的请求方法和请求路径以及方法名注册成iris的常规路由。
到这里我们先总结一下controller转换的过程:
- 首先,先将controller对象封装成ControllerActivator对象。该对象包含了controller类型的值以及具体的controller数据类型。
- 其次,通过reflect包获取该controller类型的有哪些方法,并依次遍历解析这些方法。
- 然后,根据方法名称解析出标准的HTTP请求的方法以及请求路径,即代码中的parseMethord函数。
- 最后,将该请求方法以及请求路径转换成iris常规的路由,即代码中的c.Handle函数。
因为请求方法和请求路径是根据controller中的方法名称解析出来的,所以开发人员在给controller的方法的命名时,需要遵循以下规则:
- controller中的方法必须采用驼峰式命名,且首字母必须大小。
- parseMethod会大写字母为分割符,将方法名分割成多个单词。例如。GetHome会分割成Get、Home两个单词。方法名HOME,则会被分割成H、O、M、E四个单词。具体实现算法可查看源代码methodLexer.reset函数。
- 方法名的第一个单词必须是http请求的方法名。有效的请求方法为GET、HEAD、POST、PUT、PATCH、DELETE、CONNECT、OPTIONS、TRACE。具体的实现可查看源码请求方法校验逻辑。
- 从方法名的第二个单词开始,使用
"/"
符号将各个单词连接成对应的请求路径。所以方法名实际上是由请求方法+请求路径模式组成的。
例如在testController中有如下GetMyHome方法,那么,对应的请求路径是/my/home
。请求http://localhost:8080/my/home
就会执行testController的GetMyHome方法的逻辑。
- 在方法名中可以通过
By
可以在路由中增加动态参数。
例如在testController中有如下方法GetByHome(username string),则对应的请求路径会被转换成 /{param1:string}/home。请求http://localhost:8080/yufuzi/... 就能访问到testController中的GetByHome函数的逻辑,并且在该方法中username
的变量值就是路径中的yufizi
。如下:
- 若By在方法名的中间位置,一个By只对应一个动态参数;若By在方法名的最后,则方法中的所有输入参数都被解析成路径中的动态参数。
示例一:By在方法名中间位置
GetByHome方法名中有两个参数,那么,访问该路径http://localhost:8080/yufuzi/home
时就会报panic。如下:
func (c *testController) GetByHome(username string, param2 int) {
c.Ctx.Writef(c.Ctx.Method() + " " + username + " Home")
c.Ctx.Writef("param2:" + param2) //这里param2是对应类型的默认值:0
}
示例二:By在方法名最后位置
GetHomeBy方法名中有两个参数,则会转换成对应的路径 /home/{param1:string}/{param2:int}
。如下:
func (c *testController) GetHomeBy(username string, param2 int) {
c.Ctx.Writef(c.Ctx.Method() + " " + username + " Home")
c.Ctx.Writef("param2:" + param2)
}
以上是对controller中方法的解析以及命名时需要遵守的规则。接下来,我们继续看如何将controller的方法转换成具体的路由请求处理器。
2.3 ControllerActivator的Handle方法 -- 将controller中的方法转换成请求处理器
我们知道iris的标准路由处理器是type Handler func(*Context) 类型
的这一个函数。那在mvc中,是如何将controller中的方法转换成这样标准的请求处理器类型的呢?
这个转换主要在ControllerActivator.parseMethod方法的第二部分的功能:ControllerActivator.Handle。进入该函数的源代码,直到ControllerActivator.handleMany函数,如下:
在handleMany函数中主要做了两件事:
- 将函数转换成请求处理器。即c.handlerOf函数做的事情
- 将请求处理注册成标准的路由。即c.app.Router.HandleMany函数做的事情。
这里我们主要看c.handlerOf
是如何将函数转换请求处理器的。至于注册成标准路由,大家可以参考这篇iris路由相关的文章:深入理解iris路由的底层实现原理。
2.4 将controller的函数转换成标准的请求处理器类型
处理该功能的是逻辑在handlerOf
函数中。下面是handlerOf
的源代码,我们看到主要做了两件事:一是attachInjector函数;一个是injector中的MethodHandler函数。
实际将controller的函数转换成请求处理器的是在injector的MethodHandler中进行的,即返回的handler对象。而injector就是在第一步的c.attachInjector函数中构造出来的。
那么,c.injector是什么呢?下面我们看下其对应的结构体,如下图:
可以看出来,injector实际上是对controller具体类型及其对象的描述。还是以testController为例,在Struct.ptrType就是代表testController这种数据类型;Struct.ptrValue代表的是testController对象值;bindings代表的是在testController中有哪些字段值。比如testController中要是定义如下:
type testController struct {
model *userModel
}
那么,testController
对象就需要绑定userModel类型的值。在实例化testController
时,需要将一个具体的userModel
类型的值赋值给model
变量,这样在testController
的方法中就能引用该变量从数据源中读取数据。而这种依赖就是保存在bingdings
中的。
我们主要看第二部分,c.injector.MethodHandler
将函数转换成路由处理器类型。点击进入源码,直到/iris/hero/handler.go
文件的makeHandler
函数,如下所示(注:这里为了突出最终的返回值,省略了一些代码)。
首先,makeHandler
返回值就是一个context.Handler
类型的函数。在函数体内有3部分:获取controller
函数的输入参数值、通过v.Call
函数调用controller
实际的函数体、通过dispatchFuncResult
分发函数的返回值。
到此,就把controller
中的函数转换成了标准的路由处理器类型context.Handler
。并且,只有controller中的方法名的第一个单词是HTTP标准的请求方法(GET、HEAD、POST、PUT、PATCH、DELETE、CONNECT、OPTIONS、TRACE)时,才会将该方法自动注册成对应的路由。
在了解了mvc的实现原理后,下一篇我们讲解mvc的高级使用,敬请期待。
特别推荐:一个专注go项目实战、项目中踩坑经验及避坑指南、各种好玩的go工具的公众号,「Go学堂」,专注实用性,非常值得大家关注。点击下方公众号卡片,直接关注。关注送《100个go常见的错误》pdf文档。