GitHub:
项目文档: https://github.com/fentender/book-blog-doc
目录
前言
此次项目以API-First为原则,利用Web客户端调用远端服务以开发一个前后端分离的Web博客。简单回顾此次项目开发历程,可以将其大致分为以下几步:
- 设计本次项目中博客所需要使用到RESTful风格的API
- 使用Swagger Editor编写文档,并生成利用其自动化生成客户端与服务端的API接口代码
- 利用mock模拟返回数据以进行测试,独立开发客户端项目
- 利用postman模拟请求,独立开发服务端项目
- 前后端开发完成后,进行测试,完善前后端项目
同时在本次的项目博客详细情况如下:
- 包含资源类型:Book,User,Bookshelf,Review,Token(书局来源于豆瓣读书)
- 通过/books, /reviews/{bookID},/users/{username}/bookshelfs等可获得简单 API 服务列表,并且支持分页
- Bookshelf资源与User资源支持token认证,只能在登录后才能使用。
一、API设计
在本次项目中使用了JWT的Token身份认证,设计思路如下:
- 客户端通过用户名和密码向服务器发送请求登陆
- 服务器收到请求数据,在数据库进行查询验证
- 如果验证成功,服务器签发一个Token给客户端
- 客户端可以将Token存放到SessionStroage 或者Cookie里
- 客服端设置监听,每次跳转路由,就判断 SessionStroage 中有无 Token ,没有就跳转到登录页面,有则跳转到对应路由页面
- 在每次客户端发送的请求中,在请求头中加上Token
- 在后端设置拦截器,用户登录后的每次请求都会经过这个拦截器校验Token是否有效
- 如果验证成功,则继续执行请求,返回请求到的数据
因此除了普通资源的API设计外,还需要增加用于登录、注册和注销的几个API。在本次项目BookBlog中有如下4种资源:User、Book、Bookshelf、Review、Token。其API服务如下:
{
//Book
"getBook" : GET "/books/{bookId}",
"getBooks" : GET "/books",
//Review
"getReview" : GET "/review/{reviewId}",
"getReviews" : GET "/reviews",
//User
"getUser" : GET "/users/user",
//Bookshelf
"createBookshelf" : POST "/user/{username}/bookshelfs",
"getBookshelfs" : GET "/users/{username}/bookshelfs",
"getBookshelf" : GET "/users/{username}/bookshelfs/{bookshelfName}",
"deleteBookshelf" : DELETE "/users/{username}/bookshelfs/{bookshelfName}",
//添加书籍到书架中
"addBookInBookshelf" : POST "/users/{username}/bookshelfs/{bookshelfName}/{bookId}",
"deleteBookInBookshelf" : DELETE "/users/{username}/bookshelfs/{bookshelfName}/{bookId}",
//token相关资源,用于账户JWT验证
"signIn" : GET "/token",
"signOut" : DELETE "/token",
"signUp" : POST "/token"
}
二、Swagger Editor使用
OpenAPI 规范(OAS)是一种通用的、和编程语言无关的 API 描述规范,使人类和计算机都可以发现和理解服务的功能,而无需访问源代码、文档或针对接口进行嗅探。而Swagger 规范即是OpenAPI的原身。Swagger则可以说是一个 OpenAPI 的工具集。Swagger Editor是Swagger中的一个编写OpenAPI的编辑器。其使用yaml语法来编写API文档,并且可以实时响应,使用漂亮的UI来测试效果
Swagger官网: https://swagger.io/
Swagger Editor在线编辑:https://editor.swagger.io/
1.编写API文档
Swagger Editor使用yaml语法进行编写API文档,采用OpenAPI规范描述API,并能够根据代码实时响应生成UI效果,让我们直观地看到效果。
可以通过查阅OpenAPI文档与查看官方例子学习其使用用法。
(需要注意的是XMLHttpRequest不支持GET请求中带body参数,而swagger在生成javascript时使用的superagent包是通过XMLHttpRequest发送请求的,我目前还没找到解决方案。因此在编写API文档时,注意尽量不要在GET请求中带body参数)
yaml教程:http://www.ruanyifeng.com/blog/2016/07/yaml.html
OpenAPI文档:https://swagger.io/docs/specification/api-host-and-base-path/
以下即为本次项目BookBlog使用Swagger Editor生成的文档展示
2.自动化生成项目需求文档
Swagger Editor能够根据所编写的文档自动生成客户端、服务端接口或是API文档。
选择Swagger Editor菜单栏:
API Doc文档
"Generate Client" - "html2":
导出html格式的API Doc文档。
客户端
"Generate Client" - "javascript" :
导出JavaScript编写的客户端API接口代码。其文件结构如下:
javascript-client
|-- git_push.sh
|-- mocha.opts
|-- package.json
|-- README.md
|-- docs
| |-- Book.md
| |-- BookApi.md
| ...
| |-- UserApi.md
| `-- UserBookshelfApi.md
|-- src
| |-- ApiClient.js
| |-- api
| | |-- BookApi.js
| | ...
| | |-- UserApi.js
| | `-- UserBookshelfApi.js
| |-- index.js
| `-- model
| |-- Book.js
| ...
| |-- Token.js
| `-- User.js
`-- test
|-- api
| |-- BookApi.spec.js
| ...
| |-- UserApi.spec.js
| `-- UserBookshelfApi.spec.js
|-- assert-equals.js
`-- model
|-- Book.spec.js
...
|-- Token.spec.js
`-- User.spec.js
可以看到由Swagger editor生成的客户端接口文档实际上就是一个npm包。其中各个文件作用如下:
- docs: 各个API的描述
- src: 存放使用javascript编写的API接口方法,其使用superagent(一个轻量的Ajax API)向服务端发送请求。
- test:文件夹下为API接口方法的测试文档,可在根目录下使用npm run test命令进行mocha测试。
- mocha.opts: mocha测试的配置文档
- git_push.sh: 用于快速推送文档至git上的sh脚本
- package.json: bookblogapi客户端的npm模块描述文件。
- README.md: 客户端API接口的使用方法
为了在之后的前端项目中使用这个生成的npm包,将其公布在github: https://github.com/fentender/book_blog_api。在之后的客户端项目中就可以使用以下命令安装api接口npm包。
npm install fentender/book_blog_api --save
服务端
"Generate Server" - "go-server" :
导出go编写的服务端代码,其结构如下:
go-server-server
|-- api
| `-- swagger.yaml
|-- go
| |-- README.md
| |-- api_book.go
| |-- api_review.go
| |-- api_user.go
| |-- api_user_bookshelf.go
| |-- logger.go
| |-- model_book.go
| |-- model_books.go
| |-- model_bookshelf.go
| |-- model_bookshelfs.go
| |-- model_review.go
| |-- model_reviews.go
| |-- model_user.go
| `-- routers.go
`-- main.go
各个文件作用如下:
go/module_xxx.go: 定义了API中各种数据类型的结构。
//model_book.go
type Book struct {
BookId int32 `json:"bookId"`
BookName string `json:"bookName,omitempty"`
Autor string `json:"autor,omitempty"`
Info string `json:"info,omitempty"`
}
go/logger.go: 为每个路由的处理函数都注册了一个log.Printf()语句,即在服务端运行时,会打印出接收到的每一个有效请求的信息
//logger.go
func Logger(inner http.Handler, name string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
inner.ServeHTTP(w, r)
log.Printf(
"%s %s %s %s",
r.Method,
r.RequestURI,
name,
time.Since(start),
)
})
}
go/router.go: 为我们实现了路由匹配和匹配函数调用
//router.go
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) //调用logger.go进行注册,使得每个有效的路由请求信息被打印出来
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",
"/",
Index,
},
...
}
三、BookBlog客户端
本次项目使用vue.js框架,并利用vue-cli以开发单页面应用的博客。在开发过程中使用vue router、vuex进行一些功能上的辅助,并在开发过程中使用mock.js进行mock 测试。最后使用Ajax-hook包拦截客户端向服务端发送的请求添加token以完成jwt验证功能。
vue官网: https://cn.vuejs.org/
vue-cli: https://cli.vuejs.org/zh/
mock.js: http://mockjs.com/
Ajax-hook: https://github.com/wendux/Ajax-hook
1.使用vue-cli建立项目
在vue-cli安装完成后,使用以下命令建立bookblog项目文档
vue create bookblog
之后进入如下界面
Vue CLI v4.5.9
? Please pick a preset: (Use arrow keys)
> Default ([Vue 2] babel, eslint)
Default (Vue 3 Preview) ([Vue 3] babel, eslint)
Manually select features
可以选择Default默认配置,或是Manually手动配置。在此次项目中,直接使用Default默认即可。至于在Vue版本的选择上由于Vue 3的API文档还是beta版,因此我选择了Vue 2。在安装完成后就生成了bookblog项目文档。
bookblog
|-- README.md
|-- babel.config.js
|-- node_modules
|-- package-lock.json
|-- package.json
|-- public
`-- src
在项目文档生成后,即可使用vue框架正式开始BookBlog的页面的开发。为了更好地使用vue-cli,还需要了解学习vue的语法、单文件组件、npm、webpack等等相关知识(详情参看Vue官方网站)。在这些准备工作完成后,即可正式开始项目开发了。
2. book_blog_api包的使用
为了使用在上文中使用Swagger Editor生成的api接口book_blog_api包,需要在bookblog项目文档中进行npm install安装
npm install fentender/book_blog_api --save
在安装完成后即可在项目中调用该api包,以向服务端发送数据请求。具体用法如下:
import BookBlogApi from 'book_blog_api';
var api = new BookBlogApi.BookApi()
var bookId = 56; // {Number} Book's ID
var callback = function(error, data, response) {
if (error) {
console.error(error);
} else {
console.log('API called successfully. Returned data: ' + data);
}
};
api.getBook(bookId, callback);
以下为本次项目中用于获得书籍信息时调用getBooks()接口的一次具体例子:
//Books.vue
import BookBlogApi from 'book_blog_api';
var api = new BookBlogApi.BookApi();
var Books = null;
export default {
name: 'Books',
data: function() {
return {
books: Books.books,
num: Books.num,
currentPage: 1
}
},
...
methods: {
setData(err, data) {
if(err) {
this.error = err.toString();
} else {
this.books = data.books;
this.num = data.num;
}
},
},
beforeRouteEnter(to, from, next) {
if(!Books) {
api.getBooks( {num: 1}, (err, data, response) => {
let obj
if(response.body == null) {
obj = JSON.parse(response.text);
} else {
obj = data;
}
Books = obj;
next(vm => vm.setData(err, obj));
})
} else {
next(vm => vm.setData(null, Books));
}
},
...
}
3.使用mock拦截请求,模拟数据并返回
同样的,为了在项目中使用mockjs服务,需要先进行npm包的安装
npm install mockjs --save-dev //由于mock测试只需要在开发环境进行,而在生成环境就不需要了,因此使用-dev
为了便于在完成客户端的开发后将mockjs测试关闭,独立创建一个文件用于保存mockjs测试代码。
//mock.js
import Mock from 'mockjs';
import ruler from './ruler'
// 请求模拟数据
//Books
Mock.mock(/books$/, 'get', ruler.books);
Mock.mock(/books\/[0-9]+$/, 'get', ruler.book);
Mock.mock(/books\/[0-9]+$/, 'delete', null);
...
...
...
Mock.mock(/users\/[a-zA-Z0-9%]+\/bookshelfs\/[a-zA-Z0-9%]+\/[0-9]+$/, 'post', null);
Mock.mock(/users\/[a-zA-Z0-9%]+\/bookshelfs\/[a-zA-Z0-9%]+\/[0-9]+$/, 'delete', null);
//Token
Mock.mock(/token$/, 'post', ruler.token);
Mock.mock(/token$/, 'get', ruler.token);
//User
Mock.mock(/users\/[a-zA-Z0-9%]+$/, 'get', ruler.user);
ruler.js用于保存mockjs生成的模拟数据规范
import Mock from 'mockjs';
var Random = Mock.Random;
var exports = function() {
this.book = {
'bookId|+1' : 0,
'bookName' : /[A-Z][a-z]{4,8}/,
'autor' : /[a-z]{4,6} [a-z]{4,6}/,
'info' : Random.paragraph(8, 15)
}
this.books = {
'num|25-50' : 1,
'books|10' : [this.book]
};
this.review = {
'ID|+1' : 0,
'Content' : Random.paragraph(8, 15),
'autor' : /[a-z]{4,6} [a-z]{4,6}/
};
this.reviews = {
'num|25-50' : 1,
'reviews|10' : [this.review]
};
this.user = {
'Username' : Random.word(),
'Password' : Random.word(8, 10)
};
this.bookshelf = {
'num|25-50' : 1,
'bookshelf|8' : [{
'bookName' : /[a-z]{4,6} [a-z]{4,6}/,
'bookId|+1' : 0
}]
}
this.bookshelfs = {
'num|25-50' : 1,
'bookshelfs|8' : [{'bookshelfName' : /[a-z]{4,6} [a-z]{4,6}/}]
}
this.token = {
'Token' : Random.word(8)
}
}
export default new exports();
完成以上文件后,即可在main.js文件中通过添加与取消该文件的导入来控制mock测试的开启与否。
import './mock/mock.js';
4.客户端JWT认证
按照上文所述JWT设计思路:
- 客户端通过用户名和密码向服务器发送请求登陆
- 服务器收到请求数据,在数据库进行查询验证
- 如果验证成功,服务器签发一个Token给客户端
- 客户端可以将Token存放到SessionStroage 或者Cookie里
- 客服端设置监听,每次跳转路由,就判断 SessionStroage 中有无 Token ,没有就跳转到登录页面,有则跳转到对应路由页面
- 在每次客户端发送的请求中,在请求头中加上Token
- 在后端设置拦截器,用户登录后的每次请求都会经过这个拦截器校验Token是否有效
- 如果验证成功,则继续执行请求,返回请求到的数据
因此,为了实现JWT,首先需要在使用 login API成功登录客户端接收到Token后,需要将其存储在SessionStroage中。之后在每次跳转路由时,判断SessionStroage是否存在Token。并对于客户端的每个请求都进行拦截,在其请求头上加入Token。
为了储存Token在SeesionStorage,可以在调用signIn API的回调函数中,将数据存储在SessionStroage中。
//login.vue
...
...
api.signIn(userInfo, (err, data, response) => {
if(response.statusCode == 200) {
alert("登录成功!");
let token;
if(response.body) {
token = data;
} else {
token = JSON.parse(response.text);
}
this.$store.commit("setToken", { token: token.Token }); //调用vuex的store状态中的setToken方法
this.$store.commit("setUser", { username: this.username });
this.$router.push({ name:"User", params: { username: this.username }});
} else {
alert("账号登录失败,请重新输入");
}
})
...
...
//store/index.js
mutations: {
setToken(state, token) {
state.token = token.token;
sessionStorage.setItem("token", state.token); //储存token至sessionStorage中
},
...
}
之后的实现在路由跳转时判断token存在与否则可使用vue router的导航守卫beforeEach(to ,from, next)钩子函数来实现。该钩子函数会在每次路由跳转时被调用,from为跳转路由时的起始路由,to为跳转的目标路由,next用于继续管道中的下一个钩子。
router.beforeEach((to, from, next) => {
if ( !to.meta.requiresAuth ) {
next();
} else {
let token = sessionStorage.getItem("token"); //从sessionStorage取出字段token的值
if( token === '' || token === null) { //判断token是否存在
alert("请先登录账号");
next({ name: "Login"} ) //若不存在就跳转路由至登录界面
} else {
next(); //若存在则继续管道中的下一个钩子,不对该路由跳转操作
}
}
})
最后,还需要在客户端的每一个发送请求的请求头上加入token,为了实现该功能,需要使用到Ajax-hook包,该包可以用于拦截浏览器XMLHttpRequest的库,并对其进行操作。
(其中需要注意的是,Ajax-hook与mockjs都会拦截XML HttpRequest请求,因此在同时使用这两者的功能时,两者会先后拦截客户端发送的请求并处理。这可能会造成一些错误,我暂时还没有解决。这时候的测试只能单独进行。)
官方案例:
import {proxy, unProxy} from "ajax-hook";
proxy({
//请求发起前进入
onRequest: (config, handler) => {
console.log(config.url)
handler.next(config);
},
//请求发生错误时进入,比如超时;注意,不包括http状态码错误,如404仍然会认为请求成功
onError: (err, handler) => {
console.log(err.type)
handler.next(err)
},
//请求成功后进入
onResponse: (response, handler) => {
console.log(response.response)
handler.next(response)
}
})
通过使用该包,即可以实现出我们所需要的功能。
//http/http.js
import store from "../store";
import {proxy} from "ajax-hook";
proxy({
//请求发起前进入
onRequest: (config, handler) => {
if(store.state.token != ''){
config.headers.Authorization = store.state.token //在请求的headers中加入Authorization
}
handler.next(config);
},
//请求发生错误时进入
onError: (err, handler) => {
handler.next(err)
},
//请求成功后进入
onResponse: (response, handler) => {
handler.next(response)
}
})
5.客户端结构
以下即为本次项目完成后的客户端src目录结构:
src
|-- App.vue
|-- assets
| `-- imgs
| |-- book.png
| |-- bookshelf.png
| |-- logo-mini.png
| `-- logo.png
|-- components
| |-- BookBrief.vue
| |-- BookshelfBookBrief.vue
| |-- BookshelfBrief.vue
| |-- BookshelfFooter.vue
| |-- Nav.vue
| |-- PageFooter.vue
| `-- ReviewBrief.vue
|-- http
| `-- http.js
|-- main.js
|-- mock
| |-- MockServer.js
| |-- mock.js
| `-- ruler.js
|-- router
| `-- index.js
|-- store
| `-- index.js
`-- views
|-- Book.vue
|-- Books.vue
|-- Bookshelf.vue
|-- Bookshelfs.vue
|-- Home.vue
|-- Login.vue
|-- Review.vue
|-- Reviews.vue
|-- Signup.vue
`-- User.vue
四、BookBlog服务端
本次项目服务端采用由Swagger自动生成的go-server为基础编写而成,使用Boltdb作为数据库,并借助negroni中间件实现jwt的验证。在最后使用postman进行测试。
Boltdb地址: https://github.com/boltdb/bolt
postman官网: https://www.postman.com/
negroni地址: https://github.com/urfave/negroni
1.API的编写
由于Swagger自动生成的go-server已经编写好了服务端的路由匹配功能,因此在服务端的工作只需要完成各个路由对应的API函数功能即可。以GetBooks为例:
//GetBooks 根据请求读取相应Book[]并返回
func GetBooks(w http.ResponseWriter, r *http.Request) {
var i, j, pageNumber int = 0, 0, 1
var book [30]Book = [30]Book{}
if r.URL.Query()["pageNumber"] != nil {
pageNumber, _ = strconv.Atoi(r.URL.Query()["pageNumber"][0])
}
db, err := bolt.Open("./database/bookblog.db", 0600, nil)
if err != nil {
log.Fatal(err)
}
defer db.Close()
db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("Book"))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
if ((pageNumber-1)*10 > i || i >= pageNumber*10) && pageNumber != -1 {
i++
continue
}
json.Unmarshal(v, &book[j])
j++
i++
}
return nil
})
var buf []byte
if pageNumber != -1 {
buf, _ = json.Marshal(Books{int32(i), book[:10]})
} else {
buf, _ = json.Marshal(Books{int32(i), book[:]})
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "x-requested-with,content-type")
w.WriteHeader(http.StatusOK)
w.Write(buf)
}
在完成GetBooks函数后,即可使用GET /books。 按照以上例子,逐步将所有API填补实现即可。
2.Postman的使用
在编写完成API函数后即可使用Postman向服务端请求资源,以测试对应的功能。Postman的使用方法在其官网上已有详细的教程,通过简单的学习就可以开始使用了。
测试:GET /books
3.JWT的服务端实现
为了实现token验证,我们需要服务端每次接收请求后、正式处理请求前,在这两者之间进行token的验证。因此我们可以用到中间件negroni。它能够让我们添加中间件,以达到在API函数处理请求前验证token的功能。
首先,由于博客中并不是所有的功能都需要登录账号,因此只有一部分API需要验证token,因此对于每一个路由都为其添加requiredToken属性,通过设置它来控制其是否需要用到验证token的中间件。
type Route struct {
Name string
Method string
Pattern string
HandlerFunc http.HandlerFunc
requiredToken bool
}
其次,在routers.go中修改原先的路由匹配
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 NewRouter() *mux.Router {
router := mux.NewRouter().StrictSlash(true)
for _, route := range routes {
var handler http.Handler
handler = route.HandlerFunc
handler = Logger(handler, route.Name)
//根据属性requiredToken判断是否需要使用中间件验证token
if route.requiredToken {
router.Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(negroni.New(negroni.HandlerFunc(authorizedValid), negroni.Wrap(handler)))
} else {
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(handler)
}
}
return router
}
最后完成authorizedValid中间件的编写,实现token验证
package swagger
import (
"github.com/boltdb/bolt"
"github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
"log"
"net/http"
)
func authorizedValid(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
token, errors := request.ParseFromRequest(r, request.AuthorizationHeaderExtractor,
func(token *jwt.Token) (interface{}, error) {
return []byte(SecretKey), nil
})
db, err := bolt.Open("./database/bookblog.db", 0600, nil)
if err != nil {
log.Fatal(err)
}
if errors == nil && token.Valid {
tokenString, _ := token.SignedString([]byte(SecretKey))
if DbKeyofToken(db, tokenString) != nil {
db.Close()
log.Println("Token验证成功")
next(rw, r)
} else {
db.Close()
log.Println("Token验证失败")
rw.Header().Set("Content-Type", "application/json; charset=UTF-8")
rw.Header().Set("Access-Control-Allow-Origin", "*")
rw.Header().Set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS")
rw.Header().Set("Access-Control-Allow-Headers", "x-requested-with,content-type")
rw.WriteHeader(http.StatusUnauthorized)
rw.Write([]byte("Token is not valid"))
}
} else {
db.Close()
log.Println("Token验证失败")
rw.Header().Set("Content-Type", "application/json; charset=UTF-8")
rw.Header().Set("Access-Control-Allow-Origin", "*")
rw.Header().Set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS")
rw.Header().Set("Access-Control-Allow-Headers", "x-requested-with,content-type")
rw.WriteHeader(http.StatusUnauthorized)
rw.Write([]byte("Unauthorized access to this resource"))
}
}
而JWT功能的测试也可通过Postman进行,只需要在请求中设置Authorization即可
五、前后端耦合
在完成前后端程序的编写后即可尝试将两者共同开启,测试其功能并进一步改善程序。然而在本次项目中前后端是独立部署,因此在测试时会遇到跨域的问题。
跨源资源共享(CORS): https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
我们可以通过修改服务端返回Header来解决此问题。
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "x-requested-with,content-type")
w.WriteHeader(http.StatusOK)
除此之外,在本次项目中的一些请求可能会触发 CORS 预检请求。它会在正式发送请求前先发送一次OPTIONS类型的方法请求,以获知服务器是否允许该实际请求,从而避免跨域请求对服务器的用户数据产生未预期的影响。只有满足以下如有条件的请求才不会触发预检请求,也被称为简单请求。
- 使用下列方法之一:
- 除了被用户代理自动设置的首部字段(例如
Connection
,User-Agent
)和在 Fetch 规范中定义为 禁用首部名称 的其他首部,允许人为设置的字段为 Fetch 规范定义的 对 CORS 安全的首部字段集合。该集合为:Content-Type
的值仅限于下列三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
- 请求中的任意
XMLHttpRequestUpload
对象均没有注册任何事件监听器;XMLHttpRequestUpload
对象可以使用XMLHttpRequest.upload
属性访问。- 请求中没有使用
ReadableStream
对象。
因此我们还需要创建一个处理OPTIONS的路由
Route{
"OPTIONS",
strings.ToUpper("options"),
"/{all:[a-zA-Z0-9=\\-\\/]+}",
Options,
false,
},
//Options Options请求处理
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", "X-Requested-With,Content-Type,Authorization")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(204)
}
六、项目展示
主页:
登录界面:
书架界面:(可自由创建删除且支持分页)
书籍界面: