资讯专栏INFORMATION COLUMN

5分钟实现一个Koa

Panda / 502人阅读

摘要:所以方法实现如下每次有新的请求,我们都需要把这次请求的上下文灌进数组中的每一个中间件里。笔者在写了这个分钟以后去看了源码,发现实现思路基本就是这样,相信经过我的这个分钟的洗礼,你去看源码一样小菜一碟。

原文地址

周五组内同学讨论搞一些好玩的东西,有人提到了类似『5分钟实现koa』,『100行实现react』的创意,仔细想了以后,5分钟实现koa并非不能实现,遂有了这篇博客。
准备

先打开koa官网,随意找出了一个代表koa核心功能的的demo就可以,如下

const Koa = require("koa");
const app = new Koa();

// x-response-time
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set("X-Response-Time", `${ms}ms`);
});

// logger
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});

// response
app.use(async ctx => {
  ctx.body = "Hello World";
});

app.listen(3000);

最终要实现的效果是实现的一个5min-koa模块,直接将代码中第一行替换为const Koa = require("./5min-koa");,程序可以正常执行就可以了。

Koa的核心

通过koa官网得知,app.listen方法实际上是如下代码的简写

const http = require("http");
const Koa = require("koa");
const app = new Koa();
http.createServer(app.callback()).listen(3000);

所以我们可以先把app.listen实现出来

class Koa {
  constructor() {}
  callback() {
    return (req, res) => {
      // TODO
    }
  }
  listen(port) {
    http.createServer(this.callback()).listen(port);
  }
}

koa的核心分为四部分,分别是

context 上下文

middleware 中间件

request 请求

responce 响应

Context

我们先来实现一个最简化版的context,如下

class Context {
  constructor(app, req, res) {
    this.app = app
    this.req = req
    this.res = res
    // 为了尽可能缩短实现时间,我们直接使用原生的res和req,没有实现ctx上的ctx.request ctx.response
    // ctx.request ctx.response只是在原生res和req上包装处理了一层
  }
  // 实现一些demo中使用到的ctx上代理的方法
  get set() { return this.res.setHeader }
  get method() { return this.req.method }
  get url() { return this.req.url }
}

这样就完成了一个最基本的Context,别看小,已经够用了。
每一次有新的请求,都会创建一个新的ctx对象。

Middleware

koa的中间件是一个异步函数,接受两个参数,分别是ctx和next,其中ctx是当前的请求上下文,next是下一个中间件(也是异步函数),这样想来,我们需要一个维护中间件的数组,每次调用app.use就是往数组中push一个一步函数。所以use方法实现如下

use(middleware) {
  this.middlewares.push(middleware)
}

每次有新的请求,我们都需要把这次请求的上下文灌进数组中的每一个中间件里。单单灌进ctx还不够,还要使每个中间件都能通过next函数调用到下一个中间件。当我们调用next函数时,一般是不需要传参数的,而被调用的中间件中一定会接收到ctx和next两个参数。

调用方不需要传参,被调用方却能接到参数,这让我立刻想到bind方法,只要将每一个中间件所需要的ctx和next都提前绑定好,问题就解决了。下面的代码就是通过bind方法,将用户传入的middleware列表转换成next函数列表

let bindedMiddleware = []

for (let i = middlewares.length - 1; i >= 0; i--) {
  if (middlewares.length == i + 1) {
    // 最后一个中间件,next方法设置为Promise.resolve
    bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve))
  } else {
    bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]))
  }
}

最后我们就得到了一个next函数数组,也就是bindedMiddleware这个变量了。

Request

http.createServer中的回调函数,每次接收到请求的时候会被调用,所以我们在上面callback方法的TODO位置,编写处理请求的代码, 并将上面的middleware列表转next函数列表的代码放入其中。

function handleRequest(ctx, middlewares) {
  if (middlewares && middlewares.length > 0) {
    let bindedMiddleware = []
    for (let i = middlewares.length - 1; i >= 0; i--) {
      if (middlewares.length == i + 1) {
        bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve))
      } else {
        bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]))
      }
    }
    return bindedMiddleware[0]()
  } else {
    return Promise.resolve()
  }
}
Responce

我们简单出来下相应就好了,直接将ctx.body发送给客户端。

function handleResponse (ctx) {
  return function() {
    ctx.res.writeHead(200, { "Content-Type": "text/plain" });
    ctx.res.end(ctx.body);
  }
}
完成Koa类的实现

