關於koa2,你不知道的事

引言

什麼是 koahtml

koa 是一個基於 node 實現的一個新的 web 框架,它是由 express 框架的原班人馬打造。特色是優雅、簡潔、表達力強、自由度高。和 express 相比,它是一個更輕量的 node 框架,由於它全部的功能都經過插件來實現,這種插拔式的架構設計模式,很符合 unix 哲學。前端

本文從零開始,按部就班的展現和詳解上手 koa2 框架的幾個最重要的概念,最後會串聯講解一下 koa2 的處理流程以及源碼結構。看完本文之後,相信不管對於上手 koa2 仍是深刻了解 koa2 都會有不小的幫助。node

快速開始

安裝並啓動(hello world)

按照正常邏輯,安裝使用這種通常都會去官網看一下相似guide的入門指引,卻不知 koa 官網和 koa 自己同樣簡潔(手動狗頭)。git

若是一步步搭建環境的話可能會比較麻煩,還好有項目生成器koa-generator(出自狼叔-桑世龍)。es6

// 安裝koa項目生成器koa-generator
$ npm i koa-generator -g

// 使用koa-generator生成koa2項目
$ koa2 hello_koa2

// 切到指定項目目錄,並安裝依賴
$ cd hello_koa2
$ npm install

// 啓動項目
$ npm start

項目啓動後,默認端口號是3000,在瀏覽器中運行能夠獲得下圖的效果說明運行成功。github

koa2 簡析結構

項目已經啓動起來了,下面讓咱們來簡單看一下源碼文件目錄結構吧:web

這個就是 koa2 源碼的源文件結構,核心代碼就是 lib 目錄下的四個文件:redis

application.js

application.js是 koa 的入口文件,它向外導出了建立 class 實例的構造函數,繼承自 node 自帶的events,這樣就會賦予框架事件監聽和事件觸發的能力。application 還暴露了一些經常使用的 api,好比listenuse等等。數據庫

listen的實現原理其實就是對http.createServer進行了一個封裝,這個函數中傳入的callback是核心,它裏面包含了中間件的合併,上下文的處理,對 res 的特殊處理。express

use 的做用主要是收集中間件,將多箇中間件放入一個緩存隊列中,而後經過koa-compose這個插件進行遞歸組合調用這一系列的中間件。

context.js

這部分就是 koa 的應用上下文 ctx,其實就一個簡單的對象暴露,裏面的重點在 delegate,這個就是代理,這個就是爲了開發者方便而設計的,好比咱們要訪問 ctx.repsponse.status 可是咱們經過 delegate,能夠直接訪問 ctx.status 訪問到它。

request.js、response.js

這兩部分就是對原生的resreq的一些操做了,大量使用 es6 的getset的一些語法,去取headers或者設置headers、還有設置body等等

路由(URL 處理)

原生路由實現

koa 是個極簡的 web 框架,簡單到連路由模塊都沒有配備,咱們先來能夠根據ctx.request.url或者ctx.request.path獲取用戶請求的路徑,來實現簡單的路由。

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

app.use( async ( ctx ) => {
  let url = ctx.request.url
  ctx.body = url
})
app.listen(3000)

訪問 http://localhost:3000/hello/forest 頁面會輸出 /hello/forest,也就是說上下文的請求request對象中url就是當前訪問的路徑名稱,能夠根據ctx.request.url 經過必定的判斷或者正則匹配就能夠定製出所須要的路由。

koa-router 中間件

若是依靠ctx.request.url去手動處理路由,將會寫不少處理代碼,這時候就須要對應的路由的中間件對路由進行控制,這裏介紹一個比較好用的路由中間件koa-router

安裝 koa-router 中間件

// koa2 對應的版本是 7.x
$ npm install --save koa-router@7

使用

const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

router.get('/', async (ctx) => {
  let html = `
      <ul>
        <li><a href="/hello">helloworld</a></li>
        <li><a href="/about">about</a></li>
      </ul>
    `
  ctx.body = html
}).get('/hello', async (ctx) => {
  ctx.body = 'hello forest'
}).get('/about', async (ctx) => {
  ctx.body = '前端森林'
})

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

app.listen(3000);

中間件

在上面說到路由時,咱們用到了中間件(koa-router)。那中間件到底是什麼呢?

Koa 的最大特點,也是最重要的一個設計,就是中間件(middleware)。Koa 應用程序是一個包含一組中間件函數的對象,它是按照相似堆棧的方式組織和執行的。

Koa 中使用app.use()來加載中間件,基本上 Koa 全部的功能都是經過中間件實現的。

每一箇中間件默認接受兩個參數,第一個參數是 Context 對象,第二個參數是next函數。只要調用 next 函數,就能夠把執行權轉交給下一個中間件。

下圖爲經典的 Koa 洋蔥模型:

咱們來看一下 koa 官網的這個例子:

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

// logger

