介绍
koa 源码学习,手写 koa 核心
# koa 源码学习
koa 默认就是对我们的 node 原生的 http 服务进行了封装
# 1.目录结构
application.js 整个的应用
context.js 代表的是上下文
request.js 用于扩展请求的
response.js 用于扩展响应的
# 2.核心
koa 的核心
- 封装了 ctx
- 提供了一个中间件处理流程
- 提供了更好的错误处理
# 1.封装了 ctx
原生的 http 服务会有两个参数,req 和 res,为了封装 ctx 得先分别对 req 和 res 进行扩展
context.js、request.js、response.js 三个文件相辅相成
request.js 文件
import url from 'url'
export const request: Record<any, any> = {
get path() {
const { pathname } = url.parse(this.req.url)
return pathname
},
get url(): string {
//这就是为什么在request身上加上一个req属性,为了在取值的时候可以快速获取到原生的req
//为啥使用原型链方式,因为可以通过这很方便的拿到this,谁调用this就是谁
return this.req.url
},
get query() {
const { query } = url.parse(this.req.url, true)
return query
},
}
response.js
export const response: Record<any, any> = {
_body: undefined,
get body() {
return this._body
},
set body(newValue) {
this.res.statusCode = 200
/* console.log(this.res.statusCode) */
this._body = newValue
},
}
context.js
//源码中koa用的过时的api,但兼容性很好
export const ctx = {}
//封装一个函数
function defineGetter(target: 'request' | 'response', key: PropertyKey) {
Object.defineProperty(ctx, key, {
get() {
return this[target][key]
},
//默认值为false,所以改了一次后,第二次就不能改了
configurable: true,
//由于数据描述符默认值基本都是false,所以得手动设置为true
enumerable: true,
})
}
function defineSetter(target: 'request' | 'response', key: PropertyKey) {
/* const descriptor=Object.getOwnPropertyDescriptor(ctx,key) */
Object.defineProperty(ctx, key, {
/* get:descriptor?.get, */
set(v) {
this[target][key] = v
},
})
}
defineGetter('request', 'path')
defineGetter('request', 'query')
defineGetter('response', 'body')
defineSetter('response', 'body')
/* Object.defineProperty(ctx,'body',{
get(){
return this.response.body
},
set(v){
this.response.body=v
}
}) */
/* console.log(Object.getOwnPropertyDescriptor(ctx,'body')) */
//这样对象的方式比较麻烦,不易于扩展
/* export const ctx:Record<any,any>={
get path(){//通样的谁调用指向谁,这里指向application的请求ctx
return this.request.path
}
} */
//尝试用proxy来做,遇到问题,this无法成功指向原型链下一层,this总是指向proxy本身
/* export const ctx=new Proxy({},{
get(target,p,receiver){
console.log(target)
console.log('****',p)
console.log('****')
}
}) */
这三个模块都应用的代理模式,通过 ctx 代理到 response 或者 request
context 模块中我用的是 Object.defineProperty 进行代理
object.definerProperty()
基本上数据描述符的默认值都为 false
数据描述符 configurable 默认值为 false,所以设置了一次后就不能再设置了
当且仅当该属性的 configurable
键值为 true
时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
为啥不用 proxy 进行代理?请看下面 application 模块
# 2.中间件处理流程
import http from 'http'
//用户不能直接修改下面三个对象,可以通过原型模式让他们拿到值,修改的是自身的
import { ctx } from './context'
import { request } from './request'
import { response } from './response'
import { EventEmitter } from 'events'
declare type Middleware = {
(ctx: Application['context'], next: () => any): any
}
//获取函数参数类型元组
declare type FunctionParamsType<T extends (...args: any[]) => any> = T extends (
...args: infer P
) => any
? P
: never
export default class Application extends EventEmitter {
//保证每次创建应用都是独立的上下文
public context = Object.create(ctx)
public request = Object.create(request)
public response = Object.create(response)
public middlewares: Middleware[] = []
public use(middleware: Middleware) {
this.middlewares.push(middleware)
}
constructor() {
super()
}
public handelRequest: http.RequestListener = (req, res) => {
let ctx = this.createContext(req, res)
//先默认状态码为404
res.statusCode = 404
//顺序不要搞错,先默认状态码,再执行用户函数才能修改状态码,搞反了就会一直404
this.compose(ctx)
.then(() => {
let body = ctx.body
if (body) {
res.end(body)
} else {
res.end('<h1>Not found</h1>')
}
})
.catch((e) => {
//继承自EventEmitter,触发监听的事件
this.emit('error', e)
})
}
public listen(...args: FunctionParamsType<http.Server['listen']>) {
let server = http.createServer(this.handelRequest)
server.listen(...args)
}
public createContext(
req: http.IncomingMessage,
res: http.ServerResponse
): this['context'] {
//再进行一次创建对象,以当前应用的为原型,保证每个请求之间上下文独立
let ctx = Object.create(this.context)
let request = Object.create(this.request)
let response = Object.create(this.response)
//添加属性,短写的都是原生的
ctx.request = request //自己封装的
ctx.request.req = ctx.req = req //原生的
//响应
ctx.response = response //自己封装的
ctx.response.res = ctx.res = res //原生的
return ctx
}
//组合函数,把所有中间件组成一个大的promise
public compose(ctx: typeof this.context) {
//闭包,防止多次调用next()
let index = -1
//递归
const dispatch = (i: number): Promise<any> => {
if (i <= index) {
//直接抛出错误,直接return了
return Promise.reject('next() call multiples times')
}
index = i
if (this.middlewares.length === i) return Promise.resolve()
let middleware = this.middlewares[i]
//执行下一个,包装成promise
try {
//捕获执行过程中的错误
return Promise.resolve(middleware(ctx, () => dispatch(i + 1)))
} catch (error) {
return Promise.reject(error)
}
}
//执行第一个
return dispatch(0)
//将功能组合再一起依次执行
}
}
下面来简单解释下
首先解决以下问题
1.每次请求的上下文应该是一个独立的上下
2.每个应用创建的时候使用的上下文应该是不同的
**所以 Application 类开头和 createContext 方法都使用 Object.create()方法创建新对象并指定原型,这样就相互关联起来了,由于原型链又有遮蔽效应所以相互影响小。**这就很妙
现在简单描述下请求过程,当请求过来,走 handelRequest 方法
然后创建一个新的上下文
//使用
app.use(async (ctx) => {
ctx.body = '123'
})
我们在使用过程中会调用 body,而 ctx 上没有就会通过原型链去查找最终找到 response 模块上,
export const response: Record<any, any> = {
_body: undefined,
get body() {
return this._body
},
set body(newValue) {
this.res.statusCode = 200
/* console.log(this.res.statusCode) */
this._body = newValue
},
}
然后我们在 response 模块进行处理,我们得通过 this 拿到当前的调用对象 ctx。关键就是 this 指向问题,所以用 proxy 代理就不行,proxy 代理不能代理原型上的属性或方法。而通过描述符即 Descriptor 可以。
# 中间件
中间件是发布订阅模式的产物,我们使用多次 use,通过一个数组收集起来,当请求来了再调用,为了能够处理异步返回一个大的 promise 进行包装
通过递归将 promise 串在一起,这就是所谓的洋葱模型
注意:
- koa 默认是洋葱模型调用上一个 next 会走下一个中间件函数
- 异步怎么做呢 koa 中所有的异步操作都要基于 promise
- koa 内部会将所有的中间件进行组合操作组合成了 一个大的 promise 只要从开头走到了结束就算完成
- await 和 return 都会等待 promise 执行完毕,return 后面的代码不会执行
- koa 中的中间件必须增加 await next() 或者 return next ( )否则异步逻辑可能出错
# 3.错误处理
在 compose 方法中,每执行都要捕获一次错误,然后在 handelRequest 方法中,对大的 promise 进行捕获错误,通过 node 的事件模块告诉给用户,application 类继承了 node 的事件类 EventEmitter
public handelRequest:http.RequestListener=(req,res)=>{
let ctx=this.createContext(req,res)
//先默认状态码为404
res.statusCode=404
//顺序不要搞错,先默认状态码,再执行用户函数才能修改状态码,搞反了就会一直404
this.compose(ctx).then(()=>{
let body=ctx.body
if(body){
res.end(body)
}else{
res.end('<h1>Not found</h1>')
}
}).catch(e=>{
//继承自EventEmitter,触发监听的事件
this.emit('error',e)
})
用户用过 on 方法进行监听
app.on('error', (e) => {
console.log('********', e)
})