koa的app实例上面带有on,emit等方法,这是node events模块实现好的东西。直接让Koa类继承自events模块就好了。
我们再将上面实现出来的handleRequest和handleResponse方法放入koa类的callback方法中,得到最终我们实现的Koa,一共58行代码,如下

const http = require("http");
const Emitter = require("events");

class Context {
  constructor(app, req, res) {
    this.app = app;
    this.req = req;
    this.res = res;
  }
  get set() { return this.res.setHeader }
  get method() { return this.req.method }
  get url() { return this.req.url }
}

class Koa extends Emitter{
  constructor(options) {
    super();
    this.options = options
    this.middlewares = [];
  }
  use(middleware) {
    this.middlewares.push(middleware);
  }
  callback() {
    return (req, res) => {
      let ctx = new Context(this, req, res);
      handleRequest(ctx, this.middlewares).then(handleResponse(ctx));
    }
  }
  listen(port) {
    http.createServer(this.callback()).listen(port);
  }
}

function handleRequest(ctx, middlewares) {
  if (middlewares && middlewares.length > 0) {
    let bindedMiddleware = [];
    for (let i = middlewares.length - 1; i >= 0; i--) {
      if (middlewares.length == i + 1) {
        bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve));
      } else {
        bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]));
      }
    }
    return bindedMiddleware[0]();
  } else {
    return Promise.resolve();
  }
}

function handleResponse (ctx) {
  return function() {
    ctx.res.writeHead(200, { "Content-Type": "text/plain" });
    ctx.res.end(ctx.body);
  }
}

module.exports = Koa;

试试跑一下篇首的Demo,没什么问题。

结语

简版实现,码糙理不糙,展示出了koa核心的东西,但少了错误处理,也完全没有考虑性能啥的,需要完善的地方还很多很多。

笔者在写了这个5分钟koa以后去看了koa源码,发现实现思路基本就是这样,相信经过我的这个5分钟koa的洗礼,你去看koa源码一样小菜一碟。

Done!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/107910.html

相关文章

  • Slog6_使用vue前端框架实现单页应用(SPA)

    摘要:时间时间时间每一份有限的时间里都饱含无限的价值需要非常珍惜开发环境需要的信息官方手册,一款工具,具体内容请移步官方手册官方手册,一刻代码包管理工具,具体内容请移步官方手册官方安装手册,前端框架浏览器扩展,用于调试应用程序,下一篇进行详细介 ArthurSlog SLog-6 Year·1 Guangzhou·China July 13th 2018 showImg(https://...

    FingerLiu 评论0 收藏0
  • node技术栈 - 收藏集 - 掘金

    摘要:异步最佳实践避免回调地狱前端掘金本文涵盖了处理异步操作的一些工具和技术和异步函数。 Nodejs 连接各种数据库集合例子 - 后端 - 掘金Cassandra Module: cassandra-driver Installation ... 编写 Node.js Rest API 的 10 个最佳实践 - 前端 - 掘金全文共 6953 字,读完需 8 分钟,速读需 2 分钟。翻译自...

    王伟廷 评论0 收藏0
  • 编写 Node.js Rest API 的 10 个最佳实践

    摘要:要对进行黑盒测试测试的最好办法是对他们进行黑盒测试,黑盒测试是一种不关心应用内部结构和工作原理的测试方法,测试时系统任何部分都不应该被。此外,有了黑盒测试并不意味着不需要单元测试,针对的单元测试还是需要编写的。 本文首发于之乎专栏前端周刊,全文共 6953 字,读完需 8 分钟,速度需 2 分钟。翻译自:RingStack 的文章 https://blog.risingstack.co...

    ermaoL 评论0 收藏0
  • 学习WebSocket(附: WebSocket + koa例子)

    摘要:优点参考维基与对比图客户端例子连接成功后调用当接收到服务器消息时调用连接关闭后调用服务端例子运行结果客户端服务端名词解释握手一般创建链接需要通过浏览器发出请求服务器做出回应这个过程称为握手参考链接协议分钟从入门到精通 原文地址 github项目地址 1. 什么是WebSocket? WebSocket是一种在单个TCP连接上进行全双工通信的协议。 使得客户端和服务器之间的数据交换变...

    0xE7A38A 评论0 收藏0

发表评论

0条评论

Panda

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<