阅读 888

Express VS Koa 中间件机制分析

提到 Node 就不得不说其中的两大框架ExpressKoa

然而这两者之间又有什么 渊源 和 爱恨情仇 呢?

说到ExpressKoa,我们都会发现,他们都会有 中间件(middlewares) 的概念

什么是中间件?

中间件(middlewares)其实就是一个函数
它可以访问请求对象(request object(req)), 响应对象(response object(res)), 和 web 应用处于请求-响应循环流程中的中间件

例子:

比如:生活中的租客和房主,中间需要一个中介来搭桥,这个中介就类似于中间件。

在说下面的例子时,会涉及到中间件的解释

ExpressKoa的区别

中间件的执行顺序:
其实在两种框架中,中间件的执行顺序都是自上而下
然而最大的区别就是:

  • Express 中间件链是基于回调的
  • Koa 是基于 Promise 的

模型:

  • Express为 线性模型

图片加载失败!

  • Koa为 洋葱型模型

图片加载失败!

功能: Express包含了一个完整的应用程序框架,具有路由、模板等功能。

Koa的核心模块只是 中间件内核,但是Koa却有这些功能的选项,但他们是单独的模块,用的时候需要 npm 安装

所以,Koa的模块化程度更高,因此,如果你只需要核心请求应答上下文对象,则Koa占用空间非常小。相比较而言,Express较为庞大,内置了一整套中间件功能,好处是对于大部分应用场合你可以省掉自己选择和组合模块的时间。

Express

中间件执行是有顺序的

const express = require("express")
const app = express();
app.use("/",function(req,res,next){
    console.log("这是一个中间件1...")
    // next()
})
app.use("/",function(req,res,next){
    console.log("这是一个中间件2...")
    // next()
})
app.listen(3000)
复制代码

输出结果:

这是一个中间件1...
这是一个中间件2...
复制代码

从上述代码,可以看到,启动服务后当你访问 127.0.0.1:3000/ 的时候,就会打印出相应的内容,所以Express是线性的

再给一个例子:

const express = require("express")
const app = express();
//可以匹配所有的路由
app.use("*",function(req,res,next){
    console.log("这是一个中间件")
    next()
})
app.get('/',(req,res)=>{
    console.log("中间件1");
})
app.get("/my",function(req,res){
    res.send("my")
})
app.listen(3000)
复制代码

根据这个例子就可以知道,其中具有 路由 功能,包括 getpost...

再看下面这个例子:

let express = require('express');

let app = express();
app.use((req, res, next)=> {
    console.log('第一个中间件start');
    setTimeout(() => {
        next();
    }, 1000)
    console.log('第一个中间件end');
});
app.use((req, res, next)=> {
    console.log('第二个中间件start');
    setTimeout(() => {
        next();
    }, 1000)
    console.log('第二个中间件end');
});
app.listen(3000)
复制代码

输出结果:

第一个中间件start
第一个中间件end
第二个中间件start
第二个中间件end
复制代码

但是如果没有内部的异步处理,直接调用next()呢?

let express = require('express');

let app = express();
app.use((req, res, next)=>{
    console.log('第一个中间件start');
    next()
    console.log('第一个中间件end');
});
app.use((req, res, next)=>{
    console.log('第二个中间件start');
    next()
    console.log('第二个中间件end');
});
app.listen(3000);
复制代码

输出结果:

第一个中间件start
第二个中间件start
第二个中间件end
第一个中间件end
复制代码

其实这种输出结果是由于代码的同步导致的,和洋葱模型不一样

当中间件内没有异步操作时,其实代码是以这种方式运行的:

let express = require('express');
let app = express();

app.use((req, res, next)=>{
    console.log('第一个中间件start');
    ((req, res, next)=>{
        console.log('第二个中间件start');
        (function handler(req, res, next) {
            // do something
        })()
        console.log('第二个中间件end');
    })()
    console.log('第一个中间件end');
});

app.listen(3000);
复制代码

输出结果:

第一个中间件start
第二个中间件start
第二个中间件end
第一个中间件end
复制代码

可以看到就是一层一层嵌套的回调,就是很简单的回调函数,所以代码还是要一步一步的往下走的

所以说Express的中间件是线性的,next过后继续寻找下一个中间件

Express错误处理:

const express = require("express")
const app = express();

app.use((req,res,next)=>{
    console.log("中间件");
    next(new Error('错误了'))
})
app.get('/',(req,res)=>{
    res.send('你好')
})
app.use((err,req,res,next)=>{
    console.log(err);
})
app.listen(3000)
复制代码

输出内容:

