引言
在Node.js众多web框架中,koa可谓一枝独秀,接下来我们从零开始实现半个koa,为刚开始接触Node.js 和阅读koa源码的童鞋提供一些思路。
搭建基础服务框架
演变过程
在Node的世界里创建服务器可以说特别简单:
const http = require('http');
http.createServer((req, res) => {
res.end('hello world!');
}).listen(8080)
如果只是对外提供一个接口,这就已经足够了。但是一个服务需要根据请求路径返回不同信息,这时就需要对请求进行划分。最简单的就是根据if 或者 switch 判断路径,然后调用不同的方法。
const http = require('http');
const homeHandle = (req, res)=> {
res.end('home');
}
http.createServer(function(req, res){
switch(req.url){
case '/home':
homeHandle();
break;
default:
res.end('default');
break;
}
}).listen(8686)
抽离路由
如果只有几个路由这样处理无可厚非,但是如果有几十个路由呢?这样处理很显然是不及格的。我们需要把路由抽出来,抽出来方法有不少。
比如我们可以在一个路由文件保存一个数组,在数组里定义路径和对应方法,有请求时读取文件找到对应处理方法,此l类型可以把转变成将路由和对应方法保存到数据库或者Redis。
比如我们直接在代码里用一个数组保存路由和对应方法。接下来我们就直接在代码定义一个数组来保存路由。
const http = require('http');
// 对应请求方法
const handleHello = (req,res)=> {
res.end('hello')
}
class Router {
// 保存路由已经处理数组
routerArray = []
register(method, routePath, routeHandle){
this.routerArray.push({ method, routePath, routeHandle })
}
get(routePath, routeHandle){
this.register('get',routePath, routeHandle)
}
post(routePath, routeHandle){
this.register('post', routePath, routeHandle)
}
handle(req, res){
const existed = this.routerArray.find(item => item.routePath === req.url)
existed.routeHandle(req, res)
}
}
const requestHandle = (req, res) => {
const router = new Router();
router.get('/hello', handleHello)
router.handle(req, res)
}
http.createServer(requestHandle).listen(8686)
现在我们把路由模块放到单独类里,如果你想你随时可以把它放到单独文件里。\
新增模块需求
现在又来新需求了,比如必须要在每个请求前加上日志、权限控制等模块。 这时最直接的就是就是在执行路由处理前加上日志处理和权限控制
const router = new Router();
router.get('/hello', handleHello)
handleLogger(); // 日志处理
handleAuth(); // 权限处理
router.handle(req, res)
服务拆分
随着项目的进行、人数的增加、需求的增加,所有的业务处理和基础功能处理(比如每个实现路由的函数都希望请求是被格式化过的)都放到同一个函数里项目就就变得很难管理,这时需要进行拆分了,我们把基础服务框架从中抽出来。
这时基础服务框架需要保留哪些功能,需要提供哪些功能就是各个web服务框架的区别了,比如Express 把路由模块保留了而koa则把路由也做成中间件。
现在我们把上面路由、日志都拆离基础服务框架,而基础服务框架需要支持对这些功能的拓展,最直接的思路就是在基础服务框架维护一个数组队列,其他模块可以通过框架暴露出来的接口把自己加入这个队列,当有请求过来时,可以依次调用这些模块功能。代码如下:
const http = require('http');
const MyServer = function () {
this.middles = []; // 模块队列
// 通过此接口将自己加入队列
this.use = (middleFn)=> {
this.middles.push(middleFn)
}
this.requestHandle = (req, res) => {
this.middles.map(midItem => {
midItem(req, res)
})
}
this.listen = (port)=>{
http.createServer(this.requestHandle).listen(port)
}
}
module.exports = MyServer;
调用use方法将需要执行的函数注册到服务中,具体方法如下:
const loggerMiddle = (req, res) => {
console.log('enter logger');
}
const m = new mServer()
m.use(loggerMiddle)
m.listen(8686)
这种链式调用能满足注册进来的模块被执行,但是却只能依次执行里面的代码,不能在外层进行任何控制。
比如需要知道函数具体执行时间,可能就需要在开头和结尾都插入一个模块,如果能做到在外层直接控制下一个模块执行的时机,那就不需要插入两段代码来完成一个功能。
实现思路:把下一个需要执行的模块传递个当前模块,当前模块就可以随心所欲的控制下一个模块执行的时机了。只需对上面requestHandle代码改造如下:
this.requestHandle = (req, res) => {
let midIndex = 0;
const callNext = ()=> {
midIndex++;
if(midIndex == this.middles.length) return;
this.middles[midIndex](req, res, callNext)
}
this.middles[midIndex](req, res, callNext)
}
而对应需要注册的模块也需要改成
const loggerMiddle = (req, res, next) => {
console.log('enter logger');
next();
console.log('leave');
}
而请求都是异步的,所以上述requestHandle代码需要进行少许改动
this.requestHandle = (req, res) => {
let midIndex = 0;
const callNext = ()=> {
midIndex++;
if(midIndex == this.middles.length) return Promise.resolve() ;
Promise.resolve(this.middles[midIndex](req, res, callNext))
}
Promise.resolve(this.middles[midIndex](req, res, callNext))
}
上述代码在功能上已经完成,但是从写法上有待改进
this.handleRequest = (req, res) => {
const dispatch = (index) => {
if(index >= this.middles.length) return Promise.resolve()
const fn = this.middles[index]
return Promise.resolve(fn(req, res, ()=>dispatch(index+1) ))
}
dispatch(0)
}
上下文实现
现在各个之间模块调用可控了,但是现在需要在各个模块之间传递一些数据。
思路其实很简单就是定义一个对象,把request和response也包含进去,然后用这个对象在各个模块之间传递:
this.createContext = (req, res) => {
const context = {}
context.req = req;
context.res = res;
return context;
}
this.handleMiddles = (ctx) => {
const dispatch = (index) => {
if(index >= this.middles.length) return Promise.resolve()
const fn = this.middles[index]
return Promise.resolve(fn(ctx, ()=>dispatch(index+1) ))
}
dispatch(0)
}
const handleRequest = async (req, res) => {
// 定一个对象将request 和response包含进去
const ctx = this.createContext(req, res);
// 使用该对象在模块之间传递
await this.handleMiddles(ctx)
}
而模块定义则是
const loggerMiddle = async (ctx, next) => {
console.log('enter logger');
await next();
console.log('leave logger');
}
至此koa 的骨架已经完成,剩下的就是将context 可以提供的内容就行拓展,以及各个错误处理。
结束
第一次阅读koa源码时没能掌握主要脉络,甚至工作2年后,再回头阅读才猛然惊醒,希望此文章能给初学者一点思路和启发。