什麼是 koa
?html
koa 是一個基於 node 實現的一個新的 web 框架,它是由 express
框架的原班人馬打造。特色是優雅、簡潔、表達力強、自由度高。和 express 相比,它是一個更輕量的 node 框架,由於它全部的功能都經過插件來實現,這種插拔式的架構設計模式,很符合 unix 哲學。前端
本文從零開始,按部就班的展現和詳解上手 koa2
框架的幾個最重要的概念,最後會串聯講解一下 koa2 的處理流程以及源碼結構。看完本文之後,相信不管對於上手 koa2 仍是深刻了解 koa2 都會有不小的幫助。node
按照正常邏輯,安裝使用這種通常都會去官網看一下相似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
項目已經啓動起來了,下面讓咱們來簡單看一下源碼文件目錄結構吧:web
這個就是 koa2 源碼的源文件結構,核心代碼就是 lib 目錄下的四個文件:redis
application.js
application.js
是 koa 的入口文件,它向外導出了建立 class 實例的構造函數,繼承自 node 自帶的events
,這樣就會賦予框架事件監聽和事件觸發的能力。application 還暴露了一些經常使用的 api,好比listen
、use
等等。數據庫
listen
的實現原理其實就是對http.createServer
進行了一個封裝,這個函數中傳入的callback
是核心,它裏面包含了中間件的合併,上下文的處理,對 res 的特殊處理。express
use 的做用主要是收集中間件,將多箇中間件放入一個緩存隊列中,而後經過koa-compose
這個插件進行遞歸組合調用這一系列的中間件。
context.js
這部分就是 koa 的應用上下文 ctx
,其實就一個簡單的對象暴露,裏面的重點在 delegate
,這個就是代理,這個就是爲了開發者方便而設計的,好比咱們要訪問 ctx.repsponse.status
可是咱們經過 delegate,能夠直接訪問 ctx.status
訪問到它。
request.js、response.js
這兩部分就是對原生的res
、req
的一些操做了,大量使用 es6 的get
和set
的一些語法,去取headers
或者設置headers
、還有設置body
等等
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
經過必定的判斷或者正則匹配就能夠定製出所須要的路由。
若是依靠ctx.request.url
去手動處理路由,將會寫不少處理代碼,這時候就須要對應的路由的中間件對路由進行控制,這裏介紹一個比較好用的路由中間件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
)你須要的經常使用功能基本上上面都有。
在 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=2const 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
請求的處理,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
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)
<!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') })
koa 提供了從上下文直接讀取、寫入 cookie 的方法:
ctx.cookies.get(name, [options])
讀取上下文請求中的 cookiectx.cookies.set(name, value, [options])
在上下文中寫入 cookiecookies
模塊,源碼在這裏,因此在讀寫 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 原生功能只提供了 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 的完整流程圖,
在咱們的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); })
在上面一部分,咱們看到有一個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
同時你能夠關注個人同名公衆號【前端森林】,這裏我會按期發一些大前端相關的前沿文章和平常開發過程當中的實戰總結。