实现一个简单的koa

本文详细介绍了 Koa.js 框架的核心构造,包括请求、响应、上下文对象的创建,以及中间件的使用和执行流程。通过 `use` 方法添加中间件,`compose` 方法实现中间件的串联执行。同时,展示了 Koa 的路由处理,包括基本路由和静态文件服务。此外,还涉及了请求体解析和 Cookie 处理的中间件实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

不完全的koa~

  • lib > 目录
    • request.js
    • response.js
    • context.js
    • body-parser.js
    • cookie-parser.js
    • koa-router.js
    • koa-static.js
  • koa.js

koa.js > 主入口文件

const http = require('http');
const request = require('./lib/request');
const response = require('./lib/response');
const context = require('./lib/context');
const fs = require('fs');
const Stream = require('stream');
const EventEmitter = require('events');

class Koa extends EventEmitter {
  constructor() {
    super();
    this.middleware = []; // 存放中间件
    this.context = Object.create(context); // 创建原型为context的对象
    this.request = Object.create(request); // 创建原型为request的对象
    this.response = Object.create(response); // 创建原型为response的对象
  }
  use(fn) {
    this.middleware.push(fn); // 将中间件存入
  }
  createContext(req, res) {
    const ctx = Object.create(this.context); // 创建koa真正的context对象,原型为this.context
    const request = Object.create(this.request); // 创建request对象,原型为this.request
    const response = Object.create(this.response);  // 创建response对象,原型为this.response
    ctx.request = request; // 将request对象挂载到ctx的request
    ctx.req = ctx.request.req = req; // 将原生req对象挂载到ctx.request.req再挂载到ctx.req,共享一个req
    ctx.response = response; // 将response对象挂载到ctx的response
    ctx.res = ctx.response.res = res; // 将原生res对象挂载到ctx.response.res再挂载到ctx.res,共享一个res
    return ctx; // 返回创建的ctx对象
  }
  compose(ctx) {
    const middleware = this.middleware; // 获取全部中间件
    let index = -1; // 初始化index
    function next(idx) { // 定义递归函数
      if (index >= idx) return Promise.reject(new Error('next() call multiple times')); // 如果重复调用next方法就掏出错误
      index = idx;
      if (middleware.length === idx) return Promise.resolve('Not Found~'); // idx大于中间件长度,没有匹配到对应中间件
      return Promise.resolve(middleware[idx](ctx, () => next(idx + 1))); // 调用中间件,交传入ctx和next函数,只有调用next才能继续执行下一个中间件
    }
    return next(0); // 默认从0索引调用
  }
  handleBody(ctx, body) { // 处理body响应体
    if (!body) { // 没有body响应体,直接返回状态码404和响应体Not Found
      ctx.res.statusCode = 404;
      return ctx.res.end('Not Found');
    }
    if (typeof body === 'string') { // body响应体类型为string,设置响应类型为html
      ctx.res.setHeader('Content-Type', 'text/html;charset=utf-8');
      ctx.res.end(body);
    } else if (body instanceof Stream) { // body响应体类型为stream,设置响应类型为json
      ctx.res.setHeader('Content-Type', 'application/json;charset=utf-8');
      body.pipe(ctx.res);
    } else if (typeof body === 'object') { // body响应休类型为object,设置响应类型为JSON字符串
      ctx.res.setHeader('Content-Type', 'application/json;charset=utf-8');
      ctx.res.end(JSON.stringify(body));
    } else if (typeof body === 'number') { // body响应体类型为number,设置响应类型为palin纯文本,并将数据转换成字符串
      ctx.res.setHeader('Content-Type', 'text/plain;charset=utf-8');
      ctx.res.end(body + '');
    } else if (body instanceof Buffer) { // body响应体类型为buffer,设置响应类型为palin纯文本,并将数据转换成字符串
      ctx.res.setHeader('Content-Type', 'text/plain;charset=utf-8');
      ctx.res.end(body.toString());
    }
  }
  handleRequest(req, res) {
    // const middleware = this.middleware;
    const ctx = this.createContext(req, res); // 调用创建ctx对象
    this.compose(ctx).then(() => { // 调用compose方法,返回一个promise执行then监听变化
      let body = ctx.body; // 获取body响应体
      this.handleBody(ctx, body); // 调用handleBody方法处理响应体
    }).catch(err => {
      this.emit('error', err); // 处理错误
    })
    // function next(idx) {
    //   if (middleware.length === idx) return Promise.resolve('Not Found~');
    //   return Promise.resolve(middleware[idx](ctx, () => next(idx + 1)));
    // }
    // next(0);
  }
  listen(port) { // koa的监听方法
    // 创建一个http Server 服务器
    http.createServer((req, res) => {
      this.handleRequest(req, res); // 调用handleRequest方法处理req和res对象
    }).listen(port); // 监听端口号
  }
}

