1 原生实现
1.1 启动一个服务
node起一个服务有多简单,相信只要会上网,都能搜到类似下面的代码快速启动一个服务。
const http = require('http')
const handler = ((req, res) => {
res.end('Hello World!')
})
http
.createServer(handler)
.listen(
8888,
() => {
console.log('listening 127.0.0.1:8888')
}
)
访问127.0.0.1:8888就可以看到页面上出现了'hello world!
。随后就会发现修改路由还是请求方式,都只能拿到这样一个字符串。
curl 127.0.0.1:8888
curl curl -X POST http://127.0.0.1:8888
curl 127.0.0.1:8888/about
这个时候肯定就会去找相关文档,然后发现刚刚回调函数的 req
居然内有乾坤。我们可以使用 method
属性和 url
属性针对不同的方法和路由返回不同的结果。于是很容易就想到类似下面的写法:
const http = require('http')
const handler = ((req, res) => {
let resData = '404 NOT FOUND!'
const { method, path } = req
switch (path) {
case '/':
if (method === 'get') {
resData = 'Hello World!'
} else if (method === 'post') {
resData = 'Post Method!'
}
break
case '/about':
resData = 'Hello About!'
}
res.end = resText
})
http
.createServer(handler)
.listen(
8888,
() => {
console.log('listening 127.0.0.1:8888')
}
)
但是一个服务不可能只有这么几个接口跟方法啊,总不能每加一个就增加一个分支吧,这样 handler
得变得多长多冗余,于是又很容易想到抽离 handler
,将 path
和 method
解耦。
1.2 策略模式解耦
如何解耦呢?从在新手村的代码中可以发现策略模式刚好可以拿来解决这个问题:
const http = require('http')
class Application {
constructor () {
// 收集route和method对应的回调函数
this.$handlers = new Map()
}
// 注册handler
register (method, path, handler) {
let pathInfo = null
if (this.$handlers.has(path)) {
pathInfo = this.$handlers.get(path)
} else {
pathInfo = new Map()
this.$handlers.set(path, pathInfo)
}
// 注册回调函数
pathInfo.set(method, handler)
}
use () {
return (request, response) => {
const { url: path, method } = request
this.$handlers.has(path) && this.$handlers.get(path).has(method)
? this.$handlers.get(path).get(method)(request, response)
: response.end('404 NOT FOUND!')
}
}
}
const app = new Application()
app.register('GET', '/', (req, res) => {
res.end('Hello World!')
})
app.register('GET', '/about', (req, res) => {
res.end('Hello About!')
})
app.register('POST', '/', (req, res) => {
res.end('Post Method!')
})
http
.createServer(app.use())
.listen(
8888,
() => {
console.log('listening 127.0.0.1:8888')
}
)
1.3 符合DRY原则
但是这个时候就会发现:
- 如果手抖把
method
方法写成了小写,因为Http.Request.method
都是大写,无法匹配到正确的handler
,于是返回'404 NOT FOUND'
。 - 如果我想在响应数据前增加一些操作,比如为每个请求增加一个时间戳,表示请求的时间,就必须修改每个
register
中的handler
函数,不符合DRY原则
此时再修改一下上面的代码,利用 Promise
实现按顺序执行 handler
。
const http = require('http')
class Application {
constructor() {
// 收集route和method对应的回调函数
this.$handlers = new Map()
// 暴露get和post方法
this.get = this.register.bind(this, 'GET')
this.post = this.register.bind(this, 'POST')
}
// 注册handler
register(method, path, ...handlers) {
let pathInfo = null
if (this.$handlers.has(path)) {
pathInfo = this.$handlers.get(path)
} else {
pathInfo = new Map()
this.$handlers.set(path, pathInfo)
}
// 注册回调函数
pathInfo.set(method, handlers)
}
use() {
return (request, response) => {
const { url: path, method } = request
if (
this.$handlers.has(path) &&
this.$handlers.get(path).has(method)
) {
const _handlers = this.$handlers.get(path).get(method)
_handlers.reduce((pre, _handler) => {
return pre.then(() => {
return new Promise((resolve, reject) => {
_handler.call({}, request, response, () => {
resolve()
})
})
})
}, Promise.resolve())
} else {
response.end('404 NOT FOUND!')
}
}
}
}
const app = new Application()
const addTimestamp = (req, res, next) => {
setTimeout(() => {
this.timestamp = Date.now()
next()
}, 3000)
}
app.get('/', addTimestamp, (req, res) => {
res.end('Hello World!' + this.timestamp)
})
app.get('/about', addTimestamp, (req, res) => {
res.end('Hello About!' + this.timestamp)
})
app.post('/', addTimestamp, (req, res) => {
res.end('Post Method!' + this.timestamp)
})
http
.createServer(app.use())
.listen(
8888,
() => {
console.log('listening 127.0.0.1:8888')
}
)
1.4 降低用户心智
但是这样依旧有点小瑕疵,用户总是在重复创建 Promise
,用户可能更希望无脑一点,那我们给用户暴露一个 next
方法,无论在哪里执行 next
就会进入下一个 handler
,岂不美哉!!!
class Application {
// ...
use() {
return (request, response) => {
const { url: path, method } = request
if (
this.$handlers.has(path) &&
this.$handlers.get(path).has(method)
) {
const _handlers = this.$handlers.get(path).get(method)
_handlers.reduce((pre, _handler) => {
return pre.then(() => {
return new Promise(resolve => {
// 向外暴露next方法,由用户决定什么时候进入下一个handler
_handler.call({}, request, response, () => {
resolve()
})
})
})
}, Promise.resolve())
} else {
response.end('404 NOT FOUND!')
}
}
}
}
// ...
const addTimestamp = (req, res, next) => {
setTimeout(() => {
this.timestamp = new Date()
next()
}, 3000)
}
2 Koa核心源码解析
上面的代码一路下来,基本上已经实现了一个简单中间件框架,用户可以在自定义中间件,然后在业务逻辑中通过 next()
进入下一个 handler
,使得整合业务流程更加清晰。但是它只能推进中间件的执行,没有办法跳出中间件优先执行其他中间件。比如在koa中,一个中间件是类似这样的:
const Koa = require('koa');
let app = new Koa();
const middleware1 = async (ctx, next) => {
console.log(1);
await next();
console.log(2);
}
const middleware2 = async (ctx, next) => {
console.log(3);
await next();
console.log(4);
}
const middleware3 = async (ctx, next) => {
console.log(5);
await next();
console.log(6);
}
app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
app.use(async(ctx, next) => {
ctx.body = 'hello world'
})
app.listen(8888)
可以看到控制台输出的顺序是1, 3, 5, 6, 4, 2,这就是koa经典的洋葱模型。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!