koa2特性
A Koa application is an object containing an array of middleware functions which are composed and executed in a stack-like manner upon request.
- 只提供封裝好http上下文、請求、響應,以及基於async/await的中介軟體容器
- 基於koa的app是由一系列中介軟體組成,原來是generator中介軟體,現在被async/await代替(generator中介軟體,需要通過中介軟體koa-convert封裝一下才能使用)
- 按照app.use(middleware)順序依次執行中介軟體陣列中的方法
1.0 版本是通過組合不同的 generator,可以免除重複繁瑣的回撥函式巢狀,並極大地提升錯誤處理的效率。
2.0版本Koa放棄了generator,採用Async 函式實現元件陣列瀑布流式(Cascading)的開發模式。
原始碼檔案
├── lib
│ ├── application.js
│ ├── context.js
│ ├── request.js
│ └── response.js
└── package.json
複製程式碼
核心程式碼就是lib目錄下的四個檔案
- application.js 是整個koa2 的入口檔案,封裝了context,request,response,以及最核心的中介軟體處理流程。
- context.js 處理應用上下文,裡面直接封裝部分request.js和response.js的方法
- request.js 處理http請求
- response.js 處理http響應
koa流程
koa的流程分為三個部分:初始化 -> 啟動Server -> 請求響應
-
初始化
- 初始化koa物件之前我們稱為初始化
-
啟動server
- 初始化中介軟體(中介軟體建立聯絡)
- 啟動服務,監聽特定埠,並生成一個新的上下文物件
-
請求響應
- 接受請求,初始化上下文物件
- 執行中介軟體
- 將body返回給客戶端
初始化
定義了三個物件,context
, response
, request
-
request
定義了一些set/get訪問器,用於設定和獲取請求報文和url資訊,例如獲取query資料,獲取請求的url(詳細API參見Koa-request文件) -
response
定義了一些set/get操作和獲取響應報文的方法(詳細API參見Koa-response 文件) -
context
通過第三方模組 delegate 將 koa 在 Response 模組和 Request 模組中定義的方法委託到了 context 物件上,所以以下的一些寫法是等價的://在每次請求中,this 用於指代此次請求建立的上下文 context(ctx) this.body ==> this.response.body this.status ==> this.response.status this.href ==> this.request.href this.host ==> this.request.host ...... 複製程式碼
為了方便使用,許多上下文屬性和方法都被委託代理到他們的
ctx.request
或ctx.response
,比如訪問ctx.type
和ctx.length
將被代理到response
物件,ctx.path
和ctx.method
將被代理到request
物件。每一個請求都會建立一段上下文,在控制業務邏輯的中介軟體中,
ctx
被寄存在this
中(詳細API參見 Koa-context 文件)
啟動Server
- 初始化一個koa物件例項
- 監聽埠
var koa = require('koa');
var app = koa()
app.listen(9000)
複製程式碼
解析啟動流程,分析原始碼
application.js
是koa的入口檔案
// 暴露出來class,`class Application extends Emitter`,用new新建一個koa應用。
module.exports = class Application extends Emitter {
constructor() {
super();
this.proxy = false; // 是否信任proxy header,預設false // TODO
this.middleware = []; // 儲存通過app.use(middleware)註冊的中介軟體
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development'; // 環境引數,預設為 NODE_ENV 或 ‘development’
this.context = Object.create(context); // context模組,通過context.js建立
this.request = Object.create(request); // request模組,通過request.js建立
this.response = Object.create(response); // response模組,通過response.js建立
}
...
複製程式碼
Application.js
除了上面的的建構函式外,還暴露了一些公用的api,比如常用的 listen
和use
(use放在後面講)。
listen
作用: 啟動koa server
語法糖
// 用koa啟動server
const Koa = require('koa');
const app = new Koa();
app.listen(3000);
// 等價於
// node原生啟動server
const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
https.createServer(app.callback()).listen(3001); // on mutilple address
複製程式碼
// listen
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
複製程式碼
封裝了nodejs的建立http server,在監聽埠之前會先執行this.callback()
// callback
callback() {
// 使用koa-compose(後面會講) 串聯中介軟體堆疊中的middleware,返回一個函式
// fn接受兩個引數 (context, next)
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
// this.callback()返回一個函式handleReqwuest,請求過來的時候,回撥這個函式
// handleReqwuest接受引數 (req, res)
const handleRequest = (req, res) => {
// 為每一個請求建立ctx,掛載請求相關資訊
const ctx = this.createContext(req, res);
// handleRequest的解析在【請求響應】部分
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
複製程式碼
const ctx = this.createContext(req, res);
建立一個最終可用版的context
ctx上包含5個屬性,分別是request,response,req,res,app
request和response也分別有5個箭頭指向它們,所以也是同樣的邏輯
補充瞭解 各物件之間的關係
最左邊一列表示每個檔案的匯出物件
中間一列表示每個Koa應用及其維護的屬性
右邊兩列表示對應每個請求所維護的一些列物件
黑色的線表示例項化
紅色的線表示原型鏈
藍色的線表示屬性
請求響應
回顧一下,koa啟動server的程式碼
app.listen = function() {
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
複製程式碼
// callback
callback() {
const fn = compose(this.middleware);
...
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
複製程式碼
callback()
返回了一個請求處理函式this.handleRequest(ctx, fn)
// handleRequest
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
// 請求走到這裡標明成功了,http respond code設為預設的404 TODO 為什麼?
res.statusCode = 404;
// koa預設的錯誤處理函式,它處理的是錯誤導致的異常結束
const onerror = err => ctx.onerror(err);
// respond函式裡面主要是一些收尾工作,例如判斷http code為空如何輸出,http method是head如何輸出,body返回是流或json時如何輸出
const handleResponse = () => respond(ctx);
// 第三方函式,用於監聽 http response 的結束事件,執行回撥
// 如果response有錯誤,會執行ctx.onerror中的邏輯,設定response型別,狀態碼和錯誤資訊等
onFinished(res, onerror);
// 執行中介軟體,監聽中介軟體執行結果
// 成功:執行response
// 失敗,捕捉錯誤資訊,執行對應處理
// 返回Promise物件
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
複製程式碼
Koa處理請求的過程:當請求到來的時候,會通過 req 和 res 來建立一個 context (ctx) ,然後執行中介軟體
koa中另一個常用API - use
作用: 將函式推入middleware陣列
use(fn) {
// 首先判斷傳進來的引數,傳進來的不是一個函式,報錯
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
// 判斷這個函式是不是 generator
// koa 後續的版本推薦使用 await/async 的方式處理非同步
// 所以會慢慢不支援 koa1 中的 generator,不再推薦大家使用 generator
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
// 如果是 generator,控制檯警告,然後將函式進行包裝
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
// 將函式推入 middleware 這個陣列,後面要依次呼叫裡面的每一箇中介軟體
this.middleware.push(fn);
// 保證鏈式呼叫
return this;
}
複製程式碼
koa-compose
const fn = compose(this.middleware)
app.use([MW])僅僅是將函式推入middleware陣列,真正讓這一系列函式組合成為中介軟體的,是koa-compose,koa-compose是Koa框架中介軟體執行的發動機
'use strict'
module.exports = compose
function compose (middleware) {
// 傳入的 middleware 必須是一個陣列, 否則報錯
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
// 迴圈遍歷傳入的 middleware, 每一個元素都必須是函式,否則報錯
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
// 如果中介軟體中沒有 await next ,那麼函式直接就退出了,不會繼續遞迴呼叫
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
複製程式碼
Koa2.x的compose方法雖然從純generator函式執行修改成了基於Promise.all,但是中介軟體載入的中心思想沒有發生改變,依舊是從第一個中介軟體開始,遇到await/yield next,就中斷本中介軟體的程式碼執行,跳轉到對應的下一個中介軟體執行期內的程式碼…一直到最後一箇中介軟體,然後逆序回退到倒數第二個中介軟體await/yield next下部分的程式碼執行,完成後繼續會退…一直會退到第一個中介軟體await/yield next下部分的程式碼執行完成,中介軟體全部執行結束
級聯的流程,V型載入機制
koa2常用中介軟體
koa-router 路由
對其實現機制有興趣的可以戳看看 -> Koa-router路由中介軟體API詳解
const Koa = require('koa')
const fs = require('fs')
const app = new Koa()
const Router = require('koa-router')
// 子路由1
let home = new Router()
home.get('/', async ( ctx )=>{
let html = `
<ul>
<li><a href="/page/helloworld">/page/helloworld</a></li>
<li><a href="/page/404">/page/404</a></li>
</ul>
`
ctx.body = html
})
// 子路由2
let page = new Router()
page.get('hello', async (ctx) => {
ctx.body = 'Hello World Page!'
})
// 裝載所有子路由的中介軟體router
let router = new Router()
router.use('/', home.routes(), home.allowedMethods())
router.use('/page', page.routes(), page.allowedMethods())
// 載入router
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000, () => {
console.log('[demo] route-use-middleware is starting at port 3000')
})
複製程式碼
koa-bodyparser 請求資料獲取
GET請求資料獲取
獲取GET請求資料有兩個途徑
-
是從上下文中直接獲取
- 請求物件ctx.query,返回如 { a:1, b:2 }
- 請求字串 ctx.querystring,返回如 a=1&b=2
-
是從上下文的request物件中獲取
- 請求物件ctx.request.query,返回如 { a:1, b:2 }
- 請求字串 ctx.request.querystring,返回如 a=1&b=2
POST請求資料獲取
對於POST請求的處理,koa2沒有封裝獲取引數的方法需要通過解析上下文context中的原生node.js請求物件req,將POST表單資料解析成query string(例如:a=1&b=2&c=3),再將query string 解析成JSON格式(例如:{"a":"1", "b":"2", "c":"3"})
對於POST請求的處理,koa-bodyparser中介軟體可以把koa2上下文的formData資料解析到ctx.request.body中
...
const bodyParser = require('koa-bodyparser')
app.use(bodyParser())
app.use( async ( ctx ) => {
if ( ctx.url === '/' && ctx.method === 'POST' ) {
// 當POST請求的時候,中介軟體koa-bodyparser解析POST表單裡的資料,並顯示出來
let postData = ctx.request.body
ctx.body = postData
} else {
...
}
})
app.listen(3000, () => {
console.log('[demo] request post is starting at port 3000')
})
複製程式碼
koa-static 靜態資源載入
為靜態資源訪問建立一個伺服器,根據url訪問對應的資料夾、檔案
...
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')
})
複製程式碼
PS:廣告一波,網易考拉前端招人啦~有興趣的戳我投遞簡歷