如何从零开始手写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之选择组件的深入解析
Jun 19 Javascript
jQuery Ajax异步处理Json数据详解
Nov 05 Javascript
jquery设置text的值示例(设置文本框 DIV 表单值)
Jan 06 Javascript
js调用打印机打印网页字体总是缩小一号的解决方法
Jan 24 Javascript
JavaScript编程中容易出BUG的几点小知识
Jan 31 Javascript
javascript实现复选框超过限制即弹出警告框的方法
Feb 25 Javascript
Bootstrap3制作图片轮播效果
May 12 Javascript
javascript中json基础知识详解
Jan 19 Javascript
jQuery 实时保存页面动态添加的数据的示例
Aug 14 jQuery
vue-cli的eslint相关用法
Sep 29 Javascript
JS求Number类型数组中最大元素方法
Apr 08 Javascript
vue实现匀速轮播效果
Jun 29 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 filter_var() 函数 Filter 函数
2012/04/25 PHP
深入PHP curl参数的详解
2013/06/17 PHP
奉献出一个封装的curl函数 便于调用(抓数据专用)
2013/07/22 PHP
PHP类继承 extends使用介绍
2014/01/14 PHP
两种设置php载入页面时编码的方法
2014/07/29 PHP
PHP中$_SERVER使用说明
2015/07/05 PHP
CSS常用网站布局实例
2008/04/03 Javascript
jquery遍历input取得input的name
2009/04/27 Javascript
javascript针对DOM的应用分析(四)
2012/04/15 Javascript
ECMAScript 6即将带给我们新的数组操作方法前瞻
2015/01/06 Javascript
jquery实现的横向二级导航效果代码
2015/08/26 Javascript
学习掌握JavaScript中this的使用技巧
2016/08/29 Javascript
vue滚动轴插件better-scroll使用详解
2017/10/17 Javascript
vue 2.0 购物车小球抛物线的示例代码
2018/02/01 Javascript
浅谈在node.js进入文件目录的问题
2018/05/13 Javascript
Vue中props的使用详解
2018/06/15 Javascript
详解js访问对象的属性和方法
2018/10/25 Javascript
javascript实现对话框功能警告(alert 消息对话框)确认(confirm 消息对话框)
2019/05/07 Javascript
Javascript实现鼠标点击冒泡特效
2019/12/24 Javascript
Python实现快速排序算法及去重的快速排序的简单示例
2016/06/26 Python
Python实现字符串逆序输出功能示例
2017/06/24 Python
Python探索之修改Python搜索路径
2017/10/25 Python
实例详解Matlab 与 Python 的区别
2019/04/26 Python
使用python绘制温度变化雷达图
2019/10/18 Python
vue.js刷新当前页面的实例讲解
2020/12/29 Python
台湾流行服饰购物平台:OB严选
2018/01/21 全球购物
毕业生简单求职信
2013/11/19 职场文书
捐款倡议书范文
2014/02/02 职场文书
遗嘱继承公证书
2014/04/09 职场文书
乡镇四风对照检查材料
2014/08/31 职场文书
保证金退回承诺函格式
2015/01/21 职场文书
钢铁是怎样炼成的读书笔记
2015/06/29 职场文书
血轮眼轮回眼特效 html+css
2021/03/31 HTML / CSS
CSS3实现的3D隧道效果
2021/04/27 HTML / CSS
MySQL系列之五 视图、存储函数、存储过程、触发器
2021/07/02 MySQL
python实现一个简单的贪吃蛇游戏附代码
2022/06/28 Python