Koa2 入門教程

完整Demo地址

裏面demo都是自己寫的,保證能跑,至於環境問題我就不敢保證了。懶得寫就去上面搬走看,懶得搬就直接看文章,大部分代碼連輸出信息都給你們了。
koa-demo

官網介紹

koa 是由 Express 原班人馬打造的,致力於成爲一個更小、更富有表現力、更健壯的 Web 框架。 使用 koa 編寫 web 應用,通過組合不同的 generator,可以免除重複繁瑣的回調函數嵌套, 並極大地提升錯誤處理的效率。koa 不在內核方法中綁定任何中間件, 它僅僅提供了一個輕量優雅的函數庫,使得編寫 Web 應用變得得心應手。

前期準備

我們首先安裝一些必要庫先,根據個人選擇可以使用yarn,cnpm,或者npm都行。

KOA框架

yarn add koa

這還不夠,因爲 Koa 依賴 node v7.6.0 或 ES2015及更高版本和 async 方法支持.你們可以根據自身需要安裝

Babel register

transform-async-to-generator 或 transform-async-to-module-method

因爲我用到 Nodejs10.0,所以不需要安裝這些東西,就不說了。

簡單入門

慣例拿創建應用程序作爲一個框架的入門例子。

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

app
  .use(async ctx => {
    ctx.body = '暗號:Hello World';
  })
  .listen(3000);

console.log('已建立連接,效果請看http://127.0.0.1:3000/');

代碼一目瞭然,不廢話了。
(完整代碼可以執行koa-demo的 lesson1 查看效果)

Favicon.ico

所謂 favicon,即 Favorites Icon 的縮寫,顧名思義,便是其可以讓瀏覽器的收藏夾中除顯示相應的標題外,還以圖標的方式區別不同的網站。當然,這不是Favicon的全部,根據瀏覽器的不同,Favicon顯示也有所區別:在大多數主流瀏覽器如FireFox和Internet Explorer (5.5及以上版本)中,favicon不僅在收藏夾中顯示,還會同時出現在地址欄上,這時用戶可以拖曳favicon到桌面以建立到網站的快捷方式;除此之外,標籤式瀏覽器甚至還有不少擴展的功能,如FireFox甚至支持動畫格式的favicon等。
問題在於這裡代碼瀏覽器會自動發起請求網站根目錄的這個圖標,干擾測試,所以接下來的打印結果大家無視Favicon.ico請求就好。

級聯

Koa 應用程序是一個包含一組中間件函數的對象,它是按照類似堆棧的方式組織和執行的。
當一箇中間件調用 next() 則該函數暫停並將控制傳遞給定義的下一個中間件。當在下游沒有更多的中間件執行後,堆棧將展開並且每個中間件恢復執行其上游行爲。(用一種比較相似的比喻就是中間件相當於一次DOM事件,從事件捕捉到事件冒泡的過程)。

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

// 一層中間件
app.use((ctx, next) => {
  console.log('請求資源:' + ctx.url);
  console.log('一層中間件控制傳遞下去');
  next();
  console.log('一層中間件控制傳遞回來');
  ctx.body = '暗號:Day Day Up';
});

// 二層中間件
app.use((ctx, next) => {
  console.log('二層中間件控制傳遞下去');
  next();
  console.log('二層中間件控制傳遞回來');
});

// response
app.use(ctx => {
  console.log('輸出body');
  ctx.body = '暗號:Good Good Study';
});

app.listen(3000);
console.log('已建立連接,效果請看http://127.0.0.1:3000/');

// 一層中間件控制傳遞下去
// 二層中間件控制傳遞下去
// 輸出body
// 二層中間件控制傳遞回來
// 一層中間件控制傳遞回來

(完整代碼可以執行koa-demo的 lesson2 查看效果)

從上面結果可以看出每請求一次資源都會經過所有中間件,並且在執行到最尾部中間件時候會將控制權反向傳遞,輸出結果是頭部的body覆蓋尾部的body。
說實話沒試過這種方式,有點不習慣。

上下文(Context)

Koa Context 將 Nodejs 的 requestresponse 對象封裝到單個對象中,每個請求都將創建一個 Context,並在中間件中作爲接收器引用,或者 ctx 標識符,許多上下文的訪問器和方法直接委託給它們的 ctx.requestctx.response
我們可以直接打印出來ctx對象看看有什麼。

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

// response
app.use(async ctx => {
  console.log('ctx:', ctx);
});

app.listen(3000);
console.log('已建立連接,效果請看http://127.0.0.1:3000/');

/*
ctx: { request:
   { method: 'GET',
     url: '/',
     header:
      { host: 'localhost:3000',
        connection: 'keep-alive',
        'cache-control': 'max-age=0',
        'upgrade-insecure-requests': '1',
        'user-agent':
         'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.89 Safari/537.36',
        accept:
         'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*!/!*;q=0.8',
        'accept-encoding': 'gzip, deflate, sdch',
        'accept-language': 'zh-CN,zh;q=0.8',
        cookie:
         'loginInfo={"username":"abc","password":"MjIyMjIy","rememberMe":1}' } },
  response: { status: 404, message: 'Not Found', header: {} },
  app: { subdomainOffset: 2, proxy: false, env: 'development' },
  originalUrl: '/',
  req: '<original node req>',
  res: '<original node res>',
  socket: '<original node socket>' }*/

(完整代碼可以執行koa-demo的 lesson3 查看效果)

請求(Request)

Koa Request 對象是在 Nodejs 的 vanilla 請求對象之上的抽象,提供了諸多對 HTTP 服務器開發有用的功能。Koa的 Request 對象包括由 acceptsnegotiator 提供的有用的內容協商實體。

  • request.accepts(types)
  • request.acceptsEncodings(types)
  • request.acceptsCharsets(charsets)
  • request.acceptsLanguages(langs)
  1. 如果沒有提供類型,則返回所有可接受的類型;
  2. 如果提供多種類型,將返回最佳匹配;
  3. 如果沒有找到匹配項,則返回一個false;

因爲用法都一個樣,挑選一個來講解。

request.accepts(types)

檢查給定的類型是否可以接受,type 值可能是一個或多個 mime 類型的字符串或數組,如果可以就返回最佳匹配類型字符串,否則返回false。

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

app
  .use(async ctx => {
    switch (ctx.accepts('json', 'html', 'text')) {
      case 'json':
        ctx.type = 'json';
        ctx.body = '<p>匹配類型json</p>';
        break;
      case 'html':
        ctx.type = 'html';
        ctx.body = '<p>匹配類型html</p>';
        break;
      case 'text':
        ctx.type = 'text';
        ctx.body = '<p>匹配類型text</p>';
        break;
      default:
        ctx.throw(406, 'json, html, or text only');
    }
  })
  .listen(3000);
console.log('已建立連接,效果請看http://127.0.0.1:3000/');

(完整代碼可以執行koa-demo的 lesson4 查看效果)

實際開發中需要更多不同的處理細節,所以我們可以把內容設置成模板template1.html引用。

<!DOCTYPE html>
<html lang="en" dir="ltr">

  <head>
    <meta charset="utf-8">
    <title></title>
  </head>

  <body>
    <p>匹配類型html</p>
  </body>

</html>

(完整代碼可以執行koa-demo的 template1 查看效果)

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

app
  .use(async ctx => {
    switch (ctx.accepts('json', 'html', 'text')) {
      case 'html':
        ctx.type = 'html';
        ctx.body = fs.createReadStream('./template1.html');
        break;
      default:
        ctx.throw(406, 'json, html, or text only');
    }
  })
  .listen(3000);
console.log('已建立連接,效果請看http://127.0.0.1:3000/');

(完整代碼可以執行koa-demo的 lesson5 查看效果)

路由

其實我們上面的代碼已經是原始路由的用法了,我們增加請求資源的判斷就可以了,另外新增一個template2.html模板切換看效果

<!DOCTYPE html>
<html lang="en" dir="ltr">

  <head>
    <meta charset="utf-8">
    <title></title>
  </head>

  <body>
    <p>template2</p>
  </body>

</html>

(完整代碼可以執行koa-demo的 template2 查看效果)
然後我們去掉類型判斷等代碼看效果,直接寫死html即可,不然type默認爲空,打開頁面會觸發下載的,不信你們去掉設置類型那行代碼看看。

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

app
  .use(async ctx => {
    console.log('type: ' + ctx.type);
    switch (ctx.url) {
      case '/':
        ctx.type = 'html';
        ctx.body = fs.createReadStream('./template1.html');
        break;
      case '/template2':
        ctx.type = 'html';
        ctx.body = fs.createReadStream('./template2.html');
        break;
      default:
        ctx.throw(406, 'json, html, or text only');
    }
  })
  .listen(3000);
console.log('已建立連接,效果請看http://127.0.0.1:3000/');

