源码
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);