如何从零开始手写Koa2框架


Posted in Javascript onMarch 22, 2019

01、介绍

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

02、源码目录介绍

Koa 源码目录截图

如何从零开始手写Koa2框架

通过源码目录可以知道,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 主要代码,有一句古话 - 看万遍代码不如写上一遍。 还等什么,赶紧写上一遍吧~
当你能够写出来,再去阅读源码,你会发现源码如此简单~

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jQuery使用手册之 事件处理
Mar 24 Javascript
Jquery+CSS3实现一款简洁大气带滑动效果的弹出层
May 15 Javascript
js用正则表达式来验证表单(比较齐全的资源)
Nov 17 Javascript
深入理解JavaScript系列(30):设计模式之外观模式详解
Mar 03 Javascript
jQuery解析XML与传统JavaScript方法的差别实例分析
Mar 05 Javascript
javaScript实现可缩放的显示区效果代码
Oct 26 Javascript
JavaScript手机振动API
Jun 11 Javascript
完美的js图片轮换效果
Feb 05 Javascript
解决linux下node.js全局模块找不到的问题
May 15 Javascript
JavaScript变量提升和严格模式实例分析
Jan 27 Javascript
vue过滤器用法实例分析
Mar 15 Javascript
使用 JavaScript 创建并下载文件(模拟点击)
Oct 25 Javascript
Vue服务端渲染实践之Web应用首屏耗时最优化方案
Mar 22 #Javascript
详解ES6中的Map与Set集合
Mar 22 #Javascript
js控制随机数生成概率代码实例
Mar 21 #Javascript
详解bootstrap-fileinput文件上传控件的亲身实践
Mar 21 #Javascript
详解基于React.js和Node.js的SSR实现方案
Mar 21 #Javascript
javascript中call()、apply()的区别
Mar 21 #Javascript
vue实现微信获取用户信息的方法
Mar 21 #Javascript
You might like
php结合表单实现一些简单功能的例子
2011/06/04 PHP
浅谈ThinkPHP的URL重写
2014/11/25 PHP
作为程序员必知的16个最佳PHP库
2015/12/09 PHP
ThinkPHP 框架实现的读取excel导入数据库操作示例
2020/04/14 PHP
Display SQL Server Version Information
2007/06/21 Javascript
jquery tab标签页的制作
2010/05/10 Javascript
超简单JS二级、多级联动的简单实例
2014/02/18 Javascript
node.js中的events.emitter.listeners方法使用说明
2014/12/10 Javascript
jQuery匹配文档链接并添加class的方法
2015/06/26 Javascript
Jquery常用的方法汇总
2015/09/01 Javascript
jQuery插入节点和移动节点用法示例(insertAfter、insertBefore方法)
2016/09/08 Javascript
Javascript blur与click冲突解决办法
2017/01/09 Javascript
webpack学习笔记之代码分割和按需加载的实例详解
2017/07/20 Javascript
JavaScript实现焦点进入文本框内关闭输入法的核心代码
2017/09/20 Javascript
微信sdk实现禁止微信分享(使用原生php实现)
2019/11/15 Javascript
在Vue中使用antv的示例代码
2020/06/29 Javascript
Vue时间轴 vue-light-timeline的用法说明
2020/10/29 Javascript
python del()函数用法
2013/03/24 Python
使用Python的Bottle框架写一个简单的服务接口的示例
2015/08/25 Python
轻松实现python搭建微信公众平台
2016/02/16 Python
Python 实现选择排序的算法步骤
2018/04/22 Python
python破解zip加密文件的方法
2018/05/31 Python
浅析python继承与多重继承
2018/09/13 Python
Python将列表数据写入文件(txt, csv,excel)
2019/04/03 Python
image-set实现Retina屏幕下图片显示详细介绍
2012/12/24 HTML / CSS
酒后驾驶检讨书
2014/01/27 职场文书
空气的环保标语
2014/06/12 职场文书
领导干部“四风”查摆问题个人整改措施
2014/10/28 职场文书
2014年科室工作总结
2014/11/20 职场文书
肖申克的救赎观后感
2015/06/02 职场文书
先进教师个人主要事迹材料
2015/11/03 职场文书
高中物理教学反思
2016/02/19 职场文书
python 如何将两个实数矩阵合并为一个复数矩阵
2021/05/19 Python
mysql获取指定时间段中所有日期或月份的语句(不设存储过程,不加表)
2021/06/18 MySQL
SpringDataJPA实体类关系映射配置方式
2021/12/06 Java/Android