6 Express 介绍

提到node,那么就不得不提大名鼎鼎的 express,作为一个web framework,它几乎满足了你所有的愿望 。
本篇的内容主要讲述express的基本使用。

6.1 Express 安装

当然作为一个web framework,必然要牵扯到各种配置。聪明人肯定不是吧所有配置代码从头到尾敲出来,
这就要提到 express-generator 。首先运行

npm install -g express-generator

来安装,这里用-g参数来将其安装为全局位置,因为这个样子我们就能将其安装后生成的可执行程序添加到环境变量中了。

接着运行express -e first-app && cd first-app,其中命令中-e参数是说使用 ejs模板引擎来渲染视图。
first-app就是我们生成程序生成的目录,紧接着我们通过 cd 命令进入了这个目录。最后我们运行 npm install 命令来安装所需依赖。
最终在图形化界面中进入这个目录,会看到如下文件列表:

  1. --bin
  2. ----www
  3. --public
  4. ----images
  5. ----javascripts
  6. ----stylesheets
  7. --routes
  8. --views
  9. --app.js
  10. --package.json

目录 6.1.1

6.2 Express 基本操作

express的所有配置信息在app.js中:

  1. var express = require('express');
  2. var path = require('path');
  3. var favicon = require('serve-favicon');
  4. var logger = require('morgan');
  5. var cookieParser = require('cookie-parser');
  6. var bodyParser = require('body-parser');
  7. var routes = require('./routes/index');
  8. var users = require('./routes/users');
  9. var app = express();
  10. // view engine setup
  11. app.set('views', path.join(__dirname, 'views'));
  12. app.set('view engine', 'ejs');
  13. // uncomment after placing your favicon in /public
  14. //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
  15. app.use(logger('dev'));
  16. app.use(bodyParser.json());
  17. app.use(bodyParser.urlencoded({ extended: false }));
  18. app.use(cookieParser());
  19. app.use(express.static(path.join(__dirname, 'public')));
  20. app.use('/', routes);
  21. app.use('/users', users);
  22. // catch 404 and forward to error handler
  23. app.use(function(req, res, next) {
  24. var err = new Error('Not Found');
  25. err.status = 404;
  26. next(err);
  27. });
  28. // error handlers
  29. // development error handler
  30. // will print stacktrace
  31. if (app.get('env') === 'development') {
  32. app.use(function(err, req, res, next) {
  33. res.status(err.status || 500);
  34. res.render('error', {
  35. message: err.message,
  36. error: err
  37. });
  38. });
  39. }
  40. // production error handler
  41. // no stacktraces leaked to user
  42. app.use(function(err, req, res, next) {
  43. res.status(err.status || 500);
  44. res.render('error', {
  45. message: err.message,
  46. error: {}
  47. });
  48. });
  49. module.exports = app;

代码 6.2.1 app.js
express处理网络请求,是以一种被称之为 middlewave(也翻译为中间件) 机制进行的,即网络请求先经过第一个middlewave,如果处理完成则直接返回,否则调用 next() 函数将当前请求丢给下一个middlewave进行处理。我们看到app.js中有很多 app.use函数的调用,正是这个函数配置了一个个的middleware。
其中app.use(bodyParser.json());处理请求头为application/json的数据,其实这个middleware一般用不到;app.use(bodyParser.urlencoded({ extended: false }));这句话是处理form表单数据的,这个用处就大了。app.use(cookieParser());是用来处理cookie的,没有这个middleware的话,无法读取http请求中的cookie数据。app.use(express.static(path.join(__dirname, 'public')));是定义从 public 路径读取静态文件。之前讲过当前项目目录中存在public 文件夹,假设我们在其下 javascripts 目录中放置一个 query.js 文件,那么我们在html中就可以这么引用这个静态文件:

<script type="text/javascript" src="/javascripts/jquery.js"></script>

其他静态文件依次类推。

如果是做网站项目的话,还缺少对于session的支持,这个要在后面单独讲。接下来是很重要的路由映射部分,因为项目中的url映射都是
在这里配置的。我们这里只看 routes/index.js 文件:

  1. var express = require('express');
  2. var router = express.Router();
  3. /* GET home page. */
  4. router.get('/', function(req, res, next) {
  5. res.render('index', { title: 'Express' });
  6. });
  7. module.exports = router;

代码 6.2.2 routes/index.js

在express 3.x中,定义路由需要使用到在app.js中定义的app对象,写成app.get('/path',function(req,res){})的样式,不过经过本人测试
在express 4.x中依然可用。如果采用express 3.x的编写方式,那么routes/index.js可以写成这样:

  1. module.exports = function (app) {
  2. app.get('/', function(req, res) {
  3. res.render('index', { title: 'Express' });
  4. });
  5. }

对应在app.js中,需要将app.use('/',routes);替换成routes(app);。两种写作手法而已,看个人喜好。

我们看到这里仅仅只有一个根路径的映射,假设我们网站上还有一个 /about 的路径。那么就可以在index.js中再追加一条:

  1. router.get('/about', function(req, res) {
  2. res.render('index', { title: 'about' });
  3. });

