zoukankan      html  css  js  c++  java
  • 参考KOA,5步手写一款粗糙的web框架

    我经常在网上看到类似于KOA VS express的文章,大家都在讨论哪一个好,哪一个更好。作为小白,我真心看不出他两who更胜一筹。我只知道,我只会跟着官方文档的start做一个DEMO,然后我就会宣称我会用KOA或者express框架了。但是几个礼拜后,我就全忘了。web框架就相当于一个工具,要使用起来,那是分分钟的事。毕竟人家写这个框架就是为了方便大家上手使用。但是这种生硬的照搬模式,不适合我这种理解能力极差的使用者。因此我决定扒一扒源码,通过官方API,自己写一个web框架,其实就相当于“抄”一遍源码,加上自己的理解,从而加深影响。不仅需要知其然,还要需要知其所以然。

    我这里选择KOA作为参考范本,只有一个原因!他非常的精简!核心只有4个js文件!基本上就是对createServer的一个封装。

    在开始解刨KOA之前,createServer的用法还是需要回顾下的:

    const http = require('http');
    let app=http.createServer((req, res) => {
        //此处省略其他操作
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.body="我是createServer";
        res.end('okay');
    });
    app.listen(3000)
    

    回顾了createServer,接下来就是解刨KOA的那4个文件了:

    • application.js
      • 这个js主要就是对createServer的封装,其中一个最主要的目的就是将他的callback分离出来,让我们可以通过app.use(callback);来调用,其中callback大概就是令大家闻风丧胆的中间件(middleWare)了。
    • request.js
      • 封装createServer中返回的req,主要用于读写属性。
    • response.js
      • 封装createServer中返回的res,主要用于读写属性。
    • context.js
      • 这个文件就很重要了,它主要是封装了request和response,用于框架和中间件的沟通。所以他叫上下文,也是有道理的。

    好了~开始写框架咯~

    仅分析大概思路,分析KOA的原理,所以并不是100%重现KOA。

    本文github地址:点我

    step1 封装http.createServer

    先写一个初始版的application,让程序先跑起来。这里我们仅仅实现:

    • 封装http.createServer到myhttp的类
    • 将回调独立出来
    • listen方法可以直接用

    step1/application.js

    let http=require("http")
    class myhttp{
        handleRequest(req,res){
            console.log(req,res)
        }
        listen(...args){
            // 起一个服务
            let server = http.createServer(this.handleRequest.bind(this));
            server.listen(...args)
        }
    }
    

    这边的listen完全和server.listen的用法一摸一样,就是传递了下参数

    友情链接

    server.listen的API

    ES6解构赋值...

    step1/testhttp.js

    let myhttp=require("./application")
    let app= new myhttp()
    app.listen(3000)
    

    运行testhttp.js,结果打印出了reqres就成功了~

    step2 封装原生req和res

    这里我们需要做的封装,所需只有两步:

    • 读取(get)req和res的内容
    • 修改(set)res的内容

    step2/request.js

    let request={
        get url(){
            return this.req.url
        }
    }
    module.exports=request
    

    step2/response.js

    let response={
        get body(){
            return this.res.body
        },
        set body(value){
            this.res.body=value
        }
    }
    module.exports=response
    

    如果po上代码,就是这么简单,需要的属性可以自己加上去。那么问题来这个this指向哪里??代码是很简单,但是这个指向,并不简单。

    回到我们的application.js,让这个this指向我们的myhttp的实例。

    step2/application.js

    class myhttp{
        constructor(){
            this.request=Object.create(request)
            this.response=Object.create(response)
        }
        handleRequest(req,res){
            let request=Object.create(this.request)
            let response=Object.create(this.response)
            request.req=req
            request.request=request
            response.req=req
            response.response=response
            console.log(request.headers.host,request.req.headers.host,req.headers.host)
        }
        ...
    }
    

    此处,我们用Object.create拷贝了一个副本,然后把request和response分别挂上,我们可以通过最后的一个测试看到,我们可以直接通过request.headers.host访问我们需要的信息,而可以不用通过request.req.headers.host这么长的一个指令。这为我们下一步,将requestresponse挂到context打了基础。

    step3 context闪亮登场

    context的功能,我对他没有其他要求,就可以直接context.headers.host,而不用context.request.headers.host,但是我不可能每次新增需要的属性,都去写一个get/set吧?于是Object.defineProperty这个神操作来了。

    step3/content.js

    let context = {
    }
    //可读可写
    function access(target,property){
       Object.defineProperty(context,property,{
            get(){
                return this[target][property]
            },
            set(value){
                this[target][property]=value
            }
       })
    }
    //只可读
    function getter(target,property){
       Object.defineProperty(context,property,{
            get(){
                return this[target][property]
            }
       })
    }
    getter('request','headers')
    access('response','body')
    ...
    

    这样我们就可以方便地进行定义数据了,不过需要注意地是,Object.defineProperty地对象只能定义一次,不能多次定义,会报错滴。

    step3/application.js
    接下来就是连接contextrequestresponse了,新建一个createContext,将responserequest颠来倒去地挂到context就可了。

    class myhttp{
        constructor(){
            this.context=Object.create(context)
            ...
        }
        createContext(req,res){
            let ctx=Object.create(this.context)
            let request=Object.create(this.request)
            let response=Object.create(this.response)
            ctx.request=request
            ctx.response=response
            ctx.request.req=ctx.req=req
            ctx.response.res=ctx.res=res
            return ctx
        }
        handleRequest(req,res){
            let ctx=this.createContext(req,res)
            console.log(ctx.headers)
            ctx.body="text"
            console.log(ctx.body,res.body)
            res.end(ctx.body);
        }
        ...
    }
    

    以上3步终于把准备工作做好了,接下来进入正题。
    友情链接:

    step4 实现use

    这里我需要完成两个功能点:

    • use可以多次调用,中间件middleWare按顺序执行。
    • use中传入ctx上下文,供中间件middleWare调用

    想要多个中间件执行,那么就建一个数组,将所有地方法都保存在里头,然后等到执行的地时候forEach一下,逐个执行。传入的ctx就在执行的时候传入即可。

    step4/application.js

    class myhttp{
        constructor(){
            this.middleWares=[]
            ...
        }
        use(callback){
            this.middleWares.push(callback)
            return this;
        }
        ...
        handleRequest(req,res){
            ...
            this.middleWares.forEach(m=>{
                m(ctx)
            })
            ...
        }
        ...
    }
    

    此处在use中加了一个小功能,就是让use可以实现链式调用,直接返回this即可,因为this就指代了myhttp的实例app

    step4/testhttp.js

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

    step5 实现中间件的异步执行

    任何程序只要加上了异步之后,感觉难度就蹭蹭蹭往上涨。

    这里要分两点来处理:

    • use中中间件的异步执行
    • 中间件的异步完成后compose的异步执行。

    首先是use中的异步
    如果我需要中间件是异步的,那么我们可以利用async/await这么写,返回一个promise

    app.use(async (ctx,next)=>{
        await next()//等待下方完成后再继续执行
        ctx.body="aaa"
    })
    

    如果是promise,那么我就不能按照普通的程序foreach执行了,我们需要一个完成之后在执行另一个,那么这边我们就需要将这些函数组合放入另一个方法compose中进行处理,然后返回一个promise,最后来一个then,告诉程序我执行完了。

    handleRequest(req,res){
        ....
        this.compose(ctx,this.middleWares).then(()=>{
            res.end(ctx.body)
        }).catch(err=>{
            console.log(err)
        })
        
    }
    

    那么compose怎么写呢?

    首先这个middlewares需要一个执行完之后再进行下一个的执行,也就是回调。其次compose需要返回一个promise,为了告诉最后我执行完毕了。

    第一版本compose,简易的回调,像这样。不过这个和foreach并无差别。这里的fn就是我们的中间件,()=>dispatch(index+1)就是next

    compose(ctx,middlewares){
        function dispatch(index){
            console.log(index)
            if(index===middlewares.length) return;
            let fn=middlewares[index]
            fn(ctx,()=>dispatch(index+1));
        }
        dispatch(0)
    }
    

    第二版本compose,我们加上async/await,并返回promise,像这样。不过这个和foreach并无差别。dispatch一定要返回一个promise。

    compose(ctx,middlewares){
        async function dispatch(index){
            console.log(index)
            if(index===middlewares.length) return;
            let fn=middlewares[index]
            return await fn(ctx,()=>dispatch(index+1));
        }
        return dispatch(0)
    }
    

    return await fn(ctx,()=>dispatch(index+1));注意此处,这就是为什么我们需要在next前面加上await才能生效?作为promise的fn已经执行完毕了,如果不等待后方的promise,那么就直接then了,后方的next就自生自灭了。所以如果是异步的,我们就需要在中间件上加上async/await以保证next执行完之后再返回上一个promise。无法理解?了?我们看几个例子。

    具体操作如下:

    function makeAPromise(ctx){
        return new Promise((rs,rj)=>{
            setTimeout(()=>{
                ctx.body="bbb"
                rs()
            },1000)
        })
    }
    //如果下方有需要执行的异步操作
    app.use(async (ctx,next)=>{
        await next()//等待下方完成后再继续执行
        ctx.body="aaa"
    })
    app.use(async (ctx,next)=>{
        await makeAPromise(ctx).then(()=>{next()})
    })
    

    上述代码先执行ctx.body="bbb"再执行ctx.body="aaa",因此打印出来是aaa。如果我们反一反:

    app.use(async (ctx,next)=>{
        ctx.body="aaa"
        await next()//等待下方代码完成
    })
    

    那么上述代码就先执行ctx.body="aaa"再执行ctx.body="bb",因此打印出来是bbb
    这个时候我们会想,既然我这个中间件不是异步的,那么是不是就可以不用加上async/await了呢?实践出真理:

    app.use((ctx,next)=>{
        ctx.body="aaa"
        next()//不等了
    })
    

    那么程序就不会等后面的异步结束就先结束了。因此如果有异步的需求,尤其是需要靠异步执行再进行下一步的的操作,就算本中间件没有异步需求,也要加上async/await。

    终于写完了,感觉脑细胞死了不少,接下来我去研究router和ejs,等这一块加入我的web框架之后,就很完美了~

  • 相关阅读:
    《学习之道》第十四章迁移
    《学习之道》第十四章把自己想象成那些概念
    《学习之道》第十四章阐述自己对概念的理解
    《学习之道》第十四章自然科学不好理解
    《学习之道》第十三章为何要总结
    《学习之道》第十三章什么是深层组块
    《学习之道》第十三章自己也要总结
    《学习之道》第十三章继续总结
    《学习之道》第十三章形成深层组块
    《学习之道》第十三章练习大脑,改变思维
  • 原文地址:https://www.cnblogs.com/cherryvenus/p/9527211.html
Copyright © 2011-2022 走看看