You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

629 lines
26 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 30设计先于实战需求设计和框架搭建
你好,我是轩脉刃。
从标准库开始搭建到框架核心的替换到相关功能的完善我们已经完整地将Golang的Web框架hade打造出来了。我们一起回顾一下从最初的net/http开始我们不断构建自己的框架根据“一切皆服务”的思想打造了12个服务以及15个命令行工具。这些服务和工具都是围绕实际的业务开发需求而设计的。
今天我们就来用自己搭好的框架开发一个具体的应用,不过开发什么比较好呢。之前准备的是写一个类论坛的网站,但是课程更新到这里,我突然有了一个更好的新想法。
在框架的使用过程中你一定会有各种各样的疑问使用上的或者代码理解上的而这些疑问一定会很希望有一个类似知乎那样的地方可以进行答疑所以这次我们就为hade框架打造一个类似知乎这样的问答网站。
这个问答网站的源代码,我们另外开启一个开源项目 [https://github.com/gohade/bbs](https://github.com/gohade/bbs) 来存放并且从零开始一步步演示如何使用hade框架。后续如果你也想试试自己对hade框架的掌握程度也欢迎一起参与来共建这个项目。
由于一个网站是由前端和后端协同完成的前端我们使用的是Vue框架后端使用的是自己的hade框架。之前也提过前端Vue的内容是一门很大的课程我们会介绍一下重点的部分以不影响对整体网站开发逻辑的理解为主。不过所有的代码都可以在刚才的BBS开源项目中找到方便你比对和学习。
下面开始我们的实战之旅吧。
## 需求设计
相信你肯定逛过各种各样的论坛类网站,这样的网站可以设计得很复杂,可以有用户模块、问答模块、会员模块、积分模块、排行榜模块、广场模块等等。但是对于一个以提出问题和回答问题为主的网站,它最核心的其实就只有两个模块:用户模块和问答模块。
### 用户模块
一个网站最基本的是用户模块,毕竟一个使用者在网站中的各种行为,比如提出问题、回答问题等都需要有一个用户身份。作为一个最基本的模块,用户模块负责的功能有注册和登录两个部分:
* 用户注册
用户注册功能负责为网站创建一个新的用户。但是并不是随意用一个用户名密码就可以创建用户,为了安全起见,注册的时候,用户还需要输入注册的邮箱。
所以,整个用户注册流程分为两个过程:
1. **预注册过程**。用户在浏览器输入用户名、密码、邮箱,向网站发送注册请求;而网站收到注册请求之后,往用户的邮箱中发送一个邮件,这个邮件中包含一个验证链接。
2. **注册验证过程**。用户进入邮箱,打开邮件中的验证链接;这个验证链接会往网站中发送一个验证请求;网站在后台验证这个请求的合理性,然后在浏览器中引导用户进入下一步登录的过程。
用户注册时序图如下:
![](https://static001.geekbang.org/resource/image/4b/12/4b26edc5fa6177ab07113d542cfdda12.png?wh=1362x1216)
* 用户登录
在用户完成注册之后,每次使用网站需要进行一次用户登录的过程。
登录的过程大致是用户在登录页面输入用户名密码向后端发送登录请求。后端接收到带着一个token的登录请求。
这个token在前端会种到cookie中后续所有发送给后端的请求都会带上这个token信息**而后端的所有请求都会验证一下这个cookie中的token**只有验证token确实是登录颁发的才会通过请求执行所有后续的请求如果验证失败则要求用户重新登录。
整个登录时序图如下:
![](https://static001.geekbang.org/resource/image/41/c9/414d876c07f4ef55edeb1047c09e43c9.png?wh=1140x1166)
完成了用户注册和用户登录,基本上用户模块的功能也就完成了。我们看问答模块怎么设计。
### 问答模块
问答模块负责用户提问和回答,它包含的页面比较多:
* 问题列表页,负责展示问题列表,按照最后更新时间来展示问题的列表页信息。
* 问题编写页,负责创建问题,包含问题的标题和问题的内容。
* 问题详情页,负责展示问题和问题的所有回答,回答按照最后创建时间倒序排列。
我们大致描述一下用户在问答模块的具体表现,更多细节在具体实现的时候再讨论。
用户A通过用户模块的登录页面登录进入网站先进入的是问题列表页在问题列表页他可以做两件事情。一是阅读问题列表对于有兴趣的问题直接点击进入问题详情页进行回答。另外他也可以点击右上角的“我要提问”来进入问题编写页提交问题并且点击提交他提出的问题在问题列表页就可以展示出来了。
问题列表页最终展示图如下:
![图片](https://static001.geekbang.org/resource/image/1a/59/1a83f05477538yy52yy171aa6b657e59.png?wh=1062x1240)
## 前端准备知识
现在我们搞清楚了要完成的两大模块大致有哪些需求。不过不管是什么模块都离开不了前端页面的编写如何编写我们使用的是Vue框架所以一些后续会用到的基础知识再统一梳理一遍主要讲 vue、webpack、element-UI、vue-router、vuex、axios 这六个包。
### vue
[Vue框架](https://vuejs.org)在实现前后端一体化的时候简单介绍过,是尤雨溪的大作,在[GitHub上](https://github.com/vuejs/vue)有189k之多的star。
Vue框架目前是国内用得最多的Web前端框架了没有之一。据我所见不论是大厂还是小厂Vue已经成为了必备的前端开发框架。这个使用率一部分归功于尤大在国内的不断推广更大一部分要归功于Vue自身的优雅设计和便捷使用。
Vue的使用方式非常简单首先在npm的包管理工具package.json中引入它
```json
"vue": "^2.5.16",
```
然后我们在首页模板index.html中标记好一个ID为app的元素
```xml
<body style="margin: 0px;">
<div id="app"></div>
</body>
```
接着在页面对应的Vue组件js main.js 中引入Vue的包并且创建一个Vue对象把它绑定到ID为app的元素中
```javascript
import 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的独立展示单元包含三个部分
```plain
<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](https://webpack.docschina.org)。
webpack本质也是一个npm包你同样可以在package.json 中引入:
```json
"webpack": "^2.4.1",
```
webpack配置项都存在根目录下的webpack.config.js中你可以在这个配置文件中配置所有需要打包的配置信息。比如入口js
```javascript
module.exports = (options = {}) => ({
entry: {
index: './src/main.js'
},
...
```
入口的HTML模版
```javascript
plugins: [
...
new HtmlWebpackPlugin({
template: 'src/index.html'
})
],
```
又比如编译后输出的目录和文件名:
```javascript
module.exports = (options = {}) => ({
...
output: {
path: resolve(__dirname, 'dist'), // 输出目录
filename: options.dev ? '[name].js' : '[name].js?[chunkhash]', // 输出文件名
...
},
```
又或者如何阅读vue文件
```javascript
module: {
rules: [{
test: /\.vue$/,
use: ['vue-loader']
},
```
当然webpack还有许许多多的配置项等你挖掘所有的配置信息和示例都可以在官网上看到。
### element-UI
有了Vue也有了打包Vue成为HTML、JS、CSS的打包工具webpack之后其实我们已经可以编写Vue来生成页面了。但是对于后端甚至前端工程师来说写出有功能的界面简单但是如何写出“漂亮”的页面又成了摆在面前的一道难题了。
有没有一个UI组件库我们一旦引入并且使用这个组件库之后就能很快捷方便地完成一个“漂亮”的页面呢
[element-UI](https://element.eleme.cn/#/zh-CN)就是这么一套组件库它是由饿了么公司开发并且免费开源出来的项目目前也是国内最火的一套UI组件库提供了非常多的UI组件我们可以很方便地使用这些组件库构建自己想要的样式。
比如要使用element-UI的一个卡片组件来生成类似这样的卡片样式
![图片](https://static001.geekbang.org/resource/image/8f/71/8f617yy7c6ec715d08050930d1b88771.png?wh=809x173)
首先同样要引入element-UI
```json
"element-ui": "^2.15.6",
```
在入口js、main.js中使用Vue.use将ElementUI引入到Vue中
```javascript
import 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 标签:
```plain
<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引入
```json
"vue-router": "^3.5.3",
```
然后和element-UI一样通过Vue引入这个router组件
```javascript
import Vue from 'vue'
import Router from 'vue-router'
...
Vue.use(Router)
```
这样,在需要根据路由加载不同组件的地方,就可以使用 router-view 标签来占位一个组件位置:
```plain
<template>
<el-container>
...
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</template>
```
而占位的组件具体使用哪个则是通过创建一个Router对象并且把Router对象设置进入Vue实例的router字段中实现的
```plain
import 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](https://vuex.vuejs.org/zh) 就提供了统一管理这些信息的模块,在这个模块中,我们可以把要存储的内容都放在里面,当页面跳转的时候,只需要操作这个公共模块的内容即可。
它的使用方式首先也是一样需要先在package.json 中引入vuex
```json
"vuex": "^3.6.2"
```
其次创建一个vue.Store来创建一个store实例保存相应内容这里的modules是一个key为string、value 为一个vuex对象的映射
```javascript
const store = new Vuex.Store({
modules,
getters
})
```
在入口的main.js中我们将vuex创建的store实例作为Vue的store字段传递进入
```javascript
// 创建vue对象
new Vue({
el: '#app', // 绑定id为app的元素
...
store: store, // store设置
...
})
```
之后在使用的时候对应的所有存储状态就可以使用this.$store访问到
```javascript
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count // 访问存储状态
}
}
}
```
### axios
前端一定是需要和后端进行交互的,而[axios](https://axios-http.com)库就是负责前端给后端发送请求的。axios的使用非常简单。首先同样需要在package.json中引入
```json
"axios": "^0.24.0",
```
其实这一步axios的引入就已经完成了它的使用并不依赖于Vue实例的任何字段。所以它其实是完全可以脱离Vue使用的。
之后在具体使用它组件的地方直接import调用它的接口了。
```javascript
import 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中进行一下封装
```javascript
import 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就可以了
```javascript
import 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框架搭建一个新项目。先使用命令
```go
go install github.com/gohade/hade@latest
```
安装go命令到我们的$GOPATH/bin 目录下。
然后使用 `hade new` 命令创建一个BBS的项目
![图片](https://static001.geekbang.org/resource/image/04/4c/04ffa0d16fff66fea75a5fff19cd6c4c.png?wh=1920x1215)
### 前端框架
我们开始搭建前端前端需要引入Vue、webpack、element-UI。不过其实我们并不需要一个个引入element-UI有一个现成的集成了这三者的脚手架项目 [element-starter](https://github.com/ElementUI/element-starter),直接使用这个项目是最快的方式了。
怎么直接使用这个项目呢?我们做两个步骤:
1. 将BBS项目中原先的前端文件直接删除
2. 将element-starter 全部复制到BBS项目中
BBS项目中原先的前端文件包含 build 目录、docs 目录、src 目录、package.json、package-lock.json 等,下图红色箭头所示:
![图片](https://static001.geekbang.org/resource/image/08/f9/08ac13e99eea0f3d7729e52433c3e9f9.png?wh=842x1250)
删除之后,将 [element-starter](https://github.com/ElementUI/element-starter) 项目直接复制过来,就完成了前端框架的迁移。
![图片](https://static001.geekbang.org/resource/image/3d/ee/3da3a6dd497dcffec8a210bf2184c6ee.png?wh=806x1068)
我们再来规划一下前端源码 src 目录:
![图片](https://static001.geekbang.org/resource/image/0f/ac/0f3908fe801086d70570ca757af022ac.png?wh=498x622)
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**的。看一下这三个页面,登录页面、问题列表页、问题创建页:
![图片](https://static001.geekbang.org/resource/image/83/df/831fec87f7a40a17ab8a4eda42efbddf.png?wh=1920x570)
![图片](https://static001.geekbang.org/resource/image/56/06/56a596353fdc688c461563db5b3eff06.png?wh=1920x672)
![图片](https://static001.geekbang.org/resource/image/08/e6/08c0f4fd5cde8d47ac4d6b13e64d93e6.png?wh=1920x875)
你可以看到登录页面和下面两个页面整体布局是不一样的而下面两个页面的头部header是一样的body部分不一样。我们需要设计一套布局结构来同时支持这三种布局所以就用到了二级的vue-router。
具体代码你可以参考GitHub上BBS的[geekbang/30](https://github.com/gohade/bbs/tree/geekbang/30)分支。这里大致说一下逻辑我们的路由设计为两级路由和一级路由同时存在。在src/router/index.js中
```javascript
export 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
```plain
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
```
而对于问题列表页和问题创建页它们会先去加载Container组件在Container组件src/views/layout/container.vue中我们使用第二层vue-router
```plain
<template>
<el-container>
<el-header>
<bbs-header></bbs-header>
</el-header>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</template>
```
这样前端两层路由就能满足不同的页面布局需求了。
### 后端框架
前端框架搭建差不多了,我们开始后端的框架搭建。
目录结构已经在hade框架中定义好了所有的逻辑代码都存放在 app 目录下。
![图片](https://static001.geekbang.org/resource/image/72/06/721227c8f97c0888e9c52a61878d3906.png?wh=880x846)
可以看到app/module/和/app/provider/下都有两个文件夹user和qa估计你有点疑惑为什么要这么分层。
首先基于一切皆服务的逻辑我们的服务层可以拆分为两个服务用户服务和问答服务。按照这里需求的划分分为两个服务的粒度是正好的每个服务的基本服务接口都能控制在10个左右。当然在一些更大规模的项目中是有可能分为更细粒度的比如分为三个用户服务、问题服务、回答服务。这里的服务划分其实就是业务架构师的工作了基本准则是让每个服务复杂度可控。
在这里,我们分为两个用户服务和问答服务,每个服务的复杂度基本上都在可控范围内。
下面就创建服务。回忆创建服务的三步骤服务协议服务提供者服务实现。这里我们使用之前为hade框架提供过的快速创建服务工具 `./hade provider new` 快速创建两个服务user 和 qa。
![图片](https://static001.geekbang.org/resource/image/4f/c2/4feba3be4e983862cf43c3bb25b3dcc2.png?wh=952x152)
![图片](https://static001.geekbang.org/resource/image/49/51/493bac10cf14189c9eeddfb99729f951.png?wh=927x152)
我们会将所有的user或者qa的模型都封装在这两个服务service provider中。
那么app/modules下的user 和 qa的模块负责什么呢这两个模块目前就简化为负责定义接口、定义返回对象、定义服务对象和返回对象的转换我们分别设计api.go、dto.go、mapper.go 三个文件负责做这个事情。
这种开发模型就是hade建议的标准模型。还记得[第12章](https://time.geekbang.org/column/article/425820)的课后问题让你思考如何设计业务模块的分层模型么?这个就是我的答案。
service provider层将领域对象封装成一个个的服务再上面一层是mapper层将服务结构转化为业务输出的结构最后经过DTO的数据结构过滤和组织通过API层输出。这种业务分层和结构设计的基本思想是面向领域驱动的设计思想DDD也是《企业应用架构模式》一书中推荐的。这种设计模型如图所示
![](https://static001.geekbang.org/resource/image/86/e0/86c3dfcd20a08c5a90558665498d4fe0.jpg?wh=1920x1080)
在[官方说明文档](http://hade.funaio.cn/v1.0/guide/structure.html)中也具体说明了。
这里还有一个小优化点一个模块会定义多个接口你可以把所有接口都放在一个api.go文件中但是这样一个文件就特别冗长了。所以我建议将每个接口定义一个文件以api\_xxx.go 命名。
最终对于一个业务模块比如user我们会定义两个目录一个是app/module/user负责接口设计和返回对象设计另外一个是app/provider/user负责实质的业务服务抽象。
![图片](https://static001.geekbang.org/resource/image/bf/a0/bfab7eec2cf7e991690cb1384753d0a0.png?wh=285x671)
到这里我们的前后端框架搭建完成了。当然前端部分可能还有很多细节点不过今天我们已经把搭建框架的关键点和难点都详细说明了具体的实现你可以参照GitHub上的BBS项目的[geekbang/30](https://github.com/gohade/bbs/tree/geekbang/30)分支比对查看。
这里也列一下本节课的框架搭建结构:
![图片](https://static001.geekbang.org/resource/image/9c/b5/9cb0d9ebdc9c7f7a85927ddff3a23cb5.png?wh=548x1143)
## 小结
这一节课我们进入了实战部分真正使用hade框架来开发一个类知乎的问答网站。从需求开始设计分析了两个核心的模块用户模块和问答模块并且搭建了前端和后端的框架。这节课的内容比较多且杂特别是前端的准备知识一个纯后端工程师需要花费一些时间理解如果你希望更多了解这些前端知识网上相关资料也很多。
不过回看今天的后端开发,我们使用了 `./hade new``./hade provider new` 两个命令这些都是之前实现的hade命令行工具你是不是切实感受到它们提升了不少开发效率
### 思考题
分析需求的时候,我们画了用户注册和登录的时序图来理解注册/登录逻辑,是不是一下子就简明扼要地说清楚了。其实在实际工作中,使用时序图来解释复杂交互或者服务间调用的逻辑是我们非常必要的技能了。
这节课的思考题希望你去了解一下时序图的元素和绘制方法,然后对照着文中注册和登录的时序图找出以下几个对应元素,检验一下自己的学习效果:
* 角色
* 对象
* 生命线
* 消息
* 控制焦点
* 控制逻辑
欢迎在留言区分享你的学习笔记。感谢你的收听,如果你觉得今天的内容对你有所帮助,也欢迎分享给你身边的朋友,邀请他一起学习。下节课我们继续开发用户模块。