12_Node.js Web 开发
下面开始用 Node.js 进行 Web 开发。
我是通过《Node.js开发指南》这本书来学习 Node.js Web 开发的,书中使用的 Express 框架是 2.5.8,而我的是 4.14.1,所以遇到了许多问题,在文章中我都有提到并讲解。
一、快速开始
1、建立项目
《Node.js开发指南》中建立项目的方式是:express -t ejs microblog,但是这种方式对于高版本的 Express 新建的标签替换引擎并不是 .ejs,而是 .jade,如果要使用 .ejs 我们可以
通过下面命令建立网站基本结构。
express -e NodeJSBlog
执行命令后在当前目录下出现了一些文件,并且下边提示我们通过 npm install 安装依赖。
在 npm install 之后,打开 NodeJSBlog 目录下的 package.json,可以看到已安装的包及对应的版本号。
2、启动服务器
注意,我们之前开启 Node.js 服务器,都是执行 node xxx.js,然后去浏览器访问即可,但是 Express 4.x 以上就不是这种方式了,应该是 npm start,端口配置在 bin/www 中。
启动成功访问 localhost:3000/。
3、项目结构
我们看一下 express 在 NodeJSBlog 这个目录下都生成了哪些文件。
app.js
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var index = require('./routes/index');
var users = require('./routes/users');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', index);
app.use('/users', users);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
app.js 是项目的入口,首先引入了一系列我们所需要的模块,然后引入了 routes 目录下的两个本地模块,它的功能是为指定路径组织返回内容,相当于 MVC 架构中的控制器。
接下来是视图引擎设置, app.set() 是 Express 的参数设置工具,接受一个键(key)和一个值(value),可用的参
数如下所示:
- basepath:基础地址,通常用于 res.redirect() 跳转。
- views:视图文件的目录,存放模板文件。
- view engine:视图模板引擎。
- view options:全局视图参数对象。
- view cache:启用视图缓存。
- case sensitive routes:路径区分大小写。
- strict routing:严格路径,启用后不会忽略路径末尾的“ / ”。
- jsonp callback:开启透明的 JSONP 支持
Express 依赖于 connect,提供了大量的中间件,可以通过 app.use() 启用
routes/index.js
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;
routes/index.js 是路由文件,相当于控制器,用于组织展示的内容,app.js 中通过 app.get('/', routes.index); 将“ / ”路径映射到 exports.index
函数下,其中只有一个语句 res.render('index', { title: 'Express' }),功能是
调用模板解析引擎,翻译名为 index 的模板,并传入一个对象作为参数,这个对象只有一个
属性,即 title: 'Express'。
views/index.ejs
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
</body>
</html>
index.ejs 是模板文件,即 routes/index.js 中调用的模板,内容是:
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
它的基础是 HTML 语言,其中包含了形如 <%= title %> 的标签,功能是显示引用的
变量,即 res.render 函数第二个参数传入的对象的属性。
补充 include
在书中 views 目录下是有 layout.ejs 的,它可以让所有模板去继承它,<%- body %> 中是独特的内容,其他部分是共有的,可以看作是页面框架。
书中 layout.ejs:
<head>
<title>
<%= title %>
</title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<%- body %>
</body>
</html>
但是 Express 4.x 就没有 layout.ejs 了,解决方法:
官方推荐了 include 方式,它不仅能实现 layout 的功能,还是将 view 的那些可复用的 html 片段提取成模块,在需要使用的地方直接用 <% include xxx %>。
例如先在 views 目录下新建一个 public_file.ejs ,在里面添加需要引用的公共文件:
<link rel='stylesheet' href='/stylesheets/style.css' />
然后修改一下 index.ejs,使用 <% include listitem %> 方式引用上边公共文件:
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<% include public_file %>
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
</body>
</html>
重启服务,访问 localhost:3000/,可以看到 style.css 文件正常引用。
每一次修改我们都需要重启服务才能看到修改后的结果,您可以看我的《Node.js 应用程序自动重启》这篇文章去安装 supervisor 或 nodemon 两个插件来实现应用程序自动重启。
二、路由控制
1、创建页面路由
简单说一下新增一个页面的流程。
首先在 views 目录下新建一个模板,例如 hello.ejs:
然后打开 index.js 文件,添加页面路由信息:
访问 localhost:3000/hello。
补充:在《Node.js开发指南》这本书中,还需要向 app.js 文件中添加页面路由信息,但在 Express 4.x 中是不需要的。
2、路径匹配
上面的例子是为固定的路径设置路由规则,Express 还支持更高级的路径匹配模式,例
如我们想要展示一个用户的个人页面,路径为 /user/[username],可以用下面的方法定义路由
规则:
router.get('/user/:username', function(req, res, next) {
res.send('user: ' + req.params.username);
});
重启项目,访问 localhost:3000/user/LiuZhenghe。
注意:调用模板解析引擎,用 res.render(),只是向页面发送数据,用 res.send()。
路径规则 /user/:username 会被自动编译为正则表达式,类似于 /user/([^/]+)/?
这样的形式,路径参数可以在响应函数中通过 req.params 的属性访问。
路径规则同样支持 JavaScript 正则表达式,例如 app.get(/user/([^/]+)/?,
callback),这样的好处在于可以定义更加复杂的路径规则,而不同之处是匹配的参数是匿
名的,因此需要通过 req.params[0]、req.params[1] 这样的形式访问。
3、REST 风格的路由规则
Express 支持 REST 风格的请求方式,在介绍之前我们先说明一下什么是 REST。
REST 的意思是 表征状态转移(Representational State Transfer),它是一种基于 HTTP 协议的网络应
用的接口风格,充分利用 HTTP 的方法实现统一风格接口的服务。
HTTP 协议定义了以下 8 种标准的方法:
- GET:请求获取指定资源。
- HEAD:请求指定资源的响应头。
- POST:向指定资源提交数据。
- PUT:请求服务器存储一个资源。
- DELETE:请求服务器删除指定资源。
- TRACE:回显服务器收到的请求,主要用于测试或诊断。
- CONNECT:HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。
- OPTIONS:返回服务器支持的HTTP请求方法。
其中我们经常用到的是 GET、POST、PUT 和 DELETE 方法,根据 REST 设计模式,这
4种方法通常分别用于实现以下功能。
- GET:获取
- POST:新增
- PUT:更新
- DELETE:删除
这是因为这 4 种方法有不同的特点,按照定义,它们的特点如下表所示:
请求方式 | 安全 | 幂等 |
---|---|---|
GET | 是 | 是 |
POST | 否 | 否 |
PUT | 否 | 是 |
DELETE | 否 | 是 |
所谓安全是指没有副作用,即请求不会对资源产生变动,连续访问多次所获得的结果不
受访问者的影响,而幂等指的是重复请求多次与一次请求的效果是一样的,比如获取和更
新操作是幂等的,这与新增不同,删除也是幂等的,即重复删除一个资源,和删除一次是一样的。
Express 对每种 HTTP 请求方法都设计了不同的路由绑定函数,例如前面例子全部是
app.get,表示为该路径绑定了 GET 请求,向这个路径发起其他方式的请求不会被响应。
下表是 Express 支持的所有 HTTP 请求的绑定函数。
请求方式 | 绑定函数 |
---|---|
GET | app.get(path, callback) |
POST | app.post(path, callback) |
PUT | app.put(path, callback) |
DELETE | app.delete(path, callback) |
PATCH | app.patch(path, callback) |
TRACE | app.trace(path, callback) |
CONNECT | app.connect(path, callback) |
OPTIONS | app.options(path, callback) |
所有方法 | app.all(path, callback) |
例如我们要绑定某个路径的 POST 请求,则可以用 app.post(path, callback) 的
方法,需要注意的是 app.all 函数,它支持把所有的请求方式绑定到同一个响应函数,是
一个非常灵活的函数,在后面我们可以看到许多功能都可以通过它来实现。
4、控制权转移
Express 支持同一路径绑定多个路由响应函数,例如:
index.js
// ...
/* 路径匹配模式 */
router.all('/user/:username', function(req, res, next) {
res.send('all methods captured');
});
router.get('/user/:username', function(req, res, next) {
res.send('user: ' + req.params.username);
});
// ...
当再次访问 localhost:3000/user/LiuZhenghe 时,发现页面被第一条路由规则捕获。
原因是 Express 在处理路由规则时,会优先匹配先定义的路由规则,因此后面相同的规则被屏蔽。
Express 提供了路由控制权转移的方法,即回调函数的第三个参数 next,通过调用
next(),会将路由控制权转移给后面的规则,例如:
// ...
/* 路径匹配模式 */
router.all('/user/:username', function(req, res, next) {
console.log('all methods captured');
next();
});
router.get('/user/:username', function(req, res, next) {
res.send('user: ' + req.params.username);
});
// ...
此时刷新页面,在控制台可以看到“all methods captured”,浏览器显示了 user: LiuZhenghe。
这是一个非常有用的工具,可以让我们轻易地实现中间件,而且还能提高代码的复用程
度,例如我们针对一个用户查询信息和修改信息的操作,分别对应了 GET 和 PUT 操作,而
两者共有的一个步骤是检查用户名是否合法,因此可以通过 next() 方法实现。
三、模板引擎
模板引擎也就是视图,视图决定了用户最终能看到什么,这里我们用 ejs 为例介绍模板引擎的使用方法。
1、使用模板引擎
在 app.js 中,以下两句设置了模板引擎和页面模板的位置:
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
在 index.js 中通过 res.render() 调用模板,res.render() 的功能是调用模板引擎,并将其产生的页面直接返回给客户端,它接受
两个参数,第一个是模板的名称,即 views 目录下的模板文件名,不包含文件的扩展名;第
二个参数是传递给模板的数据,用于模板翻译。
ejs 的标签系统非常简单,它只有以下 3 种标签:
- <% code %>:JavaScript 代码。
- <%= code %>:显示替换过 HTML 特殊字符的内容。
- <%- code %>:显示原始 HTML 内容。
我们可以用它们实现页面模板系统能实现的任何内容。
2、片段视图
《Node.js开发指南》中所讲的片段视图(partials)在 Express 4.x 中已经不支持了,在上面项目结构分析那一节中我曾补充过 include,这里再次介绍一下它的用法。
官方推荐了 include 方式,它不仅能实现 layout 的功能,还是将 view 的那些可复用的 html 片段提取成模块,在需要使用的地方直接用 <% include xxx %>,看下面这个例子:
首先在 index.js 中新增以下内容:
// 片断视图
router.get('/list', function(reg, res) {
res.render('list', {
title: "List",
items: [2019, 'Node.js', 'NodeJSBlog', 'Express']
});
});
然后新建 list.ejs 文件并添加以下内容:
<ul>
<% items.forEach(function(listitem){ %>
<% include listitem %>
<% }) %>
</ul>
同时新建 listitem.ejs 文件并添加:
<li><%= listitem %></li>
访问 localhost:3000/list,可以看到以下内容:
四、开始建立博客网站
1、功能分析
博客网站首先应该有登录注册功能,然后是最核心的功能——信息发表,这个功能涉及到许多方面,包括数据库访问,前端显示等。
一个完整的博客系统,应该有评论,收藏,转发等功能,处于本人目前的能力水平还不能都实现,先做一个博客网站的雏形吧。
2、路由规划
根据功能设计,我们把路由按照以下方案规划:
- /:首页
- /u/[user]:用户的主页
- /post:发表信息
- /reg:用户注册
- /login:用户登录
- /logout:用户登出
以上页面还可以根据用户状态细分,发表信息以及用户登出页面必须是已登录用户才能操作的功能,而用户注册和用户登入所面向的对象必须是未登入的用户,首页和用户主页则针对已登入和未登入的用户显示不同的内容。
在 index.js 中添加以下内容:
router.get('/', function(req, res) {
res.render('index', {
title: 'Express'
});
});
router.get('/u/:user', function(req, res) {});
router.post('/post', function(req, res) {});
router.get('/reg', function(req, res) {});
router.post('/reg', function(req, res) {});
router.get('/login', function(req, res) {});
router.post('/login', function(req, res) {});
router.get('/logout', function(req, res) {});
其中 /post、/login 和 /reg 由于要接受表单信息,因此使用 app.post 注册路由,/login
和 /reg 还要显示用户注册时要填写的表单,所以要以 app.get 注册
3、使用 Bootstrap
下载 jquery.js,bootstrap.css 和 bootstrap.js,放到 public 下对应的目录中。
在 public_file.ejs 中引用,可以使用 include 方法给模板添加公共文件。
去 Bootstrap官网 查看并使用所需的模板或组件。
下图是我从 Bootstrap 官网找的一个模板,并放到了 index.ejs 目录下并进行了简单地修改,并将页面公共部分(头尾部分)取出,用 include 方式来复用。
五、用户注册和登录
1、访问数据库
我们选用 MongoDB 作为网站的数据库系统,它是一个开源的 NoSQL 数据库,相比
MySQL 那样的关系型数据库,它更为轻巧、灵活,非常适合在数据规模很大、事务性不强
的场合下使用。
连接数据库
通过 npm 安装 mongodb。
npm install mongodb --save
补充:通过 --save 安装,包名和版本号将会出现在 package.json 中。
接下来在项目主目录中创建 settings.js 文件,这个文件用于保存数据库的连接信息,我们将用到的数据库命名为 NodeJSBlog,数据库服务
器在本地,因此 settings.js 文件的内容如下:
settings.js
module.exports = {
cookieSecret: 'NodeJSBlogbyvoid',
db: 'NodeJSBlog',
host: 'localhost',
};
其中,db 是数据库的名称,host 是数据库的地址,cookieSecret 用于 Cookie 加密与数
据库无关,我们留作后用。
接下来新建 models 目录,并在目录中创建 db.js:
models/db.js
var settings = require('../settings.js');
var Db = require('mongodb').Db;
var Connection = require('mongodb').Connection;
var Server = require('mongodb').Server;
module.exports = new Db(settings.db, new Server(settings.host, Connection.DEFAULT_ PORT, {}));
以上代码通过 module.exports 输出了创建的数据库连接,在后面的小节中我们会用
到这个模块,由于模块只会被加载一次,以后我们在其他文件中使用时均为这一个实例。
2、会话支持
会话是一种持久的网络协议,用于完成服务器和客户端之间的一些交互行为。会话是一个比连接粒度更大的概念,一次会话可能包含多次连接,每次连接都被认为是会话的一次操作。在网络应用开发中,有必要实现会话以帮助用户交互。例如网上购物的场景,用户浏览了多个页面,购买了一些物品,这些请求在多次连接中完成。许多应用层网络协议都是由会话支持的,如 FTP、Telnet 等,而 HTTP 协议是无状态的,本身不支持会话,因此在没有额外手段的帮助下,前面场景中服务器不知道用户购买了什么。
为了在无状态的 HTTP 协议之上实现会话,Cookie 诞生了。Cookie 是一些存储在客户端的信息,每次连接的时候由浏览器向服务器递交,服务器也向浏览器发起存储 Cookie 的请求,依靠这样的手段服务器可以识别客户端。我们通常意义上的 HTTP 会话功能就是这样实现的。具体来说,浏览器首次向服务器发起请求时,服务器生成一个唯一标识符并发送给客户端浏览器,浏览器将这个唯一标识符存储在 Cookie 中,以后每次再发起请求,客户端浏览器都会向服务器传送这个唯一标识符,服务器通过这个唯一标识符来识别用户。对于开发者来说,我们无须关心浏览器端的存储,需要关注的仅仅是如何通过这个唯一标识符来识别用户。很多服务端脚本语言都有会话功能,如 PHP,把每个唯一标识符存储到文件中。
Express 也提供了会话中间件,默认情况下是把用户信息存储在内存中,但我们既然已经有了 MongoDB,不妨把会话信息存储在数据库中,便于持久维护。为了使用这一功能,我们首先要通过 npm 安装一个 connect-mongo 的模块:
然后打开 app.js,添加以下内容:
app.js
var MongoStore = require('connect-mongo');
var settings = require('settings');
app.configure(function() {
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser());
app.use(express.session({
secret: settings.cookieSecret,
store: new MongoStore({
db: settings.db
})
}));
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
其中 express.cookieParser() 是 Cookie 解析的中间件。express.session() 则提供会话支持,设置它的 store 参数为 MongoStore 实例,把会话信息存储到数据库中,以避免丢失。
在后面的小节中,我们可以通过 req.session 获取当前用户的会话对象,以维护用
户相关的信息。
未完待续......