21

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

 4 years ago
source link: https://juejin.im/post/5decf130f265da339565d40e
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
2019年12月17日阅读 2653

读 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…

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

  1. 传统的 http 服务想使用模块化不是很方便,我们不能在一个服务里面判断所有的请求和一些内容。而 koa2 对模块化提供了更好的帮助
  2. koa2 把 req,res 封装到了 context 中,更简洁而且方便记忆
  3. 中间件机制,采用洋葱模型,洋葱模型流程记住一点(洋葱是先从皮到心,然后从心到皮),通过洋葱模型把代码流程化,让流水线更加清楚,如果不使用中间件,在 createServer 一条线判断所有逻辑确实不好。
  4. 看不到的优点也很多,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 一次返回就更好了!我们继续

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

看一下 createContext 的具体实现

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的调用顺序中间件由外层到里层组成了整个洋葱,整个中间件执行过程相当于由外到内再到外地穿透整个洋葱

引用一张著名的洋葱模型图:

1

每次执行 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】,拉你进技术群,长期交流学习...
  • 欢迎关注「程序员成长指北」,一个用心帮助你成长的公众号...
    1

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK