乐趣区

关于前端:Koa2从零到脚手架

什么是 Koa2

由 Express 原班人马打造的新生代 Node.js Web 框架,它的代码很简略,没有像 Express 那样,提供路由、动态服务等等,它是为了解决 Node 问题(简化了 Node 中操作)并取代之,它自身是一个简略的中间件框架,须要配合各个中间件能力应用

文档

中文文档 (家养)

最简略的 Koa 服务器

const Koa = require('koa')

const app = new Koa()

app.use((ctx) => {ctx.body = 'Hello World'})

app.listen(3000, () => {console.log('3000 端口已启动')
})

洋葱模型

这是 Koa 的洋葱模型

看看 Express 的中间件是什么样的:

申请(Request)间接顺次贯通各个中间件,最初通过申请处理函数返回响应(Response)。再来看看 Koa 的中间件是什么样的:

能够看出,Koa 中间件不像 Express 中间件那样在申请通过了之后就实现本人的使命;相同,中间件的执行清晰地分为两个阶段。咱们看看 Koa 中间件具体是什么样的

Koa 中间件的定义

Koa 的中间件是这样一个函数:

async function middleware(ctx, next) {
    // 先做什么
    await next()
    // 后做什么
}

第一个参数是 Koa Context,也就是上图中贯通中间件和申请处理函数的绿色箭头所传递的内容,外面封装了申请体和响应体(实际上还有其余属性),别离能够通过 ctx.requestctx.response 来获取,一下是一些罕用的属性:

ctx.url // 相当于 ctx.request.url
ctx.body // 相当于 ctx.response.boby
ctx.status // 相当于 ctx.response.status

更多 Context 属性请参考 Context API 文档

中间件的第二个参数便是 next 函数:用来把控制权转交给下一个中间件。但它与 Express 的 next 函数实质的区别在于,Koa 的 next 函数返回的是一个 Promise,在这个 Promise 进入实现状态(Fulfilled)后,就会去执行中间件中第二个阶段的代码。具体能够看这一篇—— 手写 koa2

有哪些常见的中间件

路由中间件——koa-router 或 @koa/router

下载 npm 包

npm install koa-router --save

有些教程应用 @koa/router,现如今这两个库由同一个人保护,代码也统一。即 koa-router === @koa/router(写自 2021 年 8 月 23 日)

NPM 包地址:koa-router、@koa/router

如何应用

在根目录下创立 controllers 目录,用来寄存控制器无关的代码。首先是 HomeController,创立 controllers/home.js,代码如下:

class HomeController {static home(ctx) {ctx.body = 'hello world'}
  static async login(ctx) {ctx.body = 'Login Controller'}
  static async register(ctx) {ctx.body = 'Register Controller'}
}

module.exports = HomeController;

实现路由

再创立 routes 文件夹,用于把控制器挂载到对应的路由下面,创立 home.js

const Router = require('koa-router')
const {home, login, register} = require('../controllers/home')

const router = new Router()

router.get('/', home)
router.post('/login', login)
router.post('/register', register)

module.exports = router

注册路由

在 routes 中创立 index.js,当前所有的路由都放入 routes,咱们创立 index.js 的目标是为了让构造更加参差,index.js 负责所有路由的注册,它的兄弟文件负责各自的路由

const fs = require('fs')
module.exports = (app) => {fs.readdirSync(__dirname).forEach((file) => {if (file === 'index.js') {return}
    const route = require(`./${file}`)
    app.use(route.routes()).use(route.allowedMethods())
  })
}

注:allowedMethods 的作用

  1. 响应 option 办法,通知它所反对的申请办法
  2. 相应地返回 405(不容许)和 501(没实现)

注:能够看到 @koa/router 的应用形式基本上与 Express Router 保持一致

引入路由

最初咱们须要将 router 注册为中间件,新建 index.js,编写代码如下:

const Koa = require('koa')
const routing = require('./routes')

// 初始化 Koa 利用实例
consr app = new Koa()

// 注册中间件
// 相应用户申请
routing(app)

// 运行服务器
app.listen(3000);

应用 postman 测试一下

