koa实现

270 阅读5分钟

功能概览

  • application: 创建上下文,合并中间件,启动服务
  • request: 对原生http模块req的扩展
  • response: 对原生http模块res的扩展
  • context:对request和response的合并与代理

服务启动:listen

使用


const Koa=require('./application')
const app=new Koa()
app.listen(3000,()=>{
    console.log('run server__')
})

实现

//内部直接使用了http模块
const http=require('http')
class Application{
    listen(...args){
       const server= http.createServer()
       server.listen(...args)
    }
}

请求处理:handleRequest

使用


const Koa=require('./application')
const app=new Koa()

app.use((ctx)=>{
    //下边这四种用法其实是一样的
    //下文会接介绍为什么
    console.log(ctx.url)
    console.log(ctx.request.url)
    console.log(ctx.req.url)
    console.log(ctx.request.req.url)
    
})

app.listen(3000,()=>{
    console.log('run server__')
})

实现



class Application{

    //函数注册(中间件也是函数)
    use(fn){
        this.fn=fn
    }

    //请求处理
    handleRequest(req,res){
        //创建上下文:合并req,res为ctx
        //这个方法实现下文会提到
        const ctx=this.createContext(req,res)
        //这里的ctx就是外部使用use时候的第一个参数
        // app.use(ctx=>{})
        this.fn(ctx)
    }


    //...启动服务
     listen(...args){
        //这里的bind是保证this指向自定义的Application
       const server= http.createServer(this.handleRequest.bind(this))
       server.listen(...args)
    }
    
}

创建上下文:createContext

  1. 想一下,为什么koa将req,res合并为了一个ctx? 盲猜是为了使用方便,记住两个不如记住一个省心。
  2. 那req,res与ctx又有什么区别?req,res有的,ctx皆有,req,res没有的,ctx还有
  3. 你问我什么是西厂?一句话:东厂管得了的我要管,东厂管不了的我更要管!

ctx

实现



/**
 * 同级目录下分别新建application.js request.js  response.js context.js 
 * 暂时导出一个空对象即可
*/


//request.js

const request={}
module.exports=request

//response.js

const response={}
module.exports=response

//context.js

const context={}
module.exports=context



//application.js
class Application{


    constructor(){
        //这三个是外部引入的
        this.context=context;
        this.request=request;
        this.response=response;
    }

 // createContext最终会返回一个合并后的context;
    createContext(req,res){

    //使用Object.create是为了在不对原模块进行干扰的情况下进行扩展,也是一层继承
 
    const context=Object.create(this.context)
    const request=Object.create(this.request)
    const response=Object.create(this.response)

    //上下文关联与合并
    // context.req=req;
    // context.request.req=req
    // context.res=res;
    // context.response.res=res

    //上述代码可简写为如下形式
     context.req=context.request.req=req    
     context.res=context.response.res=res

    // 看到这里应该明白一件事:
    //ctx.req.url和ctx.request.req.url输出一致是因为ctx.req和ctx.request.req指向本就一致
    //同理:ctx.res和ctx.response.res指向也一致
	//与此同时也应该思考另一件事:ctx.url和ctx.request.url又是怎么回事?

    //返回context
    return context


    }
}

请求增强:request

request是koa对原生http模块req的增强,这也是源码里没直接像context那样使用代理而使用getter,setter的原因。

上边我们已经理解了为什么ctx.req.urlctx.request.req.url输出一致,接下来再看看ctx.urlctx.request.url


const request={

    get url() {

        //这里的this指向request
        //而createContext方法中的request身上恰好挂了一个req
        //所以ctx.request.req 本质和ctx.req是同一指向
        // 这也解释了ctx.request.url输出和ctx.req.url,ctx.request.req.url,输出一致
        return this.req.url;
    },

}

module.exports=request

context:代理

ok,现在就差ctx.url这个小东西了,其实它本质是ctx.request.url

为什么这么说呢?来看看context内部实现吧

const context = {}

function delegateGet(prop, key) {
    //__defineGetter__这个方法是当访问对象的某个key时,执行回调
    context.__defineGetter__(key, function () {
        return this[prop][key]
    })
}

//这就相当于ctx.url=>ctx.request.url
delegateGet('request', 'url')

module.exports = context

关于__defineGetter__,可参考MDN

developer.mozilla.org/zh-CN/docs/…__

如果你去看koa源码,你会发现它使用了一个第三方包:delegates,其实这东西实现也是用的__defineGetter__

响应增强:response

使用


const Koa=require('./application')
const app=new Koa()

app.use((ctx)=>{
   ctx.body='hello lengyuexin';
   console.log(ctx.body)
})

app.listen(3000,()=>{
    console.log('run server__')
})


实现


//ctx本质是代理,ctx本质是代理,ctx本质是代理
//所以ctx做的响应相关的,一定是交给response
//同理ctx做的请求相关的,一定是交给request
//故:ctx.body=>ctx.response.body


const context = {}


function delegateGet(prop, key) {
    context.__defineGetter__(key, function () {
        return this[prop][key]
    })
}

function delegateSet(prop, key) {
    context.__defineSetter__(key, function (newValue) {
        this[prop][key] = newValue
    })
}

delegateGet('response', 'body')//访问ctx.body
delegateSet('response', 'body')//设置ctx.body='xxx'


module.exports = context

response 同request 一样, 也是getter setter 形式


const response = {

    _body: '',

    get body() {
        return this._body
    },

    set body(newBody) {
        this._body = newBody
    }

}

module.exports = response

合并中间件:compose

  • 在koa中,use函数可以多次调用,而且默认情况下只会执行第一个。
  • 后边的如果想依次执行,需要调用next函数,也就是use函数的第二个参数。
  • 此外,如果涉及异步操作,可用async,await。

洋葱模型图

onion compose


使用


// 依次打印123
const app = new (require('koa'))

app.use((ctx, next) => {
    console.log(1)
    next()
})
app.use((ctx, next) => {
    console.log(2)
    next()
})
app.use((ctx, next) => {
    console.log(3)
})

//错误监听

app.on('error',(err)=>{
    console.error(err)
})

app.listen(3000)



实现


//为了方便异步处理和错误捕获
// compose方法返回的是一个promise
//中间件也需要用一个数组存储起来


const EventEmitter = require('events')
const Stream = require('stream')
class Application extends EventEmitter {


//application 的constructor中新增middlewares,初始化为空数组
  constructor() {
        super()
        this.context = context
        this.request = request
        this.response = response
        this.middlewares = []//多个use调用,存起来
    }

//use方法就是直接push函数到middlewares

    use(fn){
        this.middlewares.push(fn)
    }



    
     handleRequest(req, res) {

        const ctx = this.createContext(req, res)

      //组合中间件 并执行返回后的promise
      // 获取到_body 响应出去

        this.compose(ctx).then(() => {
             //默认只能处理buffer 和string
            let _body = ctx.body;

            if (_body === '') {
                //如果没设置body 就给个默认值
                //状态码设置为404
                res.statusCode = 404
                _body = 'not found'
                return res.end(_body)
            } else if (_body instanceof Stream) {
                //koa也支持直接返回一个文件流,通过pipe就可以做到 
                //对流的处理
                return _body.pipe(res)
            } else if (typeof _body !== 'null' && typeof _body === 'object') {
                //对对象的处理
                return res.end(JSON.stringify(_body))
            } else if (_body == null) {
                //null 和undefined 直接字符串形式输出
                //无法直接调用toString 可以拼接一下
                return res.end(_body + '')
            } else {
                //其他类型的直接toString 
                return res.end(_body.toString())
            }
        }).catch(err => {
            //这里为app添加了错误监听
            //application继承events模块即可
            this.emit('error', err)
         })
    }


    //compose来了
    //核心逻辑三件事:1. 越界处理 2. 执行第一个中间件 3. 依次执行后边的中间件

   compose(ctx) {

       //这里dispatch(也就是next)使用箭头函数
       //内部的this就指向了自定义的Application
        const dispatch = (index) => {
            //越界处理 handleRequest还有then 不能直接return ,要返回promise

            if (index === this.middlewares.length) return Promise.resolve()


            //获取当前的中间件 最开始是第一个
            const middleware = this.middlewares[index]

            // 中间件执行需要两个参数
            const exec = middleware(ctx, () => dispatch(++index))
            //有可能这个方法没有加async,包装一层
            //保证返回的是一个promise
            //这样handleRequest的then函数就不会报错

            return Promise.resolve(exec)

        }

        return dispatch(0)

    }

}

文末碎碎念

面试被问到让说一下你所理解的koa。

我第一反应是说下洋葱模型和手写compose。

冷静下来发现,嗐,忘了怎么写了...


情如风雪无常,

却是一动既殇。

感谢你这么好看还来阅读我的文章,

我是冷月心,下期再见。