1 概述
基于nodejs的后端框架———express。express主要是在http库的基础上加入了路由,中间件和模板机制,为快速开发提供了基本工具。作为小而美的框架,它没有提供关于依赖注入,数据库和缓存等组件了。
2 路由
nodejs支持复杂的路由方式,包括get, post, put, head, delete, options, trace, copy, lock, mkcol, move, purge, propfind, proppatch, unlock, report, mkactivity, checkout, merge, m-search, notify, subscribe, unsubscribe, patch, search, 和 connect。并且包含了全方法匹配的路由,all。
2.1 普通路由
'/index',
'/index.html',
'/index/123',
'/123',
以全名称为开头的都是普通路由,执行的是全匹配方法。
2.2 参数路由
'/list',
'/list/123',
'/list/:userId',
带有冒号的是参数路由,参数能匹配任何的字符串,参数会被放进req.params里。要注意的是,参数路由的顺序恨重要,在上面的例子中,/list/123将会匹配/list/123普通路由。
'/list',
'/list/:userId',
'/list/123',
但在上面的例子中,/list/123将会匹配/list/:userId参数路由
2.3 正则路由
'/ab?cd',
'/ab+cd',
正则路由,在字符串中带有*,+,()的地方将会看作正则表达式的一部分作为解析。
2.3 分组路由
var expressRouter = express.Router({
caseSensitive:true
});
expressRouter.get('/list',xxxx)
app.use('/card',expressRouter)
建立分组路由,将一个子路由挂载在前缀匹配的中间件上。
3 中间件
中间件是express中最为精妙的部分,以一个简单的方式完成最灵活的框架扩展性。在express中,路由甚至都是由中间件来实现的。
3.1 匹配方式
app.use('/path',xxxx)
中间件的执行方式为,use的第一个参数为前缀匹配路径,可以是普通路径,参数路径或正则路径,第二个参数就是中间件函数
function(req,res,next)
其中,中间件函数,第一二个参数就是普通的request和response对象,next就是链接到下一个匹配的中间件去。
任意一个请求过来时,express的处理过程是:
- 逐个遍历中间件,如果这个中间件函数是四个及以上参数的,则被看成为错误中间件,并放入到错误处理程序堆栈中。如果这个中间件函数只有三个及以下参数的,则被看成为普通中间件。
- 逐个遍历普通中间件,如果路径不符合前缀匹配(中间件)或全匹配(路由),就跳过这个中间件,否则就调用这个普通中间件。
- 在普通中间件处理过程中,要么调用next()进入到下一个普通中间件去,要么不调用next(),然后调用res.xxx来终结请求响应循环。
- 在普通中间件的处理的过程中,如果遇到throw new Error,或next(xxx),就会从错误处理程序堆栈中取出错误中间件来逐个调用。
注意,在这里的过程中,路由与中间件是处在同样地位的,它也可以调用next来执行下一个中间件。
3.2 普通中间件
app.use('/a',function(req,res,next){
console.log('/a');
next();
})
app.use('/a',function(req,res,next){
console.log('/a2');
next();
})
app.use('/a',function(req,res,next){
console.log('/a3');
res.send('Hello World End');
})
三个相当路径的中间件
app.use('/b',function(req,res,next){
console.log('/b');
next();
})
app.use('/c',function(req,res,next){
console.log('/c');
next();
})
app.use('/b?c?',function(req,res,next){
console.log('/b?c?');
res.send('/b?c?');
})
带有一个能匹配两个路径的中间件
app.use('/d',function(req,res,next){
console.log('/d1');
next();
},function(req,res,next){
console.log('/d2');
next();
},function(req,res,next){
console.log('/d3');
res.send('/d3');
})
在同一个use上的中间件
app.get('/e',function(req,res,next){
console.log('get /e');
next();
})
app.post('/e',function(req,res,next){
console.log('post /e');
next();
})
app.all('/e',function(req,res,next){
res.send('all /e');
})
将路由看成中间件的操作
3.3 错误中间件
app.use('/i?j?',function(err,req,res,next){
console.log(err);
next();
})
//i error
app.use('/i',function(req,res,next){
console.log('use /i1');
next();
})
app.use('/i',function(req,res,next){
console.log('use /i2');
throw new Error('123');
})
app.use('/i',function(req,res,next){
console.log('use /i3');
res.send('Hello i3')
})
//j error
app.use('/j',function(req,res,next){
console.log('use /j1');
next();
})
app.use('/j',function(req,res,next){
console.log('use /j2');
next(new Error('456'));
})
app.use('/j',function(req,res,next){
console.log('use /j3');
res.send('Hello j3')
})
注意错误中间件要在最开始的地方就挂载起来。可以通过throw new Error或者next(new Error)来触发错误中间件。
3.4 提前终止
app.use('/h',function(req,res,next){
console.log('use /h1');
next();
})
app.use('/h',function(req,res,next){
console.log('use /h2');
res.send('Hello h2');
})
app.use('/h',function(req,res,next){
console.log('use /h3');
res.send('Hello h3')
})
一旦调用了res.send,json,jsonp,end,redirect等函数时,nodejs就认为这个请求已经处理完毕了,后续继续调用res.send,json,jsonp,end,redirect等函数都是会报错的。注意,这种情况下开发者就不应该继续调用next函数了,当然你硬要这么做是可以的,但没有需要这么做的场景,毕竟请求已经完毕了。
3.5 本路由层终止
function endByRouter(){
app.use('/k',function(req,res,next){
console.log('use /k1');
next();
})
app.get('/k',[function(req,res,next){
console.log('use /k2');
next('route');
console.log('use /k3')
},function(req,res,next){
console.log('use /k4');
res.send('Hello k4')
}])
app.use('/k',function(req,res,next){
console.log('use /k5');
res.send('Hello k5')
})
}
比较费解的是,express还提供了next(‘route’)的方法,它的意思是终止本路由(在该例子下是get)下的其他中间件函数,直接跳到下一个中间件去处理。
3.6 错误终止
见3.3,一样的例子。
3.7 无终止
app.use('/g',function(req,res,next){
console.log('use /g');
next();
})
app.get('/g',function(req,res,next){
console.log('get /g');
})
在某个中间件或路由下,如果你没有调用next,而且也没有调用res.xxx,那么bug就会出现,整个express会认为这个请求在处理中的状态,客户的浏览器就会一直转圈,永远都没有结束。
3.8 常用中间件
var logger = require('morgan');
var router = express.Router();
router.use(express.static("."));
router.use(logger())
router.use('/',function(req,res,next){
res.send('Hello Thridparty');
})
app.use('/l',router);
使用自带的静态中间件,以及日志中间件。
3.9 最佳实践
express的中间件灰常强大,但也要注意遵守一些默认的规则,不然就会让代码变得难懂和难调试。
- 中间件都是首先被调用的,必须要在路由层之前。
- 路由层不能做中间件的事情,也就是不能调用next函数,它的位置都要在中间件之后。
- 对于一些只在特定路径下才需要的中间件,就建立分组路由,然后在分组路由中加入中间件就可以了。
4 模板引擎
var express = require('express');
var app = express();
app.set('view engine', 'pug');
app.set('views','./views');
app.get('/',function(req,res){
res.render('index', { title: 'Hey', message: 'Hello there!'});
})
var server = app.listen(3000, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});
使用view engine和views来设置render的渲染引擎。
5 请求对象
var express = require('express');
var bodyParser = require('body-parser');
var cookieParser = require('cookie-parser');
var session = require('express-session');
var app = express();
app.use(cookieParser());
app.use(session({
resave:true,
saveUninitialized:true,
secret:'zcv',
}));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended:true}));
app.get('/get/:userId',function(req,res){
res.json({
params:req.params,
query:req.query,
cookies:req.cookies,
session:req.session,
body:req.body,
})
})
app.get('/setCookie',function(req,res){
res.cookie('name','Fish')
res.json({ok:true})
})
app.get('/setSession',function(req,res){
req.session['name'] = 'fish';
res.json({ok:true})
})
var server = app.listen(3000, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});
这个比较简单了,就是从request中获取需要的数据。
- params,参数路由中的参数
- query,查询参数
- body,正文参数,有了bodyParser和multer以后,你可以直接使用body里面的json,urlencode和form的参数了。
- session,有了sessionParser以后,可以直接读取session参数。
- cookies,有了cookieParse以后,可以读cookie参数,要注意setCookie要使用res.cookie,不能用req.cookie。
6 响应对象
var express = require('express');
var app = express();
app.set('jsonp callback name', 'cb');
app.get('/status',function(req,res){
res.status('201').end()
})
app.get('/header',function(req,res){
res.set('PoweredBy','Fish')
res.end()
})
app.get('/send',function(req,res){
res.send('Hello World')
})
app.get('/json',function(req,res){
res.json({
code:0,
msg:'',
data:'Hello World'
})
})
app.get('/jsonp',function(req,res){
res.jsonp({
code:0,
msg:'',
data:'Hello World'
})
})
app.get('/redirect',function(req,res){
res.redirect('/json')
})
var server = app.listen(3000, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});
依然很简单,使用res来输出。
- status,输出状态码
- set,输出header信息
- send,json,jsonp,redirect,各类输出了
7 总结
express最为精巧的地方就是它的中间件了,但是将路由也看成为中间件,这一点就值得商榷了。另外,它的中间件在执行时使用逐个遍历逐个匹配O(n)的方式,并不能引入tire-tree来实现O(1)匹配,在大规模的路由匹配时,这是个很大的性能瓶颈。
另外,在设计上,它的中间件是一阶的,不能修改下一个中间件的输入和输出参数,而对应的redux的中间件是二阶的,就比它灵活多了,能修改下一个中间件的输入和输出参数。
并且,更大的缺陷是,express在设计上没有很好地考虑js中的异步特性,它并不能确定请求是在处理中的状态,还是在忘记了res.send导致的bug,就是3.7中所说的问题。更简单的例子是,你无法优雅地写一个中间件,统计每个请求所使用的时间。这正是下一代nodejs上的后端框架——koa要解决的问题。
总体来说,express用中间件实现了非常灵活的扩展性,但是也放弃了性能,也放弃了对代码规范性的约束。而在设计上,没有考虑二阶中间件和js的异步特性,是其最大的缺陷。
可是,express的库爆多,周边生态爆成熟,你也是无可奈何呀。
参考资料:
- 本文作者: fishedee
- 版权声明: 本博客所有文章均采用 CC BY-NC-SA 3.0 CN 许可协议,转载必须注明出处!