内容简介:创建一个 http 服务,只绑一个中间件。从这段代码中可以看出我们再来看看 koa 源码的目录结构
创建一个 http 服务,只绑一个中间件。 创建 index.js
/** 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')
})
复制代码
从这段代码中可以看出
- Koa 是一个构造函数
- Koa 的原形上至少有 ues、listen 两个方法
- listen 的参数与 http 一致
- ues 接受一个方法,在用户访问的时候调用,并传入上下文 ctx (这里先不考虑异步与next,一步步实现)
我们再来看看 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
启动http服务
先编写 application.js 部分代码。创建 myKoa 文件夹,我们的koa代码将会放在这个文件内。 创建 myKoa/application.js
通过分析已经知道 application.js 导出一个 class,原形上至少有 listen 和 use 两个方法。listen 创建服务并监听端口号 http服务,use 用来收集中间件。实现代码如下
/** 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
创建 ctx (一)
context.js、request.js、response.js 都是一个对象,用来组成上下文 ctx
。代码如下
/** 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 (二)
获取上下文上的属性
实现一个 ctx.request.url
。开始前先考虑几个问题
- request.js 导出的是一个对象,不能接受参数
-
ctx.req.url、ctx.request.url、ctx.request.req.url三者直接应该始终相等
koa 是这样做的
- 第一步 ctx.request.req = ctx.req = req
- 访问 ctx.request.url 转成访问 ctx.request.req.url
没错,就是 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 导出的对象
设置 ctx 别名
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 显示在页面上
这步非常简单,只需要判断 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) {/* ... */}
}
复制代码
代码展示
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() {
// 存储中间件
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)
}
}
复制代码
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
复制代码
request.js
/** myKoa/request.js */
module.exports = {
get url() {
return this.req.url
}
}
复制代码
response.js
/** myKoa/response.js */
module.exports = {
set body(v) {
this._body = v
},
get body() {
return this._body
}
}
复制代码
以上所述就是小编给大家介绍的《一步步实现koa核心代码》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- WTC测评:核心团队从业经验,核心代码尚未开源
- Laravel核心代码学习 -- 外观模式
- Laravel核心代码学习--HTTP内核
- Laravel核心代码学习 -- 扩展用户认证系统
- EventBus 源码详细分析:手写 EventBus 核心代码
- Laravel核心代码学习--用户认证系统(基础介绍)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Designing Data-Intensive Applications
Martin Kleppmann / O'Reilly Media / 2017-4-2 / USD 44.99
Data is at the center of many challenges in system design today. Difficult issues need to be figured out, such as scalability, consistency, reliability, efficiency, and maintainability. In addition, w......一起来看看 《Designing Data-Intensive Applications》 这本书的介绍吧!