目录[-]
学习环境
node.js: 0.10.7+
快速开始
安装Express
?1 | npm |
我们需要用全局模式安装 express,因为只有这样我们才能在命令行中使用它。目前 express 最新版本为 express 3.2.4。
新建一个工程
今后的学习把 D:\blog 文件夹作为我们的工程目录。windows 下打开 cmd 切换到 D 盘,输入 express
(注意 express 3.* 中安装 ejs 不再是 -t 而是 -e,可以输入
-e ejs blogexpress -h
查看),然后输入 cd blog&npm install
安装所需模块,如下图所示:
安装完成后输入 node
,此时命令行中会显示 Express server listening on port 3000,在浏览器里输入
applocalhost:3000
,如下所示:
我们用 express 初始化了一个工程,并指定使用 ejs 模板引擎,下一节我们讲解工程的内部结构。
工程结构
我们回头看看生成的工程目录里面有什么,打开 D:\blog,里面如图所示:
app.js:启动文件。
package.json:存储着工程的信息及所需的依赖模块,当在 dependencies 中添加依赖时,运行npm install,会检查当前目录下的 package.json,并自动安装所有指定的依赖模块。
node_modules:存放 package.json 中安装的模块,当你在 package.json 添加依赖的模块并安装后,默认存放在这个文件夹下
public:存放 image、css、js 等文件
routes:存放路由文件
views:存放模版文件
打开 app.js,让我们看看里面究竟有什么东西:
?12345678910111213141516171819202122232425262728293031323334 | /** * Module dependencies. */ var 'express' ) , routes = require( './routes' ) , user = require( './routes/user' ) , http = require( 'http' ) , path = require( 'path' ); var // app.set( 'port' , process.env.PORT || 3000 ); app.set( 'views' , __dirname + '/views' ); app.set( 'view engine' , 'ejs' ); app.use(express.favicon()); app.use(express.logger( 'dev' )); app.use(express.bodyParser()); app.use(express.methodOverride()); app.use(app.router); app.use(express. static (path.join(__dirname, 'public' ))); // if ( 'development' == app.get( 'env' )) { app.use(express.errorHandler()); } app.get( '/' , routes.index); app.get( '/users' , user.list); http.createServer(app).listen(app.get( 'port' ), function(){ console.log( 'Express server listening on port ' + app.get( 'port' )); }); |
在 node.js 中模块分为核心模块和文件模块两种,核心模块是通过 require('xxxx') 导入的,文件模块是以 require('/xxxx') 或 require('./xxxx')、require('../xxxx') 形式导入的;核心模块是用c/c++编译的二进制模块,而文件模块是后缀为.js、.json、.node 的文件,在 node.js 中一个文件/文件夹也可以称之为一个模块。更多关于模块及模块加载顺序的信息请查阅官网:http://nodejs.org/api/all.html#all_modules
这里导入了 express、http、path 核心模块,routes 文件夹下的 index.js 和 user.js 文件模块。
因为 express 框架是依赖 connect 框架(Node的一个中间件框架)创建而成的,可查阅 connect 文档:http://www.senchalabs.org/connect/和 express 官方文档:http://expressjs.com/api.html了解更多内容。
app.set(name, value):设置 name
的值为 value
app.set('port', process.env.PORT || 3000):设置端口为 process.env.PORT
或 3000
app.set('views', __dirname + '/views'):设置 views 文件夹为视图文件的目录,存放模板文件,__dirname 为全局变量,存储着当前正在执行脚本所在的目录名。
app.set('view engine', 'ejs'):设置视图模版引擎为 ejs
app.use([path], function):使用中间件 function
,可选参数path
默认为”/”
app.use(express.favicon()):connect 内建的中间件,使用默认的 favicon 图标,如果想使用自己的图标,需改为 app.use(express.favicon(__dirname
这里我们把自定义的 favicon.ico 放到了 public/images 文件夹下。
+ '/public/images/favicon.ico'));
app.use(express.logger('dev')):connect 内建的中间件,在开发环境下使用,在终端显示简单的不同颜色的日志,比如在启动 app.js 后访问 localhost:3000,终端会输出:
123 | Express 3000 GET 200 21ms - 206b GET 304 4ms |
数字200显示为绿色,304显示为蓝色。假如你去掉这一行代码,不管你怎么刷新网页,终端都只有一行 Express server listening on port 3000。
app.use(express.bodyParser()):connect 内建的中间件,用来解析请求体,支持 application/json,
application/x-www-form-urlencoded, 和 multipart/form-data。
app.use(express.methodOverride()):connect 内建的中间件,可以协助处理 POST 请求,伪装 PUT、DELETE 和其他 HTTP 方法。
app.use(app.router):设置应用的路由(可选),详细请参考:http://*.com/questions/12695591/node-js-express-js-how-does-app-router-work
app.use(express.static(path.join(__dirname, 'public'))):connect 内建的中间件,设置根目录下的 public 文件夹为静态文件服务器,存放 image、css、js 文件于此。
if ('development' == app.get('env')) {app.use(express.errorHandler());}:开发环境下的错误处理,输出错误信息。
app.get('/', routes.index):路由控制器,如果用户访问” / “路径,则由 routes.index 来控制,routes/index.js 内容如下:
?123 | exports.index res.render( 'index' , { title: 'Express' }); }; |
通过 exports.index 导出 index 函数接口,app.get('/',
相当于:
routes.index)
123 | app.get( '/' , function(req, res){ res.render( 'index' , { title: 'Express' }); };) |
res.render('index', { title: 'Express' }):调用 ejs 模板引擎解析 views/index.ejs(我们之前通过 app.set('views',
设置了模版文件默认存储在 views 下),并传入一个对象作为参数,这个对象只有一个属性 title: 'Express',即用字符串 Express 替换 views/index.ejs 中所有 title 变量,后面我们将会了解更多关于模板引的内容。
__dirname + '/views')
123 | http.createServer(app).listen(app.get( 'port' ), function(){ console.log( 'Express server listening on port ' + app.get( 'port' )); }); |
这段代码的意思是创建服务器并监听3000端口,成功后在命令行中显示 Express server listening on port 3000,然后我们就可以通过在浏览器输入 localhost:3000 来访问了。
这一小节我们学习了如何创建一个工程并启动它,了解了工程的大体结构,下一节我们将学习 Express 的基本使用及路由控制。
路由控制
工作原理
上面提到过 app.js 中 app.get('/',
可以用以下代码取代:
routes.index)
123 | app.get( '/' , function(req, res){ res.render( 'index' , { title: 'Express' }); };) |
这段代码的意思是当访问主页时,调用 ejs 模板引擎,传入 title 变量的值为字符串 Express,来渲染 index.ejs 模版文件,生成静态页面并显示在浏览器里。
我们来作一些修改,以上代码实现了路由的功能,我们当然可以不要 routes/index.js 文件,把实现路由功能的代码都放在 app.js 里,但随着时间的推移 app.js 会变得难以维护,这也违背了代码模块化的思想,所以我们把实现路由功能的代码都放在 routes/index.js 里。官方给出的写法是在 app.js 中实现了简单的路由分配,然后再去 index.js 中找到对应的路由函数,最终实现路由功能。我们不妨把路由控制器和实现路由功能的函数都放到 index.js 里,app.js 中只有一个总的路由接口。
打开 app.js,删除 ,
(我们这里用不到 routes/user.js,同时删除这个文件)和
user = require('./routes/user')
12 | app.get( '/' , routes.index); app.get( '/users' , user.list); |
在 app.js 最后添加:
?1 | routes(app); |
修改 index.js 如下:
?12345 | module.exports app.get( '/' ,function(req,res){ res.render( 'index' , { title: 'Express' }); }); }; |
现在,再运行你的 app,你会发现主页毫无二致。这两种写法的区别就好比:
你的朋友结婚了,你收到请帖要赴宴。到了酒店门口被总管给拦住了。官方的写法是总管看了看请帖然后给你指了朋友团的地方,然后你过去坐下。咱的写法是总管看了看请帖简单确认了下你是被邀请的人,然后你进去自己找到朋友团的地方坐下。
路由规则
express 封装了多种 http 请求方式,但我们这里主要只使用 get 和 post 两种。
get 和 post 的第一个参数都为请求的路径,第二个参数为处理请求的回调函数,它有两个参数分别是 req 和 res,表示请求信息和响应信息 。路径请求及对应的获取路径有以下几种形式:
req.query
?12345678910111213 | // req.query.q // // req.query.order // req.query.shoe.color // req.query.shoe.type // |
req.body
?12345678910 | // req.body.user.name // req.body.user.email // // req.body.name // |
req.params
?123456789101112131415161718192021 | // req.params.name // // req.params[ 0 ] // **req.param(name)** // req.param( 'name' ) // // req.param( 'name' ) // // req.param( 'name' ) // |
不难看出:
req.query 处理 get 请求
req.params 处理 /:xxx 形式的 get 请求
req.body 处理 post 请求
req.param(name) 可以处理 get 和 post 请求,但查找优先级由高到低为 req.params→req.body→req.query
路径规则还支持正则表达式,更多请查阅:http://expressjs.com/api.html
添加路由规则
当我们访问 localhost:3000 时,会显示:
当我们访问 localhost:3000/nswbmw 这种不存在的页面时就会显示:
这是因为不存在 /nswbmw 的路由规则,而且它也不是一个 public 目录下的文件,所以 Express 返回了 404 Not Found 的错误。
下面我们来添加这条路由规则,使得当访问 localhost:3000/nswbmw 时,页面显示 hello,world!
注意:以下修改仅用于测试,看到效果后再把代码还原回来。
修改 index.js,在 app.get('/')
函数后添加一条路由规则:
123 | app.get( '/nswbmw' ,function(req,res){ res.send( 'hello.world!' ); }); |
访问 localhost:3000/nswbmw 页面显示如下:
很简单吧?这一节我们学习了基本的路由规则及如何添加一条路由规则,下一节我们将学习模板引擎的知识。
模版引擎
什么是模板引擎
模板引擎(Template Engine)是一个将页面模板和要显示的数据结合起来生成 HTML 页面的工具。
如果说上面讲到的 express 中的路由控制方法相当于 MVC 中的控制器的话,那模板引擎就相当于 MVC 中的视图。
模板引擎的功能是将页面模板和要显示的数据结合起来生成 HTML 页面。它既可以运
行在服务器端又可以运行在客户端,大多数时候它都在服务器端直接被解析为 HTML,解析
完成后再传输给客户端,因此客户端甚至无法判断页面是否是模板引擎生成的。有时候模板
引擎也可以运行在客户端,即浏览器中,典型的代表就是 XSLT,它以 XML 为输入,在客
户端生成 HTML 页面。但是由于浏览器兼容性问题,XSLT 并不是很流行。目前的主流还是
由服务器运行模板引擎。在 MVC 架构中,模板引擎包含在服务器端。控制器得到用户请求后,从模型获取数据,
调用模板引擎。模板引擎以数据和页面模板为输入,生成 HTML 页面,然后返回给控制器,
由控制器交回客户端。——《Node.js开发指南》
什么是 ejs ?
ejs 是模板引擎的一种,也是我们这个教程中使用的模板引擎,因为它十分简单,而且与 Express 集成良好。
使用模板引擎
前面我们通过以下两行代码设置了模板引擎和页面模板的存储位置:
?12 | app.set( 'views' , __dirname + '/views' ); app.set( 'view engine' , 'ejs' ); |
在 routes/index.js 中通过调用 res.render 渲染模版,并将其产生的页面直接返回给客户端。它接受两个参数,第一个是模板的名称,即 views 目录下的模板文件名,扩展名 .ejs 可选;第二个参数是传递给模板的数据,用于模板翻译。
index.ejs 内容如下:
1234567891011 | <!DOCTYPE <html> <head> <title><%= title %></title> <link rel= 'stylesheet' href= '/stylesheets/style.css' /> </head> <body> <h1><%= title %></h1> <p>Welcome to <%= title %></p> </body> </html> |
当我们 res.render('index',
时,模板引擎会把 <%= title %> 替换成 Express,然后把替换后的页面现实给用户。
{ title: 'Express' });
渲染后生成的页面代码为:
?1234567891011 | <!DOCTYPE <html> <head> <title>Express</title> <link rel= 'stylesheet' href= '/stylesheets/style.css' /> </head> <body> <h1>Express</h1> <p>Welcome to Express</p> </body> </html> |
注意:我们设置了静态文静目录为 public(app.use(express.static(path.join(__dirname,
),所以上面代码中的
'public')))href='/stylesheets/style.css'
就相当于 href='public/stylesheets/style.css'
。
ejs 的标签系统非常简单,它只有以下3种标签。
?123 | <% <%= <%- |
注意: <%=
和
code %><%- code %>
的区别,当变量 code 为字符串时,两者没有区别;当 code 为比如<h1>hello</h1>
时,<%= code %>
会原样输出 <h1>hello</h1>
,而 <%- code %>
则会输出H1大的 hello。
EJS 的官方示例:
The Data
?12 | { 'Cleaning Supplies' , supplies: [ 'mop' , 'broom' , 'duster' ] } |
The Template
?12345 | <ul> <% for (var i= 0 ; i<supplies.length; i++) {%> <li><%= supplies[i] %></li> <% </ul> |
The Result
- mop
- broom
- duster
我们可以用上述三种方式实现页面模板系统能实现的任何内容。
页面布局
Express 3.* 中我们不再使用 layout.ejs 进行页面布局,转而使用 include 来替代。include 的简单使用如下:
a.ejs
?123 | <%- hello,world! <%- |
b.ejs
?1 | this is b |
c.ejs
?1 | this is c |
最终 a.ejs 会显示:
?123 | this is b hello,world! this is c |
这一节我们学习了模版引擎的相关知识,下一节我们正式开始学习如何从头开始搭建一个多人博客。
搭建多人博客
功能分析
作为入门教程,我们要搭建的博客具有简单的允许多人注册、登录、发表文章、登出的功能。
设计目标
未登录:主页左侧导航显示 home、login、register,右侧显示已发表的文章、发表日期及作者。
登陆后:主页左侧导航显示 home、post、logout,右侧显示已发表的文章、发表日期及作者。
用户登录、注册、发表成功以及登出后都返回到主页。
用户登陆前:
主页:
登录页:
注册页:
用户登录后:
主页:
发表页:
注意:没有登出页,当点击 LOGOUT 后,退出登陆并返回到主页。
路由规划
我们已经把设计的构想图贴出来了,接下来的任务就是写路由规划了。路由规划,或者说控制器规划是整个网站的骨架部分,因为它处于整个架构的枢纽位置,相当于各个接口之间的粘合剂,所以应该优先考虑。
根据构思的设计图,我们作以下路由规划:
12345 | / /login /reg /post /logout |
login 和 reg 页只能是未登录的用户访问,而 post 和 logout 页只能是已登录的用户访问。首页则针对已登录和未登录的用户显示不同的内容。
修改 index.js 如下:
?12345678910111213141516171819202122 | module.exports app.get( '/' ,function(req,res){ res.render( 'index' , { title: '主页' }); }); app.get( '/reg' ,function(req,res){ res.render( 'reg' , { title: '注册' }); }); app.post( '/reg' ,function(req,res){ }); app.get( '/login' ,function(req,res){ res.render( 'login' , { title: '登录' }); }); app.post( '/login' ,function(req,res){ }); app.get( '/post' ,function(req,res){ res.render( 'post' , { title: '发表' }); }); app.post( '/post' ,function(req,res){ }); app.get( '/logout' ,function(req,res){ }); }; |
如何针对已登录和未登录的用户显示不同的内容呢?或者说如何判断用户是否已经登陆了呢?进一步说如何记住用户的登录状态呢?我们通过引入会话机制,来记录用户登录状态,还要访问数据库来保存和读取用户信息。下一节我们将学习如何使用数据库。
使用数据库
MongoDB简介
MongoDB 是一个对象数据库,它没有表、行等概念,也没有固定的模式和结构,所有的数据以文档的形式存储。所谓文档就是一个关联数组式的对象,它的内部由属性组成,一个属性对应的值可能是一个数、字符串、日期、数组,甚至是一个嵌套的文档。
——《Node.js开发指南》
下面是一个 MongoDB 文档的示例:
?123456789101112 | { "_id" : ObjectId( "4f7fe8432b4a1077a7c551e8" ), "name" : "nswbmw" , "age" : 22 , "email" : [ "xxx@126.com" , "xxx@gmail.com" ], "family" : { "mother" : { ... }, "father" : { ... }, "sister : { ... }, "address" : "earth" } } |
更多有关 MongoDB 的知识请参考 《mongodb权威指南》或查阅:http://www.mongodb.org/
安装MongoDB
安装 mongodb 很简单,去官网(http://www.mongodb.org/downloads)下载最新版的 mongodb,解压到D盘并把文件夹重命名为 mongodb,并在 mongodb 文件夹下新建 blog 文件夹作为我们的存储目录。打开 cmd,切换到 d:\mongodb\bin 目录下,在命令行中输入 mongod -dbpath “d:\mongodb\blog” 设置 blog 文件夹作为我们工程的存储目录,这样数据库就成功启动了。为了方便以后使用数据库,我们在桌面上新建 “启动mongodb.bat” ,并写入 d:\mongodb\bin\mongod.exe
,这样我们以后只需运行桌面上的 “启动mongodb.bat” 就可启动数据库了。
-dbpath d:\mongodb\blog
连接MongoDB
数据库虽然安装并启动成功了,但我们需要连接数据库后才能使用数据库。怎么才能在 Node.js 中使用 MongoDb 呢?我们需要使用 node-mongodb-native 模块,打开 package.json,在 dependencies 中添加一行代码:
?12345678910111213 | { "name" : "blog" , "version" : "0.0.1" , "private" : true , "scripts" : { "start" : "node app.js" }, "dependencies" : { "express" : "3.2.4" , "ejs" : "*" , "mongodb" : "*" } } |
然后运行 npm install 更新依赖的模块,稍等片刻后模块就下载并安装完成。
接下来在工程的目录中创建 settings.js 文件,这个文件用于保存数据库的连接信息。我们将数据库命名为 blog,数据库服务器在本地,因此 settings.js 文件的内容如下:
?12345 | module.exports cookieSecret: 'myblog' , db: 'blog' , host: 'localhost' }; |
其中 db 是数据库的名称,host 是数据库的地址。cookieSecret 用于 Cookie 加密与数据库无关,我们留作后用。
接下来在根目录下新建 models 文件夹,并在 models 文件夹下新建 db.js ,添加如下代码:
?12345 | var '../settings' ), Db = require( 'mongodb' ).Db, Connection = require( 'mongodb' ).Connection, Server = require( 'mongodb' ).Server; module.exports new Db(settings.db, new Server(settings.host, Connection.DEFAULT_PORT, {})); |
我们先不用弄清楚代码为什么这么写,我们现在只需知道以上代码通过 module.exports 输出了创建的数据库连接,在后面的小节中我们会用到这个模块。
会话支持
会话是一种持久的网络协议,用于完成服务器和客户端之间的一些交互行为。会话是一个比连接粒度更大的概念, 一次会话可能包含多次连接,每次连接都被认为是会话的一次操作。在网络应用开发中,有必要实现会话以帮助用户交互。例如网上购物的场景,用户浏览了多个页面,购买了一些物品,这些请求在多次连接中完成。许多应用层网络协议都是由会话支持的,如 FTP、Telnet 等,而 HTTP 协议是无状态的,本身不支持会话,因此在没有额外手段的帮助下,前面场景中服务器不知道用户购买了什么。
为了在无状态的 HTTP 协议之上实现会话,Cookie 诞生了。Cookie 是一些存储在客户端的信息,每次连接的时候由浏览器向服务器递交,服务器也向浏览器发起存储 Cookie 的请求,依靠这样的手段服务器可以识别客户端。我们通常意义上的 HTTP 会话功能就是这样实现的。具体来说,浏览器首次向服务器发起请求时,服务器生成一个唯一标识符并发送给客户端浏览器,浏览器将这个唯一标识符存储在 Cookie 中,以后每次再发起请求,客户端
浏览器都会向服务器传送这个唯一标识符,服务器通过这个唯一标识符来识别用户。 对于开发者来说,我们无须关心浏览器端的存储,需要关注的仅仅是如何通过这个唯一标识符来识别用户。很多服务端脚本语言都有会话功能,如 PHP,把每个唯一标识符存储到文件中。Express 也提供了会话中间件,默认情况下是把用户信息存储在内存中,但我们既然已经有了 MongoDB,不妨把会话信息存储在数据库中,便于持久维护。
——《Node.js开发指南》
为了使用这一功能,我们首先要获取一个叫做 connect-mongo 的模块,在 package.json 中添加一行代码:
?1234567891011121314 | { "name" : "blog" , "version" : "0.0.1" , "private" : true , "scripts" : { "start" : "node app.js" }, "dependencies" : { "express" : "3.2.4" , "ejs" : "*" , "mongodb" : "*" , "connect-mongo" : "*" } } |
运行 npm install 安装模块。然后打开 app.js,在 ,
后添加以下代码:
path = require('path')
12 | , 'connect-mongo' )(express) , './settings' ); |
在 app.use(express.methodOverride());
后添加:
123456789 | app.use(express.cookieParser()); app.use(express.session({ secret: settings.cookieSecret, key: settings.db, cookie: {maxAge: 1000 * 60 * 60 * 24 * 30 }, //30 days store: new MongoStore({ db: settings.db }) })); |
其中 express.cookieParser() 是 Cookie 解析的中间件。express.session() 则提供会话支持,secret 用来防止篡改 cookie,key 的值为 cookie 的名字,通过设置 cookie 的 maxAge 值设定cookie的生存期,这里我们设置 cookie 的生存期为30天,设置它的 store 参数为 MongoStore 实例,把会话信息存储到数据库中,以避免丢失。在后面的小节中,我们可以通过 req.session 获取当前用户的会话对象,以维护用户相关的信息。