app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

// x-response-time

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

// response

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

app.listen(3000);

上面的執行順序就是:請求 -> logger 中間件 -> x-response-time 中間件 -> 響應中間件 -> x-response-time 中間件 -> logger 中間件 -> 響應。

經過這個順序咱們能夠發現這是個棧結構以"先進後出"(first-in-last-out)的順序執行。

Koa 已經有了不少好用的中間件(https://github.com/koajs/koa/wiki#middleware)你須要的經常使用功能基本上上面都有。

請求數據獲取

get

獲取方法

在 koa 中,獲取GET請求數據源使用 koa 中 request 對象中的query方法或querystring方法。

query返回是格式化好的參數對象,querystring返回的是請求字符串,因爲 ctx 對 request 的 API 有直接引用的方式,因此獲取 GET 請求數據有兩個途徑。

  • 一、從上下文中直接獲取

    • 請求對象ctx.query,返回如 { name:'森林', age:23 }
    • 請求字符串 ctx.querystring,返回如 name=森林&age=23
  • 二、從上下文的 request 對象中獲取

    • 請求對象ctx.request.query,返回如 { a:1, b:2 }
    • 請求字符串 ctx.request.querystring,返回如 a=1&b=2

示例

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

app.use( async ( ctx ) => {
  let url = ctx.url
  // 從上下文的request對象中獲取
  let request = ctx.request
  let req_query = request.query
  let req_querystring = request.querystring

  // 從上下文中直接獲取
  let ctx_query = ctx.query
  let ctx_querystring = ctx.querystring

  ctx.body = {
    url,
    req_query,
    req_querystring,
    ctx_query,
    ctx_querystring
  }
})

app.listen(3000, () => {
  console.log('[demo] request get is starting at port 3000')
})

post

對於POST請求的處理,koa2 沒有封裝獲取參數的方法,須要經過本身解析上下文 context 中的原生 node.js 請求對象req,將 POST 表單數據解析成 querystring(例如:a=1&b=2&c=3),再將 querystring 解析成 JSON 格式(例如:{"a":"1", "b":"2", "c":"3"})。

咱們來直接使用koa-bodyparser 中間件從 POST 請求的數據體裏面提取鍵值對。

對於POST請求的處理,koa-bodyparser中間件能夠把 koa2 上下文的formData數據解析到ctx.request.body中。

示例

首先安裝koa-bodyparser

$ npm install --save koa-bodyparser@3

看一個簡單的示例:

const Koa = require('koa')
const app = new Koa()
const bodyParser = require('koa-bodyparser')

// 使用koa-bodyparser中間件
app.use(bodyParser())

app.use(async (ctx) => {

  if (ctx.url === '/' && ctx.method === 'GET') {
    // 當GET請求時候返回表單頁面
    let html = `
      <h1>koa2 request post demo</h1>
      <form method="POST" action="/">
        用戶名:<input name="name" /><br/>
        年齡:<input name="age" /><br/>
        郵箱: <input name="email" /><br/>
        <button type="submit">submit</button>
      </form>
    `
    ctx.body = html
  } else if (ctx.url === '/' && ctx.method === 'POST') {
    // 當POST請求的時候,中間件koa-bodyparser解析POST表單裏的數據,並展現到頁面
    ctx.body = ctx.request.body
  } else {
    // 404
    ctx.body = '<h1>404 Not Found</h1>'
  }
})

app.listen(3000, () => {
  console.log('[demo] request post is starting at port 3000')
})

模版引擎

在實際項目開發中,返回給用戶的網頁每每都會被寫成模板文件。 Koa 先讀取模板文件,而後將這個模板返回給用戶,這裏咱們就須要使用模板引擎了。

關於 Koa 的模版引擎,咱們只須要安裝 koa 模板使用中間件koa-views, 而後再下載你喜歡的模板引擎(支持列表)即可以愉快的使用了。

這裏以使用ejs模版爲例展開說明。

安裝模版

// 安裝koa模板使用中間件
$ npm install --save koa-views

// 安裝ejs模板引擎
$ npm install --save ejs

使用模版引擎

文件目錄

├── package.json
├── index.js
└── view
    └── index.ejs

./index.js 文件

const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const app = new Koa()

// 加載模板引擎
app.use(views(path.join(__dirname, './view'), {
  extension: 'ejs'
}))

app.use( async ( ctx ) => {
  let title = '森林帶你學koa2'
  await ctx.render('index', {
    title,
  })
})

app.listen(3000)

./view/index.ejs 模板

<!DOCTYPE html>
<html>
<head>
    <title><%= title %></title>
</head>
<body>
    <h1><%= title %></h1>
    <p>EJS Welcome to <%= title %></p>
</body>
</html>

靜態資源服務器

網站通常都提供靜態資源(圖片、字體、樣式表、腳本……),咱們能夠本身實現一個靜態資源服務器,但這不必,koa-static模塊封裝了這部分功能。

安裝

$ npm i --save koa-static

示例

const Koa = require('koa')
const path = require('path')
const static = require('koa-static')

const app = new Koa()

// 靜態資源目錄對於相對入口文件index.js的路徑
const staticPath = './static'

app.use(static(
  path.join( __dirname,  staticPath)
))


app.use( async ( ctx ) => {
  ctx.body = 'hello world'
})

app.listen(3000, () => {
  console.log('[demo] static-use-middleware is starting at port 3000')
})

cookie/session

koa2 中使用 cookie

使用方法

koa 提供了從上下文直接讀取、寫入 cookie 的方法:

  • ctx.cookies.get(name, [options]) 讀取上下文請求中的 cookie
  • ctx.cookies.set(name, value, [options]) 在上下文中寫入 cookie
    koa2 中操做的 cookies 是使用了 npm 的cookies模塊,源碼在這裏,因此在讀寫 cookie 時的使用參數與該模塊的使用一致。

示例

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

app.use( async ( ctx ) => {

  if ( ctx.url === '/index' ) {
    ctx.cookies.set(
      'cid',
      'hello world',
      {
        domain: 'localhost',  // 寫cookie所在的域名
        path: '/index',       // 寫cookie所在的路徑
        maxAge: 10 * 60 * 1000, // cookie有效時長
        expires: new Date('2017-02-15'),  // cookie失效時間
        httpOnly: false,  // 是否只用於http請求中獲取
        overwrite: false  // 是否容許重寫
      }
    )
    ctx.body = 'cookie is ok'
  } else {
    ctx.body = 'hello world'
  }

})

app.listen(3000, () => {
  console.log('[demo] cookie is starting at port 3000')
})

koa2 中實現 session

koa2 原生功能只提供了 cookie 的操做,可是沒有提供session操做。session 就只能本身實現或者經過第三方中間件實現。

我這裏給你們演示一下經過中間件koa-generic-session來在 koa2 中實現 session。

const Koa = require("koa");
const redisStore = require("koa-redis");
const session = require("koa-generic-session");

app.use(
  session({
    key: "forum.sid", // cookie name
    prefix: "forum:sess:", // redis key的前綴
    cookie: {
      path: "/",
      httpOnly: true,
      maxAge: 24 * 60 * 60 * 1000 // ms
    },
    ttl: 24 * 60 * 60 * 1000, // ms
    store: redisStore({
      all: `${REDIS_CONF.host}:${REDIS_CONF.port}`
    })
  })
);

koa2 處理流程

上面提到了不少 koa2 涉及到的一些概念,下面讓咱們梳理一下 koa2 完整的處理流程吧!

完整大體能夠分爲如下四部分:

  • 初始化應用
  • 請求到來-建立上下文
  • 請求到來-中間件執行
  • 返回 res-特殊處理
這裏參考 大佬的一張關於 koa2 的完整流程圖,

初始化應用

在咱們的app.js中,初始化的時候建立了 koa 實例(new koa()),而後是不少的use,最後是app.listen(3000)

use主要是把全部的函數(使用的中間件)收集到一個middleware數組中。

listen主要是對http.createServer進行了一個封裝,這個函數中傳入的callback是核心,它裏面包含了中間件的合併,上下文的處理等。也就是http.createServer(app.callback()).listen(...)

建立上下文

一個請求過來時,能夠拿到對應的 req、res,koa 拿到後就經過createContext來建立應用上下文,並進行屬性代理delegate

中間件執行

請求過來時,經過use操做已經將多箇中間件放入一個緩存隊列中。使用koa-compose將傳入的middleware組合起來,而後返回了一個 promise。

http.createServer((req, res) => {
 // ... 經過req,res建立上下文
 // fn是`koa-compose`返回的promise
 return fn(ctx).then(handleResponse).catch(onerror);
})

res 返回並進行特殊處理

在上面一部分,咱們看到有一個handleResponse,它是什麼呢?(其實到這裏咱們尚未res.end())。

const handleResponse = () => respond(ctx);

respond 到底作了什麼呢,其實它就是判斷你以前中間件寫的 body 的類型,作一些處理,而後才使用res.end(body)

到這裏就結束了,返回了頁面。

參考

福利

到這裏關於 koa2 的一些相關概念就分享結束了,不知道你有沒有收穫呢?

我這裏有兩個關於koa2的完整(何爲完整,數據庫、日誌、模型等等等等,你想要的都有)的項目,能夠供你們參考,固然感受不錯的話能夠給個 star 支持一下!!

  • https://github.com/Jack-cool/rest_node_api
  • https://github.com/Jack-cool/forum_code

最後

同時你能夠關注個人同名公衆號【前端森林】,這裏我會按期發一些大前端相關的前沿文章和平常開發過程當中的實戰總結。

相關文章
相關標籤/搜索