(完整代碼可以執行koa-demo的 lesson6 查看效果)
執行腳本之後會默認看到template2.html模板內容,手動換成http://127.0.0.1:3000/template2如果沒設置type在Chrome會看到下載彈窗,其他瀏覽器沒試過。

實際開發我們不會這麼繁瑣的去區分路由,上面說過 koa 不在內核方法中綁定任何中間件,它僅僅提供了一個輕量優雅的函數庫,所以我們需要安裝一個路由中間件。
koa-route3.2.0 ,上次推送已經是兩年前了,如果不是放棄維護就是已經很穩定了。

yarn add koa-route

如果你需要使用完整特性的路由庫可以看 koa-router
這裏簡單展示 koa-route 用法。

const Koa = require('koa'),
  _ = require('koa-route'),
  fs = require('fs'),
  app = new Koa();

const route = {
  index: ctx => {
    //doSomethings
    ctx.type = 'html';
    ctx.body = fs.createReadStream('./template1.html');
  },
  template2: ctx => {
    //doSomethings
    ctx.type = 'html';
    ctx.body = fs.createReadStream('./template2.html');
  },
};

app
  .use(_.get('/', route.index))
  .use(_.get('/template2', route.template2))
  .listen(3000);
console.log('已建立連接,效果請看http://127.0.0.1:3000/');

(完整代碼可以執行koa-demo的 lesson7 查看效果)

響應狀態

如果代碼運行過程中發生錯誤,我們需要把錯誤信息返回給用戶。

ctx.throw([status], [msg], [properties])

等價於

const err = new Error(msg);
err.status = status;
err.expose = true;
throw err;

注意:這些是用戶級錯誤,並用 err.expose 標記,這意味着消息適用於客戶端響應,這通常不是錯誤消息的內容,因爲您不想泄漏故障詳細信息。

100 "continue"
101 "switching protocols"
102 "processing"
200 "ok"
201 "created"
202 "accepted"
203 "non-authoritative information"
204 "no content"
205 "reset content"
206 "partial content"
207 "multi-status"
208 "already reported"
226 "im used"
300 "multiple choices"
301 "moved permanently"
302 "found"
303 "see other"
304 "not modified"
305 "use proxy"
307 "temporary redirect"
308 "permanent redirect"
400 "bad request"
401 "unauthorized"
402 "payment required"
403 "forbidden"
404 "not found"
405 "method not allowed"
406 "not acceptable"
407 "proxy authentication required"
408 "request timeout"
409 "conflict"
410 "gone"
411 "length required"
412 "precondition failed"
413 "payload too large"
414 "uri too long"
415 "unsupported media type"
416 "range not satisfiable"
417 "expectation failed"
418 "I'm a teapot"
422 "unprocessable entity"
423 "locked"
424 "failed dependency"
426 "upgrade required"
428 "precondition required"
429 "too many requests"
431 "request header fields too large"
500 "internal server error"
501 "not implemented"
502 "bad gateway"
503 "service unavailable"
504 "gateway timeout"
505 "http version not supported"
506 "variant also negotiates"
507 "insufficient storage"
508 "loop detected"
510 "not extended"
511 "network authentication required"

狀態碼錯誤

有兩種寫法,ctx.throw(狀態碼) 或者 ctx.status = 狀態碼 ,它們都會自動返回默認文字信息,區別在於兩者設置返回信息的方式。
注意:默認情況下,response.status 設置爲 404 而不是像 node 的 res.statusCode 那樣默認爲 200。

const Koa = require('koa'),
  _ = require('koa-route'),
  app = new Koa();

const router = {
  '403': ctx => {
    //doSomethings
    ctx.throw(403, '403啦!');
  },
  '404': ctx => {
    //doSomethings
    ctx.status = 404;
    ctx.body = `<p>404啦!</p>`;
  },
};

app
  .use(_.get('/403', router[403]))
  .use(_.get('/404', router[404]))
  .listen(3000);
console.log('已建立連接,效果請看http://127.0.0.1:3000/');

(完整代碼可以執行koa-demo的 lesson8 查看效果)
你們可以分別打開 http://localhost:3000/403http://localhost:3000/404 看輸出結果。

錯誤監聽

const Koa = require('koa'),
  _ = require('koa-route'),
  app = new Koa();

const router = {
  index: ctx => {
    //doSomethings
    ctx.throw(500, '我是故意的!');
  },
};

app
  .use(_.get('/', router.index))
  .on('error', (err, ctx) => {
    console.error('error', err);
  })
  .listen(3000);
