编写RESTful API
- Representational State Transfer翻译古来就是 “ 表现层状态转化 ”,他是一种互联网软件的架构原则,因为符合REST风格的Web API设计,就称它为RESTful API
- RESTful是目前最流行的API规范,适用于 Web 就扣规范的设计,让接口易读,且含义清晰,设计易于理解和使用的API,并且借助 Docker API 的实践说明
URL设计
动词 + 宾语
它的核心思想就是客户端发出的数据指令都是 [ 动词 + 宾语 ] 的结构,比如 GET /articles 这个命令,GET 是动词,/articles 是宾语
动词通常来说就是五种 HTTP 方法,对应我们业务接口的 CRUD 操作,而宾语就是我们要操作的资源,可以理解成面向资源设计,我们所关注的数据就是资源
- GET:读取资源
- POST:新建资源
- PUT:更新资源
- PATCH:资源部分数据更新
- DELETE:删除资源
正确的例子
- GET /zoos:列出所有动物园
- POST /zoos:新建一个动物园
- GET /zoos?zoos_id=1:获取某个指定动物园的信息
- PUT /zoos?zoos_id=1:更新某个动物园的信息 ( 提供该动物园全部信息 )
- PATCH /zoos?zoos_id=1:更新某个动物园的信息 ( 提供该动物园部分信息 )
- DELETE /zoos?zoos_id=1:删除某个动物园
- GET /zoos?zoos_id=1&animals_name=xxx:列出某个指定动物园的所有动物
- DELETE /zoos?zoos_id=1&animals_name=xxx:删除某个指定动物园的指定动物
动词的覆盖
有些客户端智能使用GET和POST这两种方法,服务器继续接受POST模拟为他三个方法(PUT、PATCH、DELETE)
这时,客户端发出的HTTP请求,要加上 X-HTTP-Mthod-Override 属性,告诉服务器应该使用哪一个动词来覆盖 POST 方法
宾语必须是名词
API的url,用HTTP动词作用的对象,所以应该是名词,例如 /books 这个 URL 就是正确的,而下面的URL 不是名词,都是错误写法
GET /getAllUsers?name=jl POST /createUser POST /deleteUser
复数名词
URL 是名词,通常操作的数据多数是一个集合,比如 GET /books,所以使用复数
避免出现多级URL
有时候需要操作的资源可能是有多个层级,因此很容易写多级URL,比如获取某个作者某个分类的文章
GET /authors/2/vategories/2 获取作者ID =2 分类=2 的文章
这种不利于扩展,语义也不清晰
更好的方法是,除了第一级,其他级别都是通过查询字符串表达,查询已发布的文章:
错误写法:GET /artichels/published
正确写法:GET /artichels?published=true
过滤信息 ( Filtering )
如果数据条数过多,服务器不可能将它们都返回给用户,API应该提供参数,过滤返回的结果
下面是一些常见的参数
- ?limit=10:指定返回数据的条数
- ?offset=10:指定返回数据开始的位置
- ?page=2&per_page=100:指定第几页,以及每页能容纳的数据条数
- ?sortby=name&order=asc:指定返回结果的排序属性依据,以及正序或倒序,倒序为dsc
- ?animal_type_id:指定筛选条件
参数的设计允许存在冗余,即允许API路劲和URL参数偶尔有重复,比如,GET /zoos/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的,推荐后者,避免出现多级URL
状态码
五个类别
客户端的请求,服务器都必须响应,包含HTTP状态码和数据
HTTP状态码是一个三位数,分成五个类别:
- 1xx:相关信息 - 前端不常见
- 2xx:操作成功
- 3xx:重定向
- 4xx:客户端错误
- 5xx:服务器错误
2xx状态码
200状态码表示操作成功,但是不同的方法可以返回更精确的状态码
- GET:200 OK
- POST:201 Created
- PUT:200 OK
- PATCH:200 OK
- DELETE:204 No Content
4xx状态码
4xx状态码表示客户端错误,主要有以下几种
- 400 Bad Request:服务器不理解客户端的请求,未做任何处理
- 401 Unauthorized:用户未提供身份验证凭据,或者没有通过身份验证
- 403 Forbidden:用户通过了身份验证,但是不具有访问资源所需的权限
- 404 Not Found:所请求的资源不存在,或不可用
- 405 Methor Not Allowed:用户已通过身份验证,但是所用的http方法不在用户的权限之内
- 410 Gone:所请求的资源已从这个地址转移,不再可用
- 415 Unsupported Media Type:客户端要求的返回格式不支持,比如,API只能返回JSON格式,但是客户端要求返回XML格式
- 422 Unprocessable Entity:客户端上传的附件无法处理,导致请求失败
- 429 Too Many Requests:客户端的请求次数超过限额
5xx状态码
5xx状态码表示服务器错误,一般来说,API不会向用户透露服务器详细信息,所以只要两个状态码就够了
- 500 Internal Server Error:客户端请求有效,服务器处理时发生了意外,代码编写出现了问题
- 503 Serveice Unavailable:服务器无法处理请求,一般用于网站维护状态
服务器响应
不要返回纯文本
API返回的数据格式,不应该是纯文本,而应该是一个JSON对象,因为需要返回标准的结构化数据,所以,服务器回应的HTTP headers 要设置 Content-Type: application/json
客户端请求时也要明确告诉服务器,可以接受JSON格式,即请求的HTTP headers 要设置
ACCEPT: application/json
发生错误时,不要返回200状态码
有一种不恰当的做法是,即使发生错误,也返回200状态码,把错误信息放在数据体里面,比如下面的例子:
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "fail",
"msg": "错误"
}
上面的代码中,解析数据体后才能得知操作失败
这种做法实际上取消了状态码,这是完全不可取的,正确的做法是,状态码反映发生的错误,具体错误信息放在数据体里面返回,比如下面的例子:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"status": "fail",
"msg": "错误"
}
API示例
// 模拟数据,将来要从数据库获取
const users = [
{
id: 1,
name: 'tom'
},
{
id: 2,
name: 'jack'
}
]
// 用户信息管理api实现query查询
router.get('/', (ctx, next) => {
console.log('GET /users')
const { name } = ctx.query // ?name=tom
// 从数据库中获取数据
let data = users
if (name) {
data = users.filter(u => u.name === name)
}
ctx.body = { ok: 1, data }
})
// params获取
router.get('/:id', (ctx, next) => {
console.log('GET /user/:id')
const { id } = ctx.params // user/1
console.log(id)
const data = users.find(u => u.id == id)
ctx.body = { ok:1, data }
})
// post请求增加
router.post('/', (ctx, next) => {
console.log('POST /user')
const { body: user } =ctx.request
user.id = users.length + 1
users.push(user)
console.log(users)
ctx.body = { ok: 1 }
})
// put请求更改整条数据
router.put('/', (ctx, next) => {
console.log('PUT /user')
const { body: user } = ctx.request
// findOneAndUpdate()
// 修改数据
const idx = users.findIndex(u => u.id == user.id)
if (idx > -1) {
users[idx] = user
}
consoe.log(users)
ctx.body = { ok: 1 }
})
router.delete('/:id'. (ctx, next) => {
const { id } = ctx.params
// 删除数据
const idx = users.findIndex(u => u.id == id)
if (idx > -1) {
users.splice(idx, 1)
}
console.log(users)
ctx.body = { ok: 1 }
})
Koa脚手架
// 全局下载脚手架
npm i koa2-generator -g
// 创建koa2服务器
koa2 --hbs
// 安装依赖
npm install
会立即生成app
修改配置
将start命令改为 nodemon app.js启用实时监听 修改配置map: {'hbs': 'hbs'}为 map: {'hbs': 'handlebars'}否则会报错
// 运行服务器
npm start
打开页面
解决跨域
安装跨域中间件
npm i koa2-cors
导入
const koa = require('koa')
const cors = require('koa2-cors')
const app = new Koa()
app.use(cors())
文件上传
安装文件上传模块
npm i koa-multer -s
配置 ./routes/users.js
const multer = require('koa-multer')
const upload = multer({ dest: './public/images'})
router.post('/upload', upload.single('avater'), (ctx, next) => {
// ctx.req.file 是avatar文件信息
// ctx.req.body 文本域数据(如果存在的话
// ctx.body配置返回给客户端的信息
ctx.body = {
ok: 1,
msg: '文件上传成功'
}
})
实例
/public/upload.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<title>上传文件</title>
<style>
img{
width: 60vw;
}
</style>
</head>
<body>
<div id="app">
<el-upload name='avatar' action="/users/upload/" :show-file-list="true" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
<img v-if="imageUrl" :src="imageUrl">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
new Vue({
el: "#app",
data() {
return {
imageUrl: ''
}
},
methods: {
handleAvatarSuccess(req, file) {
this.imageUrl = URL.createObjectURL(file.raw)
},
beforeAvatarUpload(file) {
const isJPG = file.type === 'image/jpeg'
const isLt2M = file.size / 1024 / 1024 < 2
if(!isJPG) {
this.$message.error('上传图片只能是JPG格式!')
}
if(!isLt2M) {
this.$message.error('上传头像图片大小不能超过2M')
}
return isJPG && isLt2M
}
}
})
</script>
</body>
</html>
/router/users.js
const multer = require('koa-multer')
// 配置磁盘存储
const storage = multer.diskStorage({
// 文件保存路径
destination: function (req, file, cb) {
cb(null, 'public/images/')
},
// 修改文件名称
filename: function (req, file, cb) {
var fileFormat = (file.originalname).split(".")// 以.分割成数组,数组的最后一项就是扩展名
cb(null, Date.now() + "." + fileFormat[fileFormat.length - 1])
}
})
// 加载配置
const upload = multer({ storage: storage})
// single()方法单文件上传,array()方法为多文件上传,none() 方法只处理文字数据
router.post('/users/upload',upload.single('avatar'), (ctx, next) => {
// ctx.req.file 是avatar的文件信息
// ctx.req.body 文本域数据,如果存在
console.log(ctx.req.file);
// ctx.body 返回给客户端状态和真实地址
ctx.body = {
ok: 1,
imgSrc: "localhost:3000/images/" + ctx.req.file.filename
}
})
ctx.req.file:
{
fieldname: 'avatar',
originalname: '324190.jpg',
encoding: '7bit',
mimetype: 'image/jpeg',
destination: 'public/images/',
filename: '1635433095851.jpg',
path: 'public\\images\\1635433095851.jpg',
size: 8292164
}
表单验证
表单验证有很多不同的模块,这里演示 koa-bouncer
npm i koa-bouncer -s
app.js 注册koa-bouncer,为了给ctx提供一些帮助方法
const bouncer = require('koa-bouncer')
app.use(bouncer.middleware())
/router/users.js
router.post('/users', async (ctx, next) => {
// ctx.request.body
// uname pwd1 pwd2
try {
ctx.validateBody('uname')
.required('用户名是必须的') // 只要求uname不为空
.isString() // 确保数据可以转换为字符串
.trim() // 去除字符串头尾空格
.isLength(6,12, '用户名必须是6~12位')
ctx.validateBody('email')
.optional()
.isString()
.trim()
.isEmail('非法的邮箱格式')
ctx.validateBody('pwd1')
.required('密码是必填项')
.isString()
.isLength(6,16,'密码长度必须是6~16位字符')
ctx.validateBody('pwd2')
.required('密码确认为必填项')
.isString()
.eq(ctx.vals.pwd1, '两次密码不一致')
// 校验数据库是否存在相同值
ctx.validateBody('uname')
.check(await db.findUserByUname(ctx.vals.uname), 'Username taken')
// ctx.validateBody('uname').check('tom', '用户名已存在')
// 如果代码执行到这里,校验通过
// 校验器会用净化后的值填充 ctx.vals 对象
console.log(ctx.vals);
ctx.body = {
ok: 1
}
} catch (error) {
// 处理验证错误
if(error instanceof bouncer.ValidationError) {
ctx.status = 400
ctx.body = '校验失败' + error.message
return
}
throw error
}
})
form.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>表单验证</title>
</head>
<body>
<!-- 传统的表单 -->
<form action="/users" method="post">
<input type="text" name="uname" label="用户名: "><br>
<input type="password" name="pwd1" label="密码: "><br>
<input type="password" name="pwd2" label="确认密码: "><br>
<input type="submit" value="提交">
</form>
</body>
</html>
图形验证码
安装trek-captcha
npm i trek-captcha -s
/router/index.js
const catcha = requie('trek-chatcha')
router.get('/captcha',async (ctx, next) => {
const { token, buffer } = await captcha({size: 4})
// token的作用 前端输入完验证码与此时的token做对比
ctx.body = buffer
})
vue组件
<template>
<img src="/captcha" ref="captcha" alt="">
<button @click="changeCaptcha">换一张</button>
</template>
//------------------------
<script>
methods: {
changeCaptcha() {
// 通过不同的时间差别发送请求,取新的验证码
this.$refs.captcha.src = '/captcha?r=' + Date.now()
}
</script>
之后再通过前端post请求获取到输入的字符对比服务端的token来确定输入是否正确
云短信
短信服务需要网站信息认证,无法测试