从0到1实现最简koa.js

245 阅读3分钟
作者:勇哥
时间:20200409

一、最小的web系统

web服务中最小的功能系统,包含两部分:

  • HTTP Server HTTP服务器,处理请求和响应。
  • Router 路由解析,对不同的请求返回不同的数据。

Node.js原生http模块使用

const http = require('http');

const app = http.createServer((req, res) => {
  res.end('hello wold!');
});

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

http服务构成

请求 req

提供HTTP请求request的内容和操作的方法

响应 res

提供HTTP响应response操作的方法

http服务的拆分

对不同的请求进行拆分,形成路由 router。

const http = require('http');

// 路由拆分
const router = (req, res) => {
  if (req.url === '/') {
    res.end('I am index page!');
  } else if (req.url.startsWith('/home')) {
    res.end('I am home page!');
  } else {
    res.end('I am 404 page!');
  }
}

const app = http.createServer(router);

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

对路由进行拆分形成 controller

const http = require('http');

// 控制器
const controller = {
  index(req, res) {
    res.end('I am index page!');
  },

  home(req, res) {
    res.end('I am home page!');
  },

  _404(req, res) {
    res.end('I am 404 page!');
  }
};

// 路由拆分
const router = (req, res) => {
  if (req.url === '/') {
    controller.index(req, res);
  } else if (req.url.startsWith('/home')) {
    controller.home(req, res);
  } else {
    controller._404(req, res);
  }
}

const app = http.createServer(router);

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

二、中间件引擎实现

Koa 中间件使用

koa2中间件使用如下代码所示:

const Koa = require('koa');

let app = new Koa();

const middleware1 = async (ctx, next) => {
  console.log(1);
  await next();
  console.log(6);
}

const middleware2 = async (ctx, next) => {
  console.log(2);
  await next();
  console.log(5);
}

const middleware3 = async (ctx, next) => {
  console.log(3);
  await next();
  console.log(4);
}

app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
app.use(async (ctx, next) => {
  ctx.body = 'hello world'
})

app.listen(3000);

//控制台输出 
// 1
// 2
// 3
// 4
// 5
// 6

依次输出1、2、3、4、5、6,koa.js通过洋葱模型来实现这样一个中间件引擎。下面将一步步实现这样的一个中间件引擎。

中间件原理

读者可以先自行了解下洋葱模型,这里不再进行讲解。中间件在 await next() 前后的操作,和数据结构中栈相似,先进后出。下面先用Promise做简单的实现:

let context = {
  data: []
};

async function middleware1(ctx, next) {
  console.log('action 1');
  ctx.data.push(1);
  await next();
  console.log('action 6');
  ctx.data.push(6);
}

async function middleware2(ctx, next) {
  console.log('action 2');
  ctx.data.push(2);
  await next();
  console.log('action 5');
  ctx.data.push(5);
}

async function middleware3(ctx, next) {
  console.log('action 3');
  ctx.data.push(3);
  await next();
  console.log('action 4');
  ctx.data.push(4);
}

Promise.resolve(middleware1(context, async () => {
  return Promise.resolve(middleware2(context, async () => {
    return Promise.resolve(middleware3(context, async () => {
      return Promise.resolve();
    }))
  }))
})).then(() => {
  console.log('context = ', context.data);
})

// result 
// action 1
// action 2
// action 3
// action 4
// action 5
// action 6
//context = [1,2,3,4,5,6]

中间件引擎 compose 的实现

function compose(middleware) {

  if (!Array.isArray(middleware)) {
    throw new TypeError('Middleware stack must be an array!');
  }

  return function (ctx, next) {
    let index = -1;

    return dispatch(0);

    function dispatch(i) {
      if (i < index) {
        return Promise.reject(new Error('next() called multiple times'));
      }
      index = i;

      let fn = middleware[i];

      if (i === middleware.length) {
        fn = next;
      }

      if (!fn) {
        return Promise.resolve();
      }

      try {
        return Promise.resolve(fn(ctx, () => {
          return dispatch(i + 1);
        }));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

测试如下:

let middleware = [];
let context = {
  data: []
};

middleware.push(async (ctx, next) => {
  console.log('action 1');
  ctx.data.push(1);
  await next();
  console.log('action 6');
  ctx.data.push(6);
});

middleware.push(async (ctx, next) => {
  console.log('action 2');
  ctx.data.push(2);
  await next();
  console.log('action 5');
  ctx.data.push(5);
});

middleware.push(async (ctx, next) => {
  console.log('action 3');
  ctx.data.push(3);
  await next();
  console.log('action 4');
  ctx.data.push(4);
});

const fn = compose(middleware);

fn(context)
  .then(() => {
    console.log('context = ', context);
  });
  
// result 
// action 1
// action 2
// action 3
// action 4
// action 5
// action 6
//context = [1,2,3,4,5,6]

三、最简koa实现

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

// 使用上面的中间件引擎
const compose = require('./compose');

const context = {
  _body: null,

  get body() {
    return this._body;
  },

  set body(val) {
    this._body = val;
    this.res.end(this._body);
  }
};

class Koa extends Emitter {
  constructor() {
    super();
    this.middleware = [];
    this.context = Object.create(context);
  }

  
  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

  // 注册中间件
  use(fn) {
    if (typeof fn === 'function') {
      this.middleware.push(fn);
    }
  }

  // 中间件总回调
  callback() {

    if (this.listeners('error').length === 0) {
      this.on('error', this.onerror);
    }

    const handleRequest = (req, res) => {
      let context = this.createContext(req, res);
      let middleware = this.middleware;
      // 执行中间件
      compose(middleware)(context).catch(err => this.onerror(err))
    };
    return handleRequest;
  }

  // 异常捕获
  onerror(err) {
    console.log(err);
  }

  // 上下文
  createContext(req, res) {
    let context = Object.create(this.context);
    context.req = req;
    context.res = res;
    return context;
  }
}

module.exports = Koa;

至此实现了一个最简单的koa模型,koa2中还对一些API进行了扩展和处理,后面会继续进行分析。