koa简易框架(一)

源码

koa使用分析

const Koa = require('koa');
let app = new Koa();//Koa是一个类,通过new生成一个实例

//koa的原型上有use方法,来注册中间件
app.use((ctx,next)=>{
    //koa拥有ctx属性,上面挂载了很多属性
    console.log(ctx.req.path);
    console.log(ctx.request.req.path);
    console.log(ctx.request.path);
    console.log(ctx.path);
    next();//洋葱模型,中间件组合
})

app.listen(3000);//Koa的原型上拥有监听listen

洋葱模型和中间件组合

洋葱模型

在这里插入图片描述
在这里插入图片描述

const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next)=>{
    console.log(1)
    await next();
    console.log(2)
});
app.use(async (ctx, next) => {
    console.log(3)
    await next();
    console.log(4)
})
app.use(async (ctx, next) => {
    console.log(5)
    awit next();
    console.log(6)
})
//打印结果:1 3 5 6 4 2 

中间件组合

koa洋葱模型的实现,其实就是通过use将函数存放在一个middlewares队列中,然后通过函数dispatch派发中间件。

dispatch组合中间件:
let app = {
    middlewares:[];     //缓存队列
    use(fn){    //注册中间件
        this.middlewares.push(fn);
    }
} 
app.use(next=>{
    console.log(1)
    next();
    console.log(2)
});
app.use(next => {
    console.log(3)
    next();
    console.log(4)
})
app.use(next => {
    console.log(5)
    next();
    console.log(6)
})
dispatch(0)
function dispatch(index){   //派发执行中间件
    if(index===app.middlewares.length) retrun ;
    let middleware = app.middlewares[index];
    middleware(()=>{
        dispatch(index+1);
    })
}

koa组成

在这里插入图片描述
koa主要是由四部分组成:

  • application:koa的主要逻辑,包含了中间件处理过程
  • context:koa关于ctx的封装
  • request:koa请求对象的封装
  • response:koa响应对象封装

koa实现

  1. Koa是一个类,拥有middleware、ctx、request、response
  2. Koa.prototype拥有use注册中间件
  3. Koa.prototype拥有listen监听网络请求,其内部是对http模块的封装
  4. Koa中handleRquest处理上下文ctx和中间件middleware
application.js

在koa目录下新建一个lib文件夹,新建application.js文件,代码如下:

let http = require('http');
let EventEmitter = require('events');
let context = require('./context');
let request = require('./request');
let response = require('./response');
let Stream = require('stream');
const fs = require('fs')

class koa extends EventEmitter {
    constructor(){
        super();
        this.middlewares = [];
        this.context = context;
        this.request = request;
        this.response = response;
        this.ctx = null;
    }
    
    use(fn){
        this.middlewares.push(fn);
    }
    
    createContext(req, res){
        const ctx = Object.create(this.context);
        // ctx.request为request.js里面this作了指向,this此时指向ctx
        const request = ctx.request = Object.create(this.request);
        const response = ctx.response = Object.create(this.response);

        // 请仔细阅读以下眼花缭乱的操作,后面是有用的
        /*
        * console.log(ctx.req.url)
          console.log(ctx.request.req.url)
          console.log(ctx.response.req.url)
          console.log(ctx.request.url)
          console.log(ctx.request.path)
          console.log(ctx.url)
          console.log(ctx.path)

          访问localhost:3000/abc

          /abc
          /abc
          /abc
          /undefined
          /undefined
          /undefined
          /undefined

          姿势多,不一定爽,要想爽,我们希望能实现以下两点:

          从自定义的request上取值、拓展除了原生属性外的更多属性,例如query path等。
          能够直接通过ctx.url的方式取值,上面都不够方便。
        * */
        ctx.req = request.req = response.req = req;
        ctx.res = request.res = response.res = res
        request.ctx = response.ctx = ctx;
        request.response = response;
        response.request = request;
        return ctx;
    }
    
