一、koa2
- express 中间件是异步回调,koa2 原生支持 async/await
- 新开发框架和系统,都开始基于 koa2,例如 egg.js
- express 虽然过时,但是 koa2 肯定是未来的趋势
1. async/await 语法,安装和使用 koa2
- async/await 语法
- await 后面可以追加 promise 对象,获取 resolve 的值
- await 必须包裹在 async 函数里面
- async 函数执行返回的也是一个 promise 对象
- try-catch 截获 promise 中 reject 的值
- koa2
- 安装(使用脚手架)shell
npm i koa-generator -g Koa2 koa2-test npm i & npm run dev
- 初始化代码,处理路由
- 环境配置shell
npm i cross-env -S "dev": "cross-env NODE_ENV=dev ./node_modules/.bin/nodemon bin/www", "prd": "cross-env NODE_ENV=production pm2 start bin/www",
- app.jsjs
const Koa = require('koa') const app = new Koa() const views = require('koa-views') const json = require('koa-json') const onerror = require('koa-onerror') const bodyparser = require('koa-bodyparser') const logger = require('koa-logger') const index = require('./routes/index') const users = require('./routes/users') // error handler onerror(app) // middlewares app.use(bodyparser({ enableTypes:['json', 'form', 'text'] })) app.use(json()) app.use(logger()) app.use(require('koa-static')(__dirname + '/public')) app.use(views(__dirname + '/views', { extension: 'pug' })) // logger app.use(async (ctx, next) => { const start = new Date() await next() const ms = new Date() - start console.log(`${ctx.method} ${ctx.url} - ${ms}ms`) }) // routes app.use(index.routes(), index.allowedMethods()) app.use(users.routes(), users.allowedMethods()) // error-handling app.on('error', (err, ctx) => { console.error('server error', err, ctx) }); module.exports = app
- 环境配置
- 安装(使用脚手架)
2. 开发接口、连接数据库、实现登录、日志记录
和 express 类似 基于 koa-generic-session 和 koa-redis ioredis
插件安装
shellnpm i koa-generic-session koa-redis ioredis -S
session redis 在app.js中的配置
javascriptconst session = require('koa-generic-session') const redisStore = require('koa-redis') app.use(session({ // 配置 cookie cookie: { path: '/', httpOnly: true, maxAge: 24 * 60 * 60 * 1000 }, // 配置 redis store: redisStore({ all: '127.0.0.1:6379' }) }))
路由
- 插件安装shell
npm i mysql xss -S
- 相关配置
mysql redis
jsconst env = process.env.NODE_ENV; // 配置 let MYSQL_CONF let REDIS_CONF if (env === 'dev') { MYSQL_CONF = { host: '127.0.0.1', user: 'root', password: '123qwe!@#', port: '3306', database: 'myblog' } REDIS_CONF = { port: 6379, // host: '139.129.240.151' host: '127.0.0.1' } } if (env === 'production') { MYSQL_CONF = { host: 'localhost', user: 'root', password: '123qwe!@#', port: '3306', database: 'myblog' } REDIS_CONF = { port: 6379, // host: '139.129.240.151', host: '127.0.0.1' } } module.exports = { MYSQL_CONF, REDIS_CONF }
mysql 工具封装
jsconst mysql = require('mysql'); const { MYSQL_CONF } = require('../config/db'); // 创建连接对象 const con = mysql.createConnection(MYSQL_CONF); // 开始连接 con.connect() // 统一执行 sql 的函数 function exec(sql) { const promise = new Promise((resolve, reject) => { con.query(sql, (err, result) => { if (err) { reject(err) return; } resolve(result) }) }) return promise; } module.exports = { exec, escape: mysql.escape }
jsconst crypto = require('crypto'); // 秘钥 const SECRET_KEY = 'ASDASD_12'; // md5 加密 function md5(content) { let md5 = crypto.createHash('md5'); return md5.update(content).digest('hex'); } // 加密函数 function genPassword(password) { const str = `password=${password}&key=${SECRET_KEY}`; return md5(str); } module.exports = { genPassword }
中间件
jsconst { ErrorModel } = require('../model/resModel'); module.exports = async (ctx, next) => { if (req.session.username) { await next() return; } ctx.body = new ErrorModel('未登录') }
controller
jsconst { exec } = require('../db/mysql') const getList = async (author, keyword) => { let sql = `select * from blogs where 1=1 ` if (author) { sql += `and author = '${author}' ` } if (keyword) { sql += `and title like '%${keyword}%' ` } sql += `order by createtime desc;` return await exec(sql) } const getDetail = async (id) => { const sql = `select * from blogs where id = '${id}'`; const rows = await exec(sql); return rows[0]; } const newBlog = async (blogData = {}) => { const { title, content, author } = blogData; const createTime = Date.now(); const sql = ` insert into blogs (title, content, createtime, author) values ('${title}', '${content}', '${createTime}', '${author}') ` const data = await exec(sql); return { id: data.insertId } } const updateBlog = async (id, blogData = {}) => { const {title, content} = blogData; const sql = ` update blogs set title='${title}', content='${content}' where id='${id}' ` const updateData = await exec(sql); if (updateData.affectedRows > 0) { return true } return false } const deleteBlog = async (id, author) => { const sql = `delete from blogs where id = '${id}' and author='${author}'` const delData = await exec(sql); if (delData.affectedRows > 0) { return true } return false } module.exports = { getList, getDetail, newBlog, updateBlog, deleteBlog }
router
jsconst router = require('koa-router')() const { getList, getDetail, newBlog, updateBlog, deleteBlog } = require('../controller/blog'); const { SuccessModel, ErrorModel } = require('../model/resModel'); const loginCheck = require('../middleware/loginCheck') router.prefix('/api/blog') router.get('/list', async function (ctx, next) { const author = ctx.query.author || ''; const keyword = ctx.query.keyword || ''; const result = await getList(author, keyword); ctx.body = new SuccessModel(result) }) router.get('/detail', async function (ctx, next) { const result = await getDetail(ctx.query.id); ctx.body = new SuccessModel(result); }) router.post('/new', loginCheck, async function (ctx, next) { const body = ctx.request.body body.author = ctx.session.username; // 假数据 const result = await newBlog(body); ctx.body = new SuccessModel(result); }) router.post('/update', loginCheck, async function (ctx, next) { const result = await updateBlog(ctx.query.id, ctx.request.body); ctx.body = result? new SuccessModel(): new ErrorModel('更新博客失败'); }) router.post('/del', loginCheck, async function (ctx, next) { const author = ctx.session.username; const result = deleteBlog(ctx.query.id, author); ctx.body = result? new SuccessModel(): new ErrorModel('删除博客失败'); }) module.exports = router
app.js
jsconst Koa = require('koa') const app = new Koa() const views = require('koa-views') const json = require('koa-json') const onerror = require('koa-onerror') const bodyparser = require('koa-bodyparser') const logger = require('koa-logger') const index = require('./routes/index') const users = require('./routes/users') // error handler onerror(app) // middlewares app.use(bodyparser({ enableTypes:['json', 'form', 'text'] })) app.use(json()) app.use(logger()) app.use(require('koa-static')(__dirname + '/public')) app.use(views(__dirname + '/views', { extension: 'pug' })) // logger app.use(async (ctx, next) => { const start = new Date() await next() const ms = new Date() - start console.log(`${ctx.method} ${ctx.url} - ${ms}ms`) }) // routes app.use(index.routes(), index.allowedMethods()) app.use(users.routes(), users.allowedMethods()) // error-handling app.on('error', (err, ctx) => { console.error('server error', err, ctx) }); module.exports = app
- 插件安装
日志
- access log 记录,使用 morgan
- 自定义日志使用 console.log 和 console.error
- 日志文件拆分、日志文件分析
- 插件安装shell
npm i koa-morgan -S
- 配置app.jsjavascript
const path = require('path'); const fs = require('fs'); const morgan = require('koa-morgan'); const ENV = process.env.NODE_ENV; if (ENV !== 'production') { app.use(morgan('dev') ); } else { const logFileName = path.resolve(__dirname, 'logs', 'access.log') const writeStream = fs.createWriteStream(logFileName, { flags: 'a' }) app.use(morgan('combined', { stream: writeStream }) ); }
3. 分析koa2中间件原理
js
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);
// 执行顺序 5 6 12 14 20 15 7
分析
- app.use 用来注册中间件,先收集起来
- 实现 next 机制,即上一个通过 next 触发下一个
- 不涉及 method 和 path 的判断
模拟实现
jsconst http = require('http'); // 组合中间件 function compose(middlewareList) { return function (ctx) { function dispatch(i) { const fn = middlewareList[i]; try { return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err); } } return dispatch(0) } } class LikeKoa2 { constructor() { this.middlewareList = []; } use(fn) { this.middlewareList.push(fn); return this; } createContext(req, res) { const ctx = { req, res } return ctx } handlerRequest(ctx, fn) { return fn(ctx) } callBack() { const fn = compose(this.middlewareList); return (req, res) => { const ctx = this.createContext(req, res); return this.handlerRequest(ctx, fn); } } listen(...args) { const server = http.createServer(this.callBack()); server.listen(...args); } } module.exports = LikeKoa2
jsconst Koa = require('./like-koa2'); const app = new Koa(); // 非常典型的洋葱圈模型 // logger app.use(async (ctx, next) => { console.log('第一层洋葱 - 开始') await next(); const rt = ctx['X-Response-Time']; console.log(`${ctx.req.method} ${ctx.req.url} - ${rt}`); console.log('第一层洋葱 - 结束') }); // x-response-time app.use(async (ctx, next) => { console.log('第二层洋葱 - 开始') const start = Date.now(); await next(); const ms = Date.now() - start; ctx['X-Response-Time'] = `${ms}ms`; console.log('第二层洋葱 - 结束') }); // response app.use(async ctx => { console.log('第三层洋葱 - 开始') ctx.res.end('This is like koa2') console.log('第三层洋葱 - 结束') }); app.listen(8004);