中间件  
Error:错误了  
	下面是一堆错误信息
复制代码

源码解析

这里只说部分核心代码(参考别的文章的)
这里看不懂就算了,完全不用看

中间件的挂载主要依赖 proto.useproto.handle,(删除部分 if 判断)

proto.use = function use(route, fn) {
    var handle = fn;
    var path = route;

    // 这里是对直接填入回调函数的进行容错处理
    // default route to '/'
    if (typeof route !== 'string') {
        handle = route;
        path = '/';
    }
    .
    .
    .
    this.stack.push({ route: path, handle: handle });

    return this;
};
复制代码

proto.use 主要将我们需要挂载的中间件存储在其自身 stack 属性上,同时进行部分兼容处理,这一块比较容易理解。其中间件机制的核心为 proto.handle 内部 next 方法的实现。

proto.handle = function handle(req, res, out) {
    var index = 0;
    var stack = this.stack;

    function next(err) {

        // next callback
        var layer = stack[index++];

        // all done
        if (!layer) {
            defer(done, err);
            return;
        }

        // route data
        var path = parseUrl(req).pathname || '/';
        var route = layer.route;

        // skip this layer if the route doesn't match
        if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
            return next(err);
        }

        // call the layer handle
        call(layer.handle, route, err, req, res, next);
    }

    next();
};
复制代码

在删除掉部分非核心代码后,可以清晰的看到,proto.handle 的核心就是 next 方法的实现和递归调用,对存在于 stack 中的中间件取出、执行。

这里便可以解释上文中异步和非异步过程中所输出的结果的差异:

  • 当有异步代码时,将会直接跳过继续执行,此时的 next 方法并未执行,需要等待当前队列中的事件全部执行完毕,所以此时我们输出的数据是线性的。
  • next 方法直接执行时,本质上所有的代码都已经为同步,所以层层嵌套,最外层的肯定会在最后,输出了类似剥洋葱模型的结果。

Koa

相比较 Express 而言,Koa 的整体设计和代码实现显得更高级,更精炼 因为在Koa中,只有中间件

Koa中,中间件也是自上向下执行的

Koa中没有路由,默认情况它会匹配/

const Koa = require("koa")
let app = new Koa()
app.use((ctx,next)=>{
    ctx.body = "hello"
})
app.listen(3000)
复制代码

能在浏览器输出hello

看下面代码:

const Koa = require("koa")
let app = new Koa()
// 在koa中,在一个中间件中调用next()  表示让下一个中间件执行
app.use((ctx,next)=>{
    console.log(1)
    console.log(2)
    next()
})
app.use((ctx,next)=>{
    console.log(3)
    console.log(4)
    next()
})
app.listen(3000)
复制代码

输出结果:

1
2
3
4
复制代码

为什么下面输出结果是:1,3,4,2 ? koa中间件的原理 和 express 中间件的原理不一样:

const Koa = require("koa")
let app = new Koa()
app.use((ctx,next)=>{
    console.log(1)
    next()
    console.log(2)
})
app.use((ctx,next)=>{
    console.log(3)
    next()
    console.log(4)
})
app.listen(3000)
复制代码

输出结果:

1
3
4
2
复制代码

再看一段代码:

const Koa = require("koa")
let app = new Koa()
app.use((ctx,next)=>{
    console.log(1)
    next()
    console.log(2)
})
app.use((ctx,next)=>{
    console.log(3)
    console.log(4)
})
app.listen(3000)
复制代码

输出结果:

1
3
4
2
复制代码

这不是和Express一样吗?

不一样,虽然结果是一样的,但是原理不一样

源码解析

这个非常重要,好好理解
Koa的实现主要依赖自身的koa-compose,接下来咱们看一下这个函数的源码:

