1.初始化

1
2
3
外链css等写绝对路径  (浏览器解析 相对于 浏览器地址)
模板位置写 绝对路径
调取公共部分 不许要写绝对路径 (模板引擎解析 写相对)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. 建立项目所需文件夹
public 静态资源
model 数据库操作
route 路由
views 模板
2. 初始化项目描述文件
npm init -y
3. 下载项目所需第三方模块
npm install express mongoose art-template express-art-template
4. 创建网站服务器
5. 构建模块化路由
6. 构建博客管理页面模板

npm install express mongoose art-template express-art-template

1.1 路由模块化

新建文件夹 route 里面新建 admin.js 与 home.js

image-20210210153704491

之后 设置请求方式 和做出响应 admin页面同理

1
2
3
4
5
6
7
8
9
10
// 引入 express
const express = require('express');

const home = express.Router();

home.get('/',(req,res)=>{
res.send('欢迎来到博客首页')
})
//将路由对象作为 模块成员进行导出
module.exports = home // 导出

app.js 里进行引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 引入 express
const express = require('express');
// 创建网络服务器
const app = express();
// 引入 home
const home = require('./route/home.js')
// 引入 admin
const admin = require('./route/admin.js')
// 为路由匹配路径
app.use('/home',home);
app.use('/admin',admin);


//监听端口
app.listen(80);
console.log('网站服务器启动成功');

1.2 构建页面模板文件

在app.js里指定 模板引擎的一些信息

1
2
3
4
5
6
7
8
9
10
// path 模块
const path = require('path');
// 告诉 express 框架 模板的所在的位置
app.set('views', path.join(__dirname,'views'))
// 告诉模板的默认后缀
app.set('view engine','art');
// 当渲染后缀为 art 的模板时 所使用的模板引擎是什么
app.engine('art',require('express-art-template'));
// 开放静态资源文件
app.use(express.static(path.join(__dirname,'public')))

之后在 相应的路由模块里 直接写文件名就可以了

1
2
3
4
5
6
7
8
9
10
11
// 引入 express
const express = require('express');

const admin = express.Router();

admin.get('/login',(req,res)=>{
res.render('admin/login') // 注意规定路径只写到了 views 所以要加 admin

})
//将路由对象作为 模块成员进行导出
module.exports = admin

1.3 解决模板 引用外链文件

在 模板引擎里 外链样式是相对与 请求地址的 倒数第二个开始的

例如

1
http://localhost/adminabc/login

会成这样的adminabc/css/bootstrap.min.css

但如果 路由里时

1
http://localhost/admin/login

会变成 admin/css/bootstrap.min.css

这时 css 地址就访问不到了

需要在外链写上 /

例如

1
href="/admin/lib/bootstrap/css/bootstrap.min.css"

因为之前已经定义好了 静态资源的 访问地址

1
app.use(express.static(path.join(__dirname,'public')))

所以 绝对路径 修改后就可以成为

1
public//admin/lib/bootstrap/css/bootstrap.min.css

1.4 优化模板

把公共部分 抽离 然后 利用模板引擎 渲染

可以看到 每个网页的 头部 与 侧边栏 是一样的

所以在 views/admin 建立 common 文件夹

新建 header.art aside.art 然后 把代码粘贴到里面

其他页面 通过

1
{{include '相对路径引用'}}
1
2
3
4
5
6
7
<!-- 头部 -->
{{include './common/header.art'}}
<!-- /头部 -->

<!-- 侧边栏 -->
{{include './common/aside.art'}}
<!-- 侧边栏 -->

1.5抽离 html 骨架

在common 目录下 新建 layout.art 把html骨架以及 一些页面相同的代码放进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>Blog - Content Manager</title>
<link rel="stylesheet" href="/admin/lib/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="/admin/css/base.css">
{{block 'link'}} {{/block}} <!-- 挖坑 -->
</head>

<body>
{{block 'main'}} {{/block}} <!-- 挖坑 -->

<!-- /删除确认弹出框 -->
{{block 'script'}} {{/block}} <!-- 挖坑 -->
<script src="/admin/lib/jquery/dist/jquery.min.js"></script>
<script src="/admin/lib/bootstrap/js/bootstrap.min.js"></script>
</body>

</html>

这里的 block 时预留 以后要插入代码的 位置

1
{{block 'link'}} {{/block}}  <!-- 挖坑 -->

之后 在 把其他页面的 抽离部分删除并引入骨架 这里 以 user.art 做例子

