# 前瞻
Express框架和Koa框架都出自于同一个团队,可以说Koa框架是Express框架的衍生但有别于Express框架,因为Koa框架的核心代码仅有1600+行,且由TJ大佬在维护。从架构设计上来说,Express是完整和强大的,在其中帮助我们内置了很多好用的功能;而Koa则是简洁和自由的,毕竟核心代码只有1600+行,完全称得上轻量级,它只包含最核心的功能,并不会对我们使用其他中间件进行任何的限制。两个框架的核心其实都是中间件,但两者的中间件执行机制是不同的,特别是针对某个中间件包含异步操作时,接下来会详细说说~
# Express与Koa的比较
# 创建服务器
// Express框架
const express = require('express')
const app = express()
app.listen(1234, (req, res, next) => {
console.log('Express server has running at http://localhost:1234')
})
// Koa框架
const Koa = require('koa')
const app = new Koa()
app.listen(1235, (ctx, next) => {
console.log('Koa server has running at http://localhost:1235')
})
# 静态资源服务器
Express框架
Express内置了static方法实现托管静态资源
// 通过添加路径前缀,统一访问public目录中的静态文件 app.use('/public', express.static('public'))
Koa框架
Koa实现托管静态资源需使用第三方库:koa-static
const Koa = require('koa') const static = require('koa-static') const app = new Koa() // 处理静态资源 app.use(static('./build'))
# 创建中间件
中间件的本质就是一个回调函数
Express框架
请求对象(request对象)
响应对象(response对象)
next函数(用于执行下一个中间件的函数)
app.use((req, res, next) => { console.log('这是Express中的中间件~') })
Koa框架
上下文对象
- Koa将请求对象和响应对象封装在了上下文对象中了
- 获取请求对象:ctx.request
- 获取响应对象:ctx.response
next函数
- 在Koa中,next本质上是dispatch函数(源码中体现),其作用与Express框架中的next函数类似
app.use((ctx, next) => { console.log('这是Koa中的中间件~') })
Koa没有提供methods方式来注册中间件,也没有提供path中间件来匹配路径,同样也没有连续注册中间件的方式;而Express拥有这些方式
// Express所独有的特点 // methods方式注册中间件 app.get('url', (req, res, next) => { res.end('methods方式注册中间件') }) // path中间件匹配路径 app.post('/user', (req, res, next) => { res.end('path中间件匹配路径') }) // 连续注册中间件 app.get("/home", (req, res, next) => { console.log("home path and method middleware 01") next() }, (req, res, next) => { console.log("home path and method middleware 02") next() }, (req, res, next) => { console.log("home path and method middleware 03") next() }, (req, res, next) => { console.log("home path and method middleware 04") res.end("home middleware end") })
# 路由
Express框架
Express可以自动处理路径和method的匹配问题,如果两者同时匹配成功,则Express会将这次请求,转交给对应的中间件处理
// 匹配GET请求,且请求url为 / app.get('/', (req, res, next) => { res.end('Hello Express~') })
模块化路由
const express = require('express') const userRouter = express.Router() userRouter.get('/', (req, res, next) => { res.end('获取用户列表成功~') }) userRouter.post('/', (req, res, next) => { res.end('创建用户成功~') }) userRouter.delete('/', (req, res, next) => { res.end('删除用户成功~') }) app.use('/users', userRouter)
Koa框架
Koa不能自动处理路径和method的匹配问题,但可以根据request自己来判断或使用第三方路由中间件(koa-router)
根据request自己来判断
app.use((ctx, next) => { if (ctx.request.url === '/users') { if (ctx.response.method === 'POST') { ctx.response.body = 'Create User Success' } else { ctx.response.body = 'Users List' } } else { ctx.response.body = 'Other Request' } })
使用第三方路由中间件
模块化路由
const Router = require('koa-router') // 创建路由实例,定义当前路由统一前缀 const router = new Router({ prefix: '/users' }) router.put('/', (ctx, next) => { ctx.response.body = 'put request' }) module.exports = router // 主文件中 const userRouter = require('./router/user') app.use(userRouter.routes()) app.use(userRouter.allowedMethods())
注意:allowedMethods方法用于判断一个method是否支持,如果我们请求一些未实现的请求,就会自动报错:Method Not Allowed
# 参数解析
Express框架
获取URL中携带的查询参数
请求地址:http://localhost:8000/user?name=zs&age=20
app.get('/user', (req, res, next) => { // req.query 默认是一个空对象 console.log(req.query) })
获取URL中的动态参数
请求地址:http://localhost:8000/user/216
app.get('/user/:id', (req, res, next) => { // req.params 默认是一个空对象,里面存放着通过冒号动态匹配到的参数值 console.log(req.params.id) })
解析JSON格式的数据
内置中间件 express.json()
app.use(express.json()) app.use((req, res, next) => { if (req.headers['content-type'] === 'application/json') { req.on('data', data => { const info = JSON.parse(data.toString()) // 通过req.body将数据传递给下一个中间件 req.body = info }) req.on('end', () => { next() }) } else { next() } })
解析URL-encoded格式的数据
- true:使用第三方库(qs)进行解析
- false:使用Node内置模块(querystring)进行解析
// 配置解析 application/x-www-form-urlencoded 格式数据的内置中间件 // 解析urlencoded格式的数据时,需要添加extended属性表明使用哪个模块进行解析 app.use(express.urlencoded({ extended: true }))
解析form-data格式的数据
需要用到multer这个第三方库
const multer = require('multer') =========== 解析非文件类型 =========== const upload = multer() // any()解析非文件类型数据 app.use(upload.any()) =========== 解析文件类型 =========== const upload = multer({ // 文件存放位置 dest: './uploads/' }) // upload.single('key') -> 上传单个文件,并将数据的key传递给single函数 app.post('/upload', upload.single('file'), (req, res, next) => { res.end("文件上传成功~") }) ======== 使用diskStorage自定义文件名 ======== const path = require('path') const storage = multer.diskStorage({ destination: (req, res, cb) => { cb(null, './uploads/') }, filename: (req, res, cb) => { cb(null, Date.now() + path.extname(file.originalname)) } }) const upload = multer({ storage }) app.post('/upload', upload.single('file'), (req, res, next) => { res.end("文件上传成功~") })
Koa框架
获取URL中携带的查询参数
请求地址:http://locahost:8000/login?username=licodeao&password=123
app.use((ctx, next) => { console.log(ctx.request.query) ctx.body = "Hello Koa" })
获取URL中的动态参数
请求地址:http://localhost:8000/users/123
const userRouter = new Router({ prefix: '/users' }) // 获取params,使用路由可以自动解析 userRouter.get("/:id", (ctx, next) => { console.log(ctx.params.id) ctx.body = "Hello Koa" })
解析JSON格式的数据
需要使用第三方库 koa-bodyparser
请求地址:http://localhost:8000/login
// body是json格式 { "username": "licodeao", "password": "123" } const bodyParser = require('koa-bodyparser') app.use(bodyParser()) app.use((ctx, next) => { // 解析后的数据被放在了request中 console.log(ctx.request.body) ctx.body = "Hello koa" })
解析URL-encoded格式的数据
请求地址:http://localhost:8000/login
body是x-www-form-urlencoded格式
获取数据和json一样,安装koa-bodyparser
解析form-data格式的数据
需要使用koa-multer第三方库
const multer = require('koa-multer') const upload = multer() app.use(upload.any()) app.use((ctx, next) => { // 注意解析的数据被放在了req中,而不是request(与json不一样)!!! console.log(ctx.req.body) ctx.body = "Hello Koa" })
文件上传
const Koa = require('Koa') const path = require('path') const multer = require('koa-multer') const Router = require('koa-router') const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, "./uploads/") }, filename: (req, file, cb) => { cb(null, Date.now() + path.extname(file.originalname)) } }) const upload = multer({ storage }) const fileRouter = new Router({ prefix: '/upload'}); fileRouter.post('/', upload.single('avatar'), (ctx, next) => { // 解析数据在req中 console.log(ctx.req.file) ctx.response.body = "上传成功~" })
# 错误处理
Express框架
// 错误级别的中间件必须注册在所有路由之后! app.use((err, req, res, next) => { // 在服务器打印错误消息 console.log('发生了错误:' + err.message); // 向客户端响应错误相关的内容 res.send('Error!' + err.message); })
Koa框架
const Koa = require('koa') const app = new Koa() app.use((ctx, next) => { // 通过ctx.app.emit()发出错误事件 ctx.app.emit('error', new Error("还没有登录嗷~"), ctx) }) // 监听错误事件 app.on('error', (err, next) => { // 错误信息在err.message中 console.log(err.message) ctx.response.body = err.message }) app.listen(8000, () => { console.log("错误处理服务器启动成功~") })
# 源码分析
# Express框架
调用express()到底创建的是什么
调用express()实际上是调用了createApplication函数
// 源码 exports = module.exports = createApplication // express()实际上是调用了该函数 function createApplication() { var app = function(req, res, next) { app.handle(req, res, next) } mixin(app, EventEmitter.prototype, false) mixin(app, proto, false) // 封装了request app.request = Object.create(req, { app: { configurable: true, enumerable: true, writable: true, value: app } }) // 封装了response app.response = Object.create(res, { app: { configurable: true, enumerable: true, writable: true, value: app } }) app.init() return app } // 启动服务器 app.listen = function listen() { // this就是app对象 var server = http.createServer(this) return server.listen.apply(server, arguments) }
use注册一个中间件
无论是app.use还是app.methods都会注册一个主路由
app本质上会将所有的函数交给整个主路由来处理
// 源码 app.use = function use(fn) { var offset = 0 var path = '/' ...... // 扁平化处理 var fns = flatten(slice.call(arguments, offset)) // 注册一个主路由 this.lazyrouter() var router = this._router // 不断去查找中间件 fns.forEach(function(fn) { // 无app被创建时 if (!fn || !fn.handle || !fn.set) { return router.use(path, fn) } ...... router.use(path, function mounted_app(req, res, next){ ... }) }) } // use函数中 var layer = new Layer(path, { sensitive: this.caseSensitive, strict: false, end: false }, fn) layer.route = undefined // 加入调用栈中 this.stack.push(layer)
一个请求过来,从哪里开始处理
从app函数被调用开始
// 上面观察到createApplication函数中有app.handle方法 app.handle = function(req, res, callback) { var router = this._router // 最终handle var done = callback || finalhandler(req, res, { env: this.get('env'), onerror: logger.bind(this) }) // 没有路由时 if (!router) { debug('no routes defined on app') done() return } // 开始匹配路由和方法,并处理请求 router.handle(req, res, done) }
router.handle中做了什么事情
proto.handle = function handle(req, res, out) { var self = this ...... // 取出stack var stack = self.stack; ...... } // 在handle的next方法中,不断查询是否匹配,如果匹配,就离开调用栈并执行该next方法所对应的中间件 while(match !== true && idx < stack.length) { ...... // 查看是否匹配 if (match !== true) { continue; } ...... }
# Koa框架
require('koa'),导出的是什么
导出的是Application这个类
const Koa = require('koa')
这也是为什么在创建实例时需要大写
// 源码 module.exports = class Application extends Emitter { constructor(options) { super(); options = options || {}; this.proxy = options.proxy || false; this.subdomainOffset = options.subdomainOffset || 2; this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For'; this.maxIpsCount = options.maxIpsCount || 0; this.env = options.env || process.env.NODE_ENV || 'development'; if (options.keys) { this.keys = options.keys } // 中间件数据 this.middleware = [] // 创建请求上下文 this.context = Object.create(context) this.request = Object.create(request) this.response = Object.create(response) ... } }
Koa中如何开启监听?
// 同样是封装了listen方法 listen(...args) { debug('listen'); const server = http.createServer(this.callback()) return server.listen(...args); }
Koa如何注册中间件?
// use函数注册中间件 use(fn) { // 判断是否为函数 if (typeof fn !== 'function') { throw new TypeError('middleware muse be a function!') } if (isGeneratorFunction(fn)) { deprecate('Support for generators will be removed in v3...') fn = convert(fn) } debug('use %s', fn._name || fn.name || '-'); // 推入中间件数组中,处于待执行状态 this.middleware.push(fn); return this; }
监听回调
// const server = http.createServer(this.callback()) ↑ callback() { // 处理后的中间件,返回的是个Promise const fn = compose(this.middleware); // 错误处理 if (!this.listenerCount('error')) { this.on('error', this.onerror) } // 闭包 const handleRequest = (req, res) => { // 创建请求上下文 const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); } return handleRequest; }
handleRequest方法
// 处理请求 handleRequest(ctx, fnMiddleware) { const res = ctx.res; const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); // 这里意味着: 等所有中间件运行完后,才会响应结果 // 同样注意:这里的结果是个Promise return fnMiddleware(ctx).then(handleResponse).catch(onerror); }
compose方法
function compose(middleware) { // 判断middleware是否为数组,如果不是则抛出异常 if (!Array.isArray(middleware)) { throw new TypeError('Middleware stack must be an array!') } // middleware的原生如果不是函数,则抛出异常 for (const fn of middleware) { if (typeof fn !== 'function') { throw new TypeError('Middleware must be composed of functions!') } } // 返回值 return function(context, 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 { // dispatch.bind(null, i + 1)相当于是next函数 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))) // 相当于是以下代码 Promise.resolve((async (ctx, next) => { console.log("执行中间件"); await Promise.resolve(fn(context, dispatch.bind(null, i + 1)))(); next(); console.log("中间件next之后代码") })(context, dispatch.bind(null, i + 1))) } catch (err) { return Promise.reject(err) } } } }
# 中间件的执行顺序
对于某个中间件包含异步操作时,Express框架和Koa框架的机制是不一样的
假设有三个中间件会在一次请求中匹配到,并且按照顺序执行
希望实现的结果是:
- 在middleware1中,在req.message中添加一个字符串aaa
- 在middleware2中,在req.message中添加一个字符串bbb
- 在middleware3中,在req.message中添加一个字符串ccc
- 当所有的内容添加结束后,在middleware1中,通过res返回最终的结果
Express同步数据的实现
const express = require('express');
const app = express();
const middleware1 = (req, res, next) => {
req.message = 'aaa';
// 这里去执行下一个中间件,只有当所有中间件执行完成时,才会执行下一步,这里返回的结果将会是所有中间件累加的结果(对于这个需求来说)
next();
// 中间件都执行完毕后,才会向服务器返回完整的数据
res.end(req.message);
}
const middleware2 = (req, res, next) => {
req.message += 'bbb';
next();
}
const middleware3 = (req, res, next) => {
req.message += 'ccc';
}
app.use(middleware1, middleware2, middleware3);
app.listen(8000, () => {
console.log("服务器启动成功~");
})
Express异步数据的实现
Express异步数据的实现相较于Koa比较麻烦
const express = require('express');
const axios = require('axios');
const app = express();
const middleware1 = async (req, res, next) => {
req.message = "aaa";
await next();
res.end(req.message);
}
const middleware2 = async (req, res, next) => {
req.message += "bbb";
await next();
}
const middleware3 = async (req, res, next) => {
const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876');
req.message += result.data.lrc.lyric;
}
app.use(middleware1, middleware2, middleware3);
app.listen(8000, () => {
console.log("服务器启动成功~");
})
// 结果:aaabbb
Koa同步数据的实现
实现原理图与Express同步数据的实现一致
const Koa = require('koa');
const app = new Koa();
const middleware1 = (ctx, next) => {
ctx.message = "aaa";
next();
ctx.body = ctx.message;
}
const middleware2 = (ctx, next) => {
ctx.message += "bbb";
next();
}
const middleware3 = (ctx, next) => {
ctx.message += "ccc";
}
app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
app.listen(8000, () => {
console.log("服务器启动成功~");
})
Koa异步数据的实现
Koa异步数据的实现较为方便,是因为其内部返回了一个Promise(详细可看上方源码)
const Koa = require('koa');
const axios = require('axios');
const app = new Koa();
const middleware1 = async (ctx, next) => {
ctx.message = "aaa";
// 由于内部返回的是一个Promise,所以不管同步还是异步,都会等到有结果后才执行下一步
await next();
ctx.body = ctx.message;
}
const middleware2 = async (ctx, next) => {
ctx.message += "bbb";
await next();
}
const middleware3 = async (ctx, next) => {
const result = await axios.get('http://123.207.32.32:9001/lyric?id=167876');
ctx.message += result.data.lrc.lyric;
}
app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
app.listen(8000, () => {
console.log("服务器启动成功~");
})
// 结果:aaabbb+axios得到的数据
值得注意的是:虽然上方的Express与Koa的异步实现看起来一样,但运行结果却大相径庭。
Express异步得到的结果:aaabbb
Koa异步得到的结果:aaabbb+axios返回的数据
造成这样结果的原因,显然是因为Express框架和Koa框架的机制是不一样的,Koa处理中间件时返回的是Promise,所以一定会得到一个完整的结果。 而Express处理中间件是同步执行的,有异步操作时会得不到数据。所以上方Express异步数据的实现中的async、await相当于白加。
# Koa洋葱模型
来自Koa社区针对于中间件的盛行的说法
两层理解:
中间件处理代码的过程
直至中间件所有代码执行完毕后,才会返回response结果
response返回body执行
res.body = "结果"
Express框架有洋葱模型吗?
其实算有的,上方代码《Express同步数据的实现》就是一个洋葱模型,当然在Express框架中必须得是同步数据才行,而在Koa中,不管同步还是异步,都有洋葱模型存在。至于为什么,上方已经解释的很清楚啦。