内容简介:创建一个 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核心代码学习--用户认证系统(基础介绍)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。