當前位置:網站首頁>初識Koa

初識Koa

2022-05-13 04:32:21水念

Koa -- 基於 Node.js 平臺的下一代 web 開發框架。

上面的那句話是Koa官方講的。首先Koa是一個新的web框架,由Express幕後的原班人馬打造。我們都知道現在市面上很多的Node 服務框架或者是功能,都是基於Express來進行開發的,比如說(webpack-dev-server,Nest,NodeBB等等)。這主要是因為Express出來的時間比較久,而且相對來說比較穩定;Express的生態相對的會完善一些;另外一點就是Express相對於來說比較容易入門。可以基於官方的工具直接生成,天生的自帶路由和模板解析插件,比較容易上手。

當然,既然說Koa是下一代的web開發框架,就說明Koa肯定存在有比Express更優秀的地方:首先更小;另外Koa利用async函數,來丟弃Express中的回調函數,並且,更有力的增强錯誤處理。由於Koa中沒有綁定任何的中間件,因此要搭建一套完整的功能,就需要自己去添加較多的中間件。對於初學者來說,相對的麻煩一些。

接下來就通過實踐來逐漸進入到Koa的世界裏吧。

Koa的發展曆史

在Koa的發展曆史中,存在兩個版本,一個Koa 1.x一個是Koa 2.x。但是聊到Koa的發展,就不得不聊一下Express,因為Koa 同樣出自 Express 團隊之手。

Express

Express是 Node.js 的第一代流行 web 開發框架,主要是對 http 模塊進行封裝,並提供了路由、模板渲染等 web 開發常用功能,功能齊全但需要全量引用

由於出現早於 Promise,錯誤處理使用 Node.js 的 callback 風格,相應有了回調地獄問題,不過隨著 Promise 流行,現在 Express 4.x、5.x 已經沒了這個問題

Express 中間件是線性執行的,每一個中間件處理完成之後只有兩個選擇

  1. 交給下一個中間件
  2. 返回 response

只要是離開中間件後就再也無法返回,這樣的設計讓邏輯非常簡單,但在很多需要多次處理請求的實現上變得複雜:比如統計一個請求的耗時,在 Express 中充斥著大量利用事件或者回調來 hack 這種需求的處理方式

exports.responseTime = function () {
  return function (req, res, next) {
    req.startTime = new Date(); // 開始時間

    const trackTime = function () {
      const endTime = new Date(); // 結束時間
      const duration = endTime - req.startTime;
      console.log('X-Response-Time: ', duration + 'ms');
    }

    res.once('finish', trackTime);
    res.once('close', trackTime);
    return next();
  }
}

因為其簡單、功能高度集成的特性,Express 現在仍然是最流行的 Node.js web 開發框架,但功能高度集成帶來的不靈活和中間件的線性調用特性讓越來越多企業級 web 框架封裝都選擇了使用 koa。

Koa 1.x

Koa 同樣出自 Express 團隊之手,除了 API 變化,相對於 Express 做了兩個最重要的變更

  1. 不再內置任何中間件,所有 web 處理中間件都需要引用,靈活性和複雜性相伴而來(從這個角度講 Express 才更像是 web 框架)
  2. 利用 generator 特性實現洋葱模型中間件,异步處理不再依賴 callback(主要靠 co 模塊實現)

异步的代碼書寫起來更像是同步代碼了,但使用 generator 中間件編寫風格現在看起來會感覺怪怪的

app.use(function *(next) {
  const startTime = new Date();
  yield next;
  const duration = new Date - startTime;
  console.log('X-Response-Time: ', duration + 'ms');
})

Koa 2.x

理念和 Koa 1.x 一致,不過推薦异步處理變成了 async/await,其中間件實現也就是前面介紹過的 koa-compose,中間件編寫風格好理解了很多。

app.use(async (ctx, next) => {
  ctx; // is the Context
  ctx.request; // is a koa Request
  ctx.response; // is a koa Response
});

koa 2.x 也把之前使用 this 獲取請求、響應等對象修改為了使用 ctx 對象。

接下來的內容,主要就是基於Koa 2.x來進行介紹的。有些人在使用的過程中,可能會因為某些包的使用出現為,這可能是因為版本選擇原因。

新建一個Koa的基礎項目

對於這一步,其實相對來說比較簡單,就是按照官方給出的步驟,依樣畫葫蘆就行了。首先你得有Node環境(這都2022年了,你得Node不會還停留在7.6以下吧,如果這樣,建議你昇級,否則後面會有很多的意想不到的問題)。然後選擇一個自己喜歡的Node包管理工具,npm,yarn,pnpm,cnpm,tnpm等等都行,按照自己熟練的步驟,進行初始化一個項目,然後安裝koa,新建一個名為index.js的文件,然後使用你喜愛的編輯器在文件中輸入以下內容(反正我這邊使用的vscode)。

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

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

