Express深入理解与简明实现

导读

我有一个问题和不太成熟的想法,不知道该不该提!

掘金既然支持目录TOC,为什么不能把目录放在一个显眼的地方,比如左边?一大片空白不用非要放在右下角和其它面板抢,emmm...

  • express概要
    • 创建一个服务器以及分发路由
    • 简单实现1
      • 脉络
      • 实现路由
  • .all方法和实现
    • 应用
    • 实现
      • 在app下添加.all方法(用来存储路由信息对象)
      • 对遍历路由信息对象时的规则判断做出调整
  • 中间件
    • 概要
    • 中间件和路由的异同
      • 1.都会包装成路由信息对象
      • 2.匹配条数的不同
      • 3.匹配路径上的不同
    • next与错误中间件
    • 实现
      • 在app下添加.use方法
      • 改造request方法
  • params
    • params常用属性
    • params与动态路由
      • 实现
        • 动态路由与动态路由属性的实现
        • params其它属性的实现
  • .param方法
    • api一览
    • 注意事项
    • 应用场景
    • 和中间件的区别
    • 实现
      • 添加param方法
      • 修改request

express概要

express是一个node模块,它是对node中http模块的二次封装。

express相较于原生http模块,为我们提供了作为一个服务器最重要的功能:路由。 路由功能能帮助我们根据不同的路径不同的请求方法来返回不同的内容

除此之外express还支持 中间件 以及其他类似于 req.params 这些小功能。

创建一个服务器以及分发路由

let express = require('express');
let  app = express();

//针对不同的路由进行不同的返回
app.get('/eg1',function(req,res){
  res.end('hello');
});
app.post('/eg1',function(req,res){
  res.end('world');
});

app.listen(8080,function(){
  console.log(`server started at 8080`);
});
复制代码

可以发现,引入express后会返回一个函数,我们称之为express。

express这个函数运行后又会返回一个对象,这个对象就是包装后的http的server对象

这个对象下有很多方法,这些方法就是express框架为我们提供的新东东了。

上面用到了.get方法和.post方法,get和post方法能帮助我们对路由进行分发。

什么是路由分发呢?其实就是在原生request回调中依据请求方法请求路径的不同来返回不同的响应内容。

就像在上面的示例中我们通过.get.post方法对路径为/eg1的请求绑定了一个回调函数, 但这个两个回调函数不会同时被调用,因为请求方法只能是一种(get或则post或则其它)。

如果请求方法是get请求路径是/eg1则会返回.get中所放置的回调

<<< 输出
hello
复制代码

否则若请求路径不变,请求方法是post则会返回.post方法中放置的回调

<<< 输出
world
复制代码

简单实现1

脉络

我们首先要有一个函数,这个函数运行时会返回一个app对象

function createApplication(){
  let app = function(req,res){};
  return app;
}
复制代码

这个app对象下还有一些方法.get.post.listen

app.get = function(){}
app.post = function(){}
app.listen = function(){}
复制代码

其中app.listen其实就是原生http中的server.listenapp就是原生中的request回调。

app.listen = function(){
  let server = http.createServer(app);
  server.listen.apply(server,arguments); //事件回调中,不管怎样this始终指向绑定对象,这里既是server,原生httpServer中也是如此
}
复制代码
实现路由

我们再来想想app.get这些方法到底做了什么。 其实无非定义了一些路由规则,对匹配上这些规则的路由进行一些针对性的处理(执行回调)。

上面一句话做了两件事,匹配规则 和 执行回调。 这两件事执行的时机是什么时候呢?是服务器启动的时候吗?不是。 是当接收到客户端请求的时候

这意味着什么? 当服务器启动的时候,其实这些代码已经执行了,它们根本不会管请求是个什么鬼,只要服务器启动,代码就执行。 所以我们需要将规则回调存起来。(类似于发布订阅模式)

app.routes = []; 
app.get = function(path,handler){
  app.routes.push({
    method:'get'
    ,path
    ,handler
  })
}
复制代码