1
{{block  '对应的名字填坑'}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
{{extend './common/layout.art'}}  <!-- // 引入骨架 -->

{{block 'main'}} <!-- 填坑 -->
<!-- 头部 -->
{{include './common/header.art'}}
<!-- /头部 -->
<!-- 主体内容 -->
<div class="content">
<!-- 侧边 -->
{{include './common/aside.art'}}
<!-- 侧边栏 -->
<div class="main">
<!-- 分类标题 -->
<div class="title">
<h4>用户</h4>
<span>找到1个用户</span>
<a href="user-edit.html" class="btn btn-primary new">新增用户</a>
</div>
<!-- /分类标题 -->
<!-- 内容列表 -->
<table class="table table-striped table-bordered table-hover custom-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>角色</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>5b9a716cb2d2bf17706bcc0a</td>
<td>wangjian</td>
<td>wjb19891223@163.com</td>
<td>超级管理员</td>
<td>正常</td>
<td>
<a href="user-edit.html" class="glyphicon glyphicon-edit"></a>
<i class="glyphicon glyphicon-remove" data-toggle="modal" data-target=".confirm-modal"></i>
</td>
</tr>
<tr>
<td>5b9a716cb2d2bf17706bcc0a</td>
<td>wangjian</td>
<td>wjb19891223@163.com</td>
<td>普通用户</td>
<td>禁用</td>
<td>
<a href="user-edit.html" class="glyphicon glyphicon-edit"></a>
<i class="glyphicon glyphicon-remove" data-toggle="modal" data-target=".confirm-modal"></i>
</td>
</tr>
<tr>
<td>5b9a716cb2d2bf17706bcc0a</td>
<td>wangjian</td>
<td>wjb19891223@163.com</td>
<td>普通用户</td>
<td>启用</td>
<td>
<a href="user-edit.html" class="glyphicon glyphicon-edit"></a>
<i class="glyphicon glyphicon-remove" data-toggle="modal" data-target=".confirm-modal"></i>
</td>
</tr>
</tbody>
</table>
<!-- /内容列表 -->
<!-- 分页 -->
<ul class="pagination">
<li>
<a href="#">
<span>&laquo;</span>
</a>
</li>
<li><a href="#">1</a></li>
<li><a href="#">2</a></li>
<li><a href="#">3</a></li>
<li><a href="#">4</a></li>
<li><a href="#">5</a></li>
<li>
<a href="#">
<span>&raquo;</span>
</a>
</li>
</ul>
<!-- /分页 -->
</div>
</div>
<!-- /主体内容 -->
<!-- 删除确认弹出框 -->
<div class="modal fade confirm-modal">
<div class="modal-dialog modal-lg">
<form class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
<h4 class="modal-title">请确认</h4>
</div>
<div class="modal-body">
<p>您确定要删除这个用户吗?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<input type="submit" class="btn btn-primary">
</div>
</form>
</div>
</div>
{{/block}}

2.用户登录

1
2
3
4
5
6
7
7. 根据邮箱地址查询用户信息
8. 如果用户不存在,为客户端做出响应,阻止程序向下执行
9. 如果用户存在,将用户名和密码进行比对
10. 比对成功,用户登录成功
11. 比对失败,用户登录失败
12. 保存登录状态
13. 密码加密处理
  1. 连接数据库,在model 下创建 connect.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 链接数据库
    // 引入 mongooes
    const mongoose = require('mongoose');
    // 链接数据库
    mongoose.connect('mongodb://localhost/blog', { useNewUrlParser: true, useUnifiedTopology: true })
    .then(() => {
    console.log('数据库链接成功');
    })
    .catch(() => {
    console.log('数据库连接失败');
    })
  2. 创建 user.js 创建集合与规则

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    // 链接数据库
    // 引入 mongooes
    const mongoose = require('mongoose');

    // 集合规则
    const userSchema = new mongoose.Schema({
    username:{
    type:String,
    required:true,
    minLength:2,
    maxLength:20
    },
    email:{
    type:String,
    unique:true,// 设定是否唯一,保证 email 不重复


    },
    password:{
    type:String,
    required:true
    },
    role:{
    type:String,
    required:true
    },
    state:{
    type:Number,
    default:0,// 字段值为 0 是启用状态 1 为 禁止状态
    }
    })

    const User = mongoose.model('user',userSchema);

    module.exports = {
    // User: User
    User
    }; // 开放出去 使用对象格式可以多项开放
  3. 表单设置提交地址以及方式和验证

    image-20210214090411823

    这里提交地址是 login 方式是 post

    然后设置 js代码进行验证

    在 public/js文件夹下新建 common.js

    这里注意 serializeArray() 是jQuery里的方法 是获取表单里所有的带有name的项 格式为

    1
    // [{name: 'email',value: '用户输入的内容'},{}]

    这里利用 设置空对象 以及 循环的方法把格式改为

    1
    {email: "2513177689@qq.com", password: "11"}

    这是js代码 形参为传进来的表单 返回一个结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function serializeJson(form) {
    var result = {};
    // 获取表单中用户输入的内容
    // [{name: 'email',value: '用户输入的内容'},{}]
    var f = form.serializeArray();
    f.forEach(function (item) {
    result[item.name] = item.value;

    })

    return result;
    }

    之后在模板骨架文件 中 引入js文件

    image-20210214090922858

    之后在 login页面 设置js代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <script>

    // 为表单添加提交事件
    $('#loginForm').on('submit', function (res) {

    var result = serializeJson($(this))
    console.log(result);
    // 如果用户没有输入邮件地址
    if (result.email.trim().length == 0 ) {
    alert('请输入邮件地址')
    // 阻止程序向下执行
    return false;
    }
    // 如果用户没有输入密码
    if (result.password.trim().length == 0) {
    alert('请输入密码')

    // 阻止程序向下执行
    return false;
    }
    // 阻止表单默认提交的行为
    return false;
    })
    </script>
  4. 实现登录

    先实现路由

    修改表单提交地址

    image-20210214091513455

    安装 模块

    1
    2
    3
    4
    cnpm install body-parser
    引入
    // 引入body-parser模块 用来处理post请求参数
    const bodyparser = require('body-parser');

    然后在 qpp.js 里 使用 app.use()拦截所有请求 转换 请求块代码格式

    1
    2
    // 处理post参数
    app.use(bodyparser.urlencoded({extended:false}))

    之后在 admin路由里 使用 req.body 处理 参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 实现登录功能
    admin.post('/login', (req, res) => {
    // 接收请求参数
    const { email, password } = req.body; // req.body 为一个对象 使用解构获取里面的值
    if (email.trim().length === 0 || password.trim().length === 0) {
    // 这里主要是 服务器端再次进行 数据验证 因为客户端可以禁用 javascript代码
    return res.status(400).render('admin/error', { // 返回一个模板
    msg: '用户名或者密码错误'
    })
    }
    })
  5. 查询用户实现是否可以登录

    1
    2
    // 引用集合构造函数
    const { User } = require('../model/user')
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // 实现登录功能
    admin.post('/login', async (req, res) => {
    // 接收请求参数
    const { email, password } = req.body;
    if (email.trim().length === 0 || password.trim().length === 0) {

    return res.status(400).render('admin/error', {
    msg: '用户名或者密码错误'
    })
    }

    // 根据邮箱地址查询用户信息
    // 如果查询到了用户 user变量的值是对象类型 如果没有为空
    let user = await User.findOne({ email: email });
    // User.findOne({email:email}).then(res => {
    // console.log(res);
    // })
    if (user) {
    if (user.password === password) {
    res.send('登录成功')
    } else {
    // 没有查询到用户
    res.status(400).render('admin/error', { msg: '用户名或者密码错误' })
    }
    } else {
    // 没有查询到用户
    res.status(400).render('admin/error', { msg: '用户名或者密码错误' })
    }
    })
  6. 密码加密 bcrypt

    1
    2
    3
    4
    5
    6
    7
    // 导入bcrypt模块
    const bcrypt = require('bcrypt');
    // 生成随机字符串 gen => generate 生成 salt 盐
    let salt = await bcrypt.genSalt(10);
    // 使用随机字符串对密码进行加密
    let pass = await bcrypt.hash('明文密码', salt);

    1
    2
    3
    4
    5
    6
    7
    8
    依赖
    bcrypt依赖的其他环境
    1. python 2.x
    2. node-gyp
    npm install -g node-gyp
    3. windows-build-tools
    npm install --global --production windows-build-tools

    密码比对

    1
    2
    3
    // 密码比对
    let isEqual = await bcrypt.compare('明文密码', '加密密码')

    示例

    在 model/user.js 里修改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    async function createUser() {
    const salt = await bcrypt.genSalt(10); //
    const pass = await bcrypt.hash('123456', salt);
    const user = await User.create({
    username: 'it',
    email: '2513177689@qq.com',
    password: pass,
    role: 'admin',
    state: 0

    })
    }
    // createUser(); 使用方法

    之后在登录 路由里 进行密码比对

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // 实现登录功能
    admin.post('/login', async (req, res) => {
    // 接收请求参数
    const { email, password } = req.body;
    if (email.trim().length === 0 || password.trim().length === 0) {

    return res.status(400).render('admin/error', {
    msg: '用户名或者密码错误'
    })
    }

    // 根据邮箱地址查询用户信息
    // 如果查询到了用户 user变量的值是对象类型 如果没有为空
    let user = await User.findOne({ email: email });
    // User.findOne({email:email}).then(res => {
    // console.log(res);
    // })
    if (user) {
    // 将客户端 传来的密码 与 数据库的密码进行比对
    // 参数1 是客服端密码 参数2 是数据库密码 返回值是布尔值
    let isValid = await bcrypt.compare(password, user.password)

    if (isValid) {
    res.send('登录成功')
    } else {
    // 没有查询到用户
    res.status(400).render('admin/error', { msg: '用户名或者密码错误' })
    }
    } else {
    // 没有查询到用户
    res.status(400).render('admin/error', { msg: '用户名或者密码错误' })
    }
    })
  7. cookie 与session

    cookie:浏览器在电脑硬盘中开辟的一块空间,主要供服务器端存储数据。

    lcookie中的数据是以域名的形式进行区分的。

    lcookie中的数据是有过期时间的,超过时间数据会被浏览器自动删除。

    lcookie中的数据会随着请求被自动发送到服务器端。

    image-20210215170607454

    session:实际上就是一个对象,存储在服务器端的内存中,在session对象中也可以存储多条数据,每一条数据都有一个sessionid做为唯一标识。

    image-20210215170852803

    在node.js中需要借助express-session实现session功能。

    1
    cnpm install express-session //下载模块
    1
    2
    3
    const session = require('express-session');
    app.use(session({ secret: 'secret key' }));

    之后在app.js 里引用

    1
    2
    3
    4
    // 引入express-session
    const session = require('express-session');
    // 配置 session
    app.use(session({ secret: 'secret key' }));

    之后在 admin路由中存储用户信息

    image-20210215173342027

    之后调用信息

    image-20210215173410592

  8. app.locals 存储信息 模板中能直接拿到

    image-20210215174545096

    在头部模板中直接拿到

    image-20210215174804087

  9. 请求拦截

    如果用户在 地址栏中输入 user页面其实是能直接访问的

    接下来就做一个请求拦截

    在没有登录的时候 访问不到 user页面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 拦截请求 判断用户登录状态
    app.use('/admin',(req, res,next) => {
    // 判断用户是否是登录状态
    // 判断用户的登录状态
    // 如果是登录的 将请求放行
    // 如果不是登录的 将请求重定向到登录页面
    if(req.url != '/login' && !req.session.username) {
    res.redirect('/admin/login')
    } else {
    // 用户是登陆状态 将请求放行
    next()
    }
    })
  10. 实现退出功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 实现退出功能
    admin.get('/logout', (req, res) => {
    // 删除session
    req.session.destroy(function () {
    // 删除cookie
    res.clearCookie('connect.sid');
    // 重定向到用户登录页面
    res.redirect('/admin/login');
    });
    })
  11. 代码优化

    路由代码放到 各自的 js文件中

    image-20210215184229165

3.新增用户

1
2
3
4
5
6
7
8
9
10
11
1. 为用户列表页面的新增用户按钮添加链接
2. 添加一个连接对应的路由,在路由处理函数中渲染新增用户模板
3 .为新增用户表单指定请求地址、请求方式、为表单项添加name属性
4. 增加实现添加用户的功能路由
5. 接收到客户端传递过来的请求参数
6. 对请求参数的格式进行验证
7. 验证当前要注册的邮箱地址是否已经注册过
8. 对密码进行加密处理
9. 将用户信息添加到数据库中
10. 重定向页面到用户列表页面

1.修改表单提交地址以及name属性 数据格式验证

先设置请求地址

image-20210216110336768

然后设置路由

1
2
3
4
5
6
7
8
9
10
// 创建用户信息编辑
admin.get('/user-edit',require('./admin/userEdit'))
// 这里响应 编辑页面模板
module.exports = (req, res, next) =>{
res.render('admin/user-edit')
}


// 新增用户添加
admin.post('/user-edit',require('./admin/userEditAdd'))

image-20210216110552186

之后利用第三方模块 Joi 实现表单数据格式 验证

Joi

1
cnpm install joi@14.3.1 // 这里 使用 14.3.1版本

JavaScript对象的规则描述语言和验证器。

1
2
3
4
5
6
7
8
9
10
const Joi = require('joi');
const schema = {
username: Joi.string().alphanum().min(3).max(30).required().error(new Error(‘错误信息’)),
password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/),
access_token: [Joi.string(), Joi.number()],
birthyear: Joi.number().integer().min(1900).max(2013),
email: Joi.string().email()
};
Joi.validate({ username: 'abc', birthyear: 1994 }, schema);

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 引入 joi 模块
const Joi = require('joi')

