Koa框架學習

Free Joe發表於2020-12-28

Koa 是一個新的 web 框架,由 Express 幕後的原班人馬打造, 致力於成為 web 應用和 API 開發領域中的一個更小、更富有表現力、更健壯的基石。 通過利用 async 函式,Koa 幫你丟棄回撥函式,並有力地增強錯誤處理。 Koa 並沒有捆綁任何中介軟體, 而是提供了一套優雅的方法,幫助您快速而愉快地編寫服務端應用程式。

以上是Koa官網對Koa框架的描述。

1️⃣概述

1.1❀ Koa簡介

  • Koa核心模組並未捆綁任何中介軟體(路由功能也需要引入別的中介軟體)【方便使用者的擴充】
  • Koa使用了 Promise、async/await 語法來進行非同步程式設計(Express 是基於事件和回撥的)【避免地獄回撥】
  • Koa增強了對錯誤的處理
  • Koa開發的web應用體積更小,功能更強大

可見, Koa框架和 Expres框架的主要差別在於非同步程式設計和中介軟體方面,其他特性是相似的。

由於Koa進行非同步呼叫時強制使用async/await,因此需要將非同步回撥方法轉換為Promise,為了避免每個回撥方法都需要自己包裝,接下來將介紹這一問題目前最好的解決方案-Bluebird.

1.2❀ Bluebird

Bluebird 是Node.js最出名的 Promise 實現,除了實現標準的Promise規範之外,Bluebird還提供了包裝方法,可以快速地將Node.js回撥風格的函式包裝為Promise。

安裝:

npm install bluebird --save

將Node.js 回撥風格的函式包裝為Promise函式,該方法簽名如下:

bluebird.promisifyAll(target[,options])

target 需要包裝的物件。

  • target為普通物件,則包裝後生成的非同步API只有該物件持有
  • target為原型物件,則包裝後生成的非同步API被該原型所有例項持有。

options

  • suffix:設定非同步API方法名字尾,預設為“Async”
  • multiArgs:是否允許多個回撥引數,預設false。Promise的then()方法只接受一個resolve類引數(reject類也可以接受一個),但Node.js的回撥函式function(err,...data)卻可以接受多個引數。multiArgs為true時,bluebird將回撥函式的所有引數組裝成一個陣列,然後傳遞給then,從而得到多個引數。

bluebird.promisifyAll()只會給目標物件新增新方法,原來的 Node.js 回撥風格的方法不受影響。

包裝之後的方法和包裝之前的方法使用起來只有一個差別,那就是不要傳遞迴調函式,通過 Promise 獲取結果。


??以下是包裝fs物件的例項??
const fs = require("fs");
const bluebird = require("bluebird");

bluebird.promisifyAll(fs);

//回撥函式示例
fs.readFile("./package.json", { encoding: "utf-8" }, (err, data) => {
  if (err) {
    console.warn("讀取異常", err);
    return;
  }
  console.log(data);
});

//Prmise示例
fs.readFileAsync("./package.json", { encoding: "utf-8" })
  .then((data) => {
    console.log(data);
  })
  .catch((err) => {
    console.warn("讀取異常", err);
  });

2️⃣Hello Koa

1.初始化專案

npm init -y

2.模組安裝

npm i webpack koa --save

3.編碼

const Koa = require("koa");

const app = new Koa();//不同於express Koa是new例項

app.use(async (context) => {
  context.body = "Hello Koa";
});

app.listen(8080, () => {
  console.log("listen on 8080");
});

4.執行

node .\app.js   

在這裡插入圖片描述

Koa核心模組不繫結其它中介軟體,例子沒有使用到路由,而是使用了中介軟體,無論任何對該服務的請求都只會返回Hello Koa

3️⃣Context

Koa Context包含:Koa Request、Koa Response和應用例項(app)等

Koa Request 物件是在 node 的 原生請求物件之上的抽象
Koa Response 物件是在 node 的原生響應物件之上的抽象

注意幾個容易混淆API:

ctx.req:Node 的 request 物件.
ctx.res:Node 的 response 物件.

  • 繞過 Koa 的 response 處理是 不被支援的. 應避免使用以下 node 屬性:
    res.statusCode
    res.writeHead()
    res.write()
    res.end()

ctx.request:koa 的 Request 物件.
ctx.response:koa 的 Response 物件.

3.1❀ Request 和 Response 的別名

一般不直接通過ctx.request.[propName]/ctx.response.[propName]呼叫Koa Request/Response物件屬性,而是通過對應的別名來呼叫其屬性,比如:ctx.headers就是ctx.request.headers的別名,就是簡寫一層呼叫。

