node.js 搭建blog

搭建开发环境并模拟交互数据

一、实验说明

下述介绍为实验楼默认环境,如果您使用的是定制环境,请修改成您自己的环境介绍。

三、功能模块分析与设计
四、搭建开发环境

LouBlog 使用 nodeJS 搭建后台,使用最受欢迎的 web 框架 Express 快速搭建。
有过 nodeJS 基础的同学应该对此有一定的了解,简单说一下,nodeJS 是基于 Chrome JavaScript 运行时建立的平台,用于方便地搭建响应速度快、易于扩展的网络应用。nodeJS 使用事件驱动,非阻塞 I/O 模型而得以轻量和高效。
再来介绍一下深受 nodeJS 开发者欢迎的 web 开发框架 Express 。Express 是一个基于 nodeJS 平台的极简、灵活的 web 应用开发框架,这好比如是 Flask 和 Python 的搭配一样。Express 拥有丰富的 HTTP 快捷方法和任意排列组合的 Connect 中间件,方便快速、简单地创建健壮、友好的 API

4.1 安装node.js
1.安装node.js参考菜鸟教程

http://www.runoob.com/nodejs/nodejs-install-setup.html

2.设置node.js环境变量,两个

2.1

2.2 path添加node的安装位置

4.2 使用Express生成项目框架

4.2.1 安装 Express
在 Express v3.x 之前,还内置许多中间件,但在 v4.x 后,除了 static 都被分离为单独的模块,这也是许多初学者,面对的最大的问题,因为现在许多网上的文章还停留在 v3.x。
详情可以访问 Express中文网。
先通过 npm 全局安装 Express:
sudo npm install -g express-generator
4.2.2 使用 Express
在安装好 Express 开发框架及其命令行工具后,就可以快速生成我们需要的项目框架了。执行下面这条命令:
express -e LouBlog
成功后便会生成所需的框架。
文件结构如下

|-- LouBlog
    |-- public
        |-- javascripts
        |-- images
        `-- stylesheets
            `-- style.css
    |-- routes
        |-- index.js
        `-- users.js
    |-- views
        |-- index.ejs
        `-- error.ejs
    |-- bin
        `-- www
    |-- app.js
    `-- package.json

简单了解一下 Express 都为我们准备好了什么:
LouBlog: 自然就是我们项目名,如果没有此文件夹,在创建项目框架时加上名称,就像刚刚执行完的那条命令,Express 就会自动帮我们创建此文件夹;若你在已是项目名的文件夹中生成框架,此项可省去。
public: 是我们项目的静态资源,存放 imgs、js、css 等文件;
routes: 可以说是整个项目的控制部分,存放路由文件;
views: 存放项目的视图文件;
bin: 存放可执行文件;
app.js: 项目的启动文件;
package.json: 文件中有项目的基本信息,包括项目名、版本号、开放权限、启动命令等;以及项目的模块依赖信息,当运行 npm install 时, npm 就会此文件,并根据 dependencies 对象中的属性安装模块。
简单了解了 Express 为我们生成的框架,有没有感受得到 Express 的强大呢,帮我做好了很多建站的基本工作,大大提高了效率。
注意:希望大家能多多学习生成的文件内容,尤其是 app.js 、routes/index.js 、bin/www 这几个文件,
接下来我们做一点点修改,方便我们启动项目服务,进入项目根目录,并安装项目的依赖模块:

cd LouBlog
npm install

设置路由
修改 app.js 文件, 在文件最后的
module.exports = app; 之前添加一行代码 app.listen(3000);
最后一部执行启动命令:
node app.js
启动项目,通过浏览器访问 localhost:3000,即可看到结果

4.3 熟悉Express框架
4.3.1 工作原理
接下来学习 Express 框架中非常重要的路由控制。
routers/index.js 中有以下代码:

router.get('/', functoin(req, res) {
    res.render('index', { title: 'Express' });
});

代码意思是当访问主页时,调用 ejs 模板(这里提到的 ejs 模板将会在下一节中详细讲解)来渲染 views/index.ejs 模板文件。 其中 get 指 http 的 get 请求方式,Express 封装了许多 http 请求方式,我们主要使用 get() 和 post() 两种;
参数一:'/' 则代表了其路由规则,这里指向项目根目录,同时路由规则还支持正则表达式,这给我们设计路由带来很多的方便;
参数二:为处理请求的回调函数,函数中又有两个参数 req 和 res,代表请求信息和响应信息。
路径请求及对应的获取路径有以下几种形式:
req.query: 处理get请求,获取get请求参数。
req.body: 处理post请求,获取post请求体。
req.params: 处理/:XXX形式的get或post请求,获取请求参数
req.param(): 处理get和post请求,但查找优先级由高到低为req.params -> req.body -> req.query。
res.render() 则将所有数据以 json 格式传递给模板引擎。
4.3.2 路由规则实践
现在我们直接访问 localhost:3000/login 会显示:

这是因为我们还没有建立 /login 这一路由规则
在 routers/index.js 文件中添加代码

router.get('/login', function(req, res, next) {
    res.render('login', {title: 'login'});
});

Ctrl + c 停止服务,node app.js 再次启动服务,访问 localhost:3000/login 后显示:

出现这个错误是因为 view 中并没用 login 对应的文件,添加 login.ejs 文件,文件中写入 <%= title%>,直接刷新浏览器,这时便可以看到正确的显示:

还记得之前简单讲解过 Express 生成的模板框架吗,routes 中存放路由文件,views 中存放视图文件,这就相当于 MVC 模式中的 C 和 V,而 index.ejs 文件中的 <%= title%> 是 ejs 模板引擎的语句,意思是将后台传递来的 title 数据在页面中显示出来。
现在你应该大致了解了 Express 的路由工作原理,但在刚在的操作中,我们发现每次修改后台代码时,想要浏览修改结果,就需要先重启服务。这无疑增加了开发的负担。
使用 supervisor 模块可以很好的解决这个问题,每当我们保存文件后,此模块便会自动重启服务,提高了开发效率。
首先要安装此模块:

sudo npm install -g supervisor

配置启动命令:

supervisor app.js

之后,我们的项目启动命名便从 node app.js 更改为

supervisor app.js

这样我们的开发环境已经初步配置完成,接下来我们根据 Express 的路由控制原则来设计我们的博客项目
五、搭建路由模块
依据我们博客指定好的功能,我们初步设计以下几个路由规则:

/login
/logout
/reg
/post
/search
/edit/:_id
/remove/:_id

以上几个路由规则分别对应“登录”、“退出登录”、“注册”、“发表文章”、“查询”、“编辑”、“删除”功能。
再分别建立好对应的视图文件:

views/login.ejs
views/register.ejs
views/index.ejs
views/post.ejs
views/search.ejs
views/edit.ejs

“删除”功能只是在请求完成后返回一个状态信息,因此不必要创建视图文件。
完成上面两步,就需要在浏览器中依次测试刚刚设计好的路由规则是否有效果,有没有报出错误,那这节实验的任务就完成了。

六、前端模板引擎

6.1 什么是模板引擎
模板引擎是一个将页面模板和数据数据结合起来生成 HTML 页面的工具。通过模板引擎,我们可以在 HTML 文件中直接使用后台传递过来的数据,而不必再使用通过解析 json 数据,在拼接成字符串的形式渲染数据,大大提高了开发效率。
6.2 什么是ejs

express -e   LouBlog

看过 Express API 的同学可能比较清楚,这里的 -e 正是指定 ejs 作为我们的模板引擎,而默认的模板引擎是 jade 。
那为何要选择 ejs ,不选 jade?很大的一个原因是因为 ejs 的语法更符合前端开发者的习惯,项目的目的是为了能让大家学会这一套开发流程,而不仅仅是一个模板引擎;反观 jade ,很多初学者都很难马上适应这中语法,更符合后台开发者的习惯,特别是对缩进的严格要求,使得大家感觉与 Python 很像。
但这并不是说 jade 比 ejs 有多差,相反,有人做过测试,jade 的性能反而好过 ejs。这里只是为了大家快速上手开发,所以选择了ejs。大家有时间也可以尝试使用 jade 开发本课程,亲身体验一下二者的差别吧。
6.3 使用ejs
在 Express 自动生成项目框架时,有这么两行代码

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

这里设置了模板文件的存储位置和使用的模板引擎。
在 routers/index.js 中通过 res.render() 渲染模板。它接收两个参数,第一个是模板名称,即 views 目录下的模板文件名;第二个参数是传递给模板的数据对象。
举个例子:当代码为 res.render('index', {title: 'Express'}); 时,模板引擎会把 <%= title %> 替换为 Express ,然后把替换后的页面展示给用户。
相对于 jade 来说,ejs 非常简单,只有三种标签:

<% code %>: javascript代码,这代表我们能使用 if-else 之类的逻辑判断语句
<%= code%>: 显示替换过HTML特殊字符的内容
<%- code %>: 显示原始HTML内容

6.4 页面布局
使用模板的一个原因,除了能方便处理数据,还有一点就是模板可以重用,我们可以将共用的部分分离为一个文件,并通过 include 引入。
6.4.1 分离共用模块
ejs 模板引擎的 include 语法简单粗暴,它不像许多模板能够引入成对的标签,而是硬生生将正常的 HTML 页面截断,直接将成对的标签分在了不同的文件当中。
我们先来搭建一个首页 index.ejs,课程最终完成的样式如下:

希望大家能自由发挥你的创造力,建造有自己特色的样式的博客,下面给出 index.ejs 的最简代码,通过不断优化,来学习 ejs 模板及其他知识点。

<!DOCTYPE html><html>
    <head>
        <meta charset="UTF-8">
        <title><%= title %></title>
        <link rel="stylesheet" href="/stylesheets/style.css">
        <body>
        <nav>
            <ul>
                <li><a href="#">register</a></li>
                <li><a href="#">login</a></li>
                <li><a href="#">post</a></li>
                <li><a href="#">logout</a></li>
            </ul>
        </nav>
        <div id="container">
            <%= title %>
        </div>
    </body></html>

从这个页面的构造,以及我们之前的功能设计可以看出,
部分的导航条是共用部分,也就是在其页面都有出现,这就可以用到 include 将其提取为一个文件。
使用 include 后的文件及内容:
views/header.ejs 文件:

<!DOCTYPE html><html>
    <head>
        <meta charset="UTF-8">
        <title><%= title %></title>
        <link rel="stylesheet" href="/stylesheets/style.css">
        <body>
        <nav>
            <ul>
                <li><a href="#">register</a></li>
                <li><a href="#">login</a></li>
                <li><a href="#">post</a></li>
                <li><a href="#">logout</a></li>
            </ul>
        </nav>
        <div id="container">

views/footer.ejs 文件:

        </div>
    </body></html>
修改过后的 views/index.ejs 文件为:
<%- include header %>
<%= title %>
<%- include footer %>

以上便是 include 的简单使用方法,ejs 模板引擎的基本功能也梳理完毕,在接下来的学习中,进一步体会 ejs 的魅力吧。

七、设计页面

