[服务计算] 简单 web 服务与客户端开发实战
GitHub:
项目文档: https://github.com/fentender/book-blog-doc
客户端: https://github.com/fentender/book-blog-client
服务端: https://github.com/fentender/book-blog-server
目录
前言
一、API设计
二、Swagger Editor使用
1.编写API文档
2.自动化生成项目需求文档
API Doc文档
客户端
服务端
三、BookBlog客户端
1.使用vue-cli建立项目
2. book_blog_api包的使用
3.使用mock拦截请求,模拟数据并返回
4.客户端JWT认证
5.客户端结构
四、BookBlog服务端
1.API的编写
2.Postman的使用
3.JWT的服务端实现
五、前后端耦合
六、项目展示
前言
此次项目以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 stringMethod stringPattern stringHandlerFunc http.HandlerFunc
}type Routes []Routefunc NewRouter() *mux.Router {router := mux.NewRouter().StrictSlash(true)for _, route := range routes {var handler http.Handlerhandler = route.HandlerFunchandler = 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 IDvar 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 objif(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, 1var 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 []byteif 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 stringMethod stringPattern stringHandlerFunc http.HandlerFuncrequiredToken bool
}
其次,在routers.go中修改原先的路由匹配
func NewRouter() *mux.Router {router := mux.NewRouter().StrictSlash(true)for _, route := range routes {var handler http.Handlerhandler = route.HandlerFunchandler = 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.Handlerhandler = route.HandlerFunchandler = Logger(handler, route.Name)//根据属性requiredToken判断是否需要使用中间件验证tokenif 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 swaggerimport ("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类型的方法请求,以获知服务器是否允许该实际请求,从而避免跨域请求对服务器的用户数据产生未预期的影响。只有满足以下如有条件的请求才不会触发预检请求,也被称为简单请求。
- 使用下列方法之一:
GET
HEAD
POST
- 除了被用户代理自动设置的首部字段(例如
Connection
,User-Agent
)和在 Fetch 规范中定义为 禁用首部名称 的其他首部,允许人为设置的字段为 Fetch 规范定义的 对 CORS 安全的首部字段集合。该集合为:
Accept
Accept-Language
Content-Language
Content-Type
(需要注意额外的限制)DPR
Downlink
Save-Data
Viewport-Width
Width
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)
}
六、项目展示
主页:
登录界面:
书架界面:(可自由创建删除且支持分页)
书籍界面:
[服务计算] 简单 web 服务与客户端开发实战相关推荐
- 服务计算--简单 web 服务与客户端开发实战
一.概述 利用 web 客户端调用远端服务是服务开发本实验的重要内容.其中,要点建立 API First 的开发理念,实现前后端分离,使得团队协作变得更有效率. 任务目标 选择合适的 API 风格,实 ...
- 测试开发——搭建一个简单 web服务(flask框架基础)项目实战
搭建一个简单 web服务-flask框架 一.什么是wsgi? 二.搭建一个简单 web服务 三.扩展 四.请求加参数的情况 五.安装flask 一.什么是wsgi? wsgi是webserver和a ...
- java web服务_将Java服务公开为Web服务
java web服务 本教程解决了开发人员面临的最实际的情况. 大多数时候,我们可能需要将某些现有服务公开为Web服务. 在项目生命周期的不同阶段可能会遇到这种情况. 如果这是初始阶段,那么您几乎是安 ...
- 将Java服务公开为Web服务
本教程解决了开发人员面临的最实际的情况. 大多数时候,我们可能需要将某些现有服务公开为Web服务. 在项目生命周期的不同阶段可能会遇到这种情况. 如果这是初始阶段,那么您几乎是安全的,您可以为此做好充 ...
- Openlayers 地名搜索、坐标搜索、行政区划等服务-基于天地图Web服务
Openlayers 地名搜索.坐标搜索.行政区划等服务-基于天地图Web服务 OpenLayers 教程 Openlayers 使用天地图Web服务 在线示例 OpenLayers 教程 天地图作为 ...
- SSR 学习 - 传统服务端渲染 Web 应用、客户端渲染、同构渲染、优缺点和案例演示
概述 随着前端技术栈和工具链的迭代成熟,前端工程化.模块化也已成为了当下的主流技术方案. 在这波前端技术浪潮中,涌现了诸如 React.Vue.Angular 等基于客户端渲染的前端框架. 这类框架所 ...
- golang 简单web服务
1.golang print输入 package mainimport "fmt"func main() {fmt.Printf("Hello World!\n" ...
- 无服务计算应用场景探讨及 FaaS 应用实战
简介: 无服务计算本身是一个概念或者理论模型,落地到具体技术上主要有函数即服务(FaaS)以及后端即服务(BaaS)两种形式,阿里云提供函数即服务 FaaS 产品. 作者 | 宋文龙(闻可) 阿里云 ...
- 服务计算:简单的CLI程序
Go Online传送门 Go Online 命令行程序是什么 CLI(Command Line Interface)实用程序是Linux下应用开发的基础.正确的编写命令行程序让应用与操作系统融为一体 ...
最新文章
- B站上线!DeepMind加UCL强强联手推出深度学习与强化学习进阶课程(附视频)
- CSS 居中完全指南
- pygame 移开的矩形留痕迹_Python之pygame学习矩形区域(5)
- overflowhidden用法思考
- ctb伺服驱动器说明书_青岛FANUC伺服电机364、453故障维修
- 机器学习生态全景图3.0
- Authorize.Net使用小结
- 未预期的符号 `( 附近有语法错误_苹果iOS 14.2现在提供117种新的表情符号和新的壁纸...
- “双态IT”架构下的自动化运维
- 第十三届蓝桥杯省赛 python B组复盘
- php 返回英文乱码,使用php 5时MySQL返回乱码的解决办法_php
- FPGA实现JPEG-LS图像压缩,有损无损可配置,提供工程源码和技术支持
- 手机系统更新(提示已是最新版本),怎么升级更新
- STFT和声谱图,梅尔频谱(Mel Bank Features)与梅尔倒谱(MFCCs)
- 傅里叶------傅里叶变换
- HPP结合PHP解析URL特性的SQL注入题目
- 2020年最新总结,阿里,腾讯,百度,美团,头条等技术面试题目,以及答案,专家出题人分析汇总
- 如何使用 Arduino 制作一个绘图仪
- 六年级语文计算机个人研修计划,小学教师继续教育个人研修计划
- 安卓面试测试题复习专用 2019年版本个人收集
热门文章
- Oracle同义词的好处
- yum报Exiting on user cancel
- 我的AI之路(55)--如何获取kinetics数据集和如何制作自己的kinetics数据集
- 如何最快恢复逻辑备份
- PrimoCache2.2.0汉化程序与2.0.0的交叉使用
- Lyapunov stability analysis、LaSalle’s invariance principle、Barbalat’s lemma
- ElasticSearch 全文搜索引擎
- 安装python包的时候文件夹权限报错:InvalidArchiveError(“Error with archive D:\\anaconda\\pkgs\\cudnn-8.4.1.50-h)
- 【数独个人项目】2. 拿到题目之后
- Frida Internal - Part 1: 架构、Gum 与 V8