实现自己的 简单版 Koa

简介: 实现自己的 简单版 Koa
Express 的下一代基于 Node.js 的 web 框架
  • 特点

    • 轻量无捆绑
    • 中间件架构
    • 优雅的 API 设计
    • 增强的错误处理
  • 安装

    npm i koa -S

Hello Koa

Hello Koa

const Koa = require('koa');

const app = new Koa();

// 日志中间件
app.use(async (ctx, next) => {
  const sT = new Date().getTime();
  console.log(`[${sT}]start: ${ctx.url}`);
  await next(); // 执行下一个中间件
  const eT = new Date().getTime();
  console.log(`[${eT}]end: ${ctx.url} passed ${eT - sT}ms`);
});

app.use(async (ctx, next) => {
  ctx.body = [
    {
      name: 'cell',
      msg: 'hello koa',
    }
  ];
  await next();
});

app.listen(3000);
  • Koa 中间件机制

实现简单 http 服务

const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200);
  res.end('hello http server');
});

server.listen(3000, () => {
  console.log('listen at 3000');
});

实现简单 Koa

基础 ToyKoa.js

const ToyKoa = require('./toykoa');

const app = new ToyKoa();

app.use((req, res) => {
  res.writeHead(200);
  res.end('Hello ToyKoa');
});

app.listen(3000, () => {
  console.log('listen at 3000');
});

使用 app.js

const ToyKoa = require('./toykoa');

const app = new ToyKoa();

app.use((req, res) => {
  res.writeHead(200);
  res.end('Hello ToyKoa');
});

app.listen(3000, () => {
  console.log('listen at 3000');
});

context

koa 为了能够简化 API,引入上下文 context,将原始请求对象 req 和 响应对象 res 封装并挂载到 context 上,并且在 context 上设置 getter 和 setter,从而简化操作

  • 封装 request
// request.js
module.exports = {
  get url() {
    return this.req.url;
  },
  get method() {
    return this.req.method.toLowerCase();
  },
};
  • 封装 response
// response.js
module.exports = {
  get body() {
    return this._body;
  },
  set body(val) {
    this._body = val;
  },
}
  • 封装 context
// context.js
module.exports = {
  get url() {
    return this.request.url;
  },
  get body() {
    return this.response.body;
  },
  set body(val) {
    this.response.body = val;
  },
  get method() {
    return this.request.method;
  }
};
  • 创建 context 并挂载 req 和 res
// toykoa.js
const http = require('http');

const context = require('./context');
const request = require('./request');
const response = require('./response');

class ToyKoa {
  listen(...args) {
    const server = http.createServer((req, res) => {
      // 创建上下文
      let ctx = this.createContext(req, res);

      this.cb(ctx);

      // 响应
      res.end(ctx.body);
    });
    server.listen(...args);
  }
  use(cb) {
    this.cb = cb;
  }
  /**
  * 构建上下文
  * @param {*} req 
  * @param {*} res 
  */
  createContext(req, res) {
    const ctx = Object.create(context);
    ctx.request = Object.create(request);
    ctx.response = Object.create(response);

    ctx.req = ctx.request.req = req;
    ctx.res = ctx.response.res = res;

    return ctx;
  }
}

module.exports = ToyKoa;
  • 测试 context
// app.js
const ToyKoa = require('./toykoa');

const app = new ToyKoa();

app.use(ctx => {
  ctx.body = 'Hello toykoa!';
});

app.listen(3000, () => {
  console.log('listen at 3000');
});

中间件

Koa 中间件机制:函数式组合(Compose),将一组需要顺序执行的函数复合为一个函数,外层函数的参数实际是内层函数的返回值,可以通过洋葱模型理解。

const add = (x, y) => x + y;
const square = z => z * z;

const fn = (x, y) => square(add(x, y));
console.log(fn(1, 2)); // 9

// 将上面两次函数的组合调用合成一个函数
const compose = (fn1, fn2) => {
  return (...args) => fn2(fn1(...args));
};
const fn_compose = compose(add, square);
console.log(fn_compose(1, 2)); // 9

// 多个函数组合
const composePlus = (...[first, ...other]) => {
  return (...args) => {
    let ret = first(...args);
    other.forEach(fn => {
      ret = fn(ret);
    });
    return ret;
  };
};

const fn_compose_plus = composePlus(add, square);
console.log(fn_compose_plus(1, 2)); // 9


异步函数组合

function compose(middlewares) {
  return function () {
    return dispatch(0);
    function dispatch(i) {
      let fn = middlewares[i];
      if (!fn) {
        return Promise.resolve();
      }
      return Promise.resolve(
        fn(function next() {
          // promise 完成后,再执行下一个
          return dispatch(i + 1);
        })
      );
    }
  };
}

