从一个项目看 beego 的 MVC

从一个项目看 beego 的 MVC

技术 3525 字 / 7 分钟

bilibili 干杯!🍻

最近 b 站源码泄露的事情闹得沸沸扬扬的,每天都有不同的瓜。昨天 GitHub 放出了 b 站写的 DMCA takedown 邮件。今天大家就在吐槽 b 站的小学级别英语了。 嘛,撇开这些事情不谈,b 站这次的源码泄露,确实让很多人开始注意到了 Golang 这门语言。一时间不少人嚷嚷着说要开始学习 Golang,就从 b 站源码入手什么的。 玩笑归玩笑,但 Golang 到如今所达成的种种成就,让我不禁感觉她的未来十分光明。不说取代 Java,至少也得有现如今 Python 这种热度。还记得上学期刚开始接触 Docker 的时候,学长在群里说:“Docker 真实跨世纪的发明!” 这话说得也不算夸张。 Docker 这东西,真的是无处不在了。以前个人在自己的 VPS 上建站,都是直接 LNMP 配在宿主机上的。但现在呢?Docker 几乎成为标配了。勤奋一点地自己单独拉镜像或写 Dockerfile 自己配环境,懒一点的直接一个整合包镜像拉下来docker run搞定。 然后,Docker was made by Golang。嗯,Golang 牛逼吧。

再加上现在各大厂商都开始用 Golang 重构自己的后端了。比如 b 站和知乎。我以前是说过 b 站作为一个从小站自己摸爬滚打发家的网站,其所使用的技术以及解决方案,相对来说都是可以适应于中小团队甚至个人的。十分的接地气。因此 b 站用 Golang 重构,或者说是 b 站后端源码泄露这件事,我们在吃瓜翻阅源码的同时,更重要的是着眼于 Golang 这一们语言在未来的重要性。

MVC -> Golang?

嘛,吹 Golang 吹的够多了,来说点关于我自己的事情吧。 最近做的好几个东西——学校的通用问卷调查系统,协会的 Vchat 聊天室等,都是采用前后端分离;前端都用 Vue 写的单页应用。久而久之有些单调了,我有点怀念用 PHP 框架写 MVC 模式的网站了。恰好最近在学习 Golang,想着做个项目练练手,同时又能学学 Golang 的 MVC 岂不美哉?

在仔细斟酌随意挑选后,我选择了 beego 这个框架。记得当时 HGame week4 的 HappyGo 的题,用得就是这个框架。

beego 框架的文档还算不错,中文翻译,并且给出了很多常用功能的 demo 方面读者快速上手。官方也给出了三个用 beego 框架编写的 demo。这可惜这三个 demo 都没有和数据库交互。官方文档中关于Model层以及ORM的操作也是少得可怜。 我把原来写 CodeIgniter 的那套 MVC 思想硬生生套在 Golang 的语法上…… 结果可想而知,完全把自己绕晕了。 今天偶然在 GitHub 上看到一个国人用 beego 框架写的 blog,想着可以阅读一下他的源码,来熟悉熟悉 Golang 的 MVC。

UlricQin / beego-blog https://github.com/UlricQin/beego-blog

先看看路由

跟平时打 CTF 一样,我们先看看路由嘻嘻。 这里我节选了一部分,已经可以看出些东西了。

	beego.Router("/", &controllers.MainController{})
	beego.Router("/article/:ident", &controllers.MainController{}, "get:Read")
	beego.Router("/catalog/:ident", &controllers.MainController{}, "get:ListByCatalog")

	beego.Router("/login", &controllers.LoginController{}, "get:Login;post:DoLogin")
	beego.Router("/logout", &controllers.LoginController{}, "get:Logout")

	beego.Router("/me", &controllers.MeController{}, "get:Default")
	beego.Router("/me/catalog/add", &controllers.CatalogController{}, "get:Add;post:DoAdd")
	beego.Router("/me/catalog/edit", &controllers.CatalogController{}, "get:Edit;post:DoEdit")
	beego.Router("/me/catalog/del", &controllers.CatalogController{}, "get:Del")

beego.Router()函数的第一个参数就是路由的 URL,URL 中可以用:加入诸如上面:ident的统配符;第二个参数是该路由所调用的控制器,第三个参数是指定控制器内的方法。注意这里的第三个参数是可以指定不同的 HTTP 方式可以分别调用不同的方法。

最主要的控制器

那么再看看控制器。在之前用命令行bee指令创建项目时,beego 的控制器就会被放在controllers目录下。 这里我选了main_controller.go中的一个方法:

func (this *MainController) Get() {
	this.Data["Catalogs"] = catalog.All()
	this.Data["PageTitle"] = "首页"
	this.Layout = "layout/default.html"
	this.TplName = "index.html"
}

其中this.Data["Catalogs"]相当于在View中要使用的数据,全部放在this.Data数组里。方法第一行的catalog.All(),就是在调用catalogModel中的All()方法。而this.TplName则是调用指定的View。这里有一个this.Layout,它会在View中的{{.LayoutContent }}处显示。嘛,相当于 Vue 中的<router-view></router-view>的感觉,即视图中会随着页面不同而变化的内容。 值得一提的是,这里给MainController的指针调用时名称是this,这样写起来this.xx颇有 PHP 或 Java 的感觉。是个不错的习惯。

控制器的Prepare()方法

