koa
概述
Koa是一个新的web框架,致力于成为web应用和API开发领域中的一个更小、更富有表现力、更健壮的基石。 koa是express的下一代基于node.js的web框架; koa完全使用Promise并配合async来实现异步
特点
- 轻量,无捆绑
- 中间件架构
- 优雅的API
- 增强的错误处理
安装
npm i koa -s
koa原理
一个基于nodejs的入门级http服务,类似于下面代码:
const http = require('http');
const server = http.createServer((req,res) => {
res.writeHead(200)
res.end('hi yuan')
})
server.listen(3000,() => {
console.log('监听端口3000')
})
下图蓝框框架部分是固定的,要想封装需要对红框业务逻辑进行封装
我们用koa框架,类似于下面代码:
const Koa = require('koa');
const app = new Koa();
app.use((ctx,next) => {
ctx.body = 'hi balabala'
next()
})
app.listen(3000,() => {
console.log(监听3000端口);
})
koa的目标是用更简单化、流程化、模块化的方式实现回调部分。 我们不难看出首先koa是一个class,类里面有两个方法,use和listen。接下来我们自己实现一个koa,取名ykoa
//ykoa.js
const http = require('http');
class ykoa{
listen(...args){
const server = http.createServer( (req,res) => {
this.callback(req,res);
});
server.listen(...args);
};
// use方法调用中间件,就是执行一个回调函数
use(callback){
this.callback = callback;
}
}
module.exports = ykoa
//-----------------------------------------------------------------------------------------------------------------
//index.js
const YKOA = require('./ykoa');
const app = new YKOA();
app.use((req,res) => {
res.writeHead(200)
res.end('hi ykoa');
})
app.listen(3000,() => {
console.log('监听3000端口')
})
nodemon index
启动查看,成功
到目前为止,ykoa还只是个马甲,要真正实现koa还需要引入上下文
context
和中间件机制middleware
context
koa为了能够简化api,引入上下文context概念,将原始请求对象req和响应对象res封装并挂载到context上,并且在context上设置getter和setter,从而简化操作
//创建request.js
module.exports = {
get url() {
return this.req.url
},
get method() {
return this.req.method
}
}
//创建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 method() {
return this.request.method;
},
get body() {
return this.response.body;
},
set body(val) {
this.response.body = val
}
}
class ykoa{
listen(...args){
const server = http.createServer( (req,res) => {
// 创建上下文
const ctx = this.createContext(req,res);
this.callback(ctx);
// 响应
res.end(ctx.body)
});
server.listen(...args);
};
// use方法调用中间件,就是执行一个回调函数
use(callback){
this.callback = callback;
};
// 构建上下文,把res和req都挂载到ctx之上,并且ctx.req和ctx.request.req同时保存
createContext(req,res){
// Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象
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;
}
}
//index.js 可以使用ctx对象优雅的处理req,res
app.use((ctx) => {
ctx.body = 'hi ykoa';
})
中间件机制
koa中间件机制就是函数式组合概念
compose
的概念,将一组需要顺序执行的函数复合为一个函数,外层函数的参数实际是内层函数的返回值。洋葱圈模型可以形象表示这种机制。compose函数,返回函数集 functions 组合后的复合函数;大致特点:
- 参数均为函数, 返回值也是函数
- 第一函数接受参数, 其他函数接受的上一个函数的返回值
- 第一个函数的参数是多元的, 其他函数的一元的
- 自右向左执行
// 定义两个函数
const add = (x,y) => x + y
const square = z => z * z
// 两个函数组合调用
const fn = (x,y) => square(add(x,y))
console.log(fn(1,2))
// 将上面两个函数合并成一个函数
const compose = (fn1,fn2) => (...args) => {
return fn2(fn1(...args))
}
const fn3 = compose(add,square)
console.log(fn3(2,2))
// 多个函数组合:中间件的数目是不固定的,我们可以用数组来模拟
const composes = (...[first,...other]) => (...args) => {
let ret = first(...args);
other.forEach(fn => {
ret = fn(ret)
})
return ret
}
const fn4 = composes(add,square)
console.log(fn4(5,5))
异步中间件,上面的函数都是同步的,顺序遍历执行,如果是异步的函数就是一个pormise,我们要支持async + await的中间件,所以要等异步结束后,在执行下一个中间件。
//compose.js
function compose(middlewares){
return function(){
// 执行第0个函数
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("end fn1")
}
async function fn2(next){
console.log("fn2")
await delay();
await next();
console.log("end fn2")
}
function fn3(next){
console.log('fn3')
}
function delay(){
return new Promise((resolve,reject) => {
setTimeout(() => {
resolve();
},2000);
});
}
const middlewares = [fn1,fn2,fn3];
const finalFn = compose(middlewares);
finalFn();
打印结果
修改ykoa.js,完整版
const context = require("./context");
const request = require("./request");
const response = require("./response");
// 研究koa源码,koa是一个class 里面有两个方法 一个use 一个listen
const http = require('http');
class ykoa{
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方法调用中间件,就是执行一个回调函数
use(middleware){
// 将中间件添加到数组里
this.middlewares.push(middleware);
};
// 构建上下文,把res和req都挂载到ctx之上,并且ctx.req和ctx.request.req同时保存
createContext(req,res){
// Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象
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;
}
// 合成函数
compose(middlewares){
return function(ctx){ //传入上下文
// 执行第0个函数
return dispatch(0);
function dispatch(i){
let fn = middlewares[i];
if(!fn){
return Promise.resolve();
}
return Promise.resolve(
fn(ctx,function next(){ //将上文传入中间件 fn(ctx,next)
// promise完成后再执行下一个
return dispatch(i + 1);
})
)
}
}
}
}
module.exports = ykoa
//index.js测试
const YKOA = require('./ykoa');
const app = new YKOA();
const delay = () =>{
new Promise(resolve => setTimeout(() => resolve(),2000));
}
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((ctx,next) => {
ctx.body += '3'
})
app.listen(3000,() => {
console.log('监听3000端口')
})
查看输出结果
中间件
koa中间件规范:
- 一个async函数
- 接受ctx和next两个参数
- 任务结束需要执行next
中间件常见任务
- 请求拦截
- 路由
- 日志
- 静态文件服务
路由router中间件的实现
//router.js
class Router{
constructor(){
// 初始化一个数组用来存放路由对象
this.stack = [];
}
register(path,methods,middleware){
// 创建route对象
let route = {path,methods,middleware}
// 存放到数组中
this.stack.push(route);
}
// get请求
get(path,middleware){
this.register(path,'get',middleware)
}
// post请求
post(path,middleware){
this.register(path,'post',middleware)
}
routes(){
let stock = this.stack;
return async function(ctx,next){
let currentPath = ctx.url;
let route;
stock.forEach(item => {
// 判断path和method
if(currentPath === item.path && item.methods.indexOf(ctx.method) >= 0){
route = item.middleware;
}
})
if(typeof route === 'function' ){
route(ctx,next)
return;
}
await next();
}
}
}
module.exports = Router
//使用 index.js
const YKOA = require('./ykoa');
const Router = require('./router');
const app = new YKOA();
const router = new Router();
router.get('/index', async ctx => {
console.log('index,xx')
ctx.body = 'index page';
});
router.get('/post', async ctx => { ctx.body = 'post page'; });
router.get('/list', async ctx => { ctx.body = 'list page'; });
router.post('/index', async ctx => { ctx.body = 'post page'; });
app.use(router.routes());
app.listen(3000,() => {
console.log('监听3000端口')
})
查看结果