代码 6.2.3
app.js中定义的 app.use('/',routes);,其实其中的/仅仅是定义路由的路径前缀而已,从这种意义上来讲,routes/index.jsroutes/user.js 的代码是可以合并的。我们删除user.js 文件,然后在index.js中追加一段代码:

  1. /* GET users listing. */
  2. router.get('/user', function(req, res) {
  3. res.send('respond with a resource');
  4. });

代码 6.2.4
这样我们就可以删除掉app.use('/users', users);了。其实如果看完上述关于路由器的介绍,熟悉express 3的用户会发现,除了语法和3.x不一样以外,功能上没啥不同。不过事实并非如此,index.js中的router对象还可以直接用来定义middleware,我们在 index.js 开头再添加一段代码:

  1. // middleware specific to this router
  2. router.use(function timeLog(req, res, next) {
  3. console.log('Time: ', Date.now());
  4. next();
  5. });

代码 6.2.5
那么上述代码定义的这个middleware就仅仅对 index.js 内部定义的地址起作用,对于这个路由器文件外的代码是不起作用的,这个设计就比较灵活了。之前咱们在 app.js 中通过 app.use 来定义middleware,那么理论上所有的请求都要经过这种middleware进行处理的,除非在经过这个middleware之前,已经有其他的middleware把HTTP请求处理完成了。
最后看错误捕获这一块了,app.js中对于代码捕获区分了相中情况,如果当前是开发环境就在出错的时候打印堆栈,否则只显示错误名称。我们现在修改一下 /user 的路由代码:

  1. router.get('/user', function(req, res) {
  2. console.log(noneExistVar.pp);
  3. res.send('respond with a resource');
  4. });

代码 6.2.6
接着运行项目(关于如何运行项目,将在下面讲到),然后在浏览器中打开http://localhost:3000/user,浏览器直接显示错误堆栈:

  1. noneExistVar is not defined
  2. ReferenceError: noneExistVar is not defined
  3. at D:\code\eapp\first-app\routes\index.js:15:14
  4. at Layer.handle [as handle_request] (D:\code\eapp\first-app\node_modules\express\lib\router\layer.js:95:5)
  5. at next (D:\code\eapp\first-app\node_modules\express\lib\router\route.js:131:13)
  6. at Route.dispatch (D:\code\eapp\first-app\node_modules\express\lib\router\route.js:112:3)
  7. at Layer.handle [as handle_request] (D:\code\eapp\first-app\node_modules\express\lib\router\layer.js:95:5)
  8. at D:\code\eapp\first-app\node_modules\express\lib\router\index.js:277:22
  9. at Function.process_params (D:\code\eapp\first-app\node_modules\express\lib\router\index.js:330:12)
  10. at next (D:\code\eapp\first-app\node_modules\express\lib\router\index.js:271:10)
  11. at Function.handle (D:\code\eapp\first-app\node_modules\express\lib\router\index.js:176:3)
  12. at router (D:\code\eapp\first-app\node_modules\express\lib\router\index.js:46:12)

输出 6.2.1
这说明,程序默认走到 app.get('env') === 'development' 这个条件中去了。app.get('env') 其实是读取的环境变量 NODE_ENV ,这是一个express专用的环境变量,express官方推荐在生产环境将其设置为 production(参考这里)后会带来三倍的性能提升。官方推荐使用 systemd 或者 Upstart来设置环境变量,不过如果你的程序不是开机自启动的话,直接配置 .bash_profile文件即可,也就是说直接在该文件中添加 export NODE_ENV=production

6.6 模板引擎

在代码6.2.2中遇到了一个render函数,这个函数就是express中用于加载模板的函数。通过代码也可以大体看出,第一个参数是模板的名字,它所代表的文件位于视图文件夹views目录下的index.ejsejs文件后缀是ejs模板引擎的默认后缀);而第二个参数即为传递给这个模板的参数。
接着看一下在模板中,是怎样使用刚才传递的那个titile参数的,打开views文件夹下的index.ejs

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title><%= title %></title>
  5. <link rel='stylesheet' href='/stylesheets/style.css' />
  6. </head>
  7. <body>
  8. <h1><%= title %></h1>
  9. <p>Welcome to <%= title %></p>
  10. </body>
  11. </html>

可以看到使用<%=titile%>的方式就可以把之前render函数中传递的title参数读取出来。
扩展一下在ejs中还有两个常见的标签:
<%- %>:读取变量中的值且对于变量中的html特殊符号(比如<、>、&、”等)不进行转义,如果使用<%=%>就会把特殊符号转义,
<%%>:写在这个标签里的语句会直接当成代码来解析,比如说如下代码:

  1. <% if (status == 0) { %>
  2. <input type="button" value="启用" />
  3. <% } else { %>
  4. <input type="button" value="禁用" />
  5. <% } %>

6.7 Express 中的GET和POST

