Express入门

安装

创建一个目录,然后进入此目录并将其作为当前工作目录。然后通过npm init命令为你的应用创建一个package.json文件。接下来安装Express并将其保存到依赖列表中。

mkdir myapp
cd myapp
npm init
npm install express --save

Express应用生成器

通过应用生成器工具express可以快速创建一个应用的骨架。
通过如下命令安装:

$ npm install express-generator -g

-h选项可以列出所有可用的命令行选项:

$ express -h

  Usage: express [options] [dir]
  Options:

        --version        output the version number
    -e, --ejs            add ejs engine support
        --pug            add pug engine support
        --hbs            add handlebars engine support
    -H, --hogan          add hogan.js engine support
    -v, --view <engine>  add view <engine> support (dust|ejs|hbs|hjs|jade|pug|twig|vash) (defaults to jade)
        --no-view        use static html instead of view engine
    -c, --css <engine>   add stylesheet <engine> support (less|stylus|compass|sass) (defaults to plain css)
        --git            add .gitignore
    -f, --force          force on non-empty directory
    -h, --help           output usage information

例如,下面的示例就是在当前工作目录下创建一个命名为myapp的应用。

$ express myapp

   create : myapp
   create : myapp/package.json
   create : myapp/app.js
   create : myapp/public
   create : myapp/public/javascripts
   create : myapp/public/images
   create : myapp/routes
   create : myapp/routes/index.js
   create : myapp/routes/users.js
   create : myapp/public/stylesheets
   create : myapp/public/stylesheets/style.css
   create : myapp/views
   create : myapp/views/index.jade
   create : myapp/views/layout.jade
   create : myapp/views/error.jade
   create : myapp/bin
   create : myapp/bin/www

然后安装所有依赖包:

$ cd myapp 
$ npm install

启动这个应用(MacOS或Linux平台):

$ DEBUG=myapp npm start

Windows平台使用如下命令:

> set DEBUG=myapp & npm start

然后在浏览器中打开http://localhost:3000/网址就可以看到这个应用了。
通过Express应用生成器创建的应用一般都有如下目录结构:

.
├── app.js
├── bin
│   └── www
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   ├── index.js
│   └── users.js
└── views
    ├── error.jade
    ├── index.jade
    └── layout.jade

7 directories, 9 files

路由

路由是指如何定义应用的端点以及如何响应客户端的请求。
路由是由一个URI、HTTP请求和若干个句柄组成,它的结构如下:app.METHOD(path, [callback...], callback)appexpress对象的一个实例,METHOD是一个HTTP请求方法,path是服务器上的路径,callback是当路由匹配时要执行的函数。
下面是一个基本的路由示例:

var express = require('express');
var app = express();
app.get('/', function(req, res) {
  res.send('hello world');
});

路由方法

路由方法源于HTTP请求方法,和express实例相关联。
下面这个例子展示了为应用跟路径定义的GET和POST请求:

// GET method route
app.get('/', function (req, res) {
  res.send('GET request to the homepage');
});
// POST method route
app.post('/', function (req, res) {
  res.send('POST request to the homepage');
});

Express定义了如下和HTTP请求对应的路由方法:getpostputheaddeleteoptionstracecopylockmkcolmovepurgepropfindproppatchunlockreportmkactivitycheckoutmergem-searchnotifysubscribeunsubscribepatchsearch、和connect
有些路由方法名不是合规的 JavaScript 变量名,此时使用括号记法,比如: app['m-search']('/', function() {})
app.all()是一个特殊的路由方法,没有任何HTTP方法与其对应,它的作用是对于一个路径上的所有请求加载中间件。
在下面的例子中,来自“/secret”的请求,不管使用GET、POST、PUT、DELETE或其他任何http模块支持的HTTP请求,句柄都会得到执行。

app.all('/secret', function (req, res, next) {
  console.log('Accessing the secret section ...');
  next(); // pass control to the next handler
});

路由路径

路由路径和请求方法一起定义了请求的端点,它可以是字符串、字符串模式或者正则表达式。
查询字符串不是路由路径的一部分。

