koa2框架的使用與解析

程式設計師小覃發表於2019-04-09

koa是什麼?

koa是基於nodejs的一個http中介軟體框架。koa原始碼只有一千多行,所有的功能都可以通過外掛實現,簡單易懂,自由度高。

準備

  1. node需要v7.6.0或者更高的版本(支援es6和async的語法)。
  2. npm install koa

開始

先看看來自官方github的最小demo

const Koa = require('koa');
const app = new Koa();

// response
app.use((ctx,next) => {
  ctx.body = 'Hello Koa';
});

app.listen(3000);
複製程式碼

不難發現,相對於原生nodejs來說,只多了uselisten兩個方法和ctxnext兩個引數。

中介軟體

koa的一個最重要的設計就是中介軟體。

1. 中介軟體是什麼?

中介軟體(middleware)本質上就是一個函式,處於http request和http response中間,實現某種中間的功能。使用app.use()來載入中介軟體。koa中每個中介軟體預設兩個引數,第一個是ctx物件,第二個是next函式。我們可以使用next函式來把執行權轉交給下一個中介軟體。

2. 中介軟體棧

多箇中介軟體會形成一個棧結構,以"先進後出"的原則執行。

const one = (ctx, next) => {
  console.log('1');
  next();
  console.log('2');
}

const two = (ctx, next) => {
  console.log('3');
  next(); 
  console.log('4');
}

const three = (ctx, next) => {
  console.log('5');
  next();
  console.log('6');
}

app.use(one);
app.use(two);
app.use(three);

// 1 3 5 6 4 2
複製程式碼

3. 移除next?

中介軟體通過next來將函式執行權移交到下一個中介軟體,若無next,執行權就不會移交下一個中介軟體,後續中介軟體無效。

app.use((crx, next) => {
  console.log(1)
  next()
  console.log(2)
})
app.use((crx, next) => {
  console.log(3)
  // next()
  console.log(4)
})
app.use((crx, next) => {
  console.log(5)
  next()
  console.log(6)
})
app.listen(3000);
// 1 3 4 2
複製程式碼

use

  1. 每一次use傳入一個回撥函式f,listen時執行這個函式f,demo的回撥函式f是
(ctx) => {
  ctx.body = 'Hello Koa';
}
複製程式碼
  1. 回撥函式f一共有兩個引數,分別是context(以下簡寫ctx)和next。第一個引數ctx是原生req、res經過一系列處理後產生的物件。 函式f的第二個引數next()跳到下一個回撥函式,多個use的回撥函式按照順序執行。(後面中介軟體有詳解)

listen

listen的實現原理其實就是對http.createServer進行了一個封裝,重點是這個函式中傳入的callback。callback就是所有中介軟體的組合。

原生node監聽3000埠

http.createServer(function(req, res){
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.write('Hello Http Server');
    res.end();
}).listen(3000);
複製程式碼

koa的封裝實現

listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
複製程式碼

ctx

koa中, ctx表示一次對話的上下文(包括 HTTP 請求和 HTTP 回覆)。為了開發方便而設計的一個js物件,繫結了請求和響應相關的資料和方法(如ctx.path、ctx.body等)。本質上使用request、response兩個檔案擴充屬性,ctx通過delegate實現代理拿到request和response的方法和屬性。

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');
  
delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .access('accept')
  .getter('origin')
  .getter('href')
  .getter('subdomains')
  .getter('protocol')
  .getter('host')
  .getter('hostname')
  .getter('URL')
  .getter('header')
  .getter('headers')
  .getter('secure')
  .getter('stale')
  .getter('fresh')
  .getter('ips')
  .getter('ip');
複製程式碼

比如我們要訪問ctx.repsponse.status,在開發中可以直接訪問ctx.status。

next

在中介軟體中,使用next函式來把執行權轉交給下一個中介軟體。

路由

網站一般有多個頁面/介面,通過ctx.request.path獲取使用者請求的路徑來實現簡單的路由。

const main = ctx => {
  if (ctx.request.path !== '/') {
    ctx.response.type = 'html';
    ctx.response.body = '<a href="/">Index Page</a>';
  } else {
    ctx.response.body = 'Hello World';
  }
}
複製程式碼

隨著專案不斷迭代,頁面/介面數量會上升。上述程式碼不易維護,此時可以使用koa-router

錯誤捕獲

在發生錯誤的時候,能夠捕獲到錯誤和丟擲的異常,並反饋出來。koa中有以下兩種比較通用的捕獲錯誤的方法。

  1. 執行出錯時,koa會觸發一個error事件,我們可以監聽這個事件來處理錯誤。
app.on('error', (err,ctx) => {
    console.error('server error', err)
});
複製程式碼
  1. 自己寫一個最外層的中介軟體,負責所有中介軟體的錯誤處理。
const errorHandler = async (ctx,next) => {
    try {
        await next();
    } catch (err) {
        ctx.status = err.statusCode || err.status || 500;
        ctx.body = {
            message: err.message
        }
    }
}
複製程式碼

當錯誤被try...catch捕獲時,就不會觸發koa的error事件。當我們catch捕獲到錯誤時,可以手動觸發error事件。

const errorHandler = async (ctx,next) => {
    try {
        await next();
    } catch (err) {
        ctx.status = err.statusCode || err.status || 500;
        ctx.body = {
            message: err.message
        }
        ctx.app.emit('error', err, ctx) // 手動觸發error事件
    }
}
複製程式碼

參考

相關文章