从一个项目看 beego 的 MVC
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()
,就是在调用catalog
这Model
中的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 什么的。
喜欢这篇文章?为什么不打赏一下呢?