我們先實現這個小案例
app.use(async (ctx,next)=>{
console.log(ctx.request.req.url)
console.log(ctx.req.url)
console.log(ctx.request.url);
console.log(ctx.url);
console.log(ctx.request.req.path)
console.log(ctx.req.path)
console.log(ctx.request.path);
console.log(ctx.path);
});
複製程式碼
koa中的ctx包括原生的res和req屬性,並新加了request和response屬性 ctx.req = ctx.request.req = req; ctx.res = ctx.response.res = res; ctx代理了ctx.request和ctx.response
和koa原始碼一樣,我們將程式碼分為4個檔案application.js, context.js, response.js, request.js
application.js:
let context = require('./context');
let request = require('./request');
let response = require('./response');
let stream = require('stream');
class Koa extends EventEmitter{
constructor(){
super();
this.context = context;//將context掛載到例項上
this.request = request;//將request掛載到例項上
this.response = response;//將response掛載到例項上
}
use(fn){
this.fn = fn;
}
//此方法,將原生的req掛載到了ctx和ctx.request上,將原生的res掛載到了ctx和ctx.response上
createContext(req,res){
// 建立ctx物件 request和response是自己封裝的
let ctx = Object.create(this.context);//繼承context
ctx.request = Object.create(this.request);//繼承request
ctx.response = Object.create(this.response);// 繼承response
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
return ctx;
}
handleRequest(req,res){
// 通過req和res產生一個ctx物件
let ctx = this.createContext(req,res); //cxt具備四個屬性request,response,req,res,後兩個就是原生的req,res
}
listen(){
let server = http.createServer(this.handleRequest.bind(this));
server.listen(...arguments);
}
}
複製程式碼
那例子中的ctx.request.path ctx.path和ctx.request.url/ctx.url怎麼實現? 我們知道path和url都是請求中的引數,因此可以通過req實現,request.js實現如下:
let url = require('url')
let request = {
get url(){
return this.req.url
},
get path(){
return url.parse(this.req.url).pathname
}
}
module.exports = request;
複製程式碼
當訪問ctx.request.url時,訪問get url方法,方法中的this指的是ctx.request 這樣ctx.request.url,ctx.request.path,ctx.request.query都實現了,那麼ctx.url,ctx.path怎麼實現呢?
koa原始碼中用了代理,原理程式碼context.js程式碼如下:
let proto = {}
//獲取ctx.url屬性時,呼叫ctx.request.url屬性
function defineGetter(property,name) {
proto.__defineGetter__(name,function () {
return this[property][name];
})
}
//ctx.body = 'hello' ctx.response.body ='hello'
function defineSetter(property, name) {
proto.__defineSetter__(name,function (value) {
this[property][name] = value;
})
}
defineGetter('request','path');// 獲取ctx.path屬性時,呼叫ctx.request.path屬性
defineGetter('request','url');// 獲取ctx.url屬性時,呼叫ctx.request.url屬性
module.exports = proto;
複製程式碼
現在就實現了例子中的功能,接下來如何獲取ctx.body和設定ctx.body呢? 用ctx.response.boxy實現,然後再代理下即可。 response.js程式碼如下:
let response = {
set body(value){
this.res.statusCode = 200;
this._body = value;// 將內容暫時存在_body屬性上
},
get body(){
return this._body
}
}
module.exports = response;
複製程式碼
此時ctx.response.body = 'hello',就將hello放到了ctx.response的私有屬性_body上,獲取ctx.response.body ,就可以將‘hello’取出。 然後在 context.js程式碼中將body也代理上:
let proto = {}
function defineGetter(property,name) {
proto.__defineGetter__(name,function () {
return this[property][name];
})
}
function defineSetter(property, name) {
proto.__defineSetter__(name,function (value) {
this[property][name] = value;
})
}
defineGetter('request','path');
defineGetter('request','url');
defineGetter('response','body');
defineSetter('response','body');
module.exports = proto;
複製程式碼
這樣ctx.body就可以獲取到了。
重點來了,如何實現koa 中介軟體呢?
class Koa extends EventEmitter{
constructor(){
super();
this.context = context;
this.request = request;
this.response = response;
this.middlewares = [];//增加middlewares屬性
}
use(fn){
this.middlewares.push(fn);//每次呼叫use 都將方法存起來
}
...
handleRequest(req,res){
// 通過req和res產生一個ctx物件
let ctx = this.createContext(req,res);
// composeFn是組合後的promise
let composeFn = this.compose(this.middlewares, ctx);
composeFn.then(()=>{
//渲染body到頁面
})
}
}
複製程式碼
koa內部將每一個方法都包了一層promise,這樣可以執行非同步操作,這也是和express的重要差別。 返回的composeFn也是一個promise,這樣就可以在then裡面做內容渲染了。 那compose如何實現呢?程式碼很短,如下:
compose(middlewares,ctx){
function dispatch(index) {
if (index === middlewares.length) return Promise.resolve();//防止溢位,返回一個promise
let fn = middlewares[index];
return Promise.resolve(fn(ctx,()=>dispatch(index+1)));//為保證每個方法都是promise,這裡在外面包了一層promise
}
return dispatch(0);//先執行第一個fn方法
}
複製程式碼
然後渲染頁面:需要判斷是物件,是檔案,是流,是字串的情況:
res.statusCode = 404;//預設404
composeFn.then(()=>{
//渲染body到頁面
let body = ctx.body;
if (body instanceof stream) {//是流
body.pipe(res);
}else if(typeof body === 'object'){
res.end(JSON.stringify(body));
}else if(typeof body === 'string' || Buffer.isBuffer(body)){
res.end(body);
}else{
res.end('Not Found');
}
}
})
複製程式碼
頁面錯誤如何處理呢? 例子:
app.use(async (ctx,next)=>{
ctx.body = 'hello';
throw Error('出錯了')
});
app.on('error', function (err) {
console.log(err)
})
app.listen(3000);
複製程式碼
我們知道每一個方法都被包成了promise,當出現錯誤時,錯誤會被catch捕獲,因此可以新增catch方法,捕獲錯誤
composeFn.then(()=>{
let body = ctx.body;
if (body instanceof stream) {
body.pipe(res);
}else if(typeof body === 'object'){
res.end(JSON.stringify(body));
}else if(typeof body === 'string' || Buffer.isBuffer(body)){
res.end(body);
}else{
res.end('Not Found');
}
}).catch(err=>{ // 如果其中一個promise出錯了就發射錯誤事件即可
this.emit('error',err);
res.statusCode = 500;
res.end('Internal Server Error');
})
複製程式碼