// 定义验证规则
const schema = {
username: Joi.string().min(2).max(5).required().error(new Error('Username属性没有通过验证')),
birth:Joi.number().min(1900).max(2020).error(new Error('birth没有通过验证'))
}



async function run() {
try {
// 实施验证
await Joi.validate({ username:'ab',birth:1800 }, schema) // 返回pomise对象
} catch (e) {
console.log(e.message);
return
}
console.log('验证通过');

}

run()

表单信息验证

重定向然后地址栏携带参数数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 引入 joi 模块
const Joi = require('joi');


module.exports = (req, res, next) => {
//定义对象验证规则
const schema = {
username: Joi.string().min(2).max(12).required().error(new Error('用户名不通过')),
email: Joi.string().email().error(new Error('邮箱不符合规则')),
password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/).required().error(new Error('密码格式不符合要求')),
// 这里的 valid 是规定只能是 括号里面的值
role: Joi.string().valid('normal', 'admin').required().error(new Error('角色值非法')),
state: Joi.number().valid(0, 1).required().error(new Error('状态值非法'))
}

async function run() {
try {
// 实施验证
await Joi.validate(req.body, schema)
} catch (e) {
// 验证没有通过
// console.log(e.message);
// 重定向回 用户 添加页面
return res.redirect(`/admin/user-edit?message=${e.message}`)
}
}

run()

}

之后节后参数 渲染到页面

1
2
3
4
5
6
module.exports = (req, res, next) => {
const { message } = req.query;
res.render('admin/user-edit', {
message: message
})
}

image-20210216120306068

2.验证邮箱是否存在

当添加用户时 先到数据库查询 邮箱地址是否已经被占用

1
2
3
4
5
6
// 根据邮箱地址查询用户是否存在
let user = await User.findOne({ email: req.body.email })
// 如果用户邮箱已经存在,邮箱地址被人占用
if (user) {
return res.redirect(`/admin/user-edit?message=邮箱地址被占用`) /
}

如果没有占用 对提交的密码进行加密 添加到数据库中 然后重定向user列表页面

1
2
3
4
5
6
7
8
9
10
// 对密码进行加密
// 生成随机字符串
const salt = await bcrypt.genSalt(10);
const password = await bcrypt.hash(req.body.password, salt);
// 替换密码
req.body.password = password;
// 将用户信息添加到数据库
await User.create(req.body)
// 重定向到用户列表页面
res.redirect('/admin/user')

3.代码优化 + 错误处理优化