其余中间件

  • koa-bodyparser ——申请体解析
  • koa-static —— 提供动态资源服务
  • @koa/cors —— 跨域
  • koa-json-error —— 处理错误
  • koa-parameter —— 参数校验
cnpm i koa-bodyparser -S 
cnpm i koa-static -S
cnpm i @koa/cors -S
cnpm i koa-json-error -S
cnpm i koa-parameter -S
const path = require('path')
const Koa = require('koa')
const bobyParser = require('koa-bodyparser')
const koaStatic = require('koa-static')
const cors = require('@koa/cors')
const error = require('koa-json-error')
const parameter = require('koa-parameter')
const routing = require('./routes')

const app = new Koa()

app.use(
  error({postFormat: (e, { stack, ...rest}) =>
      process.env.NODE_ENV === 'production' ? rest : {stack, ...rest},
  }),
)
app.use(bobyParser())
app.use(koaStatic(path.join(__dirname, 'public')))
app.use(cors())
app.use(parameter(app))
routing(app)

app.listen(3000, () => {console.log('3000 端口已启动')
})

实现 JWT 鉴权

JSON Web Token(JWT)是一种风行的 RESTful API 鉴权计划

先装置相干的 npm 包

cnpm install koa-jwt jsonwebtoken -S

创立 config/index.js,用来寄存 JWT Secret 常量,代码如下:

const JWT_SECRET = 'secret'

module.exports = {JWT_SECRET,}

有些路由咱们心愿只有已登录的用户才有权查看(受爱护路由),而另一些路由则是所有申请都能够拜访(不受爱护的路由)。在 Koa 的洋葱模型中,咱们能够这样实现:

能够看出,所有的申请都能够间接拜访未受爱护的路由,然而受爱护的路由都放在 JWT 中间件的前面,咱们须要再创立几个文件来做 JWT 的试验

咱们晓得,所谓的用户(users)是个最常见的须要鉴权的路由,所以咱们当初 controllers 中创立 user.js,写下如下代码:

class UserController {static async create(ctx) {
    ctx.status = 200
    ctx.body = 'create'
  }
  static async find(ctx) {
    ctx.status = 200
    ctx.body = 'find'
  }
  static async findById(ctx) {
    ctx.status = 200
    ctx.body = 'findById'
  }
  static async update(ctx) {
    ctx.status = 200
    ctx.body = 'update'
  }
  static async delete(ctx) {
    ctx.status = 200
    ctx.body = 'delete'
  }
}

module.exports = UserController

注册 JWT 中间件

用户的增删改查都安顿上了,语义很显著了,其次咱们在 routes 文件中创立 user.js,这里展现与 users 路由相干的代码:

const Router = require('koa-router')
const jwt = require('koa-jwt')
const {
  create,
  find,
  findById,
  update,
  delete: del,
} = require('../controllers/user')

const router = new Router({prefix: '/users'})
const {JWT_SECRET} = require('../config/')

const auth = jwt({JWT_SECRET})

router.post('/', create)
router.get('/', find)
router.get('/:id', findById)
router.put('/:id', auth, update)
router.delete('/:id', auth, del)

module.exports = router

综上代码,routes 文件下的 home.js 都不须要 JWT 中间件的爱护,user.js 中的 更新和删除须要 JWT 的爱护

测试一下,能看出 JWT 曾经起作用了

咱们到目前为止,实现了对 JWT 的验证,然而验证的前提是先签发 JWT,怎么签发,你登录的时候我给你一个签好名的 token,要更新 / 删除时在申请头中带上 token,我就能校验 …

这里牵扯到登录,咱们先暂停一下,先补充数据库的常识,让我的项目更加残缺

Mongoose 退出战场

如果要做一个残缺的我的项目,数据库是必不可少的,与 Node 匹配的较好的是 NoSql 数据库,其中以 Mongodb 为代表,当然如果咱们要应用这一数据库,须要依照相应的库,而这个库就是 mongoose

下载 mongoose

cnpm i mongoose -S

连贯及配置

config/index.js 中增加 connectionStr 变量,代表 mongoose 连贯的数据库地址

const JWT_SECRET = 'secret'
const connectionStr = 'mongodb://127.0.0.1:27017/basic'

