源碼
koa使用分析
const Koa = require('koa');
let app = new Koa();//Koa是一個類,通過new生成一個實例
//koa的原型上有use方法,來註冊中間件
app.use((ctx,next)=>{
//koa擁有ctx屬性,上面掛載了很多屬性
console.log(ctx.req.path);
console.log(ctx.request.req.path);
console.log(ctx.request.path);
console.log(ctx.path);
next();//洋蔥模型,中間件組合
})
app.listen(3000);//Koa的原型上擁有監聽listen
洋蔥模型和中間件組合
洋蔥模型
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next)=>{
console.log(1)
await next();
console.log(2)
});
app.use(async (ctx, next) => {
console.log(3)
await next();
console.log(4)
})
app.use(async (ctx, next) => {
console.log(5)
awit next();
console.log(6)
})
//打印結果:1 3 5 6 4 2
中間件組合
koa洋蔥模型的實現,其實就是通過use將函數存放在一個middlewares隊列中,然後通過函數dispatch派發中間件。
dispatch組合中間件:
let app = {
middlewares:[]; //緩存隊列
use(fn){ //註冊中間件
this.middlewares.push(fn);
}
}
app.use(next=>{
console.log(1)
next();
console.log(2)
});
app.use(next => {
console.log(3)
next();
console.log(4)
})
app.use(next => {
console.log(5)
next();
console.log(6)
})
dispatch(0)
function dispatch(index){ //派發執行中間件
if(index===app.middlewares.length) retrun ;
let middleware = app.middlewares[index];
middleware(()=>{
dispatch(index+1);
})
}
koa組成
koa主要是由四部分組成:
- application:koa的主要邏輯,包含了中間件處理過程
- context:koa關於ctx的封裝
- request:koa請求對象的封裝
- response:koa響應對象封裝
koa實現
- Koa是一個類,擁有middleware、ctx、request、response
- Koa.prototype擁有use註冊中間件
- Koa.prototype擁有listen監聽網絡請求,其內部是對http模塊的封裝
- Koa中handleRquest處理上下文ctx和中間件middleware
application.js
在koa目錄下新建一個lib文件夾,新建application.js文件,代碼如下:
let http = require('http');
let EventEmitter = require('events');
let context = require('./context');
let request = require('./request');
let response = require('./response');
let Stream = require('stream');
const fs = require('fs')
class koa extends EventEmitter {
constructor(){
super();
this.middlewares = [];
this.context = context;
this.request = request;
this.response = response;
this.ctx = null;
}
use(fn){
this.middlewares.push(fn);
}
createContext(req, res){
const ctx = Object.create(this.context);
// ctx.request爲request.js裏面this作了指向,this此時指向ctx
const request = ctx.request = Object.create(this.request);
const response = ctx.response = Object.create(this.response);
// 請仔細閱讀以下眼花繚亂的操作,後面是有用的
/*
* console.log(ctx.req.url)
console.log(ctx.request.req.url)
console.log(ctx.response.req.url)
console.log(ctx.request.url)
console.log(ctx.request.path)
console.log(ctx.url)
console.log(ctx.path)
訪問localhost:3000/abc
/abc
/abc
/abc
/undefined
/undefined
/undefined
/undefined
姿勢多,不一定爽,要想爽,我們希望能實現以下兩點:
從自定義的request上取值、拓展除了原生屬性外的更多屬性,例如query path等。
能夠直接通過ctx.url的方式取值,上面都不夠方便。
* */
ctx.req = request.req = response.req = req;
ctx.res = request.res = response.res = res
request.ctx = response.ctx = ctx;
request.response = response;
response.request = request;
return ctx;
}
compose(middlewares, ctx){
function dispatch(index) {
if(index === middlewares.length){
return Promise.resolve()
}
let middleware = middlewares[index];
return Promise.resolve(middleware(ctx, () => {
dispatch(index+1)
}))
}
return Promise.resolve(dispatch(0))
}
handleRequest(req, res){
res.statusCode = 404;
let ctx = this.ctx = this.createContext(req, res);
let fn = this.compose(this.middlewares, ctx);
ctx.setHeaders = (key,val)=>{
res.setHeader(key, val)
}
setTimeout(() => {
fn.then(() => {
let body = ctx.body;
if (Buffer.isBuffer(body) || typeof body === 'string'){
if(body.indexOf('<!DOCTYPE html>') != -1){
res.setHeader('Content-Type','text/html;charset=utf8')
}
else{
res.setHeader('Content-Type','text/plain;charset=utf8')
}
res.end(body);
}
else if(typeof body == 'number'){
res.setHeader('Content-Type','text/plain;charset=utf8')
res.end(body.toString());
}
else if (body instanceof Stream){
res.setHeader('Content-Type',''+ctx.contentType+';charset=utf8')
body.pipe(res);
}
else if(typeof body == 'object'){
res.setHeader('Content-Type','application/json;charset=utf8')
res.end(JSON.stringify(body));
}
else{
res.setHeader('Content-Type','text/plain;charset=utf8')
res.end('Not Found');
}
}).catch(err => {
this.emit('error', err);
res.statusCode = 500
res.end('server error')
})
},20)
}
listen(...args){
let server = http.createServer(this.handleRequest.bind(this));
server.listen(...args);
}
}
module.exports = koa;
源碼解讀:
- 這裏我們有context,response,request,context使用了代理模式,代理之後,通過ctx.body,ctx.method等方式,可以直接設置返回數據或者方式。
- 這裏的listen方法,主要是利用了http起用一個服務,handleRequest方法通過bind方式,可以直接將http綁定上去,使其handleRequest的this指向http。
- handleRequest這裏主要是處理服務啓動後的回調函數,參數包括req,res,ctx.setHeaders是我們這裏封裝的一個方式,通過這個方法,在其他頁面可以設置返回頭部,接下來我們將app.use裏面函數一個個執行
let fn = this.compose(this.middlewares, ctx);
- this.middlewares是我們通過app.use方法,將use裏面函數放到一個數組裏面,稍後會進行講解,ctx是對應的是context,我們把req,res當做參數傳入,就可以在context.js文件裏面具體設置req,res,在fn.then裏面我們需要判斷,因爲在返回的數據裏面,分爲number,string,html以及文件類型,針對於每一種類型,我們需要設置不同頭部。
- ctx.contentType,我們這裏有一個這個主要是因爲,文檔類型不同,我們需要設置不同的Content-Type。
- compose方法,主要是針對app.use方式,在app.use裏面我們寫了不同函數,我們需要執行這些函數,除此之外,如果有next,這個時候調用dispatch(index+1),同時我們需要把它封裝成Promise方式。
- createContext方法,在handleRequest調用,req,res對應的是http的回調req,res,在這裏我們使用了Object.create方式,相當於繼承,避免改變直接改變原對象,ctx.request爲request.js裏面this作了指向,this此時指向ctx,其他也是同理
- use方法是將app.use調用的函數放到一個數組裏面
request.js
/*
* 非常簡單,使用對象get訪問器返回一個處理過的數據就可以將數據綁定到request上了,
* 這裏的問題是如何拿到數據,由於前面ctx.request這一步,所以this就是ctx,
* 那this.req就是原生的req,再利用一些第三方模塊對req進行處理就可以了,
* 源碼上拓展了非常多,這裏只舉例幾個,看懂原理即可。
訪問localhost:3000/abc?id=1
/abc?id=1
/abc?id=1
/abc?id=1
/abc?id=1
/abc
undefined
undefined
*
* */
const url = require('url')
// noinspection JSAnnotator
let request = {
get url() { // 這樣就可以用ctx.request.url上取值了,不用通過原生的req
return this.req.url
},
get path() {
return url.parse(this.req.url).pathname
},
get query() {
return url.parse(this.req.url).query
},
get method(){
return this.req.method.toLowerCase()
},
get header(){
return this.req.headers
},
get requestBody(){
return this._body
},
set requestBody(val){
this._body = val
},
// 。。。。。。
}
module.exports = request
源碼解讀
- 這裏我們主要是封裝了一些屬性,共後續調用,get url(),這樣就可以用ctx.request.url上取值了,不用通過原生的req,其他同理
- 至於這裏的get,set方法,這是es6裏面寫法,參考鏈接
response.js
//response.js
let response = {
get body(){
return this._body;
},
set body(val){
this.res.statusCode = 200 // 只要設置了body,就應該把狀態碼設置爲200
this._body = val // set時先保存下來
},
get contentType(){
return this._type;
},
set contentType(val){
this._type = val // set時先保存下來
},
get render(){
return this._fn;
},
set render(val){
this._fn = val // set時先保存下來
},
}
module.exports = response;
源碼解讀
同理上面request.js
context.js
//context.js
let proto = {
};
function defineGetter(property,key){
proto.__defineGetter__(key,function(){
return this[property][key];
})
}
function defineSetter(property,key){
proto.__defineSetter__(key,function(val){
this[property][key] = val;
})
}
/*
* __defineGetter__方法可以將一個函數綁定在當前對象的指定屬性上,當那個屬性的值被讀取時,
* 你所綁定的函數就會被調用,第一個參數是屬性,第二個是函數,由於ctx繼承了proto,
* 所以當ctx.url時,觸發了__defineGetter__方法,所以這裏的this就是ctx。這樣,當調用defineGetter方法,就可以將參數一的參數二屬性代理到ctx上了。
* */
defineGetter('request','url'); //ctx代理了ctx.request.url的get
defineGetter('request','path'); //ctx代理了ctx.request.path的get
defineGetter('request','query'); //ctx代理了ctx.request.query的get
defineGetter('request','method'); //ctx代理了ctx.request.method的get
defineGetter('request','header'); //ctx代理了ctx.request.header的get
defineGetter('response','body'); //ctx代理了ctx.response.body的get
defineSetter('response','body'); //ctx代理了ctx.response.body的set
defineGetter('response','contentType'); //ctx代理了ctx.response.contentType的get
defineSetter('response','contentType'); //ctx代理了ctx.response.contentType的set
defineGetter('response','render'); //ctx代理了ctx.response.render的get
defineSetter('response','render'); //ctx代理了ctx.response.render的set
defineGetter('request','requestBody'); //ctx代理了ctx.response.render的get
defineSetter('request','requestBody'); //ctx代理了ctx.response.render的set
module.exports = proto;
源碼解讀
ctx屬性代理了一些ctx.request、ctx.response上的屬性,使得ctx.xx能夠訪問ctx.request.xx或ctx.response.xx
調用方法
const Koa = require('./lib/application');
app.use(async (ctx,next)=>{
ctx.res.setHeader('Access-Control-Allow-Origin', '*');
//用於判斷request來自ajax還是傳統請求
ctx.res.setHeader("Access-Control-Allow-Headers", "Content-Type,XFILENAME,XFILECATEGORY,XFILESIZE,content-type");
//允許訪問的方式
ctx.res.setHeader('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS')
//修改程序信息與版本
ctx.res.setHeader('X-Powered-By', ' 3.2.1')
//內容類型:如果是post請求必須指定這個屬性
ctx.res.setHeader('Content-Type', 'application/json;charset=utf-8')
if(ctx.req.method == 'OPTIONS'){
ctx.res.statusCode = 200;/*讓options請求快速返回*/
}
next()
})
app.use(async (ctx,next)=>{
ctx.body = 'hello koa'
next()
})
app.listen(3000);