思考并回答以下问题:
从标准库开始搭建,到框架核心的替换,到相关功能的完善,我们已经完整地将Golang的Web框架hade打造出来了。我们一起回顾一下,从最初的net/http开始,我们不断构建自己的框架,根据“一切皆服务”的思想,打造了12个服务以及15个命令行工具。这些服务和工具都是围绕实际的业务开发需求而设计的。
现在用框架开发一个类似知乎这样的问答网站,前端我们使用的是Vue框架,后端使用的是自己的hade框架。
前端准备知识
主要讲vue、webpack、element-UI、vue-router、vuex、axios这六个包。
Vue
Vue的使用方式非常简单,首先在npm的包管理工具package.json中引入它:1
"vue": "^2.5.16",
然后我们在首页模板index.html中标记好一个ID为app的元素:1
2
3<body style="margin: 0px;">
<div id="app"></div>
</body>
接着在页面对应的Vue组件js main.js中引入Vue的包,并且创建一个Vue对象,把它绑定到ID为app的元素中:1
2
3
4
5
6
7
8
9
10import Vue from 'vue' // 引入vue项目
import App from './App.vue' //引入App组件
...
// 创建vue对象
new Vue({
el: '#app', // 绑定id为app的元素
...
render: h => h(App) // 将App组建渲染在这个元素中
})
在Vue中有个很重要的概念,组件,比如上面代码的App.vue就是App组件。什么是组件呢?就是一个包含HTML、JS、CSS的独立展示单元,包含三个部分:1
2
3
4
5
6
7
8
9
10
11<template>
...
</template>
<script>
...
</script>
<style>
...
</style>
template中编写输出的HTML信息,当然其中可能有一些诸如用户名这类的数据信息;这类数据信息是通过script中的脚本来获取的,所以Vue组件中的script编写的是数据,以及获取数据的方法。最后style里面编写的就是HTML展示的样式信息。
webpack
前面我们提到了首页模版index.html、页面对应的Vue组件js main.js、Vue组件App.vue,这些都是开发过程中的文件,最终生成的文件其实只有三种HTML、JS、CSS。也就是说,前面定义的各种开发文件,有的可能是我们最终的文件,有的可能并不是,所以这里要有一个将开发文件编译成为最终的HTML、JS、CSS文件的过程,这个过程就是webpack。
webpack本质也是一个npm包,你同样可以在package.json中引入:1
"webpack": "^2.4.1",
webpack配置项都存在根目录下的webpack.config.js中,你可以在这个配置文件中配置所有需要打包的配置信息。比如入口js:1
2
3
4
5module.exports = (options = {}) => ({
entry: {
index: './src/main.js'
},
...
入口的HTML模版:1
2
3
4
5
6plugins: [
...
new HtmlWebpackPlugin({
template: 'src/index.html'
})
],
又比如编译后输出的目录和文件名:1
2
3
4
5
6
7module.exports = (options = {}) => ({
...
output: {
path: resolve(__dirname, 'dist'), // 输出目录
filename: options.dev ? '[name].js' : '[name].js?[chunkhash]', // 输出文件名
...
},
又或者如何阅读vue文件:1
2
3
4
5module: {
rules: [{
test: /\.vue$/,
use: ['vue-loader']
},
当然webpack还有许许多多的配置项等你挖掘,所有的配置信息和示例都可以在官网上看到。
element-UI
有了Vue,也有了打包Vue成为HTML、JS、CSS的打包工具webpack之后,其实我们已经可以编写Vue来生成页面了。但是对于后端甚至前端工程师来说,写出有功能的界面简单,但是如何写出“漂亮”的页面,又成了摆在面前的一道难题了。
有没有一个UI组件库,我们一旦引入并且使用这个组件库之后,就能很快捷方便地完成一个“漂亮”的页面呢?
element-UI就是这么一套组件库,它是由饿了么公司开发并且免费开源出来的项目,目前也是国内最火的一套UI组件库,提供了非常多的UI组件,我们可以很方便地使用这些组件库构建自己想要的样式。
比如要使用element-UI的一个卡片组件,来生成类似这样的卡片样式:
首先同样要引入element-UI:1
"element-ui": "^2.15.6",
在入口js、main.js中使用Vue.use将ElementUI引入到Vue中:1
2
3
4
5
6
7
8import Vue from 'vue' // 引入vue项目
import ElementUI from 'element-ui' // 引入element-ui组件
import 'element-ui/lib/theme-chalk/index.css' // 引入element-ui的css样式
...
Vue.use(ElementUI) // 可以在vue的任何组件中使用elemnt-ui
...
接着在需要使用卡片样式的页面Vue文件的template中直接使用el-card标签:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<template>
...
<el-card v-for="i in count" class="box-card" shadow="hover">
<div slot="header" class="clearfix">
<span>问题标题{{i}}</span>
</div>
<div class="text item">
这个是问题的具体内容,显示前200个字...
</div>
<div class="bottom clearfix">
<time class="time">2021-10-10 10:10:10 | jianfengye | 10 回答</time>
<el-button type="text" class="button">去看看</el-button>
</div>
</el-card>
...
</template>
这样编译出来的HTML文件就会有对应的样式了。
vue-router
解决了打包和样式问题,在Vue开发中我们遇到各种各样的需求,第一个遇到的就是根据URL展示不同的页面。比如问题列表页和问题详情页,它们的头部都是一样的,但是中间部分不一样,这个应该怎么处理呢?
我们当然可以每个页面写一个同样的头部,但是更好的办法是头部固定,让中间部分不固定,根据路由来选择不同Vue组件进行加载。这种“根据路由加载不同Vue组件”的技术就叫做vue-router。
vue-router首先也是要在pacakge.json引入:1
"vue-router": "^3.5.3",
然后和element-UI一样,通过Vue引入这个router组件:1
2
3
4
5import Vue from 'vue'
import Router from 'vue-router'
...
Vue.use(Router)
这样,在需要根据路由加载不同组件的地方,就可以使用router-view标签来占位一个组件位置:1
2
3
4
5
6
7
8<template>
<el-container>
...
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</template>
而占位的组件具体使用哪个,则是通过创建一个Router对象,并且把Router对象设置进入Vue实例的router字段中实现的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import ViewLogin from '../views/login/index'
export const constantRoutes = [
{
path: '/login',
component: ViewLogin,
hidden: true
},
...
]
const createRouter = () => new Router({
routes: constantRoutes
})
const router = createRouter()
new Vue({
el: '#app', // 绑定id为app的元素
router: router, // router字段
...
})
Vue的组件渲染的时候,遇到占位的router-view标签,就会去constantRoutes中根据URL进行寻找。
vuex
Vue的组件和组件之间经常需要传递各种信息,比如用户信息。那么vuex就提供了统一管理这些信息的模块,在这个模块中,我们可以把要存储的内容都放在里面,当页面跳转的时候,只需要操作这个公共模块的内容即可。
它的使用方式首先也是一样,需要先在package.json中引入vuex:1
"vuex": "^3.6.2"
其次创建一个vue.Store来创建一个store实例保存相应内容,这里的modules是一个key为string、value为一个vuex对象的映射:1
2
3
4const store = new Vuex.Store({
modules,
getters
})
在入口的main.js中,我们将vuex创建的store实例,作为Vue的store字段,传递进入:1
2
3
4
5
6
7// 创建vue对象
new Vue({
el: '#app', // 绑定id为app的元素
...
store: store, // store设置
...
})
之后在使用的时候,对应的所有存储状态就可以使用this.$store访问到:1
2
3
4
5
6
7
8const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count // 访问存储状态
}
}
}
axios
前端一定是需要和后端进行交互的,而axios库就是负责前端给后端发送请求的。axios的使用非常简单。首先同样需要在package.json中引入:1
"axios": "^0.24.0",
其实这一步axios的引入就已经完成了,它的使用并不依赖于Vue实例的任何字段。所以它其实是完全可以脱离Vue使用的。
之后在具体使用它组件的地方,直接import调用它的接口了。1
2
3
4
5
6
7
8
9
10
11import axios from 'axios'
// Send a POST request
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});
但是在使用它的时候,我们一般会习惯做一个封装。
一方面,axios的名字还是比较绕口,我们将它的名字封装成为request,会更适合阅读。另一个更重要的,我们不希望直接调用axios,在这个封装中,对所有的请求参数和返回值都可以做一个注入操作。比如统一设置请求超时时间,又比如在返回值中,一旦返回状态不是200,也就是返回了异常,我们就在页面上打印出一个错误信息。
所以在src/utils/request.js中进行一下封装:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27import axios from 'axios'
import { Message } from 'element-ui'
// 创建一个axios
const service = axios.create({
withCredentials: true, // 请求携带cookie
timeout: 10000 // 统一设置超时
})
// response中统一做处理
service.interceptors.response.use(
response => {
// 判断http status是否为200
if (response.status !== 200) {
Message({
message: "请求错误",
type: 'error',
duration: 5 * 1000
})
}
},
error => {
...
}
)
export default service
而在具体使用的时候,直接import这个request.js就可以了:1
2
3
4
5
6
7
8
9
10
11
12import request from '../../utils/request'
submitForm: function(e) {
...
request({
url: '/user/register',
method: 'post',
params: this.form
}).then(function (response) {
...
})
}
好,vue、webpack、element-ui、vue-router、vuex、axios这些组件基本上是我们这次开发页面会用到的了。不过前端的组件非常多且杂,后续开发过程中如果还有遇到,我们再详细说明。
框架搭建
首先我们要做的事情是使用hade框架搭建一个新项目。先使用命令:1
go install github.com/gohade/hade@latest
安装go命令到我们的$GOPATH/bin目录下。
然后使用hade new
命令创建一个BBS的项目:
前端框架
我们开始搭建前端,前端需要引入Vue、webpack、element-UI。不过其实我们并不需要一个个引入,element-UI有一个现成的集成了这三者的脚手架项目element-starter,直接使用这个项目是最快的方式了。
怎么直接使用这个项目呢?我们做两个步骤:
1,将BBS项目中原先的前端文件直接删除
2,将element-starter全部复制到BBS项目中
BBS项目中原先的前端文件包含build目录、docs目录、src目录、package.json、package-lock.json等,下图红色箭头所示:
删除之后,将element-starter项目直接复制过来,就完成了前端框架的迁移。
我们再来规划一下前端源码src目录:
index.html是我们前面说的页面模版,而main.js是前面说的前端js入口,这个入口加载的App.vue就是我们的入口组件,vendor.js存放一些需要import的第三方组件。
看这六个目录:
- assets里面存放的是一些png、jpg等图片信息;
- component存放一些公共组件,后续多个页面有公共组件的时候,我们会将公共组件抽象放在这个目录中;
- router存放前端的vue-router对应的路由信息;
- store存放vuex中存放的存储状态;
- utils存放一些通用的方法和函数;
- views存放的是页面组件,这里面的每个组件最终都能渲染成为一个页面。
当然这套目录结构并不是固定的,只是在前端Vue的开源届,大家都认为这种目录算是一种最佳实践。很多项目都遵照这种目录结构划分,比如这次我们使用的element-UI。
具体的路由设计,有必要说明一下。前面说了路由vue-router是根据URL来占位展示不同的组件的。但是,我们这里是需要设计一个双层vue-router的。看一下这三个页面,登录页面、问题列表页、问题创建页:
你可以看到,登录页面和下面两个页面整体布局是不一样的,而下面两个页面的头部header是一样的,body部分不一样。我们需要设计一套布局结构来同时支持这三种布局,所以就用到了二级的vue-router。
具体代码你可以参考GitHub上BBS的geekbang/30分支。这里大致说一下逻辑,我们的路由设计为两级路由和一级路由同时存在。在src/router/index.js中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23export const constantRoutes = [
{
path: '/login',
component: ViewLogin,
hidden: true
},
{
path: '/',
redirect: '/',
component: ViewContainer,
children: [
{
path: '',
component: ViewList
},
{
path: 'create',
component: ViewCreate
},
...
]
},
]
在入口vue组件App.vue中,我们使用第一层vue-router:1
2
3
4
5<template>
<div id="app">
<router-view></router-view>
</div>
</template>
而对于问题列表页和问题创建页,它们会先去加载Container组件;在Container组件src/views/layout/container.vue中我们使用第二层vue-router:1
2
3
4
5
6
7
8
9
10<template>
<el-container>
<el-header>
<bbs-header></bbs-header>
</el-header>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</template>
这样前端两层路由就能满足不同的页面布局需求了。
后端框架
前端框架搭建差不多了,我们开始后端的框架搭建。
目录结构已经在hade框架中定义好了,所有的逻辑代码都存放在app目录下。
可以看到app/module/和/app/provider/下都有两个文件夹user和qa,估计你有点疑惑为什么要这么分层。
首先基于一切皆服务的逻辑,我们的服务层可以拆分为两个服务,用户服务和问答服务。按照这里需求的划分,分为两个服务的粒度是正好的,每个服务的基本服务接口都能控制在10个左右。当然在一些更大规模的项目中,是有可能分为更细粒度的,比如分为三个用户服务、问题服务、回答服务。这里的服务划分其实就是业务架构师的工作了,基本准则是让每个服务复杂度可控。
在这里,我们分为两个用户服务和问答服务,每个服务的复杂度基本上都在可控范围内。
下面就创建服务。回忆创建服务的三步骤,服务协议,服务提供者,服务实现。这里我们使用之前为hade框架提供过的快速创建服务工具:./hade provider new
快速创建两个服务user和qa。
我们会将所有的user或者qa的模型都封装在这两个服务(service provider)中。
那么app/modules下的user和qa的模块负责什么呢?这两个模块目前就简化为负责定义接口、定义返回对象、定义服务对象和返回对象的转换,我们分别设计api.go、dto.go、mapper.go三个文件负责做这个事情。
这种开发模型就是hade建议的标准模型。还记得第12章的课后问题让你思考如何设计业务模块的分层模型么?这个就是我的答案。
service provider层将领域对象封装成一个个的服务;再上面一层是mapper层,将服务结构转化为业务输出的结构;最后经过DTO的数据结构过滤和组织,通过API层输出。这种业务分层和结构设计的基本思想,是面向领域驱动的设计思想DDD,也是《企业应用架构模式》一书中推荐的。这种设计模型如图所示:
在官方说明文档中也具体说明了。
这里还有一个小优化点:一个模块会定义多个接口,你可以把所有接口都放在一个api.go文件中,但是这样一个文件就特别冗长了。所以我建议将每个接口定义一个文件,以api_xxx.go命名。
最终对于一个业务模块,比如user,我们会定义两个目录,一个是app/module/user,负责接口设计和返回对象设计,另外一个是app/provider/user,负责实质的业务服务抽象。
到这里,我们的前后端框架搭建完成了。当然前端部分,可能还有很多细节点,不过今天我们已经把搭建框架的关键点和难点都详细说明了,具体的实现你可以参照GitHub上的BBS项目的geekbang/30分支比对查看。
这里也列一下本节课的框架搭建结构:
小结
这一节课我们进入了实战部分,真正使用hade框架来开发一个类知乎的问答网站。从需求开始设计分析了两个核心的模块,用户模块和问答模块,并且搭建了前端和后端的框架。这节课的内容比较多且杂,特别是前端的准备知识,一个纯后端工程师需要花费一些时间理解,如果你希望更多了解这些前端知识,网上相关资料也很多。
不过回看今天的后端开发,我们使用了./hade new
和./hade provider new
两个命令,这些都是之前实现的hade命令行工具,你是不是切实感受到它们提升了不少开发效率?
思考题
分析需求的时候,我们画了用户注册和登录的时序图来理解注册/登录逻辑,是不是一下子就简明扼要地说清楚了。其实在实际工作中,使用时序图来解释复杂交互或者服务间调用的逻辑是我们非常必要的技能了。
这节课的思考题希望你去了解一下时序图的元素和绘制方法,然后对照着文中注册和登录的时序图找出以下几个对应元素,检验一下自己的学习效果:
- 角色
- 对象
- 生命线
- 消息
- 控制焦点
- 控制逻辑