Express学习笔记
Express是一个基于Node.js的快速、简洁的Web开发框架。它提供了大量的HTTP助手方法、中间件供你使用,很大程度上减轻了开发的工作量。
Express在Node.js内置的网络模块的基础上封装了一个薄的层,在此层中提供Web应用后端的基础功能,你仍然可以使用Node.js的集群功能。
在通过npm初始化当前包时,Express建议将入口点名称app.js,执行下面的命令把Express添加为当前工程的依赖:
1 |
npm install express --save |
1 2 3 4 5 6 7 8 9 10 11 |
var express = require( 'express' ) var app = express() // req、resp和Node.js的http模块的回调参数一样 app.get( '/', function ( req, resp ) { resp.send( 'Hello World!' ) } ) app.listen( 3000, function () { console.log( 'Example app listening on port 3000!' ) } ) |
上述代码在3000端口启动一个HTTP服务器,并且对路由 / 响应一个简短的字符串。当你访问其它路径时,Express会响应404代码。
Express提供了一个模块,用于快速生成Express应用的结构和脚手架代码,执行下面的命令安装此模块:
1 |
npm install express-generator -g |
该模块提供了一个express命令,其格式为:
1 |
express [options] [dir] |
选项 | 说明 |
-v, --view | 指定使用的视图层引擎,支持ejs|hbs|hjs|jade|pug|twig|vas,默认jade |
-c, --css | 指定使用的CSS引擎,支持less|stylus|compass|sass,默认使用基本的CSS格式 |
-f, --force | 允许针对非空目录执行命令 |
--git | 添加.gitignore |
我们可以执行下面的命令生成一个Express应用程序:
1 |
express --view=jade --css=sass express-study |
执行命令: DEBUG=express-study:* npm start 可以启动Web应用。
命令会生成如下目录结构:
子目录 | 说明 |
bin | 包含服务器启动脚本 |
public | 静态资产,例如图片、客户端JS、样式表 |
routes | 路由定义 |
views | 视图模板 |
要想处理类似图片、CSS、客户端脚本之类的静态文件,可以使用 express.static 这个内置的中间件。示例如下:
1 2 3 4 5 6 7 8 9 10 11 |
// root 静态资产在文件系统中的根目录 // options,选项,一个对象 express.static(root, [options]) // 设置public文静态目录 app.use(express.static('public')) // 可以调用多次 app.use(express.static('files')) // URL可以映射到不同的目录 app.use('/static', express.static('public')) |
构造express.static中间件实例时,可以提供以下选项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var options = { // 点号开头的文件如何处理。allow、deny(401)、ignore(404) dotfiles: 'ignore', // 添加etag头,默认true etag: false, // 默认扩展名 extensions: [ 'htm', 'html' ], // 默认情况下,访问目录时自动返回其下的index.html index: false, // 以毫秒计的 max-age 头 maxAge: '1d', // 是否路径名对应了目录,是否重定向以 / 结尾的URL redirect: false, // 设置响应头 setHeaders: function ( res, path, stat ) { res.set( 'x-timestamp', Date.now() ) } } app.use( express.static( 'public', options ) ) |
所谓路由,是指决定应用程序如何相应客户端针对某个特定端点(Endpoint)的请求的过程。端点是URI和HTTP方法的组合。每个路由可以拥有多个处理函数,这些处理函数在路由匹配时执行。
要定义一个路由,可以调用:
1 2 3 4 5 |
app.METHOD(PATH, HANDLER) // app 为Express实例,也可以在express.Router对象上调用 // METHOD 是小写的HTTP方法名称 // PATH 服务器路径,不需要协议、域名、端口部分 // HANDLER 处理函数 |
示例:
1 2 3 4 5 6 7 |
app.get('/', function (req, res) { res.send('Hello World!') }) app.delete('/user', function (req, res) { res.send('Got a DELETE request at /user') }) |
Express支持以下HTTP方法: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,这些方法都对应到Express的实例方法。
有一个特殊的路由方法all,用于为某个路径上的所有HTTP方法加载中间件函数:
1 2 3 4 |
app.all( '/secret', function ( req, res, next ) { console.log( 'Accessing the secret section ...' ) next() // 把控制权交给下一个处理器 } ); |
路由路径即端点的URI部分,可以包含字符串、字符串模式、正则式。协议、域名、端口、查询串,不属于路由路径的组成部分。
对于字符串模式,可以使用 ? + * () :
1 2 3 4 |
'/ab?cd' // 重复0-1次,匹配 /abcd /acd '/ab+cd' // 重复1-N次,匹配 /abcd /abbcd /abbbcd '/ab*cd' // 匹配任意个字符,匹配 /abcd /abANYcd '/ab(cd)?e' // 匹配 /abe /abcde |
基于字符串来匹配路径时, - . 按照字面值解析。
使用正则式的例子:
1 2 3 4 5 6 |
// 匹配路径中具有字符a的任何路径 app.get( /a/, function ( req, res ) { } ) // 匹配以fly结尾的任何路径 app.get( /.*fly$/, function ( req, res ) { } ) |
URI中片断中可以包含变量声明,捕获的变量存放到 req.params 对象中。例如:
1 2 3 |
/users/:userId/books/:bookId # 可以捕获到 这样的参数: { "userId": "34", "bookId": "8989" } |
由于 - . 按照字面值解析,因此可以作为路由参数之间的分隔符:
1 2 3 |
/flights/:from-:to # 可以捕获到 这样的参数: { "from": "LAX", "to": "SFO" } |
你可以注册多个行为类似于中间件的路由处理器,用于处理请求。需要注意的是,这些回调函数可以调用 next() 来把请求处理权转交给下一个路由处理器:
1 2 3 4 5 6 |
router.get( '/', function ( req, res, next ) { next(); // 请求由下面的那个路由处理器来处理 }, function ( req, res ) { res.send( 'Hello World' ); res.end(); } ); |
多个路由器可以顺序传入,或者形成数组一起传入,或者两种方式混用。
该方法用于创建链式的路由处理器:
1 2 3 4 5 6 7 8 9 10 |
app.route( '/book' ) .get( function ( req, res ) { res.send( 'Get a random book' ) } ) .post( function ( req, res ) { res.send( 'Add a book' ) } ) .put( function ( req, res ) { res.send( 'Update the book' ) } ); |
该类用于创建模块化的、可挂载的路由处理器。Router的实例是一个完整的中间件和路由子系统。
下面的代码创建一个Router,在其中加载了一个中间件函数,然后定义几个路由规则(route):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var express = require( 'express' ) // 创建一个Router var router = express.Router() // 加载一个仅针对次Router使用的中间件 router.use( function timeLog( req, res, next ) { console.log( 'Time: ', Date.now() ) next() } ) // 定义index路由规则 router.get( '/', function ( req, res ) { res.send( 'Birds home page' ) } ) // 定义about路由规则 router.get( '/about', function ( req, res ) { res.send( 'About birds' ) } ) // 必须导出Router对象 module.exports = router |
你需要在Express应用中加载上述Router模块:
1 2 3 |
var birds = require('../routes/birds') // Router属于中间件。把URL前缀交由该Router处理 app.use('/birds', birds) |
这样,你访问/birds、/birds/about这两个URL时, routes/birds.js中定义的路由将获得处理权。
下面的方法用于像客户端发送响应,并且终结请求-响应处理周期。如果这些方法中的任一个都没有被调用,则客户端一直被挂起:
方法 | 说明 |
res.download() | 提示一个文件加载 |
res.end() | 结束响应处理 |
res.json() | 发送一个JSON响应 |
res.jsonp() | 发送一个带有JSONP支持的JSON响应 |
res.redirect() | 发送一个重定向响应 |
res.render() | 渲染一个视图模板 |
res.send() | 发送某种类型的响应 |
res.sendFile() | 以字节流发送文件 |
res.sendStatus() | 发送一个响应状态码,并将其字符串描述作为响应体发送 |
Express是一个路由、中间件Web框架,其本身的功能很少。一个典型的Express应用实际上是一系列中间件函数调用的组合。
Express的中间件与React的中间件的运作方式很类似。所谓Express中间件,是指以req、res、next为参数的函数。这三个函数分别代表请求、响应、应用程序请求处理周期中的下一个中间件函数。中间件可以具有以下行为:
- 执行任意代码
- 对请求、响应对象进行操作和修改
- 终止请求处理周期
- 调用下一个中间件
如果当前中间件没有终止请求处理周期,那么它就必须调用下一个中间件来移交控制权,否则客户端会一直挂起。
传递给Express或者express.Router的get/post...函数的路由处理器,本质上都属于中间件。
要加载一个中间件,可以调用Express或者express.Router的 use() 方法。下面的代码,在路由规则之前加载一个中间件:
1 2 3 4 5 6 7 8 9 10 |
var myLogger = function ( req, res, next ) { console.log( 'LOGGED' ) next() } app.use( myLogger ) app.get( '/', function ( req, res ) { res.send( 'Hello World!' ) } ) |
中间件的执行顺序,与它的加载顺序一致。先加载的中间件会先执行。 如果上面的use在get之后调用,则不会打印日志,因为get指定的路由处理器已经终止了请求处理周期。
中间件依据作用域、用途的不同,可以分为:应用级中间件、Router级中间件、错误处理中间件。对于应用级、Router级中间件,可以在加载中间件时指定一个可选的挂载路径(mount path)。你也可以把一系列中间件同时挂载,从而创建中间件系统的一个子栈(Sub stack)。
要绑定应用级中间件,需要调用app.use()或者app.METHOD(),后者用于注册特殊用途的中间件——路由处理器,METHOD必须是某种HTTP方法的小写形式。路由处理器也可以通过use注册,这样它就可以对所有类型的请求生效:
1 2 3 4 5 6 |
// 针对 /user/xxx 所有类型的HTTP方法生效 app.use( '/user/:id', function ( req, res, next ) { } ) // 针对 /user/xxx 的GET方法生效 app.get( '/user/:id', function ( req, res, next ) { } ) |
下面的例子,同时在一个挂载点加载多个中间件,形成子栈:
1 2 3 4 5 |
app.use( '/user/:id', function ( req, res, next ) { next() }, function ( req, res, next ) { next() } ) |
调用 next('route') 可以跳过当前子栈。你只能在app.METHOD()或者router.METHOD()中调用该方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 子栈一 app.get( '/user/:id', function ( req, res, next ) { // 如果用户ID为0,则跳过当前子栈,执行下一个路由规则(子栈二) if ( req.params.id === '0' ) next( 'route' ) // 否则,执行当前子栈的下一个中间件 else next() }, function ( req, res, next ) { // 子栈一的第二个中间件 res.render( 'regular' ) } ) // 子栈二 app.get( '/user/:id', function ( req, res, next ) { res.render( 'special' ) } ) |
与应用级的中间件工作方式一致,只是绑定到一个express.Router实例。express.Router用于处理URI的某个特定命名空间(前缀)。
这种中间件的签名中多一个参数:
1 2 3 4 |
app.use( function ( err, req, res, next ) { console.error( err.stack ) res.status( 500 ).send( 'Something broke!' ) } ) |
通常这种中间件放在链条的尾部,也就是应该最后调用。
如果调用next()时指定任何不为'route'的其它参数,Express认为错误已经发生,后续所有非错误处理中间件将被跳过。
Express包含了一个内置的默认错误处理器中间件,该中间件会自动的被加到中间件链条的尾部。如果你调用next(err)而又不提供任何错误处理中间件,则次默认中间件会处理err。
从Express 4.x开始,除了 express.static 以外的中间件函数都被分离到单独的模块中。
你可以使用第三方中间件来扩充Express的功能。例如:
1 2 |
# Cookie解析器中间件 npm install cookie-parser --save |
1 2 3 4 5 6 |
var express = require('express') var app = express() var cookieParser = require('cookie-parser') // 加载中间件 app.use(cookieParser()) |
通过模板引擎,你可以在应用程序中编写静态的模板文件,并在运行时替换模板中的变量占位符,最终生成客户端可用的HTML文件。
Express支持Pug、Mustache、EJS等主流模板引擎。它默认使用Jade引擎。
要使用模板引擎,你需要调用app.set来设置应用程序属性:
属性 | 说明 | ||
views | 设置模板文件的存放目录,默认值是应用根目录下的view子目录。示例:
|
||
view engine | 设置使用的模板引擎。示例:
|
调用res.render()方法可以渲染模板,并将其写入到响应体中:
1 2 3 4 |
app.get( '/', function ( req, res ) { // 渲染views/index.jade,并写入响应 res.render( 'index', { title: 'Hey', message: 'Hello there!' } ) } ) |
这是Express默认的模板引擎,你可以单独使用它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var jade = require( 'jade' ); var options = { compileDebug: false,// 是否包含调试信息 pretty: true // 格式化结果 } // 编译模板为函数 var fn = jade.compile( 'string of jade', options ); var html = fn( locals ); // 渲染字符串中的模板 var html = jade.render( 'string of jade', merge( options, locals ) ); // 渲染文件中的模板 var html = jade.renderFile( 'filename.jade', merge( options, locals ) ); |
模板示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
doctype html // 元素和属性,属性值可以包含插值 html(lang="#{lang}") // 嵌套的元素 head // 给元素赋值,即设置内部其文本节点,可以使用JS表达式 title= pageTitle // 多个属性,用空格分开,属性值也可以使用JS表达式 script(type=scrType src=''). if ( foo ) bar( 1 + 5 ) body h1 Jade - node template engine // div,id为container,样式类为col #container.col // 条件控制 if youAreUsingJade // 插值 p #{name} are amazing else p Get on it! |
Express使用debug模块来记录路由匹配、启用的中间件函数、应用程序模式、请求处理流程等信息。
debug是一个类似于console.log()的模块,但是在产品发布中,你不需要注释掉调试代码。因为除非设置好环境变量DEBUG,它不会做日志记录工作。
要查看所有Express的内部日志,可以设置: DEBUG=express:* 。要查看当前应用的日志信息,可以设置: DEBUG=express-study 。要启用指定名字空间的调试信息,可以设置: DEBUG=http,mail,express:*
要在Express应用中访问数据库,你需要加载合适的Node.js数据库驱动程序。Node.js支持主流的数据库,下面举例说明。
1 |
npm install mysql --save |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var mysql = require( 'mysql' ) // 创建连接 var connection = mysql.createConnection( { host: 'localhost', user: 'dbuser', password: 's3kreee7' } ) connection.connect() // 查询,注意Node.js这种强制的异步风格 connection.query( 'SELECT 1 + 1 AS solution', function ( err, rows, fields ) { if ( err ) throw err console.log( 'The solution is: ', rows[ 0 ].solution ) } ) // 关闭连接 connection.end() |
1 |
npm install mongodb --save |
1 2 3 4 5 6 7 8 9 10 |
var MongoClient = require( 'mongodb' ).MongoClient MongoClient.connect( 'mongodb://localhost:27017/animals', function ( err, db ) { if ( err ) throw err db.collection( 'mammals' ).find().toArray( function ( err, result ) { if ( err ) throw err console.log( result ) } ) } ) |
1 |
npm install redis --save |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var client = require( 'redis' ).createClient() client.on( 'error', function ( err ) { console.log( 'Error ' + err ) } ) client.set( 'string key', 'string val', redis.print ) client.hset( 'hash key', 'hashtest 1', 'some value', redis.print ) client.hset( [ 'hash key', 'hashtest 2', 'some other value' ], redis.print ) client.hkeys( 'hash key', function ( err, replies ) { console.log( replies.length + ' replies:' ) replies.forEach( function ( reply, i ) { console.log( ' ' + i + ': ' + reply ) } ) client.quit() } ) |
在生产环境使用Express时,你可以使用进程管理器,来达成以下目标:
- 在应用程序崩溃后,自动重新启动它
- 获取运行性能、资源消耗信息
- 动态调整设置以提高性能
- 控制集群
进程管理器是应用程序的容器,使用它可以便利部署、提供高可用性、允许你在运行时管理应用程序。有时候它就像服务器软件那样。
Express或者其它Node.js最常使用的进程管理器软件包括StrongLoop Process Manager、PM2、Forever。
StrongLoop是一个产品级的Node.js进程管理器,它包含了内置的负载均衡、监控、多主机部署支持,它还提供图形化的控制台。使用StrongLoop你可以:
- 构建、打包、部署Node.js应用程序,并在本地、远程机器上部署
- 显示CPU、内存剖析信息,以便性能优化和诊断内存泄漏
- 确保进程、集群不宕机
- 显示应用程序的性能统计信息
- 轻松的管理多主机部署、Nginx集成
StrongLoop提供的命令行接口是 slc,图形化接口是Arc。
执行下面的命令安装:
1 |
npm install -g strongloop |
命令行示例:
1 2 3 4 5 6 7 8 9 10 11 12 |
cd express-study # 启动Strongloop slc start # 查看状态和已经部署的应用 slc ctl # 停止、重启、软重启 # 软重启给予工作进程一段时间,来关闭既有连接 slc ctl stop express-study slc ctl restart express-study slc ctl soft-restart express-study # 移除受管程序 slc ctl remove |
使用HTTPS可以确保客户端 - 服务器之间的连接是安全的,数据不会被篡改或者窃听。
通过设置合适的HTTP头,该中间件能够保护你的应用不受场景的Web弱点的影响。Helmet会设置以下头:
Content-Security-Policy | 防止跨站脚本攻击,或者其它跨站攻击 |
X-Powered-By | 移除该头 |
Public Key Pinning | 防止基于伪造证书的中间人攻击 |
Strict-Transport-Security | 强制使用HTTPS协议 |
X-Download-Options | 设置为noopen,禁止IE8+,防止用户下载程序并执行 |
Cache-Control Pragma | 禁止客户端缓存 |
X-Content-Type-Options | 防止浏览器嗅探MEME类型 |
X-Frame-Options | 提供Clickjacking保护 |
X-XSS-Protection | 启用跨站脚本过滤,较新浏览器支持 |
执行下面的命令安装:
1 |
npm install --save helmet |
使用:
1 2 |
var helmet = require( 'helmet' ) app.use( helmet() ) |
如果你不使用Helmet,至少要禁用以下响应头:
1 |
app.disable('x-powered-by') |
该头允许攻击者探测服务器类型。攻击者可能针对Express进行攻击。
Express有两个主要中间件,实现基于Cookie的Session:express-session、cookie-session。两者的主要区别是存储Session数据的方式:
- express-session:把Session数据存放在服务器上,客户端仅仅存储一个Session ID。该中间件默认在内存中存储Session数据,因此不适用产品环境。注意选择合适的Session存储
- cookie-session:串行化整个Session数据到客户端,注意两点,一个是4096字节的限制,第二个是这些数据可能被客户端的恶意软件读取
这会让你容易受到攻击,修改示例:
1 2 3 4 5 |
var session = require( 'express-session' ) app.use( session( { secret: 's3Cur3', name: 'sessionId' } ) ) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
ar session = require( 'cookie-session' ) var express = require( 'express' ) var app = express() var expiryDate = new Date( Date.now() + 60 * 60 * 1000 ) app.use( session( { name: 'session', keys: [ 'key1', 'key2' ], cookie: { secure: true, // 确保浏览器仅仅通过HTTPS发送Cookie httpOnly: true, // 确保仅仅通过HTTP/HTTPS发送Cookie,禁止客户端JS发送,防止跨站脚本攻击 domain: 'example.com', // 最小化域名 path: 'foo/bar', // 最小化匹配路径 expires: expiryDate // 最小化有效期 } } ) ) |
安全风险 | 建议 |
暴力密码破解 | 使用StrongLoop API Gateway可以限制请求的频率,或者使用express-limiter这样的中间件 |
跨站请求伪造CSRF | 使用csurf中间件 |
跨站脚本攻击 | 注意总是过滤、处理用户输入 |
SQL注入攻击 | 可以使用sqlmap来检查应用程序弱点 |
Gzip压缩可以大大减小响应体的大小,因而可以加快速度、节约带宽。使用中间件:
1 2 3 4 |
var compression = require( 'compression' ) var express = require( 'express' ) var app = express() app.use( compression() ) |
对于高流量的网站,最好在反向代理服务器中实现压缩,此时不需要使用Express中间件。Nginx是反向代理的优秀备选,可以启用它的Gzip支持。
使用同步调用将极大的影响Node.js应用处理并发请求的能力,Node.js选项 --trace-sync-io 可以监控同步I/O并发起警告。
在应用程序中记录日志的目的由两个:调试、记录程序活动。
使用 console 对象的方法是最基本的日志记录手段,但是这些方法都是同步的。如果标准输出是一个Terminal或者文件,应用性能会受到很大影响。
对于调试日志,可以结合debug模块和DEBUG环境变量,这样在产品环境下不会有任何影响。
对于记录程序活动的日志,可以使用Winston或者Bunyan这样的日志库。
如果遭遇未捕获(uncaught exception)的异常,Node.js应用程序会崩溃掉。作为最后保险手段,你需要确保Worker进程崩溃后能自动重启。
重启总是要消耗一定的时间的,尽管Node.js应用通常启动很快。因此,你应该在异常发生的第一现场就做好处理。
全局的异常处理器,用来捕获JS代码中冒泡到事件循环的异常。这通常不是好办法,虽然程序会继续运行,但是其状态可能已经被破坏。
在同步代码中使用这种构造:
1 2 3 4 5 6 7 8 9 10 11 |
app.get('/search', function (req, res) { setImmediate(function () { var jsonStr = req.query.params try { var jsonObj = JSON.parse(jsonStr) res.send('Success') } catch (e) { res.status(400).send('Invalid JSON string') } }) }) |
使用Promise可以处理异步代码中的任何异常,你只需要在Promise链的尾部添加catch()即可:
1 2 3 4 5 6 7 8 9 10 11 |
app.get( '/', function ( req, res, next ) { queryDb() .then( function ( data ) { } ) .then( function ( csv ) { } ) .catch( next ) } ) app.use(function (err, req, res, next) { // 在这里处理异常 }) |
但是注意:
- 异步代码必须返回Promise,如果某个库不返回Promise,则你需要使用助手函数,例如 Bluebird.promisifyAll() 将其转换为Promise
- EventEmitter仍然可能导致未捕获异常,你必须合理的处理error事件:
123let stream = getLogoStreamById( company.id )// 转交给错误处理中间件stream.on( 'error', next ).pipe( res )
设置该环境变量为production,则Express会:
- 缓存视图模板
- 缓存Sass等CSS扩展生成的CSS文件
- 生成较为简洁的错误信息
如果你希望编写运行环境依赖的代码,可以访问 process.env.NODE_ENV 。
使用StrongLoop来管理应用程序进程,并把StrongLoop安装为系统服务:
1 2 |
# 安装StrongLoop为系统服务,系统启动后,它会自动启动受管的应用程序 sudo sl-pm-install --systemd |
多核系统中,通常会启动与核心数相同的Node.js应用,以提高性能。你可以使用Node.js内置的cluster模块或者StrongLoop。
使用Varnish或者Nginx 之类的缓存服务器,避免针对同一请求反复的执行Node.js代码。
考虑使用Nginx或者HAProxy之类的负载均衡器。要实现会话关联性(session affinity),可以使用Redis之类的外部存储来保存会话数据。
StrongLoop集成了一个Nginx控制器,可以方便的在多台机器上部署Node.js应用并实现负载均衡。
Leave a Reply