服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”

服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”

文章目录

  • 服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”
    • 一、概述
    • 二、任务目标
    • 三、开发实战流程
      • 0. 开发项目效果展示
      • 1. 开发流程
      • 2. 承担工作
    • 四、后端实现(选择 REST API 构建服务)
      • 0. 部署使用后端
      • 1. swagger 生成 API
      • 2. 数据资源来源
      • 3. 基于 golang 编写后端代码
        • ① api_article.go
        • ② api_comment.go
        • ③ api_user.go
        • ④ route.go
        • ⑤ response.go
        • ⑥ jwt.go (支持 Token 认证)
          • 1)认证技术
          • 2)代码实现
      • 4. 后端数据库支持
        • ① 数据结构
        • ② 数据库操作封装
        • ③ 数据库的 CURD 操作
          • 1) Put
          • 2)Get
        • ④ 数据库应用实例
    • 五、功能完成
      • 0. github 协同开发
      • 1. Mock 服务
      • 2. Travis 测试
    • 六、前端实现(Vue.js)
    • 七、心得体会

项目文档仓库:https://github.com/chuhongwei/WeBlog-doc

服务端仓库:https://github.com/chuhongwei/BlogServer

客户端仓库:https://github.com/zengty-2018/WeBlog_Client

我们小组实现的 ”博客网站“ 包含的功能有:

  1. 用户 User 注册
  2. 用户 User 登录
  3. 实现用户 User 登录 Token 认证
  4. 查看博客 Article 简要信息(可分页)
  5. 查看博客 Article 详细内容
  6. 对博客 Article 发布评论 Comment
  7. 查看别人对博客 Article 的评论 Comment

一、概述

  • 利用 web 客户端调用远端服务是服务开发本实验的重要内容。其中,要点建立 API First 的开发理念,实现前后端分离,使得团队协作变得更有效率。

二、任务目标

  • 选择合适的 API,实现从接口或资源(领域)建模,到 API 设计的过程
  • 使用 API 工具,编制 API 描述文件,编译生成服务器、客户端原型
  • 使用 Github 建立一个组织,通过 API 文档,实现 客户端项目 与 RESTful 服务项目同步开发
  • 使用 API 设计工具提供 Mock 服务,两个团队独立测试 API
  • 使用 travis 测试相关模块

三、开发实战流程

0. 开发项目效果展示

  • 初始页
    服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”_第1张图片

  • 主页(博客简要信息页面)
    服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”_第2张图片

  • 博客详细信息页面
    服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”_第3张图片

  • 评论
    服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”_第4张图片

  • 支持分页
    服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”_第5张图片

  • 登陆页面
    服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”_第6张图片

  • 注册页面
    服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”_第7张图片

  • 动图展示


1. 开发流程

  1. 组员之间设计并规定好 API,之后使用 Swagger 渲染 API 文档和生成 go-server 和 vue-client
  2. 前后端分离进行开发,前端使用 Mock 服务,后端使用 Postman,双方都根据 API 文档的规范进行 API 实现以及调用。前端使用 Vue.js 框架开发,后端使用 Swagger 生成的 go-server 进行进一步的开发
  3. 分别使用测试工具进行功能测试,修改需求细节,更改文档
  4. 前后端合并后一起测试,解决耦合后的BUG,最终完成博客网站的开发

2. 承担工作

笔者主要负责后端开发,后端工作包括:

  1. 建立博客信息模型
  2. 获取博客数据
  3. 建立数据库
  4. 对数据库的 CURD
  5. 实现 API
  6. 处理前端请求并返回响应
  7. 利用 jwt 进行用户认证

后端代码结构:

├── api
│   └── swagger.yaml
├── source
│   ├── Blogs
│   │   ├── WriteBlog.go
│   │   └── xxx.html
│   ├── Blog.db
│   ├── db.go
│   └── model.go
├── go
│   ├── api_article.go
│   ├── api_comment.go
│   ├── api_user.go
│   ├── jwt.go
│   ├── logger.go
│   ├── response.go
│   └── routers.go
├── LICENSE
├── README.md
└── main.go

