Koa 旨在为Web应用程序和API提供更小、更丰富和更强大的能力,相对于 express 具有更强的异步处理能力,Koa的核心代码只有1600+行,是一个更加轻量级的框架。
const Koa = require('koa');
const app = new Koa()
app.use((ctx, next) => {
console.log('use 1')
next()
})
app.use((ctx, next) => {
ctx.response.body = 'test'
// ctx.response 已经被 Koa 封装了,没有 end 方法,直接使用 .body 进行返回
// 可简写为 ctx.body = 'test'
})
// 如果没有返回结果,会自动返回 Not Found
app.listen(8000, () => {
console.log('启动成功')
})
app.get(...)
app.use('/path', callback)
app.use(callback1,callback2)
如何将路径和method分离呢
可以通过,ctx.request.path (.method) 来判断
app.use((ctx, next) => {
const req = ctx.request
if (req.path === '/user') {
if (req.method === 'POST') {
ctx.body = 'create user'
} else {
ctx.body = 'user list'
}
}
})
也可以使用第三方路由中间件
const Koa = require('koa');
const app = new Koa()
// 使用 koa-router
const Route = require('koa-router')
// 创建路由
const userRouter = new Route({ prefix: '/user' })
// 使用
userRouter.get('/', (ctx, next) => {
ctx.body = 'user get'
})
userRouter.post('/', (ctx, next) => {
ctx.body = 'user post'
})
// 导入路由
app.use(userRoute.routes())
// 使用 allowedMethods 方法
app.use(userRoute.allowedMethods())
// 请求 get,那么是正常的请求,因为有实现 get 方法
// 请求 put、delete、patch,那么就自动报错:Method Not Allowed,状态码:405
// 请求 link、copy、lock,那么久自动报错:Not Implemented,状态码:501
const userRouter = new Route({ prefix: '/user' })
userRouter.get('/:id', (ctx, next) => {
console.log(ctx.request.params)
console.log(ctx.request.query)
ctx.body = 'user get'
})
// /user/123?name=test&age=18
// { id: '123' }
// [Object: null prototype] { name: 'test', age: '18' }
// 使用 koa-bodyparser
const bodyparser = require('koa-bodyparser')
const userRouter = new Route({ prefix: '/user' })
app.use(bodyparser())
userRouter.get('/', (ctx, next) => {
console.log(ctx.request.body) // 通过 ctx.request.body 接收
ctx.body = 'user get'
})
// 使用 koa-multer
// 文件上传
const Koa = require('koa');
const Route = require('koa-router')
const multer = require('koa-multer')
const app = new Koa()
const uploadRouter = new Route({ prefix: '/upload' })
// 上传配置
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, './uploads/') // 保存路径
},
filename: (req, file, cb) => {
cb(null, file.originalname) // 文件名
}
})
const upload = multer({
storage
})
// 上传接口
uploadRouter.post('/', upload.single('file'), (ctx, next) => {
console.log(ctx.req.file)
/**
* {
fieldname: 'file',
originalname: 'img.jpg',
encoding: '7bit',
mimetype: 'image/jpeg',
destination: './uploads/',
filename: 'img.jpg',
path: 'uploads\\img.jpg',
size: 34462
}
*/
ctx.body = 'upload file'
})
app.use(uploadRouter.routes())
app.use(uploadRouter.allowedMethods())
app.listen(8000, () => {
console.log('启动成功')
})
response.status
不设置,默认为 200 或 204
// 使用 koa-static
const Koa = require('koa')
const staticServer = require('koa-static')
const app = new Koa()
app.use(staticServer('./dist'))
app.listen(8000, () => {
console.log('启动成功')
})
const Koa = require('koa')
const app = new Koa()
app.use((ctx, next) => {
// 可以通过 ctx.app 获取到 app
// class Application extends Emitter,app 继承了 Emitter 可以使用订阅/发布的方式,发布报错信息
ctx.app.emit('error', new Error('test error'), ctx)
})
// 监听 error 事件,返回错误信息
app.on('error', (err, ctx) => {
console.log(err.message)
ctx.body = err.message
})
app.listen(8000, () => {
console.log('启动成功')
})
const Emitter = require('events');
// Application 继承 Emitter
module.exports = class Application extends Emitter {
// 初始化配置
constructor(options) {
super();
options = options || {};
this.proxy = options.proxy || false;
this.subdomainOffset = options.subdomainOffset || 2;
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
this.maxIpsCount = options.maxIpsCount || 0;
this.env = options.env || process.env.NODE_ENV || 'development';
if (options.keys) this.keys = options.keys;
this.middleware = []; // 中间件数组
this.context = Object.create(context); // 执行上下文
this.request = Object.create(request); // 请求
this.response = Object.create(response); // 响应
// util.inspect.custom support for node 6+
/* istanbul ignore else */
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
// 开启监听
listen(...args) {
debug('listen');
const server = http.createServer(this.callback()); // 传入监听回调
return server.listen(...args);
}
// 添加中间件
use(fn) {
// 检查
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
// 加入中间件数组
this.middleware.push(fn);
return this;
}
// 监听回调
callback() {
// 处理中间件 const compose = require('koa-compose');, fn 的返回值 Promise
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res); // 处理 ctx
return this.handleRequest(ctx, fn); // 处理请求
};
return handleRequest;
}
// 中间件处理函数
function compose (middleware) {
// 检查
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
// 处理函数
return function (context, next) {
// last called middleware #
let index = -1 // 闭包,缓存 index
return dispatch(0) // 从 0 开始调用
function dispatch (i) {
// 判断是否已经调用过(index 已经 +1)
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i] // 拿到执行函数
if (i === middleware.length) fn = next // 指向 next
if (!fn) return Promise.resolve() // fn 为空,返回 Promise.resolve() 结束回调
try {
// 对函数进行 Promise 封装
return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); // 回调调用
} catch (err) {
return Promise.reject(err)
}
}
}
}
// 处理请求
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx); // 处理响应函数
onFinished(res, onerror);
// 执行中间件,全部中间件执行完,才会调用 handleResponse, 因此执行中多次改变 ctx.body 会取最后一次的值
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
// respond
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return;
if (!ctx.writable) return;
const res = ctx.res;
let body = ctx.body;
const code = ctx.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
if ('HEAD' === ctx.method) {
if (!res.headersSent && !ctx.response.has('Content-Length')) {
const { length } = ctx.response;
if (Number.isInteger(length)) ctx.length = length;
}
return res.end();
}
// status body
if (null == body) {
if (ctx.response._explicitNullBody) {
ctx.response.remove('Content-Type');
ctx.response.remove('Transfer-Encoding');
return res.end();
}
if (ctx.req.httpVersionMajor >= 2) {
body = String(code);
} else {
body = ctx.message || String(code);
}
if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' === typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
context 内部使用了 delegates 对 request & response 的属性和方法进行了代理,因此可以使用 ctx.body 进行响应
const delegate = require('delegates');
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('has')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts')
.method('get')
.method('is')
.access('querystring')
.access('idempotent')
.access('socket')
.access('search')
.access('method')
.access('query')
.access('path')
.access('url')
.access('accept')
.getter('origin')
.getter('href')
.getter('subdomains')
.getter('protocol')
.getter('host')
.getter('hostname')
.getter('URL')
.getter('header')
.getter('headers')
.getter('secure')
.getter('stale')
.getter('fresh')
.getter('ips')
.getter('ip');
ctx.request & ctx.response 都是经过 koa 封装的,ctx.req & ctx.res 才是 node 原来的对象
toJSON() {
return {
request: this.request.toJSON(),
response: this.response.toJSON(),
app: this.app.toJSON(),
originalUrl: this.originalUrl,
req: '<original node req>',
res: '<original node res>',
socket: '<original node socket>'
};
},
因为 next 使用了 Promise 封装 koa 可以方便的使用 async/await 进行异步处理,相同代码下 express 则无法做到,由于 express 内部没有进行 Promise 封装
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => {
ctx.msg = '111'
await next()
ctx.body = ctx.msg
})
app.use(async (ctx, next) => {
await new Promise((resolve, reject) => {
try {
setTimeout(() => {
ctx.msg += '222'
resolve()
}, 1000)
} catch (e) {
reject(e)
}
})
})
app.listen(8000, () => {
console.log('启动成功')
})
// 请求 http://localhost:8000 一秒后输出 111222
const express = require('express')
const app = express()
app.use(async (req, res, next) => {
req.msg = '111'
await next()
res.end(req.msg)
})
app.use(async (req, res, next) => {
await new Promise((resolve, reject) => {
try {
setTimeout(() => {
req.msg += '222'
resolve()
}, 1000)
} catch (e) {
reject(e)
}
})
})
app.listen(8000, () => {
console.log('启动成功')
})
// 请求 http://localhost:8000 直接输出 111