async function fn1(next) {
  console.log('fn1');
  await next();
  console.log('fn1 end');
}

async function fn2(next) {
  console.log('fn2');
  await delay();
  await next();
  console.log('fn2 end');
}

function fn3(next) {
  console.log('fn3');
}

function delay() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, 1000);
  });
}

const middlewares = [fn1, fn2, fn3];
const finalFn = compose(middlewares);
finalFn();
// 输出
// fn1
// fn2
// fn3
// fn2 end
// fn1 end

将 compose 应用在 koa 中间件中

// toykoa.js
const http = require('http');

const context = require('./context');
const request = require('./request');
const response = require('./response');

class ToyKoa {
  constructor() {
    // 初始化中间件数组
    this.middlewares = [];
  }
  listen(...args) {
    const server = http.createServer(async (req, res) => {
      // 创建上下文
      const ctx = this.createContext(req, res);

      // 中间件合成
      const fn = this.compose(this.middlewares);

      // 执行合成函数并传入上下文
      await fn(ctx);

      // 响应
      res.end(ctx.body);
    });
    server.listen(...args);
  }
  use(middleware) {
    this.middlewares.push(middleware);
  }
  /**
  * 构建上下文
  * @param {*} req 
  * @param {*} res 
  */
  createContext(req, res) {
    const ctx = Object.create(context);
    ctx.request = Object.create(request);
    ctx.response = Object.create(response);

    ctx.req = ctx.request.req = req;
    ctx.res = ctx.response.res = res;

    return ctx;
  }
  /**
  * 合成函数
  * @param {*} middlewares 
  */
  compose(middlewares) {
    return function(ctx) {
      return dispatch(0);
      function dispatch(i) {
        let fn = middlewares[i];
        if (!fn) {
          return Promise.resolve();
        }
        return Promise.resolve(
          fn(ctx, function next() {
            return dispatch(i + 1);
          })
        );
      }
    };
  }
}

module.exports = ToyKoa;

测试中间件

// app.js
const ToyKoa = require('./toykoa');

const app = new ToyKoa();

// app.use((req, res) => {
//   res.writeHead(200);
//   res.end('Hello ToyKoa');
// });

const delay = () => new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve();
  }, 1000);
});

app.use(async (ctx, next) => {
  ctx.body = '1'
  await next();
  ctx.body += '5';
});

app.use(async (ctx, next) => {
  ctx.body += '2'
  await delay();
  await next();
  ctx.body += '4';
});

app.use(async (ctx, next) => {
  ctx.body += 'Hello toykoa!';
});

app.listen(3000, () => {
  console.log('listen at 3000');
});

返回响应结果为 12Hello toykoa!45

常用 koa 中间件的实现

  • koa 中间件的规范

    • 一个 async 函数
    • 接收 ctx 和 next 两个参数
    • 任务结束需要执行 next
    // 【洋葱状态】:(
    const middleware = async (ctx, next) => {
      // 进入当前洋葱 【洋葱状态】:(当前洋葱
      // (当前洋葱(
      next(); // 进入 子洋葱 【洋葱状态】:(当前洋葱(子洋葱
      // 出 子洋葱 【洋葱状态】:(当前洋葱(子洋葱)当前洋葱
    }
    // 【洋葱状态】:(当前洋葱(子洋葱)当前洋葱)
  • 中间件常见任务

    • 请求拦截
    • 路由
    • 日志
    • 静态文件服务
  • 路由实现

    // router.js
    class Router {
      constructor() {
        this.stack = [];
      }
    
      register(path, methods, middleware) {
        let route = { path, methods, middleware };
        this.stack.push(route);
      }
    
      get(path, middleware) {
        this.register(path, 'get', middleware);
      }
      post(path, middleware) {
        this.register(path, 'post', middleware);
      }
    
      routes() {
        let stock = this.stack;
        return async function(ctx, next) {
          let currentPath = ctx.url;
          let route;
    
          for (let i = 0; i < stock.length; i++) {
            let item = stock[i];
            if (currentPath === item.path && item.methods.indexOf(ctx.method) >= 0) {
              route = item.middleware;
              break;
            }
          }
    
          if (typeof route === 'function') {
            route(ctx, next);
            return;
          }
    
          await next();
        };
      }
    }
    
    module.exports = Router;

    路由测试

    // app.js
    const ToyKoa = require('./toykoa');
    
    const app = new ToyKoa();
    
    const Router = require('./router');
    const router = new Router();
    
    router.get('/index', async ctx => { ctx.body = 'get index'; });
    router.get('/post', async ctx => { ctx.body = 'get post'; });
    router.get('/list', async ctx => { ctx.body = 'get list'; });
    router.post('/index', async ctx => { ctx.body = 'post index'; });
    
    // 路由实例输出父中间件 router.routes()
    app.use(router.routes());
    
    // ...
    
    app.listen(3000, () => {
      console.log('listen at 3000');
    });
  • 静态文件服务 koa-static

    • 配置绝对资源目录地址,默认为 static
    • 获取文件或者目录信息
    • 静态文件读取
    • 返回

    static 实现

    // static.js
    const fs = require('fs');
    const path = require('path');
    
    module.exports = (dirPath = './public') => {
      return async (ctx, next) => {
        if (ctx.url.indexOf('/public') === 0) {
          const url = path.resolve(__dirname, dirPath);
          const filepath = url + ctx.url.replace('/public', '');
          try {
            const stats = fs.statSync(filepath);
            if (stats.isDirectory()) { // 文件夹
              const dir = fs.readdirSync(filepath);
              const ret = ['<div style="padding-left:20px">'];
              dir.forEach(filename => {
                if (filename.indexOf('.') > -1) {
                  ret.push(`
                    <p>
                      <a style="color:black" href="${ctx.url}/${filename}">${filename}</a>
                    </p>
                  `);
                } else {
                  ret.push(`
                    <p>
                      <a href="${ctx.url}/${filename}">${filename}</a>
                    </p>
                  `);
                }
              });
              ret.push('</div>');
              ctx.body = ret.join('');
            } else { // 文件
              const content = fs.readFileSync(filepath);
              ctx.body = content;
            }
          } catch (err) {
            ctx.body = '404 not found';
          }
        } else {
          // 不是静态资源忽略
          await next();
        }
      };
    };

    添加测试资源
    public/index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
      <link rel="stylesheet" href="styles/style.css">
    </head>
    <body>
      <h1>Hello ToyKoa!</h1>
    </body>
    </html>

    public/styles/style.css

    body {
      background-color: red;
    }

    static 测试

    // app.js
    const ToyKoa = require('./toykoa');
    
    const app = new ToyKoa();
    
    const static = require('./static');
    app.use(static(__dirname + '/public'));
    
    // ...
    
    app.listen(3000, () => {
      console.log('listen at 3000');
    });
    
