Koa原理和封装

相关文章
最基础
实现一个简单的koa2框架
实现一个简版koa
koa实践及其手撸

Koa源码只有4个js文件

  • application.js:简单封装http.createServer()并整合context.js
  • context.js:代理并整合request.js和response.js
  • request.js:基于原生req封装的更好用
  • response.js:基于原生res封装的更好用

如果我们要封装一个Koa,
需要实现use加载中间件,
next下一个中间件,并且是环形的,
中间件是promise的

ctx=>对应常用的是 body(可读写)/ url(只读)/ method(只读)

// request.js
const request = {
  get url() {
    return this.req.url;
  },
  set url(val) {
    this.req.url = val;
  }
};

module.exports = request;
// response.js
const response = {
  get body() {
    return this._body;
  },
  set body(data) {
    this._body = data;
  },
  get status() {
    return this.res.statusCode;
  },
  set status(statusCode) {
    if (typeof statusCode !== 'number') {
      throw new Error('statusCode 必须为一个数字');
    }
    this.res.statusCode = statusCode;
  }
};

module.exports = response;
// context.js
const context = {
  get url() {
    return this.request.url;
  },
  set url(val) {
    this.request.url = val;
  },
  get body() {
    return this.response.body;
  },
  set body(data) {
    this.response.body = data;
  },
  get status() {
    return this.response.statusCode;
  },
  set status(statusCode) {
    if (typeof statusCode !== 'number') {
      throw new Error('statusCode 必须为一个数字');
    }
    this.response.statusCode = statusCode;
  }
};
module.exports = context;
const Emitter = require('events');
const http = require('http');

// 引入 context request, response 模块
const context = require('./context');
const request = require('./request');
const response = require('./response');

class Application extends Emitter {
  /* 构造函数 */
  constructor() {
    super();
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    // 保存所有的中间件函数
    this.middlewares = [];
  }
  // 开启 http server 并且传入参数 callback
  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
  use(fn) {
    // this.callbackFunc = fn;
    // 把所有的中间件函数存放到数组里面去
    this.middlewares.push(fn);
    return this;
  }
  callback() {
    return (req, res) => {

      // 创建ctx
      const ctx = this.createContext(req, res);
      // 响应内容
      const response = () => this.responseBody(ctx);

      // 响应时 调用error函数
      const onerror = (err) => this.onerror(err, ctx);

      //调用 compose 函数,把所有的函数合并
      const fn = this.compose();
      return fn(ctx).then(response).catch(onerror);
    }
  }
  /**
     监听失败,监听的是上面的catch
   */
  onerror(err) {
    if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));

    if (404 == err.status || err.expose) return;
    if (this.silent) return;

    const msg = err.stack || err.toString();
    console.error();
    console.error(msg.replace(/^/gm, '  '));
    console.error();
  }
  /*
   构造ctx
   @param {Object} req实列
   @param {Object} res 实列
   @return {Object} ctx实列
  */
  createContext(req, res) {
    // 每个实列都要创建一个ctx对象
    const ctx = Object.create(this.context);
    // 把request和response对象挂载到ctx上去
    ctx.request = Object.create(this.request);
    ctx.response = Object.create(this.response);
    ctx.req = ctx.request.req = req;
    ctx.res = ctx.response.res = res;
    return ctx;
  }
  /*
   响应消息
   @param {Object} ctx 实列
  */
  responseBody(ctx) {
    const content = ctx.body;
    if (typeof content === 'string') {
      ctx.res.setHeader('Content-Type', 'text/pain;charset=utf-8')
      ctx.res.end(content);
    } else if (typeof content === 'object') {
      ctx.res.setHeader('Content-Type', 'text/json;charset=utf-8')
      ctx.res.end(JSON.stringify(content));
    }
  }
  /*
   把传进来的所有的中间件函数合并为一个中间件
   @return {function}
  */
   compose(){
      let middlewares = this.middlewares
      return function(ctx){
        return dispatch(0)
        function dispatch(i){
           let fn = middlewares[i]
           if(!fn){
              return Promise.resolve()
           }
           return Promise.resolve(fn(ctx, function next(){
              return dispatch(i+1)
           }))
        }
      }
    } 
}

module.exports = Application;
// 使用
const testKoa = require('./application');
const app = new testKoa();

