此文已由做者張佃鵬受權網易雲社區發佈。
node
歡迎訪問網易雲社區,瞭解更多網易技術產品運營經驗。git
Koa 就是一種簡單好用的 Web 框架。它的特色是優雅、簡潔、表達力強、自由度高。自己代碼只有1000多行。koa一箇中間件框架,其提供的是一個架子,而幾乎全部的功能都須要由第三方中間件完成,它只是node原生的http的一個封裝,再加入中間件元素,koa 不在內核方法中綁定任何中間件, 它僅僅提供了一個輕量優雅的函數庫,使得編寫 Web 應用變得駕輕就熟github
Koa目前分爲兩個版本:koa 1.0和koa2數組
koa 1.0: 依賴generator函數和Promise實現異步處理(ES6)安全
koa2: 依賴async函數和Promise實現異步處理(ES7)cookie
如下的關於koa的介紹主要在koa2的基礎上進行分析:app
koa框架主要由如下幾個元素組成:框架
const Koa = require('koa');const app = new Koa();
app的主要屬性以下:dom
proxy: 表示是否開啓代理信任開關,默認爲false,若是開啓代理信任,對於獲取request請求中的host,protocol,ip分別優先從Header字段中的X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-For獲取:koa
//如下是koa獲取request對象部分屬性的源碼,都是由app.proxy屬性決定的:{ get ips() { const proxy = this.app.proxy; const val = this.get('X-Forwarded-For'); return proxy && val ? val.split(/\s*,\s*/) : []; }, get host() { const proxy = this.app.proxy; let host = proxy && this.get('X-Forwarded-Host'); host = host || this.get('Host'); if (!host) return ''; return host.split(/\s*,\s*/)[0]; }, get protocol() { const proxy = this.app.proxy; if (this.socket.encrypted) return 'https'; if (!proxy) return 'http'; const proto = this.get('X-Forwarded-Proto') || 'http'; return proto.split(/\s*,\s*/)[0]; }, get URL() { if (!this.memoizedURL) { const protocol = this.protocol; const host = this.host; const originalUrl = this.originalUrl || ''; // originalUrl爲req.url try { this.memoizedURL = new URL(`${protocol}://${host}${originalUrl}`); } catch (err) { this.memoizedURL = Object.create(null); } } return this.memoizedURL; }, get hostname() { const host = this.host; if (!host) return ''; if ('[' == host[0]) return this.URL.hostname || ''; // IPv6 return host.split(':')[0]; }, }
env:node運行環境
this.env = process.env.NODE_ENV || 'development';
keys: app.keys是一個設置簽名的Cookie密鑰的數組,用於生成cookies對象
subdomainOffset:表示子域名是從第幾級開始的,這個參數決定了request.subdomains的返回結果,默認值爲2
//好比有netease.youdata.163.com域名app.subdomainOffset = 2;console.log(ctx.request.subdomains); //返回["youdata", "netease"]app.subdomainOffset = 3;console.log(ctx.request.subdomains); //返回["netease"]//koa獲取subdomains的源碼get subdomains() { const offset = this.app.subdomainOffset; const hostname = this.hostname; if (net.isIP(hostname)) return []; return hostname .split('.') .reverse() .slice(offset); },
middleware:app對應的中間件數組,使用app.use函數會將會將中間件加到該數組中
koa使用中間件方式來實現不一樣功能的級聯,當一箇中間件調用next(),則該函數暫停並將控制傳遞給定義的下一個中間件。當在下游沒有更多的中間件執行後,堆棧將展開而且每一箇中間件恢復執行其上游行爲,相似一個入棧出棧的模式,中間件的使用方式以下:
const Koa = require('koa');const app = new Koa(); app.use((ctx, next) => { console.log('step1-begin'); next(); console.log('step1-end'); }); app.use((ctx, next) => { console.log('step2-begin'); next(); console.log('step2-end'); }); app.listen(3000);/*輸出結果爲: step1-begin step2-begin step2-end step1-end */
context:這個是建立中間件中使用的「ctx」的原型,直接使用app.context意義不大,並且app.context上不少屬性實際上是爲ctx準備的,直接用app.context調用會報錯:
//如下context.js中的部分源碼:toJSON() { return { request: this.request.toJSON(), //若是直接使用app.context調用這個會報錯,由於這個時候this.request是undefined,只有在中間件裏使用ctx調用纔不會報錯 response: this.response.toJSON(), app: this.app.toJSON(), originalUrl: this.originalUrl, req: '<original node req>', res: '<original node res>', socket: '<original node socket>' }; },
context主要有如下用途:
//咱們能夠在context對象上加一些全局路由裏公用的屬性,這樣就不須要每次請求都在中間件裏賦值const Koa = require('koa');const app = new Koa(); app.context.datasourceConfig = { "connectionLimit": 100, "database": "development", "host": "10.165.124.134", "port": 3360, "user": "sup_bigviz", "password": "123456", "multipleStatements": true}; app.use((ctx, next) => { console.log('datasourceConfig:', ctx.datasourceConfig); //這裏能夠打印出全局配置 next(); });
request: 這個是建立ctx.request的原型,直接使用app.context.request幾乎沒有意義,不少屬性都會報錯,不過和app.context同樣,能夠給app.context添加一些ctx.request中用到的公共屬性
response: 這個是建立ctx.response的原型,直接使用app.context.response幾乎沒有意義,不少屬性都會報錯,不過和app.context同樣,能夠給app.context添加一些ctx.request中用到的公共屬性
app的主要函數以下:
use函數: use函數主要做用是給app.middleware數組中添加中間件
let koa = require('koa'); koa.use(async (ctx, next) => { //before do something... next(); //after await do something... })
listen函數:app.listen函數是建立服務的入口,只有調用app.listen函數之後,全部的中間件纔會被使用
//app.listen實際上是http.createServer的語法糖,源碼實現以下:function listen(...args) { debug('listen'); const server = http.createServer(this.callback()); //最終全部路由處理是在app..callback中實現的 return server.listen(...args); }
callback函數:返回一個函數供http.createServer() 方法的回調函數來處理請求。你也可使用此回調函數將koa應用程序掛載到Connect/Express應用程序中
//koa的callback函數實現源碼function callback() { const fn = compose(this.middleware); //koa-compose包負責講多箇中間件組裝成一箇中間件 if (!this.listeners('error').length) this.on('error', this.onerror); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); //這個函數負責生成中間件接收器ctx,綁定一些對象的關聯關係 return this.handleRequest(ctx, fn); //使用中間件函數fn處理路由請求 }; return handleRequest; }//handleRequest函數的源碼實現也很簡單,執行中間件函數,並作一些返回處理和異常處理function 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); }
ctx是中間件中的上下文環境,也是koa框架中最經常使用最重要的對象,每一個請求都會根據app.context建立一個新的ctx,並在中間件中做爲接收器引用
ctx對象上會綁定app,request,response等對象
//生成ctx的源碼function createContext(req, res) { const context = Object.create(this.context); //由上文中講解的app.context生成 const request = context.request = Object.create(this.request); //由上文中講解的app.request生成 const response = context.response = Object.create(this.response); //由上文中講解的app.response生成 context.app = request.app = response.app = this; context.req = request.req = response.req = req; //req是node的req,儘可能避免使用,而是使用ctx.request; context.res = request.res = response.res = res; //res是node的res,儘可能避免使用,而是應該使用ctx.response; request.ctx = response.ctx = context; request.response = response; response.request = request; context.originalUrl = request.originalUrl = req.url; context.cookies = new Cookies(req, res, { //生成cookies,是由[cookie模塊生成的](https://github.com/pillarjs/cookies): keys: this.keys, secure: request.secure //secure是根據域名是否是https返回的結果 }); request.ip = request.ips[0] || req.socket.remoteAddress || ''; //客戶端訪問ip context.accept = request.accept = accepts(req); // context.state = {}; //這個給用戶使用,用於存放用戶在多箇中間件中用到的一些屬性或者函數 return context; }
ctx會代理ctx.response和ctx.request上的一些屬性和函數(這個代理邏輯是在ctx.response和ctx.request的原型上實現的)
//如下是koa源碼(method表示代理方法,access表示代理屬性可讀可寫,getter表示代理屬性可讀):delegate(proto, 'response') .method('attachment') //將Content-Disposition 設置爲 「附件」 以指示客戶端提示下載 .method('redirect') //返回重定向,若是沒有code設置,默認設置code爲302 .method('remove') //刪除響應頭的某個屬性 .method('vary') //設置Vary響應頭 .method('set') //設置響應頭,能夠傳遞對象,數組,單個值的形式 .method('append') //給response.headers中的某個key值追加其它value .method('flushHeaders') //執行this.res.flushHeaders() .access('status') //http返回code碼,優先選擇用戶的設置,若是用戶沒有主動設置,而設置了ctx.body的值, 若是設置值爲null,則返回204,若是設置值不爲null,那麼返回200,不然默認狀況下是404 .access('message') //獲取響應的狀態消息. 默認狀況下, response.message 與 response.status 關聯 .access('body') //response的返回結果 .access('length') //response的headers的Content-Length,能夠本身設置,默認根據body二進制大小設置 .access('type') //設置響應的content-type .access('lastModified') //設置響應頭Last-Modified .access('etag') //設置包含 " 包裹的 ETag 響應頭 .getter('headerSent') //檢查是否已經發送了一個響應頭。 用於查看客戶端是否可能會收到錯誤通知 .getter('writable'); //返回是否能夠繼續寫入delegate(proto, 'request') .method('acceptsLanguages') .method('acceptsEncodings') .method('acceptsCharsets') .method('accepts') //accepts函數用於判斷客戶端請求是否接受某種返回類型 .method('get') //獲取請求頭中的某個屬性值 .method('is') //判斷請求頭但願返回什麼類型 .access('querystring') //獲取原始查詢字符串 .access('idempotent') .access('socket') //返回請求套接字 .access('search') //搜索字符串 .access('method') //請求方法 .access('query') //獲取請求的查詢字符串對象 .access('path') //獲取請求路徑名 .access('url') //請求的url,該url能夠被重寫 .getter('origin') //獲取url的來源:包括 protocol 和 host(http://example.com) .getter('href') //獲取完整的請求URL,包括 protocol,host 和 url(http://example.com/foo/bar?q=1) .getter('subdomains') //獲取請求的子域名 .getter('protocol') //返回請求協議 .getter('host') //獲取當前主機的host(hostname:port) .getter('hostname') //獲取當前主機的host .getter('URL') //獲取 WHATWG 解析的 URL 對象 .getter('header') //返回請求頭對象 .getter('headers') //返回請求頭對象 .getter('secure') //經過 ctx.protocol == "https" 來檢查請求是否經過 TLS 發出 .getter('stale') .getter('fresh') .getter('ips') //當 X-Forwarded-For 存在而且 app.proxy 被啓用時,這些 ips 的數組被返回 .getter('ip'); //請求遠程地址//好比如下操做是等價的:ctx.body = { code: 200, result: { nick: "zhangdianpeng" } } ctx.response.body = { code: 200, result: { nick: "zhangdianpeng" } }console.log('ctx.method:', ctx.method);console.log('ctx.request.method:', ctx.request.method);
更多網易技術、產品、運營經驗分享請點擊。
相關文章:
【推薦】 網易杭研易盾實習心得(4)
【推薦】 如何玩轉基於風險的測試
【推薦】 AndroidTVOverscan