jquery+nodejs-博客项目
1.初始化
1 | 外链css等写绝对路径 (浏览器解析 相对于 浏览器地址) |
1 | 1. 建立项目所需文件夹 |
1.1 路由模块化
新建文件夹 route 里面新建 admin.js 与 home.js
之后 设置请求方式 和做出响应 admin页面同理
1 | // 引入 express |
app.js 里进行引用
1 | // 引入 express |
1.2 构建页面模板文件
在app.js里指定 模板引擎的一些信息
1 | // path 模块 |
之后在 相应的路由模块里 直接写文件名就可以了
1 | // 引入 express |
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 | <!-- 头部 --> |
1.5抽离 html 骨架
在common 目录下 新建 layout.art 把html骨架以及 一些页面相同的代码放进去
1 |
|
这里的 block 时预留 以后要插入代码的 位置
1 | {{block 'link'}} {{/block}} <!-- 挖坑 --> |
之后 在 把其他页面的 抽离部分删除并引入骨架 这里 以 user.art 做例子
1 | {{block '对应的名字填坑'}} |
1 | {{extend './common/layout.art'}} <!-- // 引入骨架 --> |
2.用户登录
1 | 7. 根据邮箱地址查询用户信息 |
连接数据库,在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('数据库连接失败');
})创建 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
}; // 开放出去 使用对象格式可以多项开放表单设置提交地址以及方式和验证
这里提交地址是 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
12function serializeJson(form) {
var result = {};
// 获取表单中用户输入的内容
// [{name: 'email',value: '用户输入的内容'},{}]
var f = form.serializeArray();
f.forEach(function (item) {
result[item.name] = item.value;
})
return result;
}之后在模板骨架文件 中 引入js文件
之后在 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>实现登录
先实现路由
修改表单提交地址
安装 模块
1
2
3
4cnpm 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: '用户名或者密码错误'
})
}
})查询用户实现是否可以登录
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: '用户名或者密码错误' })
}
})密码加密 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
13async 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: '用户名或者密码错误' })
}
})cookie 与session
cookie:浏览器在电脑硬盘中开辟的一块空间,主要供服务器端存储数据。
lcookie中的数据是以域名的形式进行区分的。
lcookie中的数据是有过期时间的,超过时间数据会被浏览器自动删除。
lcookie中的数据会随着请求被自动发送到服务器端。
session:实际上就是一个对象,存储在服务器端的内存中,在session对象中也可以存储多条数据,每一条数据都有一个sessionid做为唯一标识。
在node.js中需要借助express-session实现session功能。
1
cnpm install express-session //下载模块
1
2
3const 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路由中存储用户信息
之后调用信息
app.locals 存储信息 模板中能直接拿到
在头部模板中直接拿到
请求拦截
如果用户在 地址栏中输入 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()
}
})实现退出功能
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');
});
})代码优化
路由代码放到 各自的 js文件中
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属性 数据格式验证
先设置请求地址
然后设置路由
1 | // 创建用户信息编辑 |
之后利用第三方模块 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
})
}
2.验证邮箱是否存在
当添加用户时 先到数据库查询 邮箱地址是否已经被占用
1 | // 根据邮箱地址查询用户是否存在 |
如果没有占用 对提交的密码进行加密 添加到数据库中 然后重定向user列表页面
1 | // 对密码进行加密 |
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 | const {User} = require('../../model/user') |
1 | {{each userlist}} |
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>«</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>»</span>
</a>
</li>
</ul>
4.修改用户
1.判断是否是修改操作
当点击修改按钮时
首先要给 按钮 传递一个 数据库 _id 属性
这样做
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 | const { User } = require('../../model/user'); |
3.删除功能
1
2
3
4
5
6
7
8 1. 在确认删除框中添加隐藏域用以存储要删除用户的ID值
2. 为删除按钮添自定义属性用以存储要删除用户的ID值
3. 为删除按钮添加点击事件,在点击事件处理函数中获取自定义属性中存储的ID值并将ID值存储在表单的隐藏域中
4. 为删除表单添加提交地址以及提交方式
5. 在服务器端建立删除功能路由
6. 接收客户端传递过来的id参数
7. 根据id删除用户
在确认删除框中添加隐藏域用以存储要删除用户的ID值
1
2
3
4 <div class="modal-body">
<p>您确定要删除这个用户吗?</p>
<input type="hidden" name="id" id="deletUserId">
</div>为删除按钮添自定义属性用以存储要删除用户的ID值
1
2 <i class="glyphicon glyphicon-remove delete" data-toggle="modal" data-target=".confirm-modal"
data-id="{{@$value._id}}"></i>为删除按钮添加点击事件,在点击事件处理函数中获取自定义属性中存储的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>为删除表单添加提交地址以及提交方式
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>×</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>在服务器端建立删除功能路由
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')
}接收客户端传递过来的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')
}根据id删除用户
1 await User.findOneAndDelete({ _id: req.query.id });
5.文章管理功能实现
1.显示文章管理页面 与 页面编辑页面
设置对应路由 然后 把不同的模板响应就可以了
设置对应路由
文章列表页面 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)
})
}
实现文件上传 与 预览
var reader = new FileReader(); reader.readAsDataURL('文件'); reader.onload = function () { console.log(reader.result); }
> > 预览 > > 文件上传后把读取编码 放到 img标签的src 里面 > >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)
> })
>
> }> >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
18var 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 | // 引入 formidable 第三方模块 |
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>«</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>»</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.开发环境与生产环境
什么是开发环境与生产环境
环境,就是指项目运行的地方,当项目处于开发阶段,项目运行在开发人员的电脑上,项目所处的环境就是开发环境。当项目开发完成以后,要将项目放到真实的网站服务器电脑中运行,项目所处的环境就是生产环境。
为什么要区分开发环境与生产环境
因为在不同的环境中,项目的配置是不一样的,需要在项目代码中判断当前项目运行的环境,根据不同的环境应用不同的项目配置。
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"
}
}
9.主界面
1.配置路由
配置路由 方法跟用户界面高差不多
2.渲染模板
在进入 home路由时查询数据库 把数据 渲染到模板上
1 | const {Article} = require('../../model/article'); |
需要注意的地方
模板里 这里字符去除 html标签 以及 截取
1 | <div class="brief"> |
接下来就是点击页面 渲染数据
原理差不多
10.文章评论
1.创建评论集合
2.判断用户是否登录,如果用户登录,再允许用户提交评论表单
3.在服务器端创建文章评论功能对应的路由
4.在路由请求处理函数中接收客户端传递过来的评论信息
5.将评论信息存储在评论集合中
6.将页面重定向回文章详情页面
7.在文章详情页面路由中获取文章评论信息并展示在页面中
创建集合
1 | // 引入 mongoes |
修改用户登录功能
只有超级管理员能够 进入后台管理系统
登陆时判断角色 如果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: '用户名或者密码错误' })
}登陆拦截 在登录拦截文件里 也要判断 用户角色
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用户评论模块的显示与隐藏
如果有 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>设置路由
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})
}