source 文件夹用于存取数据

  • Blog.db 是数据库 ,它是由 blotDB 实现的;
  • db.go 中,对数据库的 CURD操作被封装成函数;
  • model.go 中定义了 Article、tag、user 和 comment 四个结构体;
  • Blogs 文件夹下是一些网上的博客,作为本项目的素材。

go 文件夹下是对以下各个功能的实现

  • api_article.go: Article API 的实现
  • api_comment.go: Comment API 的实现
  • api_user.go: User API 的实现
  • route.go: 路由
  • response.go: API 响应
  • logger.go: 标准日志输出
  • jwt.go: 实现 token 签发认证

四、后端实现(选择 REST API 构建服务)

在前端没有完成的时候,后端可以用 postman 来测试响应结果
服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”_第8张图片

0. 部署使用后端

  • install
go get -v github.com/chuhongwei/BlogServer
  • run
go run main.go

1. swagger 生成 API

  • OpenAPI 是描述 REST API 的标准规范。 OpenAPI 描述允许人类和计算机无需首先阅读文档或了解实现实现即可发现 API 的功能。

  • Swagger 为 OpenAPI 的工具集,是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。

  • go-swagger 源码

  • 编辑工具:Swagger Editor,页面如下图
    服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”_第9张图片

  • 使用 Swagger 编辑器编写 yaml 文件,生成 API 接口文件。只要在给定的 yaml 文件,修改我们需要修改的部分,然后点击上方的 [Generate Server] 即可生成服务端代码,其中的 index.html 打开后是 API 可视化的页面

  • 之后需要往里面填充与前端交互的逻辑,以及完成配置数据库等对后端的操作

2. 数据资源来源

本次项目选用的博客都拉取自 CSDN 博客网站

  • 2021年GO语言(Golang)就业班全系列
  • B/S架构及其运行原理
  • Beego_ubuntu下golang及beego环境的全局配置
  • Go&&阿里云服务器(Ubuntu)-- Golang项目(beego)服务器部署
  • Go - 学习/实践
  • 阿里巴巴十年Java架构师分享,会了这个知识点的人都去BAT了
  • 领域驱动实践总结(基本理论总结与分析+架构分析与代码设计+具体应用设计分析V)

服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”_第10张图片

3. 基于 golang 编写后端代码

  • API 采用REST v3风格,要实现的 API 接口如下
//获取博客文章的简要信息
GET /articles

//根据 id 获取某篇文章的详细内容
GET /article/{id} 

//根据 id 获取文章的评论
GET /article/{id}/comments

//用户提交评论到文章中
POST /article/{id}/comments 

//用户登录
POST /user/login 

//用户注册
POST /user/register
  • 其中参数类型如下:
  • Path Parameter:即只包含请求地址的传参,比如 GET /article/3
  • Query Parameter:拼在请求地址上的传参,比如 Get /articles?page=1
  • Body Parameter:参数中涉及到传递对象,比如 POST /article/4/comment

① api_article.go

实现 Article 相关的 API:

  • GET /articles
  • GET /article/{id}/comments
  • 获取所有博客文章简要信息
// GET /articles?page=3
// 包含一个 query parameter: page
func GetArticles(w http.ResponseWriter, r *http.Request) {
	// query parameter
	r.ParseForm()
	pageStr := r.FormValue("page")
	page := int64(1)
	if pageStr != "" {
		page, _ = strconv.ParseInt(pageStr, 10, 64)
	}
	articles := db.GetArticles(-1, page)
	Response(ResponseMessage{articles,nil,}, w, http.StatusOK)
}
  • 获取某篇博客文章的具体内容
