Koa 原来如此简单(一)

koa 框架

koa 是Node.js平台下一代Web 开发框架,是 Express原班人马打造。

架设 http 服务

    let Koa = require('koa');
    let app = new Koa();
    app.listen(3000);
复制代码

以上是一个最简单的 http 服务,访问http://localhost:3000页面返回的是 Not Found,是因为我们告诉 koa 应该显示什么内容。ctx.status 默认404,下面会讲解到 app.listen(3000) 只是下面方法的语法糖

    let http = require('http');
    http.createServer().listen(3000);
复制代码

Context

Context 将 Node 的 request 和 response 对象封装在单个对象中,每个请求都将创建一个 context,并在中间件中作为接收器使用,或者可以使用 ctx 标识符。

    let Koa = require('koa');
    let app = new Koa();
    app.use(ctx=>{
        ctx.body = 'hello world!';
    });
    app.listen(3000);
复制代码

上面代码中,使用 use 方法加载一个函数,使用 ctx.body发送给用户内容,ctx.body等同于ctx.response.body

meddleware 中间件

中间件就是就是处于http request 和 http response 的中间,用来实现某种中间的功能,比如,请求参数解析等 每个中间件都有两个参数ctx对象,next函数,只要调用 next 函数,就可以把执行权交给下一个中间件。

    let Koa = require('koa');
    let app = new Koa();
    app.use((ctx, next)=>{
        console.log('1');
        next();
        console.log('2');
    })
    app.use((ctx, next)=>{
        console.log('3');
        next();
        console.log('4');
    })
    app.use((ctx, next)=>{
        console.log(5);
    })
    app.listen(4000);

    //输出结果
    1
    3
    5
    4
    2
复制代码

next 函数执行后, 相当于把将上面代码改成下面代码:

    let Koa = require('koa');
    let app = new Koa();
    app.use((ctx,next)=>{
        console.log('1');
        //next() 当调用next函数的时候,其实相当于把下一个 use 函数嵌套进来
        (ctx, next)=>{
            console.log('3');
            //next(); 当调用next函数的时候,其实相当于把下一个 use 函数嵌套进来
            (ctx, next)=>{
                console.log(5);
            }
            console.log('4');
        }
        console.log('2');
    })
    app.listen(4000);
复制代码

从上面修改后的代码可以看出,next函数执行后,代码形成嵌套的形式执行,形成洋葱模型,所以最终输出 1 3 5 4 2

use 函数

官网称:将给定的中间件方法添加到此应用程序,白话就是,use 方法是添加中间件的,那么,use 到底是这么添加中间件方法的呢,看下面代码:

    //上面已经讲过 use 的使用案例
    let Koa = require('koa');
    let app = new Koa();
    app.use((ctx, next)=>{
        console.log(1);
        next();
    })
    app.use((ctx, next)=>{
        console.log(2);
    })
    app.listen(4000);
复制代码

从上面我们可以看出,use 接受一个函数,这个函数中接受两个参数 ctx对象 和 next函数,use方法可以给应用程序绑定多个中间件,如果中间件中执行 next 方法,会将执行权交给下一个中间件函数执行,当所有的 next 函数执行完成后,再讲执行权交给上游去继续执行,下面我们来实现 use函数 原理

    function app() {};
    app.middleware = [];
    app.use = (fn)=>{
        app.middleware.push(fn); //将所有 use 函数中的方法存放在一起
    }
    //下面两个 use 方法会将 use 中的两个函数全部添加到 middleware数组中
    app.use((ctx, next)=>{
        console.log('1');
        next();
    })
    app.use((ctx, next)=>{
        console.log('2');
    })
    
    let index = 0;
    function dispatch(index){
        if(index >= app.middleware.length) return;
        app.middleware[index]({},()=>{ dispatch(index+1) })
    }
    dispatch(index); //开始执行 middleware中的每个中间件,从第一个开始

复制代码

异步中间件

上面的代码虽然表面上没有什么问题,是因为 next 方法是同步执行的,但是如果遇到下面这种 下一个 use 方法中带有异步的情况,程序会输出什么?

    let Koa = require('koa');
    let app = new Koa();
    app.use((ctx, next)=>{
        console.log('1');
        setTimeout(()=>{
            console.log('2');
        },2000)
        next();
        console.log('3');
    })
    app.use((ctx, next)=>{
        console.log('4');
    })
    app.listen(4000);

    //按照我们之前案例,应该输出 1 2 4 3,但是。。。。。
    //结果输出:
    1
    4
    3
    2  //2是等待了两秒
复制代码

为什么跟我们预想的不一样,是因为我们注意到,在下一个中间件中存在异步方法,next 函数并不等待异步函数执行完才执行,而是绕过异步函数,直接执行 next 方法。 但是我们肯定是想让 next 方法等待异步方法执行完后再执行,所以自然而然应该想到用 promise去包装一下异步函数, 并使用 async+await 去执行

    //将上面的 setTimeout封装成 promise
    function setT(){
        return new Promise((resolve, reject)=>{
            setTimeout(()=>{
                console.log('2');
                resolve();
            })
        })
    }

    let Koa = require('koa');
    let app = new Koa();
    app.use(async (ctx, next)=>{
        console.log('1');
        await setT();
        next();
        console.log('3');
    })
    app.use((ctx, next)=>{
        console.log('4');
    })
    app.listen(4000);

    //运行结果,输出:
    1
    2
    4
    5
    //跟我们之前预想的是一样的
复制代码

中间件用法案例

提交表单

