KOA2 中间件机制实现、路由处理、错误处理、模版引擎、源码
总结:基于 NodeJS 的 HTTP 中间件框架
-
常见错误
-
中间件-洋葱模型 中传入空数组,要返回兑现的promise
if(!fn) { return Promise.resolve() } // 不要忘记处理这个边界情况
-
中间件-洋葱模型 中执行中间件要包裹promise
```js rerurn Promise.resolve( fn(ctx, nextFunc) ) // 返回的值使用 Promise.resolve 包裹
-
路由处理时 router 要调用 methods
router.get('/cat', async (ctx) => {}) // 记得要调用 methods 来声明
-
全局错误中间件,KOA2 使用 ctx 包装了req和res,错误的抛出是在 ctx.status 和 ctx.body 上
app.use(async (ctx, next) => { try{ await next() } catch(e) { ctx.status = 500; ctx.body = { error: '错误' } // 经常错写成为 ctx.res.send() - 写法没错,但是不推荐 } })
-
源码实现中 callback需要返回一个函数
// 1、callback 需要返回一个函数:先获取 ctx 上下文,触发执行中间件 return (req, res) => { const ctx = this.createContext(req, res); // 2、中间件的执行需要传入 ctx return fnMiddleware(ctx) // 3、respond需要传递的数据在 ctx 上,不需要明文传递 .then(() => respond(ctx)) // 4、要记得 catch 错误,错误在 ctx 上处理 .catch((err) => ctx.onerror(err)); } // 5、createContext(req, res) 挂载的是 ctx ctx.request 上 ctx.req = ctx.request.req = req; ctx.res = ctx.response.res = res; // 6、respond(ctx) 响应数据在 ctx.body 上 ctx.res.statusCode = ctx.status; // ctx.res.statusCode 是Node原生的http.ServerResponse对象,直接操作原生响应对象的状态码属性 // ctx.status 是 KOA 封装的上下文,便捷属性,会映射到 ctx.res.statusCode ctx.res.end(ctx.body); // ctx.res.end 是 Node.js 原生的 http.ServerResponse 对象,用于结束响应并发送数据 // ctx.res.send 是错误的写法,send() 方法通常是 Express 框架中响应对象的方法 // ctx.body 是 KOA 用于设置响应体的方法
-
-
核心概念
-
中间件机制:洋葱模型,请求进入应用后,依次进入每个中间件的入栈处理,到达最内层中间件后,再依次经过出栈处理 - 组件组合 compose,借用 async/await 避免回调地狱
```js async function fn1(ctx, next) { console.log(‘1’);await next(); console.log(‘5’)} async function fn2(ctx, next) { console.log(‘2’);await next(); console.log(‘4’)} async function fn3(ctx, next) { console.log(‘3’) } function compose(middlewares) { return (ctx, next) => { let index = -1; return dispatch(0);
function dispatch(i) { if (i <= index) { return Promise.reject(new Error('next multiple times')) } index = i; let fn = middlewares[i]; if (i === middlewares.length) { fn = next } if (!fn) { return Promise.resolve() } try { // 定义 next 函数 const nextFunc = () => dispatch(i + 1); // 执行中间件函数 nextFunc 还可以写成 dispatch.bind(null, i+1) return Promise.resolve(fn(ctx, nextFunc)) } catch (e) { return Promise.reject(e) } } }; } const ctx = {}; compose([fn1, fn2, fn3])(ctx, () => Promise.resolve()) .then(() => console.log(11111))
-
中间件编写:
async (ctx, next) =>{ await next() };
-
context 上下文对象:封装 req 请求对象、res 响应对象
ctx.request/ctx.response/ctx.body
-
-
路由处理
-
KOA2 本身没有内置路由功能,使用第三方路由模块 koa-router
```js router.get(‘/cat’, async (ctx) => { }) // 路由定义
-
路由参数处理:路由参数
ctx.params
,链接参数ctx.query
-
-
错误处理
-
全局错误处理中间件
app.use(async (ctx, next) => { try { await next(); } catch (err) { ctx.status = err.statusCode || 500; ctx.body = { error: err.message }; } });
-
-
模版引擎 - 在服务端生成动态 HTML
- ejs模版引擎 <% %>:适合复杂模版,性能弱 - 需要动态解析、执行 js
- art-template模版引擎:``,性能好 - 预编译技术,直接执行 js
-
其他一些配置
- koa-bodyparser:获取 post 提交的数据
app.use( bodyParser() )
- koa-static:静态资源托管
app.use( static(__dirname +'public') )
- ctx.cookies:设置 cookie
ctx.cookies.set(key, value, options)/ctx.cookies.get(key)
- koa-session:在服务端上存储客户端状态,
app.use(session(CONFIG, app))
+ctx.session.key
- koa-bodyparser:获取 post 提交的数据
-
源码实现两件事:① 封装 http 包,通过 get set 简化 API 方法 ② 引入中间件机制/洋葱模型
const http = require('http'); class KOA2 { constructor() { this.middlewares = [] } // 初始化中间件数组 listen(...args) { // 创建一个 http 服务器 const server = http.createServer(this.callback()); // 传入回调函数 return server.listen(...args); // 启动服务 } use(fn) { this.middlewares.push(fn); return this; } // 用于注册中间件 callback() { // 返回一个处理请求的回调函数 const fn = compose(this.middlewares); // 组合中间件 - 组合成一个函数 const handleRequest = (req, res) => { const ctx = this.createContext(req, res); // 创建上下文对象 return this.handleRequest(ctx, fn); // 处理请求 } return handleRequest } createContext(req, res) { // 创建上下文对象的方法, this 上的值是导入其他三个文件所得 const ctx = Object.create(this.context); ctx.request = Object.create(this.request); ctx.response = Object.create(this.response); ctx.req = ctx.request.req = req; // 将 req res 挂载到 ctx 上 ctx.res = ctx.response.res = res; return ctx; } handleRequest(ctx, fnMiddleware) { // 调用组合后的中间件函数 return fnMiddleware(ctx) .then(() => respond(ctx)) // 中间件处理完后,调用 respond 响应 .catch((err) => ctx.onerror(err)); } } function respond(ctx) { // 发送响应 - 根据ctx.body的类型和ctx.status的值自动进行一些响应头的设置和响应体的处理,从而完成响应的发送 - 通过 Node 的http模块将响应发送给客户端 ctx.res.statusCode = ctx.status; ctx.res.end(ctx.body); } // 将中间件函数组合成一个函数,实现中间件的洋葱模型 function compose(middlewares) {} module.exports = KOA2;
为什么要看KOA2
// 项目文件中简单声明即可定义页面标题
const subscribeout = async function (ctx, next) {
await ctx.render('xretail/subscribe_out', {
title: '订阅',
path: 'xretail/subscribe_out'
})
return next();
}
为什么要使用KOA2?
原生 http 的不足
- ①令人困惑的 request 和response,例如 res.end、res.writehead等
- ②对复杂业务逻辑:流程描述、切面描述AOP(语言级、框架级)
KOA2 是一款基于 NODEJS 的 HTTP 中间件框架
- 最大特点:避免异步嵌套
- 要求版本 node > 7.6(完全支持 async/await)
- 源码 4个文件
application.js、context.js、request.js、response.js
1、初步启动服务
const koa = require('koa');
const app = new koa();
app.use(middleware);
app.use(async (ctx) => { // 匹配任意路由
ctx.body = 'hello koa2';
}); // 应用级中间件
app.listen(3000);
app.use 功能:
- 传两个参数 app.use(‘url’, handler) - 匹配第一个参数指定的路由 【路由级中间件】
- 使用场景:router.get(‘/’, myMiddleware, async ((ctx) => { ctx.body=‘1’ }))
- 传一个参数 app.use(handler) - 匹配任意路由,包括未声明的路由 【应用级中间件/错误处理中间件】
ctx 的主要结构:
ctx = {
request: {
method: 'GET',
url: '/',
header: {
host: 'localhost:3002',
connection: 'keep-alive','user-agent': 'Mozilla/5.0...',
accept: 'text/html...'
}
},
response: {
status: 404,
message: 'Not Found',
header: [Object: null prototype] {}
},
app: { subdomainOffset: 2, proxy: false, env: 'development' },
originalUrl: '/',
req: '<original node req>',
res: '<original node res>',
socket: '<original node socket>'
}
2、Koa-router 路由
路由模块:koa-router
// 路由实现方式 - 引入并实例化 router.METHOD(PATH, HANDLER)
const Router = require('koa-router');
const router = new Router(); //【关键】
router.get('/star', async (ctx) => {
ctx.body = 'hello 千玺'; // 相当于 res.writeHead()
})
路由相关的参数获取
localhost:3000/?id=123&name=Jackson // id=123&name=Jackson 链接参数
localhost:3000/star/212 // 212 动态路由
链接参数 ctx.query
ctx.query // 返回的是格式化的参数对象 { id: '123', name: 'Jackson' }
ctx.querystring // 返回的是请求字符串 id=123&name=Jackson
// 也可以从 ctx.url / ctx.request 中获取参数
动态路由 ctx.params
router.get('/star/:id', async (ctx) => {
console.log(ctx.params); //{ id: '212' }
ctx.body = 'hello 千玺';
})
3、中间件
时机:早于 路由处理函数(router.get) 执行
中间件机制:即 函数式组合 的概念,将一组需要顺序执行的函数,复合为一个函数,外层函数的参数实际是内层函数的返回值,即洋葱圈模型 + async/await (避免回调地狱)
const mid = async (ctx, next) => { // ctx 上下文对象 next 下一个中间件
// 来到中间件,洋葱圈左边
await next(); // 进入其他中间件
// 再次来到中间件,洋葱圈右边
}
// 最内层的中间件处理完请求后,会将控制权返回给上一个中间件
1、组合函数(同步实现)
// 组合函数(同步实现)
const add = (x,y) => x + y;
const square = z => z * z; // 先加后平方
// 方案一: const fn = (x, y) => square(add(x, y));
// 方案二: const compose = (fn1, fn2) => (...args) => fn2(fn1(...args));
// 方案三:
const compose = (...[first, ...other]) => (...args) => {
let res = first(...args);
other.forEach(fn => {
res = fn(res);
})
return res;
}
const fn = compose(add, square);
2、组合函数(异步实现)
compose 源码:
- 核心:① 将 context 一路传下去 ② 将 middleware 中的下一个中间件 fn 作为未来 next 的返回值
- 比较关键的就是 dispatch 函数 - 遍历整个 middleware,然后将ctx和dispatch(i + 1)传给下一个方法
async function fn1(next){ console.log('fn1'); next(); console.log('end fn1');}
async function fn2(next){ console.log('fn2'); next(); console.log('end fn2');}
async function fn3(next){ console.log('fn3');}
function compose(middlewares) {
return function () {
return dispatch(0);
// 执⾏第0个 - 递归
function dispatch(i) {
let fn = middlewares[i];
if (!fn) {
return Promise.resolve();
}
return Promise.resolve(
fn(function next() {
// promise完成后,再执⾏下⼀个
return dispatch(i + 1);
})
);
}
};
}
const middlewares = [fn1, fn2, fn3];
const finalFn = compose(middlewares);
finalFn(); // fn1 - fn2 - fn3 - end fn1 - end fn2
4、模版引擎 - 在服务器端生成动态 HTML 页面
① ejs模版引擎
- 语法:使用
<% %>
包裹 js 代码,<%= %>
输出变量的值
是一种JS模版引擎,主要使用 koa-views、ejs 实现
const views = require('koa-views');
// step1: 使用 koa-views 配置中间件
// 第一种 - 对应的路径下,文件后缀为ejs:views/index.ejs 【推荐】
app.use(views('views', { map: { html: 'ejs' }}));
// 第二种 - 对应的路径下,文件后缀为html:views/index.html
app.use(views('views', { extension: 'ejs' }))
// step2: 在 KOA2 路由中使用 ejs
router.get('/add',async (ctx)=>{
let title = 'hello';
await ctx.render('index',{
title // 可以将数据渲染到对应静态页面 views/index.ejs 上的 title 变量
})
})
<h1><%=title%></h1> <!-- 补充 views/index.ejs 中的代码 -->
补充一些 ejs 的设定
-
ejs中的公共数据,在每个路由 render 里面都渲染一个公共数据:ctx.state
app.use(async (ctx, next) => { ctx.state = { name: '千纸鹤'}; await next(); });
-
其他一些功能
```html
-
<%for(var i=0;i<list.length;i++) { %>
- <%=list[i] %> <%}%>
<%-html%>
<% if(num>20){ %> <div>大于20</div> <%} else{ %> <div>小于20</div> <%} %>
② art-template模版引擎
- 语法:使用 包裹变量和表达式,使用 逻辑控制
- 和 ejs 模版引擎的比较
- 同:都是在服务器端生成动态 HTML 页面的模板引擎
- 异:ejs 适合模版需要编写复杂逻辑;art-template 适合简洁语法和高性能
- ①语法
- ②性能:ejs 比较弱,因为要动态解析 js 和执行;art-template 采用了 预编译技术,将模版编译成js函数,执行时直接执行函数,避免了重复解析模版
- ③功能特性:ejs 侧重于提供简单的模版语法和基本逻辑,优势是和 JS无缝集成;art-template 除了基本模版,还有一些高级特性,例如模版继承、子模板引入、过滤器
- ④社区:ejs Express
主要使用 art-template、koa-art-template
const render = require('koa-art-template');
render(app, {
root: path.join(__dirname, 'views'), // 视图的位置
extname: '.html', // 后缀名
debug: process.env.NODE_ENV !== 'production' // 是否开启调试
});
5、koa-bodyparser【post类型】
作用:获取 post 提交的数据
依赖:koa-bodyparser
var bodyParser = require('koa-bodyparser');
app.use(bodyParser()); // 中间件配置
router.post('/toAdd', (ctx) => { // 接收 post 中提交的数据
ctx.body = ctx.request.body;
});
6、koa-static 资源
功能:静态资源托管 - 静态资源路径直接加载,不匹配路由
解决的问题:直接引入静态资源,根据路径加载资源过程中,由于没有对应路由,会 404 找不到对应资源
依赖:koa-static
const static = require('koa-static');
app.use(static(__dirname + 'public')));
app.use(static('static'))) // 可配置多个
过程解析:访问 http://localhost:3000/css/basis.css ,使用 上述配置后
- 先去 public 下查找
- 如果能找到则返回对应文件
- 如果找不到,就 next() 继续匹配路由(底层封装)
7、ctx.cookies
定义:存储于计算机浏览器中的变量,可以在同一浏览器中访问 同一域名 时共享数据
ctx.cookies.set(name, value, [options])
ctx.cookies.get('name');
options 参数
8、session
定义:是另一种记录客户状态的机制,保存在服务器上
依赖:koa-session
app.keys = ['some secret hurr']; // 默认
const CONFIG = {
key: 'koa:sess', //cookie key (默认)
maxAge: 86400000, // cookie 的过期时间 ms,默认 1天
overwrite: true, //是否可以 overwrite (默认 true)
httpOnly: true, //cookie 是否只有服务器端可以访问 (默认 true)
signed: true, //签名默认 true
rolling: true, //在每次请求时强行设置 cookie,重置过期时间,默认 false
renew: false, //renew session when session is nearly expired 默认 false
}
app.use(session(CONFIG, app));
设置 ctx.session.username = “张三” 后获取 ctx.session.username 流程
首次访问:
- ① 先在服务端创建对应 session
- ② 并将 session 中的 key 值返回给 客户端,客户端存储在 cookie 中
再次访问:
- ① 客户端 从 cookie 中获取对应的 key 值 发送给服务端
- ② 服务端根据对应的 key 值查询 session 中的 value 值
- ③ 服务端将 value 值发送给客户端
9、koa应用生成器
功能:通过应用 koa 脚手架生成工具 可以快速创建一个基于 koa2 的应用的骨架
1、全局安装 `npm install koa-generator -g`
2、创建项目 `koa your_project_name`
3、进入到项目中,安装依赖 `npm install`
4、启动项目 `npm start`
10、KOA2 源码解析
源码一共做了两件事
-
1、通过get、set方法简化了API的实现
-
2、引入中间件机制 - 洋葱圈模型
① listen use 方法的封装 - http 包
如下,是用 http 包实现的请求,KOA2 基于该模型,将业务逻辑 可变部分 作为参数传入,框架则封装起来
- 目标是用更简单化、流程化、模块化的方法实现回调部分
// 框架 - 用 http 包实现的请求
const http = require('http');
const server = http.createServer((req, res) => {
// 业务逻辑
res.writeHead(200);
res.end('hi');
})
server.listen(3000, () => { console.log('监听端口3000') })
KOA2 源码中,将 http 包封装成 listen 和 use:
const http = require('http');
class KKB { // 模块化
listen(...arg) {
const server = http.createServer((req, res) => { this.callback(req, res) })
server.listen(...arg);
}
use(callback) { this.callback = callback };
}
module.exports = KKB;
// 调用
const KKB = require('./kkb');
const app = new KKB();
app.use((req, res) => {
res.writeHead(200);
res.end('hi');
})
server.listen(3000, () => { console.log('监听端口3000') })
② context 的封装
为了简化 API ,KOA2引入了上下文的概念,将原始请求对象 req 和响应对象 res 封装并挂载 到 context,并设置了 getter 和 setter
原理图如下:
③ 引入中间件机制
引入了组合函数的概念, 代码中引入了这个包:koa-compose
简单实现一下包里面的源码逻辑就是 compose 方法
源码的四个文件,request.js response.js都是 get 和set 方法对 http 原生的方法进行了封装、简化
request.js
module.exports = {
get url() { return this.req.url },
get method() { return this.req.method.toLowerCase() }
}
response.js
module.exports = {
get body() { return this._body },
set body(val) { this._body = val }
}
context.js
module.exports = {
get url() { return this.request.url },
get body() { return this.response.body },
set body(val) { this.response.body = val },
get method() { return this.request.method }
}
application.js
// 导⼊这三个类
const context = require("./context");
const request = require("./request");
const response = require("./response");
class Application extends Emitter {
constructor() {
this.middlewares = [];
}
// 构建上下⽂, 把res和req都挂载到ctx之上,并且在ctx.req和ctx.request.req同时保存
createContext(req, res) {
const ctx = Object.create(context);
ctx.request = Object.create(request);
ctx.response = Object.create(response);
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
return ctx;
},
listen(...args) {
const server = http.createServer((req, res) => {
let ctx = this.createContext(req, res); // 创建上下⽂
// 组合函数,将所有中间件调用的函数合成
const fn = this.compose(this.middlewares);
// 数据响应
res.end(ctx.body);
});
},
use(middleware) { // 将中间件加到数组⾥
this.middlewares.push(middleware);
},
compose(middlewares) {
// 看上文
}
}
调用的时候只需要写
app.use((ctx) => {
ctx.body = '测试';
});
app.use(async (ctx, next) => {
ctx.body = "1";
await next();
ctx.body += "5";
});
app.use(async (ctx, next) => {
ctx.body += "2";
await next();
ctx.body += "4";
});
app.use(async (ctx, next) => {
ctx.body += "3";
});
1,2,3,4,5