    compose(middlewares, ctx){
        function dispatch(index) {
            if(index === middlewares.length){
                return Promise.resolve()
            }
            let middleware = middlewares[index];
            return Promise.resolve(middleware(ctx, () => {
                    dispatch(index+1)
            }))
        }
       return Promise.resolve(dispatch(0))
    }

    handleRequest(req, res){
        res.statusCode = 404;
        let ctx = this.ctx = this.createContext(req, res);
        let fn = this.compose(this.middlewares, ctx);
        ctx.setHeaders = (key,val)=>{
            res.setHeader(key, val)
        }
        setTimeout(() => {
            fn.then(() => {
                let body = ctx.body;
                if (Buffer.isBuffer(body) || typeof body === 'string'){
                    if(body.indexOf('<!DOCTYPE html>') != -1){
                        res.setHeader('Content-Type','text/html;charset=utf8')
                    }
                    else{
                        res.setHeader('Content-Type','text/plain;charset=utf8')
                    }
                    res.end(body);
                }
                else if(typeof body == 'number'){
                    res.setHeader('Content-Type','text/plain;charset=utf8')
                    res.end(body.toString());
                }
                else if (body instanceof Stream){
                    res.setHeader('Content-Type',''+ctx.contentType+';charset=utf8')
                    body.pipe(res);
                }
                else if(typeof body == 'object'){
                    res.setHeader('Content-Type','application/json;charset=utf8')
                    res.end(JSON.stringify(body));
                }
                else{
                    res.setHeader('Content-Type','text/plain;charset=utf8')
                    res.end('Not Found');
                }

            }).catch(err => {
                this.emit('error', err);
                res.statusCode = 500
                res.end('server error')
            })
        },20)
    }
    
    listen(...args){
        let server = http.createServer(this.handleRequest.bind(this));
        server.listen(...args);
    }
}
module.exports = koa;
源码解读:
  1. 这里我们有context,response,request,context使用了代理模式,代理之后,通过ctx.body,ctx.method等方式,可以直接设置返回数据或者方式。
  2. 这里的listen方法,主要是利用了http起用一个服务,handleRequest方法通过bind方式,可以直接将http绑定上去,使其handleRequest的this指向http。
  3. handleRequest这里主要是处理服务启动后的回调函数,参数包括req,res,ctx.setHeaders是我们这里封装的一个方式,通过这个方法,在其他页面可以设置返回头部,接下来我们将app.use里面函数一个个执行
let fn = this.compose(this.middlewares, ctx);
  1. this.middlewares是我们通过app.use方法,将use里面函数放到一个数组里面,稍后会进行讲解,ctx是对应的是context,我们把req,res当做参数传入,就可以在context.js文件里面具体设置req,res,在fn.then里面我们需要判断,因为在返回的数据里面,分为number,string,html以及文件类型,针对于每一种类型,我们需要设置不同头部。
  2. ctx.contentType,我们这里有一个这个主要是因为,文档类型不同,我们需要设置不同的Content-Type。
  3. compose方法,主要是针对app.use方式,在app.use里面我们写了不同函数,我们需要执行这些函数,除此之外,如果有next,这个时候调用dispatch(index+1),同时我们需要把它封装成Promise方式。
  4. createContext方法,在handleRequest调用,req,res对应的是http的回调req,res,在这里我们使用了Object.create方式,相当于继承,避免改变直接改变原对象,ctx.request为request.js里面this作了指向,this此时指向ctx,其他也是同理
  5. use方法是将app.use调用的函数放到一个数组里面

request.js

/*
*   非常简单,使用对象get访问器返回一个处理过的数据就可以将数据绑定到request上了,
*   这里的问题是如何拿到数据,由于前面ctx.request这一步,所以this就是ctx,
*   那this.req就是原生的req,再利用一些第三方模块对req进行处理就可以了,
*   源码上拓展了非常多,这里只举例几个,看懂原理即可。
    访问localhost:3000/abc?id=1

    /abc?id=1
    /abc?id=1
    /abc?id=1
    /abc?id=1
    /abc
    undefined
    undefined
*
* */
const url = require('url')
// noinspection JSAnnotator
let request = {
    get url() { // 这样就可以用ctx.request.url上取值了,不用通过原生的req
        return this.req.url
    },
    get path() {
        return url.parse(this.req.url).pathname
    },
    get query() {
        return url.parse(this.req.url).query
    },
    get method(){
        return this.req.method.toLowerCase()
    },
    get header(){
        return this.req.headers
    },
    get requestBody(){
        return this._body
    },
    set requestBody(val){
        this._body = val
    },
    // 。。。。。。
}
module.exports = request