现在我们模拟真实场景,中间件处理提交表单 中间件会拦截每一次请求

    let Koa = require('koa');
    let app = new Koa();
    app.use((ctx, next)=>{
        if(ctx.path == '/' && ctx.method == 'GET'){
            ctx.set('Content-Type', 'text/html;charset=utf-8');
            ctx.body = `
                <form action="/" method="post">
                    <input type="text" name="username" />
                    <input type="text" name="password" />
                    <input type="submit"/>
                </form>
            `;
        }else{
            next();
        }
    });
    app.use((ctx, next)=>{
        if(ctx.method == 'POST' && ctx.path == '/'){
            let arr = [];
            ctx.req.on('data', data=>{
                arr.push(data);
            })
            ctx.req.on('end', ()=>{
                console.log(Buffer.concat(arr).toString());
                ctx.body = Buffer.concat(arr).toString()
            })
        }
    })
    app.listen(4000);
复制代码

执行上面代码,我们发现一个问题,在控制台可以输出我们打印的信息,但是ctx.body并没有输出到页面上,为什么? 原因还是上面讲到的,on监听方法是异步的,当执行 on 方法时,其实 use 方法已经走完了,我们还是要将 on 方法包装成 promise 方法

    let Koa = require('koa');
    let app = new Koa();
    app.use(async (ctx, next)=>{
        if(ctx.path == '/' && ctx.method == 'GET'){
            ctx.set('Content-Type', 'text/html;charset=utf-8');
            ctx.body = `
                <form action="/" method="post">
                    <input type="text" name="username" />
                    <input type="text" name="password" />
                    <input type="submit"/>
                </form>
            `;
        }else{
            await next();  //这里不要忘记加上 await
        }
    });
    function bodyParaser(ctx){
        return new Promise((resolve, reject)=>{
            let arr = [];
            ctx.req.on('data', data=>{
                arr.push(data);
            })
            ctx.req.on('end', ()=>{
                let body = Buffer.concat(arr).toString();
                resolve(body);
            })
        })  
    }
    app.use(async (ctx, next)=>{
        if(ctx.method == 'POST' && ctx.path == '/'){
            let body = await bodyParaser(ctx);
            console.log(body);
            ctx.body = body;
        }
    })
    app.listen(3000);

复制代码
文件上传

我们在页面中有上传文件功能时,我们点击提交按钮后,我们来看下 request 请求: ![](http://m.qpic.cn/psb?/V10mpiHr3BdCn8/dBud546pK5WQq9.KAHvWWQ0*i1SCqoeuP4ALeZLyGO0!/b/dFQBAAAAAAAA&bo=DgQzAQAAAAADBxo!&rf=viewer_4) 我们再来看下提交的表单数据: ![](http://m.qpic.cn/psb?/V10mpiHr3BdCn8/oItf6qyjSsMvu6iGejcQr43WKxC8xBvTnuB3Frs4q1Y!/b/dFQBAAAAAAAA&bo=HgRzAQAAAAADB0o!&rf=viewer_4) 我们最终想要的数据是:

    {
        username: 'xx',
        password: 'cc'
    }
复制代码

所以我们需要对提交的数据进行解析,那么我们应该怎么解析呢?不难发现我们可以用请求中的 Content-Type 中的 boundary去分割 请求的数据,然后将数据组装成我们想要的格式:

    let Koa = require('koa');
    let app = new Koa();

    //Buffer对象没有 split 方法
    Buffer.prototype.split = function(sep){
        let index = 0;
        let pos = 0;
        let arr = [];
        let len = Buffer.from(sep).length;
        while(-1 != (pos = this.indexOf(sep, index))){
            arr.push(this.slice(index, pos));
            index = pos + len;
        }
        arr.push(this.slice(index));
        return arr;
    }
    function bodyParser(uploader){
        return async (ctx, next) =>{
            await new Promise((resolve, reject)=>{
                let buffer = [];
                ctx.req.on('data', data=>{
                    buffer.push(data);
                })
                ctx.req.on('end', ()=>{
                    let buf = Buffer.concat(buffer);  //获取到所有的请求数据
                    let contentType = ctx.get('Content-Type').split('----')[1]; //获取 boundary
                    let boundary = '-------' + contentType;
                    let arrs = buf.split(boundary); //用 boundary 去分割请求数据
                    console.log(arrs);
                    arrs = arrs.slice(1, -1); //去掉开头和末尾
                    console.log(arrs);
                    let body = {};
                    arrs.forEach(item => {  //获取每一个 field
                        let [head,tail] = item.split('\r\n\r\n'); //将每一组 field 进行空行回车分割
                        head = head.toString(); //将 Buffer 转码
                        console.log(head);
                        if(head.includes('filename')){ //如果是文件
                            let tail = lines.slice(head.length + 4, -2);
                            let ws = require('fs').createWriteStream(Date.now() + Math.random() + "");
                            ws.end(tail);
                        }else{ //否则是普通文本

                            let key = head.match(/name="(\w+)"/)[1];
                            console.log(key);
                            body[key] = tail.toString().slice(0, -2);
                        }
                    });
                    ctx.request.fields = body;
                    resolve();
                })
                    
            })
            await next();
        }
    }
    app.use(bodyParser({
        uploadDir: __dirname
    }))

    app.use(async (ctx, next)=>{
        if(ctx.method == 'GET' && ctx.path == '/'){
            ctx.set('Content-Type', 'text/html;charset=utf-8');
            ctx.body = `
                <form action="/" method="post" enctype="multipart/form-data">
                    <input type="text" name="username" />
                    <input type="text" name="password" />
                    <input type="file" name="filename">
                    <input type="submit"/>
                </form>
            `;
        }else{
            await next();
        }
    })
    app.use(async (ctx, next)=>{
        if(ctx.method == 'POST' && ctx.path == '/'){
            ctx.body = ctx.request.fields;
        }
    })
    app.listen(4000);
复制代码

转载于:https://juejin.im/post/5b56ff24e51d4519601ad4c5

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值