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

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

如何从零开始手写Koa2框架的更多相关文章

  1. 如何从零开始手写Koa2框架

    这篇文章主要介绍了如何从零开始手写Koa2框架,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

随机推荐

  1. Error: Cannot find module ‘node:util‘问题解决

    控制台 安装 Vue-Cli 最后一步出现 Error: Cannot find module 'node:util' 问题解决方案1.问题C:\Windows\System32>cnpm install -g @vue/cli@4.0.3internal/modules/cjs/loader.js:638 throw err; &nbs

  2. yarn的安装和使用(全网最详细)

    一、yarn的简介:Yarn是facebook发布的一款取代npm的包管理工具。二、yarn的特点:速度超快。Yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快。超级安全。在执行代码之前,Yarn 会通过算法校验每个安装包的完整性。超级可靠。使用详细、简洁的锁文件格式和明确的安装算法,Yarn 能够保证在不同系统上无差异的工作。三、y

  3. 前端环境 本机可切换node多版本 问题源头是node使用的高版本

    前言投降投降 重头再来 重装环境 也就分分钟的事 偏要折腾 这下好了1天了 还没折腾出来问题的源头是node 使用的高版本 方案那就用 本机可切换多版本最终问题是因为nodejs的版本太高,导致的node-sass不兼容问题,我的node是v16.14.0的版本,项目中用了"node-sass": "^4.7.2"版本,无法匹配当前的node版本根据文章的提

  4. nodejs模块学习之connect解析

    这篇文章主要介绍了nodejs模块学习之connect解析,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  5. nodejs npm package.json中文文档

    这篇文章主要介绍了nodejs npm package.json中文文档,本文档中描述的很多行为都受npm-config(7)的影响,需要的朋友可以参考下

  6. 详解koa2学习中使用 async 、await、promise解决异步的问题

    这篇文章主要介绍了详解koa2学习中使用 async 、await、promise解决异步的问题,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  7. Node.js编写爬虫的基本思路及抓取百度图片的实例分享

    这篇文章主要介绍了Node.js编写爬虫的基本思路及抓取百度图片的实例分享,其中作者提到了需要特别注意GBK转码的转码问题,需要的朋友可以参考下

  8. CentOS 8.2服务器上安装最新版Node.js的方法

    这篇文章主要介绍了CentOS 8.2服务器上安装最新版Node.js的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  9. node.js三个步骤实现一个服务器及Express包使用

    这篇文章主要介绍了node.js三个步骤实现一个服务器及Express包使用,文章通过新建一个文件展开全文内容,具有一定的参考价值,需要的小伙伴可以参考一下

  10. node下使用UglifyJS压缩合并JS文件的方法

    下面小编就为大家分享一篇node下使用UglifyJS压缩合并JS文件的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

返回
顶部