目录
项目效果
项目的搭建
编辑
响应静态网页
编辑
编辑
结合MongoDB数据库
结合API接口
进行会话控制
项目效果
该案例实现账单的添加删除查看,用户的登录注册。功能比较简单,但是案例主要是使用前段时间学习的知识进行实现的,主要包括express服务的搭建以及使用,并结合MongoDB数据库对数据进行存储以及操作,同时编写相应的API接口,最后进行会话控制,确保数据的安全。如果以下的某一些部分感觉不太理解的话可以看我之前对应的文章。案例中使用到的知识点都是前面文章有涉及到的。
项目的搭建
首先我们直接使用express-generator来快速地搭建express应用骨架,输入命令行:express -e 文件名 然后使用npm i来进行项目依赖的下载。完成之后可以在package.json中对运行的命令 "start": "node./bin/www" 修改为 "start": "nodemon ./bin/www",这样后续的内容修改服务器就会自动地运行了。接下来运行:npm start,进行服务的启动。服务默认是监听3000端口,我们输入http://127.0.0.1:3000进行访问。出现以下页面即服务搭建成功。
接下来,我们需要对路由规则进行配置,我们可以app.js文件中进行查看,app.use('/', indexRouter);我们可以找到对应的路由导入var indexRouter = require('./routes/index'); 因此我们在routes文件夹下面的index.js文件进行路由的配置。
//index.jsvar express = require('express');
var router = express.Router();// 记账本列表
router.get('/account', function(req, res, next) {res.send('账单列表');
});// 记账本列表添加
router.get('/account/create', function(req, res, next) {res.send('添加记录');
});module.exports = router;
输入不同的路径得到不同的结构:
响应静态网页
我们事先准备好了两个页面,一个为账单页面,一个为添加页面。我们借助res.rend()可以对ejs中的内容响应给浏览器的功能来进行操作。在views文件夹下面创建两个ejs文件。将账单页面以及添加页面加入。
//list.ejs<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><linkhref="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css"rel="stylesheet"/><style>label {font-weight: normal;}.panel-body .glyphicon-remove{display: none;}.panel-body:hover .glyphicon-remove{display: inline-block}</style></head><body><div class="container"><div class="row"><div class="col-xs-12 col-lg-8 col-lg-offset-2"><h2>记账本</h2><hr /><div class="accounts"><div class="panel panel-danger"><div class="panel-heading">2023-04-05</div><div class="panel-body"><div class="col-xs-6">抽烟只抽煊赫门,一生只爱一个人</div><div class="col-xs-2 text-center"><span class="label label-warning">支出</span></div><div class="col-xs-2 text-right">25 元</div><div class="col-xs-2 text-right"><spanclass="glyphicon glyphicon-remove"aria-hidden="true"></span></div></div></div><div class="panel panel-success"><div class="panel-heading">2023-04-15</div><div class="panel-body"><div class="col-xs-6">3 月份发工资</div><div class="col-xs-2 text-center"><span class="label label-success">收入</span></div><div class="col-xs-2 text-right">4396 元</div><div class="col-xs-2 text-right"><spanclass="glyphicon glyphicon-remove"aria-hidden="true"></span></div></div></div></div></div></div></div></body>
</html>
//create.ejs
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>添加记录</title><linkhref="/css/bootstrap.css"rel="stylesheet"/><link href="/css/bootstrap-datepicker.css" rel="stylesheet"></head><body><div class="container"><div class="row"><div class="col-xs-12 col-lg-8 col-lg-offset-2"><h2>添加记录</h2><hr /><form method="post" action="/account"><div class="form-group"><label for="item">事项</label><inputname="title"type="text"class="form-control"id="item"/></div><div class="form-group"><label for="time">发生时间</label><inputname="time"type="text"class="form-control"id="time"/></div><div class="form-group"><label for="type">类型</label><select name="type" class="form-control" id="type"><option value="-1">支出</option><option value="1">收入</option></select></div><div class="form-group"><label for="account">金额</label><inputname="account"type="text"class="form-control"id="account"/></div><div class="form-group"><label for="remarks">备注</label><textarea name="remarks" class="form-control" id="remarks"></textarea></div><hr><button type="submit" class="btn btn-primary btn-block">添加</button></form></div></div></div><script src="/js/jquery.min.js"></script><script src="/js/bootstrap.min.js"></script><script src="/js/bootstrap-datepicker.min.js"></script><script src="/js/bootstrap-datepicker.zh-CN.min.js"></script><script src="/js/main.js"></script></body>
</html>
然后我们修改原本的路由,让其输入/account时为账单页表页面,输入/account/create时为列表添加页面。
// 记账本列表
router.get('/account', function(req, res, next) {res.render('list');
});// 记账本列表添加
router.get('/account/create', function(req, res, next) {res.render('create');
});
结合MongoDB数据库
接下来我们结合前几篇文章我们学习到的MongoDB数据库来对数据的添加,删除以及读取等操作。如果不懂这一步操作的小伙伴看完前面发的文章。
我们在我们项目中创建三个文件夹:config,db以及models。config文件用于对配置进行统一的设置,在里面单独地设置域名端口以及数据库名等信息。db文件夹中创建db.js,主要进行导入mongoose,连接mongoose服务,设置成功以及失败的回调等信息。module用于创建文档结构对象。
//config.js
module.exports={DBHOST:'127.0.0.1',DBPORT:27017,DBNAME:'bilibili',secret:'atguigu'
}
//db.js
module.exports = function (success, error) {//导入配置文件const {DBHOST,DBPORT,DBNAME}=require('../config/config');//导入mongooseconst mongoose = require('mongoose');//连接mongodb服务mongoose.connect(`mongodb://${DBHOST}:${DBPORT}/${DBNAME}`);mongoose.connection.once('open', () => {success();})//设置连接错误的回调mongoose.connection.on('error', () => {error();});//设置连接关闭的回调mongoose.connection.on('close', () => {console.log('连接关闭');});
}
接着再www文件中导入db.js中的函数,在文件中调用函数,传入两个函数,成功的回调以及失败的回调,成功的回调直接写原本启动http服务的代码,确保数据库连接成功之后再启动http服务。失败的回调我们直接输出失败即可。
//www
const db = require('../db/db');
//连接上数据库再来启动http服务
db(()=>{//原本文件中的代码}
},()=>{console.log("连接失败")
})
接着想要操作数据库,我们需要先准备好模型文件:
//models/AccountModel.js
const mongoose = require('mongoose');
//创建文档结构对象
let AccountSchema = new mongoose.Schema({title: {type: String,required: true},time: Date,type: {type: Number,required: true},account: {type: Number,required: true},remarks: {type: String}
});
//创建文档模型对象
let AccountModel = mongoose.model('accounts', AccountSchema);
//暴露模型对象
module.exports = AccountModel;
模型文件准备好之后,我们在对路由文件,routes/index.js文件来进行操作,在原本的文件中添加新增数据的操作:
//新增记录
router.post('/account', function(req, res, next) {AccountModel.create({...req.body,time:moment(req.body.time).toDate()}).then(data=>{res.render('success',{msg:'添加成功~~',url:'/account'});}).catch(err=>{res.status(500).send('插入失败~~');})
});
这里面使用到了一个moment包,主要是用于对日期进行转换为对象形式方便后续的操作。需要在文件中导入const moment=require('moment');
接着我们添加账单,可以使用数据库可视化工具看到自己添加的数据,我使用的是Navicat。新建连接选择Mongo,连接成功之后就可以看到对应的数据库,以及相应的集合。
以上就是我们所添加的数据,但是我们需要在页表页面上也可以看到对应的数据,我们在路由配置文件中进行读取数据的操作。
// 记账本列表
router.get('/account',function(req, res, next) {//获取所有账单信息AccountModel.find().sort({time:-1}).exec().then(data=>{res.render('list',{accounts:data,moment:moment});}).catch(err=>{res.status(500).send('读取失败~~')})});
接着需要修改list.ejs中的代码,方便对数据进行展示:
<!DOCTYPE html>
<html lang="en"><body><div class="container"><div class="row"><div class="col-xs-12 col-lg-8 col-lg-offset-2"><div class="row text-right"><div class="col-xs-12" style="padding-top: 20px;"><form action="/logout" method="post"><button class="btn btn-danger">退出</button></form></div></div><hr><div class="row"><h2 class="col-xs-6">记账本</h2><h2 class="col-xs-6 text-right"><a href="/account/create" class="btn btn-primary">添加账单</a></h2></div><hr /><div class="accounts"><% accounts.forEach(item =>{ %><div class="panel <%= item.type=== -1 ? 'panel-danger':'panel-success' %>"><div class="panel-heading"><%= moment(item.time).format('YYYY-MM-DD') %></div><div class="panel-body"><div class="col-xs-6"><%= item.title %></div><div class="col-xs-2 text-center"><span class="label <%= item.type=== -1 ? 'label-warning':'label-success' %>"><%= item.type=== -1 ? '支出':'收入' %></span></div><div class="col-xs-2 text-right"><%= item.account %> 元</div><div class="col-xs-2 text-right"><a class="delBtn" href="/account/<%= item._id %>"><spanclass="glyphicon glyphicon-remove"aria-hidden="true"></span></a></div></div></div><% }) %> </div></div></div></div></body>
</html>
接着我们来对数据进行删除操作,我们在list.ejs中对叉号绑定一个事件,当用户确定删除时再进行删除数据,防止数据误删。
<div class="col-xs-2 text-right"><a class="delBtn" href="/account/<%= item._id %>"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a></div><script>let delBtns=document.querySelectorAll('.delBtn');delBtns.forEach(item =>{item.addEventListener('click',function(e){if(confirm('您确定要删除该文档吗?')){return true;}else{e.preventDefault();}})})</script>
并在路由中设置删除的操作:
//删除记录
router.get('/account/:id',(req,res)=>{//获取params 的 id参数let id=req.params.id;//删除AccountModel.deleteOne({_id:id}).then(data=>{res.render('success',{msg:'删除成功~~',url:'/account'});}).catch(err=>{res.status(500).send('删除失败~~')})});
结合API接口
当我们需要将项目推广到更多的客户端程序时,而不仅仅是局限在我们的浏览器进行访问时,我们就需要为它添加对应的API接口,同样的前几篇文章也有介绍了API接口的详细内容。如果不是太了解的小伙伴可以回头去看看。
为了更好地区分,我们在routes文件夹下创建一个名为web的文件,将原本的路由配置文件放入其中,再创建一个名为api的文件夹,创建一个名为account.js的文件用于存放API路由的配置。文件路径修改之后需要修改引入该文件的路径。并在app.js中导入并使用:
const accountRouter=require('./routes/api/account');app.use('/api',accountRouter);
在api的路由文件中实现创建账单接口、删除账单接口、获取单条数据接口以及更新账单接口。对应代码如下:
// /api/account.js
const express = require('express');
const jwt = require('jsonwebtoken');
//导入moment
const moment = require('moment');
const AccountModel = require('../../models/AccountModel');
//导入中间件
let checkTokenMiddleware = require('../../middlewares/checkTokenMiddleware')
const router = express.Router();// 记账本列表
router.get('/account', checkTokenMiddleware,function (req, res, next) {AccountModel.find().sort({ time: -1 }).exec().then(data => {res.json({//响应码 code: '0000',//响应信息msg: '读取成功',//响应数据data: data})}).catch(err => {res.json({//响应码 code: '1001',//响应信息msg: '读取失败',//响应数据data: null})})});// 记账本列表添加
router.get('/account/create',checkTokenMiddleware, function (req, res, next) {res.render('create');
});//新增记录
router.post('/account', checkTokenMiddleware,function (req, res) {AccountModel.create({...req.body,time: moment(req.body.time).toDate()}).then(data => {res.json({//响应码 code: '0000',//响应信息msg: '创建成功',//响应数据data: data})}).catch(err => {res.json({//响应码 code: '1002',//响应信息msg: '创建失败',//响应数据data: null})})});//删除记录
router.delete('/account/:id', checkTokenMiddleware,(req, res) => {//获取params 的 id参数let id = req.params.id;//删除AccountModel.deleteOne({ _id: id }).then(data => {res.json({//响应码 code: '0000',//响应信息msg: '删除成功',//响应数据data: {}})}).catch(err => {res.json({//响应码 code: '1003',//响应信息msg: '删除失败',//响应数据data: null})})
});
//获取当个账单信息
router.get('/account/:id', checkTokenMiddleware,(req, res) => {//获取params 的 id参数let id = req.params.id;//查询数据库AccountModel.findById(id).then(data => {res.json({//响应码 code: '0000',//响应信息msg: '读取成功',//响应数据data: data})}).catch(err => {res.json({//响应码 code: '1004',//响应信息msg: '读取失败',//响应数据data: null})})
});//更新单个账单信息
router.patch('/account/:id', checkTokenMiddleware,(req, res) => {//获取params 的 id参数let id = req.params.id;AccountModel.updateOne({ _id: id }, req.body).then(data => {//再次查询数据库AccountModel.findById(id).then(data => {res.json({//响应码 code: '0000',//响应信息msg: '更新成功',//响应数据data: data}).catch(err => {res.json({//响应码 code: '1004',//响应信息msg: '读取失败',//响应数据data: null})})}).catch(err => {res.json({//响应码 code: '1005',//响应信息msg: '更新失败',//响应数据data: null})})});})
module.exports = router;
那如何对我们写好的接口做测试呢,在前面的文章中,介绍了Apipost软件来测试接口,我们来尝试一下,发一个GET请求来获取表单的信息数据。成功得到对应的数据。
进行会话控制
接下来我们使用我们学过的session以及token来对数据进行保护。具体的知识点可以看我前几篇发的文章。
接下来,我们为项目添加一个注册的页面,在routes中web文件夹下创建一个auth.js文件,用户配置注册以及登录时的session相关信息。我们同样使用模板引擎来响应注册页面。将该创建好的路由文件在app.js中进行导入以及使用。
创建一个reg.ejs文件:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>注册</title><link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet" />
</head><body><div class="container"><div class="row"><div class="col-xs-12 col-md-8 col-md-offset-2 col-lg-4 col-lg-offset-4"><h2>注册</h2><hr /><form method="post" action="/reg"><div class="form-group"><label for="item">用户名</label><input name="username" type="text" class="form-control" id="item" /></div><div class="form-group"><label for="time">密码</label><input name="password" type="password" class="form-control" id="time" /></div><hr><button type="submit" class="btn btn-primary btn-block">注册</button></form></div></div></div>
</body></html>
在响应注册页面
//注册
router.get('/reg',(req,res)=>{res.render('auth/reg');
});
我们需要先创建用户模型,后续才能对其进行插入数据库以及设置session等操作。
const mongoose = require('mongoose');
//创建文档结构对象
let UserSchema = new mongoose.Schema({username:String,password:String});
//创建文档模型对象
let UserModel = mongoose.model('users', UserSchema);
//暴露模型对象
module.exports = UserModel;
将该模型导入到对应的配置文件中,并进行注册等相关操作:
/导入用户模型
const UserModel=require('../../models/UserModel');
const md5=require('md5');//注册用户
router.post('/reg',(req,res)=>{UserModel.create({...req.body,password:md5(req.body.password)}).then(data=>{res.render('success',{msg:'注册成功',url:'/login'});}).catch(err=>{res.status(500).send('注册失败')})
});
接下来实现用户登录功能,我们同样创建一个login.ejs文件放置登录页面模板,复制注册页面的代码,将对应的文字以及路径修改一下即可。这部分不进行代码展示。进行登录的相关操作,当用户登录之后,我们需要对他的session进行写入,并返回sessionid。
//登录
router.get('/login',(req,res)=>{res.render('auth/login');
});
//登录操作
router.post('/login',(req,res)=>{//获取用户名和密码let {username,password}=req.body;UserModel.findOne({username:username,password:md5(password)}).then(data=>{if(!data){return res.send('账号或者密码错误~~')}//写入sessionreq.session.username=data.username;req.session._id=data._id;//登录成功响应res.render('success',{msg:'登录成功',url:'/account'});}).catch(err=>{res.status(500).send('登录失败')})
});
这部分需要先安装express-session以及connect-mongo,并在app.js中进行导入,并设置中间件。
//导入 express-session
const session = require("express-session");
const MongoStore = require('connect-mongo');//导入配置项
const {DBHOST, DBPORT, DBNAME} = require('./config/config');
//设置 session 的中间件
app.use(session({name: 'sid', //设置cookie的name,默认值是:connect.sidsecret: 'atguigu', //参与加密的字符串(又称签名) 加盐saveUninitialized: false, //是否为每次请求都设置一个cookie用来存储session的idresave: true, //是否在每次请求时重新保存session 20 分钟 4:00 4:20store: MongoStore.create({mongoUrl: `mongodb://${DBHOST}:${DBPORT}/${DBNAME}` //数据库的连接配置}),cookie: {httpOnly: true, // 开启后前端无法通过 JS 操作maxAge: 1000 * 60 * 60 * 24 * 7 // 这一条 是控制 sessionID 的过期时间的!!!},
}))
当我们登录成功之后我们可以在数据库中看到我们对应的session信息。
写入之后,我们还需要判断用户是否登录,若用户没有进行登录则拒绝访问,跳转到登录页面。我们在web文件夹下的index.js中编写一个中间件。
//检测登录的中间件
const checkLoginMiddleware = (req, res, next) => {//判断if(!req.session.username){return res.redirect('/login');}next();}
并在下面的路由规则中使用它。这里只对查看记账本列表做演示,其他的一致。
// 记账本列表
router.get('/account',checkLoginMiddleware,function(req, res, next) {AccountModel.find().sort({time:-1}).exec().then(data=>{res.render('list',{accounts:data,moment:moment});}).catch(err=>{res.status(500).send('读取失败~~')})});
接下来继续在auth.js中实现退出登录功能。
//退出登录
router.post('/logout',(req,res)=>{//销毁sessionreq.session.destroy(()=>{res.render('success',{msg:'退出成功',url:'/login'})})
})
在退出登录界面,部分进行修改,防止CSRF跨站请求伪造。它会导致用户的session被获取。大部分的CSRF跨站请求伪造都是使用一个天生具有跨域能力的标签。但是它们发送的请求都是get请求,因此我们将原本的退出修改为post请求。可以防止发生。
<div class="col-xs-12" style="padding-top: 20px;"><form action="/logout" method="post"><button class="btn btn-danger">退出</button></form>
</div>
我们接着在web文件夹下的index.js对首页添加路由规则:
//添加首页路由规则
router.get('/',(req,res)=>{//重定向res.redirect('/account');
});
在app.js中添加404的响应的模板,在views中创建一个404.ejs文件。
// catch 404 and forward to error handler
app.use(function(req, res, next) {res.render('404');
});
以上的操作,我们使用了session对网页端进行了约束,接下来我们使用token来对接口来进行约束。在api文件夹下创建一个auth.js文件对其进行设置。这部分就不再详细介绍了,文件相应的代码如下:
var express = require('express');
var router = express.Router();
//导入jwt
const jwt=require('jsonwebtoken');
//读取配置项
const {secret} =require('../../config/config');
//导入用户模型
const UserModel=require('../../models/UserModel');
const md5=require('md5');//登录操作
router.post('/login',(req,res)=>{//获取用户名和密码let {username,password}=req.body;UserModel.findOne({username:username,password:md5(password)}).then(data=>{if(!data){return res.json({code:'2002',msg:'用户名或者密码错误',data:null})}//创建当前用户tokenlet token=jwt.sign({username:data.username,_id:data._id},secret,{expiresIn:60 * 60 * 24 *7});//响应tokenres.json({code:'0000',msg:'登录成功',data:token})}).catch(err=>{res.json({code:'2001',msg:'数据库读取失败',data:null})})
});//退出登录
router.post('/logout',(req,res)=>{//销毁sessionreq.session.destroy(()=>{res.render('success',{msg:'退出成功',url:'/login'})})
})module.exports = router;
中间件文件:
//checkTokenMiddleware.js
const jwt=require('jsonwebtoken');
//读取配置项
const {secret} =require('../config/config');
module.exports = (req, res, next) => {//获取tokenlet token = req.get('token');//判断if (!token) {return res.json({code: '2003',msg: 'token 缺失',data: null})}//校验tokenjwt.verify(token, secret, (err, data) => {if (err) {return res.json({code: '2004',msg: '校验失败',data: null})}//保存用户的信息req.user=data;//如果执行成功next();});}
通过Apipost来进行校验,当没有携带token时,获取不到数据,当设置请求头token数据时,能够获取到对应的数据。
好啦!本文就到这里了,Node.js系列的文章就告一段落了!如果有不足之处还请见谅~~