源码解读

  1. 这里我们主要是封装了一些属性,共后续调用,get url(),这样就可以用ctx.request.url上取值了,不用通过原生的req,其他同理
  2. 至于这里的get,set方法,这是es6里面写法,参考链接

response.js

//response.js

let response = {
    get body(){
        return this._body;
    },
    set body(val){
        this.res.statusCode = 200 // 只要设置了body,就应该把状态码设置为200
        this._body = val // set时先保存下来
    },
    get contentType(){
        return this._type;
    },
    set contentType(val){
        this._type = val // set时先保存下来
    },
    get render(){
        return this._fn;
    },
    set render(val){
        this._fn = val // set时先保存下来
    },
}
module.exports = response;

源码解读

同理上面request.js

context.js

//context.js

let proto = {
};
function defineGetter(property,key){
    proto.__defineGetter__(key,function(){
        return this[property][key];
    })
}
function defineSetter(property,key){
    proto.__defineSetter__(key,function(val){
        this[property][key] = val;
    })
}

/*
* __defineGetter__方法可以将一个函数绑定在当前对象的指定属性上,当那个属性的值被读取时,
* 你所绑定的函数就会被调用,第一个参数是属性,第二个是函数,由于ctx继承了proto,
* 所以当ctx.url时,触发了__defineGetter__方法,所以这里的this就是ctx。这样,当调用defineGetter方法,就可以将参数一的参数二属性代理到ctx上了。
* */
defineGetter('request','url');  //ctx代理了ctx.request.url的get
defineGetter('request','path'); //ctx代理了ctx.request.path的get
defineGetter('request','query'); //ctx代理了ctx.request.query的get
defineGetter('request','method'); //ctx代理了ctx.request.method的get
defineGetter('request','header'); //ctx代理了ctx.request.header的get
defineGetter('response','body'); //ctx代理了ctx.response.body的get
defineSetter('response','body'); //ctx代理了ctx.response.body的set
defineGetter('response','contentType'); //ctx代理了ctx.response.contentType的get
defineSetter('response','contentType'); //ctx代理了ctx.response.contentType的set
defineGetter('response','render'); //ctx代理了ctx.response.render的get
defineSetter('response','render'); //ctx代理了ctx.response.render的set
defineGetter('request','requestBody'); //ctx代理了ctx.response.render的get
defineSetter('request','requestBody'); //ctx代理了ctx.response.render的set
module.exports = proto;

源码解读

ctx属性代理了一些ctx.request、ctx.response上的属性,使得ctx.xx能够访问ctx.request.xx或ctx.response.xx

调用方法

const Koa = require('./lib/application');

app.use(async (ctx,next)=>{
    ctx.res.setHeader('Access-Control-Allow-Origin', '*');
    //用于判断request来自ajax还是传统请求
    ctx.res.setHeader("Access-Control-Allow-Headers", "Content-Type,XFILENAME,XFILECATEGORY,XFILESIZE,content-type");
    //允许访问的方式
    ctx.res.setHeader('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS')
    //修改程序信息与版本
    ctx.res.setHeader('X-Powered-By', ' 3.2.1')
    //内容类型:如果是post请求必须指定这个属性
    ctx.res.setHeader('Content-Type', 'application/json;charset=utf-8')
    if(ctx.req.method == 'OPTIONS'){
        ctx.res.statusCode = 200;/*让options请求快速返回*/
    }
    next()
})
app.use(async (ctx,next)=>{
    ctx.body = 'hello koa'
    next()
})

app.listen(3000);




  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值