如何从零开始手写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添加输出窗口的代码
Feb 07 Javascript
jquery如何改变html标签的样式(两种实现方法)
Jan 16 Javascript
javascript的alert box在java中如何显示多行
May 18 Javascript
JavaScript实现判断图片是否加载完成的3种方法整理
Mar 13 Javascript
JS实现页面中所有img对象添加onclick事件及新窗口查看图片的方法
Dec 27 Javascript
微信小程序 JS动态修改样式的实现代码
Feb 10 Javascript
详解在WebStorm中添加Vue.js单文件组件的高亮及语法支持
Oct 21 Javascript
详解如何实现一个简单的 vuex
Feb 10 Javascript
vue keep-alive请求数据的方法示例
May 16 Javascript
Vue循环组件加validate多表单验证的实例
Sep 18 Javascript
vue-cli设置publicPath小记
Apr 14 Javascript
js实现拖拽与碰撞检测
Sep 18 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
虹吸式咖啡壶操作
2021/03/03 冲泡冲煮
session 的生命周期是多长
2006/10/09 PHP
关于php连接mssql:pdo odbc sql server
2011/07/20 PHP
php使用反射插入对象示例分享
2014/03/11 PHP
最常用的8款PHP调试工具
2014/07/06 PHP
PHP获取文件相对路径的方法
2015/02/26 PHP
微信公众平台开发实现2048游戏的方法
2015/04/15 PHP
PHP iconv()函数字符编码转换的问题讲解
2019/03/22 PHP
PHP autoload使用方法及步骤详解
2020/09/05 PHP
仿迅雷焦点广告效果(JQuery版)
2008/11/19 Javascript
js 利用className得到对象的实现代码
2011/11/15 Javascript
页面刷新时记住滚动条的位置jquery代码
2014/06/17 Javascript
jQuery的deferred对象详解
2014/11/12 Javascript
JS更改select内option属性的方法
2015/10/14 Javascript
Bootstrap每天必学之简单入门
2015/11/19 Javascript
Node.js插件安装图文教程
2016/05/06 Javascript
PassWord输入框代码分享
2016/06/07 Javascript
ionic由于使用了header和subheader导致被遮挡的问题的两种解决方法
2016/09/22 Javascript
详解JavaScript权威指南之对象
2016/09/27 Javascript
jQuery实现的分页功能示例
2017/01/22 Javascript
想用好React的你必须要知道的一些事情
2017/07/24 Javascript
iview table高度动态设置方法
2018/03/14 Javascript
详解es6新增数组方法简便了哪些操作
2019/05/09 Javascript
JavaScript canvas绘制圆弧与圆形
2020/02/18 Javascript
js实现简易ATM功能
2020/10/27 Javascript
Python中的复制操作及copy模块中的浅拷贝与深拷贝方法
2016/07/02 Python
使用requests库制作Python爬虫
2018/03/25 Python
Python使用cx_Oracle模块操作Oracle数据库详解
2018/05/07 Python
Python实现快速大文件比较代码解析
2020/09/04 Python
关于HTML5你必须知道的28个新特性,新技巧以及新技术
2012/05/28 HTML / CSS
采购部部门职责
2013/12/15 职场文书
小学生美德少年事迹
2014/02/02 职场文书
家长学校实施方案
2014/03/15 职场文书
团代会宣传工作方案
2014/05/08 职场文书
中文专业自荐书
2014/06/29 职场文书
2016年六一文艺汇演开幕词
2016/03/04 职场文书