上面我们定义了一个routes数组,用来存放每一条规则和规则所对应的回调以及请求方式,即路由信息对象

但有一个地方需要我们优化。不同的请求方法所要做的事情都是相同的(只有method这个参数不同),我们不可能每增加一个就重复的写一次,请求的方法是有非常多的,这样的话代码会很冗余。

//http.METHODS能列出所有的请求方法
>>>
console.log(http.METHODS.length);

<<<
33
复制代码

So,为了简化我们的代码我们可以遍历http.METHODS来创建函数

http.METHODS.forEach(method){
  let method = method.toLowerCase();
  app[method] = function(path,handler){
    app.routes.push({
      method
      ,path
      ,handler
    })
  }
}
复制代码

然后我们会在请求的响应回调中用到这些路由信息对象。而响应回调在哪呢? 上面我们已经说过其实app这个函数对象就是原生的request回调。 接下来我们只需要等待请求来临然后执行这个app回调,遍历每一个路由信息对象进行匹配,匹配上了则执行对应的回调函数。

let app = function(req,res){
  for(let i=0;i<app.routes.length;++i){
    let route = app.routes[i];
    let {pathname} = url.parse(req.url);
    if(route.method==req.method&&route.path==pathname){
    	route.handler(req,res);
    }
  }
}
复制代码

.all方法和实现

应用

.all也是一个路由方法,

app.all('/eg1',function(req,res){})
复制代码

和普通的.get,.post这些和请求方法直接绑定的路由分发不同,.all方法只要路径匹配得上各种请求方法去请求这个路由都会得到响应。

还有一种更暴力的使用方式

app.all('*',function(req,res){})
复制代码

这样能匹配所有方法所有路劲,all! 通常它的使用场景是对那些没有匹配上的请求做出兼容处理。

实现

在app下添加.all方法(用来存储路由信息对象)

和一般的请求方法是一样的,只是需要一个标识用以和普通方法区分开。 这里是在method取了一个all关键字作为method的值。

app.all = function(path,handler){
  app.routs.push({
    method:'all'
    ,path
    ,handler
  })
}
复制代码
对遍历路由信息对象时的规则判断做出调整

另外还需要在request回调中对规则的匹配判断做出一些调整

if((route.method==req.method||route.method=='all')&&(route.path==pathname||route.path=='*')){
    route.handler(req,res);
}
复制代码

中间件

概要

中间件是什么鬼呢?中间件嘛,顾名思义中间的件。。。emmm,我们直接说说它的作用吧!

中间件主要是在请求和真正响应之间再加上一层处理, 处理什么呢?比如说权限验证、数据加工神马的。

这里所谓的真正响应,你可以把它当做.get这些路由方法所要执行的那些个回调。

app.use('/eg2',function(req,res,next){
  //do something
  next();
})
复制代码

中间件和路由的异同

1.都会包装成路由信息对象

服务器启动时,中间件也会像路由那样被存储为一个一个路由信息对象

2.匹配条数的不同

路由只要匹配上了一条就会立马返回数据并结束响应,不会再匹配第二条(原则上如此)。 而中间件只是一个临时中转站,对数据进行过滤或则加工后会继续往下匹配。 So,中间件一般放在文件的上方,路由放在下方。

3.匹配路径上的不同

中间件进行路径匹配时,只要开头匹配的上就能执行对应的回调。

这里所谓的开头意思是: 假若中间件要匹配的路径是/eg2, 那么只要url.path是以/eg2开头,像/eg2/eg2/a/eg2/a/b即可。(/eg2a这种不行,且必须以/eg2开头,a/eg2则不行)

而路由匹配路径时必须是完全匹配,也就是说规则若是/eg2则只有/eg2匹配的上。这里的完全匹配其实是针对路径的 /的数量 来说的,因为动态路由中匹配的值不是定死的。

除此之外,中间件可以不写路径,当不写路径时express系统会为其默认填上/,即全部匹配。

next与错误中间件

中间件的回调相较于路由多了一个参数next,next是一个函数。 这个函数能让中间件的回调执行完后继续向下匹配,如果没有写next也没有在中间件中结束响应,那么请求会一直处于pending状态。