相关文章
|
移动开发 JavaScript HTML5
Vue2视频播放(Video)
这篇文章介绍了如何在Vue 3框架中创建一个视频播放组件(Video),支持自定义视频源、封面、自动播放等多种播放选项和样式设置。
797 1
Vue2视频播放(Video)
|
物联网 数据管理 Apache
拥抱IoT浪潮,Apache IoTDB如何成为你的智能数据守护者?解锁物联网新纪元的数据管理秘籍!
【8月更文挑战第22天】随着物联网技术的发展,数据量激增对数据库提出新挑战。Apache IoTDB凭借其面向时间序列数据的设计,在IoT领域脱颖而出。相较于传统数据库,IoTDB采用树形数据模型高效管理实时数据,具备轻量级结构与高并发能力,并集成Hadoop/Spark支持复杂分析。在智能城市等场景下,IoTDB能处理如交通流量等数据,为决策提供支持。IoTDB还提供InfluxDB协议适配器简化迁移过程,并支持细致的权限管理确保数据安全。综上所述,IoTDB在IoT数据管理中展现出巨大潜力与竞争力。
367 1
|
机器学习/深度学习 运维 安全
阿里云 ACK One Serverless Argo 助力深势科技构建高效任务平台
阿里云 ACK One Serverless Argo 助力深势科技构建高效任务平台
101748 8
|
Linux
Linux通过QQ邮箱账号使用mailx发送邮件
Linux通过QQ邮箱账号使用mailx发送邮件
491 2
|
C++
[C++] 提取字符串中的所有数字并组成一个数
[C++] 提取字符串中的所有数字并组成一个数
268 0
|
人工智能 小程序 JavaScript
【Java】智慧校园家校互通小程序源码
【Java】智慧校园家校互通小程序源码
265 0
|
前端开发 小程序
【微信小程序-原生开发】实用教程20 - 生成海报(实战范例为生成活动海报,内含生成指定页面的小程序二维码,保存图片到手机,canvas 系列教程)
【微信小程序-原生开发】实用教程20 - 生成海报(实战范例为生成活动海报,内含生成指定页面的小程序二维码,保存图片到手机,canvas 系列教程)
648 0
|
Go Windows
golang hello 安装环境异常【已解决】
golang hello 安装环境异常【已解决】
257 1
|
前端开发 JavaScript 数据库
layui框架实战案例(20):常用条件判断和信息展示技巧(图片预览、动态表格、短信已读未读、链接分享、信息脱敏、内置框架页)
layui框架实战案例(20):常用条件判断和信息展示技巧(图片预览、动态表格、短信已读未读、链接分享、信息脱敏、内置框架页)
817 0