// 使用字符串的路由路径
// 匹配根路径的请求
app.get('/', function (req, res) {
  res.send('root');
});
// 匹配 /about 路径的请求
app.get('/about', function (req, res) {
  res.send('about');
});
// 匹配 /random.text 路径的请求
app.get('/random.text', function (req, res) {
  res.send('random.text');
});

// 使用字符串模式的路由路径
// 匹配 acd 和 abcd
app.get('/ab?cd', function(req, res) {
  res.send('ab?cd');
});
// 匹配 abcd、abbcd、abbbcd等
app.get('/ab+cd', function(req, res) {
  res.send('ab+cd');
});
// 匹配 abcd、abxcd、abRABDOMcd、ab123cd等
app.get('/ab*cd', function(req, res) {
  res.send('ab*cd');
});
// 匹配 /abe 和 /abcde
app.get('/ab(cd)?e', function(req, res) {
 res.send('ab(cd)?e');
});

字符?、+、*()是正则表达式的子集,-.在基于字符串的路径中按照字面值解释。

// 使用正则表达式的路由路径
// 匹配任何路径中含有a的路径
app.get(/a/, function(req, res) {
  res.send('/a/');
});
// 匹配 butterfly、dragonfly,不匹配butterflyman、dragonfly man等
app.get(/.*fly$/, function(req, res) {
  res.send('/.*fly$/');
});

路由句柄

可以为请求处理提供多个回调函数,其行为类似中间件。唯一的区别是这些回调函数有可能调用next('route')方法而略过其他路由回调函数。可以利用该机制为路由定义前提条件,如果在现有路径上继续执行没有意义,则可将控制权交给剩下的路径。
路由句柄有多种形式,可以是一个函数、一个函数数组,或者是两者混合。

// 使用一个回调函数处理路由
app.get('/example/a', function (req, res) {
  res.send('Hello from A!');
});
// 使用多个回调函数处理路由(记得指定`next`对象)
app.get('/example/b', function (req, res, next) {
  console.log('response will be sent by the next function ...');
  next();
}, function (req, res) {
  res.send('Hello from B!');
});
// 使用回调函数数组处理路由
var cb0 = function (req, res, next) {
  console.log('CB0');
  next();
}
var cb1 = function (req, res, next) {
  console.log('CB1');
  next();
}
var cb2 = function (req, res) {
  res.send('Hello from C!');
}
app.get('/example/c', [cb0, cb1, cb2]);
// 混合使用函数和函数数组处理路由
var cb0 = function (req, res, next) {
  console.log('CB0');
  next();
}
var cb1 = function (req, res, next) {
  console.log('CB1');
  next();
}
app.get('/example/d', [cb0, cb1], function (req, res, next) {
  console.log('response will be sent by the next function ...');
  next();
}, function (req, res) {
  res.send('Hello from D!');
});

响应方法

下表中响应对象(res)的方法向客户端返回响应,终结请求响应的循环。如果在路由句柄中一个方法也不调用,来自客户端的请求会一直挂起。

方法 描述
res.download() 提示下载文件。
res.end() 终结响应处理流程。
res.json() 发送一个JSON格式的响应。
res.jsonp() 发送一个支持JSONP的JSON格式的响应。
res.redirect() 重定向请求。
res.render() 渲染视图模板。
res.send() 发送各种类型的响应。
res.sendFile 以八位字节流的形式发送文件。
res.sendStatus() 设置响应状态代码,并将其以字符串形式作为响应体的一部分发送。

app.route()

可使用app.route()创建路由路径的链式路由句柄。由于路径在一个地方指定,这样做有助于创建模块化的路由。

使用app.route()定义链式路由句柄
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');
  });

express.Router

可使用express.Router类创建模块化、可挂载的路由句柄。Router实例是一个完整的中间件和路由系统,因此常称其为一个mini-app
下面的实例程序创建了一个路由模块,并加载了一个中间件,定义了一些路由,并且将它们挂载至应用的路径上。
app目录下创建名为birds.js的文件,内容如下:

