express基于nodejs的后端框架

2018-06-05 fishedee 后端

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的库爆多,周边生态爆成熟,你也是无可奈何呀。

参考资料:

相关文章