app.use((ctx) => {
  str += 'hello world'; // 没有声明该变量, 所以直接拼接字符串会报错
  ctx.body = str;
});

app.on('error', (err, ctx) => { // 捕获异常记录错误日志
  console.log(err);
});

app.listen(3000, () => {
  console.log('listening on 3000');
});

优化
如果有一个中间件写了两个next,会执行两次,需要通过判断next的总执行次数和中间件的长度,如果不一样,就要报错

环形【洋葱】有什么好处
上面的洋葱圈可能没看懂,上一个简易版的

var arr = [function(next){
   console.log(1)
   next()
   console.log(2)
},function(next){
   console.log(3)
   next()
   console.log(4)
}]
var i = 0;
function init(){
   arr[i](function(){
    i++
    if(arr[i]){
       init()
    }
   })
}
init()
// 1342

为什么是1342
上面的代码打个断点就知道了

// 这样应该看得懂吧
function(){
   console.log(1)
   var next = function(){
      console.log(3)
      var next = ...
      console.log(4)
   }
   next()
   console.log(2)
}

在以前不是express设计的框架,整个请求到响应结束是链结构的,一个修改响应的插件就需要放到最后面,但是有个环形的设计,只要把要修改响应的代码写到next执行后就行了,对于开发者也是,获取请求的数据,修改请求的数据,next,查数据库,响应body

文件访问中间件

module.exports = (dirPath = "./public") => {
    return async (ctx, next) => {
        if (ctx.url.indexOf("/public") === 0) {
            // public开头 读取文件
            const url = path.resolve(__dirname, dirPath);
            const fileBaseName = path.basename(url);
            const filepath = url + ctx.url.replace("/public", ""); 
                console.log(filepath);
            // console.log(ctx.url,url, filepath, fileBaseName) 
            try {
                stats = fs.statSync(filepath);
                if (stats.isDirectory()) {
                    const dir = fs.readdirSync(filepath);
                    const ret = ['<div style="padding-left:20px">'];
                    dir.forEach(filename => {
                        console.log(filename);
                        // 简单认为不带小数点的格式,就是文件夹,实际应该用statSync 
                        if (filename.indexOf(".") > -1) {
                            ret.push(
                                `<p><a style="color:black" href="${
                                ctx.url
                                }/${filename}">${filename}</a></p>`
                            );
                        } else {
                            // 文件
                            ret.push(
                                `<p><a href="${ctx.url}/${filename}">${filename}</a></p>`
                            );
                        }
                    });
                    ret.push("</div>");
                    ctx.body = ret.join("");
                } else {
                    console.log("文件");
                    const content = fs.readFileSync(filepath);
                    ctx.body = content;
                }
            } catch (e) {
                // 报错了 文件不存在
                ctx.body = "404, not found";
            }
        } else {
            // 否则不是静态资源,直接去下一个中间件
            await next();
        }
    }
}

// 使用
const static = require('./static') 
app.use(static(__dirname + '/public'));

路由中间件

class Router {
    constructor() {
        this.stack = [];
    }
    // 每次定义一个路由,都注册一次
    register(path, methods, middleware) {
        let route = { path, methods, middleware }
        this.stack.push(route);
    }
    // 现在只支持get和post,其他的同理 
    get(path, middleware) {
        this.register(path, 'get', middleware);
    }
    post(path, middleware) {
        this.register(path, 'post', middleware);
    }
      //调用
    routes() {
        let stock = this.stack;
        return async function (ctx, next) {
            let currentPath = ctx.url;
            let route;
            for (let i = 0; i < stock.length; i++) {
                let item = stock[i];
                if (currentPath === item.path && item.methods.indexOf(ctx.method) >= 0) {
                    // 判断path和method
                    route = item.middleware; break;
                }
            }
            if (typeof route === 'function') {
                route(ctx, next);
                return;
            }
            await next();
        };
    }
}

module.exports = Router;

// 使用
const Koa = require('Koa')
const Router = require('./router')
const app = new Koa()
const router = new Router();
router.get('/index', async ctx => { ctx.body = 'index page'; });
router.get('/post', async ctx => { ctx.body = 'post page'; });
router.get('/list', async ctx => { ctx.body = 'list page'; });
router.post('/index', async ctx => { ctx.body = 'post page'; });
// 路由实例输出父中间件 
app.use(router.routes());

下一篇mongodb插件mongoose的使用

相关推荐