手摸手带你从0到1实现Koa简易版本

640 阅读5分钟

前言

大家好,我是作曲家种太阳,我相信最好的学习就是输出
这次我会带大家一步步从零到1封装一个Koa框架,让你掌握koa其内部运行原理,更好的使用koa框架.\

在开始之前,你得的对node中http模块和Events模块有所使用与了解

1.介绍koa

(1)Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。---Koa官网官方介绍

Koa官网 (2).洋葱模型

image.png

2.介绍Http中res,req 与Koa中context的关系

Koa是基于Http模块封装的,在Http中res,req的对象基础上,
封装了request,response两个对象,并合并成了context对象.
画一张图你就明白了

image.png

3.了解Api与Koa目录结构

(1) 读到这里,我希望你是对Koa的基本使用与api是有过了解
或者做一些demo的.
我们下面看下Koa的api基本使用.利用api反推下Koa的内部结构组成

const Koa = require('koa');  
// 通过new的方式创建一个应用
const app  = new Koa();  
// 使用中间件
app.use((ctx)=>{
    ctx.body =  {a:1}
    console.log(ctx.body)
})
// 开启服务
app.listen(3000,function(){
    console.log(`server start 3000`);
}) ; // server.listen
// 监听错误
app.on('error',function (err) {
    console.log('err',err)
})

ps:看到这里你会发现

  1. 可以new获取一个Koa实例对象
  2. Koa内部有use,listen方法
  3. app.on是一个事件监听机制,所以Koa使用了event事件机制模块

(2) Koa源码的目录结构:

image.png 我们根据源码源码的目录结构,创建lib下所有文件,和package.json文件

4.开始编写Koa(application.js部分)

上一步我们晓得了koa的api与目录结构,下面我们开始编写Koa
在lib/application.js中

const EventEmitter = require('events')
const http = require('http')
const context = require("./context")
const request = require('./request')
const response = require('./response')

// 实现一个koa
class Koa extends EventEmitter {
    constructor() {
        super();
        // 创建一个新的上下文,保证应用之间不共享上下文,不会造成混乱
        this.context = Object.create(context) // this.context
        // 请求
        this.request = Object.create(request)
        // 响应
        this.response = Object.create(response)
        // 中间件
        this.middlewares = []
    }

    // 存取函数
    use(middleware) {
        this.middlewares.push(middleware)
    }

    //  执行中间件,并做一些处理
    compose(ctx) {
        // 我需要将middlewares 中的 所有的方法拿出来,先调用第一个,第一个调用完毕后,会调用next ,再去执行第二个
        let index = -1;
        const dispatch = (i) => {
            // 传递的小于index下标,防止同一个中间件内多次调用 next()
            if (i <= index) return Promise.reject('next() called multiple times;next() 不能在一个中间件中多次调用');
            // 记录index下标
            index = i;
            // 到末尾的next()返回空的promise
            if (this.middlewares.length === i) return Promise.resolve();
            return Promise.resolve(this.middlewares[i](ctx, () => dispatch(i + 1)))
        }
        return dispatch(0);
    }

    // 创建上下文
    createContext(req, res) {
        // 每次请求我都穿件一个全新的上下文
        let ctx = Object.create(this.context) // this.context
        // 请求
        let request = Object.create(this.request)
        // 响应
        let response = Object.create(this.response)
        // 合并属性,画图演示过~
        ctx.request = request
        ctx.response = response
        ctx.req = ctx.request.req = req
        ctx.res = ctx.response.res = res
        return ctx
    }

    // 处理请求
    handleRequest = (req, res) => {
        // 创建合并上下文
        const ctx = this.createContext(req, res)
        // 设置请求头
        res.statusCode = 404
        // 执行完毕中间件,再继续处理请求
        this.compose(ctx).then(() => {
            if (typeof ctx.body === "object") {
                // 设置响应头
                res.setHeader("Content-Type", "application/json;charset=utf-8")
                // Json转义
                res.end(JSON.stringify(ctx.body))
            } else if (ctx.body) {
                // 发送请求体
                res.end(ctx.body)
            } else {
                res.end("Not Found")
            }
        }).catch(err => {
            this.emit('error', err)
        })
    }

    //
    listen(...args) {
        // 开启服务
        const server = http.createServer(this.handleRequest)
        server.listen(...args)
    }
}

module.exports = Koa

PS:主干逻辑其实并不是很难,你需要注意的几点是:

  1. 需要好好琢磨的是中间件是怎么存储(use函数)的和执行(compose函数)的,里面有详细的注释
  2. 中间件中next()的裂解 (compose函数中dispatch方法)
  3. createContext函数constructor函数分别都使用Object.create方法创建了新的context,request,response对象.目的就是为了每个实例对象和每次请求都有不一样的上下文.
  4. promise是为了保证中间件调用顺序, .then()方法时为了保证先执行中间件,再继续处理请求

5.context.js编写

Koa是用的是__defineGetter__和__defineSetter__进行属性的代理的
在lib/context.js中编写

const context = {}
// 定义属性封装
function defineGetter(target, key) {
    // 定义 属性取值
    context.__defineGetter__(key, function () {
        return this[target][key]
    })
}
//
function defineSetter(target, key) {
    // 定义 属性取值
    context.__defineSetter__(key, function (value) {
        this[target][key] = value;
    })
}
// 定义属性取值代理
defineGetter("request", "query")
defineGetter("request", "path")
defineGetter("response", "body")
//设置值
defineSetter("response", "body")
module.exports = context

ps: 这里主要是做了属性的代理,取值和设置值的时候都代理到指定的对象当中去(把对象深层的属性拍平)
建议和Koa中createContext函数结合this指向一起读,就明白了

6.request.js编写

lib/request.js中

const url = require('url')
const request = {
    // 属性访问器
    get url() {
        return this.req.url
    },

    get path() {
        // url.parse解析成url的对象
        return url.parse(this.req.url).pathname
    },
    get query() {
        return url.parse(this.req.url).query
    }
}
module.exports = request

ps:都是是属性代理

7.response.js编写

lib/response.js中

const response = {
    _body: undefined,
    get body() {
        return this._body
    },
    set body(value) {
        // 用户调用ctx.body 的时候 会更改状态码
        this.res.statusCode = 200;
        this._body = value;
    }
}
module.exports = response

ps:每次给ctx.body设置值的时候,只是在response._body设置了值而已

8.测试编写结果

把第三步骤的调用koa的api代码跑起来,看下测试是通过(别忘了引入自己编写的koa文件!) 在浏览器中访问: http://localhost:3001/

image.png

到这一步,我就一步一步的带你封装好了Koa,麻雀虽小五脏俱全,有兴趣的同学可以理解完代码,直接看看Koa的源码

本文如果对你有帮助,欢迎Start,评论,收藏三连击