module.exports = {
  JWT_SECRET,
  connectionStr,
}

创立 db/index.js

const mongoose = require('mongoose')
const {connectionStr} = require('../config/')

module.exports = {connect: () => {
    mongoose.connect(connectionStr, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    })

    mongoose.connection.on('error', (err) => {console.log(err)
    })

    mongoose.connection.on('open', () => {console.log('Mongoose 连贯胜利')
    })
  },
}

进入主文件 index.js,批改配置并启动

...
const db = require('./db/')
...

db.connect()

启动服务 npm run serve,即 nodemon index.js,能看出 mongoose 曾经连贯胜利了

创立数据模型定义

在根目录下创立 models 目录,用来存放数据模型定义文件,在其中创立 User.js,代表用户模型,代码如下:

const mongoose = require('mongoose')

const schema = new mongoose.Schema({username: { type: String},
  password: {type: String},
})

module.exports = mongoose.model('User', schema)

具体能够看看 Mongoose 这篇文章,这里咱们就看行为,以上代码示意建设了一个数据对象,供操作器来操作数据库

在 Controller 中操作数据库

而后就能够在 Controller 中进行数据的增删改查操作。首先咱们关上 constrollers/user.js

const User = require('../models/User')

class UserController {static async create(ctx) {const { username, password} = ctx.request.body
    const model = await User.create({username, password})
    ctx.status = 200
    ctx.body = model
  }
  static async find(ctx) {const model = await User.find()
    ctx.status = 200
    ctx.body = model
  }
  static async findById(ctx) {const model = await User.findById(ctx.params.id)
    ctx.status = 200
    ctx.body = model
  }
  static async update(ctx) {const model = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
    ctx.status = 200
    ctx.body = model
  }
  static async delete(ctx) {await User.findByIdAndDelete(ctx.params.id)
    ctx.status = 204
  }
}

module.exports = UserController

以上代码中,

  • User.create({xxx}):在 User 表中创立一个数据
  • User.find():查看所有的 User 表中的数据
  • User.findById(id):查看 User 表中的其中一个
  • User.findByIdAndUpdate(id, body):更新 User 表中的其中一个数据
  • User.findByIdAndDelete(id):删除 User 表中的其中一个数据

以上就是对数据库的 增删改查

加盐

这个咱们须要对明码进行一下加密,无它,平安。

进数据库一查,就能看到明码,这阐明数据对开发人员是公开的,开发人员能够拿着用户的账号密码做任何事,这是不被容许的

下载 npm 包——bcrypt

cnpm i bcrypt --save

咱们返回 models/User.js 中,对其进行革新

...
const schema = new mongoose.Schema({username: { type: String},
  password: {
    type: String,
    select: false,
    set(val) {return require('bcrypt').hashSync(val, 10)
    },
  },
})
...

增加 select:false 不可见,set(val) 对值进行加密,咱们来测试一下

能看到 password 被加密了,即便在数据库里,也看不出用户的明码,那用户输出的明码难道输出这么一串明码,显然不是,用户要是输出的话,咱们要对其进行验证,例如咱们的登录

咱们进入 constrollers/home 文件中,对其进行革新,

...
class HomeController {static async login(ctx) {const { username, password} = ctx.request.body
    const user = await User.findOne({username}).select('+password')
    const isValid = require('bcrypt').compareSync(password, user.password)
    ctx.status = 200
    ctx.body = isValid
  }
  ...
}
  • User.findOne({username}) 能查到到没有 password 的数据,因为咱们人为的把 select 设为 false,如果要看,加上 select(‘+password’) 即可
  • require(‘bcrypt’).compareSync(password, user.password) 将用户输出的明文明码和数据库中的加密明码进行验证,为 true 是正确,false 为明码不正确

回到 JWT

在 Login 中签发 JWT Token

咱们须要提供一个 API 端口让用户能够获取到 JWT Token,最合适的当然是登录接口 /login,关上 controllers/home.js,在 login 控制器中实现签发 JWT Token 的逻辑,代码如下:

const jwt = require('jsonwebtoken')
const User = require('../models/User')

