从零开始手写Koa2框架

48次阅读

共计 8192 个字符,预计需要花费 21 分钟才能阅读完成。

01、介绍

Koa — 基于 Node.js 平台的下一代 web 开发框架
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造,致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。
与其对应的 Express 来比,Koa 更加小巧、精壮,本文将带大家从零开始实现 Koa 的源码,从根源上解决大家对 Koa 的困惑

本文 Koa 版本为 2.7.0, 版本不一样源码可能会有变动
02、源码目录介绍

Koa 源码目录截图

通过源码目录可以知道,Koa 主要分为 4 个部分,分别是:

application: Koa 最主要的模块, 对应 app 应用对象
context: 对应 ctx 对象
request: 对应 Koa 中请求对象
response: 对应 Koa 中响应对象

这 4 个文件就是 Koa 的全部内容了,其中 application 又是其中最核心的文件。我们将会从此文件入手,一步步实现 Koa 框架

03、实现一个基本服务器

代码目录

my-application
const {createServer} = require(‘http’);

module.exports = class Application {
constructor() {
// 初始化中间件数组, 所有中间件函数都会添加到当前数组中
this.middleware = [];
}
// 使用中间件方法
use(fn) {
// 将所有中间件函数添加到中间件数组中
this.middleware.push(fn);
}
// 监听端口号方法
listen(…args) {
// 使用 nodejs 的 http 模块监听端口号
const server = createServer((req, res) => {
/*
处理请求的回调函数,在这里执行了所有中间件函数
req 是 node 原生的 request 对象
res 是 node 原生的 response 对象
*/
this.middleware.forEach((fn) => fn(req, res));
})
server.listen(…args);
}
}

index.js
// 引入自定义模块
const MyKoa = require(‘./js/my-application’);
// 创建实例对象
const app = new MyKoa();
// 使用中间件
app.use((req, res) => {
console.log(‘ 中间件函数执行了~~~111’);
})
app.use((req, res) => {
console.log(‘ 中间件函数执行了~~~222’);
res.end(‘hello myKoa’);
})
// 监听端口号
app.listen(3000, err => {
if (!err) console.log(‘ 服务器启动成功了 ’);
else console.log(err);
})

运行入口文件 index.js 后,通过浏览器输入网址访问 http://localhost:3000/ , 就可以看到结果了~~
神奇吧!一个最简单的服务器模型就搭建完了。当然我们这个极简服务器还存在很多问题,接下来让我们一一解决

04、实现中间件函数的 next 方法

提取 createServer 的回调函数,封装成一个 callback 方法(可复用)
// 监听端口号方法
listen(…args) {
// 使用 nodejs 的 http 模块监听端口号
const server = createServer(this.callback());
server.listen(…args);
}
callback() {
const handleRequest = (req, res) => {
this.middleware.forEach((fn) => fn(req, res));
}
return handleRequest;
}

封装 compose 函数实现 next 方法
// 负责执行中间件函数的函数
function compose(middleware) {
// compose 方法返回值是一个函数,这个函数返回值是一个 promise 对象
// 当前函数就是调度
return (req, res) => {
// 默认调用一次,为了执行第一个中间件函数
return dispatch(0);
function dispatch(i) {
// 提取中间件数组的函数 fn
let fn = middleware[i];
// 如果最后一个中间件也调用了 next 方法,直接返回一个成功状态的 promise 对象
if (!fn) return Promise.resolve();
/*
dispatch.bind(null, i + 1)) 作为中间件函数调用的第三个参数,其实就是对应的 next
举个栗子:如果 i = 0 那么 dispatch.bind(null, 1))
–> 也就是如果调用了 next 方法 实际上就是执行 dispatch(1)
–> 它利用递归重新进来取出下一个中间件函数接着执行
fn(req, res, dispatch.bind(null, i + 1))
–> 这也是为什么中间件函数能有三个参数,在调用时我们传进来了
*/
return Promise.resolve(fn(req, res, dispatch.bind(null, i + 1)));
}
}
}

使用 compose 函数
callback () {
// 执行 compose 方法返回一个函数
const fn = compose(this.middleware);

const handleRequest = (req, res) => {
// 调用该函数,返回值为 promise 对象
// then 方法触发了, 说明所有中间件函数都被调用完成
fn(req, res).then(() => {
// 在这里就是所有处理的函数的最后阶段,可以允许返回响应了~
});
}

return handleRequest;
}

