koa原理实践

2019/12/30 node

根据koa的原理写一个仿koa的库。

1、koa

定义:

  • Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。 ——官网

使用:

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

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

简介:

  • Koa将Node的request 和 response对象都封装到了context中,每次请求都会创建一个ctx,并且在中间件中作为接收器使用。
  • 使用Koa过程中要避免使用node原生的方法,绕开koa的response是不被处理的。
  • Koa的中间件是一个很强大的功能,接受两个参数ctx对象、next函数,通过调用next将执行权交给下一个中间件。多个中间件会形成堆栈结构,按先进后出顺序执行,类似于洋葱模型。
  • ctx代理了ctx.requset/ctx.response/req/res,所以他们两个的方法、属性可以直接使用ctx获取到。

2、使用

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

// 对于任何请求,app将调用该异步函数处理请求:
app.use(async (ctx, next) => {
    await next();
    ctx.response.body = 'Hello, koa';
    // 或者
    ctx.body = 'Hello, koa';

    // 如果想使用node原生的可以, 官网说避免使用node原生的,因为koa封装好了更好用的
    ctx.res.end('hello koa');
});

app.listen(3000);

注意:绕过Koa的resposne处理是不被支持的。避免使用以下属性

  1. res.statusCode: 换成ctx.status,如果 response.status 未被设置, Koa 将会自动设置状态为 200 或 204。
  2. res.writeHead():
  3. res.write():
  4. res.end():换成ctx.body()

2.1 use的坑

use中间件可以使用多次,调用next()会走到下一个中间件里面去,use会把函数封装成promise,当同时有多个use共同处理的时候就会很难搞,下一个use需要“卡住”不然就直接完成了,不会等第二个的promise执行。

app.use((ctx, next)=>{
  next();
})
app.use((ctx, next)=>{
  ctx.body = 'hello 2';
  // 注意这里的异步并不会被等待执行完毕
  fs.readFile('./1.txt', function(chunk){
    // todo..
  })
})

我们肯定是需要在第二个use里异步操作一些什么的,比如读取个文件啥的,然后返回,这个时候就尴尬了。我们只能这样做

app.use((ctx, next)=>{
  next();
})
app.use((ctx, next)=>{
  ctx.body = 'hello 2';
  return Promise((resolve, reject)=>{
    fs.readFile('./1.txt', function(chunk){
      // 注意这里的异步并不会被等待执行完毕
    })
    resolve();
  })
})

一个请求还好,如果多个请求,都写成这样,GG思密达~

我们需要用到一个包koa-bodyparser,解析用户的请求体,它用先帮我们处理完请求,然后再走中间件,需要先调用一下方法,不是原生的需要npm install koa-bodyparser


let bodyparser = require('bodyparser');
app.use(bodyparser());
app.use(()=>{
  // todo..
})

需要注意的是app.use(bodyparser())需要放到实际业务逻辑的上面,需要先执行它。

2.2 bodyparser

中间件的特点:

  1. 可以扩展公共属性和方法
  2. 可以做权限校验

根据上面的使用,大概可以总结出来bodyparser的原理,它返回一个async promise,在里面执行了某些操作之后,把结果挂载到ctx.request.body上,然后再走next方法,走下面的use其他方法。

let bodyparser = function (){
  return async (ctx, next)=>{
    // 等待当前的promise执行完毕。
    await new Promise((resolve, reject)=>{
      // 进行一系列的判断和操作
      ctx.request.body = 'ok';
      resolve();
    })
    // 执行下一个use
    await next();
  }
}

2.3 koa-static

处理静态文件需要使用koa-static这个包进行处理。

2.4 router

处理响应使用router包,快速匹配,安装npm install koa-router

const Router = require('koa-router');
let router = new Router();
let children = new Router();
router.get('/info', async (ctx, next)=>{
  ctx.body = 'info';
})
router.post('/save', async (ctx, next)=>{
  ctx.body = 'save';
})
children.get('/user', async (ctx, next)=>{
  ctx.body = 'user';
})
// 分级匹配
router.post('/save', async (ctx, next)=>{
  ctx.body = 'save';
})
router.use('/save', children.routes());
app.use(router.routes());

2.5 koa-generator

安装npm install koa-generator -g 可以快速生成koa项目。

3、创建服务器

来仿一个简易的koa,根据源码会发现Koa有四个文件,所以创建一个文件夹下面有四个文件index.js,context.js,request.js,reponse.js

index.js里面创建服务器

const http = require('http');
class App{
  constructor() {
  }
  handleRequset(req, res){
    res.end('hello')
  }
  listen(...args) {
    let server = http.createServer(this.handleRequset.bind(this));
    server.listen(...args);
    console.log('server is running')
  }
}
module.exports = App;

在根目录创建一个文件server.js

const Server = require('./lowK/index');
const app = new Server();
app.listen(3000);  // server is running

这样我们的服务就创建起来了。

koa的中间件use是一个很强大的功能,接下来实现简易版的use