module.exports = Koa;

context.js

const context = { // 定义context对象

}
function defineGetter(target, key) { // get代理方法
  context.__defineGetter__(key, function () {
    return this[target][key]; // 将context上的属性代理到target上>context.pathname == context.request.pathname
  })
}
function defineSetter(target, key) { // set代理方法
  context.__defineSetter__(key, function (value) {
    this[target][key] = value;
  })
}
defineGetter('request', 'url'); // 将requst上的url方法代理到context上
defineGetter('request', 'pathname'); // 将requst上的pathname方法代理到context上
defineGetter('request', 'query'); // 将requst上的query方法代理到context上
defineGetter('request', 'method'); // 将requst上的method方法代理到context上
defineGetter('request', 'body'); // 将requst上的body方法代理到context上 - 获取

defineSetter('request', 'body'); // 将request上的body方法代理到context上 - 设置

module.exports = context;

request.js

const url = require('url');

const request = {
  get url() {
    return this.req.url; // 获取url地址
  },
  get pathname() {
    return url.parse(this.req.url).pathname; // 获取请求路径
  },
  get query() {
    return url.parse(this.req.url,true).query; // 获取query查询字符串
  },
  get method() {
    return this.req.method; // 获取请求方法
  },
  get body() {
    return this.req.body; // 获取请求体
  },
  set body(value) {
    this.req.body = value; // 设置请求休
  },
  get request() {
    return this.req; // 获取原生req对象
  }
}
module.exports = request;

response.js

const response = {
  _body: undefined,
  get body() {
    return this._body;
  },
  set body(value) {
    this._body = value;
  },
  get response() {
    return this.res;
  }
}

module.exports = response;

koa-router.js > 路由实例

此处仅为一个简单路由,只能处理单层路径


class Layer { // 用来放在单条路由规则
  constructor(method, path, callback) {
    this.method = method;
    this.path = path;
    this.callback = callback;
  }
  match(requestPath, requestMethod) { // 匹配路由方法
    return requestPath === this.path && requestMethod == this.method;
  }
}

class KoaRouter {
  constructor() {
    this.stack = []; // 存放router实例
  }
  post(pathname, callback) {
    let layer = new Layer('POST', pathname, callback);
    this.stack.push(layer);
  }
  get(pathname, callback) {
    let layer = new Layer('GET', pathname, callback);
    this.stack.push(layer);
  }
  put(pathname, callback) {
    let layer = new Layer('PUT', pathname, callback);
    this.stack.push(layer);
  }
  delete(pathname, callback) {
    let layer = new Layer('DELETE', pathname, callback);
    this.stack.push(layer);
  }
  routes() {
    return async (ctx, next) => { // 注册routes中间件
      let requestPath = ctx.pathname; // 获取请求路径
      let requestMethod = ctx.method; // 获取施救方法
      // 用请求路径和请求方法过滤匹配到的路由
      let matchLayers = this.stack.filter(layer => layer.match(requestPath, requestMethod));
      this.compose(matchLayers, ctx, next); // 用cmpose方法处理匹配到的路由
    }
  }
  compose(matchLayers, ctx, next) {
    function dispatch(index) {
      if (index === matchLayers.length) return next(); // 索引越界,交给下一个中间件处理
      return Promise.resolve(matchLayers[index].callback(ctx, () => dispatch(++index))); // 递归处理路由
    }
    return dispatch(0);
  }
}

module.exports = KoaRouter;

koa-static.js > 静态路由中间件

const path = require('path');
const crypto = require('crypto');
const fs = require('fs').promises;
const { readFileSync } = require('fs');
// 缓存文件方法
function cacheFile(req, res, filePath, fileStat) {
  res.setHeader('Cache-Control', 'max-age=10'); // 设定强制缓存,时间为10秒
  const lastModifyed = fileStat.ctime.toGMTString(); // 获取文件最后修改时间
  const etag = crypto.createHash('md5').update(readFileSync(filePath)).digest('base64'); // 将文件内容时间摘要(通常不会对整个文件进行摘要,因为内容太多影响性能,一般选择其中一部分,由于这里只是学习使用,就不用考虑太多!)
  res.setHeader('Last-Modified', lastModifyed); // 设定最后修改时间的响应头
  res.setHeader('Etag', etag); // 设置Etag响应头
  const ifModifiedSince = req.headers['if-modified-since']; // 获取上一次修改时间
  const ifNoneMatch = req.headers['if-none-match']; // 与Etag搭配使用的请求头,对比Etag值,如果相同则表示文件没变化,可走缓存,否则重新加载
  if (ifModifiedSince !== lastModifyed) { // 判断文件的最后修改时间和请求头里最后的修改时间是否相同
    return false;
  }
  if (etag !== ifNoneMatch) { // 判断 Etag和ifNoneMatch值是否相同
    return false;
  }
  return true;
}