那為什麼ctx.headersctx.request.headers,而不是ctx.response.headers呢?

以下是我從官網目前版本擷取的別名欄位:?官網Koa 別名
在這裡插入圖片描述
可以看到,ctx.[propName]在Request和Response別名裡,沒有重複的propName。說明Koa對Context維護了一個唯一鍵名指向對應Koa Request和Response物件屬性名的資料結構。

ctx.headers==ctx.request.headers
ctx.body==ctx.response.body

假如想要Koa Response物件的headers怎麼辦?

那就不使用別名,直接讀取即可 ctx.response.headers

使用別名,這不是很容易混淆嗎?

我也是這麼覺得的!估計初學者都是如此。但存在肯定有它的意義,別名歸屬(Request/Response)的劃分,可能更傾向於該屬性在伺服器中的使用頻度。以headers為例,雖然請求物件和響應物件都可以用到這個屬性,但是對於伺服器而言,人們可能更關注的是請求頭的資訊而非響應頭的資訊。但對於熟悉Koa的人來說,通過別名,簡寫也是一種效率的提升。這是我個人的一點看法。

3.2❀ Context常用方法和屬性

只是羅列Conetxt常用的方法和屬性,想看更全面的和具體用法?官網Context

  • ctx.request:Koa的請求物件,一般不直接使用,通過別名引用來訪問。

  • ctx.response:Koa的響應物件,一般不直接使用,通過別名引用來訪問。

  • ctx.state:自定義資料儲存,比如中介軟體需要往請求中掛載變數就可以存放在ctx.state中,後續中介軟體可以讀取。
    前面的中介軟體 ctx.state.username=‘xx’ ,執行next()後,後面的中介軟體可以通過ctx.state.username獲取到xx

  • ctx.throw():丟擲 HTTP異常。

  • ctx.headers:請求報頭,ctx.request.headers的別名。

  • ctx.method:請求方法,ctx.request.method的別名。

  • ctx.url:請求連結,ctx.request.url的別名。

  • ctx.path:請求路徑,ctx.request.path的別名。

  • ctx.query:解析後的GET引數物件,ctx.request.query的別名。

  • ctx.host:當前域名,ctx.request.host的別名。

  • ctx.ip:客戶端IP,ctx.request.ip的別名。

  • ctx.ips:反向代理環境下的客戶端IP列表,ctx.request.ips的別名。

  • ctx.get():讀取請求報頭,ctx.request.get的別名。

  • ctx.body:響應內容,支援字串、物件、Buffer,ctx.response.body的別名。

  • ctx.status:響應狀態碼,ctx.response.status的別名。

  • ctx.type:響應體型別,ctx.response.type 的別名。

  • ctx.redirect():重定向,ctx.response.redirect的別名。

  • ctx.set():設定響應報頭,ctx.response.set的別名。


??顯示當前請求頭資訊並新增自定義報文頭??
const Koa = require("koa");

const app = new Koa();

app.use(async (ctx) => {
  ctx.set("customer-header", "nothing");
  ctx.body = {
    method: ctx.method,
    path: ctx.path,
    url: ctx.url,
    query: ctx.query,
    headers: ctx.headers,
    respHeaders: ctx.response.headers,
  };
});

app.listen(8080, () => {
  console.log("listen on 8080");
});

在這裡插入圖片描述

4️⃣Cookie

Cookie是Web應用維持少量資料的一種手段,常通過Cookie來維持服務端與客戶端使用者的身份認證。

4.1❀ Cookie 簽名

由於 Cookie 存放在瀏覽器端,存在篡改風險,因此Web應用一般會在存放cookie資料的時候同時存放一個簽名 cookie,以保證 Cookie 內容不被篡改。配置了cookie簽名,一旦cookie被改動,那麼該cookie直接會被瀏覽器移除。

Koa中需要配置 Cookie 簽名金鑰才能使用 Cookie功能,否則將報錯。

app.keys=['signedkey'];//這是自定義金鑰,你可以寫任何字串,推薦使用隨機字串,開啟簽名後會根據金鑰使用加密演算法加密該值

4.2❀ 寫入Cookie

const Koa = require("koa");
const app = new Koa();
app.keys = ["signedkey"];
app.use(async (ctx) => {
  ctx.cookies.set("logged", 1, {
    signed: true,//啟用cookie簽名
    httpOnly: true,
    maxAge: 3600 * 10,
  });
  ctx.body = "ok";
});

app.listen(8080);