next还可以进行传参,如果传了惨,表示程序运行出错,将匹配错误中间件进行处理且只会交由错误中间件处理

错误中间件相较于普通中间件在回调函数中又多了一个参数err,用以接收中间件next()传递过来的错误信息。

app.use('/eg2',function(req,res,next){
  //something wrong
  next('something wrong!');
})

app.use('/eg2',function(err,req,res,next){
  console.log('i catch u'+err);
  next(err); //pass to another ErrorMiddle
});

// 错误中间接收了错误信息后仍然允许接着向下传递

app.use('/eg2',function(err,req,res,next){
  res.end(err);
});
复制代码

其实错误中间件处理完成后也能匹配路由

app.use('/eg2',function(req,res,next){
  //something wrong
  next('something wrong!');
})

app.use('/eg2',function(err,req,res,next){
  console.log('i catch u'+err);
  next(err); //pass to another ErrorMiddle
});

app.get('/eg2',function(req,res){
  //do someting
})
复制代码

实现

在app下添加.use方法

像路由方法一样,其实就是用来存储路由信息对象

app.use = function(path,handler){
    if(typeof handler != 'function'){ //说明只有一个参数,没有path只有handler
      handler = path;
      path = "/"
    }
    app.routes.push({
      method:'middle' //需要一个标识来区分中间件
      ,path
      ,handler
    });
  };
复制代码
改造request方法
let app = function(req,res){
	const {pathname} = url.parse(req.url, true);
    let i = 0;
	function next(err){
        if(index>=app.routes.length){ //说明路由信息对象遍历完了仍没匹配上,给出提示
        	return res.end(`Cannot ${req.method} ${pathname}`);
        }
    	let route = app.routes[i++];
        if(err){ //是匹配错误处理中间件
            //先判断是不是中间件
            if(route.method == 'middle'){
                //如果是中间件再看路径是否匹配
                if(route.path=='/'||pathname.startsWith(route.path+'/')||route.path==pathname){
                	//再看是否是错误处理中间件
                    if(route.handler.length==4){
                        route.handler(err,req,res,next);
                    }else{
                        next(err);
                    }
                }else{
                    next(err);
                }
            }else{
            	next(err); //将err向后传递直到找到错误处理中间件
            }
        }else{ //匹配路由和普通中间件
            if(route.method == 'middle'){ //说明是中间件
            	if(route.path=='/'||pathname.startsWith(route.path+'/')||route.path==pathname){
                	route.handler(req,res,next);
                }else{ //此条路由没有匹配上,继续向下匹配
                	next();
                }
            }else{ //说明是路由
                if((route.method==req.method||route.method=='all')&&(route.path==pathname||route.path=='*')){
                //说明匹配上了
                	route.handler(req,res);
                }else{
                	next();
                }
            }
        }
    }
	next();
}
复制代码

我们可以把对错误中间件的判断封装成一个函数

function checkErrorMiddleware(route){
  if(route.method == 'middle'&&(route.path=='/'||pathname.startsWith(route.path+'/')||route.path==pathname)&&route.handler.length==4){
    return true;
  }else{
    next(err);
  }
}
复制代码

params

params常用属性

express为我们在request回调中的req对象参数下封装了一些常用的属性

app.get('/eg3',function(req,res){
  console.log(req.hostname);
  console.log(req.query);
  console.log(req.path);
})
复制代码

params与动态路由

app.get('/article/:artid',function(req,res){
  console.log(req.artid);
})

>>>
/article/8

<<<
8
复制代码

实现

动态路由与动态路由属性的实现

首先因为路由规则所对应的路径我们看得懂,但机器看不懂。 So我们需要在存储路由信息对象时,对路由的规则进行正则提炼,将其转换成正则的规则。

...
app[method] = function(path,handler){
  let paramsNames = [];
  path = path.replace(/:([^\/]+)/g,function(/*/:aaa ,aaa*/){ 
    paramsNames.push(arguments[1]); //aaa
    return '([^\/]+)';  // /user/:aaa/:bbb 被提炼成 /user/([^\/]+)/([^\/]+)
  });
      
  layer.reg_path = new RegExp(path);
  layer.paramsNames = paramsNames;
}
...
复制代码

