node学习koa

215 阅读7分钟

1.koa介绍

Koa 是现在最流行的基于Node.js平台的web开发框架,由 Express 原班人马打造,利用 async 函数,Koa丢弃了回调函数,并有力地增强错误处理.体积小。

koa原理:組合comprose方法,把多个promise组合一个promise,主要靠递归的方式,先取出第一个,第一个会等待第二个方式执行。

const Koa = require('koa'); // babel
const app = new Koa(); // 创建一个app实例
// 请求到来时会执行此方法
app.use((ctx)=>{
    ctx.body = 'hello'
});
app.listen(3000);

// 1.listen 方法
// 2.use 方法
// 3.ctx 上下问对象
// 4.监控错误

目录结构

  • application 应用文件 默认引用的
  • req/res (node中的默认的 req,res原生的)
  • request response (这个属性是koa自己封装的)
  • context 上下文 整合 req\res\request\response

2.三大模块

  • Application: 基本服务器框架
  • Context: 服务器框架基本数据结构的封装,用以 http 请求解析及响应
  • Middleware: 中间件,也是洋葱模型的核心机制

3.req和request和ctx的关系

  • 创建app的应用

    packge.json文件路径改为自己的,

    "main": "./lib/application.js"

    //server.js
    const Koa = require('./koa');
    // 创建app的应用
    let app = new Koa();
    
    app.use((ctx)=>{
       res.end("hello")
    });
    
    app.listen(3000,()=>{
        console.log(`server start 3000`)
    });
    //application.js
    const http = require('http');
    module.exports = class Application {
        constructor() {
            this.handleRequest = this.handleRequest.bind(this)
        }
        use(callback) {
            this.callback = callback;
        }
        handleRequest(req,res){
            // this
            //请求到来时需要执行use方法
            this.callback(req, res);
        };
        listen(...args){
            //通过bind绑定这个方法,可以在构造函数中绑定
            //箭头函数的方式改变this
            const server = http.createServer(this.handleRequest.bind(this));
            server.listen(...args);
        }
    }
    
  • 实现ctx

    主要考虑this指向的问题.

    思考:1.req 和 request的区别 request.req = req;

    1. ctx.path = ctx.request.path 为什么?

      解释:ctx内部就是defineProperty 代理,它会去content.js找getPath方法,返回的就是request.path

    //server.js
    app.use((ctx)=>{
        console.log(ctx.req.url); // 原生的url
        // 自己封装的request上有原生的req属性
        console.log(ctx.request.req.url); 
        //上面2个是等价的,下面2个是等价的
        // pathname  => url.path()
        console.log(ctx.request.path); // 自己封装的url属性
        //简写
        console.log(ctx.path); // 他表示的是 ctx.request.url    
    });
    
    // 通过请求和响应 + 自己封装的request和response 组成一个当前请求的上下文 
    handleRequest(req,res){
        let ctx = this.createContext(req,res);
        this.callBack(ctx)
    }
              
    

    context.js需要很多方法例如get url ,get path,升级版的写法defineGetter('request','url');

    defineGetter('request','path');

    看以下代码解释

    let obj = {
        url:{
            a:1
        }
    }
    // obj.a => obj.url.a
    // 废弃的方法
    obj.__defineGetter__('a',function () {
        return obj.url.a
    })
    console.log(obj.a)
    
    //完整版的context.js
    let context = {
    //     get url(){ // 这里也是一个代理的机制 相当于你去 ctx.url   ctx指代的并不是我们的context文件指的是我们在应用中拷贝出来的一份 ,拷贝的一份上 context.request.url 
    //         return this.request.url
    //     },
    //     get path(){
    //         return this.request.path
    //     } 
    }
    // defineProperty proxy 
    
    function defineGetter(target,key){
        context.__defineGetter__(key,function () { // context.url = > context.request.url
            // console.log(this.__proto__.__proto__ === context);
            return this[target][key];
        })
    }
    
    function defineSetter(target,key){
        context.__defineSetter__(key,function (value) { 
            this[target][key] = value;
        })
    }
    //等价于上面的get url(){return} 
    defineGetter('request','url');
    defineGetter('request','path');
    defineGetter('request','query');
    
    defineGetter('response','body');
    defineSetter('response','body');
    module.exports = context;
    
  • request

    const url = require('url');
    let request = {
        get url() {
            // 这里的this 指代的是 ctx.request 因为使用的时候是通过 ctx.request.url来使用的
            // ctx.request.req = req
            return this.req.url;
        },
        get path() {
            return url.parse(this.req.url).pathname
        },
        get query() {
            return url.parse(this.req.url).query
        }
    }
    // 导出request对象
    module.exports = request;
    
    
    // 对象的属性访问器
    
  • response

    let response = {
        _body: undefined,
        set body(value) {
            this.res.statusCode = 200;
            this._body = value;
        },
        get body() {
            return this._body
        }
    }
    module.exports = response;
    
    // defineProperty
    

