如何从零开始手写Koa2框架

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 主要代码,有一句古话 - 看万遍代码不如写上一遍。 还等什么,赶紧写上一遍吧~

当你能够写出来,再去阅读源码,你会发现源码如此简单~

以上是 如何从零开始手写Koa2框架 的全部内容, 来源链接: utcz.com/z/323883.html

回到顶部