如何从零开始手写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 相关文章推荐
JavaScript加密解密7种方法总结分析
Oct 07 Javascript
JS面向对象、prototype、call()、apply()
May 14 Javascript
web的各种前端打印方法之jquery打印插件PrintArea实现网页打印
Jan 09 Javascript
jquery实现一个简单好用的弹出框
Sep 26 Javascript
jQuery+easyui中的combobox实现下拉框特效
Feb 27 Javascript
jquery插件jquery.nicescroll实现图片无滚动条左右拖拽的方法
Aug 10 Javascript
javascript使用Promise对象实现异步编程
Mar 01 Javascript
Omi v1.0.2发布正式支持传递javascript表达式
Mar 21 Javascript
Vue Spa切换页面时更改标题的实例代码
Jul 15 Javascript
javascript变量提升和闭包理解
Mar 12 Javascript
微信小程序websocket实现聊天功能
Mar 30 Javascript
JS代码触发事件代码实例
Jan 02 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
2006/12/13 PHP
thinkphp模板赋值与替换实例简述
2014/11/24 PHP
php使用Imagick生成图片的方法
2015/07/31 PHP
关于IFRAME 自适应高度的研究
2006/07/20 Javascript
用js实现层随着内容大小动态渐变改变 推荐
2009/12/19 Javascript
js 获取服务器控件值的代码
2010/03/05 Javascript
aspx中利用js实现确认删除代码
2010/07/22 Javascript
js获取select标签选中值的两种方式
2014/01/09 Javascript
基于JQuery实现滚动到页面底端时自动加载更多信息
2014/01/31 Javascript
验证码在IE中不刷新而谷歌等浏览器正常的解决方案
2014/03/18 Javascript
jQuery中even选择器的定义和用法
2014/12/23 Javascript
jquery获取当前日期的方法
2015/01/14 Javascript
js实现的动画导航菜单效果代码
2015/09/10 Javascript
关于vue中 $emit的用法详解
2018/04/12 Javascript
原生JS实现的雪花飘落动画效果
2018/05/03 Javascript
微信小程序中遇到的iOS兼容性问题小结
2018/11/14 Javascript
JavaScript使用百度ECharts插件绘制饼图操作示例
2019/11/26 Javascript
JS图片懒加载的优点及实现原理
2020/01/10 Javascript
[45:52]2018DOTA2亚洲邀请赛 4.1小组赛 A组加赛 LGD vs Liquid
2018/04/02 DOTA
[01:05:41]EG vs Optic Supermajor 败者组 BO3 第二场 6.6
2018/06/07 DOTA
python实现批量改文件名称的方法
2015/05/25 Python
Python实现将sqlite数据库导出转成Excel(xls)表的方法
2017/07/17 Python
Python拼接微信好友头像大图的实现方法
2018/08/01 Python
Python实现的线性回归算法示例【附csv文件下载】
2018/12/29 Python
利用Django提供的ModelForm增删改数据的方法
2019/01/06 Python
python3.6使用tkinter实现弹跳小球游戏
2019/05/09 Python
Python依赖包整体迁移方法详解
2019/08/15 Python
Python3常用内置方法代码实例
2019/11/18 Python
python hash每次调用结果不同的原因
2019/11/21 Python
Tensorflow 模型转换 .pb convert to .lite实例
2020/02/12 Python
python实现密码强度校验
2020/03/18 Python
Python extract及contains方法代码实例
2020/09/11 Python
社区优秀志愿者材料
2014/02/02 职场文书
大宝sod蜜广告词
2014/03/21 职场文书
教师读书笔记
2015/06/29 职场文书
趣味运动会标语口号
2015/12/26 职场文书