读 Koa2 源码后的一些思考与实现(面试必备)

koa2的特色优点

什么是 koa2

  1. Nodejs官方api支持的都是callback形式的异步编程模型。问题:callback嵌套问题
  2. koa2 是由 Express原班人马打造的,是如今比较流行的基于Node.js平台的web开发框架,Koa 把 Express 中内置的 router、view 等功能都移除了,使得框架自己更轻量,并且扩展性很强。使用koa编写web应用,能够免除重复繁琐的回调函数。

做者简介:koala,专一完整的 Node.js 技术栈分享,从 JavaScript 到 Node.js,再到后端数据库,祝您成为优秀的高级 Node.js 工程师。【程序员成长指北】做者,Github 博客开源项目 github.com/koala-codin…javascript

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 的实现 经过以上两段代码,会发现下面三个优势java

  1. 传统的 http 服务想使用模块化不是很方便,咱们不能在一个服务里面判断全部的请求和一些内容。而 koa2 对模块化提供了更好的帮助
  2. koa2 把 req,res 封装到了 context 中,更简洁并且方便记忆
  3. 中间件机制,采用洋葱模型,洋葱模型流程记住一点(洋葱是先从皮到心,而后从心到皮),经过洋葱模型把代码流程化,让流水线更加清楚,若是不使用中间件,在 createServer 一条线判断全部逻辑确实很差。
  4. 看不到的优势也不少,error 错误处理,res的封装处理等。

本身实现一个koa2

在实现的过程当中会我看看能够学到那些知识git

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 一次返回就更好了!咱们继续github

const ctx = this.createContext(req,res);
复制代码

看一下 createContext 的具体实现web

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面试

module.exports = {
    get url(){
        return this.request.url;
    },
    get body(){
        return this.response.body;
    },
    set body(val){
        this.response.body = val;
    }
}
复制代码

request.js数据库

module.exports = {
    get url(){
        return this.req.url;
    }
}
复制代码

这里在写 context.js 时候,用到了set 与 get 函数,get 语句做为函数绑定在对象的属性上,当访问该属性时调用该函数。set 语法能够将一个函数绑定在当前对象的指定属性上,当那个属性被赋值时,你所绑定的函数就会被调用。编程

实现洋葱模型

compose 另外一个应用场景

说洋葱模型以前先看一个函数式编程内容: compose 函数前端用过 redux 的同窗确定都很熟悉。redux 经过compose来处理 中间件 。 原理是 借助数组的 reduce 对数组的参数进行迭代

// 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 函数实现了洋葱模型机制。

具体代码实现以下:

// 其中包含一个递归
 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 吧,用它作一道常考洋葱模型面试题,我想文章若是懂了,输出结果应该不会错了,本身试一下!

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 事件,用来输出错误日志。

// 在调用洋葱模型函数后面,koa 会挂载一个默认的错误处理【运行时肯定异常处理】
    if (!this.listenerCount("error")) this.on("error", this.onerror);
复制代码
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 源码的想法,下篇文章见。

Node系列原创文章

深刻理解Node.js 中的进程与线程

想学Node.js,stream先有必要搞清楚

require时,exports和module.exports的区别你真的懂吗

源码解读一文完全搞懂Events模块

Node.js 高级进阶之 fs 文件模块学习

关注我

  • 欢迎加我微信【coder_qi】,拉你进技术群,长期交流学习...
  • 欢迎关注「程序员成长指北」,一个用心帮助你成长的公众号...
相关文章
相关标签/搜索