# Why Koa
Express 的下一代基于 Node.js 的 web 框架
特点
- 轻量无捆绑
- 中间件架构
- 优雅的 API 设计
- 增强的错误处理
安装
代码语言:javascript复制npm i koa -S
# Hello Koa
Hello Koa
代码语言:javascript复制const Koa = require('koa');
const app = new Koa();
// 日志中间件
app.use(async (ctx, next) => {
const sT = new Date().getTime();
console.log(`[${sT}]start: ${ctx.url}`);
await next(); // 执行下一个中间件
const eT = new Date().getTime();
console.log(`[${eT}]end: ${ctx.url} passed ${eT - sT}ms`);
});
app.use(async (ctx, next) => {
ctx.body = [
{
name: 'cell',
msg: 'hello koa',
}
];
await next();
});
app.listen(3000);
- Koa 中间件机制
# 实现简单 http 服务
代码语言:javascript复制const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('hello http server');
});
server.listen(3000, () => {
console.log('listen at 3000');
});
# 实现简单 Koa
基础 ToyKoa.js
const ToyKoa = require('./toykoa');
const app = new ToyKoa();
app.use((req, res) => {
res.writeHead(200);
res.end('Hello ToyKoa');
});
app.listen(3000, () => {
console.log('listen at 3000');
});
使用 app.js
const ToyKoa = require('./toykoa');
const app = new ToyKoa();
app.use((req, res) => {
res.writeHead(200);
res.end('Hello ToyKoa');
});
app.listen(3000, () => {
console.log('listen at 3000');
});
# context
koa 为了能够简化 API,引入上下文 context,将原始请求对象 req 和 响应对象 res 封装并挂载到 context 上,并且在 context 上设置 getter 和 setter,从而简化操作
- 封装 request
// request.js
module.exports = {
get url() {
return this.req.url;
},
get method() {
return this.req.method.toLowerCase();
},
};
- 封装 response
// response.js
module.exports = {
get body() {
return this._body;
},
set body(val) {
this._body = val;
},
}
- 封装 context
// 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;
}
};
- 创建 context 并挂载 req 和 res
// toykoa.js
const http = require('http');
const context = require('./context');
const request = require('./request');
const response = require('./response');
class ToyKoa {
listen(...args) {
const server = http.createServer((req, res) => {
// 创建上下文
let ctx = this.createContext(req, res);
this.cb(ctx);
// 响应
res.end(ctx.body);
});
server.listen(...args);
}
use(cb) {
this.cb = cb;
}
/**
* 构建上下文
* @param {*} req
* @param {*} res
*/
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;
}
}
module.exports = ToyKoa;
- 测试 context
// app.js
const ToyKoa = require('./toykoa');
const app = new ToyKoa();
app.use(ctx => {
ctx.body = 'Hello toykoa!';
});
app.listen(3000, () => {
console.log('listen at 3000');
});
# 中间件
Koa 中间件机制:函数式组合(Compose),将一组需要顺序执行的函数复合为一个函数,外层函数的参数实际是内层函数的返回值,可以通过洋葱模型理解。
代码语言:javascript复制const add = (x, y) => x y;
const square = z => z * z;
const fn = (x, y) => square(add(x, y));
console.log(fn(1, 2)); // 9
// 将上面两次函数的组合调用合成一个函数
const compose = (fn1, fn2) => {
return (...args) => fn2(fn1(...args));
};
const fn_compose = compose(add, square);
console.log(fn_compose(1, 2)); // 9
// 多个函数组合
const composePlus = (...[first, ...other]) => {
return (...args) => {
let ret = first(...args);
other.forEach(fn => {
ret = fn(ret);
});
return ret;
};
};
const fn_compose_plus = composePlus(add, square);
console.log(fn_compose_plus(1, 2)); // 9
异步函数组合
代码语言:javascript复制function compose(middlewares) {
return function () {
return dispatch(0);
function dispatch(i) {
let fn = middlewares[i];
if (!fn) {
return Promise.resolve();
}
return Promise.resolve(
fn(function next() {
// promise 完成后,再执行下一个
return dispatch(i 1);
})
);
}
};
}
async function fn1(next) {
console.log('fn1');
await next();
console.log('fn1 end');
}
async function fn2(next) {
console.log('fn2');
await delay();
await next();
console.log('fn2 end');
}
function fn3(next) {
console.log('fn3');
}
function delay() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 1000);
});
}
const middlewares = [fn1, fn2, fn3];
const finalFn = compose(middlewares);
finalFn();
// 输出
// fn1
// fn2
// fn3
// fn2 end
// fn1 end
将 compose 应用在 koa 中间件中
代码语言:javascript复制// toykoa.js
const http = require('http');
const context = require('./context');
const request = require('./request');
const response = require('./response');
class ToyKoa {
constructor() {
// 初始化中间件数组
this.middlewares = [];
}
listen(...args) {
const server = http.createServer(async (req, res) => {
// 创建上下文
const ctx = this.createContext(req, res);
// 中间件合成
const fn = this.compose(this.middlewares);
// 执行合成函数并传入上下文
await fn(ctx);
// 响应
res.end(ctx.body);
});
server.listen(...args);
}
use(middleware) {
this.middlewares.push(middleware);
}
/**
* 构建上下文
* @param {*} req
* @param {*} res
*/
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;
}
/**
* 合成函数
* @param {*} middlewares
*/
compose(middlewares) {
return function(ctx) {
return dispatch(0);
function dispatch(i) {
let fn = middlewares[i];
if (!fn) {
return Promise.resolve();
}
return Promise.resolve(
fn(ctx, function next() {
return dispatch(i 1);
})
);
}
};
}
}
module.exports = ToyKoa;
测试中间件
代码语言:javascript复制// app.js
const ToyKoa = require('./toykoa');
const app = new ToyKoa();
// app.use((req, res) => {
// res.writeHead(200);
// res.end('Hello ToyKoa');
// });
const delay = () => new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 1000);
});
app.use(async (ctx, next) => {
ctx.body = '1'
await next();
ctx.body = '5';
});
app.use(async (ctx, next) => {
ctx.body = '2'
await delay();
await next();
ctx.body = '4';
});
app.use(async (ctx, next) => {
ctx.body = 'Hello toykoa!';
});
app.listen(3000, () => {
console.log('listen at 3000');
});
返回响应结果为 12Hello toykoa!45
# 常用 koa 中间件的实现
koa 中间件的规范
- 一个 async 函数
- 接收 ctx 和 next 两个参数
- 任务结束需要执行 next
// 【洋葱状态】:(
const middleware = async (ctx, next) => {
// 进入当前洋葱 【洋葱状态】:(当前洋葱
// (当前洋葱(
next(); // 进入 子洋葱 【洋葱状态】:(当前洋葱(子洋葱
// 出 子洋葱 【洋葱状态】:(当前洋葱(子洋葱)当前洋葱
}
// 【洋葱状态】:(当前洋葱(子洋葱)当前洋葱)
中间件常见任务
- 请求拦截
- 路由
- 日志
- 静态文件服务
路由实现
代码语言:javascript复制// router.js
class Router {
constructor() {
this.stack = [];
}
register(path, methods, middleware) {
let route = { path, methods, middleware };
this.stack.push(route);
}
get(path, middleware) {
this.register(path, 'get', middleware);
}
post(path, middleware) {
this.register(path, 'post', middleware);
}
routes() {
let stock = this.stack;
return async function(ctx, next) {
let currentPath = ctx.url;
let route;
for (let i = 0; i < stock.length; i ) {
let item = stock[i];
if (currentPath === item.path && item.methods.indexOf(ctx.method) >= 0) {
route = item.middleware;
break;
}
}
if (typeof route === 'function') {
route(ctx, next);
return;
}
await next();
};
}
}
module.exports = Router;
路由测试
代码语言:javascript复制// app.js
const ToyKoa = require('./toykoa');
const app = new ToyKoa();
const Router = require('./router');
const router = new Router();
router.get('/index', async ctx => { ctx.body = 'get index'; });
router.get('/post', async ctx => { ctx.body = 'get post'; });
router.get('/list', async ctx => { ctx.body = 'get list'; });
router.post('/index', async ctx => { ctx.body = 'post index'; });
// 路由实例输出父中间件 router.routes()
app.use(router.routes());
// ...
app.listen(3000, () => {
console.log('listen at 3000');
});
静态文件服务 koa-static
- 配置绝对资源目录地址,默认为 static
- 获取文件或者目录信息
- 静态文件读取
- 返回
static 实现
代码语言:javascript复制// static.js
const fs = require('fs');
const path = require('path');
module.exports = (dirPath = './public') => {
return async (ctx, next) => {
if (ctx.url.indexOf('/public') === 0) {
const url = path.resolve(__dirname, dirPath);
const filepath = url ctx.url.replace('/public', '');
try {
const stats = fs.statSync(filepath);
if (stats.isDirectory()) { // 文件夹
const dir = fs.readdirSync(filepath);
const ret = ['<div style="padding-left:20px">'];
dir.forEach(filename => {
if (filename.indexOf('.') > -1) {
ret.push(`
<p>
<a style="color:black" href="${ctx.url}/${filename}">${filename}</a>
</p>
`);
} else {
ret.push(`
<p>
<a href="${ctx.url}/${filename}">${filename}</a>
</p>
`);
}
});
ret.push('</div>');
ctx.body = ret.join('');
} else { // 文件
const content = fs.readFileSync(filepath);
ctx.body = content;
}
} catch (err) {
ctx.body = '404 not found';
}
} else {
// 不是静态资源忽略
await next();
}
};
};
添加测试资源 public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="styles/style.css">
</head>
<body>
<h1>Hello ToyKoa!</h1>
</body>
</html>
public/styles/style.css
body {
background-color: red;
}
static 测试
代码语言:javascript复制// app.js
const ToyKoa = require('./toykoa');
const app = new ToyKoa();
const static = require('./static');
app.use(static(__dirname '/public'));
// ...
app.listen(3000, () => {
console.log('listen at 3000');
});