代码优化

  • 把数据验证代码放到 model/user.js 里

  • // 验证用户信息
    const validateuser = function (user) {
        //定义对象验证规则
        const schema = {
            username: Joi.string().min(2).max(12).required().error(new Error('用户名不通过')),
            email: Joi.string().email().error(new Error('邮箱不符合规则')),
            password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/).required().error(new Error('密码格式不符合要求')),
            // 这里的 valid 是规定只能是 括号里面的值
            role: Joi.string().valid('normal', 'admin').required().error(new Error('角色值非法')),
            state: Joi.number().valid(0, 1).required().error(new Error('状态值非法'))
        }
    
        // 实施验证
        return Joi.validate(user, schema)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    ![image-20210216174032227](https://zhihuan-1302250699.cos.ap-nanjing.myqcloud.com/images/image-20210216174032227.png)

    - 之后进行引入 然后调用

    - ```js
    //引入用户集合的构造函数
    const { User, validateuser } = require('../../model/user')

    try {
    await validateuser(req.body)
    } catch (e) {
    // 验证没有通过
    // console.log(e.message);
    // 重定向回 用户 添加页面
    // return res.redirect(`/admin/user-edit?message=${e.message}`)
    // JSON.stringify() 将对象数据类型转为字符串数据类型
    return next(JSON.stringify({ path: '/admin/user-edit', message: e.message }))
    }
  • 对错误重复代码进行处理

  • 在 app.js 里 设置错误处理中间件

  • // 错误信息处理中间件
    app.use((err, req, res, next) => {
        // 将字符串对象 转为 对象类型
        // JSON.parse()
        const result = JSON.parse(err)
        res.redirect(`${result.path}?message=${result.message}`)
    })
    
    1
    2
    3
    4
    5

    - 有错误了 将数据传递过去

    - ```js
    return next(JSON.stringify({ path: '/admin/user-edit', message: e.message }))

4.展示用户列表信息

在渲染用户列表页面时 查询数据库 把数据渲染到页面

1
2
3
4
5
6
7
8
9
10
const {User} = require('../../model/user')
module.exports = async (req, res) => {
// 将用户信息从数据库中查询
const userlist = await User.find()
// res.send(userlist)
res.render('admin/user',{
userlist
})

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{{each userlist}}

<tr>
<!-- @ 代表原文输出 -->
<td>{{@$value._id}}</td>
<td>{{$value.username}}</td>
<td>{{$value.email}}</td>
<td>{{$value.role == 'admin' ? '超级管理员' : '普通用户' }}</td>
<td>{{$value.state == 0? '启用':'禁用'}}</td>
<td>
<a href="user-edit.html" class="glyphicon glyphicon-edit"></a>
<i class="glyphicon glyphicon-remove" data-toggle="modal" data-target=".confirm-modal"></i>
</td>
</tr>

{{/each}}

image-20210216175839188

5.数据分页

1
2
3
limit(2) // limit 限制查询数量  传入每页显示的数据数量
skip(1) // skip 跳过多少条数据 传入显示数据的开始位置

数据开始查询位置=(当前页-1)* 每页显示的数据条数

1
2
3
1.当前页,用户通过点击上一页或者下一页或者页码产生,客户端通过get参数方式传递到服务器端
2.总页数,根据总页数判断当前页是否为最后一页,根据判断结果做响应操作

1
2
3
limit(2) // limit 限制查询数量  传入每页显示的数据数量
skip(1) // skip 跳过多少条数据 传入显示数据的开始位置

数据开始查询位置=(当前页-1)* 每页显示的数据条数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const {User} = require('../../model/user')
module.exports = async (req, res) => {
// 接收客户端传递过来的的当前页参数
let page = req.query.page || 1;
// 每一页显示的数据条数

let pagesize = 1;
// 查询用户数据的总数
let count = await User.countDocuments({})
// 总页数
let total = Math.ceil(count / pagesize); // 向上取整数

// 页码对应的开始位置
let start = (page - 1) * pagesize;
// 将用户信息从数据库中查询
const userlist = await User.find({}).limit(pagesize).skip(start)
// res.send(userlist)
res.render('admin/user',{
userlist ,
page:page,
total:total
})

}

模板页 语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<ul class="pagination">

<li style="display: {{page-1 <= 0? 'none' : 'inline'}}">
<a href="/admin/user?page=<%page-1%>">
<span>&laquo;</span>
</a>
</li>

<% for (var i=1 ; i <= total; i++) { %>
<li><a href="/admin/user?page=<%=i%>">{{i}}</a></li>
<% } %>


<li style="display: {{page-0+1 > total? 'none' : 'inline'}}">
<a href="/admin/user?page=<%=page-0+1%>">
<span>&raquo;</span>
</a>
</li>

</ul>

4.修改用户

1.判断是否是修改操作

当点击修改按钮时

image-20210217101714314

首先要给 按钮 传递一个 数据库 _id 属性

这样做

image-20210217101848459

1
2
3
4
5
6
7
8
9
10
11
12
<tr>
<!-- @ 代表原文输出 -->
<td>{{@$value._id}}</td>
<td>{{$value.username}}</td>
<td>{{$value.email}}</td>
<td>{{$value.role == 'admin' ? '超级管理员' : '普通用户' }}</td>
<td>{{$value.state == 0? '启用':'禁用'}}</td>
<td>
<a href="/admin/user-edit?id={{@$value._id}}" class="glyphicon glyphicon-edit"></a>
<i class="glyphicon glyphicon-remove" data-toggle="modal" data-target=".confirm-modal"></i>
</td>
</tr>

因为 修改与添加 是一个 模板 所以要判断 当前是修改操作 还是 添加操作

可以根据 传递参数 是否有 id 属性来判断

所以 在 userEdit.js 里修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const {User} = require('../../model/user')

module.exports = async (req, res, next) => {
// 获取到地址栏中的 id 参数
const { message,id } = req.query;
// 如果传递了 id 参数 就是修改操作
if(id) {
// 修改操作
let user = await User.findOne({_id:id});

// 渲染用户编辑页面
res.render('admin/user-edit', {
message: message,
user:user,
link:'/admin/user-add', // 这里的 是 表单提交地址
btn:'修改' // 这里显示是 修改还是 添加 还有 可以用来判断是否显示 id
})

} else {
// 添加操作
res.render('admin/user-edit', {
message: message,
link:'/admin/user-edit',
btn:'添加'
})
}

}

用户信息修改或者 添加 模板进行修改

这里进行举例

1
2
3
4
5
6
<!-- 分类标题 -->
这是id 的是否显示
<div class="title">
<h4 style="display: {{btn === '修改' ? 'block' : 'none'}};">{{@user && user._id}}</h4>
<p class="tips">{{message}}</p>
</div>
1
2
3
4
5
6
7
8
<div class="form-group">
这是表单 属性的 是否 显示
<label>角色</label>
<select class="form-control" name="role">
<option value="normal" {{user && user.role == 'normal' ? 'selected' : ''}}>普通用户</option>
<option value="admin" {{user && user.role == 'admin' ? 'selected' : ''}}>超级管理员</option>
</select>
</div>

2.实现修改功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const { User } = require('../../model/user');
const bcrypt = require('bcrypt')
module.exports = async (req, res, next) => {
// 接收客户端传递过来的请求参数
const { username, email, password, role, state } = req.body;

// 即将要修改的用户id
const id = req.query.id;

// res.send(body.password)
let user = await User.findOne({ _id: id });
//密码比对
const isValid = await bcrypt.compare(password, user.password) // 密码比对成功 返回 true
if (isValid) {

// 将用户信息 更新到数据库中
await User.updateOne({ _id: id }, {
username: username,
email: email,
role: role,
state: state,
})

// 重定向列表页面
res.redirect('/admin/user')
} else {
// 密码比对失败
let obj = {
path: '/admin/user-edit',
message: '密码比对失败,不能对用户信息更新',
id: id
}
obj = JSON.stringify(obj)
next(obj)

}


// res.send(user)

}

3.删除功能

1
2
3
4
5
6
7
8
1. 在确认删除框中添加隐藏域用以存储要删除用户的ID值
2. 为删除按钮添自定义属性用以存储要删除用户的ID值
3. 为删除按钮添加点击事件,在点击事件处理函数中获取自定义属性中存储的ID值并将ID值存储在表单的隐藏域中
4. 为删除表单添加提交地址以及提交方式
5. 在服务器端建立删除功能路由
6. 接收客户端传递过来的id参数
7. 根据id删除用户

  1. 在确认删除框中添加隐藏域用以存储要删除用户的ID值

    image-20210217192853223

    1
    2
    3
    4
    <div class="modal-body">
    <p>您确定要删除这个用户吗?</p>
    <input type="hidden" name="id" id="deletUserId">
    </div>
  2. 为删除按钮添自定义属性用以存储要删除用户的ID值

    1
    2
    <i class="glyphicon glyphicon-remove delete" data-toggle="modal" data-target=".confirm-modal"
    data-id="{{@$value._id}}"></i>
  3. 为删除按钮添加点击事件,在点击事件处理函数中获取自定义属性中存储的ID值并将ID值存储在表单的隐藏域中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <script>
    // 这里注意 不能 用 id 获取 删除按钮 因为 id 代表 唯一 不会绑定多个按钮点击事件
    $('.delete').on('click', function () {
    // 获取 用户id
    var id = $(this).attr('data-id');
    // 将要删除的 id 储存到隐藏域中
    $('#deletUserId').val(id)
    })
    </script>
  4. 为删除表单添加提交地址以及提交方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <form class="modal-content" action="/admin/delete" method="get">
    <div class="modal-header">
    <button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
    <h4 class="modal-title">请确认</h4>
    </div>
    <div class="modal-body">
    <p>您确定要删除这个用户吗?</p>
    <input type="hidden" name="id" id="deletUserId">
    </div>
    <div class="modal-footer">
    <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
    <input type="submit" class="btn btn-primary">
    </div>
    </form>
  5. 在服务器端建立删除功能路由

    image-20210217193052875

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const { User } = require('../../model/user')
    module.exports = async (req, res) => {
    // res.send('ok')
    // 获取要删除的 用户 id
    // res.send(req.query.id)

    await User.findOneAndDelete({ _id: req.query.id });
    res.redirect('/admin/user')
    }
  6. 接收客户端传递过来的id参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const { User } = require('../../model/user')
    module.exports = async (req, res) => {
    // res.send('ok')
    // 获取要删除的 用户 id
    // res.send(req.query.id)

    await User.findOneAndDelete({ _id: req.query.id });
    res.redirect('/admin/user')
    }
  7. 根据id删除用户

    1
    await User.findOneAndDelete({ _id: req.query.id });

5.文章管理功能实现

1.显示文章管理页面 与 页面编辑页面

设置对应路由 然后 把不同的模板响应就可以了

image-20210219105347121

image-20210219105348485

设置对应路由

文章列表页面 ariicle.js

文章编辑页面 article-edit.js

设置不同路由

1
2
3
4
// 文章列表页面路由
admin.get('/article', require('./admin/article'))
// 文章编辑页面路由
admin.get('/article-edit',require('./admin/article-edit'))

二级路由文件

1
2
3
4
5

module.exports = (req,res) => {
req.app.locals.currentLink = 'article'
res.render('admin/article.art')
}
1
2
3
4
module.exports = (req,res) => {
req.app.locals.currentLink = 'article'
res.render('admin/article-edit.art')
}

之后设置 用户管理 或者 文章管理的 切换效果

侧边栏模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<ul class="menu list-unstyled">
<li>
<a class="item {{currentLink === 'user' ? 'active' :''}}" href="/admin/user">
<span class="glyphicon glyphicon-user"></span>
用户管理
</a>
</li>
<li>
<a class="item {{currentLink === 'article' ? 'active' :''}}" href="/admin/article">
<span class="glyphicon glyphicon-th-list"></span>
文章管理
</a>
</li>
</ul>

2.文章管理功能

  • 创建文章集合

    • // 1. 引入 mongooes模块
      const mongoose = require('mongoose');
      // 2. 创建文章集合规则
      const articleSchema = new mongoose.Schema({
          title:{
              type:String,
              maxlength:20,
              minlength:4,
              required:[true,'标题未填写']
          },
          author:{
              type:mongoose.Schema.Types.ObjectId,
              ref:'User',
              required:[true,'请传递作者']
          },
          publishDate:{
              type:Date,
              default:Date.now
          },
          cover:{
              type:String,
              default:null
          },
          content:{
              type:String
          }
      })
      // 3. 根据规创建集合
      const Article =  mongoose.model('Article',articleSchema);
      // 4. 将集合导出
      module.exports = {
          Article
      }
      
      1
      2
      3
      4
      5
      6
      7

      - 实现文章添加功能

      - 添加发布连接

      - ```html
      <a href="/admin/article-edit.html" class="btn btn-primary new">发布新文章</a>
  • 设置表单提交事件 以及 提交地址

    • <!-- /分类标题 -->
             <!-- enctype 指定表单数据的编码类型 
                 application/x-www-form-urlencoded
                 name=zhansan&age=20
                 multipart/form-data 将表单数据 编码成2进制
             -->
             <form class="form-container" action="/admin/article-add" method="post" enctype="multipart/form-data">
      
      1
      2
      3
      4
      5

      - formidable

      - ```
      cnpm install formidable
    • 作用:解析表单,支持get请求参数,post请求参数、文件上传。

    • // 引入formidable模块
      const formidable = require('formidable');
      // 创建表单解析对象
      const form = new formidable.IncomingForm();
      // 设置文件上传路径
      form.uploadDir = "/my/dir";
      // 是否保留表单上传文件的扩展名
      form.keepExtensions = false;
      // 对表单进行解析
      form.parse(req, (err, fields, files) => {
          // fields 存储普通请求参数
              // files 存储上传的文件信息
      });
       
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26

      - 实例

      - ```js
      // 引入 formidable 第三方模块
      const formidable = require('formidable');
      const path = require('path');
      module.exports = (req,res)=>{
      // 1.创建表单解析对象
      const form = new formidable.IncomingForm();
      // 2.配置上传文件的存放位置
      // __dirname 指向执行js 文件的 绝对路径 这里为 route/admin
      form.uploadDir = path.join(__dirname,'../','../','public','uploads')
      // 3.是否保留上传文件的后缀
      form.keepExtensions = true;
      // 4.解析表单
      // 参数 1 指哪个表单
      // 参数2 回调函数
      // 1.err 错误对象
      // 2.fields 对象类型 保存 普通表单数据
      // 3.files 对象类型 保存了 上传文件数据
      form.parse(req,(err,fields,feles)=>{
      res.send(feles)
      })

      }
    • image-20210220174710260

  • 实现文件上传 与 预览

  • var reader = new FileReader();
    reader.readAsDataURL('文件');
    reader.onload = function () {
        console.log(reader.result); 
    }
     
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28



    > 上传
    >
    > ```js
    > // 引入 formidable 第三方模块
    > const formidable = require('formidable');
    > const path = require('path');
    > module.exports = (req,res)=>{
    > // 1.创建表单解析对象
    > const form = new formidable.IncomingForm();
    > // 2.配置上传文件的存放位置
    > // __dirname 指向执行js 文件的 绝对路径 这里为 route/admin
    > form.uploadDir = path.join(__dirname,'../','../','public','uploads')
    > // 3.是否保留上传文件的后缀
    > form.keepExtensions = true;
    > // 4.解析表单
    > // 参数 1 指哪个表单
    > // 参数2 回调函数
    > // 1.err 错误对象
    > // 2.fields 对象类型 保存 普通表单数据
    > // 3.files 对象类型 保存了 上传文件数据
    > form.parse(req,(err,fields,feles)=>{
    > res.send(feles)
    > })
    >
    > }
    > > 预览 > > 文件上传后把读取编码 放到 img标签的src 里面 > >
    1
    2
    3
    4
    5
    6
    7
    8
    给选择文件 按钮 绑定一个选择时间
    <div class="form-group">
    <label for="exampleInputFile">文章封面</label>
    <input type="file" name="cover" id="file" multiple>
    <div class="thumbnail-waper">
    <img class="img-thumbnail" src="" id="preview">
    </div>
    </div>
    > >
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    var file = document.querySelector('#file');
    var preview = document.querySelector('#preview')
    // 当用户选择完文件以后
    file.onchange = function () {
    // 1.创建文件读取对象
    var reader = new FileReader();
    // 用户选择的文件列表
    console.log( this.files);

    // 2.读取文件
    reader.readAsDataURL(this.files[0])
    // 3.监听onload事件
    reader.onload = function() {
    // console.log(reader.result);
    // 将文件读取的结果显示在页面中
    preview.src = reader.result;
    }
    }
    > >

3.实现文章数据库插入

1.引入 Article数据库构造函数 + 数据库插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 引入 formidable 第三方模块
const formidable = require('formidable');
const path = require('path');
const { Article } = require('../../model/article');
module.exports = (req, res) => {
// 1.创建表单解析对象
const form = new formidable.IncomingForm();
// 2.配置上传文件的存放位置
// __dirname 指向执行js 文件的 绝对路径 这里为 route/admin
form.uploadDir = path.join(__dirname, '../', '../', 'public', 'uploads')
// 3.是否保留上传文件的后缀
form.keepExtensions = true;
// 4.解析表单
// 参数 1 指哪个表单
// 参数2 回调函数
// 1.err 错误对象
// 2.fields 对象类型 保存 普通表单数据
// 3.files 对象类型 保存了 上传文件数据
form.parse(req, async (err, fields, files) => {
// console.log(files.cover.path.split('public')[1]);
// res.send(files.cover.path.split('public')[1])

await Article.create({
title: fields.title,
author: fields.author,
publishDate: fields.publishDate,
cover: files.cover.path.split('public')[1],
content: fields.content
});
// 将页面重定向到 文章列表页面
res.redirect('/admin/article')
})

}

2.文章列表数据展示功能

大坑 优先使用第二个

解决办法:

1:在populate后面加lean方法;

1
2
	例:.populate().lean();
1

注意:

lean():是告诉mongoose返回的是普通对象,而不是mongoose文档对象

2.先通过JSON.stringify()这个方法将文档对象转为字符串,将他的其他属性全部格式掉,只需要留下需要的数据字符串即可!

然后再通过JSON.parse()这个方法转为对象,这个方法虽然丢失效率,但是暂时解决问题。
在这里插入图片描述

进入 article 页面时 把数据 渲染到模板上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const {Article} = require('../../model/article');

module.exports = async (req,res) => {
req.app.locals.currentLink = 'article'
// 查询所有文章数据
// 注意 这里 使用多表查询时 再把数据渲染到模板时 要在populate() 后面加lean()
// lean():是告诉mongoose返回的是普通对象,而不是mongoose文档对象
let articleList = await Article.find().populate('author').lean(); // 多表查询

// res.send(articleList)
res.render('admin/article.art',{
articleList
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<tbody>
{{each articleList}}
<tr>
<td>{{@$value._id}}</td>
<td>{{$value.title}}</td>
<td>{{$value.publishDate}}</td>
<td>{{$value.author.username}}</td>
<td>
<a href="article-edit.html" class="glyphicon glyphicon-edit"></a>
<i class="glyphicon glyphicon-remove" data-toggle="modal" data-target=".confirm-modal"></i>
</td>
</tr>
{{/each}}
</tbody>

时间格式处理

因为时间格式处理只有 art-template 模板能用 所以还需要导入一下 art-template

1
cnpm install dateformat
1
2
3
4
// 导入 dateformat第三方模块
const dateFormat = require('dateformat');
//导入 art-template
const template = require('art-template');
1
2
// 向模板中 导入 dateFormat 变量
template.defaults.imports.dateFormat = dateFormat;

3.数据分页

数据分页 mongoose-sex-page

1
cnpm install mongoose-sex-page
1
2
3
const pagination = require('mongoose-sex-page');
pagination(集合构造函数).page(1) .size(20) .display(8) .exec();

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const {Article} = require('../../model/article');
// 导入 mongoose-sex-page
const pagination = require('mongoose-sex-page')
module.exports = async (req,res) => {
// 接收客户端传来的页码
const page = req.query.page || 1;
console.log(page);
req.app.locals.currentLink = 'article'
// 查询所有文章数据
// 注意 这里 使用多表查询时 再把数据渲染到模板时 要在populate() 后面加lean()
// lean():是告诉mongoose返回的是普通对象,而不是mongoose文档对象
// page 指定当前页
// size 指定每页数据条数
// display 指定客户端要显示的页码数量
let articleList = await pagination(Article).find().page(page).size(10).display(3).populate('author').exec() // 多表查询
let str = JSON.stringify(articleList);
articleList = JSON.parse(str);
// console.log(articleList);
// res.send(articleList)
res.render('admin/article.art',{
articleList
})
}

这里要讲一下 数据格式 大坑大坑 大坑

因为 多表查询时 渲染模板肯定会报错

1
Unexpected token R in JSON at position 0     at JSON.parse (<anonymous>)

所以这里不得不 先将 json 转为字符串, 去掉不必要的格式 然后 在 转为 对象

然后再向模板里渲染

1
2
3
let articleList =  await pagination(Article).find().page(page).size(10).display(3).populate('author').exec() // 多表查询
let str = JSON.stringify(articleList);
articleList = JSON.parse(str);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
{
"page": 1,
"size": 10,
"total": 4,

"records": [

{
"cover": "\uploads\upload_fbda5763dd464cf8780d9fef777f2d92.jpg",
"_id": "6030f2b1edb52b4180c09c19",
"title": "jQuery插件库",

"author": {
"state": 0,
"_id": "602d0cfb69621575e0b62337",
"username": "2513177689",
"email": "2513177689@qq.com",
"password": "$2b$10$A46vaud6RcUQgCpmwW66D.D5iwHP2BzGEP.C.dI1c7zONC/Y/drkK",
"role": "admin",
"__v": 0
},
"publishDate": "2021-02-23T00:00:00.000Z",
"content": "<p>1111</p>",
"__v": 0
},

{
"cover": "\uploads\upload_de141b67ae447c77cd9012746ff40c56.jpg",
"_id": "603101266cd77f5a580c142d",
"title": "测试22",

"author": {
"state": 0,
"_id": "602d0cfb69621575e0b62337",
"username": "2513177689",
"email": "2513177689@qq.com",
"password": "$2b$10$A46vaud6RcUQgCpmwW66D.D5iwHP2BzGEP.C.dI1c7zONC/Y/drkK",
"role": "admin",
"__v": 0
},
"publishDate": "2021-02-18T00:00:00.000Z",
"content": "<h2>111111111111111111111111111</h2><h3>1111</h3>",
"__v": 0
},

{
"cover": "\uploads\upload_7b5c1ebe0f81673e1b7c06c472d277cd.png",
"_id": "603101646cd77f5a580c142e",
"title": "测试33",

"author": {
"state": 1,
"_id": "602b96941394d341c0c7524b",
"username": "17664512753",
"email": "123111@163.com",
"password": "$2b$10$dgR2jGoqv75/pNY/GoEDQ.M/eGbUUqKvUjcaO8PrFvu.Om4Ru7lGC",
"role": "admin",
"__v": 0
},

"publishDate": null,
"content": "<p>111111111111111111</p>",
"__v": 0
},

{
"cover": "\uploads\upload_460045d9e2acc2d747f84bbacca4dcd7.jpg",
"_id": "603103546609963a0cc6b576",
"title": "测试44",

"author": {
"state": 0,
"_id": "602d0cfb69621575e0b62337",
"username": "2513177689",
"email": "2513177689@qq.com",
"password": "$2b$10$A46vaud6RcUQgCpmwW66D.D5iwHP2BzGEP.C.dI1c7zONC/Y/drkK",
"role": "admin",
"__v": 0
},
"publishDate": "2021-02-17T00:00:00.000Z",
"content": "<p>1111111111111111111111111111111</p>",
"__v": 0
}
],
"pages": 1,

"display": [
1
]
}
$.theater[*].movies

之后模板里要改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
表格 
<tbody>
{{each articleList.records}}
<tr>
<td>{{@$value._id}}</td>
<td>{{$value.title}}</td>
<td>{{dateFormat($value.publishDate,'yyyy-mm-dd')}}</td>
<td>{{$value.author.username}}</td>
<td>
<a href="article-edit.html" class="glyphicon glyphicon-edit"></a>
<i class="glyphicon glyphicon-remove" data-toggle="modal" data-target=".confirm-modal"></i>
</td>
</tr>
{{/each}}
</tbody>


以及 按钮
<!-- 分页 -->
<ul class="pagination">
{{if articleList.page > 1 }}
<li>
<a href="/admin/article?page={{articleList.page - 1 }}">
<span>&laquo;</span>
</a>
</li>
{{/if}}

<li>
{{each articleList.display}}
<a href="/admin/article?page={{$value}}">{{$value}}</a>
{{/each}}
</li>
{{if articleList.page < articleList.pages }}
<li>
<a href="/admin/article?page={{articleList.page -0+1}}">
<span>&raquo;</span>
</a>
</li>
{{/if}}
</ul>
<!-- /分页 -->

4.文章修改删除 与 用户差不多

6.mongoDB数据库添加账号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1. 以系统管理员的方式运行powershell
2. 连接数据库 mongo
3. 查看数据库 show dbs
4. 切换到admin数据库 use admin
5. 创建超级管理员账户 db.createUser()
db.createUser({user:'root',pwd:'root',roles:['root']})
6. 切换到blog数据 use blog
7. 创建普通账号 db.createUser()
db.createUser({user:'itcast',pwd:'itcast',roles:['readWrite']})
8. 卸载mongodb服务
1. 停止服务 net stop mongodb
2. mongod --remove
9. 创建mongodb服务
日志 与 数据库存储目录
mongod --logpath="C:\Program Files\MongoDB\Server\4.1\log\mongod.log" --dbpath="C:\Program Files\MongoDB\Server\4.1\data" --install –-auth
10. 启动mongodb服务 net start mongodb
11. 在项目中使用账号连接数据库
mongoose.connect('mongodb://user:pass@localhost:port/database')


7.开发环境与生产环境

什么是开发环境与生产环境

环境,就是指项目运行的地方,当项目处于开发阶段,项目运行在开发人员的电脑上,项目所处的环境就是开发环境。当项目开发完成以后,要将项目放到真实的网站服务器电脑中运行,项目所处的环境就是生产环境。

为什么要区分开发环境与生产环境

因为在不同的环境中,项目的配置是不一样的,需要在项目代码中判断当前项目运行的环境,根据不同的环境应用不同的项目配置。

image-20210221165038315

image-20210221165059037

morgan

1
cnpm install morgan
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 导入 morgan
const morgan = require('morgan');
// 获取系统环境变量 返回值是对象
// console.log();
if(process.env.NODE_ENV == 'development') {
// 当前是开发环境
console.log('当前是开发环境');
// 在开发环境中 将客户 发送到 服务器端的请求信息打印到控制台中
app.use(morgan('dev'))
} else {
// 生产环境
console.log('当前是生产环境');

}

8.第三方模块 config

1
2
3
4
作用:允许开发人员将不同运行环境下的应用配置信息抽离到单独的文件中,模块内部自动判断当前应用的运行环境,
并读取对应的配置信息,极大提供应用配置信息的维护成本,避免了当运行环境重复的多次切换时,手动到项目代码
中修改配置信息

使用步骤

1.使用npm install config命令下载模块

2.在项目的根目录下新建config文件夹

3.在config文件夹下面新建default.json、development.json、production.json文件

4.在项目中通过require方法,将模块进行导入

5.使用模块内部提供的get方法获取配置信息

1
2
3
4
5
6
7
8
9
{
"db":{
"user":"itcast",
"pwd":"itcast",
"host":"localhost",
"port":"27017",
"name":"blog"
}
}

connect.js

1
2
3
4
5
6
7
8
9
10
11
12
13
// 链接数据库
// 引入 mongooes
const mongoose = require('mongoose');
// 导入 config
const config = require('config')
// 链接数据库
mongoose.connect(`mongodb://${config.get('db.user')}:${config.get('db.pwd')}@${config.get('db.host')}:${config.get('db.port')}/${config.get('db.name')}`, { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => {
console.log('数据库链接成功');
})
.catch(() => {
console.log('数据库连接失败');
})

将敏感配置信息存储在环境变量中

1
2
3
4
1.在config文件夹中建立custom-environment-variables.json文件
2.配置项属性的值填写系统环境变量的名字
3.项目运行时config模块查找系统环境变量,并读取其值作为当前配置项属于的值

1
2
3
4
5
6
{ 
"db": {
"pwd": "APP_PWD"
}
}

image-20210221175703509

9.主界面

1.配置路由

配置路由 方法跟用户界面高差不多

image-20210221200810097

image-20210221200828808

2.渲染模板

在进入 home路由时查询数据库 把数据 渲染到模板上

1
2
3
4
5
6
7
8
9
10
11
12
13
const {Article} = require('../../model/article');
module.exports = async (req,res) => {

const id = req.query.id;

let art = await Article.findOne({_id:id}).populate('author');
let str = JSON.stringify(art);
art = JSON.parse(str);
// res.send(art)
res.render('home/article',{
art
})
}

需要注意的地方

模板里 这里字符去除 html标签 以及 截取

1
2
3
<div class="brief">
{{@$value.content.replace(/<[^>]+>/g,'').substr(0,150)+ '...'}}
</div>

image-20210222074812196

接下来就是点击页面 渲染数据

原理差不多

image-20210222074848074

10.文章评论

1.创建评论集合

2.判断用户是否登录,如果用户登录,再允许用户提交评论表单

3.在服务器端创建文章评论功能对应的路由

4.在路由请求处理函数中接收客户端传递过来的评论信息

5.将评论信息存储在评论集合中

6.将页面重定向回文章详情页面

7.在文章详情页面路由中获取文章评论信息并展示在页面中

创建集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 引入 mongoes
const mongooes = require('mongooes');
// 创建评论集合规则
const commentSchema = new mongooes.Schema({
// 文章id
aid:{
type:mongooes.Schema.Types.ObjectId,
ref:'Article'
},
uid:{
type:mongooes.Schema.Types.ObjectId,
ref:'User'
},
time:{
type:Date
},
content:{
type:String
}
});

// 创建评论集合
const Comment = mongooes.model('Comment',commentSchema);
module.exports = {
Comment
}

修改用户登录功能

只有超级管理员能够 进入后台管理系统

  1. 登陆时判断角色 如果role 是normal 直接跳转 到 home

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
     if (isValid) {
    req.session.username = user.username; // 将用户名存储到 req的session 对象中 cookie中
    // 存储用户角色
    req.session.role = user.role;

    // 重定向到用户列表页面
    req.app.locals.userInfo = user;
    // 对用户角色进行判断
    if(user.role == 'admin') {
    // 重定向到用户列表也买你
    res.redirect('/admin/user')
    } else {
    // 博客首页
    res.redirect('/home')
    }

    } else {
    // 没有查询到用户
    res.status(400).render('admin/error', { msg: '用户名或者密码错误' })
    }
  2. 登陆拦截 在登录拦截文件里 也要判断 用户角色

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const guard = (req, res,next) => {
    // 判断用户是否是登录状态
    // 判断用户的登录状态
    // 如果是登录的 将请求放行
    // 如果不是登录的 将请求重定向到登录页面
    if(req.url != '/login' && !req.session.username) {
    res.redirect('/admin/login')
    } else {
    // 用户是登陆状态 将请求放行
    if(req.session.role == 'normal') {
    // 如果是普通用户 就跳转 /home
    return res.redirect('/home/')
    }
    next()
    }
    }

    module.exports = guard
  3. 用户评论模块的显示与隐藏

    如果有 locals.userInfo 就显示 没有就隐藏

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {{if userInfo}}
    <h4>评论</h4>

    <form class="comment-form">
    <textarea class="comment"></textarea>
    <div class="items">
    <input type="submit" value="提交">
    </div>
    </form>
    {{else}}
    <div><h2>请先登录,再进行评论</h2></div>
    {{/if}}

    但要在 用户退出时清除 userInfo信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    module.exports = (req, res) => {
    // 删除session
    req.session.destroy(function () {
    // 删除cookie
    res.clearCookie('connect.sid');
    // 重定向到用户登录页面
    res.redirect('/admin/login');
    // 清楚模板中的用户信息
    req.app.locals.userInfo = null
    });
    }

评论添加功能

设置标单提交地址 以及提交方式

1
2
3
4
5
6
7
8
<form class="comment-form" action="/home/comment" method="post">
<textarea class="comment" name="content"></textarea>
<input type="hidden" name="uid" value="{{@userInfo._id}}">
<input type="hidden" name="aid" value="{{@art._id}}">
<div class="items">
<input type="submit" value="提交">
</div>
</form>

设置路由

image-20210222092524848

image-20210222092539387

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 导入评论集合构造函数
const {Comment} = require('../../model/comment');

module.exports = (req, res) => {
const {uid,aid,content} = req.body;

// 将评论信息存储到评论集合中
Comment.create({
content:content,
uid:uid,
aid:aid,
time:new Date()
})
// 将页面重定向会文章详情页面
res.redirect('/home/article?id=' + aid)
}

数据库规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 引入 mongoes
const mongoose = require('mongoose');
// 创建评论集合规则
const commentSchema = new mongoose.Schema({
// 文章id
aid:{
type:mongoose.Schema.Types.ObjectId,
ref:'Article'
},
uid:{
type:mongoose.Schema.Types.ObjectId,
ref:'User'
},
time:{
type:Date
},
content:{
type:String
}
});

// 创建评论集合
const Comment = mongoose.model('Comment',commentSchema);
module.exports = {
Comment
}

评论展示

在article详情路由中 引入 comment 集合构造函数 然后根据文章id 查询 相应评论

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const {Article} = require('../../model/article');
// 导入评论集合构造函数
const {Comment} = require('../../model/comment');
module.exports = async (req,res) => {

const id = req.query.id;

let art = await Article.findOne({_id:id}).populate('author');
let com = await Comment.find({aid:id}).populate('uid').sort({'time':-1}); //sort排序
// 这里使用了 解构方法
let [strA,strC] = [JSON.stringify(art),JSON.stringify(com)];
[art,com] = [JSON.parse(strA),JSON.parse(strC)];

res.render('home/article',{art,com})
}