接下来,我们主要完成前端的展示部分,使用 bootstrap 前端框架快速搭建样式优美的响应式页面。
7.1 使用 bootstrap 前端框架
7.1.1 引入 bootstrap
注意:bootstrap 所有的 JavaScript 插件都依赖 jquery, 并且通过查看 bootstrap 的官方文档就可以知道,bootstrap 所依赖的 jquery 版本最低为 v1.9.1,因此,还需要大家自己尝试引入 jquery 文件。
再补充一下 bootstrap 的几种安装方式:
1.手动下载并导入项目:从官网下载所需的 bootstrap 开发包,选择“用于生产环境的 bootstrap”,需要的文件为 fonts/*、js/bootstrap.min.js、css/bootstrap.min.css;
2.通过 npm 安装:执行 npm install bootstrap --save。此时 bootstrap 就相当于一个模块,通过 require('bootstrap') 导入后使用;
3.通过 bower 安装:执行 bower install bootstrap。bower 是客户端技术的软件包管理器,方便管理客户端依赖关系,在安装 bootstrap 之前应该先执行 sudo npm install -g bower 全局安装 bower;
4.使用CDN加速服务:bootstrap 中文网提供了免费的 CDN 加速服务,复制以下代码,即可使用:

<link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css">
<script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
<script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>

建议你使用 bower 安装 bootstrap。
7.1.2 使用 bootstrap
在 viwes/header.ejs 中添加上面的代码,并将
部分替换为 bootstrap 的导航条组件:

<nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container-fluid">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">LouBlog</a>
        </div>

        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
            <ul class="nav navbar-nav navbar-left">
                <li><a href="/post">post</a></li>
            </ul>
            <ul class="nav navbar-nav navbar-right">
                <li class="dropdown">
                    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
                            username
                        <span class="caret"></span>
                    </a>
                    <ul class="dropdown-menu">
                        <li><a href="#">about</a></li>
                        <li><a href="/logout">logout</a></li>
                    </ul>
                </li>
                <ul class="nav navbar-nav">
                    <li><a href="/login">login</a></li>
                    <li><a href="/reg">register</a></li>
                </ul>
            </ul>
            <form class="navbar-form navbar-right" role="search" action='/search' method="get">
                <div class="form-group">
                    <input type="text" class="form-control" placeholder="Search" name="title">
                </div>
                <button type="submit" class="btn btn-default">search</button>
            </form>
        </div>
    </div></nav>

这段代码中就是使用 bootstrap 的一个简单例子,也只用到了导航条组件,除此之外,我们开发博客系统还可能用到表单样式,栅格系统等,同学们可选择合适的组件,当然也可以使用其他前端框架,比如 AmazeUI 等,开发自己的博客系统。
另外需要提出的一点是,在上面的导航条中,同时出现了“登录”和“用户信息”等逻辑上不该同时出现的按钮。这就需要通过 session 机制来判断用户的登录状态,并返回应该显示在页面中的按钮选项。这将在下一节,添加 mongoDB 后做详细的讲解。
7.2 自己搭建页面样式
这一步主要是使用 bootstrap 框架,搭建每一个功能模块的页面样式,自己可以更具自己的喜好,自由发挥,在此列出每一个页面必要的元素,以方便后期讲解的统一。
views/login.ejs(登录页面):
构建一个 form 表单,需填写的内容包括 “用户名” 和 “密码”。
views/register.ejs(注册页面):
一个 form 表单,需填写的内容包括 “用户名”、“密码”、“确认密码”和“邮箱”。
views/index.ejs(主页):
主页展示文章信息,有 “标题”、“作者”、“创建日期”、“标签”和“文章内容”。
views/post.ejs(发表页面):
form 表单,需要填写的内容包括 “标题”,“标签”和“文章内容”,至于 “作者”和“创建时间” 将通过其他方式记录。
views/search.ejs(查询结果页面):
展示内容和主页一致。
views/edit.ejs(编辑页面):
form 表单,和发表页面一致,需要自动填写原有的数据,方便编辑。
7.3 模拟数据
接下来,我们通过修改 routes/index.js 向模板传递模拟数据,方便我们理解数据传递过程、编写首页样式。
跳转首页的路由规则为:

router.get('/', function(req, res, next) {
    res.render('index', {title: '主页'});
});

上一节提到,res.render() 会将数据传递给模板,其中参数一便是对应的模板,这里便是 index.ejs,而数据便是{...} 这一对象。所以,我们编写的模拟数据,就写在这个对象里,以 json 格式为标准。

router.get('/', function(req, res, next) {
    res.render('index', {
        title: '主页',
        arts: [{
            title: 'nodeJS入门',
            tags: 'nodeJS',
            author: '...',
            createTime: '',
            content: '...'
        },{
            title: 'nodeJS入门',
            tags: 'nodeJS',
            author: '...',
            createTime: '',
            content: '...'
        },{
            title: 'nodeJS入门',
            tags: 'nodeJS',
            author: '...',
            createTime: '',
            content: '...'
        }]
    });
});

这里简单添加了三条数据,模板 views/index.ejs 中添加:

<% arts.forEach(function(art) { %>
    <%= art.title %>
    <%= art.tags %>
    <%= art.author %>
    <%= art.createTime %>
<%= art.content %>
<% }) %>

这样,模板便成功的从后台得到了数据,并展示到页面中。页面样式很粗糙,这需要同学们自己完成。
八、本节总结
本节实验简单介绍了开发环境的搭建以及 Express 框架的使用,其中需要大家多花时间理解 Express 框架,从模板生成,到路由控制,希望大家能多看官网中的 API,熟悉 v3.x 和 v4.x 之间的区别,介绍了 ejs 模板的用法,熟悉 <% code %>、<%= code%>、<%- code%> 以及使用 <%- include views %> 进行模板重用是一重要知识点。
《mongoDB基础教程》

使用 mongoDB 数据库并整理功能模块
一、实验说明
下述介绍为实验楼默认环境,如果您使用的是定制环境,请修改成您自己的环境介绍。

  1. 环境登录
    无需密码自动登录,系统用户名shiyanlou
  2. 环境介绍
    本实验环境采用带桌面的Ubuntu Linux环境,实验中会用到桌面上的程序:
    1.LX终端(LXTerminal): Linux命令行终端,打开后会进入Bash环境,可以使用Linux命令
    2.Firefox:浏览器,可以用在需要前端界面的课程里,只需要打开环境里写的HTML/JS页面即可
    3.GVim:非常好用的编辑器,最简单的用法可以参考课程Vim编辑器
    二、课程介绍
    这一节,我们将学习在 Express 框架如何操作 mongoDB 中的数据,以及如何配合 session 完成一些基本的逻辑状态判断,完成本节,我们的 LouBlog 博客系统也将初具模型。
    Mongodb安装
    https://www.mongodb.com/download-center#community
    1.启动数据库
    mongod.exe --dbpath c:\data\db
    2.连接数据库
    mongod
    3.查询数据库和使用数据库和查找数据表
 show dbs
Use datas
Db.users.find()

三、使用 mongoDB 数据库
mongoDB 是一个基于分布式文件存储的非关系型数据库(NoSQL)的一种。它支持的数据结构非常松散,类似 json 的 bjson 格式。
mongoDB 没有关系型数据库中行和表的概念,但有类似文档 (document)和集合(collection)的概念。文档是 mongoDB 最基本的单位,集合是许多文档的总和,一个数据库可以有多个集合,一个集合可以有多个文档。
因为实验环境中默认安装有 mongoDB 因此我们跳过安装这一步,但先要通过以下指令开启 mongoDB 服务;
开启服务:

sudo service mongodb start

在根目录下创建数据库存储目录,并启动服务:

sudo mkdir /data/db
mongod

最后输入 mongo 即可访问数据库。
问题记录:
运行mongodb出现计算机丢失api-ms-win-crt-runtime-|1-1-0.dll
解决:
http://blog.csdn.net/mblhq/article/details/53896920
3.1 连接 mongoDB
mongoDB 的服务启动后,开始编写程序建立连接,这里我们使用 mongoDB 的模型工具 -- mongoose,可以方便我们减化代码,并且这还是为 nodeJS 设计的。
3.1.1 使用 mongoose 连接 mongoDB
运行以下命令安装 mongoose 包:

npm install mongoose --save

接下需要修改 app.js,添加下面的代码:

//先引入 mongoose 模块
var mongoose = require('mongoose');
//连接数据库
mongoose.connect('mongodb://localhost:27017/datas');
mongoose.connection.on('error', console.error.bind(console, '连接数据库失败'));

刷新页面时,若没有报出 “连接数据库失败” 则成功连接数据库。接下来,我们便要建立数据库模型,向数据库中存储数据。
3.2 设置 schema
schema 是 mongoose 中的模型对象,就类似关系型数据库中的表结构,为 key/value 的键值对形式。
我们的博客系统中主要存储用户和文章两类数据,也就是需要建立两个模型对象,暂且叫做 userSchema、articleSchema。
在根目录下新建一个文件夹 models,再新建一个文件 model.js。
3.2.1 设置 userSchema
在 models/model.js 中引入 mongoose 模块,并定义 schema 模型对象:

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

根据上一节实验中,我们统一定义的用户属性有 “用户名”、“密码”和“邮箱”,可以确定模型中的属性:username、password、email,此外考虑到以后操作数据的方便,在添加一个属性createTime。
就可以创建具体的模型对象:

var userSchema = new Schema({
    username: String,
    password: String,
    email: String,
    createTime: {
        type: Date,
        default: Date.now
    }
});

最后通过 mongoose 的 model() 方法,将 schema 发布为 model,model 具有抽象属性和行为的数据库操作对,这就使模型对象具有了数据库的 CRUD 操作方法。
exports.User = mongoose.model('User', userSchema);
3.2.2 设置 articleSchema
同样是在 models/model.js 下,我们创建文章模型对象 -- articleSchema:

var articleSchema = new Schema({
    title: String,
    author: String,
    tag: String,
    content: String,
    createTime: {
        type: Date,
        default: Date.now
    }
})

//发布为 model 
exports.Article = mongoose.model('Article', articleSchema);

注意:此处创建好的 model 对应数据库中的集合,可以通过 show colections 查看数据库中的所有集合。
创建好模型,就可以操作数据了。
3.3 操作数据
3.3.1 注册用户、发表文章
首先我们学习添加数据的操作 -- 注册用户和发表文章,在 routes/index.js 中,我们添加以下代码:

var mongoose = require('mongoose');//引入加密模块
var crypto = require('crypto');
//引入模型对象
var model = require('../models/model');
var User = model.User;

router.post('/reg', function(req, res, next) {
    //req.body 处理 post 请求
    var username = req.body.username,
        password = req.body.password,
        passwordRepeat = req.body.passwordRepeat;

    //检查两次输入的密码是否一致
    if(password != passwordRepeat) {
        console.log('两次输入的密码不一致!');
        return res.redirect('/reg');
    }

    //检查用户名是否已经存在
    //mongoose findOne() 方法
    User.findOne({username:username}, function(err, user) {
        if(err) {
            console.log(err);
            return res.redirect('/reg');
        }

        if(user) {
            console.log('用户名已经存在');
            return res.redirect('/reg');
        }

        //对密码进行md5加密
        var md5 = crypto.createHash('md5'),
            md5password = md5.update(password).digest('hex'); //16进制

        var newUser = new User({
            username: username,
            password: md5password,
            email: req.body.email
        });

        //mongoose save()方法
        newUser.save(function(err, doc) {
            if(err) {
                console.log(err);
                return res.redirect('/reg');
            }
            console.log('注册成功!');
            newUser.password = null;
            delete newUser.password;
            req.session.user = newUser;
            return res.redirect('/');
        });
    });
});

这样便实现了注册功能,需要注意 req.body 处理 post 请求的参数,建立 User 模型对象实体操作数据库,其实有 JavaScript 基础的同学应该很熟悉这样的写法。
再来是文章发表功能,这时就要用到 articleSchema 模型对象:

var Article = model.Article;

router.post('/post', function(req, res, next) {
    var data = new Article({
        title: req.body.title,
        //这里的 author 元素通过 session 获得,后面会详细讲解
        author: req.session.user.username,
        tag: req.body.tag,
        content: req.body.content
    });

    data.save(function(err, doc) {
        if(err) {
            req.flash('error', err);
            return res.redirect('/post');
        }
        console.log('文章发表成功!');
        return res.redirect('/');
    });
});

有了用户注册的基础,发表文章就简单许多了,但现在还没讲到 session 的运用,author 元素的值可以暂时通过 post 表单获得,稍后讲到 session 时,我们再改为上面的写法即可。
3.3.2 删除文章
接下来是文章的删除操作,依然是 routes/index.js,修改我们第一节实验写好的路由规则 /remove/:_id:
//mongoose 的 remove() 方法,通过传递检索参数,直接删除检索结果

router.get('/remove/:_id', function(req, res, next) {

    //req.params 处理 /:xxx 形式的 get 或 post 请求,获取请求参数
    Article.remove({_id: req.params._id}, function(err) {
        if(err) {
            console.log(err);
        } else {
            console.log('文章删除成功!');
        }
        return res.redirect('back');
    })
});

3.3.3 编辑文章
编辑文章,不仅需要获取文章信息,初始化表单内容,同时还需要有和发表文章一样的功能:

router.get('/edit/:_id', function(req, res, next) {
    Article.findOne({_id: req.params._id}, function(err, art) {
        if(err) {
            console.log(err);
            return res.redirect('back');
        }
        res.render('edit', {
            title: '编辑',
            // code ....
            art: art
        });
    });
});

router.post('/edit/:_id', function(req, res, next) {
    //mongoose 的 update() 方法用过检索参数并返回修改结果
    Article.update({_id: req.params._id},{
        title: req.body.title,
        tag: req.body.tag,
        content: req.body.content,
        createTime: Date.now()
    }, function(err, art) {
        if(err) {
            console.log(err);
            return res.redirect('back');
        }
        console.log('文章编辑成功!');
        return res.redirect('/u/' + req.session.user.username);
    });
});

3.3.4 查询文章
这里我们可以通过正则表达式,实现模糊查询,因为 Express 路由规则支持正则匹配查询。

router.get('/search', function(req, res, next) {
    //req.query 获取 get 请求的参数,并构造为正则对象
    var query = req.query.title,
        title = new RegExp(query, 'i');
    Article
    .find({title: title})
    .sort('-createTime')
    .exec(function(err, arts) {
        if(err) {
            console.log(err);
            return res.redirect('/');
        }
        res.render('search', { 
            title: '查询结果',
            arts: arts
        });
    });
});

完成以上四步,我们的博客系统就具有了基本的功能,但是还有几个小问题:
session 保存登录状态;
控制访问权限;
解决这两个问题,就需要靠接下来我们将要学习的 session 。
四、创建 session
session 是一种持久网络协议,在客户端与服务器之间起到交换数据包的作用。用户登录后的基本信息都会保存其中,Express 也提供了会话中间件,同时我们还可以将会话信息存储到数据库中,便于维护。为此,我们需要引入两个中间件 express-session 和 connect-mongo,安装方式如下:
4.1 引入中间件,创建 session

npm install express-session --save
npm install connect-mongo --save

接着我们要在 app.js 中添加以下代码:

var session = require('express-session');
var MongoStore = require('connect-mongo')(session);
//这里设置 session 参数,并确保以下代码在 `app.use('/', routes)` 前引入
app.use(session({
  key: 'session',
  secret: 'keboard cat',
  cookie: {maxAge: 1000 * 60 * 60 * 24},//1day
  store: new MongoStore({
    db: 'datas',
    mongooseConnection: mongoose.connection
  }),
  resave: false,
  saveUninitialized: true
}));

完成对 app.js 的以上修改之后,我们便能通过 req.session 获取当前用户的会话对象,获取用户的相关信息。
4.2 使用 session
举两个例子:
之前提到过的发表文章,其中的 author 属性需要通过获取 session 中保存的用户信息,此处我们就可以修改发表文章的方法以实现我们的需求:

router.post('/post', function(req, res, next) {
    var data = new Article({
        title: req.body.title,
        //这里的 author 元素通过 session 获得
        author: req.session.user.username,
        tag: req.body.tag,
        content: req.body.content
    });

    data.save(function(err, doc) {
        if(err) {
            console.log(err);
            return res.redirect('/post');
        }
        console.log('文章发表成功!');
        return res.redirect('/');
    });
});

session 另一个很大的作用就是判断用户登录状态并控制页面的元素显示:
我们就修改上一节给出的导航条代码,修改如下:

<nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container-fluid">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">实验楼</a>
        </div>

        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
            <% if(user) { %>
                <ul class="nav navbar-nav navbar-left">
                    <li><a href="/post">发表</a></li>
                </ul>
            <% } %>
            <ul class="nav navbar-nav navbar-right">
                <% if(user) { %>
                    <li class="dropdown">
                        <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
                                <%= user.username %>
                            <span class="caret"></span>
                        </a>
                        <ul class="dropdown-menu">
                            <li><a href="/u/<%= user.username %>">账户信息</a></li>
                            <li><a href="/logout">退出登录</a></li>
                        </ul>
                    </li>
                <% } else { %>
                    <ul class="nav navbar-nav">
                        <li><a href="/login">登录</a></li>
                        <li><a href="/reg">注册</a></li>
                    </ul>
                <% } %>
            </ul>
            <form class="navbar-form navbar-right" role="search" action='/search' method="get">
                <div class="form-group">
                    <input type="text" class="form-control" placeholder="Search" name="title">
                </div>
                <button type="submit" class="btn btn-default">搜索</button>
            </form>
        </div>
    </div></nav>
代码中我们可以看到有许多判断,其中的参数 user 便是通过 session 获取的用户信息,为此,我们还需要在 res.render()中传递 session:
res.render('index', { 
    title: '主页',
    user: req.session.user
    // code ....
});

除了以上两个例子,还有许多用到 session 的地方,大家在学习过程中可以自己好好感悟。
五、扩展功能
5.1 添加 flash 信息提示
上一节我们完成了博客系统的基本功能,但是有心的同学会发现,示例代码里很多的 console.log() 信息提示,这是打印一些返回信息以帮助我们调试代码,但不知有多少同学这样思考过 -- 每次刷新调试,都要看服务程序的信息提示好不方便,要是刷新就能在页面显示这些提示可好,此外,用户登录,发表文章,成功还好,要是哪出错了却没有任何提示多不方便。
恭喜你已经能够主动思考、学习,接着往下你应该就会自己查询解决办法了吧。
只要走到这一步,相信大家就知道 flash 模块能很好地帮助我们实现这一功能:
flash 存储于 session 中,但与之前我们存入的用户登录信息不同,flash 信息将会在下一次刷新后被清楚。
首先还是安装模块,并引入:

npm install connect-flash --save

// app.js 中
var flash = require('connect-flash');
app.use(flash());

这样,我们便成功引入了 flash 模块,接下来就可以将所有的 console.log() 使用 req.flash('type', content) 替换,其中参数一是一个字符串,代表了信息的类型,我们常用的就是 'success' 和 'error' 两种,参数二就是信息的具体内容。
这只是将信息保存到了 session 中,想要在页面中展示,我们就必须要从 session 中获取并传递给 ejs 模板,方法如下:

// code ...
    res.render('xxx', {
        // ...
        success: req.flash('success').toString();
        error: req.flash('error').toString();
        // ...
    });// code ...

模板获得信息后就可以通过简单的判断语句展示这一内容:

            <%if(success){ %>
            <%=success %>

           <% } %>
           <%if(error){ %>
            <%=error %>

           <% } %>

// error 同上,样式可以通过 bootstrap 调整。问题:这样许多页面都需要信息提示,我们应该在何处做以上调整?
这就是我们添加 flash 信息提示的大致过程。
5.2 添加分页功能
分页功能是很常见的一个功能,当展示的信息条目很多时能分批次显示,减小了浏览器压力,避免渲染速度过慢。
要实现分页的功能,主要需要考虑这几个元素:“当前页码”,“每页展示个数”,“条目总数”;此外分页主要有两种展示形式:“只有上/下一页”,“展示多个页码”。我们就尝试实现简单的 “只有上/下一页”。
修改 views/index.ejs:

var page = 1;
var pageSize = 5;
router.get('/', function(req, res, next) {
    page = req.query.page ? parseInt(req.query.page) : 1;
    Article
    .count(function(err, total) {
        Article
        .find()
        //skip 跳过指定的页数
        .skip((page - 1) * pageSize)
        //限制读取 pageSize 条数据
        .limit(pageSize)
        //以 createTime 倒序排序
        .sort('-createTime')
        //执行回调方法
        .exec(function(err, arts) {
            if(err) {
                req.flash('error',err);
                return res.redirect('/');
            }
            res.render('index', { 
                title: '主页',
                user: req.session.user,
                success: req.flash('success').toString(),
                error: req.flash('error').toString(),
                total: total,
                page: page,
                pageSize: pageSize,
                isFirstPage: (page - 1) == 0,
                isLastPage: ((page - 1) * pageSize + arts.length) == total,
                arts: arts
            });
        });
    });
});

将所有数据传递给模板后,剩余的事就简单了许多。其中的 skip() 和 limit() 是实现分页的关键部分,也有很多人说当数据量少的时候,skip() 的效率还可以,但当数据量很大的时候,效率就会很差。大家有兴趣可以查阅资料,尝试发现效率更高的方法。
Js使用express render的数据

六、本节总结
本节添加了mongoDB,并使用 mongoose 连接了数据库,创建 session 判断用户状态,完成了一些功能的扩展,使我们的 LouBlog 博客系统更充实。
再总结一下整个系列的实验,提出了许多课程的知识点,示例代码只是展示了核心部分,希望大家能尝试在这种点到即止的方式中去查找解决办法。
参考文档:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容