手写一个express系列express的基本用法constexpress=require("express");constapp=express();app.get("/test",(req,res,next)=>{console.log("*1");//res.end("2");next();});app.get("/test",(req,res,next)=>{console.log("*2");res.end("2");});app.listen(8888,(err)=>{!err&&console.log("监听成功");});当我访问localhost:8888/test时,返回:2,服务器打印*1*2从上面你能看出什么?express默认引入调用,返回一个app对象app.listen会在每次收到请求时启动进程并监听端口,对应的url和method会触发挂载在app上的对应回调函数调用下一次方法,它将触发下一个实现属于我们的快速文件条目的简单快速框架定义。这里用class来实现classexpress{}module.exports=express;所需native模块http,创建进程监听端口const{createServer}=require("http");为类定义一个listen方法,监听端口classexpress{listen(...args){createServer(cb).listen(...args);这样就可以通过调用类的listen来调用http模块的listen了。我们可以忽略这里的cb。要知道每次收到请求都会调用cb函数。这是createServer的原生模块帮我们封装好的实现。接收请求触发执行app.getapp.post等方法。现在我们收到响应,它会触发cb回调函数。我们打印出来看看参数是什么?类express{cb(){return(req,res)=>{console.log(res,res,"开始操作");};}listen(...args){createServer(this.cb()).listen(...args);}}发现req和res正是我们想要的可读写流。开始编写get和post方法。请注意,有带有'/'的路由,无论任何路由{this.routers={get:[],post:[],};都会触发constructor()}get(path,handle){这个。路由器。得到。推({路径,句柄,});}post(path,handle){这个。路由器。邮政。推({路径,句柄,});}初始化时,定义get和post数组,存放对应的路径和句柄。当需要触发路由回调时,首先要在对应的请求方式中找到对应url的handle方法,然后触发Callback。如何在对应的请求方式中找到url对应的handle方法?当收到请求时,必须遍历一次。这里必须考虑匹配多条路由,也就是说我们可能会遇到像开头测试路由那样的两个get方法cb(){return(req,res)=>{constmethod=req.method.toLowerCase();console.log(this.routers[method],",method");consturl=req.url;this.routers[method].forEach((item)=>{item.path===url&&item.handle(req,res);});};}listen(...args){createServer(this.cb()).listen(...args);}上面根据方法找到对应的数组,遍历找到请求的路由,并触发回调。这时候可以正常返回数据[{method:'get',path:'/test',handle:[Function]}],method最简单的express此时已经完成,但是我们好像忘记了最重要的中间件完成最重要的中间件功能。首先我们要知道express中间件有两种,一种是带路由的,就是根据Routing来决定是否触发。另一种是没有路由的,比如静态资源。当用户访问任意路由时触发一次,所以我们需要一个all数组来存储这种任意路由。Constructor(){this.routers={get:[],post:[],all:[],};}之前的直推方式太粗糙了。如果用户需要中间件功能,不通过路由,则需要特殊处理。这里使用了一个中间函数来修改get和post方法,定义了handleAddRouter方法handleAddRouter(path,handle){letrouter={};if(typeofpath==="string"){router={path,handle,};}else{router={path:"/",handle:path,};}返回路由器;}get(path,handle){constrouter=this.handleAddRouter(path,handle);this.routers.get.push(路由器);}post(path,handle){constrouter=this.handleAddRouter(path,handle);这个.routers.post.push(路由器);}使用(路径,句柄){constrouter=this.handleAddRouter(路径,句柄);this.routers.all.push(路由器);}每次添加之前,触发一次handleAddRouter,如果是一个空路径的中间件,直接传给函数,那么path会帮它设置为'/',我们还是留点,实现next的,因为现在我们添加了all数组,也就是说中间件可能有多个,所以如果一个请求进来,这里会触发多个路由。注意promise.then的源码实现有点类似express的next,还有koa的洋葱圈,redux的中间件实现。当你真正能够看懂前后端框架的源码时,你会发现大部分都是大同小异的。看完我的这篇文章,就足以攻破所有前后端源码了。而且可以手写。我们只学核心,专注重点学习,疯狂成长!实现next的思路:先找到所有匹配的路由,一条一条执行(见next的调用)定义搜索方法,找到所有匹配的路由search(method,url){constmatchedList=[];[...this.routers[method],...this.routers.all].forEach((item)=>{item.path===url&&matchedList.push(item.handle);});返回匹配列表;}cb(){return(req,res)=>{constmethod=req.method.toLowerCase();consturl=req.url;constmatchedList=this.search(方法,url);};}matchedList就是我们要查找的所有路由。为了完成next,我们需要在闭包中存储req,res,matchedList并定义句柄方法handle(req,res,matchedList){constnext=()=>{constmidlleware=matchedList.shift();如果(中间件){中间件(请求、资源、下一个);}};下一个();}cb(){return(req,res)=>{constmethod=req.method.toLowerCase();consturl=req.url;constmatchedList=this.search(方法,url);this.handle(req,res,matchedList);};}就这样,我们就完成了next方法,只要我们手动调用next下一个匹配的路由回调函数就不到一百行代码,这个简单的express框架就写到最后了。只要按照我的文章认真执行,一年之内拿个P6应该是没问题的。希望大家能够通过这些文章真正的学习到框架的原理,然后自己去写一些框架,更上一层楼。我是Peter,曾经是20万人超级群的桌面软件架构师,现在明源云工作。作为分院前端负责人,深圳目前需要招聘中高级前端2名,3D数据可视化方向,期待你的到来。如果您觉得本文对您有帮助,别忘了点击关注哦。我们的技术团队我们也会持续产出原创文章,共同见证你的成长