class App{
  constructor() {
    this.context = context;
    this.response = response;
    this.request = request;
  }
  use(fn){
    this.fn = fn;
  }
  createContext(req, res){
    let context = Object.create(this.context);
    // 把req res都放到上下文上
    context.request =  Object.create(this.request);
    context.response =  Object.create(this.response);
    context.req = context.request.req = req;
    context.res= context.response.res = res;
    return context;
  }
  handleRequset(req, res){
    // 创建上下文
    let ctx = this.createContext(req, res);
    this.fn(ctx);
    ctx.res.end('hello')
  }
  listen(...args) {
    let server = http.createServer(this.handleRequset.bind(this));
    server.listen(...args);
    console.log('server is running')
  }
}

3.1 获取属性

使用:server.js

app.use((ctx, next)=>{
  console.log(ctx.req.url); // '/'
})

需要把req,res,request,response都挂载到context上,然后执行use传进来的函数,把context传过去。

koa源码还可以ctx.request.url获取,这个时候ctx.request指的文件还为空,来完善一下。

// request.js
let request = {
  get url() {
    // this 为 ctx.request, ctx.request上有req属性,它是原生的req属性,有url,把它的url返回就好了。
    return this.req.url;
  }
};

module.exports =request;

这个时候这样是可以使用的,但是Koa源码上,可以使用ctx.url这样获取url,我们来完善这一个功能,给ctx代理上response, request

// context.js
let context = {};
context.__defineGetter__('url',function(){
  return this.request.url;
})
module.exports =context;

源码上用的是__defineGetter__这个方法,MCD上说

  • 该特性是非标准的,请尽量不要在生产环境中使用它!
  • 该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。

那我们用另一个来实现一下。

Object.defineProperty(context, 'url', {
  get: function() {
    return this.request.url;
  }
})

__defineGetter__就相当于defineProperty的get,同时还有一个__defineSetter__相当于definePropertyset

封装起来:

function getter(prop, name) {
  context.__defineGetter__(name,function(){
    return this[prop][name];
  })
}

使用:getter('response', 'url');

3.2 设置属性

当我们要实现设置的时候,Koa源码可以直接在中间件里面设置

ctx.body = 'xxx';

我们来实现这个问题,首先需求确认一点,koa源码里面可以采用ctx.body='xxx' or ctx.response.body='xxx';

能设置肯定可以获取,我们先来代理上body,方便可以获取

getter('response', 'body');

我们在response里代理好body

let response = {
  _body:'',
  get body(){
    return this._body
  },
  set body(n){
    this._body = n
  }
};
module.exports =response;

然后在上下文中代理上,做一个双向代理

// context.js
function setter(prop, name){
  context.__defineSetter__(name,function(val){
    this[prop][name] = val;
  })
}
setter('response', 'body');

这样就可以了

3.3 next方法

koa.use(ctx, next)方法的第二个参数是一个方法,决定是否继续向下执行,支持async+await语法,所以next肯定是一个promise。

来实现一下它,先确定用法

// 使用,期待服务器返回页面一个hello 3
app.use((ctx, next)=>{
  ctx.body = 'hello 1';
  next();
})
app.use((ctx, next)=>{
  ctx.body = 'hello 2';
  next();
})
app.use((ctx, next)=>{
  ctx.body = 'hello 3';
})

首先这是一个并发问题,然后next是个promise,使用队列的原理来做,我们把所有的next都放到一个数组里,由于next是个promise,我们最后肯定要返回一个promise

// 开搞  index.js
// 这样就把所以的fn都存起来了,我们把他们组合成一个大的promise,源码里就是这样搞的
constructor() {
  this.context = context;
  this.response = response;
  this.request = request;
  this.middlewares = [];
}
use(fn){
  this.middlewares.push(fn);
}
handleRequset(req, res){
  // 创建上下文
  let ctx = this.createContext(req, res);
  // 需要一个函数把所有的fn都执行完毕之后返回一个promise,然后执行
  this.compose(ctx).then( ()=>{
    let _body = ctx.body;
    res.end(_body);
  })
}
compose(ctx){
  let dispatch = (index) => {
    if(index == this.middlewares.length){
      return Promise.resolve();
    }
    // 拿到每一个fn
    let fn = this.middlewares[index];
    // 因为是一个promise,所以需要这里返回一个成功的promise
    // fn() 接受两个参数,一个ctx,一个next方法。
    return Promise.resolve(fn(ctx, dispatch.bind(null, index+1)));
  }
  return dispatch(0);
}

// 页面展示 hello 3

3.4 错误处理

node中所有事件都是基于events模块的,所以我们可以通过它开实现错误监听

// index.js
const events = require('events');
class App extends events{
  constructor() {
    super();
    this.context = context;
    this.response = response;
    this.request = request;
    this.middlewares = [];
  }
  handleRequset(req, res){
    // 创建上下文
    let ctx = this.createContext(req, res);
    this.compose(ctx).then( ()=>{
      let _body = ctx.body;
      res.end(_body);
    }).catch((err)=>{
      this.emit('error', err);
    })
  }
}

// server.js
// 监听错误
app.on('error', (err)=>{
  console.log(err);
})

最终

源码我会放到GitHubhttps://github.com/LB-nan/lowK。欢迎star

Search

    Table of Contents