我有一个问题和不太成熟的想法,不知道该不应提!node
掘金既然支持目录TOC,为何不能把目录放在一个显眼的地方,好比左边?一大片空白不用非要放在右下角和其它面板抢,emmm...正则表达式
express是一个node模块,它是对node中http
模块的二次封装。express
express相较于原生http模块,为咱们提供了做为一个服务器最重要的功能:路由
。 路由功能能帮助咱们根据不一样的路径不一样的请求方法来返回不一样的内容。api
除此以外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。bash
express这个函数运行后又会返回一个对象,这个对象就是包装后的http的server对象。服务器
这个对象下有不少方法,这些方法就是express框架为咱们提供的新东东了。app
上面用到了.get
方法和.post
方法,get和post方法能帮助咱们对路由进行分发。框架
什么是路由分发呢?其实就是在原生request
回调中依据请求方法
和请求路径
的不一样来返回不一样的响应内容。函数
就像在上面的示例中咱们经过.get
和.post
方法对路径为/eg1
的请求各绑定了一个回调函数, 但这个两个回调函数不会同时被调用,由于请求方法只能是一种(get或则post或则其它)。
若是请求方法是get请求路径是/eg1
则会返回.get
中所放置的回调
<<< 输出
hello
复制代码
不然若请求路径不变,请求方法是post则会返回.post
方法中放置的回调
<<< 输出
world
复制代码
咱们首先要有一个函数,这个函数运行时会返回一个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.listen
。 app
就是原生中的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
也是一个路由方法,
app.all('/eg1',function(req,res){})
复制代码
和普通的.get
,.post
这些和请求方法直接绑定的路由分发不一样,.all方法只要路径匹配得上各类请求方法去请求这个路由都会获得响应。
还有一种更暴力的使用方式
app.all('*',function(req,res){})
复制代码
这样能匹配全部方法全部路劲,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();
})
复制代码
服务器启动时,中间件也会像路由那样被存储为一个一个路由信息对象
。
路由
只要匹配上了一条就会立马返回数据并结束响应,不会再匹配第二条(原则上如此)。 而中间件
只是一个临时中转站,对数据进行过滤或则加工后会继续往下匹配。 So,中间件通常放在文件的上方,路由放在下方。
中间件进行路径匹配时,只要开头匹配的上就能执行对应的回调。
这里所谓的开头意思是: 倘若中间件要匹配的路径是/eg2
, 那么只要url.path是以/eg2
开头,像/eg2
,/eg2/a
,/eg2/a/b
便可。(/eg2a这种不行,且必须以/eg2开头,a/eg2则不行)
而路由匹配路径时必须是彻底匹配,也就是说规则如果/eg2
则只有/eg2
匹配的上。这里的彻底匹配实际上是针对路径的 /的数量 来讲的,由于动态路由
中匹配的值不是定死的。
除此以外,中间件能够不写路径,当不写路径时express系统会为其默认填上/
,即所有匹配。
中间件的回调相较于路由多了一个参数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 = function(path,handler){
if(typeof handler != 'function'){ //说明只有一个参数,没有path只有handler
handler = path;
path = "/"
}
app.routes.push({
method:'middle' //须要一个标识来区分中间件
,path
,handler
});
};
复制代码
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);
}
}
复制代码
express为咱们在request
回调中的req对象参数下封装了一些经常使用的属性
app.get('/eg3',function(req,res){
console.log(req.hostname);
console.log(req.query);
console.log(req.path);
})
复制代码
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);
}
复制代码
这里是内置中间件,即在框架内部,它会在第一时间被注册为路由信息对象。
实现很简单,就是利用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();
});
复制代码
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并不会包装成一个路由信息对象也就不会参与到队列中进行匹配,
它的触发时机是在它所对应的那些动态路由被匹配上时才会触发。
在app下添加了一个param方法,而且建立了一个paramHandlers
对象来存储这个方法所对应的回调。
app.paramHandlers = {};
app.param = function(name,handler){
app.paramHandlers[name] = handler; //userid
};
复制代码
修改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;
复制代码