// GET /article/1
// 包含一个 path parameter: id
func GetArticleByID(w http.ResponseWriter, r *http.Request) {
	// path parameter
    // 分解 URL
	str := strings.Split(r.URL.Path, "/")
	articleID, err := strconv.ParseInt((str[len(str)-1]), 10, 64)
	fmt.Println(articleID)
	// Article ID 字符串转换为数字失败
	if err != nil {
		Response(ResponseMessage{nil, "Invalid Article ID !"}, w, http.StatusBadRequest)
		return
	}

	articles := db.GetArticles(articleID, 0)
	if len(articles) != 0 {
		Response(ResponseMessage{articles[0],nil,}, w, http.StatusOK)
		return
	} else {
		// Article ID 对应的文章不存在
		Response(ResponseMessage{nil, "Article Not Exists !",}, w, http.StatusNotFound)
		return 
	}
}

② api_comment.go

实现 Comment 相关的 API:

  • GET /article/{id}/comments
  • POST /article/{id}/comments
  • 以 发布评论 PostComment 为例,首先要验证 User Token
    token, isValid := CheckToken(w, r)
  • 接着读入参数,解码为数据结构 Comment
    var comment db.Comment
	err := json.NewDecoder(r.Body).Decode(&comment)
  • 更新 User
if claims, ok := token.Claims.(jwt.MapClaims); ok {
		name, _ := claims["name"].(string)
		comment.User = name
	}
  • 生成评论时间
comment.Date = fmt.Sprintf("%d-%d-%d", time.Now().Year(), time.Now().Month(), time.Now().Day())
  • 获取文章 ID
	articleId := strings.Split(r.URL.Path, "/")[2]
	comment.ArticleId, err = strconv.ParseInt(articleId, 10, 64)
  • 将评论写入数据库
for i := 0; i < len(articles); i++ {
		articles[i].Comments = append(articles[i].Comments, comment)
	}
  • 为了避免在博客贴入太多代码,其余函数具体请看后端仓库中的文件

③ api_user.go

实现 User 相关的 API:

  • POST /user/login
  • POST /user/register
  • 以下是用户登陆的实现函数
// 包含 body parameter: userName 和 password, 验证成功返回一个 token 作为该登陆用户的验证信息
func PostUserLogin(w http.ResponseWriter, r *http.Request) {
  • 读取参数,解码为数据结构 User
    db.Init()
	var user db.User
	err := json.NewDecoder(r.Body).Decode(&user)
  • 从数据库中获得用户名对应的 User 信息,并进行验证
	name := db.GetUser(user.Username).Username
	password := db.GetUser(user.Username).Password
	// 验证用户名与密码是否对应
	if name != user.Username || password != user.Password {
		Response(ResponseMessage{nil,"UserName or Password Error !",}, w, http.StatusBadRequest)
		return
	}
  • 产生用户认证,并返回
	tokenStr, err := SignToken(user.Username, user.Password)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintln(w, "Error when sign the token !")
		Response(ResponseMessage{"Sign token error !",nil,}, w, http.StatusOK)
	}
    // 返回 Token
	Response(ResponseMessage{tokenStr,nil,}, w, http.StatusOK)
}
  • 注册和登录的实现类似,为了避免在博客贴入太多代码,其余函数具体请看后端仓库中的文件

④ route.go
  • Route 存储路由的基本信息,是由 swagger 自动生成的服务端代码,主要用于连接对应的 API 和实现它们的函数

⑤ response.go

response.go 用来处理响应 Response

  • ResponseMessage 结构存储 Response 信息
type ResponseMessage struct {
	OkMessage    interface{} `json:"ok,omitempty"`
	ErrorMessage    interface{} `json:"error,omitempty"`
}
  • Response 发送回复信息
func Response(response interface{}, w http.ResponseWriter, code int) {
	jsonData, err := json.Marshal(&response)

	if err != nil {
		log.Fatal(err.Error())
	}

	w.Header().Set("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS")
	w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Access-Control-Allow-Origin, Access-Control-Allow-Credentials, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Authorization, X-Requested-With")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.Write(jsonData)
	w.WriteHeader(code)
}
  • 前端使用 POST 方法发送请求时,会先发送一个 OPTIONS 方法的请求,收到正确的响应后才会发送 POST 请求
func Options(w http.ResponseWriter, r *http.Request){
	w.Header().Set("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS")
	w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Access-Control-Allow-Origin, Access-Control-Allow-Credentials, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Authorization, X-Requested-With")
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.WriteHeader(http.StatusOK)
}

⑥ jwt.go (支持 Token 认证)
1)认证技术
  • 为了方便实现用户认证,建议采用 JWT 产生 token 实现用户认证。
  • 什么是 jwt? 官网:https://jwt.io/ 中文支持:http://jwtio.com/
  • 如何使用 jwt 签发用户 token ,用户验证 http://jwtio.com/introduction.html
  • 使用 Token 身份认证后,在服务端不需要存储用户的登录记录。大致流程如下
  1. 客户端使用 userName 跟 password 请求登录
  2. 服务端收到请求,验证 userName 和 password
  3. 验证成功后,服务端会生成一个 Token,把它发给客户端
  4. 客户端收到 Token 后把它存储起来,比如放在浏览器的 Cookie 里
  5. 客户端每次向服务端请求资源时,带着服务端签发的 Token
  6. 服务端收到请求,验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据,免去登录步骤
2)代码实现
  • 生成 Token
const jwtSecret = "serviceHW9"

func SignToken(userName, password string) (string, error) {
	nowTime := time.Now()
	expireTime := nowTime.Add(time.Hour * time.Duration(1))

	claims := make(jwt.MapClaims)
	claims["name"] = userName
	claims["pwd"] = password
	claims["iat"] = nowTime.Unix()
	claims["exp"] = expireTime.Unix()
	
	// crypto.Hash 方案
	token := jwt.New(jwt.SigningMethodHS256)
	token.Claims = claims
	//  该方法内部生成签名字符串,再用于获取完整、已签名的 token
	return token.SignedString([]byte(jwtSecret))
}
  • 验证 Token
func CheckToken(w http.ResponseWriter, r *http.Request) (*jwt.Token, bool) {
	// 用户登录请求取出 token
	token, err := request.ParseFromRequest(r, request.AuthorizationHeaderExtractor,
		func(token *jwt.Token) (interface{}, error) {
			return []byte(jwtSecret), nil
		})
	
	if (err == nil && token.Valid) {
		return token, true
	}
	
	w.WriteHeader(http.StatusUnauthorized)

	if err != nil {
		fmt.Fprint(w, "Authorized access to this resource iss invalid !")
	}

	if !token.Valid {
		fmt.Fprint(w, "Token is invalid !")
	}

	return token, false
}

4. 后端数据库支持

数据库使用 BoltDB,它是一个纯粹的 Key/Value 模型的程序,能够提供一个简单,快速,可靠的数据库

① 数据结构
  • Article 存储博客信息
type Article struct {
	Id int64 `json:"id,omitempty"`

	Title string `json:"title,omitempty"`

	Username string `json:"username,omitempty"`

	Tags []Tag `json:"tags,omitempty"`

	Date string `json:"date,omitempty"`

	Content string `json:"content,omitempty"`

	Comments []Comment `json:"comments,omitempty"`
}
  • User 存储用户信息
type User struct {
    Username string `json:"username,omitempty"`

    Password string `json:"password,omitempty"`
}

② 数据库操作封装

将对数据库的操作,封装成函数存放在 db.go 文件

  • db.go 中,Init 用于创建和启动数据库。
  • open 的第一个参数为路径,如果数据库不存在则会创建名为 my.db 的数据库,第二个为文件操作,第三个参数是可选参数,内部可以配置 只读和超时时间等
// 获取数据库的路径
//get the database path
func DBPATH() string {
	pt, _ := os.Getwd()
	fmt.Println(path.Join(pt ,"/source/Blog.db"))
	return  path.Join(pt ,"/source/Blog.db")
}

