Koa 就是一种简单好用的 Web 框架。它的特点是优雅、简洁、表达力强、自由度高。本身代码只有 1000 多行。Koa 一个中间件框架,其提供的是一个架子,而几乎所有的功能都需要由第三方中间件完成,它只是 Node 原生的 http 的一个封装,再加入中间件元素,Koa 不在内核方法中绑定任何中间件, 它仅仅提供了一个轻量优雅的函数库,使得编写 Web 应用变的得心应手
Koa 目前分为两个版本:koa 1.0 和 koa2
以下的关于koa的介绍主要在koa2的基础上进行分析:
koa框架主要由以下几个元素组成:
const Koa = require('koa');
const app = new Koa();
app的主要属性如下:
//以下是koa获取request对象部分属性的源码,都是由app.proxy属性决定的:
{
get ips() {
const proxy = this.app.proxy;
const val = this.get('X-Forwarded-For');
return proxy && val
? val.split(/\s*,\s*/)
: [];
},
get host() {
const proxy = this.app.proxy;
let host = proxy && this.get('X-Forwarded-Host');
host = host || this.get('Host');
if (!host) return '';
return host.split(/\s*,\s*/)[0];
},
get protocol() {
const proxy = this.app.proxy;
if (this.socket.encrypted) return 'https';
if (!proxy) return 'http';
const proto = this.get('X-Forwarded-Proto') || 'http';
return proto.split(/\s*,\s*/)[0];
},
get URL() {
if (!this.memoizedURL) {
const protocol = this.protocol;
const host = this.host;
const originalUrl = this.originalUrl || ''; // originalUrl为req.url
try {
this.memoizedURL = new URL(`${protocol}://${host}${originalUrl}`);
} catch (err) {
this.memoizedURL = Object.create(null);
}
}
return this.memoizedURL;
},
get hostname() {
const host = this.host;
if (!host) return '';
if ('[' == host[0]) return this.URL.hostname || ''; // IPv6
return host.split(':')[0];
},
}
this.env = process.env.NODE_ENV || 'development';
为了保证签名的安全性,这个 keys 可以定时更新,keygrip 这个库就是用来维护管理签名 key 的工具,每次签名时都会获取keys中的第一个key作为签名认证的密钥,所以实时更新app.keys可以提高安全性。实时更新 keys 需要业务开发者实时操作
keys 设置的两种方式:
app.keys = [‘im a newer secret’, ‘i like turtle’]; // 直接设置数组
app.keys = new KeyGrip([‘im a newer secret’, ‘i like turtle’], ‘sha256’); // 也可以使用 KeyGrip 模块直接生成
//比如有netease.youdata.163.com域名
app.subdomainOffset = 2;
console.log(ctx.request.subdomains); //返回["youdata", "netease"]
app.subdomainOffset = 3;
console.log(ctx.request.subdomains); //返回["netease"]
//koa获取subdomains的源码
get subdomains() {
const offset = this.app.subdomainOffset;
const hostname = this.hostname;
if (net.isIP(hostname)) return [];
return hostname
.split('.')
.reverse()
.slice(offset);
},
koa 使用中间件方式来实现不同功能的级联,当一个中间件调用next(),则该函数暂停并将控制传递给定义的下一个中间件。当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件恢复执行其上游行为,类似一个入栈出栈的模式,中间件的使用方式如下:
const Koa = require('koa');
const app = new Koa();
app.use((ctx, next) => {
console.log('step1-begin');
next();
console.log('step1-end');
});
app.use((ctx, next) => {
console.log('step2-begin');
next();
console.log('step2-end');
});
app.listen(3000);
/*输出结果为:
step1-begin
step2-begin
step2-end
step1-end
*/
//以下context.js中的部分源码:
toJSON() {
return {
request: this.request.toJSON(), //如果直接使用app.context调用这个会报错,因为这个时候this.request是undefined,只有在中间件里使用ctx调用才不会报错
response: this.response.toJSON(),
app: this.app.toJSON(),
originalUrl: this.originalUrl,
req: '' ,
res: '' ,
socket: ''
};
},
context主要有以下用途:
//我们可以在context对象上加一些全局路由里公用的属性,这样就不需要每次请求都在中间件里赋值
const Koa = require('koa');
const app = new Koa();
app.context.datasourceConfig = {
"connectionLimit": 100,
"database": "development",
"host": "10.165.124.134",
"port": 3360,
"user": "sup_bigviz",
"password": "123456",
"multipleStatements": true
};
app.use((ctx, next) => {
console.log('datasourceConfig:', ctx.datasourceConfig); //这里可以打印出全局配置
next();
});
request: 这个是创建ctx.request的原型,直接使用app.context.request几乎没有意义,很多属性都会报错,不过和app.context一样,可以给app.context添加一些ctx.request中用到的公共属性
response: 这个是创建ctx.response的原型,直接使用app.context.response几乎没有意义,很多属性都会报错,不过和app.context一样,可以给app.context添加一些ctx.request中用到的公共属性
app的主要函数如下:
let koa = require('koa');
koa.use(async (ctx, next) => {
//before do something...
next();
//after await do something...
})
//app.listen 其实是 http.createServer 的语法糖,源码实现如下:
function listen(...args) {
debug('listen');
const server = http.createServer(this.callback()); //最终所有路由处理是在app..callback中实现的
return server.listen(...args);
}
//koa的callback函数实现源码
function callback() {
const fn = compose(this.middleware); //koa-compose包负责讲多个中间件组装成一个中间件
if (!this.listeners('error').length) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res); //这个函数负责生成中间件接收器ctx,绑定一些对象的关联关系
return this.handleRequest(ctx, fn); //使用中间件函数fn处理路由请求
};
return handleRequest;
}
//handleRequest函数的源码实现也很简单,执行中间件函数,并做一些返回处理和异常处理
function handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
ctx是中间件中的上下文环境,也是koa框架中最常用最重要的对象,每个请求都会根据app.context创建一个新的ctx,并在中间件中作为接收器引用
ctx对象上会绑定app,request,response等对象
//生成ctx的源码
function createContext(req, res) {
const context = Object.create(this.context); //由上文中讲解的app.context生成
const request = context.request = Object.create(this.request); //由上文中讲解的app.request生成
const response = context.response = Object.create(this.response); //由上文中讲解的app.response生成
context.app = request.app = response.app = this;
context.req = request.req = response.req = req; //req是node的req,尽量避免使用,而是使用ctx.request;
context.res = request.res = response.res = res; //res是node的res,尽量避免使用,而是应该使用ctx.response;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, { //生成cookies,是由[cookie模块生成的](https://github.com/pillarjs/cookies):
keys: this.keys,
secure: request.secure //secure是根据域名是不是https返回的结果
});
request.ip = request.ips[0] || req.socket.remoteAddress || ''; //客户端访问ip
context.accept = request.accept = accepts(req); //
context.state = {}; //这个给用户使用,用于存放用户在多个中间件中用到的一些属性或者函数
return context;
}
ctx会代理ctx.response和ctx.request上的一些属性和函数(这个代理逻辑是在ctx.response和ctx.request的原型上实现的)
//以下是koa源码(method表示代理方法,access表示代理属性可读可写,getter表示代理属性可读):
delegate(proto, 'response')
.method('attachment') //将Content-Disposition 设置为 “附件” 以指示客户端提示下载
.method('redirect') //返回重定向,如果没有code设置,默认设置code为302
.method('remove') //删除响应头的某个属性
.method('vary') //设置Vary响应头
.method('set') //设置响应头,可以传递对象,数组,单个值的形式
.method('append') //给response.headers中的某个key值追加其它value
.method('flushHeaders') //执行this.res.flushHeaders()
.access('status') //http返回code码,优先选择用户的设置,如果用户没有主动设置,而设置了ctx.body的值, 如果设置值为null,则返回204,如果设置值不为null,那么返回200,否则默认情况下是404
.access('message') //获取响应的状态消息. 默认情况下, response.message 与 response.status 关联
.access('body') //response的返回结果
.access('length') //response的headers的Content-Length,可以自己设置,默认根据body二进制大小设置
.access('type') //设置响应的content-type
.access('lastModified') //设置响应头Last-Modified
.access('etag') //设置包含 " 包裹的 ETag 响应头
.getter('headerSent') //检查是否已经发送了一个响应头。 用于查看客户端是否可能会收到错误通知
.getter('writable'); //返回是否可以继续写入
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts') //accepts函数用于判断客户端请求是否接受某种返回类型
.method('get') //获取请求头中的某个属性值
.method('is') //判断请求头希望返回什么类型
.access('querystring') //获取原始查询字符串
.access('idempotent')
.access('socket') //返回请求套接字
.access('search') //搜索字符串
.access('method') //请求方法
.access('query') //获取请求的查询字符串对象
.access('path') //获取请求路径名
.access('url') //请求的url,该url可以被重写
.getter('origin') //获取url的来源:包括 protocol 和 host(http://example.com)
.getter('href') //获取完整的请求URL,包括 protocol,host 和 url(http://example.com/foo/bar?q=1)
.getter('subdomains') //获取请求的子域名
.getter('protocol') //返回请求协议
.getter('host') //获取当前主机的host(hostname:port)
.getter('hostname') //获取当前主机的host
.getter('URL') //获取 WHATWG 解析的 URL 对象
.getter('header') //返回请求头对象
.getter('headers') //返回请求头对象
.getter('secure') //通过 ctx.protocol == "https" 来检查请求是否通过 TLS 发出
.getter('stale')
.getter('fresh')
.getter('ips') //当 X-Forwarded-For 存在并且 app.proxy 被启用时,这些 ips 的数组被返回
.getter('ip'); //请求远程地址
//比如以下操作是等价的:
ctx.body = {
code: 200,
result: {
nick: "zhangdianpeng"
}
}
ctx.response.body = {
code: 200,
result: {
nick: "zhangdianpeng"
}
}
console.log('ctx.method:', ctx.method);
console.log('ctx.request.method:', ctx.request.method);
ctx的其它属性和函数介绍:
Koa 默认的返回类型是 text/plain,如果想返回其他类型的内容,可以先用 ctx.request.accepts 判断一下,客户端希望接受什么数据(根据 HTTP Request 的Accept字段),然后使用ctx.response.type指定返回类型。
const main = ctx => {
if (ctx.request.accepts('xml')) {
ctx.response.type = 'xml';
ctx.response.body = 'Hello World';
} else if (ctx.request.accepts('json')) {
ctx.response.type = 'json';
ctx.response.body = { data: 'Hello World' };
} else if (ctx.request.accepts('html')) {
ctx.response.type = 'html';
ctx.response.body = 'Hello World
';
} else {
ctx.response.type = 'text';
ctx.response.body = 'Hello World';
}
};
如果 response.status 未被设置, Koa 将会自动设置状态为 200 或 204,只有第一次赋值会生效,后面的赋值会被自动忽略,可以将响应体设置为以下几种类型:
//string 写入
ctx.body = JSON.stringify({code: 200}); //有空格的情况content-type = 'text/html' 否则content-type = 'text/plain';
//Buffer 写入
ctx.body = new Buffer('hellowold'); //默认content-type = 'bin';
//Stream 管道写入
ctx.body = file.createReadStream('./test.js'); //默认content-type = 'bin';
//Object 写入
ctx.body = {code: 200}; //默认content-type = 'json';
ctx.response.set('Cache-Control', 'no-cache'); //设置返回头
ctx.response.get('ETag') //获取返回头
为了绕过 Koa 的内置 response 处理,你可以显式设置ctx.respond=false;。如果您想要写入原始的res对象而不是让Koa处理你的response,请使用此参数。请注意,Koa不建议使用此功能。这可能会破坏 Koa 中间件和 Koa 本身的预期功能。使用这个属性被认为是一个 hack,只是便于那些希望在 Koa 中使用传统的 fn(req, res) 功能和中间件的人
ctx.throw(400); //status默认是500
ctx.throw(400, 'name required');
ctx.throw(400, 'name required', { user: user });
function assert(value, status, msg, opts) {
if (value) return;
throw createError(status, msg, opts);
}
Koa继承了Emitter类,所以我们可以给app添加error事件的处理函数:
let app = require('koa');
app.on('error', err => console.log(err));
//如果我们没有给app添加任何error事件的处理函数,koa会给app添加一个默认的error事件处理函数,源码实现如下:
function onerror(err) {
assert(err instanceof Error, `non-error thrown: ${err}`);
//除非 app.silent为true或者当err.status是404或者err.expose是true时默认错误处理程序不会在控制台打印错误信息
if (404 == err.status || err.expose) return;
if (this.silent) return;
const msg = err.stack || err.toString();
console.error();
console.error(msg.replace(/^/gm, ' '));
console.error();
}
用于将多个中间件合并成一个中间件
const app = require('koa');
const koaCompose = require('koa-compose');
let middlewareA = async (ctx, next) => {
//do something;
async next();
}
let middlewareB = async (ctx, next) => {
//do something;
async next();
}
app.use(koaCompose[middlewareA, middlewareB]);
路由处理中间件
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
router.get('/', (ctx, next) => {
// ctx.router available
});
app
.use(router.routes())
.use(router.allowedMethods());
页面渲染中间件
var views = require('koa-views');
// Must be used before any router is used
app.use(views(__dirname + '/views', {
map: {
html: 'underscore'
}
}));
app.use(async function (ctx, next) {
ctx.state = {
session: this.session,
title: 'app'
};
await ctx.render('user', {
user: 'John'
});
});
session处理中间件
var session = require('koa-session-redis');
var koa = require('koa');
var app = koa();
app.keys = ['some secret hurr'];
app.use(session({
store: {
host: process.env.SESSION_PORT_6379_TCP_ADDR || '127.0.0.1',
port: process.env.SESSION_PORT_6379_TCP_PORT || 6379,
ttl: 3600,
},
},
));
app.use(function *(){
var n = this.session.views || 0;
this.session.views = ++n;
this.body = n + ' views';
})
app.listen(3000);
console.log('listening on port 3000');
post请求body处理中间件
var Koa = require('koa');
var bodyParser = require('koa-bodyparser');
var app = new Koa();
app.use(bodyParser());
app.use(async ctx => {
// the parsed body will store in ctx.request.body
// if nothing was parsed, body will be an empty object {}
ctx.body = ctx.request.body;
});
用于管理cookie签名密钥的工具
签名的keys配置成一个数组,Cookie 在使用这个配置进行加解密时:
如果我们想要更新 Cookie 的秘钥,但是又不希望之前设置到用户浏览器上的Cookie失效,可以将新的秘钥配置到 keys 最前面,等过一段时间之后再删去不需要的秘钥即可
用于管理koa中cookie的库,其中会使用keygrip作为cookie签名的库