我们拿到了一个paramsNames包含所有路径的分块,并将每个分块的值作为了一个新的param的名称,

我们还拿到了一个reg_path,它能帮助我们对请求的路径进行分块匹配,匹配上的每一个子项就是我们新param的值。

request路由匹配部分做出修改

if(route.paramsNames){
    let matchers = pathname.match(req.reg_path);
    if(matchers){
    	let params = {};
        for(let i=0;i<route.paramsNames.length;++i){
            params[route.paramsNames[i]] = matchers[i+1]; //marchers从第二项开始才是匹配上的子项
        }
    	req.params = params;
    }
    route.handler(req,res);
}

复制代码
params其它属性的实现

这里是内置中间件,即在框架内部,它会在第一时间被注册为路由信息对象。

实现很简单,就是利用url模块对req.url进行解析

app.use(function(req,res,next){
    const urlObj = url.parse(req.url,true);
    req.query = urlObj.query;
    req.path = urlObj.pathname;
    req.hostname = req.headers['host'].split(':')[0];
    next();
});
复制代码

.param方法

api一览

app.param('userid',function(req,res,next,id){
  req.user = getUser(id);
  next();
});
复制代码

next和中间件那个不是一个意思,这个next执行的话会执行被匹配上的那条动态路由所对应的回调

id为请求时userid这个路径位置的实际值,比如

访问路径为:http://localhost/ahhh/9

动态路由规则为:/username/userid

userid即为9
复制代码

注意事项

必须配合动态路由!! param和其它方法最大的一点不同在于,它能对路径进行截取匹配

什么意思呢, 上面我们讲过,路由方法路径匹配时必须完全匹配,而中间件路径匹配时需要开头一样

param方法无需开头一样,也无需完全匹配,它只需要路径中某一个分块(即用/分隔开的每个路径分块)和方法的规则对上即可。

应用场景

当不同的路由中包含相同路径分块且使用了相同的操作时,我们就可以对这部分代码进行提取优化。

比如每个路由中都需要根据id获取用户信息

