koa是有express原班人馬打造的基於node.js的下一代web開發框架。koa 1.0使用generator實現非同步,相比於回調簡單和優雅和不少。koa團隊並沒有止步於koa 1.0, 隨著node.js開始支持async/await,他們又馬不停蹄的發佈了koa 2.0,koa2完全使用P... ...
koa是有express原班人馬打造的基於node.js的下一代web開發框架。koa 1.0使用generator實現非同步,相比於回調簡單和優雅和不少。koa團隊並沒有止步於koa 1.0, 隨著node.js開始支持async/await,他們又馬不停蹄的發佈了koa 2.0,koa2完全使用Promise並配合async/await來實現非同步,使得非同步操作更臻完美。
一、快速開始
koa使用起來非常簡單,安裝好node.js後執行以下命令安裝koa:
npm init
npm install --save koa
一個簡單的Hello World程式開場,
//index.js const Koa = require('koa') const app = new Koa() app.use(async ctx => { ctx.body = 'Hello World' }) app.listen(3000, () => { console.log("server is running at 3000 port"); })
在命令行執行
node index.js
打開瀏覽器查看http://localhost:3000就可以看到頁面輸出的 Hello World。
中間件 middleware
Koa中使用 app.use()用來載入中間件,基本上Koa 所有的功能都是通過中間件實現的。
中間件的設計非常巧妙,多個中間件會形成一個棧結構(middle stack),以”先進後出”(first-in-last-out)的順序執行。每個中間件預設接受兩個參數,第一個參數是 Context 對象,第二個參數是 next函數。只要調用 next函數,就可以把執行權轉交給下一個中間件,最裡層的中間件執行完後有會把執行權返回給上一級調用的中間件。整個執行過程就像一個剝洋蔥的過程。
比如你可以通過在所有中間件的頂端添加以下中間件來列印請求日誌到控制台:
app.use(async function (ctx, next) { let start = new Date() await next() let ms = new Date() - start console.log('%s %s - %s', ctx.method, ctx.url, ms) })
常用的中間件列表可以在這裡找到: https://github.com/koajs/koa/wiki
二、koa源碼解讀
打開項目根目錄下的node_modules文件夾,打開並找到koa的文件夾,如下所示:
打開lib文件夾,這裡一共有4個文件,
-
application.js - koa主程式入口
-
context.js - koa中間件參數ctx對象的封裝
-
request.js - request對象封裝
-
response.js - response對象封裝
我們這裡主要看下application.js,我這裡摘取了主要功能相關的 代碼如下:
/** * Shorthand for: * * http.createServer(app.callback()).listen(...) * * @param {Mixed} ... * @return {Server} * @api public */ listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); } /** * Use the given middleware `fn`. * * Old-style middleware will be converted. * * @param {Function} fn * @return {Application} self * @api public */ use(fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); 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'); fn = convert(fn); } debug('use %s', fn._name || fn.name || '-'); this.middleware.push(fn); return this; } /** * Return a request handler callback * for node's native http server. * * @return {Function} * @api public */ callback() { const fn = compose(this.middleware); if (!this.listenerCount('error')) this.on('error', this.onerror); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; } /** * Handle request in callback. * * @api private */ handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); return fnMiddleware(ctx).then(handleResponse).catch(onerror); }
通過註釋我們可以看出上面代碼主要乾的事情是初始化http服務對象並啟動。我們註意到 callback()方法裡面有這樣一段代碼 :
const fn = compose(this.middleware);
compose其實是Node模塊koa-compose,它的作用是將多個中間件函數合併成一個大的中間件函數,然後調用這個中間件函數就可以依次執行添加的中間件函數,執行一系列的任務。遇到await next()時就停止當前中間件函數的執行並把執行權交個下一個中間件函數,最後next()執行完返回上一個中間件函數繼續執行下麵的代碼。
它是用了什麼黑魔法實現的呢?我們打開node_modules/koa-compose/index.js,代碼如下 :
function compose(middleware) { 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 if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } }
乍一看好難好複雜,沒事,我們一步一步的來梳理一下。
這個方法裡面的核心就是dispatch函數(廢話,整個compose方法就返回了一個函數)。沒有辦法簡寫,但是我們可以將dispatch函數類似遞歸的調用展開,以三個中間件為例:
第一次,此時第一個中間件被調用,dispatch(0),展開:
Promise.resolve(function(context, next){ //中間件一第一部分代碼 await/yield next(); //中間件一第二部分代碼}());
很明顯這裡的next指向dispatch(1),那麼就進入了第二個中間件;
第二次,此時第二個中間件被調用,dispatch(1),展開:
Promise.resolve(function(context, 中間件2){ //中間件一第一部分代碼 await/yield Promise.resolve(function(context, next){ //中間件二第一部分代碼 await/yield next(); //中間件二第二部分代碼 }()) //中間件一第二部分代碼}());
很明顯這裡的next指向dispatch(2),那麼就進入了第三個中間件;
第三次,此時第二個中間件被調用,dispatch(2),展開:
Promise.resolve(function(context, 中間件2){ //中間件一第一部分代碼 await/yield Promise.resolve(function(context, 中間件3){ //中間件二第一部分代碼 await/yield Promise(function(context){ //中間件三代碼 }()); //中間件二第二部分代碼 }) //中間件一第二部分代碼}());
此時中間件三代碼執行完畢,開始執行中間件二第二部分代碼,執行完畢,開始執行中間一第二部分代碼,執行完畢,所有中間件載入完畢。
再舉一個例子加深下理解。新建index.js並粘貼如下代碼:
const compose = require('koa-compose') const middleware1 = (ctx, next) => { console.log('here is in middleware1, before next:'); next(); console.log('middleware1 end'); } const middleware2 = (ctx, next) => { console.log('here is in middleware2, before next:'); next(); console.log('middleware2 end'); } const middleware3 = (ctx, next) => { console.log('here is in middleware3, before next:'); next(); console.log('middleware3 end'); } const middlewares = compose([middleware1, middleware2, middleware3]) console.dir(middlewares())
在命令行輸入node index.js執行,輸出結果如下:
here is in middleware1, before next:
here is in middleware2, before next:
here is in middleware3, before next:
middleware3 end
middleware2 end
middleware1 end
Promise { undefined }
可以看到每個中間件都按照“剝洋蔥”的流程一次執行。當我們初始化app對象並調用app.use()時,就是在不斷往app.middleware數組裡添加中間件函數,當調用app.listen()再執行組合出來的函數。
-END-
轉載請註明來源
掃描下方二維碼,或者搜索 前端提高班 關註公眾號,即可獲取最新走心文章
記得把我設為星標或置頂哦
在公眾號後臺回覆 前端資源 即可獲取最新前端開發資源