我在写这个小玩意儿的时候遇到了这样一个情况:所有的View中都要有页面header.tpl的模板,并且要往这个模板中动态注入数据。 第一反应想到的是我们可以在控制器的每个方法里都写上this->Data['TypeList'] = type_list.GetList()这样的语句来注入一样数据。但是每个方法都有这一句,之后改起来会超级麻烦,太不优雅了。 对比以前写 PHP 时,都会将这些公有的部分,全部写在控制器类的构造函数里。框架内部实例化控制器时,构造函数里然而 Golang 并不是个面向对象的语言。 之前问过部长,如何在 Golang 里实现 OOP。关于构造函数这里,都是在结构体里写一个New()方法,如何实例化就是手动去xxx.New()调这个方法。真的无语了。而在 beego 框架的控制器里,即 beego.Controller里,是有一个Prepare()方法的。beego 会在执行路由所指向的相关方法前,先执行Prepare()方法。 因此我们只要重写这个Prepare()方法即可。

func (this *TypeController) Prepare() {
	this.Data["TypeList"] = note_type.List()
	this.Data["NoteList"] = note.GetNoteList()
}

有比较大的区别的 Model

beego 中的 Model 和 CI 中还是有很大的不同的。在 Golang 中,往数据库中插入数据、获取传入的 JSON 的数据等,都是需要先声明一个结构体的。因为 Model 主要是负责数据库交互方面的,因此我们在models文件下建立一个model.go文件,在这里面声明我们每张数据库表的结构体。这一点和 PHP 是有很大的不同的。然后其它的功能对应的Model再写到其它的.go文件中。 因为所有的Model都是在models文件夹下的,所以它们都属于models这个包,之前声明的结构体互相之间都可以调用。不需要再引入包什么的。 我这边是和 CI 一样的风格,将传入数据的验证放在 Controller 里面,对数据库的操作写在Model里。

View——模板语言

使用 Golang 进行 Web 应用的开发,从某种意义上来说和 Python 很像——她们最后编写出来的程序,直接就是一个 HTTP 服务器,不像 PHP 那种,还得配个 Nginx 或 Apache。beego 的 View 也是依靠类似模板语言一样的写法。上面聊 Controller 的时候,这个项目引入的是.html文件,而 beego 官方的文档引入的是.tpl文件。其实这两者都是可以的。只要是放在views文件夹里,在编译打包时,不管该文件有没有调用,都会全部包进去。 而其中关于模板语言的语法,也是很简洁好用:

{{.PageTitle}}

像这样就将我们上面那个this.Data["PageTitle"]给“插入”进来了。 而关于条件判断显示,以及循环渲染也是很容易,这里拿我正在写的练手项目举例子:

 {{if .isSuccess}}
	<div class="uk-alert-success" uk-alert>
		<a class="uk-alert-close" uk-close></a>
		<p>{{.Message}}</p>
	</div>
{{end}}

最后加上{{end}}即可。 如果是要进行比较的话,必须要使用 beego 中这些奇怪的写法:

eq: arg1 == arg2
ne: arg1 != arg2
lt: arg1 < arg2
le: arg1 <= arg2
gt: arg1 > arg2
ge: arg1 >= arg2

上面的代码改写后是:

{{if eq .Status "success"}}
	<div class="uk-alert-success" uk-alert>
		<a class="uk-alert-close" uk-close></a>
		<p>{{.Message}}</p>
	</div>
{{end}}

不过使用这种方式进行比较时要特别注意。比如上面的.Status,一定要保证改变量的存在;就算不使用也要this.Data["Status"] = ""传一个。不然会因为在模板中找不到这个变量而报错。

 {{range $index, $elem := .List}}
	<tr>
		<td>{{$elem.Name}}</td>
		<td><a href="/DeleteType?ID={{$elem.Id}}" class="uk-button uk-button-danger uk-button-small" type="button">删除</a></td>
	</tr>
{{end}}

循环和 Vue 挺像的啦,指定索引$index,元素$elem即可。最后同样是使用{{end}}结尾。

ORM

在 CodeIgniter 中,我们有数据库查询构造器,能让我们不需要写 SQL 语句,并且能防御 SQL 注入的危险。而在beego 里,我们使用她的 ORM 来实现。还记得之前协会的笔试题里就有问这个,当时我还不知道这是什么东西。 首先在main.go中的init()函数中声明数据库的连接:

func init(){
	orm.RegisterDriver("mysql", orm.DRMySQL)
	orm.RegisterDataBase("default", "mysql", "root:root@tcp(localhost:3306)/gote?charset=utf8")
}

之后指定默认的表,并开启 ORM 调试模式来辅助我们:

orm.Debug = true
o := orm.NewOrm()
o.Using("default")		// 注意这里必须得是 default

添加数据

_, err := orm.NewOrm().Insert(n)

if err != nil{
	fmt.Println(err)
}

很简单吧,n就是我们数据结构体的指针。短短一行搞定!

选择数据

var noteTypes []NoteTypes
_, err := orm.NewOrm().QueryTable("NoteTypes").All(&eTypes, "Id", "Name")

if err != nil{
	fmt.Println("获取 NoteType 列表失败")
}

第一个QueryTable("NoteTypes")是指定我们要查询的表,后面的.All(&eTypes, "Id", "Name")是获取所有数据,其中noteTypes是存数据的数组,后面跟着的是我们要的字段。

删除数据

_, err := orm.NewOrm().Delete(&NoteTypes{Id: noteID})

if err != nil{
	fmt.Println("删除失败")
}

与插入数据大致一样,传入一个数据表所对应的结构体,然后指定要进行筛选的字段即可。

总结

嘛,大概就是这些啦。beego 的 MVC 和 PHP 的 CodeIgniter 还是有很大的区别的。但是 beego 的 MVC 写起来也别有一般风味,还挺爽的其实。关于上面提到的这个练手的小玩意儿,其实也是为了解决平时的一大硬伤吧。(比如打 CTF 时要翻博客找自己以前的 payload 什么的。