此文已由作者張佃鵬授權網易雲社群釋出。
歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。
Koa 就是一種簡單好用的 Web 框架。它的特點是優雅、簡潔、表達力強、自由度高。本身程式碼只有1000多行。koa一箇中介軟體框架,其提供的是一個架子,而幾乎所有的功能都需要由第三方中介軟體完成,它只是node原生的http的一個封裝,再加入中介軟體元素,koa 不在核心方法中繫結任何中介軟體, 它僅僅提供了一個輕量優雅的函式庫,使得編寫 Web 應用變得得心應手
Koa目前分為兩個版本:koa 1.0和koa2
koa 1.0: 依賴generator函式和Promise實現非同步處理(ES6)
koa2: 依賴async函式和Promise實現非同步處理(ES7)
以下的關於koa的介紹主要在koa2的基礎上進行分析:
koa框架的使用
koa框架主要由以下幾個元素組成:
app
const Koa = require('koa');const app = new Koa();複製程式碼
app的主要屬性如下:
proxy: 表示是否開啟代理信任開關,預設為false,如果開啟代理信任,對於獲取request請求中的host,protocol,ip分別優先從Header欄位中的X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-For獲取:
//以下是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
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);複製程式碼
更多網易技術、產品、運營經驗分享請點選。
相關文章:
【推薦】 使用者研究如何獲取更為真實的使用者資訊