# koa2 框架
- express 中间件是异步回调,koa2 原生支持 async/await
- egg.js 也是基于 koa2;
# async/await 语法
- await 后面可以追加 promise 对象,获取 resolve 的值
- await 必须包裹在 async 函数里面
- async 函数执行返回的也是一个 promise 对象
- try-catch 截获 promise 中 reject 的值
function getFileContent(fileName) {
const promise = new Promise((resolve, reject) => {
// 读取文件的操作
})
return promise
}
// promise实现
getFileContent('a.json').then(aData => {
console.log(aData)
return getFileContent(aData.next)
}).then(bData => {
console.log(bData)
return getFileContent(bData.next)
}).then(cData => {
console.log(cData)
})
// 同步的写法
async function readFileData() {
// promise的resolve里面的内容直接赋值给了aData
const aData = await getFileContent('a.json')
console.log('a data', aData)
const bData = await getFileContent(aData.next)
console.log('b data', bData)
const cData = await getFileContent(bData.next)
console.log('c data', cData)
}
readFileData()
// 下面的这种方法也是可以的 async 函数执行的时候返回的还是 promise
async function readData() {
const aData = await getFileContent('a.json')
return aData
}
async function test() {
const aData = await readData()
console.log(aData)
}
test()
# 使用 koa2
首先安装:npm install koa-generator -g, koa2 脚手架; 新建项目:koa2 project-koa2
# app.js 文件
const Koa = require('koa')
const app = new Koa()
const views = require('koa-views')
const json = require('koa-json')
const onerror = require('koa-onerror')
// post data
const bodyparser = require('koa-bodyparser')
// 日志
const logger = require('koa-logger')
// 实现登录
const session = require('koa-generic-session')
const redisStore = require('koa-redis')
const path = require('path')
const fs = require('fs')
const morgan = require('koa-morgan')
const index = require('./routes/index')
const users = require('./routes/users')
const blog = require('./routes/blog')
const user = require('./routes/user')
// 引入redis 配置
const { REDIS_CONF } = require('./config/db')
// error handler
onerror(app)
// middlewares
// 处理 postData,接收处理很多格式
app.use(bodyparser({
enableTypes:['json', 'form', 'text']
}))
// 处理 post 请求的数据,获取 content-type=applica/json 格式的数据,可以通过 req.body 获取数据
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))
app.use(views(__dirname + '/views', {
extension: 'pug'
}))
// logger --中间件是一个异步函数
// 所谓洋葱圈模型,是一个中间件就是一层,中间件开始就是第一层的开始,
// 如果遇到next函数,就会跳转到第二层,等到依次执行下去之后,就开始从最后一层next函数后面继续执行,直到
// 第一层执行到next后面的代码,整个结束
// 碰到next() 方法就会跳转到下一个中间件去执行。
app.use(async (ctx, next) => {
const start = new Date()
await next()
// 等待下一个中间件执行完之后再继续执行下面的代码
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})
// 日志记录
const ENV = process.env.NODE_ENV
// https://github.com/expressjs/morgan 查看打印日志的格式
if(ENV != 'production') {
// 对日志进行配置
app.use(morgan('dev', {
// 标准输出,直接输出到控制台
stream: process.stdout
}));
} else {
// 线上环境使用,打印的日志比较详细--将日志写到access.log文件当中
const logFileName = path.join(__dirname, 'logs', 'access.log')
const writeStream = fs.createWriteStream(logFileName, {
flags: 'a'
})
app.use(morgan('combined', {
stream: writeStream
}))
}
// session 配置
app.keys = ['jiegiser_#812*']
app.use(session({
// 配置 cookie
cookie: {
path: '/',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000
},
// 配置 redis
store: redisStore({
// all: '127.0.0.1:6379', // 写死本地的 redis
all: `${REDIS_CONF.host}:${REDIS_CONF.port}`
})
}))
// routes
app.use(index.routes(), index.allowedMethods())
app.use(users.routes(), users.allowedMethods())
// 注册blog路由
app.use(blog.routes(), blog.allowedMethods())
app.use(user.routes(), user.allowedMethods())
// error-handling
app.on('error', (err, ctx) => {
console.error('server error', err, ctx)
});
module.exports = app
可以使用 corss-env 来设置环境变量:npm install cross-env --save-dev ; 然后在命令中添加:cross-env NODE_ENV=dev:
"scripts": {
"start": "node bin/www",
"dev": "cross-env NODE_ENV=dev ./node_modules/.bin/nodemon bin/www",
"prd": "cross-env NODE_ENV=production pm2 start pm2.conf.json"
},
# 路由写法
const router = require('koa-router')()
// get 请求
router.get('/string', async (ctx, next) => {
ctx.body = 'koa2 string'
})
router.get('/json', async (ctx, next) => {
// 获取 query 参数
const query = ctx.query
ctx.body = {
title: 'koa2 json'
}
})
// post 请求
// 异步 async
router.post('/login', async (ctx, next) => {
// 获取到 post 中 data
const { username, password } = ctx.request.body
const data = await login(username, password)
if(data.username) {
ctx.body = {
username,
password
}
return
}
ctx.body = new ErrorModel('登录失败')
})
module.exports = router
# 中间件机制
- app.use(callback) 注册中间件;callback 都是一个 async 格式的函数,返回 promise 对象
- next 机制;返回一个 promise 对象。
# koa2 中实现 session
和 express
一样,基于 koa-generic-session
和 koa-redis
;
首先进行安装:npm install koa-generic-session koa-redis redis --save
使用:
// 实现登录
const session = require('koa-generic-session')
const redisStore = require('koa-redis')
// 引入redis 配置
const { REDIS_CONF } = require('./config/db')
// session 配置
app.keys = ['jiegiser_#812*']
app.use(session({
// 配置 cookie
cookie: {
path: '/',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000
},
// 配置 redis
store: redisStore({
// all: '127.0.0.1:6379', // 写死本地的 redis
all: `${REDIS_CONF.host}:${REDIS_CONF.port}`
})
}))
./config/db 文件:
const env = process.env.NODE_ENV //环境变量
// 配置
let MYSQL_CONF
let REDIS_CONF
if(env === 'dev') {
MYSQL_CONF = {
host: 'localhost',
user: 'root',
password: '',
port: '3306',
database: 'myblog'
}
// redis
REDIS_CONF = {
port: 6379,
host: '127.0.0.1'
}
}
if(env === 'production') {
MYSQL_CONF = {
host: 'localhost',
user: 'root',
password: '',
port: '3306',
database: 'myblog'
}
// dedis
REDIS_CONF = {
port: 6379,
host: '127.0.0.1'
}
}
module.exports = {
MYSQL_CONF,
REDIS_CONF
}
使用:存储用户信息
// 存储用户信息 session
// 异步 async
router.post('/login', async (ctx, next) => {
const { username, password } = ctx.request.body
const data = await login(username, password)
if(data.username) {
ctx.session.username = data.username
ctx.session.realname = data.realname
ctx.body = new SuccessModel()
return
}
ctx.body = new ErrorModel('登录失败')
})
// 读取 session 用户信息
const { ErrorModel } = require('../model/resModel')
// 写一个中间件
module.exports = async (ctx, next) => {
if(ctx.session.username) {
// 已经登录了
await next()
return
}
ctx.body = new ErrorModel('未登录')
}
实现一个完整的登录:
// app.js 中注册路由
const user = require('./routes/user')
app.use(user.routes(), user.allowedMethods())
// './routes/user' 文件---注意这里的 await 的 login 方法。
const router = require('koa-router')()
const { login } = require('../controller/user')
const { SuccessModel, ErrorModel } = require('../model/resModel')
// 路由前缀
router.prefix('/api/user')
// 异步 async
router.post('/login', async (ctx, next) => {
const { username, password } = ctx.request.body
const data = await login(username, password)
if(data.username) {
ctx.session.username = data.username
ctx.session.realname = data.realname
ctx.body = new SuccessModel()
return
}
ctx.body = new ErrorModel('登录失败')
})
module.exports = router
// '../controller/user' 注意这里的 login 方法为 async 异步
const { exec, escape } = require('../db/mysql')
const { genPassword } = require('../utils/cryp')
const login = async (username, password) => {
// 防止sql注入
username = escape(username)
// 生成加密密码 ---密码没有进行加密,所以取消验证加密后的密码---需要在登录的时候进行密码的加密存储到数据库中
// password = genPassword(password)
password = escape(password)
console.log(username, password)
// 删除单引号
const sql = `select username,realname from users where username =${username} and password =${password}`
const rows = await exec(sql)
return rows[0] || {}
}
module.exports = {
login
}
# 中间件的使用
自定义一个中间件,用于检查是否登录:
const { ErrorModel } = require('../model/resModel')
// 写一个中间件
module.exports = async (ctx, next) => {
if(ctx.session.username) {
// 已经登录了
await next()
return
}
ctx.body = new ErrorModel('未登录')
}
使用:
const loginCheck = require('../middleware/loginCheck')
router.post('/new', loginCheck, async (ctx, next) => {
const body = ctx.request.body
body.author = ctx.session.username
const data = await newBlog(body)
ctx.body = new SuccessModel(data)
})
# 处理日志
还是使用 morgan 实现对日志的处理:
需要安装 npm install koa-morgan --save 插件;
// 注意这里的 koa-logger 只是美化了在控制台打印日志的效果。并不处理日志文件
// const logger = require('koa-logger')
const path = require('path')
const fs = require('fs')
const morgan = require('koa-morgan')
// 日志记录
const ENV = process.env.NODE_ENV
// https://github.com/expressjs/morgan 查看打印日志的格式
if(ENV != 'production') {
// 对日志进行配置
app.use(morgan('dev', {
// 标准输出,直接输出到控制台
stream: process.stdout
}));
} else {
// 线上环境使用,打印的日志比较详细--将日志写到access.log文件当中
const logFileName = path.join(__dirname, 'logs', 'access.log')
const writeStream = fs.createWriteStream(logFileName, {
flags: 'a'
})
app.use(morgan('combined', {
stream: writeStream
}))
}
# koa2 中间件原理
# 执行过程
如下面代码: 不使用 koa 脚手架,直接安装 npm install koa --save
const Koa = require('koa');
const app = new Koa();
// logger
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
上面的执行过程,首先执行 logger 遇到 await next() 跳转到下一个中间件 x-response-time,又遇到 await next() 跳转到下一个中间件 response ,然后 response 中间件执行完成,继续返回到 x-response-time 中间件,执行 await next() 后面的语句,执行完之后,继续返回到 logger 中间件的 await next() 后面,继续执行,至此整个请求过程完成。
这就是洋葱圈模型:
# 如何实现
- app.use() 用来注册中间件,先收集注册的中间件
- 实现 next 机制,用来触发下一个中间件的执行
实现一个简单的 koa:
const http = require('http')
// 组合中间件
function compose(middlewareList) {
return ctx => {
// 中间件调用
function dispath(i) {
const fn = middlewareList[i]
try {
return Promise.resolve(
// 第二个参数这样实现了 next 的机制
fn(ctx, dispath.bind(null, i + 1))
)
} catch(err) {
return Promise.reject(err)
}
}
return dispath(0)
}
}
class LikeKoa2 {
constructor() {
// 中间件存储的地方
this.middlewareList = []
}
use(fn) {
this.middlewareList.push(fn)
// 可以一直.use()进行注册中间件
return this
}
createContext(req, res) {
const ctx = {
req,
res
}
ctx.query = req.query
return ctx
}
handleRequest(ctx, fn) {
return fn(ctx)
}
callback() {
const fn = compose(this.middlewareList)
return (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
}
listen(...args) {
const server = http.createServer(this.callback())
server.listen(...args)
}
}
module.exports = LikeKoa2
阅读量: