目录
项目效果
项目的搭建
编辑
响应静态网页
编辑
编辑
结合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.js
var 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>
<link
href="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">
<span
class="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">
<span
class="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>
<link
href="/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>
<input
name="title"
type="text"
class="form-control"
id="item"
/>
</div>
<div class="form-group">
<label for="time">发生时间</label>
<input
name="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>
<input
name="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');
//导入mongoose
const 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 %>">
<span
class="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('账号或者密码错误~~')
}
//写入session
req.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.sid
secret: 'atguigu', //参与加密的字符串(又称签名) 加盐
saveUninitialized: false, //是否为每次请求都设置一个cookie用来存储session的id
resave: true, //是否在每次请求时重新保存session 20 分钟 4:00 4:20
store: 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)=>{
//销毁session
req.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
})
}
//创建当前用户token
let token=jwt.sign({
username:data.username,
_id:data._id
},secret,{
expiresIn:60 * 60 * 24 *7
});
//响应token
res.json({
code:'0000',
msg:'登录成功',
data:token
})
}).catch(err=>{
res.json({
code:'2001',
msg:'数据库读取失败',
data:null
})
})
});
//退出登录
router.post('/logout',(req,res)=>{
//销毁session
req.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) => {
//获取token
let token = req.get('token');
//判断
if (!token) {
return res.json({
code: '2003',
msg: 'token 缺失',
data: null
})
}
//校验token
jwt.verify(token, secret, (err, data) => {
if (err) {
return res.json({
code: '2004',
msg: '校验失败',
data: null
})
}
//保存用户的信息
req.user=data;
//如果执行成功
next();
});
}
通过Apipost来进行校验,当没有携带token时,获取不到数据,当设置请求头token数据时,能够获取到对应的数据。
好啦!本文就到这里了,Node.js系列的文章就告一段落了!如果有不足之处还请见谅~~