var express = require('express');
var router = express.Router();
// 该路由使用的中间件
router.use(function timeLog(req, res, next) {
  console.log('Time: ', Date.now());
  next();
});
// 定义网站主页的路由
router.get('/', function(req, res) {
  res.send('Birds home page');
});
// 定义 about 页面的路由
router.get('/about', function(req, res) {
  res.send('About birds');
});
module.exports = router;

然后在应用中加载路由模块。

var birds = require('./birds');
app.use('/birds', birds);

应用即可处理发自/birds/birds/about的请求,并且调用为该路由指定的timeLog中间件。

使用中间件

Express是一个自身功能极简,完全是由路由和中间件构成一个的web开发框架:从本质上来说,一个Express应用就是在调用各种中间件。
中间件(Middleware)是一个函数,它可以访问请求对象(request object(req)), 响应对象(response object(res)),和web应用中处于请求—响应循环流程中的中间件,一般被命名为next的变量。
中间件的功能包括:

  • 执行任何代码
  • 修改请求和响应对象
  • 终结请求-响应循环
  • 调用堆栈中的下一个中间件

如果当前中间件没有终结请求—响应循环,则必须调用next()方法将控制权交给下一个中间件,否则请求就会挂起。
Express应用可使用如下几种中间件:

  • 应用级中间件
  • 路由级中间件
  • 错误处理中间件
  • 内置中间件
  • 第三方中间件

使用可选则挂载路径,可在应用级别或路由级别装载中间件。另外,还可以同时装在一系列中间件函数,从而在一个挂载点上创建一个子中间件栈。

应用级中间件

应用级中间件绑定到app对象使用app.use()app.METHOD(),其中,METHOD是需要处理的HTTP请求的方法,例如GET,PUT,POST等等,全部小写。

var app = express();
// 没有挂载路径的中间件,应用的每个请求都会执行该中间件
app.use(function (req, res, next) {
  console.log('Time:', Date.now());
  next();
});
// 挂载至/user/:id的中间件,任何指向/user/:id的请求都会执行它
app.use('/user/:id', function (req, res, next) {
  console.log('Request Type:', req.method);
  next();
});
// 路由和句柄函数(中间件系统),处理指向/user/:id的GET请求
app.get('/user/:id', function (req, res, next) {
  res.send('USER');
});

下面这个例子展示了在一个挂载点装载一组中间件。

// 一个中间件栈,对任何指向/user/:id的HTTP请求打印出相关信息
app.use('/user/:id', function(req, res, next) {
  console.log('Request URL:', req.originalUrl);
  next();
}, function (req, res, next) {
  console.log('Request Type:', req.method);
  next();
});

作为中间件系统的路由句柄,使得为路径定义多个路由成为可能。在下面的例子中,为指向/user/:id的GET请求定义了两个路由。第二个路由虽然不会带来任何问题,但却永远不会被调用,因为第一个路由已经终止了请求—响应循环。

// 一个中间件栈,处理指向/user/:id的GET请求
app.get('/user/:id', function (req, res, next) {
  console.log('ID:', req.params.id);
  next();
}, function (req, res, next) {
  res.send('User Info');
});
// 处理 /user/:id,打印出用户id
app.get('/user/:id', function (req, res, next) {
  res.end(req.params.id);
});

如果需要在中间件栈中跳过剩余中间件,调用next('route')方法将控制权交给下一个路由。 注意:next('route')只对使用app.VERB()router.VERB()加载的中间件有效。

// 一个中间件栈,处理指向/user/:id的GET请求
app.get('/user/:id', function (req, res, next) {
  // 如果user id为0, 跳到下一个路由
  if (req.params.id == 0) next('route');
  // 否则将控制权交给栈中下一个中间件
  else next();
}, function (req, res, next) {
  res.render('regular');   // 渲染常规页面
});
// 处理/user/:id,渲染一个特殊页面
app.get('/user/:id', function (req, res, next) {
  res.render('special');
});

路由级中间件

路由级中间件和应用级中间件一样,只是它绑定的对象为express.Router()

var router = express.Router();

