前言
Koa是继Express后新的Node框架,由Express原班人马开发,相比Express更加简洁,源码只有2000多行,结合最新的ECMA语法,这使得Koa更小
更具有表现力
更健壮
,因为每个中间件的执行结果都是Promise,结合Async Await抛弃复杂的传统回调形式。并且错误结果处理起来也更加方便
创建一个简单的Koa程序
// yarn add koa
const Koa = require('koa');
const app = new Koa();
app.use((ctx)=>{
ctx.body = 'Hello Koa';
})
app.listen(9001,()=>{
console.log('🎉服务开启成功,端口号为:9001')
})
分析源码并实现自己的Koa
一、目录分析
- 创建一个新的文件夹,使用npm init初始项目,package.json中添加启动命令
{
"name": "koa-server",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"serve": "nodemon server.js"
}
}
- 文件夹内新建koa文件夹并使用npm init初始项目,package.json中指定入口文件
{
"name": "koa",
"version": "1.0.0",
"main": "./lib/application.js"
}
- 在koa文件中新建lib文件,在lib文件中新建application.js context.js request.js response.js
二、分析并实现request.js文件
- Koa源码中request.js文件做了很多请求相关的参数处理,通过get/set的访问方式对属性进行了包装,使用户获取属性更加方便
- 实现自己的request.js
- 内部的this指向ctx.request,所以ctx.request上面必须有req对象,该对象指向原生的request对象
const url = require('url');
module.exports = {
get query() {
const { query } = url.parse(this.req.url);
return query;
},
get path() {
const { pathname } = url.parse(this.req.url);
return pathname;
},
};
- 实现context.js
- context除了提供自身方法和属性外,还对其他属性进行了委托
(将请求相关的属性委托到ctx.requset上,将响应相关的属性和方法代理到ctx.response)
- 用户访问ctx.body其实访问的是ctx.request.body
(后续创建上下文对象ctx时,会将request挂载到ctx身上)
- delegate的原理就是
__defineGetter__
,__defineSetter__
属性,可以访问对象属性时,将属性委托到其他对象身上
- context除了提供自身方法和属性外,还对其他属性进行了委托
const delegate = require('delegates');
const proto = (module.exports = {
// 给context自身添加属性和方法
toJSON() {
return {};
},
});
// 当直接访问ctx.xx时 委托到ctx.response.xx身上
delegate(proto, 'response')
.access('body')
.access('status');
// 当直接访问ctx.xx时 委托到ctx.request.xx身上
delegate(proto, 'request')
.access('query')
.access('path')
.access('url');
- delegatene内部也是通过
__defineGetter__
,__defineSetter__
两种方法实现的属性委托\ - 上面的context实现方式,也可以通过下面
__defineGetter__
,__defineSetter__
直接实现
const proto = (module.exports = {
// 给context自身添加属性和方法
toJSON() {
return {};
},
});
function defineGetters(taregt, key) {
proto.__defineGetter__(key, function() {
return this[taregt][key];
});
}
defineGetters('request', 'query');
defineGetters('request', 'path');
defineGetters('request', 'url');
defineGetters('response', 'body');
defineGetters('response', 'status');
function defineSetters(target, key) {
proto.__defineSetter__(key, function(value) {
this[target][key] = value;
});
}
defineSetters('response', 'body');
defineSetters('response', 'status');
- 分析并实现response.js
- response内部通过get set 提供了很多响应相关的属性和方法
- 简单实现自己的response.js
简单实现:
- 因为上面实现context是对response添加了属性代理,当context直接访问body或status属性时,其实访问的是context.response.body(后续只需实现将response对象挂载到body上面即可)
const response = {
_body: undefined,
get body() {
return this._body;
},
set body(value) {
this._body = value;
},
get status() {
return this.res.statusCode;
},
set status(code) {
this.res.statusCode = code;
},
};
module.exports = response;
- 剖析application源码并实现它
(1)构造函数
- 继承Events函数,可以直接订阅或发布事件
- 通过Object.create()分别创建context,request,response对象,目的是为了基于原型链创建一个新对象,避免全局中多个程序造成对象引用污染
- 创建中报错间件的集合middleware
module.exports = class Application extends EventEmitter {
constructor() {
super();
// 创建全新的context request response对象
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
// 保存中间件的数组
this.middleware = [];
}
}
(2)use()
- 验证并添加中间件
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
// 将注册的中间件添加到数组中管理
this.middleware.push(fn);
}
(3)listen()
- 通过http创建server,通过this.callback完成回调
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
(4)callback()
- 通过调用compose包装中间件,返回一个可执行的函数,调用该函数则开始执行中间件
- 创建请求相关的处理函数,内部创建全局上下文对象ctx,将ctx和中间件的调用函数交给this.handleRequest函数处理
callback() {
// fn函数内部将执行注册的中间件
const fn = this.compose();
// 处理request请求
const handleRequest = (req, res) => {
// 创建上下文对象ctx
const ctx = this.createContext(req, res);
this.handleRequest(ctx, fn);
};
return handleRequest;
}
(5)compose()
- 默认直接执行第一个中间件
- 没有中间件或中间件执行完毕直接返回成功的结果
- 记录上一个中间件的索引index,防止一个中间件内多次调用next()
- 递归调用dispatch(),中间件的第一个参数是ctx对象,第二个参数next为 dispatch(i+1)
compose() {
// 每个中间价必须是个方法
for (const fn of this.middleware) {
if (typeof fn !== 'function')
throw new TypeError('Middleware must be composed of functions!');
}
// 开始执行中间件
return ctx => {
// 上一个中间件的索引
let index = -1;
const dispatch = i => {
// 防止中间内多次调用next函数
if (i <= index) return Promise.reject(new Error('next() called multiple times'));
index = i;
// 没有中间件 或执行完最后一个中间件 直接返回成功
if (this.middleware.length === i) return Promise.resolve();
let fn = this.middleware[i];
try {
// next 函数内部调用了dispatch,并且直接执行下一个中间件
let next = () => dispatch.bind(null, i + 1);
return Promise.resolve(fn(ctx, next()));
} catch (err) {
return Promise.reject(err);
}
};
// 默认直接执行第一个中间件
return dispatch(0);
};
}
(6)createContext()
- 每一个请求都需要有一个全新的上下文对象,通过Object.create创建
- 将request,response对象挂载到上下对象ctx身上,方便通过__defineGetter__和__defineSetter__进行属性委托
createContext(req, res) {
// 基于原型链创建新的ctx request response(避免不同的请求污染)
const ctx = Object.create(this.context);
const request = Object.create(this.request);
const response = Object.create(this.response);
ctx.request = request; // 上下文对象中保存包装后的request对象
ctx.request.req = ctx.req = req; // 将原生的request对象分别挂载到 ctx.request 和 ctx上
ctx.response = response; // 上下文对象中保存包装后的response对象
ctx.response.res = ctx.res = res; // 将原生的response对象分别挂载到 ctx.response 和 ctx上
return ctx;
}
(7)handleRequest()
- 创建默认的状态码
- 执行全部中间件
handleRequest(ctx, fn) {
// 默认的状态码
ctx.res.statusCode = 404;
// 不同情况的响应处理
const handleResponse = () => this.respond(ctx)
// 执行中间件 全部中间件成功执行完毕 执行respond响应结果
fn(ctx)
.then(handleResponse)
.catch(err => {
this.emit('error', err);
});
}
(8)respond()
respond(ctx) {
// [1] 这里上下文对象的body其实是代理的response对象中的body
// [2] ctx.body ==== ctx.response.body
// [3] Koa源码中使用delegate函数完成代理 (__defineGetter__ , __defineSetter__)
const body = ctx.body || 'Not Define';
return ctx.res.end(body);
}
(9)自己实现的application.js完整流程
const request = require('./request');
const response = require('./response');
const context = require('./context');
const http = require('http');
const EventEmitter = require('events');
module.exports = class Application extends EventEmitter {
constructor() {
super();
// 创建全新的context request response对象
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
// 保存中间件的数组
this.middleware = [];
}
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
// 将注册的中间件添加到数组中管理
this.middleware.push(fn);
}
createContext(req, res) {
// 基于原型链创建新的ctx request response(避免不同的请求污染)
const ctx = Object.create(this.context);
const request = Object.create(this.request);
const response = Object.create(this.response);
ctx.request = request; // 上下文对象中保存包装后的request对象
ctx.request.req = ctx.req = req; // 将原生的request对象分别挂载到 ctx.request 和 ctx上
ctx.response = response; // 上下文对象中保存包装后的response对象
ctx.response.res = ctx.res = res; // 将原生的response对象分别挂载到 ctx.response 和 ctx上
return ctx;
}
respond(ctx) {
// [1] 这里上下文对象的body其实是代理的response对象中的body
// [2] ctx.body ==== ctx.response.body
// [3] Koa源码中使用delegate函数完成代理 (__defineGetter__ , __defineSetter__)
const body = ctx.body || 'Not Define';
return ctx.res.end(body);
}
compose() {
// 每个中间价必须是个方法
for (const fn of this.middleware) {
if (typeof fn !== 'function')
throw new TypeError('Middleware must be composed of functions!');
}
// 开始执行中间件
return ctx => {
let index = -1;
const dispatch = i => {
// 防止中间内多次调用next函数
if (i <= index) return Promise.reject(new Error('next() called multiple times'));
index = i;
// 没有中间件 或执行完最后一个中间件 直接返回成功
if (this.middleware.length === i) return Promise.resolve();
let fn = this.middleware[i];
try {
// next 函数内部调用了dispatch,并且直接执行下一个中间件
let next = () => dispatch.bind(null, i + 1);
return Promise.resolve(fn(ctx, next()));
} catch (err) {
return Promise.reject(err);
}
};
// 默认直接执行第一个中间件
return dispatch(0);
};
}
handleRequest(ctx, fn) {
// 默认的状态码
ctx.res.statusCode = 404;
// 不同情况的响应处理
const handleResponse = () => this.respond(ctx);
// 执行中间件 全部中间件成功执行完毕 执行respond响应结果
fn(ctx)
.then(handleResponse)
.catch(err => {
this.emit('error', err);
});
}
callback() {
// fn函数内部将执行注册的中间件
const fn = this.compose();
// 处理request请求
const handleRequest = (req, res) => {
// 创建上下文对象ctx
const ctx = this.createContext(req, res);
this.handleRequest(ctx, fn);
};
return handleRequest;
}
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
};
(9)调用并测试自己的koa程序
const Koa = require('./koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(8);
});
app.use(async (ctx, next) => {
console.log(2);
await next();
console.log(7);
});
app.use(async (ctx, next) => {
console.log(3);
await next();
console.log(6);
});
app.use(async (ctx, next) => {
console.log(4);
ctx.status = 201;
ctx.body = 'Hello Koa';
await next();
console.log(5);
});
app.listen(9001, () => {
console.log('🎉服务开启成功,端口号为:9001');
});
总结
本文通过浅析Koa源码,并手动实现一个Koa程序来帮助学习Koa或加深对Koa的理解,希望本文章对您学习Koa有所帮助
- 实现request.js文件,
- 通过get/set访问或设置属性,封装更多请求参数和方法,并导出包装后的该对象。
- 实现response.js文件
- 通过get/set访问或设置属性,封装更多响应时的参数和方法,并导出包装后的该对象。
- 实现context.js文件
- 内部除了实现自身方法外,通过_defineGetter_,_defineSetter_,将属性的访问和设置,委托到request对象或response对象身上。
- 实现application.js文件
- use() 添加中间件
- listen() 创建server,通过this.callback完成回调
- callback() 调用compose包装中间件,创建上下文对象,使用handleRequest处理请求响应
- compose() 执行中间件 (洋葱模型)
- createContext() 创建ctx上下文对象,并将response,request对象添加到ctx上,方便通过委托访问(ctx.body === ctx.response.body)
- handleRequest() 处理服务响应,创建状态码,执行中间件
- respond() 不同情况的响应处理