// 初始化
//create the bucket for article and user
func Init() {
	db, err := bolt.Open(DBPATH(), 0600, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	err = db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("article"))
		if b == nil {
			_, err := tx.CreateBucket([]byte("article"))
			if err != nil {
				log.Fatal(err)
			}
		}

		b = tx.Bucket([]byte("user"))
		if b == nil {
			_, err := tx.CreateBucket([]byte("user"))
			if err != nil {
				log.Fatal(err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}
}
  • boltdb 是文件操作类型的数据库,所以只能单点写入和读取,如果多个同时操作的话后者会被挂起直到前者关闭操作为止,数据具有较强的一致性。

③ 数据库的 CURD 操作
  • 对数据库的 CURD 实现,即创建(Create)、更新(Update)、读取(Retrieve)和删除(Delete)操作,具体步骤如下

打开数据库
获取一个事务 tx
根据 tx 获取bucket b
进行更新——b.Put(key, data)
进行查询——b.Get(key)

下面以 Articles 和 Users 为例

1) Put
  • 存取键值对到桶里,实现文章和用户数据的添加
  • 为了避免在博客贴入太多代码,具体请看后端仓库中的 db.go 文件
2)Get
  • 从桶里获取键值对,实现对文章和用户信息的查询
  • 为了避免在博客贴入太多代码,具体请看后端仓库中的 db.go 文件

④ 数据库应用实例
  • 将 Article 和 User 数据写入到数据库中
func writeOneBlog(id int64,title string,author string,tags []db.Tag,date string,content string,comments []db.Comment){

	articles := db.Article{id,title,author,tags,date,content,comments}
	users := db.User{author,author}
	db.PutArticle(articles)
	db.PutUser(users)
}

五、功能完成

0. github 协同开发

  • 使用 Github 建立一个组织,通过 API 文档,实现 客户端项目 与 RESTful 服务项目同步开发
  • 后端仓库
    服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”_第11张图片
  • commit 次数
    服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”_第12张图片

1. Mock 服务

  • 使用 API 设计工具提供 Mock 服务,两个团队独立测试 API
  • Mock 的作用是在测试过程中,创建一个 mock 对象来模拟对象的行为。Mock 能帮助前端独立于后端进行开发,进行 API 模拟并返回一些假数据
  • 在本次项目中,前端使用 mockjs 实现 mock 数据,针对要测试的 API 及其 API 文档规定的返回内容编写相应的假数据即可

2. Travis 测试

  • 使用 travis 测试相关模块
  • travis 是一个帮助你自动化进行测试的一个工具,有效的减少程序崩溃与 bug 的几率
  • 点击 browae our directory 选择 tarvis ci 这个工具添加这个网站,使用 github 给这个网站授权,并且需要开启一下仓库里面哪一个仓库使用这种服务服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”_第13张图片
  • 每次在提交到 main 分支之后都会进行代码测试,即执行以下指令
    服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”_第14张图片
  • 在以下网站显示结果 https://www.travis-ci.org/github/chuhongwei/BlogServer

服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”_第15张图片


六、前端实现(Vue.js)

  • 前端由小组其他两位成员负责,使用 Vue.js 框架开发
  • 前端github仓库:
  • 使用部署前端

使用命令 npm install 安装好对应的依赖
使用命令 npm run dev 运行项目后进入网址 localhost:8080

  • 虽然本次实验没有很多地接触前端的开发,但通过阅读伙伴的前端源码,也可以很好的理解到前端开发的框架。

七、心得体会

  • 由于后端数据库是由我的一位组员负责的,且我对前端的使用不熟悉,一开始没有意识到要先运行 WriteBlog.go 将博客信息写到数据库中,才能在前端显示的页面中获取信息
    服务计算 作业9 简单 web 服务与客户端开发实战 “博客网站”_第16张图片
  • 使用 RESTful API 进行 API 设计,让前后端分离,也让开发变得便捷,前端只需要调用相应的接口就可以获取到想要的资源进行页面展示。
  • 学习了创建组织管理团队项目,协同开发
  • 学习了使用一些工具,比如 swagger,boltDB 等

你可能感兴趣的:(go,golang,vue,swagger2,github)