koa 的官方简介
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。
koa源码目录
koa的源码十分简洁,lib文件夹下只有四个js文件,分别为:
application.js: 主干部分,在这里进行了中间件合并、上下文封装、处理请求&响应、错误监听等操作。context.js: 代表上下文ctxrequest.js: 封装ctx.request的逻辑,是对原生req的扩展response.js: 封装ctx.response的逻辑,是对原生res的扩展
简单体验koa
const Koa = require("koa");
const app = new Koa();
app.use((ctx) => {
console.log(123);
ctx.body = "<h1>hello world !</h1>";
});
app.listen(3000, () => {
console.log("server start 3000");
});
页面显示:
从上面简单的演示,可以看出koa导出的是一个类,这个类有use,listen等方法.
use可以接收一个函数来处理http请求,listen用来指定服务跑在哪个端口,并且有一个成功的回调
开始实现mini-koa
我们也创建一个lib文件,里面有application.js context.js request.js response.js,和koa源码保持一直
在application.js入口文件我们创建一个Application类,先把架子搭好
class Application {
constructor() {}
use() {}
listen() {}
}
module.exports = Application;
我们都知道koa是对原生http模块的封装,所以listen方法实现很简单
handleRequest(req, res) {}
listen() {
const server = http.createServer(this.handleRequest); //this.handleRequest来处理请求
server.listen(...arguments);
}
但是注意这里会有一个问题createServer传入的函数this默认指向的是http创建的server,我们实际让this指向application的实例,所以这里需要改造一下,改造的方式很多,我们选择bind函数
const server = http.createServer(this.handleRequest.bind(this)); //this.handleRequest来处理请求
listen实现完了,我们再来实现use和handleRequest.我么都知道use方法可以处理多个函数,通过调用next来执行下一个函数,例如
const Koa = require("koa");
const app = new Koa();
app.use((ctx, next) => {
ctx.body = "1";
next();
});
app.use((ctx, next) => {
ctx.body = ctx.body + "2";
next();
});
app.use((ctx) => {
ctx.body = ctx.body + "3";
});
app.listen(3000, () => {
console.log("server start 3000");
});
但是我们首先处理简单的情况,use只能处理一个函数
我们在context.js request.js response.js文件下都导出一个空对象{},
//context.js request.js response.js
module.exports = {};
注意
context 上下文对象不能够共享,每个new Koa()的实例的context不能一样,每次http请求的context也不能一样,也就是不能引用同一块地址.
- 每个应用独立的上下文
- 每次请求独立的上下文
// application.js
const http = require("http");
const context = require("./context");
const request = require("./request");
const response = require("./response");
class Application {
constructor() {
//1.每个应用独立的上下文
//this.context.__proto__ = context
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn) {
this.fn = fn; //先实现简单的,假设use只能处理一个函数
}
createContext(req, res) {
const ctx = Object.create(this.context);
const request = Object.create(this.request);
const response = Object.create(this.response);
//ctx的request和response对象
ctx.request = request;
ctx.response = response;
//原生的req,res保存
ctx.req = ctx.request.req = req;
ctx.res = ctx.request.res = res;
return ctx;
}
handleRequest(req, res) {
// 2.每次请求都产生新的上下文
const ctx = this.createContext(req, res);
this.fn(ctx);
}
listen() {
const server = http.createServer(this.handleRequest.bind(this)); //this.handleRequest来处理请求
server.listen(...arguments);
}
}
module.exports = Application;
//request.js
const parse = require("parseurl");
module.exports = {
//获取path方法
get path() {
return parse(this.req).pathname;
},
};
const Koa = require("../koa");
const app = new Koa();
app.use((ctx) => {
console.log(ctx.request.path);
});
app.listen(3000, () => {
console.log("server start 3000");
});
这样通过原型链就可以找到属性,又可以使每个context对象独立
通过访问ctx.request.path,进入get path() 方法,此时this指向ctx.request,又因为先前把req挂载到了ctx.request,所以return parse(this.req).pathname;中this.req获取的是当前请求原生的req对象
这就是为什么request身上为什么加上一个req属性,为了在取值的时候可以快速获取到原生的req
测试一下:
访问http://localhost:3000/test,控制台输出/test
有了这样的"套路"我们就可以不停的给request和response上扩展不同的功能
比如实现ctx.request.headers
//request.js
const parse = require("parseurl");
module.exports = {
//获取path方法
get path() {
return parse(this.req).pathname;
},
get headers() {
return this.req.headers;
},
};
然而ctx.path也能获取到path值,那是怎么实现的呢?
我们可以这样实现它
// context.js
module.exports = {
get path() {
return this.request.path;
},
};
测试一下发现没有问题,但是我们想一下如果ctx有100个属性,我们就要创建100个吗?
所以,我们可以考虑使用函数包装一下
例如:我们需要request或response定义的方法 委托到了 context 对象上,就向下面一样传入委托的target和key
defineGetter("request", "path");
defineGetter("request", "headers");
我们可以用Object.defineProperty来实现
function defineGetter(target, key) {
Object.defineProperty(proto, key, {
get() {
// 当在ctx读取值时,ctx上没有的属性会向原型找,就会触发get方法
//this是ctx对象
return this[target][key];
},
});
}
但是koa中实现使用的并不是Object.defineProperty而是一个delegate库,内部的原理是一个废弃的api__defineSetter__
``
//koa源码
/**
* Response delegation.
*/
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');
/**
* Request delegation.
*/
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.response.body怎么实现
//response.js
module.exports = {
_body: undefined,
get body() {
return this._body;
},
set body(content) {
this._body = content;
},
};
// context.js
const proto = (module.exports = {});
function defineGetter(target, key) {
Object.defineProperty(proto, key, {
get() {
// 当在ctx读取值时,ctx上没有的属性会向原型找,就会触发get方法
//this是ctx对象
return this[target][key];
},
});
}
function defineGetterAndSetter(target, key) {
Object.defineProperty(proto, key, {
set(newValue) {
this[target][key] = newValue;
},
get() {
return this[target][key];
},
});
}
defineGetter("request", "path");
defineGetter("request", "headers");
defineGetterAndSetter("response", "body");
//测试
const Koa = require("../koa");
const app = new Koa();
app.use((ctx) => {
// ctx.body和ctx.request.body可以多次调用,并且可以获取
ctx.body = "hello";
ctx.body = "world";
ctx.response.body = "hello world";
console.log(ctx.body); //打印结果:hello world
});
app.listen(3000, () => {
console.log("server start 3000");
});
console.log(ctx.body); //打印结果:hello world
*但是页面没有响应
需要修改一下handleRequest方法
//application.js
const http = require("http");
const context = require("./context");
const request = require("./request");
const response = require("./response");
class Application {
constructor() {...}
use(fn) {...}
createContext(req, res) {...}
handleRequest(req, res) {
// 2.每次请求都产生新的上下文
const ctx = this.createContext(req, res);
res.statusCode = 404; //默认404
this.fn(ctx);
let body = ctx.body;
//如果有值返回信息最终结果响应给用户
if (body != null) {
res.statusCode = 200;
res.end(ctx.body);
} else {
res.end("Not Found");
}
}
listen() {...}
}
module.exports = Application;
这样我们就完成了一个只能处理一个函数的koa
然而 Koa 最有亮点的就是 洋葱模型
koa 洋葱模型
//测试
const Koa = require("koa");
const app = new Koa();
app.use((ctx, next) => {
console.log(1);
next(); // next 表示下一个中间件
console.log(2);
});
app.use((ctx, next) => {
console.log(3);
next();
console.log(4);
});
app.use((ctx, next) => {
console.log(5);
next();
console.log(6);
});
app.listen(3000, () => {
console.log("server start 3000");
});
打印顺序1 3 5 6 4 2,调用next就会进入下一个中间件,就像剥洋葱一样 (1(3(5 6)4)2)
那么中间件有异步事件呢?
- koa 中所有的异步操作都要基于promise
- koa 内部会将所有的中间件组合操作 组合成立一个大的 promise 只要从头走到结束就算完成了
- koa 中所有中间件都要加
await next()或者return next(),否则异步逻辑可能出错
function sleep() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("sleep");
}, 500);
});
}
app.use((ctx, next) => {
console.log(1);
ctx.body = "1";
next(); // next 表示下一个中间件
console.log(2);
ctx.body = "2";
});
app.use(async (ctx, next) => {
console.log(3);
ctx.body = "3";
await sleep();
next();
console.log(4);
ctx.body = "4";
});
app.use((ctx, next) => {
console.log(5);
ctx.body = "5";
next();
console.log(6);
ctx.body = "6";
});
输出顺序1 3 2 5 6 4
next如果没有await,不会等待下一个中间件中异步执行完
实现多个use
在constructor中添加this.middlewares = [];,每次use时this.middlewares.push(fn)
新增一个compose函数用来合并所有中间件,compose返回的是一个promise
constructor() {
//1.每个应用独立的上下文
//this.context.__proto__ = context
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
// 存放所有middleware的数组
this.middlewares = []; //+
},
use(fn) {
this.middlewares.push(fn);
},
handleRequest(req, res) {
// 2.每次请求都产生新的上下文
const ctx = this.createContext(req, res);
res.statusCode = 404; //默认404
// 组合所有middleware
this.compose(ctx).then(() => {
let body = ctx.body;
//如果有值返回信息最终结果响应给用户
if (body != null) {
res.statusCode = 200;
res.end(ctx.body);
} else {
res.end("Not Found");
}
});
}
//compose
compose(ctx) {
let index = -1; //记录上一次调用索引
const dispath = (i) => {
if (i <= index) {
// 如果当前i <= index,说明重复调用,报错
throw new Error("next() 调用多次");
}
index = i;
if (i === this.middlewares.length) {
//如果是最后一个中间件调用的next直接返回Promise.resolve()
return Promise.resolve();
}
const middleware = this.middlewares[i]; //当前middleware
// try catch 下 middleware执行可能报错,可以捕获错误,一起处理
try {
return Promise.resolve(middleware(ctx, () => dispath(i + 1)));
} catch (err) {
return Promise.reject(err);
}
};
return dispath(0); //执行第一个中间件
}
最后继承node中发布订阅events
const EventEmitter = require("events");
class Application extends EventEmitter
在中间件执行抛出的错误
this.compose(ctx)
.then(() => {
let body = ctx.body;
//如果有值返回信息最终结果响应给用户
if (body != null) {
res.statusCode = 200;
res.end(ctx.body);
} else {
res.end("Not Found");
}
})
.catch((err) => {
this.emit("error", err);//发布
});
const app = new Koa();
// 统一处理错误
app.on("error", (e) => {
console.log(e);
});
koa 特点
- koa 中可以丰富ctx,而且内部提供了一个中间件流程
- 提供了更好的错误处理
代码
const http = require("http");
const context = require("./context");
const request = require("./request");
const response = require("./response");
const EventEmitter = require("events");
class Application extends EventEmitter {
constructor() {
super();
//1.每个应用独立的上下文
//this.context.__proto__ = context
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
this.middlewares = []; //+
}
use(fn) {
this.middlewares.push(fn);
}
createContext(req, res) {
const ctx = Object.create(this.context);
const request = Object.create(this.request);
const response = Object.create(this.response);
//ctx的request和response对象
ctx.request = request;
ctx.response = response;
//原生的req,res保存
ctx.req = ctx.request.req = req;
ctx.res = ctx.request.res = res;
return ctx;
}
compose(ctx) {
let index = -1; //记录上一次调用索引
const dispath = (i) => {
if (i <= index) {
// 如果当前i <= index,说明重复调用,报错
throw new Error("next() 调用多次");
}
index = i;
if (i === this.middlewares.length) {
//如果是最后一个中间件调用的next直接返回Promise.resolve()
return Promise.resolve();
}
const middleware = this.middlewares[i]; //当前middleware
// try catch 下 middleware执行可能报错,可以捕获错误,一起处理
try {
return Promise.resolve(middleware(ctx, () => dispath(i + 1)));
} catch (err) {
return Promise.reject(err);
}
};
return dispath(0); //执行第一个中间件
}
handleRequest(req, res) {
// 2.每次请求都产生新的上下文
const ctx = this.createContext(req, res);
res.statusCode = 404; //默认404
// 组合所有middleware
this.compose(ctx)
.then(() => {
let body = ctx.body;
//如果有值返回信息最终结果响应给用户
if (body != null) {
res.statusCode = 200;
res.end(ctx.body);
} else {
res.end("Not Found");
}
})
.catch((err) => {
this.emit("error", err);
});
}
listen() {
const server = http.createServer(this.handleRequest.bind(this)); //this.handleRequest来处理请求
server.listen(...arguments);
}
}
module.exports = Application;