cookie中不僅存在logged還有logged.sid,這個logged.sid就繫結的簽名。伺服器讀取logged的時候還會讀取logged.sid,一旦發現二者不匹配,設定cookie未undefined.
在這裡插入圖片描述

4.3❀ 讀取Cookie

const Koa = require("koa");
const app = new Koa();
app.keys = ["signedkey"];
app.use(async (ctx) => {
    const logged=ctx.cookies.get("logged",  {
      signed: true,
    });
    ctx.body = logged;
  });
app.listen(8080);

5️⃣中介軟體

5.1❀ 概念

類似Express中介軟體,可以訪問請求物件、響應物件 和 next函式。只不過Koa的中介軟體通過操作Context物件獲取請求物件、響應物件。

如果一個請求流程中,任何中介軟體都沒有輸出響應,Koa 中此次請求將返回404狀態碼(在Express中會將請求掛起直至超時)。

造成這種差別的原因是Express 需要手動執行輸出函式才可以結束請求流程,而Koa 使用了async/await來進行非同步程式設計,不需要執行回撥函式,直接對ctx.body賦值即可(如果連body都沒有,相當於資源不存在,自然404)。

Koa的中介軟體是一個標準的非同步函式,函式簽名如下:

async function middleware(ctx, next) //next函式:指向下一個中介軟體。

執行完邏輯程式碼,將需要傳遞的資料掛載到ctx.state,並且呼叫 await next()才能將請求交給下一個中介軟體處理。

5.2❀ 洋蔥模型 (中介軟體執行流程)

在這裡插入圖片描述
Koa的中介軟體模型稱為“洋蔥圈模型”,請求從左邊進入,有序地經過中介軟體處理,最終從右邊輸出響應。

最先use的中介軟體在最外層,最後use的中介軟體在最內層。

一般的中介軟體會執行兩次,呼叫next之前為第一次,也就是“洋蔥左半邊”這一部分,從外層向內層依次執行。當後續沒有中介軟體時,就進入響應流程,也就是“洋蔥右半邊”這一部分,從內層向外層依次執行,這是第二次執行。


??洋蔥圈模型??
const Koa = require("koa");
const app = new Koa();

async function middleware1(ctx,next){
    console.log('m1 start');
    await next();
    console.log('m1 end');
}

async function middleware2(ctx,next){
    console.log('m2 start');
    await next();
    console.log('m2 end');
}

app.use(middleware1);
app.use(middleware2);

app.use(async (ctx)=>{
    console.log('我是路由,我後面沒有中介軟體了');
    ctx.body='洋蔥圈模型';
})

app.listen(8080);

在這裡插入圖片描述
類似棧幀,多了路由。

6️⃣錯誤處理

Koa採用了洋蔥圈模型,所以Koa的錯誤處理中介軟體需要在應用的開始處掛載,這樣才能將整個請求-響應週期涵蓋,捕獲其發生的錯誤。而Express錯誤處理中介軟體需要放置在應用末尾。


??多個錯誤處理器(先處理後記錄)??
const Koa = require("koa");
const app = new Koa();

async function errorHandler(ctx,next){
    try{
        await next();
    }catch(e){
        ctx.status=e.status||500;
        ctx.body='Error:'+e.message;
    }
}

async function errorLogger(ctx,next){
    try{
        await next();
    }catch(e){
       console.log(`${ctx.method} ${ctx.path} Error:${e.message}`);
        throw e;//繼續往外丟擲錯與,被errorHandler接收處理
    }
}

app.use(errorHandler);
app.use(errorLogger);

app.use((ctx)=>{
    ctx.throw(403,'Forbidden');
})

app.listen(8080);

在這裡插入圖片描述

在這裡插入圖片描述

7️⃣路由模組

7.1❀ 預設路由函式

Koa核心並沒有提供路由功能,但是可以使用一個預設的路由函式來提供響應。所有的請求都會執行該預設的路由函式。

路由函式的定義如下:

async function(ctx,next)

如果路由函式內部未使用非同步邏輯,async是可以省略的。一個路由可以有多個處理函式。

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

app.use(async (ctx,next)=>{//預設路由函式1
    ctx.body=1;
    next();//沒有next就不會執行下一個路由函式了
});
app.use((ctx)=>{//預設路由函式2
    ctx.body=2;
});
app.listen(8080,()=>{
    console.log('listen on 8080');
})

這個並不算真正路由功能,下面介紹koa-router.

7.2❀ Hello koa-router

npm i koa-router --save
const Koa = require("koa");
const Router=require('koa-router');
const app = new Koa();
const router=new Router();

router.get('/',async (ctx)=>{
    ctx.body='root page';
})