然後使用控制臺,進入該目錄下面,直接運行:

node ./index.js

不出意外的話(有一種情况就是你本地的3000端口被占用了),項目就啟動了,然後打開瀏覽器,輸入:http://localhost:3000,即可在瀏覽器中看到有個Hello World的字樣。就像下面這樣:

這樣就說明你已經使用了Koa創建了項目了,並且該項目能够正常的運行起來了。

中間件的概念

上面說到,Koa自身是沒有綁定任何的中間件的。這裏就會有人不清楚中間件到底是個什麼鬼。從字面的理解上,就是在某個過程中間所存在工具。那這個過程對於Koa來說,這個過程就是一次http通信的過程,即Request和Response之間,那麼這裏的中間件,主要是用來處理請求數據和響應數據的。

請求報文是由請求方法、請求URI、協議版本、可選的請求首部字段和內容實體構成的。

響應報文基本上由協議版本、狀態碼(錶示請求成功或失敗的數字代碼)、用以解釋狀態碼的原因短語、可選的響應首部字段以及實體主體構成。

凡是處理請求報文和響應報文的,都可以被稱之為中間件,我們可以使用中間件來做什麼:

  • URI處理(也就是所謂的路由處理)
  • 重置HTTP請求路由
  • 統一安全限制、信息上報
  • Header操作、http請求認證
  • 屏蔽爬蟲
  • 提供調試信息
  • 請求日志記錄
  • 對相應結果進行處理(視圖渲染,格式化響應數據)
  • ……

Koa官方也給收集了不少的中間件(Home · koajs/koa Wiki · GitHub),我們常用的中間件主要包括有:

  • 路由的處理:koa-router
  • 靜態資源處理:koa-static
  • cookie,session處理:koa-session
  • 日志處理:koa-logger
  • ……

更多的中間件,可以在自己的平時開發的時候,根據業務的需要,進行查找和自己編寫(其實不要把這個東西看的很高大上,其實只要你明白這中間的一切,你就會發現,原來不過如此)。

中間件的使用

上面聊了中間件的概念了,但是到現在還是對Koa的中間件一竅不通,可能還是不會使用。但是我們從上面的概念中,可以看出來,其實Koa中間件一定是一個函數。呢麼對於Koa來說應該怎麼去使用中間件呢?

  • app.use()

這個方法將給定的中間件方法添加到應用程序中, 並且這個方法會返回 this,因此 這個use是能够進行鏈式調用的。

像我們的的Demo中使用的use,本質上也是在使用了一個Koa的中間件。

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

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

Koa上下文(ctx)

從上面的Demo中,我們會看到,在中間件中,會有一個參數ctx,這個參數呢,我們稱之為上下文。如果之前了解過express的同學,應該知道我們在使用express定義一個中間建的時候會這樣去寫:

app.get('/', (req, res, next) => {
  res.send('Hello World!')
})

我們對比會發現,相對於Express來說,Koa使用了一個叫做Context的對象,將Node中的requestresponse 對象封裝在了一起。為編寫 Web 應用程序和 API 提供了許多有用的方法。 這些操作在 HTTP 服務器開發中頻繁使用,它們被添加到此級別而不是更高級別的框架,這將强制中間件重新實現此通用功能。

其實Context中不僅僅只有request和response兩個對象,還有其他很多的對象(可以去官網),當我們在使用一些中間件的時候,也會給ctx注入一些屬性或者是方法之類的東西。下面列舉一些我們常用的一些屬性和方法:

  • ctx.request: koa 封裝的 request 對象,中間件應該盡量使用
  • ctx.req:Node.js 原生的 request 對象
  • ctx.response:koa 封裝的 response 對象,中間件應該盡量使用
  • ctx.res:Node.js 原生的 response 對象
  • ctx.state:koa 推薦的命名空間,用於通過中間件傳遞信息到前端視圖
  • ctx.app:對應用實例 app 的引用
  • ctx.cookies:cookie 操作對象
  • ctx.throw:通過 http status 拋出錯誤,讓 koa 可以正確處理

Koa的級聯(洋葱模型)

有關Koa的級聯,說白了就是我們常說的Koa的洋葱模型:

先來通過一個例子來看一下這個級聯的效果吧:

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