app.get('/username/:userid/:name',function(req,res){}
app.get('/userage/:userid/:age',function(req,res){}
app.param('userid',function(req,res,next,id){
  req.user = getUser(id);
  next();
});
复制代码

和中间件的区别

相较于中间件它更像是一个真正的钩子,它不存在放置的先后问题。 如果是中间件,一般来说它必须放在文件的上方,而param方法不是。

导致这样结果的本质原因在于,中间件类似于一个路由,它会在请求来临时加入的路由匹配队列中参与匹配。而param并不会包装成一个路由信息对象也就不会参与到队列中进行匹配,

它的触发时机是在它所对应的那些动态路由被匹配上时才会触发。

实现

添加param方法

在app下添加了一个param方法,并且创建了一个paramHandlers对象来存储这个方法所对应的回调。

app.paramHandlers = {};
app.param = function(name,handler){
    app.paramHandlers[name] = handler; //userid
};
复制代码
修改request

修改request回调中 动态路由被匹配上时的部分

当动态路由被匹配上时,通过它的动态路由参数来遍历paramHandlers,看是否设置了对应的param回调

if(route.paramsNames){
    let matchers = pathname.match(route.reg_path);

    if(matchers){
      let params = {};
      for(let i=0;i<route.paramsNames.length;++i){
        params[route.paramsNames[i]] = matchers[i+1];
      }
      req.params = params;
      for(let j=0;j<route.paramsNames.length;++j){
        let name = route.paramsNames[j];
        let handler = app.paramHandlers[name];
        if(handler){
        //回调触发更改在了这里
        //第三个参数为next,这里把route.handler放在了这里,是让param先执行再执行该条路由
          return handler(req,res,()=>route.handler(req,res),req.params[name]); 
        }else{
          return route.handler(req,res);
        }
      }

    }else{
      next();
    }
}
复制代码

源码

let http = require('http');
let url = require('url');

function createApplication() {
  //app其实就是真正的请求监听函数

  let app = function (req, res) {
    const {pathname} = url.parse(req.url, true);
    let index = 0;
    function next(err){
      if(index>=app.routes.length){
        return res.end(`Cannot ${req.method} ${pathname}`);
      }
      let route = app.routes[index++];
      if(err){
        //先判断是不是中间件
        if(route.method == 'middle'){
          //如果是中间件再看路径是否匹配
          if(route.path=='/'||pathname.startsWith(route.path+'/')||route.path==pathname){
            //再看是否是错误处理中间件
            if(route.handler.length==4){
              route.handler(err,req,res,next);
            }else{
              next(err);
            }
          }else{
            next(err);
          }
        }else{
          next(err); //将err向后传递直到找到错误处理中间件
        }
      }else{
        if(route.method == 'middle'){ //中间件
          //只要请求路径是以此中间件的路径开头即可
          if(route.path=='/'||pathname.startsWith(route.path+'/')||route.path==pathname){
            route.handler(req,res,next);
          }else{
            next();
          }
        }else{ //路由
          if(route.paramsNames){
            let matchers = pathname.match(route.reg_path);

            if(matchers){
              let params = {};
              for(let i=0;i<route.paramsNames.length;++i){
                params[route.paramsNames[i]] = matchers[i+1];
              }
              req.params = params;
              for(let j=0;j<route.paramsNames.length;++j){
                let name = route.paramsNames[j];
                let handler = app.paramHandlers[name];
                if(handler){ //如果存在paramHandlers 先执行paramHandler再执行路由的回调
                  return handler(req,res,()=>route.handler(req,res),req.params[name]);
                }else{
                  return route.handler(req,res);
                }
              }

            }else{
              next();
            }
          }else{
            if ((route.method == req.method.toLowerCase() || route.method == 'all') && (route.path == pathname || route.path == '*')) {
              return route.handler(req, res);
            }else{
              next();
            }
          }
        }
      }

    }
    next();

  };

  app.listen = function () { //这个参数不一定
    let server = http.createServer(app);
    //server.listen作为代理,将可变参数透传给它
    server.listen.apply(server, arguments);
  };
  app.paramHandlers = {};
  app.param = function(name,handler){
    app.paramHandlers[name] = handler; //userid
  };

  //此数组用来保存路由规则
  app.routes = [];
  // console.log(http.METHODS);
  http.METHODS.forEach(function (method) {
    method = method.toLowerCase();
    app[method] = function (path, handler) {
      //向数组里放置路由对象
      const layer = {method, path, handler};
      if(path.includes(':')){
        let paramsNames = [];
        //1.把原来的路径转成正则表达式
        //2.提取出变量名
        path = path.replace(/:([^\/]+)/g,function(){ //:name,name
          paramsNames.push(arguments[1]);
          return '([^\/]+)';
        });
        // /user/ahhh/12
        // /user/([^\/]+)/([^\/]+)
        layer.reg_path = new RegExp(path);
        layer.paramsNames = paramsNames;
      }

      app.routes.push(layer);
    };

  });

  //all方法可以匹配所有HTTP请求方法
  app.all = function (path, handler) {
    app.routes.push({
      method: 'all'
      , path
      , handler
    });
  };
  //添加一个中间件
  app.use = function(path,handler){
    if(typeof handler != 'function'){ //说明只有一个参数,没有path只有handler
      handler = path;
      path = "/"
    }
    app.routes.push({
      method:'middle' //需要一个标识来区分中间件
      ,path
      ,handler
    });
  };
  //系统内置中间件,用来为请求和响应对象添加一些方法和属性
  app.use(function(req,res,next){
    const urlObj = url.parse(req.url,true);
    req.query = urlObj.query;
    req.path = urlObj.pathname;
    req.hostname = req.headers['host'].split(':')[0];
    next();
  });
  return app;
}

module.exports = createApplication;
复制代码
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值