路由级使用router.use()router.VERB()加载。
上述在应用级创建的中间件系统,可通过如下代码改写为路由级。

var app = express();
var router = express.Router();
// 没有挂载路径的中间件,通过该路由的每个请求都会执行该中间件
router.use(function (req, res, next) {
  console.log('Time:', Date.now());
  next();
});
// 一个中间件栈,显示任何指向/user/:id的HTTP请求的信息
router.use('/user/:id', function(req, res, next) {
  console.log('Request URL:', req.originalUrl);
  next();
}, function (req, res, next) {
  console.log('Request Type:', req.method);
  next();
});
// 一个中间件栈,处理指向/user/:id的GET请求
router.get('/user/:id', function (req, res, next) {
  // 如果 user id 为 0, 跳到下一个路由
  if (req.params.id == 0) next('route');
  // 负责将控制权交给栈中下一个中间件
  else next();
}, function (req, res, next) {
  res.render('regular');   // 渲染常规页面
});
// 处理 /user/:id, 渲染一个特殊页面
router.get('/user/:id', function (req, res, next) {
  console.log(req.params.id);
  res.render('special');
});
// 将路由挂载至应用
app.use('/', router);

错误处理中间件

错误处理中间件有4个参数,定义错误处理中间件时必须使用这4个参数。即使不需要next对象,也必须在签名中声明它,否则中间件会被识别为一个常规中间件,不能处理错误。

app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

内置中间件

从4.x版本开始,Express已经不再依赖Connect了。除了express.static,Express以前内置的中间件现在已经全部单独作为模块安装使用了。

express.static(root, [options])

express.static是Express唯一内置的中间件。它基于serve-static,负责在Express应用中提供管静态资源。
参数root指提供静态资源的根目录。可选的options参数拥有如下属性。

属性 描述 类型 缺省值
dotfiles 是否对外输出文件名以点(.)开头的文件。可选值为 allowdenyignore String ignore
etag 是否启用etag生成 Boolean true
extensions 设置文件扩展名备份选项 Array []
index 发送目录索引文件,设置为false禁用目录索引。 Mixed index.html
lastModified 设置Last-Modified头为文件在操作系统上的最后修改日期。可能值为truefalse Boolean true
maxAge 以毫秒或者其字符串格式设置Cache-Control头的max-age属性。 Number 0
redirect 当路径为目录时,重定向至/ Boolean true
setHeaders 设置HTTP头以提供文件的函数。 Function
var options = {
  dotfiles: 'ignore',
  etag: false,
  extensions: ['htm', 'html'],
  index: false,
  maxAge: '1d',
  redirect: false,
  setHeaders: function (res, path, stat) {
    res.set('x-timestamp', Date.now());
  }
}
app.use(express.static('public', options));

每个应用可有多个静态目录。

app.use(express.static('public'));
app.use(express.static('uploads'));
app.use(express.static('files'));

第三方中间件

通过使用第三方中间件从而为Express应用增加更多功能。
安装所需功能的node模块,并在应用中加载,可以在应用级加载,也可以在路由级加载。

// 安装并加载解析cookie的中间件:cookie-parser
$ npm install cookie-parser
var express = require('express');
var app = express();
var cookieParser = require('cookie-parser');
// 加载用于解析cookie的中间件
app.use(cookieParser());

在Express中使用模板引擎

需要在应用中进行如下设置才能让Express渲染模板文件:

  • views:放模板文件的目录,比如:app.set('views', './views')
  • view engine:模板引擎,比如:app.set('view engine', 'jade')

然后安装相应的模板引擎npm软件包。

$ npm install jade --save

和Express兼容的模板引擎,比如Jade,通过res.render()调用其导出方法__express(filePath, options, callback)渲染模板。
有一些模板引擎不遵循这种约定,Consolidate.js能将Node中所有流行的模板引擎映射为这种约定,这样就可以和Express无缝衔接。
一旦view engine设置成功,就不需要显式指定引擎,或者在应用中加载模板引擎模块,Express已经在内部加载,如下所示。

app.set('view engine', 'jade');

