功能概览
- application: 创建上下文,合并中间件,启动服务
- request: 对原生http模块req的扩展
- response: 对原生http模块res的扩展
- context:对request和response的合并与代理
服务启动:listen
使用
const Koa=require('./application')
const app=new Koa()
app.listen(3000,()=>{
console.log('run server__')
})
实现
//内部直接使用了http模块
const http=require('http')
class Application{
listen(...args){
const server= http.createServer()
server.listen(...args)
}
}
请求处理:handleRequest
使用
const Koa=require('./application')
const app=new Koa()
app.use((ctx)=>{
//下边这四种用法其实是一样的
//下文会接介绍为什么
console.log(ctx.url)
console.log(ctx.request.url)
console.log(ctx.req.url)
console.log(ctx.request.req.url)
})
app.listen(3000,()=>{
console.log('run server__')
})
实现
class Application{
//函数注册(中间件也是函数)
use(fn){
this.fn=fn
}
//请求处理
handleRequest(req,res){
//创建上下文:合并req,res为ctx
//这个方法实现下文会提到
const ctx=this.createContext(req,res)
//这里的ctx就是外部使用use时候的第一个参数
// app.use(ctx=>{})
this.fn(ctx)
}
//...启动服务
listen(...args){
//这里的bind是保证this指向自定义的Application
const server= http.createServer(this.handleRequest.bind(this))
server.listen(...args)
}
}
创建上下文:createContext
- 想一下,为什么koa将req,res合并为了一个ctx? 盲猜是为了使用方便,记住两个不如记住一个省心。
- 那req,res与ctx又有什么区别?req,res有的,
ctx皆有,req,res没有的,ctx还有。 - 你问我什么是西厂?一句话:东厂管得了的我要管,东厂管不了的我更要管!
实现
/**
* 同级目录下分别新建application.js request.js response.js context.js
* 暂时导出一个空对象即可
*/
//request.js
const request={}
module.exports=request
//response.js
const response={}
module.exports=response
//context.js
const context={}
module.exports=context
//application.js
class Application{
constructor(){
//这三个是外部引入的
this.context=context;
this.request=request;
this.response=response;
}
// createContext最终会返回一个合并后的context;
createContext(req,res){
//使用Object.create是为了在不对原模块进行干扰的情况下进行扩展,也是一层继承
const context=Object.create(this.context)
const request=Object.create(this.request)
const response=Object.create(this.response)
//上下文关联与合并
// context.req=req;
// context.request.req=req
// context.res=res;
// context.response.res=res
//上述代码可简写为如下形式
context.req=context.request.req=req
context.res=context.response.res=res
// 看到这里应该明白一件事:
//ctx.req.url和ctx.request.req.url输出一致是因为ctx.req和ctx.request.req指向本就一致
//同理:ctx.res和ctx.response.res指向也一致
//与此同时也应该思考另一件事:ctx.url和ctx.request.url又是怎么回事?
//返回context
return context
}
}
请求增强:request
request是koa对原生http模块req的增强,这也是源码里没直接像context那样使用代理而使用getter,setter的原因。
上边我们已经理解了为什么
ctx.req.url和ctx.request.req.url输出一致,接下来再看看ctx.url和ctx.request.url
const request={
get url() {
//这里的this指向request
//而createContext方法中的request身上恰好挂了一个req
//所以ctx.request.req 本质和ctx.req是同一指向
// 这也解释了ctx.request.url输出和ctx.req.url,ctx.request.req.url,输出一致
return this.req.url;
},
}
module.exports=request
context:代理
ok,现在就差ctx.url这个小东西了,其实它本质是ctx.request.url
为什么这么说呢?来看看context内部实现吧
const context = {}
function delegateGet(prop, key) {
//__defineGetter__这个方法是当访问对象的某个key时,执行回调
context.__defineGetter__(key, function () {
return this[prop][key]
})
}
//这就相当于ctx.url=>ctx.request.url
delegateGet('request', 'url')
module.exports = context
关于__defineGetter__,可参考MDN
developer.mozilla.org/zh-CN/docs/…__
如果你去看koa源码,你会发现它使用了一个第三方包:delegates,其实这东西实现也是用的__defineGetter__
响应增强:response
使用
const Koa=require('./application')
const app=new Koa()
app.use((ctx)=>{
ctx.body='hello lengyuexin';
console.log(ctx.body)
})
app.listen(3000,()=>{
console.log('run server__')
})
实现
//ctx本质是代理,ctx本质是代理,ctx本质是代理
//所以ctx做的响应相关的,一定是交给response
//同理ctx做的请求相关的,一定是交给request
//故:ctx.body=>ctx.response.body
const context = {}
function delegateGet(prop, key) {
context.__defineGetter__(key, function () {
return this[prop][key]
})
}
function delegateSet(prop, key) {
context.__defineSetter__(key, function (newValue) {
this[prop][key] = newValue
})
}
delegateGet('response', 'body')//访问ctx.body
delegateSet('response', 'body')//设置ctx.body='xxx'
module.exports = context
response 同request 一样, 也是getter setter 形式
const response = {
_body: '',
get body() {
return this._body
},
set body(newBody) {
this._body = newBody
}
}
module.exports = response
合并中间件:compose
- 在koa中,use函数可以多次调用,而且默认情况下只会执行第一个。
- 后边的如果想依次执行,需要调用next函数,也就是use函数的第二个参数。
- 此外,如果涉及异步操作,可用async,await。
洋葱模型图
使用
// 依次打印123
const app = new (require('koa'))
app.use((ctx, next) => {
console.log(1)
next()
})
app.use((ctx, next) => {
console.log(2)
next()
})
app.use((ctx, next) => {
console.log(3)
})
//错误监听
app.on('error',(err)=>{
console.error(err)
})
app.listen(3000)
实现
//为了方便异步处理和错误捕获
// compose方法返回的是一个promise
//中间件也需要用一个数组存储起来
const EventEmitter = require('events')
const Stream = require('stream')
class Application extends EventEmitter {
//application 的constructor中新增middlewares,初始化为空数组
constructor() {
super()
this.context = context
this.request = request
this.response = response
this.middlewares = []//多个use调用,存起来
}
//use方法就是直接push函数到middlewares
use(fn){
this.middlewares.push(fn)
}
handleRequest(req, res) {
const ctx = this.createContext(req, res)
//组合中间件 并执行返回后的promise
// 获取到_body 响应出去
this.compose(ctx).then(() => {
//默认只能处理buffer 和string
let _body = ctx.body;
if (_body === '') {
//如果没设置body 就给个默认值
//状态码设置为404
res.statusCode = 404
_body = 'not found'
return res.end(_body)
} else if (_body instanceof Stream) {
//koa也支持直接返回一个文件流,通过pipe就可以做到
//对流的处理
return _body.pipe(res)
} else if (typeof _body !== 'null' && typeof _body === 'object') {
//对对象的处理
return res.end(JSON.stringify(_body))
} else if (_body == null) {
//null 和undefined 直接字符串形式输出
//无法直接调用toString 可以拼接一下
return res.end(_body + '')
} else {
//其他类型的直接toString
return res.end(_body.toString())
}
}).catch(err => {
//这里为app添加了错误监听
//application继承events模块即可
this.emit('error', err)
})
}
//compose来了
//核心逻辑三件事:1. 越界处理 2. 执行第一个中间件 3. 依次执行后边的中间件
compose(ctx) {
//这里dispatch(也就是next)使用箭头函数
//内部的this就指向了自定义的Application
const dispatch = (index) => {
//越界处理 handleRequest还有then 不能直接return ,要返回promise
if (index === this.middlewares.length) return Promise.resolve()
//获取当前的中间件 最开始是第一个
const middleware = this.middlewares[index]
// 中间件执行需要两个参数
const exec = middleware(ctx, () => dispatch(++index))
//有可能这个方法没有加async,包装一层
//保证返回的是一个promise
//这样handleRequest的then函数就不会报错
return Promise.resolve(exec)
}
return dispatch(0)
}
}
文末碎碎念
面试被问到让说一下你所理解的koa。
我第一反应是说下洋葱模型和手写compose。
冷静下来发现,嗐,忘了怎么写了...
情如风雪无常,
却是一动既殇。
感谢你这么好看还来阅读我的文章,
我是冷月心,下期再见。