const {JWT_SECRET} = require('../config/')

class HomeController {static async login(ctx) {const { username, password} = ctx.request.body

    // 1. 依据用户名找用户
    const user = await User.findOne({username}).select('+password')
    if (!user) {
      ctx.status = 422
      ctx.body = {message: '用户名不存在'}
    }
    // 2. 校验明码
    const isValid = require('bcrypt').compareSync(password, user.password)
    if (isValid) {const token = jwt.sign({ id: user._id}, JWT_SECRET)
      ctx.status = 200
      ctx.body = token
    } else {
      ctx.status = 401
      ctx.body = {message: '明码谬误'}
    }
  }
  ...
}

login 中,咱们首先依据用户名(申请体中的 name 字段)查问对应的用户,如果该用户不存在,则间接返回 401;存在的话再通过 (bcrypt').compareSync 来验证申请体中的明文明码 password 是否和数据库中存储的加密明码是否统一,如果统一则通过 jwt.sign 签发 Token,如果不统一则还是返回 401。

在 User 控制器中增加访问控制

Token 的中间件和签发都搞定之后,最初一步就是在适合的中央校验用户的 Token,确认其是否有足够的权限。最典型的场景便是,在更新或删除用户时,咱们要 确保是用户自己在操作。关上 controllers/user.js

const User = require('../models/User')

class UserController {
  ...
  static async update(ctx) {
    const userId = ctx.params.id
    if (userId !== ctx.state.user.id) {
      ctx.status = 403
      ctx.body = {message: '无权进行此操作',}
      return
    }
    const model = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
    ctx.status = 200
    ctx.body = model
  }
  static async delete(ctx) {
    const userId = ctx.params.id

    if (userId !== ctx.state.user.id) {
      ctx.status = 403
      ctx.body = {message: '无权进行此操作'}
      return
    }

    await User.findByIdAndDelete(ctx.params.id)
    ctx.status = 204
  }
}

module.exports = UserController

增加了一些用户并登录,将 Token 增加到申请头中,应用 DELETE 删除用户,能看到 状态码变成 204,删除胜利

断言解决

在做登录时、更新用户信息、删除用户时,咱们须要 if else 来判断,这看起来很蠢,如果咱们能用断言来解决,代码在看上去会优雅很多,这个时候 http-assert 就进去了

// constrollers/home.js
...
const assert = require('http-assert')


class HomeController {static async login(ctx) {const { username, password} = ctx.request.body
    // 1. 依据用户名找用户
    const user = await User.findOne({username}).select('+password')
    // if (!user) {
    //   ctx.status = 401
    //   ctx.body = {message: '用户名不存在'}
    // }
    assert(user, 422, '用户不存在')
    // 2. 校验明码
    const isValid = require('bcrypt').compareSync(password, user.password)
    assert(isValid, 422, '明码谬误')
    const token = jwt.sign({id: user._id}, JWT_SECRET)
    ctx.body = {token}
  }
   ...
}

同理,解决 controllers/user

...
  static async update(ctx) {
    const userId = ctx.params.id
    assert(userId === ctx.state.user.id, 403, '无权进行此操作')
    const model = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
    ctx.status = 200
    ctx.body = model
  }
  static async delete(ctx) {
    const userId = ctx.params.id
    assert(userId === ctx.state.user.id, 403, '无权进行此操作')
    await User.findByIdAndDelete(ctx.params.id)
    ctx.status = 204
  }
...

代码看起来就是整洁清新

参数校验

之前咱们加了一个中间件——koa-parameter,咱们当初只是注册了这个中间件,然而未应用,咱们在创立用户时须要判断用户名和明码的数据类型为 String 类型且必填,进入 controllers/user.js 增加代码如下:

...
class UserController {static async createUser(ctx) {
    ctx.verifyParams({username: { type: 'string', required: true},
      password: {type: 'string', required: true},
    })
    const {username, password} = ctx.request.body
    const model = await User.create({username, password})
    ctx.status = 200
    ctx.body = model
  }
  ...
}

Github 地址:koa-basic

参考资料

一杯茶的工夫,上手 Koa2 + MySQL 开发

退出移动版