中間件的執行流程,能夠用下面這張圖片來生動的說明(圖片使用了 Koa 2 的 async 語法):javascript
對於 Koa 1 來講也相似,只是 async 函數換做 generator 函數,await 換做 yield 關鍵字。css
對於前端程序員,能夠把 yield 以前的代碼認爲是捕獲階段,yield 以後的認爲的冒泡階段,從而理解多箇中間件之間代碼的執行流程。html
路由通常屬於業務代碼,咱們通常放在其餘基礎中間件以後來註冊。路由的基本原理就是判斷 url path, 而後決定是否執行某個中間件邏輯。前端
簡單實現能夠相似這樣:java
const Koa = require('koa') const app = new Koa() app.use(function *(next) { if (this.path === '/home') { this.body = '首頁' } else { yield next } console.log('這裏會執行哦') }) app.use(function *(next) { if (this.path === '/admin') { this.body = '管理端' } }) app.listen(3000)
能夠看到,對於不符合本中間件的請求 path, 就直接丟棄,並去執行下一個中間件。若是全部中間件都匹配不到,會返回 404(Koa 默認行爲).node
上面代碼有一個問題,就是 "console.log" 會一直執行,要解決這個也很簡單。由於對於路由中間件來講,全部邏輯都是匹配path的if判斷內部的,因此對於這個不匹配的else代碼,能夠直接當作該 generator 的結束。能夠在 yield 前面加 return 或這樣修改:git
app.use(function* (next) { if (this.path !== '/') return yield next; this.body = 'we are at home!'; })
爲了應對更復雜的路由功能,咱們須要引入第三方的 koa-router 路由模塊。不過 Koa1 須要使用 4.x 版本的。程序員
Issue: You are using koa@1.x but koa-views@5.x needs koa@2 or above. If you are still at v1 please consider using koa-views@4.x. Note however, there are no updates supporting v1
npm i koa-router@4 -d
koa-router 暴露一個 Router 類,像 Vue.js 同樣,只需建立一個 router 實例,就能夠註冊對應的路由規則。github
var app = require('koa')(); var Router = require('koa-router'); var myRouter = new Router(); myRouter.get('/', function *(next) { this.response.body = 'Hello World!'; }); app.use(myRouter.routes()); app.listen(3000);
從用法中顯然能看出來,routers 方法返回的應該就是一個 generator 中間件函數,只是內部由 koa-router 進行了路由規則的處理和邏輯執行。開發者只需關注如何向 koa-router 對象上註冊 處理中間件
。npm
koa-router 像多數路由同樣支持不少 http 方法和匹配規則:
router.get() router.post() router.put() router.del() router.patch()
Koa 有不少的中間件 好比 [koa-view](
https://github.com/queckezz/k...
該中間件支持多種模板引擎。
cookie的獲取和設置是 Koa 內置的context集成的能力,不須要中間件的參與。
this.cookies.get('cookieName') this.cookies
爲了兼容 Koa1,咱們須要安裝一個老一點的koa-router
npm i koa-router@5.x
用於打印請求日誌和耗時
npm i koa-logger@1 // 1.x 支持 Koa1
// 爲了兼容 Koa1 npm i koa-session@3.x
該版本的 koa-session 只須要執行其導出函數並傳入一個 app 對象便可使用
const session = require('koa-session') app.use(session(app)) app.use(function * (next) { console.log(this.session.xxx) })
默認的Koa應用,咱們觀察下瀏覽器的 Response 響應的話,會發現雖然請求時瀏覽器攜帶了 Accept-Encoding: gzip
, 但實際上響應裏面並無 Content-Encoding: gzip
, 也就是說並無壓縮。
安裝 koa-compress@1.x
以後,就可讓 Koa 默認開啓對響應內容的壓縮了:
app.use(require('koa-compress')())
大一點的文件纔有效果哦,過小的話還比不上 'Content-Encoding' 頭所佔的字節的話,就有點得不償失了。
# 爲了兼容 Koa1 請安裝 2.x 版本 npm i koa-csrf@2.x
該模塊的導出對象是一個函數,函數會建立一箇中間件,你須要將他註冊到 Koa 的app裏面。使用方式以下:
app.use(session(app)) // koa-csrf 的機制要依賴session能力 app.use(csrf()) // 這是koa1的用法
解決跨站請求僞造攻擊,須要在客戶端請求時攜帶一個祕密的token,這個token要確保只有服務器端知道,並且用後即焚. 其思路是,一個用戶在訪問頁面時,服務端先把這個 csrf-token 放置到頁面中,而後頁面再次發起 POST 請求時,頁面須要帶上這個token,由服務端來校驗是否是服務器頒發的token.
回到koa-csrf這個模塊,在每次請求週期中, koa-csrf 都會在它的中間件內生成一個祕鑰secret, 而後基於secret生成一個 csrf-token; 並把這個 csrf-token 掛在 ctx 上,把 secret 掛在session上(由於secret做爲一個祕鑰基於session能夠針對一個獨立用戶,不必每次都變). 咱們把koa-csrf 中生成token過程的源碼撿出來以下:
// 建立一個祕鑰並放在session裏,每次生成和校驗csrf時都用這個token var secret = this.session.secret || (this.session.secret = tokens.secretSync()) // 摘錄tokens.secretSync的實現以下: Tokens.prototype.secretSync = function secretSync () { return uid.sync(this.secretLength) // 其實就是使用 uid-safe 模塊生成一個固定長度的隨機uniqueId }
有了secret祕鑰了,再來看下 csrf-token 咋生成的:
// 基於上一步的祕鑰secret來生成csrf-token 放在ctx對象上 this._csrf = tokens.create(secret) // csrf-token的生成過程以下: Tokens.prototype.create = function create (secret) { if (!secret || typeof secret !== 'string') { throw new TypeError('argument secret is required') } // 重點在這裏。其中rndm模塊僅僅就是用來生成n位數的隨機字符串;而_tokenize函數就是用來生成csrf-token的,其實現我摘錄在下面 return this._tokenize(secret, rndm(this.saltLength)) } // tokenize實現 Tokens.prototype._tokenize = function tokenize (secret, salt) { // csrf-token 的格式爲: salt隨機字符串 + hash(salt+secret) return salt + '-' + hash(salt + '-' + secret) }
至此,csrf-token就生成了。接下來,你須要在 GET 請求的頁面上,把 this.csrf 渲染到頁面中。而後前端再次請求後端的 POST 接口時,須要帶上那個token。這樣,POST請求到達服務器時 koa-csrf 中間件就會在請求到來時優先進行校驗。
校驗規則已經顯而易見了:
橫線
前面的字符串做爲 salt隨機串
,取後面的做爲 待校驗的哈希[fehash]
secret
var result = hash('前端傳來的salt' + '服務端祕鑰secret')
那麼,會不會存在黑客在中間網絡竊取到某次請求的token後,再利用這個token來實施 CSRF 呢? 這個其實是沒法避免的,既然黑客能竊取到http報文(說明請求被中間人劫持或站點被XSS注入),那黑客徹底能夠竊取到 cookie 等信息,至關於徹底模擬了用戶,這種狀況下任何防範都沒有做用了;只能說若是發現IP變了那就要求用戶從新登陸且切換 secret。
幾乎全部的網絡應用所需的功能都有中間件提供。能夠在官方 wiki 中看到中間件列表
對於編寫公共中間件的場景來講,更多的須要用戶能自定義中間件中一些配置。此時須要支持用戶對中間件進行配置。要實現可配置的中間件也簡單,只須要寫一個包裝函數,返回一個 generator 的函數便可。例如咱們的日誌中間件,能夠容許用戶自定義日誌格式,則能夠這樣:
// 可自定義日誌格式的中間件 const mylogger = function (format) { format = format || '{{method}} {{url}} - {{time}}' return function *(next) { const start = Date.now() yield next const ms = Date.now() - start console.log(format .replace('{{method}}', this.method) .replace('{{url}}', this.url) .replace('{{time}}', ms) ) } } // 使用該中間件 app.use(mylogger('{{time}} - {{method}} : {{url}}'))
有時可能須要將多箇中間件合併爲一個。對於 Generator來講,可使用 .call(this. next) 的方式將他們合併。
const Koa = require('koa') const app = new Koa() function *a(next) { console.log('come a') yield next; console.log('end a') } function *b(next) { console.log('come b') yield next console.log('end b') } function *all(next) { console.log('come all') yield a.call(this, b.call(this, next)); console.log('end all') } app.use(all) app.listen(3000)
執行上述代碼,控制檯會輸出:
come all come a come b end b end a end all
你必定比較疑惑爲何多個 generator 函數經過 call(this, next)
是怎麼作到如此合併執行的? 其實本質上 Koa 的運做也是基於合併 middlware 來執行的。這裏大概是這樣的:
function *a(next) { console.log('come a') yield (b中間件 的 generator 對象); console.log('end a') }
上述過程相似於 koa-compse 模塊的合併能力, 這裏貼一個 compose 模塊的實現(引用自阮一峯的 Koa 教程):
function compose(middleware){ return function *(next){ if (!next) next = noop(); var i = middleware.length; while (i--) { next = middleware[i].call(this, next); } yield *next; } } function *noop(){}
以上就是中間件合併的原理了,合併後會返回一個新的 generator 函數。而 Koa 是如何使用 co
庫把合併後的 generator 中間件函數運行起來的呢? 這個就有點複雜了,更詳細的 middleware合併 和 Koa 原理能夠參考: qianlongo github
本章節介紹了幾個經常使用中間件,如koa-router和 koa-view,並對中間件的合併和傳參進行了簡單介紹。基本上 Koa 的全部使用方式都已經介紹完畢,後面就是赤裸裸的實踐了