我们先实现这个小案例
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');
})
作者:芦梦宇
本文来源: 掘金 如需转载请联系原作者