console.log('已建立連接,效果請看http://127.0.0.1:3000/');
/*
error { InternalServerError: 我是故意的!
    at Object.throw (C:\project\test\koa-demo\node_modules\koa\lib\context.js:93:11)
    at Object.index (C:\project\test\koa-demo\lesson9.js:8:18)
    at C:\project\test\koa-demo\node_modules\koa-route\index.js:39:44
    at dispatch (C:\project\test\koa-demo\node_modules\koa-compose\index.js:42:32)
    at C:\project\test\koa-demo\node_modules\koa-compose\index.js:34:12
    at Application.handleRequest (C:\project\test\koa-demo\node_modules\koa\lib\application.js:150:12)
    at Server.handleRequest (C:\project\test\koa-demo\node_modules\koa\lib\application.js:132:19)
    at Server.emit (events.js:182:13)
    at parserOnIncoming (_http_server.js:654:12)
    at HTTPParser.parserOnHeadersComplete (_http_common.js:109:17) message: '我是故意的!' }*/

(完整代碼可以執行koa-demo的 lesson9 查看效果)

錯誤捕捉

你也能直接使用try...catch()直接處理,但是“error”監聽事件就不會再接收該錯誤信息。

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

const err = async (ctx, next) => {
    try {
      await next();
    } catch (err) {
      ctx.status = 404;
      ctx.body = `<p>你看看終端有沒打印錯誤!</p>`;
    }
  },
  index = ctx => {
    ctx.throw(500);
  };

app
  .use(err)
  .use(index)
  .on('error', function(err) {
    console.error('error', err);
  })
  .listen(3000);
console.log('已建立連接,效果請看http://127.0.0.1:3000/');

(完整代碼可以執行koa-demo的 lesson10 查看效果)
注意,這裡的錯誤處理如果使用ctx.throw()方法的話能被“error”事件監聽到,不是因為該方法會再拋出新的錯誤。

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

const err = async (ctx, next) => {
    try {
      await next();
    } catch (err) {
      ctx.throw(404, '你看看終端有沒打印錯誤!');
    }
  },
  index = ctx => {
    ctx.throw(500);
  };

app
  .use(err)
  .use(index)
  .on('error', function(err) {
    console.error('error', err);
  })
  .listen(3000);
console.log('已建立連接,效果請看http://127.0.0.1:3000/');

(完整代碼可以執行koa-demo的 lesson1 查看效果)1
如果想同時觸發錯誤監聽,KOA也提供了 emit 方法可以實現。

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

const err = async (ctx, next) => {
    try {
      await next();
    } catch (err) {
      ctx.status = 404;
      ctx.body = `<p>你看看終端有沒打印錯誤!</p>`;
      ctx.app.emit('error', err, ctx);
    }
  },
  index = ctx => {
    ctx.throw(500);
  };

app
  .use(err)
  .use(index)
  .on('error', function(err) {
    console.error('error', err);
  })
  .listen(3000);
console.log('已建立連接,效果請看http://127.0.0.1:3000/');

/*
error { InternalServerError: Internal Server Error
    at Object.throw (C:\project\test\koa-demo\node_modules\koa\lib\context.js:93:11)
    at index (C:\project\test\koa-demo\lesson12.js:14:18)
    at dispatch (C:\project\test\koa-demo\node_modules\koa-compose\index.js:42:32)
    at err (C:\project\test\koa-demo\lesson12.js:6:19)
    at dispatch (C:\project\test\koa-demo\node_modules\koa-compose\index.js:42:32)
    at C:\project\test\koa-demo\node_modules\koa-compose\index.js:34:12
    at Application.handleRequest (C:\project\test\koa-demo\node_modules\koa\lib\application.js:150:12)
    at Server.handleRequest (C:\project\test\koa-demo\node_modules\koa\lib\application.js:132:19)
    at Server.emit (events.js:182:13)
    at parserOnIncoming (_http_server.js:654:12) message: 'Internal Server Error' }
*/

(完整代碼可以執行koa-demo的 lesson11 查看效果)

靜態資源

聰明的人在上面代碼就能看出一些問題,還記得我們說過忽略 Favicon.ico 的請求麼。我們不僅僅有頁面的請求,還有其他資源的請求。
我們現在還是通過url判斷返回頁面,如果是其他靜態資源如圖片那些又怎麼辦?這裡介紹一下依賴庫koa-static5.0.0

yarn add koa-static
--------------------------
require('koa-static')(root, opts)

