文章已同步至掘金:https://juejin.cn/post/6844903922428035085
欢迎访问😃,有任何问题都可留言评论哦~
提到 Node 就不得不说其中的两大框架Express
和Koa
然而这两者之间又有什么 渊源 和 爱恨情仇 呢?
说到Express
和Koa
,我们都会发现,他们都会有 中间件(middlewares) 的概念
什么是中间件?
中间件(middlewares)其实就是一个函数
它可以访问请求对象(request object(req))
, 响应对象(response object(res))
, 和 web 应用
中处于请求-响应循环流程中的中间件。
例子:
比如:生活中的租客和房主,中间需要一个中介来搭桥,这个中介就类似于中间件。
在说下面的例子时,会涉及到中间件的解释
Express
与Koa
的区别
中间件的执行顺序:
其实在两种框架中,中间件的执行顺序都是自上而下的
然而最大的区别就是:
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)
根据这个例子就可以知道,其中具有 路由 功能,包括 get
、post
…
再看下面这个例子:
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.use
和 proto.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) => {
// ...
}
}))
评论区