router.post('/user/:userId(\\d+)',ctx=>{//限定路由引數為數值
    ctx.body=ctx.path;
})

app.use(router.routes());
app.use(router.allowedMethods());

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

allowedMethods是當所有路由中介軟體執行完成之後,若ctx.status為空或者404的時候,豐富response物件的header頭。當然,如果我們不設定router.allowedMethods()在表現上除了ctx.status不會自動設定,以及response header中不會加上Allow之外,不會造成其他影響.
在這裡插入圖片描述

7.3❀ 路由物件

路由需要例項化後才能配置和掛載。

路由構造器:

function Router([options]) //常用的option有 prefix,指定路由字首

路由定義方法:

router.method(path,handler);//支援多個handler

7.4❀ 路由函式

Koa-router的路由函式與預設路由函式相似,支援多個路由函式處理同一個請求。

router.get(
  "/",
  async (ctx, next) => {
    ctx.state.data = "root page";
    await next();
  },
  (ctx) => {
    ctx.body = ctx.state.data;
  }
);

7.5❀ 路由級別中介軟體

Koa預設的中介軟體是應用級別的,所有請求都被中介軟體處理。因為Koa-router支援多個路由函式,因此可以在指定路由或者整個路由物件上(常應用於模組化路由)使用中介軟體。

async function logger(ctx,next){
    console.log(`${ctx.method} ${ctx.path} `);
    await next();
}
router.get('/',logger,ctx=>{
	ctx.body='hello';
})

7.6❀ 模組化路由

將路由拆分在對應業務模組,方便維護!

  • 獨立檔案實現路由邏輯
  • 入口檔案掛載路由

user.js

const Router = require("koa-router");
const router = new Router({prefix:'/user'});

router.get('/',ctx=>{
    ctx.body='user Page';
})

router.post('/login',async (ctx)=>{
    ctx.body='login';
})

module.exports=router;

sites.js

const Router = require("koa-router");
const router = new Router();

router.get('/',ctx=>{
    ctx.body='index Page';
})

router.get('/about',ctx=>{
    ctx.body='about Page';
})

module.exports=router;

app.js

const Koa = require("koa");
const Router = require("koa-router");
const app = new Koa();
const user=require('./user');
const sites=require('./sites');

app.use(user.routes()).use(user.allowedMethods());
app.use(sites.routes()).use(sites.allowedMethods());

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

在這裡插入圖片描述

8️⃣模板渲染

支援很多模板,暫時使用ejs模板

8.1❀ koa-ejs

koe-ejs支援ctx.state,掛載到ctx.state中的變數可以直接在ejs模板中使用。

安裝koa-ejs模組

npm i koa-ejs --save 

templates/home.ejs

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><%= title  %> </title>
</head>
<body>
    <%= name  %> 
</body>
</html>

app.js

const Koa = require("koa");
const ejsRender =require('koa-ejs');
const app = new Koa();

ejsRender(app,{//使用ejs外掛
    root:'./templates',//模板目錄
    layout:false,//關閉模板佈局
    viewExt:'ejs'//使用ejs模板引擎
})

app.use(async (ctx)=>{
    ctx.state.title='首頁';
    await ctx.render('home',{//給home模板頁傳遞屬性
        name:'Joe'
    })
});

app.listen(8080, () => {
    console.log("listen on 8080");
  });

在這裡插入圖片描述

8.2❀ 模板佈局

Web頁面一般由一下結構組成:

  • 頭部區域(導航欄等)
  • 內容主體
  • 底部區域(版權宣告等)

一般而言,頭部和底部區域不變化,只有內容主體變化,那就以內容主體構建佈局!

templates/main.ejs

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>main 頁面</title>
</head>
<body>
    <header style="border: 1px solid black;">頁頭</header>
    <%- body %> 
    <footer style="border: 1px solid black;">頁尾</footer>
</body>
</html>

templates/home.ejs

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><%= title  %> </title>
</head>
<body>
    <%= name  %> 
</body>
</html>

app.js

const Koa = require("koa");
const ejsRender =require('koa-ejs');
const app = new Koa();

ejsRender(app,{//使用ejs外掛
    root:'./templates',//模板目錄
    layout:'main',//開啟模板佈局 以main模板作為佈局模板
    viewExt:'ejs'//使用ejs模板引擎
})

app.use(async (ctx)=>{
    ctx.state.title='主頁';
    await ctx.render('home',{//指定home模板作為子模板 之後父模板 <%- body %> body就是home模板的body節點 
        name:'Joe'
    })
});

app.listen(8080, () => {
    console.log("listen on 8080");
  });

在這裡插入圖片描述

相關文章