目录
5.1 准备
5.2 Express 框架
路由控制;
模板解析支持;
动态视图;
用户会话;
CSRF 保护;
静态文件服务;
错误控制器;
访问日志;
缓存;
插件支持。
- 安装 Express
为了使用这个工具,我们需要用全局模式安装Express,因为只有这样我们才能在命令行中使用它。运行以下命令:
$ npm install -g express
express --help 查看帮助信息:
Usage: express [options] [path]
Options:
-s, --sessions add session support
-t, --template <engine> add template <engine> support (jade|ejs). default=jade
-c, --css <engine> add stylesheet <engine> support (stylus). default=plain css
-v, --version output framework version
-h, --help output help information
Express 在初始化一个项目的时候需要指定模板引擎,默认支持Jade和ejs。
- 建立工程
通过以下命令建立网站基本结构:
express -t ejs microblog
$ cd microblog && npm install
其中 dependencies 属性中有express 和ejs。无参数的 npm install 的功能就是检查当前目录下的 package.json,并自动安装所有指定的依赖。
- 启动服务器
要关闭服务器的话,在终端中按 Ctrl + C。
- 工程的结构
app.js 是工程的入口,我们先看看其中有什么内容:
/**
* Module dependencies.
*/
var express = require('express')
, routes = require('./routes');
var app = module.exports = express.createServer();
// Configuration
app.configure(function(){
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
app.configure('development', function(){
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
app.configure('production', function(){
app.use(express.errorHandler());
});
// Routes
app.get('/', routes.index);
app.listen(3000);
console.log("Express server listening on port %d in %s mode", app.address().port,
app.settings.env);
app.set 是 Express 的参数设置工具
basepath:基础地址,通常用于 res.redirect() 跳转。
views:视图文件的目录,存放模板文件。
view engine:视图模板引擎。
view options:全局视图参数对象。
view cache:启用视图缓存。
case sensitive routes:路径区分大小写。
strict routing:严格路径,启用后不会忽略路径末尾的“ / ”。
jsonp callback:开启透明的 JSONP 支持。
/*
* GET home page.
*/
exports.index = function(req, res) {
res.render('index', { title: 'Express' });
};
这是一个典型的 MVC 架构,浏览器发起请求,由路由控制器接受,根据不同的路径定向到不同的控制器。控制器处理用户的具体请求,可能会访问数据库中的对象,即模型部分。控制器还要访问模板引擎,生成视图的 HTML,最后再由控制器返回给浏览器,完成一次请求。
- 创建路由规则
- 路径匹配
- 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:删除
- 控制权转移
5.3 模板引擎
模板引擎(Template Engine)是一个从页面模板根据一定的规则生成 HTML 的工具。它的发轫可以追溯到 1996 年 PHP 2.0 的诞生。PHP 原本是 Personal Home Page Tools(个人主页工具)的简称,用于取代 Perl 和 CGI 的组合,其功能是让代码嵌入在 HTML 中执行,以产生动态的页面,因此 PHP 堪称是最早的模板引擎的雏形。随后的 ASP、JSP 都沿用了这个模式,即建立一个 HTML 页面模板,插入可执行的代码,运行时动态生成 HTML。
页面功能逻辑与页面布局样式耦合,网站规模变大以后逐渐难以维护。
语法复杂,对于非技术的网页设计者来说门槛较高,难以学习。
功能过于全面,页面设计者可以在页面上编程,不利于功能划分,也使模板解析效
率降低。
- 使用模板引擎
ejs 的标签系统非常简单,它只有以下3种标签。
<% code %>:JavaScript 代码。
<%= code %>:显示替换过 HTML 特殊字符的内容。
<%- code %>:显示原始 HTML 内容。
- 页面布局
- 片段视图
Express 的视图系统还支持片段视图(partials),它就是一个页面的片段,通常是重复的内容,用于迭代显示。通过它你可以将相对独立的页面块分割出去,而且可以避免显式地使用 for 循环。
5.4 建立微博网站
- 功能分析
开发中的一个大忌就是没有想清楚要做什么就开始动手,因此我们准备在动手实践之前先规划一下网站的功能,即使是出于学习目的也不例外。首先,微博应该以用户为中心,因此需要有用户的注册和登录功能。微博网站最核心的功能是信息的发表,这个功能涉及许多方面,包括数据库访问、前端显示等。一个完整的微博系统应该支持信息的评论、转发、圈点用户等功能,但出于演示目的,我们不能一一实现所有功能,只是实现一个微博社交网站的雏形。 - 路由规划
- /:首页
- /u/[user]:用户的主页
- /post:发表信息
- /reg:用户注册
- /login:用户登录
- /logout:用户登出
app.get('/', routes.index);
app.get('/u/:user', routes.user);
app.post('/post', routes.post);
app.get('/reg', routes.reg);
app.post('/reg', routes.doReg);
app.get('/login', routes.login);
app.post('/login', routes.doLogin);
app.get('/logout', routes.logout);
exports.index = function(req, res) {
res.render('index', { title: 'Express' });
};
exports.user = function(req, res) {
};
exports.post = function(req, res) {
};
exports.reg = function(req, res) {
};
exports.doReg = function(req, res) {
};
exports.login = function(req, res) {
};
exports.doLogin = function(req, res) {
};
exports.logout = function(req, res) {
};
- 界面设计
- 使用 Bootstrap
- 连接数据库
- 会话支持
- 用户模型
- 视图交互
app.get('/login', function(req, res) {
res.render('login', {
title: '用户登入',
});
});
app.post('/login', function(req, res) {
//生成口令的散列值
var md5 = crypto.createHash('md5');
var password = md5.update(req.body.password).digest('base64');
User.get(req.body.username, function(err, user) {
if (!user) {
req.flash('error', '用户不存在');
return res.redirect('/login');
}
if (user.password != password) {
req.flash('error', '用户口令错误');
return res.redirect('/login');
}
req.session.user = user;
req.flash('success', '登入成功');
res.redirect('/');
});
});
app.get('/logout', function(req, res) {
req.session.user = null;
req.flash('success', '登出成功');
res.redirect('/');
});
- 页面权限控制
var crypto = require('crypto');
var User = require('../models/user.js');
module.exports = function(app) {
app.get('/', function(req, res) {
res.render('index', {
title: '首页'
});
});
app.get('/reg', checkNotLogin);
app.get('/reg', function(req, res) {
res.render('reg', {
title: '用户注册',
});
});
app.post('/reg', checkNotLogin);
app.post('/reg', function(req, res) {
//检验用户两次输入的口令是否一致
if (req.body['password-repeat'] != req.body['password']) {
req.flash('error', '两次输入的口令不一致');
return res.redirect('/reg');
}
//生成口令的散列值
var md5 = crypto.createHash('md5');
var password = md5.update(req.body.password).digest('base64');
var newUser = new User({
name: req.body.username,
password: password,
});
//检查用户名是否已经存在
User.get(newUser.name, function(err, user) {
if (user)
err = 'Username already exists.';
if (err) {
req.flash('error', err);
return res.redirect('/reg');
}
//如果不存在则新增用户
newUser.save(function(err) {
if (err) {
req.flash('error', err);
return res.redirect('/reg');
}
req.session.user = newUser;
req.flash('success', '注册成功');
res.redirect('/');
});
});
});
app.get('/login', checkNotLogin);
app.get('/login', function(req, res) {
res.render('login', {
title: '用户登入',
});
});
app.post('/login', checkNotLogin);
app.post('/login', function(req, res) {
//生成口令的散列值
var md5 = crypto.createHash('md5');
var password = md5.update(req.body.password).digest('base64');
User.get(req.body.username, function(err, user) {
if (!user) {
req.flash('error', '用户不存在');
return res.redirect('/login');
}
if (user.password != password) {
req.flash('error', '用户口令错误');
return res.redirect('/login');
}
req.session.user = user;
req.flash('success', '登入成功');
res.redirect('/');
});
});
app.get('/logout', function(req, res) {
req.session.user = null;
req.flash('success', '登出成功');
res.redirect('/');
});
};
function checkLogin(req, res, next) {
if (!req.session.user) {
req.flash('error', '未登入');
return res.redirect('/login');
}
next();
}
function checkNotLogin(req, res, next) {
if (req.session.user) {
req.flash('error', '已登入');
return res.redirect('/');
}
next();
}
- 发表微博
- 微博模型
// models/post.js
var mongodb = require('./db');
function Post(username, post, time) {
this.user = username;
this.post = post;
if (time) {
this.time = time;
} else {
this.time = new Date();
}
};
module.exports = Post;
Post.prototype.save = function save(callback) {
// 存入 Mongodb 的文档
var post = {
user: this.user,
post: this.post,
time: this.time,
};
mongodb.open(function(err, db) {
if (err) {
return callback(err);
}
// 读取 posts 集合
db.collection('posts', function(err, collection) {
if (err) {
mongodb.close();
return callback(err);
}
// 为 user 属性添加索引
collection.ensureIndex('user');
// 写入 post 文档
collection.insert(post, {safe: true}, function(err, post) {
mongodb.close();
callback(err, post);
});
});
});
};
Post.get = function get(username, callback) {
mongodb.open(function(err, db) {
if (err) {
return callback(err);
}
// 读取 posts 集合
db.collection('posts', function(err, collection) {
if (err) {
mongodb.close();
return callback(err);
}
// 查找 user 属性为 username 的文档,如果 username 是 null 则匹配全部
var query = {};
if (username) {
query.user = username;
}
collection.find(query).sort({time: -1}).toArray(function(err, docs) {
mongodb.close();
if (err) {
callback(err, null);
}
// 封装 posts 为 Post 对象
var posts = [];
docs.forEach(function(doc, index) {
var post = new Post(doc.user, doc.post, doc.time);
posts.push(post);
});
callback(null, posts);
});
});
});
};
- 发表微博
app.post('/post', checkLogin);
app.post('/post', function(req, res) {
var currentUser = req.session.user;
var post = new Post(currentUser.name, req.body.post);
post.save(function(err) {
if (err) {
req.flash('error', err);
return res.redirect('/');
}
req.flash('success', '发表成功');
res.redirect('/u/' + currentUser.name);
});
});
- 用户页面
app.get('/u/:user', function(req, res) {
User.get(req.params.user, function(err, user) {
if (!user) {
req.flash('error', '用户不存在');
return res.redirect('/');
}
Post.get(user.name, function(err, posts) {
if (err) {
req.flash('error', err);
return res.redirect('/');
}
res.render('user', {
title: user.name,
posts: posts,
});
});
});
});