views目录下生成名为index.jadeJade模板文件,内容如下:

html
  head
    title!= title
  body
    h1!= message

然后创建一个路由渲染index.jade文件。如果没有设置view engine,您需要指明视图文件的后缀,否则就会遗漏它。

app.get('/', function (req, res) {
  res.render('index', { title: 'Hey', message: 'Hello there!'});
});

此时向主页发送请求,index.jade会被渲染为HTML。

错误处理

定义错误处理中间件和定义其他中间件一样,除了需要4个参数,而不是3个,其格式如下(err, req, res, next)。例如:

app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

在其他app.use()和路由调用后,最后定义错误处理中间件。

var bodyParser = require('body-parser');
var methodOverride = require('method-override');
app.use(bodyParser());
app.use(methodOverride());
app.use(function(err, req, res, next) {
  // 业务逻辑
});

中间件返回的响应是随意的,可以响应一个HTML错误页面、一句简单的话、一个JSON字符串,或者其他任何东西。
为了便于组织(更高级的框架),可能会像定义常规中间件一样,定义多个错误处理中间件。比如您想为使用XHR的请求定义一个,还想为没有使用的定义一个,那么:

var bodyParser = require('body-parser');
var methodOverride = require('method-override');
app.use(bodyParser());
app.use(methodOverride());
app.use(logErrors);
app.use(clientErrorHandler);
app.use(errorHandler);

logErrors将请求和错误信息写入标准错误输出、日志或类似服务。

function logErrors(err, req, res, next) {
  console.error(err.stack);
  next(err);
}

clientErrorHandler的定义如下(注意这里将错误直接传给了next):

function clientErrorHandler(err, req, res, next) {
  if (req.xhr) {
    res.status(500).send({ error: 'Something blew up!' });
  } else {
    next(err);
  }
}

errorHandler能捕获所有错误,其定义如下:

function errorHandler(err, req, res, next) {
  res.status(500);
  res.render('error', { error: err });
}

如果向next()传入参数(除了'route'字符串),Express会认为当前请求有错误的输出,因此跳过后续其他非错误处理和路由/中间件函数。如果需做特殊处理,需要创建新的错误处理路由。
如果路由句柄有多个回调函数,可使用'route'参数跳到下一个路由句柄。比如:

app.get('/a_route_behind_paywall', 
  function checkIfPaidSubscriber(req, res, next) {
    if(!req.user.hasPaid) { 
      // 继续处理该请求
      next('route');
    }
  }, function getPaidContent(req, res, next) {
    PaidContent.find(function(err, doc) {
      if(err) return next(err);
      res.json(doc);
    });
  });

在这个例子中,句柄getPaidContent会被跳过,但app中为/a_route_behind_paywall定义的其他句柄则会继续执行。
next()next(err)类似于Promise.resolve()Promise.reject()。它们让您可以向Express发信号,告诉它当前句柄执行结束并且处于什么状态。next(err)会跳过后续句柄,除了那些用来处理错误的句柄。

缺省错误处理句柄

Express内置了一个错误处理句柄,它可以捕获应用中可能出现的任意错误。这个缺省的错误处理中间件将被添加到中间件堆栈的底部。
如果你向next()传递了一个error,而你并没有在错误处理句柄中处理这个error,Express内置的缺省错误处理句柄就是最后兜底的。最后错误将被连同堆栈追踪信息一同反馈到客户端。堆栈追踪信息并不会在生产环境中反馈到客户端。
设置环境变量NODE_ENV“production”就可以让应用运行在生产环境模式下。
如果你已经开始向response输出数据了,这时才调用next()并传递了一个error,比如你在将向客户端输出数据流时遇到一个错误,Express内置的缺省错误处理句柄将帮你关闭连接并告知request请求失败。
因此,当你添加了一个自定义的错误处理句柄后,如果已经向客户端发送包头信息了,你还可以将错误处理交给Express内置的错误处理机制。

function errorHandler(err, req, res, next) {
  if (res.headersSent) {
    return next(err);
  }
  res.status(500);
  res.render('error', { error: err });
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容