本次的文章是之前 koa
专题的延续,计划书写两篇文章,本篇从零实现一个简单版的 koa
框架(里面可能涉及一点 node
知识,不会太讲,大家如果遇到不了解的可以自行百度查看,也可以看官网文档了解使用)。包括上下文 ctx
组合 req
, res
的实现,中间件机制的实现。第二篇写下 bodyparser
、 router
中间件的简单实现,理解其原理。
koa 使用
test.js
代码语言:txt复制const Koa = require('koa')
const app = new Koa()
let port = 3000
app.use(async (ctx, next) => {
ctx.body = '测不准1111'
// 如果不调用 next, 页面会显示 测不准1111
await next()
})
app.use(async (ctx, next) => {
ctx.body = '测不准2222'
})
// 出现异常 端口号自动 1
app.on('error', () => {
app.listen( port)
})
app.listen(port, () => {
console.log(`正在监听 ${PORT} 端口`)
})
从上面的代码我们可以看出来:
Koa
是一个类,有use
和listen
方法(on
监听器明显是发布订阅)- 由于我们启动了一个服务,所以要用到
http
模块 - 页面显示了‘测不准2222’,所以
ctx.body
的结果会赋值到r
es.end()` 中 use
中的两个函数都是中间件,调用next
方法执行下一个
初始化项目结构
- package.json
主要配置下 main
字段,包的入口
{
...
"main": "./lib/application",
...
}
- 目录结构保持一致
编写 application
文件
- 初始化结构
const EventEmitter = require('events')
const http = require('http')
// 继承 可以使用发布订阅
class Koa extends EventEmitter{
constructor() {
super()
}
use() {
}
listen() {
}
}
module.exports = Koa
- listen 中创建服务
// es7 保证this。也可以使用bind
handleRequest = (req, res) => {
}
// 接收传的 端口号 和 回调
listen(...args) {
const server = http.createServer(this.handleRequest)
server.listen(...args)
}
- 初始化
context request response
三个文件,application
中引入request
、response
两个文件是对原生req
、res
的拓展
const context = {
}
module.exports = context
------------------------------
const request = {
}
module.exports = request
---------------------------------
const response = {
}
module.exports = response
- 创建新的上下文、请求、响应
constructor() {
super()
/**
* Koa 使用时可以创建多个 koa实例
* let app1 = new Koa()
* let app2 = new Koa()
* 因为我们创建的上下文和请求响应是引用类型,如果改了一个,也会影响其他的实例。
* 所以在创建时,就要新创建一个,互不干扰
* 本质 this.context.__proto__ = context
*/
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
}
use(middleware) {
}
//
createContext(req, res) {
}
handleRequest = (req, res) => {
// 创建服务的时候,创建上下文
const ctx = this.createContext(req, res)
}
避免创建的上下文,请求响应冲突,也需要新创建对象,同时改写 ctx
// 在创建上下文中 改写 ctx
createContext(req, res) {
const ctx = Object.create(this.context)
const request = Object.create(this.request)
const response = Object.create(this.response)
// res.xx req.xx 都是原生的
ctx.request = request
ctx.request.req = req = ctx.req = req
ctx.response = response
ctx.response.res = ctx.res = res
return ctx
}
- 中间件操作
我们知道中间件是通过 app.use
形式注入的,可以写多个,所以用数组形式存储,执行时按顺序执行。因为 koa
中间件是洋葱模型,所以我们只是返回第一个中间件的执行,其余的在第一个的执行过程中就被执行了,我们把所有中间件拼接成 promise
返回执行;同时我们还要注意使用中间件时一定要 async await
,才能确保达到想要的结果
constructor() {
...
this.middlewares = []
}
// 中间件存储
use(middleware) {
this.middlewares.push(middleware)
}
// 洋葱方式执行
compose() {
let index = -1
const dispatch = i => {
// 传入的 i 值是不变的,如果在一个中间件中多次调用 await next(),那么内部中间件执行完执行当前中间件下一个 next 时,传入的i一定小于 index,大家可以自行打印
if (i <= index) return Promise.reject('不要使用多个next')
index = i
// 执行到最后一项退出
if (i == this.middlewares.length) return Promise.resolve()
// 使用 Promise.resolve 包裹执行中间件
let fn = this.middlewares[i]
return Promise.resolve(fn(ctx, () => dispatch(i 1)))
}
return dispatch(0)
}
处理完中间件(执行完毕)最后返回给页面 ctx.body
即可
handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
// 默认 404,当修改 ctx.body 时,改成 200
// ctx.body 是在中间件中设置的,确保执行了
ctx.status = 404
this.compose(ctx).then(() => {
// koa 可以直接读流
if (ctx.body instanceof Stream) {
ctx.body.pipe(res)
} else if(typeof ctx.body == 'object' && ctx.body) {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
res.end(JSON.parse(ctx.body))
} else if (ctx.body) {
res.setHeader('Content-Type', 'text/plain;charset=utf-8')
res.end(ctx.body)
} else {
res.end('Not Found')
}
}).catch(err => {
this.emit('error', err)
})
}
代理 ctx、request、response
添加一个测试中间件
代码语言:txt复制app.use(async (ctx, next) => {
console.log(ctx.url)
console.log(ctx.request.url)
console.log(ctx.request.req.url) // 直接调用原生res
await next()
})
打印结果
因为我们获取的数据都在原生的 req
,res
中,我们想要获取 ctx.url
,实际获取的是 req.rul
,这里我们就用到了类似 defineProperty
的代理,熟悉 vue
的小伙伴应该不陌生。
- request.jsconst request = { get url() { // 谁调用的 this 就是谁 => ctx.request.req.url return this.req.url } } // 我们可以对每个属性进行 get 请求书写,但是比较麻烦 const url = require('url') const request = { get url() { return this.req.url }, get query() { return url.parse(this.url, true) }, get path() { return url.parse(this.url).pathname } }
- response.js
我们把修改 body
的操作放在 response
文件中,因为 ctx.body
作为 res.end
的值得。默认的状态码是 404
,在修改 body
的函数中设置状态码为 200
const response = {
// 我们可以设置多个 ctx.body = 'xxx',会以最后一个为准
// 所以一定是操作某个值,中间件执行完成后才执行 res.end()
_body: undefined,
get body() {
return this._body
},
set body(val) {
this.res.statusCode = 200
this._body = val
}
}
module.exports = response
- context.js
const context = {}
// 这里使用函数操作,这三种代理方式是一样的,但是 __defineGetter__
// 不推荐使用了,但是源码是这种方式
function defineGetter(target, key) {
context.__defineGetter__(key, function() {
return this[target][key]
})
}
function defineSetter(target, key) {
context.__defineSetter__(key, function(val) {
this[target][key] = val
})
}
defineGetter('request', 'url')
defineGetter('request', 'query')
defineGetter('request', 'path')
defineGetter('request', 'body')
defineSetter('response', 'body')
module.exports = context
这时我们再看执行的 test.js
打印就是正常的了。大家如果感兴趣可以自己发布到 npm
上,当做是一个学习分享了。
koa
的源码相对较少,比较简单;相比来说 express
的内容比较多,像路由这种都封装到内部了,而 koa
只是提供了个架子,辅助操作都是用中间件的形式。下一篇我们介绍下几个中间件的实现。
有疑问可以添加小编 wx: wajh123654789