项目实战:博客项目
一、项目概括
1 功能描述
设计一个博客文章管理系统,使之具有管理员登录验证、密码加密、显示、新增、修改、删除、查询用户和文章、数据分页、退出登录等功能。
2 具体设计信息
3 技术栈选择
mongodb + express框架 + node.js
4 项目文件结构
二、案例初始化
1. 建立项目所需文件夹
-
public 静态资源 model 数据库操作 route 路由 views 模板
2. 初始化项目描述文件
-
使用 npm init -y 生成 package.json 文件
3. 下载项目所需第三方模块
npm install express mongoose art-template express-art-template
4. 创建网站服务器
-
app.js 项目入口文件项目主文件
// 引入 express 框架 const express = require(express); // 创建网站服务器 const app = express(); // 监听80端口 app.listen(80); console.log(服务器已启动);
5. 构建模块化路由
-
route/home.js 创建博客展示页面路由
// 引入 express 框架 const express = require(express); //创建博客展示页面路由对象 const home = express.Router(); home.get(/home, (req, res) => { res.send(欢迎来到博客首页); }); //将路由对象作为模块成员进行导出 module.exports = home;
-
route/admin.js 创建博客管理页面路由
// 引入 express 框架 const express = require(express); //创建博客展示页面路由对象 const admin = express.Router(); //创建登录路由 admin.get(/login, require(./admin/loginPage)); //实现登录功能 admin.post(/login, require(./admin/login)); //创建用户列表路由 admin.get(/user, require(./admin/userPage)); //实现退出功能 admin.get(/logout, require(./admin/logout)); //创建用户编辑页面路由 admin.get(/user-edit, require(./admin/user-edit)); //创建实现用户添加功能路由 admin.post(/user-edit, require(./admin/user-edit-fn)); //实现修改用户信息功能 admin.post(/user-modify, require(./admin/user-modify)); //实现删除用户功能路由 admin.get(/delete, require(./admin/user-delete)); //将路由对象作为模块成员进行导出 module.exports = admin;
-
将home、admin路由引入到app.js中
//引入路由模块 const home = require(./route/home); const admin = require(./route/admin); //将路由和请求路径进行匹配 app.use(/home, home); app.use(/admin, admin);
6. 构建博客管理页面模板
需要注意:
- 静态资源的外链文件,是浏览器解析的,所以相对路径是相对于浏览器的请求地址。
<link href="/admin/css/boot-crm.css" rel="stylesheet" type="text/css" />
- 模板的路径是由模板引擎来解析的,所以写相对路径是没有问题的。
{ { include ./common/header}}
三、登录功能
1. 创建用户集合,初始化用户
1.1 连接数据库
model/connect.js,代码如下:
//连接数据库 // 引入系统模块mongoose const mongoose = require(mongoose); // 数据库连接 27017是mongodb数据库的默认端口 mongoose.connect(mongodb://localhost/blog, { useNewUrlParser: true, useUnifiedTopology: true }) .then(() => console.log(数据库连接成功)) //连接成功 .catch((err) => console.log(err, 数据库连接失败)); //连接失败
1.2 创建用户集合
model/user.js,代码如下:
const mongoose = require(mongoose); //创建用户集合规则 const userSchema = new mongoose.Schema({ username: { type: String, //true表示title 属性必传;后面的是错误提示内容 required: [true, 请传入用户名], //最小长度是2,最大长度是5 minlength: [2, 姓名长度不能小于2], maxlength: [20, 姓名长度最大不能超过20] }, email: { type: String, //保证邮箱地址在插入数据库时不重复 unique: true, required: true }, password: { type: String, required: true }, role: { type: String, required: true }, //0 启用状态 //1 禁用状态 state: { type: Number, default: 0 }, }); //使用规则,创建集合,返回集合构造函数(参数一:集合名称;参数二:集合规则) const User = mongoose.model(User, userSchema); //将用户集合作为模块成员进行导出 module.exports = { User };
1.3 初始化用户
1.4 mongodb下的数据库结构
2. 登录验证
2.1 验证步骤
- 为登录表单项设置请求地址、请求方式以及表单项name属性。
<form action="/admin/login" method="post" id="loginForm"> <div class="form-group"> <label for="exampleInputEmail1">邮箱:</label> <input type="text" class="form-control" id="usercode" placeholder="请输入邮箱地址" name="email"> </div> <div class="form-group"> <label for="exampleInputPassword1">密码:</label> <input type="password" class="form-control" id="userpassword" placeholder="请输入密码" name="password"> </div> <div class="form-group"> <div class="checkbox"> <label> <input type="checkbox">记住密码 </label> </div> </div> <div class="form-group"> <button type="submit" class="btn btn-primary" id="btn">立即登录</button> </form>
- 当用户点击登录按钮时,客户端验证用户是否填写了登录表单,如果邮箱或密码没有输入,阻止表单提交。
<script type="text/javascript"> //为表单添加提交事件 $(#loginForm).on(submit, function() { //获取到表单中用户输入的内容 var result = serializeToJson($(this)); //如果用户没有输入邮件地址的话 if(result.email.trim().length == 0){ alert(请输入邮件地址); //阻止程序向下执行 return false; } //如果用户没有输入密码 if(result.password.trim().length == 0){ alert(请输入密码); //阻止程序向下执行 return false; } }) </script>
- 服务器端接收请求参数,验证用户是否填写了登录表单,如果邮箱或密码没有输入,为客户端做出相应,阻止程序向下执行。
//接收请求参数 //解构 出 email password const { email, password } = req.body; //如果用户没有输入邮箱地址 if (email.trim().length == 0 || password.trim().length == 0) { return res.status(400).render(admin/error, { msg: 邮件地址或者密码错误 });
- 根据邮箱地址查询用户信息。如果用户不存在,为客户端做出响应,阻止程序向下执行;如果用户存在,将密码进行比对,比对成功->登陆成功;比对失败->登陆失败。
let user = await User.findOne({ email }); //查询到了用户 if (user) { // 将客户端传递过来的密码和用户信息中的密码进行比对 if (password == user.password) { //登陆成功 //将用户名存储在请求对象中 req.session.username = user.username; req.app.locals.userInfo = user; //重定向到用户列表页面 res.redirect(/admin/user); } else { res.status(400).render(admin/login, { msg: 邮箱地址或密码错误 }); } } else { //没有查询到用户 res.status(400).render(admin/login, { msg: 邮箱地址或密码错误 }); }
2.2 密码加密bcrypt
bcrypt依赖的其他环境 python 3.8.3 node-gyp npm install -g node-gyp windows-build-tools npm install – global – production windows-build-tools
2.3 cookie与session
cookie: 浏览器在电脑中硬盘中开辟的一块空间,主要供服务器端存储数据。 cookie中的数据是以域名的形式进行区分的。 cookie中的数据是有过期时间的,超过时间数据会被浏览器自动删除。 cookie中的数据会随着请求被自动发送到服务器端。
session: 实际上就是一个对象,存储在服务器端的内存中,在session对象中也可以存储多条数据,每一条数据都有一个sessionid作为唯一标识。
3. 退出系统
① 建立退出功能的路由。
//实现退出功能 admin.get(/logout, require(./admin/logout));
② 实现退出功能
module.exports = (req, res) => { //删除session req.session.destroy(function() { //删除cookie res.clearCookie(connext.sid); //重定向到用户登录页面 res.redirect(admin/login); }) }
四、 用户管理功能
1. 新增用户
① 为用户列表页面的新增用户按钮添加链接。
<a href="/admin/user-edit" class="btn btn-primary" >新建用户</a>
② 添加一个链接对应的路由,在路由处理函数中渲染新增用户模板。
//创建用户编辑页面路由 admin.get(/user-edit, require(./admin/user-edit));
module.exports = async(req, res) => { //添加操作 res.render(admin/user-edit, { message: message, link: /admin/user-edit, button: 添加 }); };
③为新增用户表单制定请求地址、请求方式、为表单项添加name属性。
<form class = "from-container" action="/admin/user-edit" method="post"> <div class="form-group"> <label for="">用户名</label> <input type="text" class="form-control" placeholder="请填写用户名" name="username"> </div> <div class="form-group"> <label for="">邮箱</label> <input type="text" class="form-control" placeholder="请填写邮箱地址" name="email"> </div> <div class="form-group"> <label for="">密码</label> <input type="password" class="form-control" placeholder="请输入密码" name="password"> </div> <div class="form-group"> <label for="">角色</label> <select name="role" id="normal" class="form-control"> <option value="normal">普通用户</option> <option value="admin">超级管理员</option> </select> </div> <div class="form-group"> <label for="">状态</label> <select name="state" id="normal" class="form-control"> <option value="0">启用</option> <option value="1">禁用</option> </select> </div> <button type="submit" class="btn btn-primary">添加用户</button> </form>
④ 增加实现添加用户的功能路由。
//创建实现用户添加功能路由 admin.post(/user-edit, require(./admin/user-edit-fn));
⑤接收到客户端传递过来的请求参数。
res.send(req.body);
⑥ 对请求参数的格式进行验证。 使用 npm install joi 命令下载第三方模块Joi。
//引入joi模块 const Joi = require(joi); module.exports = async(req, res) => { //定义对象的验证规则 const schema = { //必须按字段 要加 required 方法 //.error 自定义错误信息 username: Joi.string().min(2).max(12).required().error(new Error(用户名不符合验证规则)), email: Joi.string().email().required().error(new Error(邮箱格式不符合验证规则)), password: Joi.string().regex(/^[a-zA-z0-9]{3,30}$/).required().error(new Error(密码格式不符合验证规则)), //客户端必须 传入 noemal 或者 admin,传入其他的均是错的 role: Joi.string().valid(normal, admin).required().error(new Error(角色不符合验证规则)), state: Joi.number().valid(0, 1).required().error(new Error(状态值不符合验证规则)), }; try { //实施验证 await Joi.validate(req.body, schema); } catch (error) { //验证没有通过 //重定向回用户添加页面 res.redirect(`/admin/user-edit?message=${ error.message}`); return; } console.log(验证通过); res.send(req.body); };
⑦ 验证当前要注册的邮箱地址是否已经注册过。
//根据邮箱地址查询用户是否存在 let user = await User.findOne({ email: req.body.email }); //如果用户已经存在 邮箱地址已经被别人占用 if (user) { //重定向回用户添加页面 return res.redirect(`/admin/user-edit?message=邮箱地址已经被占用`); }
⑧ 对密码进行加密处理。 ⑨ 优化请求处理代码和错误处理代码,将其放到单独的js文件中。
2. 修改用户
① 将要修改的用户ID传递到服务器。
<a href="/admin/user-edit?id={ {@$value._id}}" class="btn btn-success btn-xs">修改</a>
② 建立用户信息修改功能对应的路由。
//实现修改用户信息功能 admin.post(/user-modify, require(./admin/user-modify));
③ 接收客户端表单传递过来的请求参数;根据客户端id查询用户信息,并将客户端传递过来的密码和数据库中的密码进行比对;如果比对失败,对客户端做出相应;如果比对成功,将用户信息更新到数据库中。
//导入用户集合的构造函数 const { User } = require(../../model/user); module.exports = async(req, res) => { //接收客户端传递过来的请求参数 const { username, email, role, state } = req.body; //即将要修改的用户id const id = req.query.id; //let user = await User.findOne({ _id: id }); //将用户信息更新到数据库中 await User.updateOne({ _id: id }, { username: username, email: email, role: role, state: state }); res.redirect(/admin/user); };
3. 删除用户
① 在确认删除框中添加隐藏域用以存储要删除用户的ID值。
<!-- 删除确认弹出框 开始--> <div class="modal fade confire-modal"> <div class="modal-dialog modal-lg"> <form action="" class="modal-content"> <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"> </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> <!-- 删除确认弹出框 结束-->
② 为删除按钮添加自定义属性用以存储要删除用户的ID值。
<a href="#" class="btn btn-danger btn-xs delete" data-id="{ {@$value._id}}" data-toggle="modal" data-target=".confire-modal">删除</a>
③ 为删除按钮添加点击事件,在点击事件处理函数中获取自定义属性中的ID值并存储在表单的隐藏域中。
{ { block script}} <!-- 编写js代码 --> <script type="text/javascript"> //删除用户 $(.delete).on(click, function() { //获取用户id var id = $(this).attr(data-id); alert(id); //将要删除的用户id存储在隐藏域中 $(#deleteUserId).val(id); }) </script> { { /block}} { { /block}}
④ 为删除表单添加提交地址以及提交方式。
<form action="/admin/delete" method="get" class="modal-content">
⑤ 在服务器端建立删除功能路由,接收客户端传递过来的id参数,根据id删除用户。
//导入用户集合的构造函数 const { User } = require(../../model/user); module.exports = async(req, res) => { //获取要删除的用户id const id = req.query.id; //将用户信息从数据库中删除 await User.findByIdAndDelete({ _id: id }); res.redirect(/admin/user); };
4. 数据分页
① 创建数据分页路由(相关代码放在展示页面路由中)。
//导入用户集合的构造函数 const { User } = require(../../model/user); module.exports = async(req, res) => { //接收客户端传递过来的当前页参数 let page = req.query.page || 1; //每一页显示的数据条数 let pagesize = 10; //查询用户数据的总数 let count = await User.countDocuments({ }); //总页数 let total = Math.ceil(count / pagesize); //页码对应的数据查询开始位置 let start = (page - 1) * pagesize; //将用户信息从数据库中查询出来 let users = await User.find({ }).limit(pagesize).skip(start); //渲染用户列表模板 res.render(admin/user, { users: users, page: page, total: total }); };
② user.art 模板中对应的分页代码:
<!--分页开始--> <nav aria-label="Page navigation"> <ul class="pagination"> <!-- 上一页 --> <li style="display:<%= page-0-1 < 1 ? none : inline %>"> <a href="/admin/user?page=<%=page-1 %>" aria-label="Previous"> <span aria-hidden="true">«</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 %>" aria-label="Next"> <span aria-hidden="true">»</span> </a> </li> </ul> </nav> <!--分页结束-->
五、部分测试结果展示
- 登录验证 ①未填写密码: ②未填写邮箱: ③邮箱地址或密码错误
- 显示用户信息
- 新增用户
- 删除用户:将 哈利波特 删除
- 数据分页
- 退出系统