导语:实现简单的koa核心代码,便于理解koa原理。顺便学习koa代码的那些骚操做node
建立一个 http 服务,只绑一个中间件。建立 index.js
npm
/** index.js */
const Koa = require('koa')
const app = new Koa()
app.use(ctx => {
ctx.body = 'Hello World'
})
app.listen(8080, function() {
console.log('server start 8080')
})
复制代码
从这段代码中能够看出json
咱们再来看看 koa 源码的目录结构数组
|-- koa
|-- .npminstall.done
|-- History.md
|-- LICENSE
|-- Readme.md
|-- package.json
|-- lib
|-- application.js
|-- context.js
|-- request.js
|-- response.js
复制代码
其中 application.js 是入口文件,打开后能够看到是一个 class。context.js、request.js、response.js 都是一个对象,用来组成上下文 ctx
bash
先编写 application.js 部分代码。建立 myKoa 文件夹,咱们的koa代码将会放在这个文件内。建立 myKoa/application.js
app
经过分析已经知道 application.js 导出一个 class,原形上至少有 listen 和 use 两个方法。listen 建立服务并监听端口号 http服务,use 用来收集中间件。实现代码以下koa
/** myKoa/application.js */
const http = require('http')
module.exports = class Koa {
constructor() {
// 存储中间件
this.middlewares = []
}
// 收集中间件
use(fn) {
this.middlewares.push(fn)
}
// 处理当前请求方法
handleRequest(req, res) { // node 传入的 req、res
res.end('手写koa核心代码') // 为了访问页面有显示,暂时加上
}
// 建立服务并监听端口号
listen(...arges) {
const app = http.createServer(this.handleRequest.bind(this))
app.listen(...arges)
}
}
复制代码
代码很简单。use
把中间件存入 middlewares
。listen
启动服务,每次请求到来调用 handleRequest
异步
context.js、request.js、response.js 都是一个对象,用来组成上下文 ctx
。代码以下async
/** myKoa/context.js */
const proto = {}
module.exports = proto
复制代码
/** myKoa/request.js */
module.exports = {}
复制代码
/** myKoa/response.js */
module.exports = {}
复制代码
三者的关系是: request.js、response.js 两个文件的导出会绑定到 context.js 文件导出的对象上,分别做为 ctx.request 和 ctx.response 使用。函数
koa 为了每次 new Koa()
使用的 ctx 都是相互独立的,对 context.js、request.js、response.js 导出的对象作了处理。源码中使用的是 Object.create()
方法建立一个新对象,使用现有的对象来提供新建立的对象的__proto__
。一会在代码中演示用法
建立ctx以前,再看一下ctx上的几个属性,和他们直接的关系。必定要分清哪些是node自带的,哪些是koa的属性
app.use(async ctx => {
ctx; // 这是 Context
ctx.req; // 这是 node Request
ctx.res; // 这是 node Response
ctx.request; // 这是 koa Request
ctx.response; // 这是 koa Response
ctx.request.req; // 这是 node Request
ctx.response.res; // 这是 node Response
});
复制代码
为何这个设计,在文章后面将会解答
开始建立 ctx。部分代码以下
/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
module.exports = class Koa {
constructor() {
// 存储中间件
this.middlewares = []
// 绑定 context、request、response
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
}
// 建立上下文 ctx
createContext(req, res) {
const ctx = this.context
// koa 的 Request、Response
ctx.request = this.request
ctx.response = this.response
// node 的 Request、Response
ctx.request.req = ctx.req = req
ctx.response.res = ctx.res = res
return ctx
}
// 收集中间件
use(fn) {/* ... */}
// 处理当前请求方法
handleRequest(req, res) {
// 建立 上下文,准备传给中间件
const ctx = this.createContext(req, res)
res.end('手写koa核心代码') // 为了访问页面有显示,暂时加上
}
// 建立服务并监听端口号
listen(...arges) {/* ... */}
}
复制代码
此时就建立了一个基础的上下文 ctx。
实现一个 ctx.request.url
。开始前先考虑几个问题
ctx.req.url
、ctx.request.url
、ctx.request.req.url
三者直接应该始终相等koa 是这样作的
没错,就是 get 语法糖
/** myKoa/request.js */
module.exports = {
get url() {
return this.req.url
}
}
复制代码
此时的
this
指向的是Object.create(request)
生成的对象,并非 request.js 导出的对象
接下来咱们实现 ctx.response.body = 'Hello World'
。当设置 ctx.response.body
时其实是把属性存到了 ctx.response._body
上,当获取 ctx.response.body
时只须要在 ctx.response._body
上取出就能够了 。代码以下
/** myKoa/response.js */
module.exports = {
set body(v) {
this._body = v
},
get body() {
return this._body
}
}
复制代码
此时的
this
指向的是Object.create(response)
生成的对象,并非 response.js 导出的对象
koa 给咱们设置了不少别名,好比 ctx.body
就是 ctx.response.body
有了以前的经验,获取/设置属性就比较容易。直接上代码
/** myKoa/context.js */
const proto = {
get url() {
return this.request.req.url
},
get body() {
return this.response.body
},
set body(v) {
this.response.body = v
},
}
module.exports = proto
复制代码
有没有感受很简单。固然koa上下文部分没有到此结束。看 koa/lib/context.js 代码,在最下面能够看到这样的代码(从只挑选了access
方法)
delegate(proto, 'response')
.access('status')
.access('body')
delegate(proto, 'request')
.access('path')
.access('url')
复制代码
koa 对 get/set 作了封装。用的是 Delegator
第三方包。核心是用的 __defineGetter__
与 __defineSetter__
两个方法。这里为了简单易懂,只是简单封装两个方法代替 Delegator
实现简单的功能。
// 获取属性。调用方法如 defineGetter('response', 'body')
function defineGetter(property, key) {
proto.__defineGetter__(key, function() {
return this[property][key]
})
}
// 设置属性。调用方法如 defineSetter('response', 'body')
function defineSetter(property, key) {
proto.__defineSetter__(key, function(v) {
this[property][key] = v
})
}
复制代码
myKoa/context.js 文件最终修改成
/** myKoa/context.js */
const proto = {}
function defineGetter(property, key) {
proto.__defineGetter__(key, function() {
return this[property][key]
})
}
function defineSetter(property, key) {
proto.__defineSetter__(key, function(v) {
this[property][key] = v
})
}
// 请求
defineGetter('request', 'url')
// 响应
defineGetter('response', 'body')
defineSetter('response', 'body')
module.exports = proto
复制代码
这步很是简单,只须要判断 ctx.body
是否有值,并触发 req.end()
就完成了。相关代码以下
/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
module.exports = class Koa {
constructor() {/* ... */}
// 建立上下文 ctx
createContext(req, res) {/* ... */}
// 收集中间件
use(fn) {/* ... */}
// 处理当前请求方法
handleRequest(req, res) {
const ctx = this.createContext(req, res)
res.statusCode = 404 //默认 status
if (ctx.body) {
res.statusCode = 200
res.end(ctx.body)
} else {
res.end('Not Found')
}
}
// 建立服务并监听端口号
listen(...arges) {/* ... */}
}
复制代码
同理能够处理 header
等属性
中间件接受两个参数,一个上下文ctx
,一个 next
方法。上下文ctx
已经写好了,主要是怎么实现next
方法。
写一个dispatch方法,他的主要功能是:好比传入下标0,找出数组中下标为0的方法middleware
,调用middleware
并传入一个方法next
,而且当next
调用时, 查找下标加1的方法。实现以下
const middlewares = [f1, f2, f3]
function dispatch(index) {
if (index === middlewares.length) return
const middleware = middlewares[index]
const next = () => dispatch(index+1)
middleware(next)
}
dispatch(0)
复制代码
此时就实现了next
方法。
在koa中,是不容许一个请求中一个中间件调用两次next
。好比
app.use((ctx, next) => {
ctx.body = 'Hello World'
next()
next() // 报错 next() called multiple times
})
复制代码
koa 用了一个小技巧。记录每次调用的中间件下标,当发现调用的中间件下标没有加1(中间件下标 <= 上一次中间件下标)时,就报错。修改代码以下
const middlewares = [f1, f2, f3] // 好比中间件中有三个方法
let i = -1
function dispatch(index) {
if (index <= i) throw new Error('next() called multiple times')
if (index === middlewares.length) return
i = index
const middleware = middlewares[index]
const next = () => dispatch(index+1)
middleware(next)
}
dispatch(0)
复制代码
中间件代码基本完成,传入 ctx 、加入 myKoa/application.js
文件。
/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
module.exports = class Koa {
constructor() {/* ... */}
// 建立上下文 ctx
createContext(req, res) {/* ... */}
// 收集中间件
use(fn) {/* ... */}
// 处理中间件
compose(ctx) {
const middlewares = this.middlewares
let i = -1
function dispatch(index) {
if (index <= i) throw new Error('next() called multiple times')
if (index === middlewares.length) return
i = index
const middleware = middlewares[index]
const next = () => dispatch(index+1)
middleware(ctx, next)
}
dispatch(0)
}
// 处理当前请求方法
handleRequest(req, res) {
const ctx = this.createContext(req, res)
this.compose(ctx)
res.statusCode = 404 //默认 status
if (ctx.body) {
res.statusCode = 200
res.end(ctx.body)
} else {
res.end('Not Found')
}
}
// 建立服务并监听端口号
listen(...arges) {/* ... */}
}
复制代码
到此就实现了同步中间件
koa 中使用异步中间件的写法以下
app.use(async (ctx, next) => {
ctx.body = 'Hello World'
await next()
})
app.use(async (ctx, next) => {
await new Promise((res, rej) => setTimeout(res,1000))
console.log('ctx.body:', ctx.body)
})
复制代码
上述代码接受请求后大约1s 控制台打印 ctx.body: Hello World
。能够看出,koa是基于 async/await
的。指望每次 next() 后返回的是一个 Promise
。
同时考虑到中间件变为异步执行,那么handleRequest
应该等待中间件执行完再执行相关代码。那么compose
也应该返回Promise
能够经过async
快速完成 普通函数 =》Promise 的转化。
修改compose
代码和handleRequest
代码
/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
module.exports = class Koa {
constructor() {/* ... */}
// 建立上下文 ctx
createContext(req, res) {/* ... */}
// 收集中间件
use(fn) {/* ... */}
// 处理中间件
compose(ctx) {
const middlewares = this.middlewares
let i = -1
async function dispatch(index) {
if (index <= i) throw new Error('next() called multiple times')
if (index === middlewares.length) return
i = index
const middleware = middlewares[index]
const next = () => dispatch(index+1)
return middleware(ctx, next)
}
return dispatch(0)
}
// 处理当前请求方法
handleRequest(req, res) {
const ctx = this.createContext(req, res)
const p = this.compose(ctx)
p.then(() => {
res.statusCode = 404 //默认 status
if (ctx.body) {
res.statusCode = 200
res.end(ctx.body)
} else {
res.end('Not Found')
}
}).catch((err) => {
console.log(err)
})
}
// 建立服务并监听端口号
listen(...arges) {/* ... */}
}
复制代码
/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
module.exports = class Koa {
constructor() {
// 存储中间件
this.middlewares = []
// 绑定 context、request、response
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
}
// 建立上下文 ctx
createContext(req, res) {
const ctx = this.context
// koa 的 Request、Response
ctx.request = this.request
ctx.response = this.response
// node 的 Request、Response
ctx.request.req = ctx.req = req
ctx.response.res = ctx.res = res
return ctx
}
// 收集中间件
use(fn) {
this.middlewares.push(fn)
}
// 处理中间件
compose(ctx) {
const middlewares = this.middlewares
let i = -1
async function dispatch(index) {
if (index <= i) throw new Error('multi called next()')
if (index === middlewares.length) return
i = index
const middleware = middlewares[index]
const next = () => dispatch(index+1)
return middleware(ctx, next)
}
return dispatch(0)
}
// 处理当前请求方法
handleRequest(req, res) {
const ctx = this.createContext(req, res)
const p = this.compose(ctx)
p.then(() => {
res.statusCode = 404 //默认 status
if (ctx.body) {
res.statusCode = 200
res.end(ctx.body)
} else {
res.end('Not Found')
}
}).catch((err) => {
console.log(err)
})
}
// 建立服务并监听端口号
listen(...arges) {
const app = http.createServer(this.handleRequest.bind(this))
app.listen(...arges)
}
}
复制代码
/** myKoa/context.js */
const proto = {}
function defineGetter(property, key) {
proto.__defineGetter__(key, function() {
return this[property][key]
})
}
function defineSetter(property, key) {
proto.__defineSetter__(key, function(v) {
this[property][key] = v
})
}
// 请求
defineGetter('request', 'url')
// 响应
defineGetter('response', 'body')
defineSetter('response', 'body')
module.exports = proto
复制代码
/** myKoa/request.js */
module.exports = {
get url() {
return this.req.url
}
}
复制代码
/** myKoa/response.js */
module.exports = {
set body(v) {
this._body = v
},
get body() {
return this._body
}
}
复制代码