修改入口文件 index.js 代码
// 引入自定义模块
const MyKoa = require(‘./js/my-application’);
// 创建实例对象
const app = new MyKoa();
// 使用中间件
app.use((req, res, next) => {
console.log(‘ 中间件函数执行了~~~111’);
// 调用 next 方法,就是调用堆栈中下一个中间件函数
next();
})
app.use((req, res, next) => {
console.log(‘ 中间件函数执行了~~~222’);
res.end(‘hello myKoa’);
// 最后的 next 方法没发调用下一个中间件函数,直接返回 Promise.resolve()
next();
})
// 监听端口号
app.listen(3000, err => {
if (!err) console.log(‘ 服务器启动成功了 ’);
else console.log(err);
})

此时我们实现了 next 方法,最核心的就是 compose 函数,极简的代码实现了功能,不可思议!

05、处理返回响应

定义返回响应函数 respond
function respond(req, res) {
// 获取设置的 body 数据
let body = res.body;

if (typeof body === ‘object’) {
// 如果是对象,转化成 json 数据返回
body = JSON.stringify(body);
res.end(body);
} else {
// 默认其他数据直接返回
res.end(body);
}
}

在 callback 中调用
callback() {
const fn = compose(this.middleware);

const handleRequest = (req, res) => {
// 当中间件函数全部执行完毕时,会触发 then 方法,从而执行 respond 方法返回响应
const handleResponse = () => respond(req, res);
fn(req, res).then(handleResponse);
}

return handleRequest;
}

修改入口文件 index.js 代码
// 引入自定义模块
const MyKoa = require(‘./js/my-application’);
// 创建实例对象
const app = new MyKoa();
// 使用中间件
app.use((req, res, next) => {
console.log(‘ 中间件函数执行了~~~111’);
next();
})
app.use((req, res, next) => {
console.log(‘ 中间件函数执行了~~~222’);
// 设置响应内容,由框架负责返回响应~
res.body = ‘hello myKoa’;
})
// 监听端口号
app.listen(3000, err => {
if (!err) console.log(‘ 服务器启动成功了 ’);
else console.log(err);
})

此时我们就能根据不同响应内容做出处理了~ 当然还是比较简单的,可以接着去扩展~

06、定义 Request 模块
// 此模块需要 npm 下载
const parse = require(‘parseurl’);
const qs = require(‘querystring’);

module.exports = {
/**
* 获取请求头信息
*/
get headers() {
return this.req.headers;
},
/**
* 设置请求头信息
*/
set headers(val) {
this.req.headers = val;
},
/**
* 获取查询字符串
*/
get query() {
// 解析查询字符串参数 –> key1=value1&key2=value2
const querystring = parse(this.req).query;
// 将其解析为对象返回 –> {key1: value1, key2: value2}
return qs.parse(querystring);
}
}
07、定义 Response 模块
module.exports = {
/**
* 设置响应头的信息
*/
set(key, value) {
this.res.setHeader(key, value);
},
/**
* 获取响应状态码
*/
get status() {
return this.res.statusCode;
},
/**
* 设置响应状态码
*/
set status(code) {
this.res.statusCode = code;
},
/**
* 获取响应体信息
*/
get body() {
return this._body;
},
/**
* 设置响应体信息
*/
set body(val) {
// 设置响应体内容
this._body = val;
// 设置响应状态码
this.status = 200;
// json
if (typeof val === ‘object’) {
this.set(‘Content-Type’, ‘application/json’);
}
},
}
08、定义 Context 模块
// 此模块需要 npm 下载
const delegate = require(‘delegates’);

const proto = module.exports = {};

// 将 response 对象上的属性 / 方法克隆到 proto 上
delegate(proto, ‘response’)
.method(‘set’) // 克隆普通方法
.access(‘status’) // 克隆带有 get 和 set 描述符的方法
.access(‘body’)

// 将 request 对象上的属性 / 方法克隆到 proto 上
delegate(proto, ‘request’)
.access(‘query’)
.getter(‘headers’) // 克隆带有 get 描述符的方法

09、揭秘 delegates 模块
module.exports = Delegator;

/**
* 初始化一个 delegator.
*/
function Delegator(proto, target) {
// this 必须指向 Delegator 的实例对象
if (!(this instanceof Delegator)) return new Delegator(proto, target);
// 需要克隆的对象
this.proto = proto;
// 被克隆的目标对象
this.target = target;
// 所有普通方法的数组
this.methods = [];
// 所有带有 get 描述符的方法数组
this.getters = [];
// 所有带有 set 描述符的方法数组
this.setters = [];
}

/**
* 克隆普通方法
*/
Delegator.prototype.method = function(name){
// 需要克隆的对象
var proto = this.proto;
// 被克隆的目标对象
var target = this.target;
// 方法添加到 method 数组中
this.methods.push(name);
// 给 proto 添加克隆的属性
proto[name] = function(){
/*
this 指向 proto, 也就是 ctx
举个栗子:ctx.response.set.apply(ctx.response, arguments)
arguments 对应实参列表,刚好与 apply 方法传参一致
执行 ctx.set(‘key’, ‘value’) 实际上相当于执行 response.set(‘key’, ‘value’)
*/
return this[target][name].apply(this[target], arguments);
};
// 方便链式调用
return this;
};

/**
* 克隆带有 get 和 set 描述符的方法.
*/
Delegator.prototype.access = function(name){
return this.getter(name).setter(name);
};

/**
* 克隆带有 get 描述符的方法.
*/
Delegator.prototype.getter = function(name){
var proto = this.proto;
var target = this.target;
this.getters.push(name);
// 方法可以为一个已经存在的对象设置 get 描述符属性
proto.__defineGetter__(name, function(){
return this[target][name];
});

return this;
};

/**
* 克隆带有 set 描述符的方法.
*/
Delegator.prototype.setter = function(name){
var proto = this.proto;
var target = this.target;
this.setters.push(name);
// 方法可以为一个已经存在的对象设置 set 描述符属性
proto.__defineSetter__(name, function(val){
return this[target][name] = val;
});

return this;
};
10、使用 ctx 取代 req 和 res

修改 my-application
const {createServer} = require(‘http’);
const context = require(‘./my-context’);
const request = require(‘./my-request’);
const response = require(‘./my-response’);

module.exports = class Application {
constructor() {
this.middleware = [];
// Object.create(target) 以 target 对象为原型, 创建新对象, 新对象原型有 target 对象的属性和方法
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}

use(fn) {
this.middleware.push(fn);
}

listen(…args) {
// 使用 nodejs 的 http 模块监听端口号
const server = createServer(this.callback());
server.listen(…args);
}

callback() {
const fn = compose(this.middleware);

const handleRequest = (req, res) => {
// 创建 context
const ctx = this.createContext(req, res);
const handleResponse = () => respond(ctx);
fn(ctx).then(handleResponse);
}

return handleRequest;
}

// 创建 context 上下文对象的方法
createContext(req, res) {
/*
凡是 req/res,就是 node 原生对象
凡是 request/response,就是自定义对象
这是实现互相挂载引用,从而在任意对象上都能获取其他对象的方法
*/
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;

return context;
}
}
// 将原来使用 req,res 的地方改用 ctx
function compose(middleware) {
return (ctx) => {
return dispatch(0);
function dispatch(i) {
let fn = middleware[i];
if (!fn) return Promise.resolve();
return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)));
}
}
}

function respond(ctx) {
let body = ctx.body;
const res = ctx.res;
if (typeof body === ‘object’) {
body = JSON.stringify(body);
res.end(body);
} else {
res.end(body);
}
}

修改入口文件 index.js 代码
// 引入自定义模块
const MyKoa = require(‘./js/my-application’);
// 创建实例对象
const app = new MyKoa();
// 使用中间件
app.use((ctx, next) => {
console.log(‘ 中间件函数执行了~~~111’);
next();
})
app.use((ctx, next) => {
console.log(‘ 中间件函数执行了~~~222’);
// 获取请求头参数
console.log(ctx.headers);
// 获取查询字符串参数
console.log(ctx.query);
// 设置响应头信息
ctx.set(‘content-type’, ‘text/html;charset=utf-8’);
// 设置响应内容,由框架负责返回响应~
ctx.body = ‘<h1>hello myKoa</h1>’;
})
// 监听端口号
app.listen(3000, err => {
if (!err) console.log(‘ 服务器启动成功了 ’);
else console.log(err);
})

到这里已经写完了 Koa 主要代码,有一句古话 – 看万遍代码不如写上一遍。还等什么,赶紧写上一遍吧~ 当你能够写出来,再去阅读源码,你会发现源码如此简单~

正文完
 0