通過設置根目錄和可選項會配置靜態資源查找路徑,我們先創建一個img目錄存放一張圖片,然後在 template3.html 引用,再設置路徑 require('koa-static')(__dirname + '/img/'),它會自動到指定目錄下查找資源。

<!DOCTYPE html>
<html lang="en" dir="ltr">

  <head>
    <meta charset="utf-8">
    <title></title>
  </head>

  <body>
    <p>沒錯,我就是首頁</p>
    <img src="./1.gif"/>
  </body>

</html>

(完整代碼可以執行koa-demo的 template3 查看效果)

const Koa = require('koa'),
  _ = require('koa-route'),
  serve = require('koa-static')(__dirname + '/img/'),
  fs = require('fs'),
  app = new Koa();

const router = {
  index: ctx => {
    //doSomethings
    ctx.type = 'html';
    ctx.body = fs.createReadStream('./template3.html');
  },
};

app
  .use(serve)
  .use(_.get('/', router.index))
  .listen(3000);
console.log('已建立連接,效果請看http://127.0.0.1:3000/');

(完整代碼可以執行koa-demo的 lesson13 查看效果)
如果你還是有些不懂的話修改下路徑,require('koa-static')(__dirname),然後圖片地址換成 "./img/1.gif" 。你就看到還是能找到對應資源。

中間件管理

隨着項目開發你可能會安裝越來越多的中間件,所有可以使用koa-compose做中間件管理。這個很多庫都有類似的中間件,用於簡化中間件的使用。
上面我們用來講解 koa 級聯的那個例子可以直接拿來修改使用。

const Koa = require('koa'),
  compose = require('koa-compose'),
  app = new Koa();

// 一層中間
const mid1 = (ctx, next) => {
  console.log('請求資源:' + ctx.url);
  console.log('一層中間件控制傳遞下去');
  next();
  console.log('一層中間件控制傳遞回來');
};

// 二層中間
const mid2 = (ctx, next) => {
  console.log('二層中間件控制傳遞下去');
  next();
  console.log('二層中間件控制傳遞回來');
};

// response
const mid3 = ctx => {
  console.log('輸出body');
  ctx.body = '暗號:Hello World';
};

app.use(compose([mid1, mid2, mid3])).listen(3000);
console.log('已建立連接,效果請看http://127.0.0.1:3000/');
// 請求資源:/
// 一層中間件控制傳遞下去
// 二層中間件控制傳遞下去
// 輸出body
// 二層中間件控制傳遞回來
// 一層中間件控制傳遞回來

(完整代碼可以執行koa-demo的 lesson14 查看效果)
可以看出大概原理就是把引用多箇中間件的使用方式改成將多箇中間件組裝成一個使用。

請求處理

我們處理請求的時候可以用 koa-body 解析請求體。

A full-featured koa body parser middleware. Support multipart, urlencoded and json request bodies. Provides same functionality as Express's bodyParser - multer. And all that is wrapped only around co-body and formidable.

一個功能豐富的body解析中間件,支持多部分,urlencoded,json請求體,提供Express裏bodyParse一樣的函數方法

直接安裝依賴

yarn add koa-body

新建一個提交頁面

<!DOCTYPE html>
<html lang="en" dir="ltr">

  <head>
    <meta charset="utf-8">
    <title></title>
  </head>

  <body>
    <form class="" action="/upload" method="post">
      <input type="text" name="name" value="">
      <button type="submit" name="button">提交</button>
    </form>
  </body>

</html>

(完整代碼可以執行koa-demo的 template4 查看效果)

可以輸出格式看看效果

const Koa = require('koa'),
  koaBody = require('koa-body'),
  _ = require('koa-route'),
  fs = require('fs'),
  app = new Koa();

const router = {
    index: ctx => {
      //doSomethings
      ctx.type = 'html';
      ctx.body = fs.createReadStream('./template4.html');
    },
  },
  upload = ctx => {
    console.log(ctx.request.body);
    ctx.body = `Request Body: ${JSON.stringify(ctx.request.body)}`;
  };

app
  .use(koaBody())
  .use(_.get('/', router.index))
  .use(_.post('/upload', upload))
  .listen(3000);
console.log('已建立連接,效果請看http://127.0.0.1:3000/');

(完整代碼可以執行koa-demo的 lesson15 查看效果)
提交內容之後在頁面和終端都能看到body內容。

參考資料

Koa (koajs)
Koa examples
Koa 框架教程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章