携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第28天,点击查看活动详情
手写 koa
koa 👉👉👉👉👉 kao
模仿 koa,搭建最基本的骨架:
kao/application.js
const http = require('http');
class Application {
constructor() {
// 回调函数
this.callbackFn = null;
}
use(fn) {
this.callbackFn = fn;
}
callback() {
return (req, res) => this.callbackFn(req, res)
}
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
}
module.exports = Application;
测试文件kao/index.js
const Kao = require('./application');
const app = new Kao();
app.use(async (req, res) => {
res.writeHead(200);
res.end('hello world');
});
app.listen(3001, () => {
console.log('server start at 3001');
});
通过new实例一个对象,use注册回调函数,listen启动server并传入回调。
使用 get 和 set 来处理原生请求,进行代理
kao/request.js
module.exports = {
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
get url() {
return this.req.url;
},
set url(val) {
this.req.url = val;
},
}
当访问request.url时,其实就是在访问原生的req.url。
kao/response.js
module.exports = {
get status() {
return this.res.statusCode;
},
set status(code) {
this.res.statusCode = code;
},
get body() {
return this._body;
},
set body(val) {
// 源码里有对val类型的各种判断,这里省略
/* 可能的类型
1. string
2. Buffer
3. Stream
4. Object
*/
this._body = val;
}
}
处理上下文对象 context
const delegate = require('delegates');
const proto = module.exports = {
// context自身的方法
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>'
};
},
}
// delegates 原理就是__defineGetter__和__defineSetter__
// method是委托方法,getter委托getter,access委托getter和setter。
// proto.status => proto.response.status
delegate(proto, 'response')
.access('status')
.access('body')
// proto.url = proto.request.url
delegate(proto, 'request')
.access('url')
.getter('header')
真正注入原生对象,是在application.js里的createContext方法中注入
const http = require('http');
const context = require('./context');
const request = require('./request');
const response = require('./response');
class Application {
constructor() {
this.callbackFn = null;
// 每个Kao实例的context request respones
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn) {
this.callbackFn = fn;
}
callback() {
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx)
};
return handleRequest;
}
handleRequest(ctx) {
const handleResponse = () => respond(ctx);
// callbackFn是个async函数,最后返回promise对象
return this.callbackFn(ctx).then(handleResponse);
}
createContext(req, res) {
// 针对每个请求,都要创建ctx对象
// 每个请求的ctx request response
// ctx代理原生的req res就是在这里代理的
let ctx = Object.create(this.context);
ctx.request = Object.create(this.request);
ctx.response = Object.create(this.response);
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
ctx.app = ctx.request.app = ctx.response.app = this;
return ctx;
}
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
}
module.exports = Application;
function respond(ctx) {
// 根据ctx.body的类型,返回最后的数据
/* 可能的类型,代码删减了部分判断
1. string
2. Buffer
3. Stream
4. Object
*/
let content = ctx.body;
if (typeof content === 'string') {
ctx.res.end(content);
}
else if (typeof content === 'object') {
ctx.res.end(JSON.stringify(content));
}
}
代码中使用了Object.create的方法创建一个全新的对象,通过原型链继承原来的属性。这样可以有效的防止污染原来的对象。
createContext在每次http请求时都会调用,每次调用都新生成一个ctx对象,并且代理了这次http请求的原生的对象。
respond才是最后返回http响应的方法。根据执行完所有中间件后ctx.body的类型,调用res.end结束此次http请求。
洋葱圈模型的真正实现是 koa-compose,原理前面的源码分析已经讲过了。这里我们直接使用。
kao/compose.js
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
return dispatch(0)
function dispatch (i) {
// 一个中间件里多次调用next
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// fn就是当前的中间件
let fn = middleware[i]
if (i === middleware.length) fn = next // 最后一个中间件如果也next时进入(一般最后一个中间件是直接操作ctx.body,并不需要next了)
if (!fn) return Promise.resolve() // 没有中间件,直接返回成功
try {
/*
* 使用了bind函数返回新的函数,类似下面的代码
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
*/
// dispatch.bind(null, i + 1)就是中间件里的next参数,调用它就可以进入下一个中间件
// fn如果返回的是Promise对象,Promise.resolve直接把这个对象返回
// fn如果返回的是普通对象,Promise.resovle把它Promise化
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
// 中间件是async的函数,报错不会走这里,直接在fnMiddleware的catch中捕获
// 捕获中间件是普通函数时的报错,Promise化,这样才能走到fnMiddleware的catch方法
return Promise.reject(err)
}
}
}
}
module.exports = compose;
修改 kao/application.js
class Application {
constructor() {
this.middleware = []; // 存储中间件
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn) {
this.middleware.push(fn); // 存储中间件
}
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
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
callback() {
// 合成所有中间件
const fn = this.compose(this.middleware);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn)
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const handleResponse = () => respond(ctx);
// 执行中间件并把最后的结果交给respond
return fnMiddleware(ctx).then(handleResponse);
}
createContext(req, res) {
// 针对每个请求,都要创建ctx对象
let ctx = Object.create(this.context);
ctx.request = Object.create(this.request);
ctx.response = Object.create(this.response);
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
ctx.app = ctx.request.app = ctx.response.app = this;
return ctx;
}
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
}
module.exports = Application;
function respond(ctx) {
let content = ctx.body;
if (typeof content === 'string') {
ctx.res.end(content);
}
else if (typeof content === 'object') {
ctx.res.end(JSON.stringify(content));
}
}
修改 index.js 进行测试
const Kao = require('./application');
const app = new Kao();
app.use(async (ctx, next) => {
console.log('1-start');
await next();
console.log('1-end');
})
app.use(async (ctx) => {
console.log('2-start');
ctx.body = 'hello tc';
console.log('2-end');
});
app.listen(3001, () => {
console.log('server start at 3001');
});
// 1-start 2-start 2-end 1-end
错误处理机制
因为compose组合之后的函数返回的仍然是Promise对象,所以我们可以在catch捕获异常
kao/application.js
handleRequest(ctx, fnMiddleware) {
const handleResponse = () => respond(ctx);
const onerror = err => ctx.onerror(err);
// catch捕获,触发ctx的onerror方法
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
kao/context.js
const proto = module.exports = {
// context自身的方法
onerror(err) {
// 中间件报错捕获
const { res } = this;
if ('ENOENT' == err.code) {
err.status = 404;
} else {
err.status = 500;
}
this.status = err.status;
res.end(err.message || 'Internal error');
}
}
kao/index.js
const Kao = require('./application');
const app = new Kao();
app.use(async (ctx) => {
// 报错可以捕获
a.b.c = 1;
ctx.body = 'hello tc';
});
app.listen(3001, () => {
console.log('server start at 3001');
});
让Application继承原生的Emitter,从而实现error监听,实现框架层发生错误的捕获机制
kao/application.js
const Emitter = require('events');
// 继承Emitter
class Application extends Emitter {
constructor() {
// 调用super
super();
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
}
kao/context.js
const proto = module.exports = {
onerror(err) {
const { res } = this;
if ('ENOENT' == err.code) {
err.status = 404;
} else {
err.status = 500;
}
this.status = err.status;
// 触发error事件
this.app.emit('error', err, this);
res.end(err.message || 'Internal error');
}
}
kao/index.js
const Kao = require('./application');
const app = new Kao();
app.use(async (ctx) => {
// 报错可以捕获
a.b.c = 1;
ctx.body = 'hello tc';
});
app.listen(3001, () => {
console.log('server start at 3001');
});
// 监听error事件
app.on('error', (err) => {
console.log(err.stack);
});
参考文章: