本次项目的重点是利用web客户端调用远端服务,并且实现前后端分离开发,以此提高团队协作的效率。
我们的大致开发流程是:
- 设计REST风格的API;
- 使用swagger生成后端框架,并完成后端的编写;与此同时,使用vue框架完成前端的编写;
- 使用postman测试后端,使用mock测试前端;
- 运行测试
- 完成文档的编写
下面是具体的实现过程。
一. 构思阶段
项目的要求是,至少含有6个API,4种资源。我们设计的博客网站,支持以下六个API:
- /openapi101/auth/signup --用户注册
- /openapi101/auth/signin --用户登陆
- /openapi101/article --发布文章
- /openapi101/article/articleID --根据ID查找文章
- /openapi101/article/articleID/comment --查看文章评论
- /openapi101/article/articleID/comment --发表评论
四种资源分别是:
- 用户(包含数据:用户名,密码)
- 文章(包含数据:文章ID,标题,内容)
- 评论(包含数据:用户名,评论内容)
- 标签(包含数据:文章标签)
二. API设计
使用swagger-editor来编写API文档,遵循yaml语法:
swagger: "2.0"
info:
description: "A simple API to learn how to write OpenAPI Specification"
version: "1.0.0"
title: "Simple API"
host: "simple.api"
basePath: "/openapi101"
schemes:
- "https"
paths:
/auth/signin:
post:
summary: "sign in"
parameters:
- name: "username"
in: "path"
required: true
type: "string"
x-exportParamName: "Username"
responses:
"200":
description: "A list of Person"
schema:
required:
- "username"
properties:
username:
type: "string"
"404":
description: "The user does not exists"
/auth/signup:
post:
summary: "sign up a user"
description: "sign up a user"
parameters:
- in: "body"
name: "user"
required: false
schema:
$ref: "#/definitions/user"
x-exportParamName: "User"
responses:
"204":
description: "OK"
"400":
description: "Wrong"
/article/{article_id}:
get:
summary: "get post"
parameters:
- name: "article_id"
in: "path"
description: "article id"
required: true
type: "string"
x-exportParamName: "ArticleId"
responses:
"200":
description: "A post"
schema:
properties:
name:
type: "string"
data:
type: "string"
/article:
post:
summary: "Create article"
parameters:
- in: "body"
name: "content"
required: true
schema:
$ref: "#/definitions/content"
x-exportParamName: "Content"
responses:
"204":
description: "A post"
definitions:
user:
required:
- "password"
- "username"
properties:
username:
type: "string"
password:
type: "string"
content:
required:
- "article_content"
properties:
article_content:
type: "string"
author:
type: "string"
点击go-server后,swagger会自动生成后端框架,结构如下:
- go-server
- .swagger-codegen
- VERSION
- api
- swagger.yaml
- go
- routers.go
- logger.go
- user.go
- user_api.go
- ...
- README.md
- main.go
其中,routers.go定义了url的路由去向,不同的url对应不同的API实现。
routers.go如下:
package swagger
import (
"fmt"
"net/http"
"strings"
"github.com/gorilla/mux"
)
type Route struct {
Name string
Method string
Pattern string
HandlerFunc http.HandlerFunc
}
type Routes []Route
func NewRouter() *mux.Router {
router := mux.NewRouter().StrictSlash(true)
for _, route := range routes {
var handler http.Handler
handler = route.HandlerFunc
handler = Logger(handler, route.Name)
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(handler)
}
return router
}
func Index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
}
var routes = Routes{
Route{
"Index",
"GET",
"/openapi101/",
Index,
},
Route{
"ArticleArticleIdGet",
strings.ToUpper("Get"),
"/openapi101/article/{article_id}",
ArticleArticleIdGet,
},
Route{
"ArticlePost",
strings.ToUpper("Post"),
"/openapi101/article",
ArticlePost,
},
Route{
"AuthSigninPost",
strings.ToUpper("Post"),
"/openapi101/auth/signin",
AuthSigninPost,
},
Route{
"AuthSignupPost",
strings.ToUpper("Post"),
"/openapi101/auth/signup",
AuthSignupPost,
},
}
三.API实现
后端的编写中,我主要负责两个API的实现:文章的发布和根据ID查找文章。
首先,定义Article结构体:
type Article struct {
Id int `json:"id"`
ArticleContent string `json:"articlecontent"`
Author string `json:"author"`
Title string `json:"title"`
}
在介绍发布文章之前,我们需要明确一点:如何确保用户在发布文章的时候已经是登陆状态呢?又如何确保用户不能以别人的名义发布文章呢?
这里使用的是jwt(JOSON Web Token)来认证,用户注册之后, 服务器生成一个 JWT token返回给浏览器, 浏览器向服务器请求数据时将 JWT token 发给服务器, 服务器用 signature 中定义的方式解码 JWT 获取用户信息。客户端获得Token后,可以将其储存在cookie中,当要请求需要授权的服务时,把Token赋给Header的Authorization项。
认证函数如下:
func Authorization(w http.ResponseWriter, r *http.Request, username string) bool {
token, _ := request.ParseFromRequest(r,
request.AuthorizationHeaderExtractor,
func(token *jwt.Token) (interface{}, error) {
return []byte(SecretKey), nil
})
claim, _ := token.Claims.(jwt.MapClaims)
if username != claim["sub"] {
return false
}
return true
}
接下来,处理发布文章的请求和查找文章的请求,分别是POST和GET操作。文章存储在数据库中(采用的是boltDB),两种请求分别对应数据库的写和读操作。
发布文章:
func ArticlePost(w http.ResponseWriter, r *http.Request) {
db, err := bolt.Open("my.db", 0600, nil)
if err != nil {
log.Fatal(err)
}
defer db.Close()
fmt.Print(r.Header)
fmt.Print(r.Body)
var article Article
err = json.NewDecoder(r.Body).Decode(&article)
fmt.Print(article)
ok := Authorization(w, r, article.Author)
if !ok {
response := ErrorResponse{"Authorization failed!"}
JsonResponse(response, w, http.StatusUnauthorized)
return
}
if article.Title == "" || article.ArticleContent == "" {
response := ErrorResponse{"Wrong title or content"}
JsonResponse(response, w, http.StatusBadRequest)
return
}
if err != nil {
response := ErrorResponse{err.Error()}
JsonResponse(response, w, http.StatusBadRequest)
return
}
err = db.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte("Article"))
if err != nil {
return err
}
id, _ := b.NextSequence()
article.Id = int(id)
fmt.Println("%d",int(id))
encoded, err := json.Marshal(article)
byte_id := itob(article.Id)
return b.Put(byte_id, encoded)
})
if err != nil {
response := ErrorResponse{err.Error()}
JsonResponse(response, w, http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
}
根据ID查找文章:
func ArticleArticleIdGet(w http.ResponseWriter, r *http.Request) {
db, err := bolt.Open("my.db", 0600, nil)
if err != nil {
log.Fatal(err)
}
defer db.Close()
articleId := strings.Split(r.URL.Path, "/")[3]
Id, err:= strconv.Atoi(articleId)
if err != nil {
reponse := ErrorResponse{"Wrong ArticleId"}
JsonResponse(reponse, w, http.StatusBadRequest)
return
}
var article Article
err = db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("Article"))
if b != nil {
v := b.Get(itob(Id))
if v == nil {
return errors.New("Article Not Exists")
} else {
_ = json.Unmarshal(v, &article)
return nil
}
} else {
return errors.New("Article Not Exists")
}
})
if err != nil {
reponse := ErrorResponse{err.Error()}
JsonResponse(reponse, w, http.StatusNotFound)
return
}
JsonResponse(article, w, http.StatusOK)
}
四.使用postman进行测试
由于前端开发尚未完成,这里需要借助postman发起请求。开启服务器后,在postman上向服务器发起POST请求发布文章:
发布成功时,显示状态码200。为了验证文章是否正确存至数据库,再在postman上向服务器发起GET请求,根据ID查找文章:
查找成功,显示状态码200,结果正好是刚才发布的文章。说明发布文章、根据ID查找文章这两个API的实现基本正确。
五.前端开发
前端开发采用VUE框架,这里不再赘述。
最后,放上github项目地址。