网络应用程序,分为前端和后端两个部分。当前的发展趋势,就是前端设备层出不穷(手机、平板、桌面电脑、其他专用设备......)。
因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信。这导致API构架的流行,甚至出现"API First"的设计思想。RESTful API是目前比较成熟的一套互联网应用程序的API设计理论。
# 一、koa替代express
koa 是由 Express 原班人马打造的,致力于成为一个更小、更富有表现力、更健壮的 Web 框架。使用 koa 编写 web 应用,通过组合不同的 generator,可以免除重复繁琐的回调函数嵌套,并极大地提升错误处理的效率。koa 不在内核方法中绑定任何中间件,它仅仅提供了一个轻量优雅的函数库,使得编写 Web 应用变得得心应手。
# 1.1 koa 快速入门
koa的用法跟express基本一致,都有中间件,路由等应用
安装
npm i koa --save
使用
koa 的req 和res 都在存在ctx 参数当中,这一点跟express 的(req, res) => { ... }有细微差别
# 引入文件并实例化一个实例
const Koa = require('koa')
const app = new Koa()
# 监听所有请求并设置返回内容
app.use((cxt)=>{
cxt.body = 'Hello world!'
})
# 启动项目在本地指定端口
app.listen(3001,()=>{
console.log('服务运行成功,地址是:http://localhost:3001')
})
2
3
4
5
6
7
8
9
10
11
12
13
启动
nodemon app.js
# 1.2 koa-router
koa-router 是koa开发中非常常用的一个中间件,他可以让我们更优雅更高效的实现路由的基本功能
安装
npm i koa-router --save
引入并实例化
# 引入模块
const Router = require('koa-router')
# 正常实例化
const router = new Router()
2
3
4
5
上面的方式是正常实例化,默认无前缀
可以通过以下方式来进行带前缀的路由实例化
# 匹配前缀为 /users 的路由
const usersRouter = new Router({prefix:'/users'})
2
使用
# 配置路由
router.get('/', (ctx)=>{
ctx.body = 'hello world'
})
# 使用中间件
app.use(router.routes())
2
3
4
5
6
7
上面的示例都是在app.js
中直接引入使用的,但是在实际开发中,一般会将路由独立抽离出单独的文件
# 1.3 路由的高级用法
allowedMethods() 方法
作用:
- 响应options 的请求方法,告诉请求者该路由支持的请求方式
- 返回405(不允许)和501(没实现)两种特殊状态码
使用allowedMethods() 方法非常简单,只要使用下面中间件即可
app.user(usersRouter.allowedMethods())
响应options 的请求方法
当使用options 方法请求该路由时
请求状态会由以前的404变为200,并且会在返回的响应头中携带Allow 记录至此的请求方式
上图表示该路由仅支持HEAD 和GET 的请求方法
返回特殊状态码
当配置了allowedMethods()方法后,使用不同请求方式会返回不同状态码
- 已实现:返回正常逻辑状态码
- 未实现,但属于7个常用请求方式:返回405(不允许)
- 未实现,不属于7个常用请求方式:返回501(没实现)
- 常用:GET POST PUT PATCH DELETE HEAD OPTIONS
- 不常用:LINK UNLINK COPY PURGE
安全层模拟鉴权示例
多中间件,在koa中,router可以配合多个中间件一起使用,下面以安全层模拟鉴权为例
# 模拟鉴权中间件
const auth = async (ctx, next) =>{
if(ctx.url!=='/users'){
return ctx.throw(401)
}
await next()
}
# 使用鉴权中间件
router.get('/users', auth, (ctx)=>{
ctx.body = 'hello world'
})
2
3
4
5
6
7
8
9
10
11
12
# 1.4 不同请求方式的参数解析
路由参数
有很多时候,我们希望直接通过路由参数来指定id或其他信息,这时候我们可能会这样传递请求
PUT
http://loaclhost:3001/users/1
2
这里的最后一个参数/1
的含义是的希望修改指定id 为1的用户信息
这时候我们在编路由规则时应该这样写
router.put('/user/:id', (ctx) => {
console.log(ctx.params.id) // {id:1}
})
2
3
GET 参数
get请求的参数是通过请求地址附带的,路由之后由?key1=val1&key2=val2
形式传递参数
GET
http://loaclhost:3001/users?name=lisi&age=18
2
这里通过请求地址携带了两个参数
在编写路由逻辑时可以直接通过ctx.query
或ctx.request.query
来获取参数
router.get('/user', (ctx) => {
console.log(ctx.query)
console.log(ctx.request.query)
// 均返回 [Object: null prototype] { name: 'lisi' , age: '18'}
})
2
3
4
5
POST 参数
post 请求是通过请求体来传递参数的,相对于get 请求的明文方式更为安全,所以账号密码都是通过post 传递
$.post('http://localhost:3001',{user:'abc',pwd:'123'},(res)=>{...})
上面代码通过jquery 的ajax 请求将用户账号和密码传递到后端
对于post请求,application/x-www-form-urlencoded
和application/json
两种格式请请求无法直接解析,但是这两种请求有是我们平常用的最多的请求格式,所以需要引入一个中间件来处理
koa-bodyparser
:body请求体专用解析中间件
npm i koa-bodyparser --save
koa-bodyparser
的使用方式比express-bodyparser
更为简洁方便
# express 使用方法
const bodyParser = require('express-bodyparser')
app.use(bodyParser.json());
app.use(bodyParser.urlencoded());
# koa 使用方法
const bodyParser = require('koa-bodyparser')
app.use(bodyParser())
2
3
4
5
6
7
8
9
10
当在app实例对象使用了中间件之后,可以通过以下方式获取请求参数
router.post('/user', (ctx) => {
console.log(ctx.request.body)
// 返回 { user: 'abc' , pwd: 123}
})
2
3
4
请求头参数
任何请求头都可以携带参数,很多时候鉴权的token
就是用请求头去携带传递的
$.ajax({
...
headers: {
'Access-Token':111222333
}
...
})
2
3
4
5
6
7
在服务端直接可以通过ctx.headers
来获取传递的参数
console.log(ctx.headers['access-token']) // 返回111222333
# 二、 RESTful 的最佳响应
# 2.1 标准CRUD操作
在标准的CRUD 操作中,我们使用不同请求方式调用接口之后应该各种返回什么响应内容是RESTful 风格的最佳选择呢?
增加(Create)=> POST 请求
- 增加单项,直接返回增加的个体对象
POST:fetch('/users', data)
data = {...}
res = {...}
2
3
4
- 增加多项,返回增加的对象数组
POST:fetch('/users', data)
data = [{...}, {...}, ...]
res = [{...}, {...}, ...]
2
3
4
查询(Retrieve) => GET 请求
- 查询单项,直接返回指定的对象
GET:fetch('/users/:id')
res = {...}
2
3
- 查询多项,返回对象数组
GET:fetch('/users')
res = [{...}, {...}, ...]
2
3
更新(Update)=> PUT/PATCH 请求
- 更新替换整个,返回被更新的对象
PUT:fetch('/users/:id', data)
data = {...}
res = {...}
2
3
4
- 更新部分信息,返回对象数据
PATCH:fetch('/users/:id', data)
data = {...}
res = {...}
2
3
4
删除(Delete)=> DELETE 请求
不需要返回任何数据,只需要将响应码改为204即可
router.delete('/users/1', (ctx)=>{
...
ctx.ststus = 204
})
2
3
4
# 2.2 响应状态码
# 三、统一错误处理
一般我们在写接口时会对程序运行中所遇到的各种问题进行统一的异常处理,比较典型的错误类型有
- 运行时错误 500
- 找不到页面 404
- 逻辑错误 变量未定义就错误使用 412
下面就分别说明三种情景下的针对3个典型类型的错误处理方式
# 3.1 koa自带异常处理(不推荐)
直接请求一个不存在的接口,会返回404 状态码,并返回文本消息 “Not Found”
412 错误演示
@ /controllers/users.js clsss userCtl{ findById(ctx)=>{ if(ctx.params.id * 1 >= db.length){ // 请求的id 越界了 ctx.throw(412, '所请求的id不存在') } ctx.body = db[ctx.params.id * 1] } }
1
2
3
4
5
6
7
8
9
10此时,当请求的id 越界时,会返回412 状态码,并返回文本信息 “所请求的id不存在”
当
ctx.throw(412)
时,返回的文本信息为 “Precondition Failed” 先决条件错误500 错误演示
@ /controllers/users.js clsss userCtl{ find(ctx)=>{ a.b // a没有被定义,值为undefind ctx.body = db[ctx.params.id * 1] } }
1
2
3
4
5
6
7
8程序执行上面代码时,会遇到
undefind.b
错误,程序出错此时会返回500状态码,并返回文本 “Internal Server Error”
此时后台控制台也会打印出错误堆栈
# 3.2 自定义错误处理中间件
相比于koa 自带异常处理的返回文本错误信息不同,RESTful 最佳时间更希望返回统一的json 格式数据,方便客户端使用
编写自定义错误处理中间件
@ app.js
app.use(async (ctx, next) => {
try {
await next()
} catch(err){
ctx.status = err.status || err.statusCode|| 500
ctx.body = {
message: err.message
}
}
})
2
3
4
5
6
7
8
9
10
11
12
制造三种错误来测试
- 412都是返回状态码并返回
{message: 错误信息}
- 500错误返回状态码并返回
{message: 'a is not defind'}
- 401错误捕捉不到,只返回状态码和文本 “Not Found”
# 3.3 koa-json-error
一个非常好用的社区第三方中间件,可以返回json 格式报错信息外,可以根据配置返回其他所需要的的报错信息
安装
koa-json-error --save
引用并使用
const error = require('koa-json-error')
app.use(error()) // 放在逻辑路由的前面使用,才能起到拦截作用
2
3
测试默认
不仅500,412错误能处理得很好,返回的信息相比于自定义中间件更全面
另外404错误也能做处理了
按需定制返回格式
在开发环境下,返回的错误堆栈信息stack可以帮助我们快速定位错误
但是在生产环境下我们并不希望客户端返回这些信息,这时候我们需要通过环境变量判断返回信息
app.use(error({
postFormat: (e, {stack, ...rest}) => process.env.NODE_ENV === 'production'?rest:{stack, ...rest}
}))
2
3
这时候我们已经有了针对不同环境变量的不同处理方式
这个时候需要营造变量环境,这里需要用到一个小插件工具
cross-env
:跨平台设置环境变量工具
npm i cross-env --save --dev
安装后直接在package.json
中配置脚本
@ package.json
"script": {
"start": "cross-env NODE_ENV=production node app",
"dev": "nodemon app"
}
2
3
4
5
6
这样一来,只要运行npm run start
就代表是在生产环境下启动,这时候边不会返回stack 信息
运行npm run dev
代表在开发环境下启动,会返回stack 信息
# 3.4 koa-parameter
koa-parameter是一个优秀的参数校验中间件,可以帮我们快速校验参数是否符合我们的规则,并且如果有错误,会返回很详细的错误提示,开发中非常常用
安装
npm i koa-parameter
使用
在实例化koa时全局配置使用
@ index.js
const parameter = require('koa-parameter')
app.use(parameter(app))
2
3
4
5
在配置使用中间件时,将app 实例作为参数传递到方法中,这时候每个ctx 都新增一个verifyParams
方法,这个方法能帮我们很好的校验参数合法
router.post('/users', (ctx) => {
ctx.verifyParams({
name: {type:'string', require: true},
age: {type:'number', require: false}
})
})
2
3
4
5
6
通过配置schema 结构规则,可以很方便的规范传递过来的参数
而且,如果出现错误,他能非常清楚的报出错误原因,非常方便
# 四、NoSQL 非关系型数据库
数据库一般分为两种,一种是关系型数据库,如mysql、oracle、Sqlserser等
而非关系型数据库有文档存储型MongoDB,键值对存储型Redis等
# 4.1 NoSql 的特点
- 简单(没有原子性、一致性、隔离性等复杂的规范)
- 便于横向扩展(可以通过增加服务器来增加容量)
- 适合超大规模数据的存储
- SchemaFree可以灵活存取结构复杂的数据
# 4.2 mongoDB 数据库
mongoDB简介
- 来源于英文单词“Humongous” 庞大
- 面向文档存储的开源数据库
- 由C++编写而成
优点
- 性能好(内存计算)
- 大规模数据存储(可扩展性)
- 可靠安全(本地复制,自动故障转移)
- 方便存储数据结构(Schema Free)
设计Schema
- 分析用户模块的属性(需求分析)
- 编写用户模块的Schema(结构设计)
- 使用Schema 生成model
安装mongoDB
- 软件安装包安装
- (推荐)如果是在服务器上安装,推荐直接在宝塔面板上一键安装
# 4.3 mongooes
mongoose 是官方提供的一个封装插件,可以让我们更方便的操作mongoDB 数据库
安装
npm i mongoose --save
具体的使用方法请参考《官网文档》
# 五、用户权限控制
当前用的比较多的两种用户权限控制方式有两种,一种是cookie + session 的方式,另一种是JsonWebToken(简称:JWT) 的方式
# 5.1 session和jwt方式的比较
session 的优势
- 相比于jwt,最大的优势就是在于可以主动清除session,做到随时切断权限
- session 保存在服务器端,相对来说比较安全
- 结合cookie 使用,较为灵活,兼容性好
session 的劣势
- cookie + session 在跨域场景表现的并不好
- 如果分布式部署,需要做多机共享session 机制
- 基于cookie 的机制很容易被CSRF (仿照cookie 攻击)
- 查询session 信息可能会有数据库查询操作
什么是jwt
JsonWebToken 是一个开放性标准(RFC7519)
定义了一种独立切紧凑的方式,可以将各方的信息作为json 对象进行安全传输
该信息可以验证和信任,因为是经过数字签名的
jwt 是由三个部分构成的
- 头部(Header)
- 有效载荷(Payload)
- 签名(Signature)
# 5.2 JsonWebToken
安装
npm i jsonwebtoken
签名
const jwt = require('jsonwebtoken')
const token = jwt.sign({name:'lisi'},'secret')
2
3
使用jwt 的sign 方法可以对json 数据进行加密,sign 传入两个参数,第一个参数是需要加密的 json信息,第二个参数是一个自定义的秘钥字符串 ,还可以传入第三个参数来定义过期时间
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibGlzaSIsImlhdCI6MTU5MzI2MDY5M30.b97nAQnmjPnTU2CKn4YCsGNGzrYH8We2zQn63AhaWWI'
上面一串即为加密后的字符串
这个时候服务端一般会将token 返回给客户端,由客户端去保存
验证
在需要鉴权的操作时将token 携带在请求头上传到服务端,服务端可以通过解密操作去验证用户的信息
方法一:jwt.decode()
(不推荐)
jwt.decode('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibGlzaSIsImlhdCI6MTU5MzI2MDY5M30.b97nAQnmjPnTU2CKn4YCsGNGzrYH8We2zQn63AhaWWI')
// { name: 'lisi', iat: 1593260693 }
2
3
这种方法只能进行简单的base64解密,如果token在客户端中被篡改了,也无法验证出来
方式二:jwt.verify()
(推荐)
jwt.verify('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibGlzaSIsImlhdCI6MTU5MzI2MDY5M30.b97nAQnmjPnTU2CKn4YCsGNGzrYH8We2zQn63AhaWWI', 'secret')
// { name: 'lisi', iat: 1593260693 }
2
3
这种方式需要提供token 和生成时自定义的秘钥字符串,两个中有任何一个被篡改,程序都会报错
# 5.3 用户认证和授权
用户在进行一些敏感操作的时候,往往还需要验证用户的身份信息,判断用户是否有权限进行操作,比如修改信息,删除信息这些操作
自定义认证中间件
@ app/routers/users.js
const jsonwebtoken = require('jsonwebtoken')
const {
secret
} = require('../config')
const auth = async (ctx, next) => {
const {
authorization = ''
} = ctx.request.header
const token = authorization.replace('Bearer ', '')
try {
const user = jsonwebtoken.verify(token, secret)
ctx.state.user = user
} catch (error) {
ctx.throw(401, error.message)
}
await next()
}
router.patch('/:id', auth, checkOwner, update)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
自定义授权中间件
@ app/routers/users.js
const checkOwner = async (ctx, next) => {
if (ctx.params.id !== ctx.state.user._id) {
ctx.throw(403, '权限不足')
}
await next()
}
router.patch('/:id', auth, checkOwner, update)
2
3
4
5
6
7
8
9
10
# 5.4 koa-jwt
koa-jwt 插件中间内置了jsonwebtoken 中间件,同时,对于上一节所说的用户认证和授权有更简便更合理的处理方式
安装
npm i koa-jwt