下面开始用 Node.js 进行 Web 开发。css
我是经过《Node.js开发指南》这本书来学习 Node.js Web 开发的,书中使用的 Express 框架是 2.5.8,而个人是 4.14.1,因此遇到了许多问题,在文章中我都有提到并讲解。html
☞GitHub 地址前端
《Node.js开发指南》中创建项目的方式是:express -t ejs microblog,可是这种方式对于高版本的 Express 新建的标签替换引擎并非 .ejs,而是 .jade,若是要使用 .ejs 咱们能够经过下面命令创建网站基本结构。node
express -e NodeJSBlog
复制代码
执行命令后在当前目录下出现了一些文件,而且下边提示咱们经过 npm install 安装依赖。jquery
在 npm install 以后,打开 NodeJSBlog 目录下的 package.json,能够看到已安装的包及对应的版本号。git
注意,咱们以前开启 Node.js 服务器,都是执行 node xxx.js,而后去浏览器访问便可,可是 Express 4.x 以上就不是这种方式了,应该是 npm start,端口配置在 bin/www 中。github
启动成功访问 localhost:3000/。正则表达式
咱们看一下 express 在 NodeJSBlog 这个目录下都生成了哪些文件。mongodb
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),可用的参数以下所示:
Express 依赖于 connect,提供了大量的中间件,能够经过 app.use() 启用
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'。
<!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 函数第二个参数传入的对象的属性。
在书中 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 两个插件来实现应用程序自动重启。
简单说一下新增一个页面的流程。
首先在 views 目录下新建一个模板,例如 hello.ejs:
而后打开 index.js 文件,添加页面路由信息:
访问 localhost:3000/hello。
补充:在《Node.js开发指南》这本书中,还须要向 app.js 文件中添加页面路由信息,但在 Express 4.x 中是不须要的。
上面的例子是为固定的路径设置路由规则,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] 这样的形式访问。
Express 支持 REST 风格的请求方式,在介绍以前咱们先说明一下什么是 REST。
REST 的意思是表征状态转移(Representational State Transfer),它是一种基于 HTTP 协议的网络应用的接口风格,充分利用 HTTP 的方法实现统一风格接口的服务。
HTTP 协议定义了如下 8 种标准的方法:
其中咱们常常用到的是 GET、POST、PUT 和 DELETE 方法,根据 REST 设计模式,这4种方法一般分别用于实现如下功能。
这是由于这 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 函数,它支持把全部的请求方式绑定到同一个响应函数,是一个很是灵活的函数,在后面咱们能够看到许多功能均可以经过它来实现。
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 为例介绍模板引擎的使用方法。
在 app.js 中,如下两句设置了模板引擎和页面模板的位置:
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
复制代码
在 index.js 中经过 res.render() 调用模板,res.render() 的功能是调用模板引擎,并将其产生的页面直接返回给客户端,它接受两个参数,第一个是模板的名称,即 views 目录下的模板文件名,不包含文件的扩展名;第二个参数是传递给模板的数据,用于模板翻译。
ejs 的标签系统很是简单,它只有如下 3 种标签:
咱们能够用它们实现页面模板系统能实现的任何内容。
《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,能够看到如下内容:
博客网站首先应该有登陆注册功能,而后是最核心的功能——信息发表,这个功能涉及到许多方面,包括数据库访问,前端显示等。
一个完整的博客系统,应该有评论,收藏,转发等功能,处于本人目前的能力水平还不能都实现,先作一个博客网站的雏形吧。
根据功能设计,咱们把路由按照如下方案规划:
以上页面还能够根据用户状态细分,发表信息以及用户登出页面必须是已登陆用户才能操做的功能,而用户注册和用户登入所面向的对象必须是未登入的用户,首页和用户主页则针对已登入和未登入的用户显示不一样的内容。
在 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 因为要接受表单信息,所以使用 router.post 注册路由,/login 和 /reg 还要显示用户注册时要填写的表单,因此要以 router.get 注册。
下载 jquery.js,bootstrap.css 和 bootstrap.js,放到 public 下对应的目录中。
在 public_file.ejs 中引用,可使用 include 方法给模板添加公共文件。
去 Bootstrap官网 查看并使用所需的模板或组件。
下图是我从 Bootstrap 官网找的一个模板,并放到了 index.ejs 目录下并进行了简单地修改,并将页面公共部分(头尾部分)取出,用 include 方式来复用。
咱们选用 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'),
Db = require('mongodb').Db,
Connection = require('mongodb').Connection,
Server = require('mongodb').Server;
module.exports = new Db(settings.db, new Server(settings.host, 27017, {}), {
safe: true
});
复制代码
以上代码经过 module.exports 输出了建立的数据库链接,在后面的小节中咱们会用到这个模块,因为模块只会被加载一次,之后咱们在其余文件中使用时均为这一个实例。
《Node.js开发指南》中对会话支持是这样描述的:
会话是一种持久的网络协议,用于完成服务器和客户端之间的一些交互行为。会话是一个比链接粒度更大的概念,一次会话可能包含屡次链接,每次链接都被认为是会话的一次操做。在网络应用开发中,有必要实现会话以帮助用户交互。例如网上购物的场景,用户浏览了多个页面,购买了一些物品,这些请求在屡次链接中完成。许多应用层网络协议都是由会话支持的,如 FTP、Telnet 等,而 HTTP 协议是无状态的,自己不支持会话,所以在没有额外手段的帮助下,前面场景中服务器不知道用户购买了什么。
为了在无状态的 HTTP 协议之上实现会话,Cookie 诞生了。Cookie 是一些存储在客户端的信息,每次链接的时候由浏览器向服务器递交,服务器也向浏览器发起存储 Cookie 的请求,依靠这样的手段服务器能够识别客户端。咱们一般意义上的 HTTP 会话功能就是这样实现的。具体来讲,浏览器首次向服务器发起请求时,服务器生成一个惟一标识符并发送给客户端浏览器,浏览器将这个惟一标识符存储在 Cookie 中,之后每次再发起请求,客户端浏览器都会向服务器传送这个惟一标识符,服务器经过这个惟一标识符来识别用户。
对于开发者来讲,咱们无须关心浏览器端的存储,须要关注的仅仅是如何经过这个惟一标识符来识别用户。不少服务端脚本语言都有会话功能,如 PHP,把每一个惟一标识符存储到文件中。Express 也提供了会话中间件,默认状况下是把用户信息存储在内存中,但咱们既然已经有了 MongoDB,不妨把会话信息存储在数据库中,便于持久维护。
可是若是你的 Express 版本是 4.x,按照书中接下来的内容编写代码,就会出各类问题,下面我来讲一下 Express 4.x 中的会话支持。
在 Express 4.x 中咱们须要本身安装 express-session 包,而后添加引用:
var session = require('express-session');
复制代码
而后再引用 connect-mongo 包:
var MongoStore = require('connect-mongo')(session);
复制代码
最终在 app.js 中新增的内容就是:
var settings = require('./settings');
var session = require('express-session');
var MongoStore = require('connect-mongo')(session);
app.use(session({
secret: settings.cookieSecret,
store: new MongoStore({
db: settings.db,
})
}));
复制代码
可是此时程序会报错:'Connection strategy not found':
从网上查找到该问题以后找到了解决办法,打开 package.json 文件,将 connect-mongo 的版本改成 0.8.2,而后执行 npm install 便可解决。
若是程序没有报错,那么就成功了,你可使用 mongo.exe 或可视化工具来查看数据库是否新建成功。
经过可视化工具或 mongodb 终端新建一个用户表 users。
注册页面:
首先添加注册页面的模板 views/reg.ejs,并添加表单结构:
<div class="container">
<div class="bs-example" data-example-id="basic-forms">
<form>
<div class="form-group">
<label for="username">用户名</label>
<input type="text" class="form-control" id="username" placeholder="请输入用户名">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" class="form-control" id="password" placeholder="请输入密码">
</div>
<div class="form-group">
<label for="password_repeat">再次输入密码</label>
<input type="password" class="form-control" id="password_repeat" placeholder="再次输入密码">
</div>
<button type="submit" class="btn btn-default">注册</button>
</form>
</div>
</div>
复制代码
而后打开 index.js 添加注册页面的路由信息:
index.js
router.get('/reg', function(req, res) {
res.render('reg', {
title: '用户注册'
});
});
复制代码
而后访问 localhost:3000/reg。
注册响应:
在书中使用了 flash,可是最新版本的 Express 已经不支持 flash 了,你须要先经过 npm 安装 connect-flash。
而后在 app.js 中添加以下代码:
var flash = require('connect-flash');
复制代码
在 routes/index.js 中添加 /reg 的 POST 响应函数:
routes/index.js
// 注册响应
var crypto = require('crypto');
var User = require('../models/user.js');
var MongoClient = require('mongodb').MongoClient;
const DB_CONN_STR='mongodb://localhost:27017/users';
router.post('/reg', function(req, res) {
let newUser = {
username: req.body.username,
password: req.body.password,
password_repeat: req.body.password_repeat
};
let addStr = [{
username: newUser.username,
password: newUser.password
}];
MongoClient.connect(DB_CONN_STR, function(err, db) {
db.collection('users').findOne({
username: newUser.username
}, function(err, result) {
if (!result) {
if (newUser.password === newUser.password_repeat) {
MongoClient.connect(DB_CONN_STR, function(err, db) {
req.session.error = '注册成功,请登陆!';
db.collection('users').insert(addStr);
db.close();
return res.redirect('/login');
});
} else {
req.session.error = '两次密码不一致!';
return res.redirect('/register');
}
} else {
req.session.error = '用户名已存在!';
return res.redirect('/register');
}
})
db.close();
});
});
复制代码
用户模型
在前面的代码中,咱们直接使用了 User 对象,User 是一个描述数据的对象,即 MVC 架构中的模型,前面咱们使用了许多视图和控制器,这是第一次接触到模型。与视图和控制器不一样,模型是真正与数据打交道的工具,没有模型,网站就只是一个外壳,不能发挥真实的做用,所以它是框架中最根本的部分。如今就让咱们来实现 User 模型吧。
在 models 目录中建立 user.js 的文件,内容以下:
models/user.js
var mongodb = require('./db');
function User(user) {
this.name = user.name;
this.password = user.password;
};
module.exports = User;
User.prototype.save = function save(callback) {
// 存入 Mongodb 的文档
var user = {
name: this.name,
password: this.password,
};
mongodb.open(function(err, db) {
if (err) {
return callback(err);
}
// 读取 users 集合
db.collection('users', function(err, collection) {
if (err) {
mongodb.close();
return callback(err);
}
// 为 name 属性添加索引
collection.ensureIndex('name', {
unique: true
});
// 写入 user 文档
collection.insert(user, {
safe: true
}, function(err, user) {
mongodb.close();
callback(err, user);
});
});
});
};
User.get = function get(username, callback) {
mongodb.open(function(err, db) {
if (err) {
return callback(err);
}
// 读取 users 集合
db.collection('users', function(err, collection) {
if (err) {
mongodb.close();
return callback(err);
}
// 查找 name 属性为 username 的文档
collection.findOne({
name: username
}, function(err, doc) {
mongodb.close();
if (doc) {
// 封装文档为 User 对象
var user = new User(doc);
callback(err, user);
} else {
callback(err, null);
}
});
});
});
};
复制代码
以上代码实现了两个接口,User.prototype.save 和 User.get,前者是对象实例的方法,用于将用户对象的数据保存到数据库中,后者是对象构造函数的方法,用于从数据库中查找指定的用户。
视图交互
如今几乎已经万事俱备,只差视图的支持了。为了实现不一样登陆状态下页面呈现不一样内容的功能,咱们须要建立动态视图助手,经过它咱们才能在视图中访问会话中的用户数据,同时为了显示错误和成功的信息,也要在动态视图助手中增长响应的函数。
在书中,在 app.js 中添加的视图交互代码是:
app.dynamicHelpers({
user: function(req, res) {
return req.session.user;
},
error: function(req, res) {
var err = req.flash('error');
if (err.length)
return err;
else
return null;
},
success: function(req, res) {
var succ = req.flash('success');
if (succ.length)
return succ;
else
return null;
},
});
复制代码
可是在 Express 4.x 中会报错“app.dynamicHelpers is not a function ”,此处应该添加:
app.js
app.use(flash());
app.use(function(req, res, next) {
res.locals.user = req.session.user;
res.locals.post = req.session.post;
var error = req.flash('error');
res.locals.error = error.length ? error : null;
var success = req.flash('success');
res.locals.success = success.length ? success : null;
next();
});
复制代码
注意,这段代码不要放的太靠后,应该放到路由控制代码以前。
接下来修改公共导航部分。
header.ejs
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">NodeJS Blog</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<form class="navbar-form navbar-right">
<% if (!user) { %>
<a href="/login" type="submit" class="btn btn-success">登陆</a>
<a href="/reg" type="submit" class="btn btn-success">注册</a>
<% } else { %>
<a href="" type="submit" class="btn">注销</a>
<% } %>
</form>
</div>
</div>
</nav>
复制代码
而后打开注册页,输入用户名、密码,点击注册按钮,发现又遇到坑了...“db.collection is not a function”。
引发这个错误的缘由是你经过 npm 安装的 mongodb 的版本和你 Node.js 操做数据的 api 版本不一致,查看了一下 package.json,发现 mongodb 的版本是 3.1.13。
解决方法,下载低版本 mongodb,例:
"mongodb": "^2.2.33",
复制代码
npm install 安装。
未完待续......