// 完整版
function compose (middleware) {
  // 判断参数是否合法,middleware 要求为数组且其中每个数组元素都为 function
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    // 递归返回一个函数 该函数返回一个 Promise 的对象
    return dispatch(0)
    function dispatch (i) {
      // 当 next 方法被多次调用时会出现
      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()
      // Promise 封装中间件 进行递归调用
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
复制代码

有点长不好看懂,简化之后如下:

// 简化版
function compose(middleware) {
  return function(context, next) {
    let index = -1
    return dispatch(0)
    function dispatch(i) {
      index = i
      const fn = middleware[i] || next
      if (!fn) return Promise.resolve()
      return Promise.resolve(fn(context, function next() {
        return dispatch(i + 1)
      }))
    }
  }
}  
复制代码

可以看到,一个递归调用,连续调用中间件,返回一个 Promise

举例分析过程:

// 中间件 fn1 和 fn2
async function fn1 (ctx, next) {
  console.log('第一个start')
  await next()
  console.log('第一个 end')
}
async function fn2 (ctx, next) {
  console.log('第二个 start')
  await next()
  console.log('第二个 end')
}
// 模拟中间件数组
const arr = [fn1, fn2]
// 执行函数,这里返回的是一个 Promise 对象
compose(arr)()
复制代码

输出结果:

第一个 start
第二个 start
第二个 end
第一个 end
复制代码

其实,在 compose 内部递归执行的操作后,形成多个 Promise 层层嵌套(如下面代码所示),此时 next 函数其实就是下一个中间件,await 需要等待内部的 Promise ,所以其执行结果会呈现一个剥洋葱的模式。

function mycompose() {
  return function () {
    const ctx = {}
    return Promise.resolve(fn1(ctx, () => {
      return Promise.resolve(fn2(ctx, () => {
      }))
    }))
  }
}
mycompose()()
复制代码

自己模拟compose:

let app = {
    middlewares:[],
    use(fn){
        this.middlewares.push(fn)
    }
}
app.use((next) => {
    console.log(1)
    next()
    console.log(2)
    
})
app.use((next) => {
    console.log(3)
    console.log(4)
    
})
function compose(middlewares) {
    return middlewares.reduce(function (a, b) {
        return function (arg) {
            return a(function () {
                return b(arg)
            })
        }
    })
}
let fn = compose(app.middlewares);
fn(() => { })
复制代码

输出结果:

1
3
4
2
复制代码

把里面的代码简化:

let app = {
    middlewares: [],
    use(fn) {
        this.middlewares.push(fn)
    }
}
app.use((next) => {
    console.log(1)
    next()
    console.log(2)
    
})
app.use((next) => {
    console.log(3)
    console.log(4)
    
})
function compose(middlewares) {
    return middlewares.reduce((a,b)=>(arg)=>a(()=>b(arg)))
}
let fn = compose(app.middlewares);
fn(() => { })
复制代码

再写一个:

let app = {
    middlewares:[],
    use(fn){
        this.middlewares.push(fn)
    }
}
app.use((next)=>{
    console.log(1)
    next()
    console.log(2)
})
app.use((next)=>{
    console.log(3)
    next()
    console.log(4)
})
app.use((next)=>{
    console.log(5)
    console.log(6)
})
function compose(middlewares){
    return middlewares.reduceRight((a,b)=>()=>b(a))
}
let fn = compose(app.middlewares)
fn()
复制代码

输出结果:

1
3
5
6
4
2
复制代码

使用 dispatch

let app = {
    middlewares:[],
    use(fn){
        this.middlewares.push(fn)
    }
}
app.use((next)=>{
    console.log(1);
    next()
    console.log(2);
})
app.use((next)=>{
    console.log(3);
    next()
    console.log(4);
})
app.use((next)=>{
    console.log(5);
    console.log(6);
    next()
})
// koa中间件的原理
function dispatch(index){
    if(app.middlewares.length === index) return;
    let route = app.middlewares[index]
    route(()=>{dispatch(index+1)})
}
//当参数是 0 的时候
dispatch(0)
复制代码

dispatch参数是 0 的时候,输出结果:

1
3
5
6
4
2
复制代码

dispatch参数是 1 的时候,输出结果:

3
5
6
4
复制代码

dispatch参数是 2 的时候,输出结果:

5
6
复制代码

dispatch参数是 3 的时候,无输出结果

如果中间件中有异步

const Koa = require("koa")
const app = new Koa()
app.use((ctx,next)=>{
    // 调用一个中间件,返回promise
    let a = next()  // a是一个promise
    console.log(a) // Promise { 'hello' }
})
app.use((ctx,next)=>{
    return "hello"
})
app.listen(3000)
复制代码

输出结果:

Promise { 'hello' }
复制代码

使用 async+await,仅仅是把Pomise状态 转化 普通值

const Koa = require("koa")
const app = new Koa()
app.use(async (ctx,next)=>{
    let a = await next()  
    console.log(a) 
})
app.use((ctx,next)=>{
    return "hello"
})
app.listen(3000)
复制代码

输出结果:

hello
复制代码

Koa常用中间件

都需要 npm 安装

koa-compose

const Koa = require("koa")
const compose = require("koa-compose")
const app = new Koa()
let f1 = async (ctx,next)=>{
    console.log(f1);
    await next()
}
let f2 = async (ctx,next)=>{
    console.log(f2);
    await next()
}
let f3 = async (ctx,next)=>{
    console.log(f3);
    await next()
}
let all = compose([f1,f2,f3])
app.use(all)
app.listen(3000)
复制代码

koa-router

const Koa = require("koa")
const Router = require('koa-router');
let app = new Koa();
let router = new Router();
app.use(router.routes()).use(router.allowedMethods());

router.get("/",(ctx,next)=>{
    ctx.body = "首页"
})
router.get("/my",(ctx,next)=>{
    ctx.body = "个人中心"
})
router.get("/setting",(ctx,next)=>{
    ctx.body = "设置"
})
app.listen(3000)
复制代码

koa-bodyparser

const Koa = require('koa')
let bodyParser = require('koa-bodyparser')
let app = new Koa()
app.use(bodyParser()) // 使用中间件
app.use(async (ctx,next)=>{
    ctx.body = ctx.request.body
    await next()
})
app.listen(3000)
复制代码

koa-views

把数据渲染到模板中,然后把模板返回浏览器

let views = require('koa-views');

// Must be used before any router is used
app.use(views(__dirname + '/views', {
  map: {
    html: 'underscore'
  }
}));

app.use(async function (ctx) {
  ctx.state = {
    session: this.session,
    title: 'app'
  };

  await ctx.render('user', {
    user: 'John'
  });
});
复制代码

koa-static

托管静态资源

const serve = require('koa-static');
const Koa = require('koa');
const app = new Koa();

// $ GET /package.json
app.use(serve('.'));

// $ GET /hello.txt
app.use(serve('test/fixtures'));

// or use absolute paths
app.use(serve(__dirname + '/test/fixtures'));

app.listen(3000);

console.log('listening on port 3000');
复制代码

koa-session

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

app.keys = ['some secret hurr'];

const CONFIG = {
  key: 'koa:sess', /** (string) cookie key (default is koa:sess) */
  /** (number || 'session') maxAge in ms (default is 1 days) */
  /** 'session' will result in a cookie that expires when session/browser is closed */
  /** Warning: If a session cookie is stolen, this cookie will never expire */
  maxAge: 86400000,
  autoCommit: true, /** (boolean) automatically commit headers (default true) */
  overwrite: true, /** (boolean) can overwrite or not (default true) */
  httpOnly: true, /** (boolean) httpOnly or not (default true) */
  signed: true, /** (boolean) signed or not (default true) */
  rolling: false, /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. (default is false) */
  renew: false, /** (boolean) renew session when session is nearly expired, so we can always keep user logged in. (default is false)*/
};

app.use(session(CONFIG, app));
// or if you prefer all default config, just use => app.use(session(app));

app.use(ctx => {
  // ignore favicon
  if (ctx.path === '/favicon.ico') return;

  let n = ctx.session.views || 0;
  ctx.session.views = ++n;
  ctx.body = n + ' views';
});

app.listen(3000);
console.log('listening on port 3000');
复制代码

koa-jwt

let Koa = require('koa');
let jwt = require('koa-jwt');

let app = new Koa();

// Custom 401 handling if you don't want to expose koa-jwt errors to users
app.use(function(ctx, next){
  return next().catch((err) => {
    if (401 == err.status) {
      ctx.status = 401;
      ctx.body = 'Protected resource, use Authorization header to get access\n';
    } else {
      throw err;
    }
  });
});

// Unprotected middleware
app.use(function(ctx, next){
  if (ctx.url.match(/^\/public/)) {
    ctx.body = 'unprotected\n';
  } else {
    return next();
  }
});

// Middleware below this line is only reached if JWT token is valid
app.use(jwt({ secret: 'shared-secret' }));

// Protected middleware
app.use(function(ctx){
  if (ctx.url.match(/^\/api/)) {
    ctx.body = 'protected\n';
  }
});

app.listen(3000);
复制代码

koa-ejs

const Koa = require('koa');
const render = require('koa-ejs');
const path = require('path');

const app = new Koa();
render(app, {
  root: path.join(__dirname, 'view'),
  layout: 'template',
  viewExt: 'html',
  cache: false,
  debug: true
});

app.use(async function (ctx) {
  await ctx.render('user');
});

app.listen(7001);
复制代码

koa-compress

let compress = require('koa-compress')
let Koa = require('koa')

let app = new Koa()
app.use(compress({
  filter: function (content_type) {
  	return /text/i.test(content_type)
  },
  threshold: 2048,
  flush: require('zlib').Z_SYNC_FLUSH
}))
复制代码

koa-logger

const logger = require('koa-logger')
const Koa = require('koa')
const app = new Koa()  
app.use(logger({
  transporter: (str, args) => {
    // ...
  }
}))
复制代码

@_@