Koa原理和封裝

相關文章 最基礎 實現一個簡單的koa2框架 實現一個簡版koa koa實踐及其手擼html

Koa源碼只有4個js文件mongodb

  • 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(只讀)express

// 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的總執行次數和中間件的長度,若是不同,就要報錯json

環形【洋蔥】有什麼好處 上面的洋蔥圈可能沒看懂,上一個簡易版的segmentfault

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,查數據庫,響應bodypromise

文件訪問中間件app

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的使用

相關文章
相關標籤/搜索