接下来的内容来讲一下express中怎样使用get和post,首先我们在views文件夹下新建目录user,然后在user目录下新建文件sign.ejs(当然你也可以把它当成静态页,放到public中;但是正常环境下,对于html一般都是通过视图的方式来加载)。

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  5. <title>Node.js注册演示</title>
  6. </head>
  7. <body>
  8. <h1>注册</h1>
  9. <form id="signup" method="get" action="/users/do/sign">
  10. <label> 帐号:</label><input type="text" name="username" />
  11. <label> Email:</label><input type="text" name="email" />
  12. <input type="submit" value="注册" /><br>
  13. </form>
  14. </body>
  15. </html>

代码6.7.1 sign.ejs代码

这里表单method是get(虽然一般情况下网服务器添加数据都是用post方式,但是这里为了演示方便,现将其写成get)。接下来看一下express中怎样在GET方式下获取表单中的数据。为了演示用户注册这个流程,我们新建controllers目录,在里面创建user_controller.js文件:

  1. exports.showSign = function(req, res) {
  2. res.render('user/sign');
  3. }
  4. exports.doSign = function(req, res) {
  5. var name = req.query.name;
  6. var email = req.query.email;
  7. res.send('恭喜' + name +'注册成功,你的邮箱为:'+email);
  8. }

代码6.7.2 user_controller.js文件中处理函数

web编程中广泛使用MVC(模型Model、视图View、控制器Controller,这三个单词的缩写)的设计模式,在项目创建controllers正是为了符合这一模式,同时你还需要创建一个models文件夹,专门负责处理数据。具体的使用流程是这样的:controllers里面放置请求处理的代码,即接收请求参数,对其进行有效性校验,然后调用models里面的代码进行数据操作(比如说数据库的增删改查等操作),拿到处理结果后加载视图进行渲染。关于MVC的介绍,可以参见维基百科

然后添加相应的路由如下:

  1. router.get('/users/sign', user.showSign);
  2. router.get('/users/do/sign', user.doSign);

代码6.7.3 新增路由配置

运行npm start,即可查看效果,打开http://localhost:3000/users/sign ,可看到如下界面:

注册显示界面

输入数据后,点击注册,显示提示信息:

注册成功显示界面

这就完成了get操作,但是前面提到了类似于这种注册操作一般都是用post的,将上面的代码改成post是很简单的,只需在代码代码6.7.1 中将表单的method改成post,代码6.7.2中获取请求数据是这么写的:

  1. var name = req.query.name;
  2. var email = req.query.email;

如果改成post,只需将其改为

  1. var name = req.body.name;
  2. var email = req.body.email;

6.8 Express AJAX 应用示例

还是上面的例子,只不过这次换成用ajax来提交数据,我们在views/user文件夹下再新建文件sign2.ejs:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  5. <title>Node.js注册演示</title>
  6. <script language="javascript" src="/javascripts/jquery-1.10.2.js"></script>
  7. </head>
  8. <body>
  9. <h1>注册</h1>
  10. <form id="signup" method="post" action="/users/sign2">
  11. <label>帐号:</label><input type="text" name="name" /><br />
  12. <label>Email:</label><input type="text" name="email" /><br />
  13. <input type="submit" value="注册" /><br>
  14. </form>
  15. <script language="javascript">
  16. $(document).ready(function() {
  17. $('#signup').submit(function() {
  18. $.post($(this).attr('action'),$(this).serialize(),function(result) {
  19. if (result.code == 0) {
  20. alert('注册成功');
  21. } else {
  22. if (result.msg) {
  23. alert(result.msg);
  24. } else {
  25. alert('服务器异常');
  26. }
  27. }
  28. },'json')
  29. return false;
  30. });
  31. });
  32. </script>
  33. </body>
  34. </html>

代码6.8.1 sign2.ejs

为了使用ajax,我们引入了jquery,并将jquery-1.10.2.js放到了public/javascripts文件夹下,为了演示ajax和普通请求处理的区别,这里仅仅给出处理post请求的代码:

  1. exports.doSign2 = function(req, res) {
  2. var name = req.body.name;
  3. var result = {};
  4. if (!name) {
  5. result.code = 1;
  6. result.msg = '账号不能为空';
  7. res.send(result);
  8. return;
  9. }
  10. var email = req.body.email;
  11. if (!email) {
  12. result.code = 2;
  13. result.msg = '邮箱不能为空';
  14. res.send(result);
  15. return;
  16. }
  17. res.send({code : 0});
  18. }

代码6.8.2 ajax后台处理代码

express中res的send函数中传一个json对象,则发送给浏览器的时候会自动序列化成json字符串。
我们继续添加两个路由:

  1. router.get('/users/sign2', user.showSign2);
  2. router.post('/users/do/sign2', user.doSign2);

代码6.8.3 ajax相关路由
重启项目后访问地址http://localhost:3000/users/sign2 即可进行测试。

6.9 代码

本章用的部分代码:https://github.com/yunnysunny/expressdemo/tree/master/chapter6