app.use(async (ctx, next) => {
  console.log(' 中間件 1開始');
  await next();
  console.log('中間件 1結束');
  // ctx.body = JSON.stringify(ctx);
});

app.use(async (ctx, next) => {
  console.log(' 中間件 2開始');
  await next();
  console.log('中間件 2結束');
})

app.listen(3000);

我們在運行後,訪問頁面,能够在運行的控制臺中,看到如下的內容輸出:

也就是說,在中間件1執行完成next之後,到中間件2中,中間件2執行完next之後,輸出中間件2中的結束內容,然後再返回到中間件1中,進行後續的操作。這樣的整個過程,就像洋葱一樣,一層包裹一層,因此被稱為洋葱模型。

洋葱模型的好處

按照正常的操作邏輯,就是事情不是需要一件一件的做麼,所以,只需要保持順序的執行過程就行了,但是為對於Koa來說,卻以next函數的執行完成作為分界線,要在執行完成之後又回到了當前這個中間件中。

Koa之所以這麼設計肯定是有他的用途,以及使用場景的,也就是說,這樣的設計肯定是為了解决一些平時常見但是又不太容易去處理的東西。

舉個例子,如果我們想要統計這次請求的在執行過重用了多長的時間,如果沒有Koa模型,我們實現起來會很複雜,並且還會再執行過程中注入一些內容,然後在執行完成之後,再將注入的內容給卸載掉,這樣的一個過程可能會來一些不可預知的問題,比如說在請求過程中會污染請求對象或者是參數等、在不能够准確的判斷我們應該在什麼時候去卸載我們注入的內容等……

當然還有別的一些問題,因此,在Koa中引入了洋葱模型,對於解决這樣的問題,相對來說算是一種比較好的解决思路和方案。

洋葱模型的實現方式

從上面有關Koa的洋葱模型的效果,我們大致了解了有關洋葱模型的執行過程,但是這裏想聊一句,如果讓你去實現一個洋葱模型你會怎麼去實現呢。下面是我的一個實現思路:

class App {
  constructor () {
    this.middleware = [];
    this.ctx = {};
  }

  use(asynFun) {
    this.middleware.push(asynFun);
  }

  async run(ctx, next) {
    const middleware = this.middleware;
    const that = this;
    let index = -1;
    dispatch(0);
    function dispatch(index) {
      let fun;
      if (index === that.middleware.length) {
        fun = next;
      } else {
        fun = that.middleware[index];
      }
      if (!fun) {
        return Promise.resolve();
      }
      try {
        return fun(ctx, async () => {
          await dispatch(index + 1);
        })
      } catch (err) {
        return Promise.reject(err)
      }
    } 
  }
}


const app = new App();
app.use(async (ctx, next) => {
  console.log('middleware 1 start');
  await next();
  console.log('middleware 1 end');
})
app.use(async (ctx, next) => {
  console.log('middleware 2 start');
  await next();
  console.log('middleware 2 end');
})

// app.dispatch(0);
app.run({}, async () => {
  console.log('run ……');
})

如果在控制臺中進行執行的話,則執行的結果如下所示:

 

這個結果是不是很熟悉,沒錯,這個就是Koa的核心的級聯的實現的基礎版。接下來我們打開Koa的源碼,能够看到有以下的內容:

  1. use方法:維護得到 middleware 中間件數組
use (fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
    debug('use %s', fn._name || fn.name || '-')
    this.middleware.push(fn)
    return this
}

2. listen方法:Node.js 原生 http 模塊 createServer 方法創建了一個服務。

listen (...args) {
    debug('listen')
    // 創建一個服務
    const server = http.createServer(this.callback())
    return server.listen(...args)
}

3. 如果我們繼續往深處看,會看到Koa引入了一個叫做koa-compose的模塊,然後我們打開koa-compose這個庫,能够看到他的底層實現。

function compose(middleware) {
  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)

    function dispatch(i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function next() {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }

是不是很熟悉,沒錯,這個方法確實是和我們的自己實現的結果是一樣的,如果我們將上述的結果進行分裝,也就是將Http的listen,監聽有請求過來之後,就執行我們的run方法,然後在對context加以封裝,實現一些Koa的基礎方法,是不是就會覺得,其實Koa也不過如此。如果有時間或者說有機會,我們也能够實現一個簡易版的Koa。

小結

經過以上的分析,使我們對Koa有一個簡單的認識,並且能够使用Koa的來創建項目,而且對Koa中的中間件和洋葱模型有一個簡單的認識。其實我們會發現,很多Koa這個高大上的東西背後,其實也不過如此。

版權聲明
本文為[水念]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/133/202205130428578249.html

隨機推薦