function KoaStatic(staticPath) { // 静态文件服务中间件
  return async (ctx, next) => {
    let filePath = path.join(staticPath, ctx.pathname); // 将静态目录和路由路径合并得到资源地址
    try {
      let fileStat = await fs.stat(filePath); // 获取文件状态信息(是否存在此文件或文件夹)
      if (fileStat.isDirectory()) { // 判断是否为目录
        filePath = path.join(filePath, 'index.html'); // 如果为目录则默认加上 index.html 路径
      }
      if (cacheFile(ctx.request.req, ctx.response.res, filePath, fileStat)) { // 如果文件符合缓存策略就使用缓存,返回状态码304
        ctx.res.statusCode = 304;
      }
      ctx.body = await fs.readFile(filePath, 'utf-8'); // 用fs模块读取此文件内容
    } catch {
      await next(); // 如果找不到资源就执行下一个中间件继续处理
    }

  }
}

module.exports = KoaStatic;

body-parser.js

const querystring = require('querystring');
function KoaBodyParser() { // body-parser中间件
  return async (ctx, next) => { // 中间件需要返回一个函数
    // 监听请求,将数据存放到ctx.request.body上
    ctx.request.body = await new Promise((resolve, reject) => {
      const dataBuffer = []; // 定义一个buffer缓冲区
      let contentType = ctx.request.req.headers['content-type']; // 获取请求数据类型
      ctx.req.on('data', chunk => {
        dataBuffer.push(chunk); // 建立数据传输通道,将buffer数据存入buffer缓冲区
      })
      ctx.req.on('end', () => { // 数据传输完成或者get请求没有请求体,会直接触发end事件
        let resultBody;
        if (contentType === 'application/x-www-form-urlencoded') {
          resultBody = querystring.parse(Buffer.concat(dataBuffer).toString()); // 处理单urlencoded类型数据
        } else if (contentType && contentType.includes('multipart/form-data')) {
          resultBody = Buffer.concat(dataBuffer).toString(); // 处理form-data数据
        } else if (contentType === 'text/plain') {
          resultBody = Buffer.concat(dataBuffer).toString(); // 处理plain纯文本数据
        }
        resolve(resultBody); // 将数据返回
      })
    })
    await next(); // 当没有监听到请求就直接执行一下个中间件
  }
}

// parseBody(data) {

// }
module.exports = KoaBodyParser;

cookie-parser.js

const crypto = require('crypto');


function CookiePaser() {
  return async (ctx, next) => {
    ctx.cookies = {}; // 定义cookies对象
    const sign = value => { // 签名方法
      // 采用sha1加密,密码为xin(自定义)
      return crypto.createHmac('sha1', 'xin').update(value).digest('base64')
        .replace(/\+/g, '-').replace(/\=/g, '').replace(/\//g, '_'); // 将\替换成-、-删除、/替换成_
    }
    ctx.cookies.get = function (key, options = {}) { // cookies的get方法
      let cookieArr = ctx.req.headers['cookie'].split('; '); // 将cookie以; 分割成数组
      cookieArr.forEach(item => { // 遍历cookies
        ctx.cookies[item.split('=')[0]] = item.split('=')[1]; // 将cookie条目用=切割,左边0索引为key,右边1索引为value
      })
      if (options.signed) { // 当有signed选项
        if (ctx.cookies[key + '.sig'] === sign(`${ctx.cookies[key]}`)) { // 对.sig的cookie和原cookie重要加密看是否被 修改
          return ctx.cookies[key]; // 如果没有被修改就返回
        } else {
          return '';
        }
      }
      return ctx.cookies[key] || ''; // 返回匹配到的cookie
    }
    let cookiesArr = [];
    
    ctx.cookies.set = function (key, value, options) { // cookie的set方法 
      function setCookie(key, value, options = {}) {
        const { domain, path, expires, maxAge, httpOnly, secure, signed } = options;
        const parts = [`${key}=${value}`];
        if (domain) parts.push(`Domain=${domain}`);
        if (path) parts.push(`Path=${path}`);
        if (expires) parts.push(`Expires=${expires}`);
        if (maxAge) parts.push(`Max-Age=${maxAge}`);
        if (httpOnly) parts.push('httpOnly');
        if (secure) parts.push('securt');
        if (signed) cookiesArr.push(`${key}.sig=${sign(String(value))}`);
        const cookies = parts.join('; ');
        return cookies;
      }
      cookiesArr.push(setCookie(key, value, options));
      ctx.res.setHeader('Set-Cookie', cookiesArr);
    }
    await next();
  }
}
module.exports = CookiePaser;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值