node之koa核心程式碼

蘆夢宇發表於2018-06-30

我們先實現這個小案例

app.use(async (ctx,next)=>{
    console.log(ctx.request.req.url)
    console.log(ctx.req.url)

    console.log(ctx.request.url);
    console.log(ctx.url);

    console.log(ctx.request.req.path)
    console.log(ctx.req.path)

    console.log(ctx.request.path);
    console.log(ctx.path);
});
複製程式碼

koa中的ctx包括原生的res和req屬性,並新加了request和response屬性 ctx.req = ctx.request.req = req; ctx.res = ctx.response.res = res; ctx代理了ctx.request和ctx.response

和koa原始碼一樣,我們將程式碼分為4個檔案application.js, context.js, response.js, request.js

application.js:

let context = require('./context');
let request = require('./request');
let response = require('./response');
let stream = require('stream');
class Koa extends EventEmitter{
  constructor(){
    super();
    this.context = context;//將context掛載到例項上
    this.request = request;//將request掛載到例項上
    this.response = response;//將response掛載到例項上
  }
  use(fn){
    this.fn = fn;
  }
  //此方法,將原生的req掛載到了ctx和ctx.request上,將原生的res掛載到了ctx和ctx.response上
  createContext(req,res){
    // 建立ctx物件 request和response是自己封裝的
    let ctx = Object.create(this.context);//繼承context
    ctx.request = Object.create(this.request);//繼承request
    ctx.response = Object.create(this.response);// 繼承response
    ctx.req = ctx.request.req = req;
    ctx.res = ctx.response.res = res;
    return ctx;
  }
  handleRequest(req,res){
    // 通過req和res產生一個ctx物件
    let ctx = this.createContext(req,res);  //cxt具備四個屬性request,response,req,res,後兩個就是原生的req,res
  }
  listen(){
    let server = http.createServer(this.handleRequest.bind(this));
    server.listen(...arguments);
  }
}

複製程式碼

那例子中的ctx.request.path ctx.path和ctx.request.url/ctx.url怎麼實現? 我們知道path和url都是請求中的引數,因此可以通過req實現,request.js實現如下:

let url = require('url')
let request = {
  get url(){
    return this.req.url  
  },
  get path(){
    return url.parse(this.req.url).pathname
  }
}
module.exports = request;
複製程式碼

當訪問ctx.request.url時,訪問get url方法,方法中的this指的是ctx.request 這樣ctx.request.url,ctx.request.path,ctx.request.query都實現了,那麼ctx.url,ctx.path怎麼實現呢?

koa原始碼中用了代理,原理程式碼context.js程式碼如下:

let proto = {}
//獲取ctx.url屬性時,呼叫ctx.request.url屬性
function defineGetter(property,name) {
  proto.__defineGetter__(name,function () {
    return this[property][name]; 
  })
}
//ctx.body = 'hello' ctx.response.body ='hello'
function defineSetter(property, name) {
  proto.__defineSetter__(name,function (value) {
    this[property][name] = value;
  })
}
defineGetter('request','path');// 獲取ctx.path屬性時,呼叫ctx.request.path屬性
defineGetter('request','url');// 獲取ctx.url屬性時,呼叫ctx.request.url屬性
module.exports = proto;
複製程式碼

現在就實現了例子中的功能,接下來如何獲取ctx.body和設定ctx.body呢? 用ctx.response.boxy實現,然後再代理下即可。 response.js程式碼如下:

let response = {
  set body(value){
    this.res.statusCode = 200;
    this._body = value;// 將內容暫時存在_body屬性上
  },
  get body(){
    return this._body
  }
}
module.exports = response;
複製程式碼

此時ctx.response.body = 'hello',就將hello放到了ctx.response的私有屬性_body上,獲取ctx.response.body ,就可以將‘hello’取出。 然後在 context.js程式碼中將body也代理上:

let proto = {}

function defineGetter(property,name) {
  proto.__defineGetter__(name,function () {
    return this[property][name]; 
  })
}

function defineSetter(property, name) {
  proto.__defineSetter__(name,function (value) {
    this[property][name] = value;
  })
}
defineGetter('request','path');
defineGetter('request','url');
defineGetter('response','body');
defineSetter('response','body');
module.exports = proto;
複製程式碼

這樣ctx.body就可以獲取到了。

重點來了,如何實現koa 中介軟體呢?

class Koa extends EventEmitter{
  constructor(){
    super();
    this.context = context;
    this.request = request;
    this.response = response;
    this.middlewares = [];//增加middlewares屬性
  }
  use(fn){
    this.middlewares.push(fn);//每次呼叫use 都將方法存起來
  }
  ...
  handleRequest(req,res){
    // 通過req和res產生一個ctx物件
    let ctx = this.createContext(req,res);
    // composeFn是組合後的promise
    let composeFn = this.compose(this.middlewares, ctx);
     composeFn.then(()=>{
         //渲染body到頁面
     })
  }
}
複製程式碼

koa內部將每一個方法都包了一層promise,這樣可以執行非同步操作,這也是和express的重要差別。 返回的composeFn也是一個promise,這樣就可以在then裡面做內容渲染了。 那compose如何實現呢?程式碼很短,如下:

 compose(middlewares,ctx){
    function dispatch(index) {
      if (index === middlewares.length) return Promise.resolve();//防止溢位,返回一個promise
      let fn = middlewares[index];
      return Promise.resolve(fn(ctx,()=>dispatch(index+1)));//為保證每個方法都是promise,這裡在外面包了一層promise
    }
    return dispatch(0);//先執行第一個fn方法
  }
複製程式碼

然後渲染頁面:需要判斷是物件,是檔案,是流,是字串的情況:

res.statusCode = 404;//預設404
composeFn.then(()=>{
     //渲染body到頁面
     let body = ctx.body;
      if (body instanceof stream) {//是流
        body.pipe(res);
      }else if(typeof body === 'object'){
        res.end(JSON.stringify(body));
      }else if(typeof body === 'string' || Buffer.isBuffer(body)){
        res.end(body);
      }else{
        res.end('Not Found');
      }
  }
 })
複製程式碼

頁面錯誤如何處理呢? 例子:

app.use(async (ctx,next)=>{
    ctx.body = 'hello';
    throw Error('出錯了')
});
app.on('error', function (err) {
  console.log(err)
})
app.listen(3000);
複製程式碼

我們知道每一個方法都被包成了promise,當出現錯誤時,錯誤會被catch捕獲,因此可以新增catch方法,捕獲錯誤

composeFn.then(()=>{
      let body = ctx.body;
      if (body instanceof stream) {
        body.pipe(res);
      }else if(typeof body === 'object'){
        res.end(JSON.stringify(body));
      }else if(typeof body === 'string' || Buffer.isBuffer(body)){
        res.end(body);
      }else{
        res.end('Not Found');
      }
    }).catch(err=>{ // 如果其中一個promise出錯了就發射錯誤事件即可
      this.emit('error',err);
      res.statusCode = 500;
      res.end('Internal Server Error');
})
複製程式碼

相關文章