koa2的特点优势
什么是 koa2
- Nodejs官方api支持的都是callback形式的异步编程模型。问题:callback嵌套问题
- koa2 是由 Express原班人马打造的,是现在比较流行的基于Node.js平台的web开发框架,Koa 把 Express 中内置的 router、view 等功能都移除了,使得框架本身更轻量,而且扩展性很强。使用koa编写web应用,可以免除重复繁琐的回调函数。
koa2 的优点
优点这个东西,我直接说它多好,你可能又不开心,但是我们可以对比哦!这里我只说它对比原生的 Node.js开启 http 服务 带来了哪些优点!
- 先看一下原生 Node.js 我开启一个 http 服务
const http = require('http');
http.createServer((req,res)=>{
res.writeHead(200);
res.end('hi koala');
}).listen(3000);
- 看一下使用 koa2 开启一个http 服务
const Koa = require('koa') ;
const app = new Koa();
const {createReadStream} = require('fs');
app.use(async (ctx,next)=>{
if(ctx.path === '/favicon.ico'){
ctx.body = createReadStream('./avicon.ico')
}else{
await next();
}
});
app.use(ctx=>{
ctx.body = 'hi koala';
})
app.listen(3000);
我在 koa2 中添加了一个判断 /favicon.ico 的实现 通过以上两段代码,会发现下面几个优点
- 传统的 http 服务想使用
模块化
不是很方便,我们不能在一个服务里面判断所有的请求和一些内容。而 koa2 对模块化提供了更好的帮助 - koa2 把 req,res 封装到了
context
中,更简洁而且方便记忆 - 中间件机制,采用
洋葱模型
,洋葱模型流程记住一点(洋葱是先从皮到心,然后从心到皮),通过洋葱模型把代码流程化
,让流水线
更加清楚,如果不使用中间件,在createServer
一条线判断所有逻辑确实不好。 - 看不到的优点也很多,error 错误处理,res的封装处理等。
自己实现一个koa2
在实现的过程中会我看看可以学到那些知识
listen 函数简单封装
koa2 直接使用的时候,我们通过 const app = new Koa();
,koa
应该是一个类,而且可以直接调用 listen
函数,并且没有暴漏出http
服务的创建,说明在listen
函数中可能创建了服务。到此简单代码实现应该是这样的:
class Kkb{
constructor(){
this.middlewares = [];
}
listen(...args){
http.createServer(async (req,res)=>{
// 给用户返回信息
this.callback(req,res);
res.writeHead(200);
res.statusCode = 200;
res.end('hello koala')
}).listen(...args)
}
}
module.exports = Kkb;
实现 context 的封装
实现了简单 listen 后,会发现回调函数返回的还是 req 和 res ,要是将二者封装到 context 一次返回就更好了!我们继续
代码语言:javascript复制 const ctx = this.createContext(req,res);
看一下 createContext 的具体实现
代码语言:javascript复制const request = require('./lib/request');
const response = require('./lib/response');
const context = require('./lib/context');
createContext(req,res){
// 创建一个新对象,继承导入的context
const ctx = Object.create(context);
ctx.request = Object.create(request);
ctx.response = Object.create(response);
// 这里的两等于判断,让使用者既可以直接使用ctx,也可以使用原生的内容
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
return ctx;
}
context.js
代码语言:javascript复制module.exports = {
get url(){
return this.request.url;
},
get body(){
return this.response.body;
},
set body(val){
this.response.body = val;
}
}
request.js
代码语言:javascript复制module.exports = {
get url(){
return this.req.url;
}
}
这里在写 context.js 时候,用到了set 与 get 函数,get 语句作为函数绑定在对象的属性上,当访问该属性时调用该函数。set 语法可以将一个函数绑定在当前对象的指定属性上,当那个属性被赋值时,你所绑定的函数就会被调用。
实现洋葱模型
compose 另一个应用场景
说洋葱模型之前先看一个函数式编程内容:compose 函数前端用过 redux 的同学肯定都很熟悉。redux 通过compose来处理 中间件 。原理是 借助数组的 reduce 对数组的参数进行迭代
代码语言:javascript复制// redux 中的 compose 函数
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
洋葱模型实现
再看文章开头 koa2 创建 http 服务函数,会发现多次调用 use 函数,其实这就是洋葱模型的应用。
洋葱是由很多层组成的,你可以把每个中间件看作洋葱里的一层,根据app.use的调用顺序中间件由外层到里层组成了整个洋葱,整个中间件执行过程相当于由外到内再到外地穿透整个洋葱
引用一张著名的洋葱模型图:
每次执行 use 函数,我们实际是往一个函数数组
中添加了一个函数,然后再次通过一个 compose 函数,处理添加进来函数的执行顺序,也就是这个 compose 函数实现了洋葱模型机制。
具体代码实现如下:
代码语言:javascript复制// 其中包含一个递归
compose(middlewares){
return async 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)
})
)
}
}
}
首先执行一次 dispatch(0) 也就是默认返回第一个 app.use 传入的函数 使用 Promise 函数封装返回,其中第一个参数是我们常用的 ctx,
第二个参数就是 next 参数,next 每次执行之后都会等于下一个中间件函数,如果下一个中间件函数不为真则返回一个成功的 Promise。因此我们每次调用 next() 就是在执行下一个中间件函数。
来试试我们自己实现的koa2
使用一下我们自己的 koa2 吧,用它做一道常考洋葱模型面试题,我想文章如果懂了,输出结果应该不会错了,自己试一下!
代码语言:javascript复制const KKB = require('./kkb');
const app = new KKB();
app.use(async (ctx,next)=>{
ctx.body = '1';
await next();
ctx.body = '3';
})
app.use(async (ctx,next)=>{
ctx.body = '4';
await delay();
await next();
ctx.body = '5';
})
app.use(async (ctx,next)=>{
ctx.body = '6'
})
async function delay(){
return new Promise((reslove,reject)=>{
setTimeout(()=>{
reslove();
},1000);
})
}
app.listen(3000);
解题思路:还是洋葱思想,洋葱是先从皮到心,然后从心到皮
答案: 1 4 6 5 3
补充与说明
本文目的主要是让大家学到一个koa2的基本流程,简单实现koa2,再去读源码有一个清晰的思路。实际源码中还有很多优秀的值得我们学习的点,接下来再列举一个我觉得它很优秀的点——错误处理,大家可在原有基础上继续实现,也可以去读源码继续看!加油加油
源码中 koa 继承自 Emiiter,为了处理可能在任意时间抛出的异常所以订阅了 error 事件。error 处理有两个层面,一个是 app 层面全局的(主要负责 log),另一个是一次响应过程中的 error 处理(主要决定响应的结果),koa 有一个默认 app-level 的 onerror 事件,用来输出错误日志。
代码语言:javascript复制 // 在调用洋葱模型函数后面,koa 会挂载一个默认的错误处理【运行时确定异常处理】
if (!this.listenerCount("error")) this.on("error", this.onerror);
代码语言:javascript复制 onerror(err) {
if (!(err instanceof Error))
throw new TypeError(util.format("non-error thrown: %j", err));
if (404 == err.status || err.expose) return;
if (this.silent) return;
const msg = err.stack || err.toString();
console.error();
console.error(msg.replace(/^/gm, " "));
console.error();
}
通过 Emiiter 实现了错误打印,Emiiter 采用了发布订阅的设计模式,如果有对 Emiiter 有不太清楚的小伙伴可以看我这篇文章 [源码解读]一文彻底搞懂Events模块。
总结
本文注重思想,精简版本,代码与实现都很简单。封装,递归,设计模式都说了一丢丢,希望也能对你有一丢丢的提升和让你去看一下koa2源码的想法,下篇文章见。