当前位置: 首页 > 后端技术 > Node.js

Express的原理——手写一个

时间:2023-04-03 13:57:51 Node.js

序列:因为公司的Node业务是基于一个小框架写的,这个框架是公司前同事基于Express中间件思想写的一个Socket小框架,阅读其sourcecode之后,对Express的中间件思想有了更深入的了解,然后手写一个Express框架作为学习输出。看了同事的代码和Express的源码,发现Express的核心是中间件的思想,其次是封装了更丰富的API供我们使用。废话不多说,让我们一步步实现一个可用的Express。这篇文章的目的是为了验证学习的收获,大致分为:服务端监控原理路由解析匹配中间件的定义和核心next()方法的使用错误处理中间件的定义和内置API的使用封装文本:在手写框架之前,我们需要回顾一下Express的简单使用,从而根据它为我们提供的API来实现其相应的功能:新建一个app.js文件,添加如下代码://app.jsletexpress=require('express');letapp=express();app.listen(3000,function(){console.log('listen3000port...')})现在在命令行执行:nodeapp.js可以看到,程序已经开始在我们的后台运行了。当我们向其添加路由时:letexpress=require('Express');letapp=express();app.get('/hello',function(req,res){res.setHeader('Content-Type','text/html;charset=utf-8')res.end('我是新添加的路由,只有get方法可以访问我~')})app.listen(3000,function(){console.log('listen3000port...')})重新启动:在命令行执行启动命令:(每次修改代码,都需要重新执行脚本)并访问浏览器本地3000端口:这里的乱码是因为:服务器不知道你要怎么解析输出,所以需要指定响应头:letexpress=require('Express');letapp=express();app.get('/hello',function(req,res){res.setHeader('Content-Type','text/html;charset=utf-8')//指定utf-8res.end('我是新来的添加路由,只有get方法可以访问我~')})app.post('/hi',function(req,res){res.end('我是新添加的路由,只有post方法可以访问我~')})app.listen(3000,function(){console.log('listen3000port...')})我们先来实现上面的函数:1.服务器监听原理新建一个MyExpress.js并定义一个入口函数:lethttp=require('http');functioncreateApplication(){//定义入口函数,初始化操作letapp=function(req,res){}//定义监听方法app.listen=function(){//通过http模块创建一个服务器实例,该实例的参数是一个函数,它有两个参数,分别是req请求对象和res响应对象letserver=http.createServer(app);//传入参数列表,为实例监听配置项server.listen(...arguments);}//return函数returnapp}module.exports=createApplication;现在我们代码中的app.listen()其实已经实现了,导入的express可以换成我们写的MyExpress来验证:letexpress=require('Express');//换成letexpress=require('./MyExpress');2、路由解析匹配:接下来我们看routes中的示意图根据上图,路由数组中有多层,每一层包含三个属性,method、path、handler分别对应请求方法、请求路径、执行回调函数。代码如下:consthttp=require('http')functioncreateApp(){letapp=function(req,res){};app.routes=[];//定义路由数组letmethods=http.METHODS;//获取所有请求方法,比如常见的GET/POST/DELETE/PUT...methods.forEach(method=>{method=method.toLocaleLowerCase()//小写转换app[method]=function(path,handler){letlayer={method,path,handler,}//将每个请求保存到数组中的路由app.routes.push(layer)}})//定义监听方法app.listen=function(){letserver=http.createServer(应用程序);server.listen(...arguments)}returnapp;}module.exports=createApp这里仔细想想,脚本启动的时候,我们把所有路由保存到routes,打印路由,可以看到:是不是和上面的一模一样上图中的一个~此时我们访问对应的路径,发现浏览器一直在转圈圈。这是因为我们刚刚完成了保存操作,保存了routes中的所有layer。那我们访问的时候怎么调用对应的句柄函数呢?思路:我们在访问路径的时候,也就是拿到请求对象req的时候,需要遍历存储层,匹配访问的方法和路径。如果匹配成功,则执行相应的handler函数代码如下:consturl=require('url')......)//获取请求方法letpathName=url.parse(req.url,true).pathname//获取请求路径console.log(app.routes);app.routes.forEach(layer=>{let{method,path,handler}=layer;if(method===reqMethod&&path===pathName){handler(req,res)}});};......至此,路由的定义和解析就基本完成了。3.中间件的定义和使用接下来,就是重点了,中间件的思想。中间件的定义其实和路由的定义类似,也是存在路由中的。但是,它必须放在所有布线层之前。原理如下:其中middle1、middle2、middle3都是中间件,middle3放在最后??。作为错误处理中间件,每次访问服务器,所有的请求都要先经过middle1和middle2处理。在中间件中,有一个next方法。其实接下来的方法就是将layer的indexflag向后移动一位,进行匹配。如果匹配成功,回调将被执行。如果匹配失败,则向后继续匹配,有点像回调队列。核心的next()方法接下来我们实现一个next方法:因为只有中间件的回调才有next方法,但是我们的中间件和路由的layer层在routes中是存在的,所以我们首先要判断在方法中layer是否是middle除了第一次之外,还需要判断中间件的路由是否匹配,因为有些中间件是针对某个路由的。letreqMethod=req.method.toLocaleLowerCase()letpathName=url.parse(req.url,true).pathnameletindex=0;functionnext(){//中间件处理if(method==='middle'){//检查路径是否匹配if(path==='/'||pathName===path||pathName.startsWith(path+'/')){handler(req,res,next)//执行中间件回调}else{next()}//路由处理}else{//检查方法是否匹配路径if(method===reqMethod&&path===pathName){handler(req,res)//执行路由回调}else{next()}}}next()//这里,next必须被调用一次。意思就是初始化的时候,取第一层。路由遍历没有匹配层怎么办?所以需要在next方法中先判断边是否已经遍历:functionnext(){//判断遍历是否完成if(app.routes.length===index){returnres.end(`不能${reqMethod}${pathName}`)}let{method,path,handler}=app.routes[index++];//中间件处理if(method==='middle'){if(path==='/'||pathName===path||pathName.startsWith(path+'/')){handler(req,res,下一个)}else{下一个()}}else{//路由处理if(method===reqMethod&&path===pathName){handler(req,res)}else{next()}}}next()这样,一个next的函数方法基本完成4.错误处理中间件的定义和使用如上图所示,错误处理中间件放在最后,就像一个流水线工厂,错误处理是最后一道工序,但不是所有的产品都需要运行最后一道工序,就像:只有不合格的产品才会进入最后一道工序并被标记为不合格,以及不合格的原因。我们来看看Express中的错误是如何处理的://Middleware1app.use(function(req,res,next){res.setHeader('Content-Type','text/html;charset=utf-8')console.log('middle1')next('这是一个错误')})//中间件2app.use(function(req,res,next){console.log('middle2')next()})//中间件3(错误处理)app.use(function(err,req,res,next){if(err){res.end(err)}next()})如上图:中间有3个组件,当next方法抛出错误时,会将错误作为参数传递给next方法,然后next指向的next方法就是错误处理的回调函数,也就是说:中的参数next方法被当作错误处理中间件的handler函数的参数传入。代码如下:functionnext(err){//判断遍历是否完成if(app.routes.length===index){returnres.end(`Cannot${reqMethod}${pathName}`)}let{方法、路径、处理程序}=app.routes[index++];if(err){console.log(handler.length)//判断是否有4个参数:因为error中间件和普通中间件最直观的区别就是参数个数if(handler.length===4){//错误处理回调handler(err,req,res,next)}else{//向下传递next(err)}}else{//中间件处理if(method==='middle'){if(path==='/'||pathName===path||pathName.startsWith(path+'/')){handler(req,res,next)}else{next()}}else{//路由处理if(method===reqMethod&&path===pathName){handler(req,res)}else{next()}}}}麻雀虽然小而全,至此,一个简单的Express就完成了。大家可以根据自己的兴趣封装自己的API...总结:中间件的核心是next方法。next方法只负责维护routes数组和取出层,根据条件决定是否执行回调。附加完整代码:consthttp=require('http')consturl=require('url')functioncreateApp(){letapp=function(req,res){letreqMethod=req.method.toLocaleLowerCase()letpathName=url.parse(req.url,true).pathnameletindex=0;functionnext(err){if(app.routes.length===index){returnres.end(`Cannot${reqMethod}${pathName}`)}let{方法、路径、处理程序}=app.routes[指数++];if(err){console.log(handler.length)if(handler.length===4){console.log(1)handler(err,req,res,next)}else{next(err)}}else{if(method==='middle'){if(path==='/'||pathName===path||pathName.startsWith(path+'/')){handler(req,res,next)}else{next()}}else{if(method===reqMethod&&path===pathName){handler(req,res)}else{next()}}}}next()};让方法=http.METHODS;app.routes=[];methods.forEach(method=>{method=method.toLocaleLowerCase()app[method]=function(path,handler){letlayer={method,path,handler,}app.routes.push(layer)}})应用程序.use=function(path,handler){if(typeofpath==='function'){handler=path;路径='/';}letlayer={method:'middle',handler,path}app.routes.push(layer)}app.listen=function(){letserver=http.createServer(app);server.listen(...arguments)}returnapp;}module.exports=createApp