4.koa-compose方法

compose,顾名思义,相当于把所有注册的中间件组合,它规定了中间件的执行顺序,返回一个“大”中间件,这个大中间件,就在 node 的 http 服务onRequest事件的时候执行。

难点在中间件.洋葱模型。

把多个promise 组合成一个promise 这个promise完成后 去拿 最终的结果显示给用户

//洋葱模型
const Koa = require('koa');
const app = new Koa();
// koa 里面用的是洋葱模型 把方法都套起来  co -> next
// koa中可以使用async + await方法

app.use(async function (ctx,next) {
    console.log(1);
    next()
    console.log(2);
})
app.use(async function (ctx,next) {
    console.log(3);
    next();
    console.log(4);
})
app.use(function (ctx,next) {
    console.log(5);
    next();
    console.log(6);
})
// 输出1 3 5 6 4 2 
app.listen(3000);

// 把多个promise 组合成一个promise 这个promise完成后 去拿 最终的结果显示给用户

koa 的洋葱模型指出每一个中间件都像是洋葱的每一层,当从洋葱中心穿过时,每层都会一进一出穿过两次,且最先穿入的一层最后穿出。

  • middleware: 第一个中间件将会执行

  • next: 每个中间件将会通过 next 来执行下一个中间件。

    //middleware.js
    const Koa = require('koa');
    const app = new Koa();
    const sleep = async ()=>{
        await new Promise((resolve,reject)=>{
            setTimeout(() => {
                console.log('睡觉')
                resolve();
            }, 1000);
        })
    }
    // koa 里面用的是洋葱模型 把方法都套起来  co -> next
    // koa中可以使用async + await方法
    // 我们执行koa时  每个next 方法都需要前面都要增加await 否则不会有等待效果
    // koa为了统一处理错误  就将每个函数都转化成promise ,为了方便错误处理
    
    // 整理http的header
    // 用reduce() 来实现函数的组合  (redux-compose)
    
    app.use(async function (ctx,next) {
        // console.log(1);
        // await next(); // 这里只是调用next函数并没有等待的效果
        // next();
        // next()
        // console.log(2);
        await next();
        next();
    })
    // app.use(async function (ctx,next) {
    //     console.log(3);
    //     await sleep();
    //     await next();
    //     console.log(4);
    // })
    // app.use(function (ctx,next) {
    //     console.log(5);
    //     // throw new Error('我错了');
    //     next();
    //     console.log(6);
    // })
    app.on('error',function (err) {
        console.log(err);
    })
    app.listen(3000);
    
    
    // 把多个promise 组合成一个promise 这个promise完成后 去拿 最终的结果显示给用户
    
    

    在application.js用

        this.middlewares = []; // 存放所有的use方法
        use(callback){
            this.middlewares.push(callback); // [fn,fn,fn]
        }
    //组合多个函数
    this.compose(ctx).then(()=>{    
                let body = ctx.body;
    }
                            compose(ctx){
            // 默认将middlewares 里的第一个执行
            let index = -1; // 默认一次都每调用
            const dispatch = (i)=>{ // 0
                let middleware = this.middlewares[i];
                // 第一次调用将值保存到了index中
                if (i ==index){
                    return Promise.reject(new Error('next() called multiple times my ----'))
                }
                // index = 1
                index = i; // 相当于第一次调用时 我把index 变成0
                // 如果执行完毕后 有可能返回的不是promise
                if(i === this.middlewares.length){
                    return Promise.resolve();
                }
                // 链式等待 默认先执行第一个 之后如果用户调用了await next()
                try{ // 这里需要增加try/catch 否则直接抛错 需要捕获异常
                    return Promise.resolve(middleware(ctx,()=>dispatch(i+1)));
                }catch(e){
                    return Promise.reject(e);
                }
            }
            return dispatch(0); // 默认取出第一个执行
        }
    

5.koa返回不同的body内容

application.js组合多个函数,1默认是字符串或者buffer2可能是个流Stream3是一个对象4数字number

handleRequest(req,res){
        // this
        // 通过请求和响应 + 自己封装的request和response 组成一个当前请求的上下文 
        let ctx = this.createContext(req,res);   

        // 组合多个函数
        res.statusCode = 404;
        this.compose(ctx).then(()=>{    
            let body = ctx.body;
            // 默认是字符串或者buffer
            if(typeof body === 'string' || Buffer.isBuffer(body)){
                res.end(body);
            }else if(body instanceof Stream){
                // 需要下载此文件
                res.setHeader('Content-Disposition',`attachment;filename=${ctx.path.slice(1)}`);
                body.pipe(res);
            }else if(typeof body == 'object'){
                res.end(JSON.stringify(body));
            }else if(typeof body == 'number'){
                res.end(body + '');
            }else{
                res.end('Not found')
            }
            
        }).catch(err=>{
            this.emit('error',err);
        })
    }

body.js

const Koa = require('./koa');
const app = new Koa();
const fs = require('fs');
const file = fs.createReadStream('./1.server.js')

app.use((ctx,next)=>{
  ctx.body = '123'
})
app.on('error',function (err) {
    console.log(err);
})
app.listen(3000);

当访问 post /form的时候 我需要获取用户在表单中填写的数据

当访问 get /form 给用户显示个表单

需要一个中间件npm install koa- bodyparser

代码如下

// 当访问 post /form的时候 我需要获取用户在表单中填写的数据
// 当访问 get /form 给用户显示个表单
const Koa = require('koa');
const bodyParser = require('./koa-bodyparser');
const static = require('./koa-static')
const app = new Koa();
 // 为了方便传递参数 所以把中间件 都封装了一个个函数
 app.use(bodyParser());

// 中间件 
app.use(async (ctx, next) => {
    if (ctx.method === 'GET' && ctx.path == '/form') {
        ctx.body = `<form action="/form" method="POST">
            用户名<input type="text" name="username"> <br/>
            密码<input type="text" name="password"> <br/>
            <button>提交 </button>
        </form>`
    } else {
        await next(); // undefined需要等待,必须加app.use(async
    }
});
// koa中不能处理回调函数的方式  所有的异步需要全部封装成promise即可

// const body = (ctx)=>{
//     return new Promise((resolve,reject)=>{
//         let arr =[]
//         ctx.req.on('data',function (chunk) {
//             arr.push(chunk);
//         });
//         ctx.req.on('end',function () {
//             resolve(Buffer.concat(arr).toString())
//         })
//     })
// }

app.use(async (ctx, next) => {
    if (ctx.method === 'POST' && ctx.path == '/form') {
        ctx.body = await ctx.request.body;
    }else{
        next();
    }
});
app.listen(3000);

app.use(static(__dirname))
app.use(static(require('path').join(__dirname,'node_modules')))

koa- bodyparser.js

中间件的特点 可以统一处理逻辑,如果处理完毕后 需要继续向下执行next()

const bodyparser = ()=>{
    return async (ctx,next)=>{
        // 中间件的特点 可以统一处理逻辑
        // 如果处理完毕后 需要继续向下执行
            await new Promise((resolve,reject)=>{
                let arr =[]
                ctx.req.on('data',function (chunk) {
                    arr.push(chunk);
                });
                ctx.req.on('end',function () {
                   ctx.request.body = Buffer.concat(arr).toString()
                   resolve();
                })
            });          
            return next()//返回结果,需要后续逻辑
    }
}
module.exports = bodyparser;

return next() 和await的区别:

后面没有逻辑了,return;还需要后续处理await

静态服务中间件const static = require('./koa-static')

let fs = require('fs').promises;
let path = require('path')

const static = (filePath) => {
    return async (ctx, next) => {
        let pathUrl = path.join(filePath,ctx.path);
        try{
            let statObj = await fs.stat(pathUrl);
            if (statObj.isFile()) {
                ctx.set('Content-Type', 'text/html;charset=utf-8');
                ctx.body = await fs.readFile(pathUrl)
            } else {
                await next();
            }
        }catch(e){
            await next()
        }
        
    }
}
module.exports = static;