宿舍管理系统
- 项目名称:宿舍管理系统
- 项目平台:ndoejs+mysql
- 项目技术:express
- 开发人员:杨标
项目架构图
一、初始化项目
$ npm init --yes
二、安装项目依赖
$ npm install mysql express --save
三、创建express项目
const express = require("express");
const http = require("http");
const app = express();
const server = http.createServer(app);
server.listen(8080, "0.0.0.0", () => {
console.log("服务器启动成功");
})
四、根据模块创建路由
根据上面的架构图我们要创建三个路由模块
- 创建routes目录
- 创建
adminInfoRoute.js
,roomInfoRoute.js
,StuInfoRoute.js
当前我们以routeInfoRoute.js
为例来做演示
/**
* @author 杨标
* @description roominfo的路由模块
*/
const express = require("express");
const router = express.Router();
module.exports = router;
接下来在app.js
里面使用一级路径来连接路由文件
app.use("/roomInfo", require("./routes/roomInfoRoute.js"));
五、完成房间显示的路由请求
需求:查询所有的房间信息
我们现在希望在roomInfoRoute.js
里面完成一个请求功能,当请求一个路径的时候,把房间信息显示出来
router.get("/roomInfoList", (req, resp) => {
//把数据以json的形式返回出来
//数据在数据库,所以要找数据库拿数据
//数据由Servcier,所以我们要找Service
})
六、封装mysql的操作过程
- 新建一个
utils
的文件夹 - 在文件夹下面新建一个
DBUtils.js
文件,封装相应的代码
/**
* @description 封装数据库操作相关的东西
* @version 1.0
* @author 杨标
*/
const mysql = require("mysql");
class DBUtils {
/**
* @description 获取一个数据库连接
* @returns {mysql.Connection} 返回数据库连接
*/
getConn() {
let conn = mysql.createConnection({
host: "127.0.0.1",
port: 3306,
user: "dev_h2103",
password: "123456",
database: "rental_house"
});
return conn;
}
/**
* @description 执行一条SQL语句
* @param {string} strSql 要执行的SQL语句
* @param {[]} ps 执行SQL语句的参数
* @returns {Promise<[]|mysql.OkPacket>} 数据库执行的结果
*/
executeSql(strSql, ps = []) {
return new Promise((resolve, reject) => {
let conn = this.getConn();
conn.connect();
conn.query(strSql, ps, (error, results) => {
if (error) {
reject(error);
}
else {
resolve(results)
}
conn.end();
});
});
}
}
module.exports = DBUtils;
七、根据模块构建BaseService对象
因为有三个数据表我们需要三个数据服务对象,但是根据我们的架构图,我们需要一个BaseService
- 创建servcie目录
- 创建
BaseService.js
文件
/**
* @description 基础的服务对象
* @author 杨标
* @description 1.0
*/
const DBUtils = require("../utils/DBUtils.js")
class BaseServcie extends DBUtils {
constructor() {
super();
this.tableMap = {
room_info: "room_info",
admin_info: "admin_info",
stu_info: "stu_info"
}
this.currentTableName = "";
}
/**
* @description 根据一个主键id查询信息
* @param {number} id 主键
* @returns {Promise<object>} 查询结果
*/
async findById(id) {
let strSql = ` select * from ${this.currentTableName} where id = ? `;
let results = await this.executeSql(strSql, [id]);
return results[0];
}
/**
* @description 根据一个主键删除信息
* @param {number} id 主键
* @returns {Promise<boolean>} true代表删除成功,false代表删除失败
*/
async deleteById(id) {
let strSql = ` delete from ${this.currentTableName} where id = ? `;
let results = await this.executeSql(strSql, [id]);
return results.affectedRows > 0 ? true : false;
}
/**
* @description 获取所有数据
* @returns {Promise<[]>} 返回查询结果
*/
getAllList() {
let strSql = ` select * from ${this.currentTableName} `;
return this.executeSql(strSql);
}
}
module.exports = BaseServcie;
八、构建RoomInfoService对象
根据之前的架构图,我们现在是要操作room_info
的数据表,为前面的路由提供服务
RoomInfoService.js文件
/**
* @description 操作room_info数据表
* @author 杨标
* @version 1.0
*/
const BaseServcie = require("./BaseService.js");
class RoomInfoService extends BaseServcie {
constructor() {
super();
this.currentTableName = this.tableMap.room_info;
}
}
module.exports = RoomInfoService;
九、创建抽象服务工厂
- 先创建一个
factory
的文件夹 - 在文件夹下面创建一个
ServiceFactory.js
/**
* @description 抽象服务工厂
* @author 杨标
* @version 1.0
*/
const path = require("path");
const fs = require("fs");
/**
* @typedef ServiceFactoryType
* @property {import("../service/RoomInfoService.js")} roomInfoService
*/
/**
* @type {ServiceFactoryType}
*/
const ServiceFactory = (() => {
let obj = {};
let arr = fs.readdirSync(path.join(__dirname, "../service"));
for (let item of arr) {
let properyName = item.replace(".js", "").replace(/^[A-Z]/, p => p.toLowerCase());
let temp = require(path.join(__dirname, "../service", item));
if (typeof temp === "function") {
obj[properyName] = Reflect.construct(temp, []);
}
}
return Object.freeze(obj);
})();
module.exports = ServiceFactory;
十、完成room_info路由是中获取所有信息的操作
/**
* @author 杨标
* @description roominfo的路由模块
*/
const express = require("express");
const router = express.Router();
const ServiceFactory = require("../factory/ServiceFactory.js");
router.get("/roomInfoList", async (req, resp) => {
let results = await ServiceFactory.roomInfoService.getAllList();
resp.json(results);
});
module.exports = router;
十一、封装返回的JSON数据类型
我们现在是使用express
来构建了一个服务器,但是有可能服务器会报相应的错误,所以我们需要处理异常情况,对于异常情况,我们也可以使用json来返回,这个时候我们返回前端的格式就要统一
//数据请求成功
{
status:"success",
msg:"数据请求成功",
data:[] //这里放真正的数据
}
//请求失败
{
status:"fail",
msg:"请求失败",
data:[]
}
- 在当前项目下面创建一个
model
的文件夹 - 创建一个
ResultJson
的文件,代码如下
/**
* @description 返回给客户端的json数据模型
* @author 杨标
* @version 1.0
*/
class ResultJson {
constructor(flag, msg, data = []) {
this.status = flag ? "success" : "fail";
this.msg = msg;
this.data = data;
}
}
module.exports = ResultJson;
十二、完成前端页面
前端页面我们使用bootstrap4,同时使用art-template
模板引擎
<!DOCTYPE html>
<html lang="zh">
<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">
<meta name="renderer" content="webkit">
<title>房间信息列表</title>
<link rel="stylesheet" href="./css/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="./css/bootstrap/font/bootstrap-icons.css">
<!--[if lt ie 9]>
<script src="./css/bootstrap/html5shiv.min.js"></script>
<script src="./css/bootstrap/respond.min.js"></script>
<![endif]-->
</head>
<body>
</body>
<script src="./js/jquery-3.5.1.min.js"></script>
<script src="./js/template-web.js"></script>
<script src="./css/bootstrap/js/bootstrap.min.js"></script>
</html>
以上就是bootstrap4
的导入项
当我们使用ajax去请求数据的时候,如下所示
$(function() {
$.get(baseURL + "/roomInfo/roomInfoList", function(res) {
console.log(res);
});
});
这个问题是因为AJAX不允许请求跨域的数据产生的
-
当前网页的地址为
http://127.0.0.1:5500/roomInfoList.html
它的域是
http://127.0.0.1:5000
-
ajax请求的目标服务器地址为
http://192.168.1.254:8080/roomInfo/roomInfoList
它的域是
http://192.168.1.254:8080
两个域不相同,所以ajax就会请求失败
十三、解决Ajax跨域的问题
ajax跨域的问题是一个非常常见的问题,也是必须要解决的问题,它有解决方式有很多
- 反射代理
- 服务端添加响应头实现CORS
jsonp
本次我们即有前端,又有后端,所以我们采用第2种方式去完成
我们参考了别人的网站以后,我们发现,只要在服务器上面添加上面的这三个东西就可以解决跨域的问题
这个三个东西是在服务端加的
resp.setHeader("Access-Control-Allow-Headers", "Content-Type");
resp.setHeader("Access-Control-Allow-Origin", "*");
resp.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
上面的三个方式设置好了以后,Ajax就可以跨域了,这种设置方式,我们叫CORS(跨域资源共享)
十四、设置CORS的拦截器
因为所有的请求都要添加响应头,我们可以借用于express里面的拦截器来实现
//设置一个拦截器
app.use((req, resp, next) => {
resp.setHeader("Access-Control-Allow-Headers", "Content-Type");
resp.setHeader("Access-Control-Allow-Origin", "*");
resp.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
next();
});
//一定是要设置在路由之前
十五、查询功能的完成
查询的时候,我们需要先获取页面上面输入的查询参数,然后将参数发送给服务器
要完成这个功能,需要单独设置一个点,就是服务器如何接收前端传过来的数据
$("#btn-query").on("click", function() {
$.get(baseURL + "/roomInfo/getListByPage?room_name=" + $("#room_name").val(), function(res) {
console.log(res);
});
});
在上面的代码里面,我们对查询按钮绑定了一个点击的事件,当点击的时候向后台的服务器发起一个Ajax的请求,然后在这个地址的后面通过search
的方式传递参数到服务器
服务器就可以通过req.query
的方式接收到get
请求携带过来的参数
router.get("/getListByPage", (req, resp) => {
try {
console.log(req.query);
//{ room_name: '桃花菀' }
//把值交给service,让service帮我查
resp.send("我收到你的查询请求了");
} catch (error) {
resp.status(500).json(new ResultJson(false, error));
}
});
当我们接收到这个参数以后就要交给service
去操作数据库
RoomInfoService.js文件
/**
* @description 操作room_info数据表
* @author 杨标
* @version 1.0
*/
const BaseServcie = require("./BaseService.js");
class RoomInfoService extends BaseServcie {
constructor() {
super();
this.currentTableName = this.tableMap.room_info;
}
/**
* 分页查询
* @param {{room_name}} param 查询参数
* @returns {Promise<[]>} 返回查询结果
*/
getListByPage({ room_name }) {
let strSql = ` select * from ${this.currentTableName} where 1 `;
let ps = []
if (room_name) {
strSql += ` and room_name like ? `;
ps.push(`%${room_name}%`);
}
return this.executeSql(strSql, ps);
}
}
module.exports = RoomInfoService;
当我们的Service已经完成了以后,我们又可以回到路由 去使用这个Service获取数据
roomInfoService.js文件
router.get("/getListByPage", async (req, resp) => {
try {
console.log(req.query);
//{ room_name: '桃花菀'}
//把值交给service,让service帮我查
let results = await ServiceFactory.roomInfoService.getListByPage(req.query);
resp.json(new ResultJson(true, "获取数据成功", results));
} catch (error) {
resp.status(500).json(new ResultJson(false, error));
前端的页面经过ajax请求以后就可以得到数据,得到数据以后,重新渲染页面
【前端页面】
//查询
$("#btn-query").on("click", function() {
Qmsg.loading("正在请求数据...");
$.get(baseURL + "/roomInfo/getListByPage?room_name=" + $("#room_name").val(), function(res) {
Qmsg.closeAll();
if (res.status == "success") {
//数据请求成功
Qmsg.success("请求数据成功");
let htmlStr = template("temp1", {
roomInfoList: res.data
});
$("#table-roomInfo>tbody").html(htmlStr);
} else {
//数据请求失败
Qmsg.error("请求数据失败");
}
});
});
十六、ajax请求的封装
/**
* 网页里面的JS不要瞎写,有兼容性的
* 不像nodejs,所以最好是有ES5为主
*/
var baseURL = "http://127.0.0.1:8080";
/**
* 将原来jQuery里面的ajax封装成一个Promise的处理方式
*/
var request = {
get: function (url, data) {
return new Promise(function (resolve, reject) {
$.ajax({
url: baseURL + url,
method: "get",
data: data,
success: function (res) {
resolve(res);
},
error: function (error) {
reject(error);
}
});
});
},
post: function (url, data) {
return new Promise(function (resolve, reject) {
$.ajax({
url: baseURL + url,
method: "post",
data: data,
success: function (res) {
resolve(res);
},
error: function (error) {
reject(error);
}
});
});
}
}
十七、构建分页
我们现在如果希望构建上面的分页信息展示,怎么办呢?
我们现在已经知道分页的多条SQL语句执行的结果如下
let results = [
[
RowDataPacket {
id: 1,
room_name: '西-s-0751',
max_count: 2,
kt: 1,
network: 1,
washroom: 0,
room_size: 191.25
},
//.........
],
[
RowDataPacket {
total_count: 56
}
]
]
在上面的结果 里面,第一个数组代表的就是第一条SQL语句查询的结果,我们可以通过results[0]
得到列表的结果,第二条SQL语句可以通过results[1]
得到结果,所以我们可以通过results[1][0].total_count
得到结果
页整个页面需要构建的数据如正是
- 当前第几页
pageIndex
- 共多少页
pageCount
- 共多少条
totalCount
我们在model
的文件夹下面创建了一个PageList
的对象,如下所示
class PageList {
/**
*
* @param {number} pageIndex 当前第几页
* @param {number} totalCount 共多少条数据
* @param {number} pageSize 每页显示多少
* @param {[]} listData 查询的分页列表数据
*/
constructor(pageIndex, totalCount, pageSize, listData) {
this.pageIndex = pageIndex;
this.totalCount = totalCount;
this.pageCount = Math.ceil(totalCount / pageSize);
this.listData = listData;
}
}
module.exports = PageList;
最后在getListByPage
里面完成如下代码
/**
* 分页查询
* @param {{room_name,pageIndex}} param 查询参数
* @returns {Promise<PageList>} 返回分页查询以后的封装的PageList
*/
async getListByPage({ room_name, pageIndex }) {
let strSql = ` select * from ${this.currentTableName} where 1 `;
let countSql = ` select count(*) 'total_count' from ${this.currentTableName} where 1 `;
let strWhere = ` `;
let ps = []
if (room_name) {
strWhere += ` and room_name like ? `;
ps.push(`%${room_name}%`);
}
strSql += ` ${strWhere} limit ${(pageIndex - 1) * this.pageSize} , ${this.pageSize} `;
countSql += strWhere;
let results = await this.executeSql(strSql + ";" + countSql, [...ps, ...ps]);
let pageList = new PageList(pageIndex, results[1][0].total_count, this.pageSize, results[0]);
return pageList;
}
roomInfoRoute.js文件
router.get("/getListByPage", async (req, resp) => {
try {
//{ room_name: '', pageIndex: '1' }
let pageList = await ServiceFactory.roomInfoService.getListByPage(req.query);
resp.json(new ResultJson(true, "获取数据成功", pageList));
} catch (error) {
console.log(error);
resp.status(500).json(new ResultJson(false, error));
}
});
roomInfoList.html文件
<!DOCTYPE html>
<html lang="zh">
<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">
<meta name="renderer" content="webkit">
<title>房间信息列表</title>
<link rel="stylesheet" href="./css/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="./css/bootstrap/font/bootstrap-icons.css">
<!--[if lt ie 9]>
<script src="./css/bootstrap/html5shiv.min.js"></script>
<script src="./css/bootstrap/respond.min.js"></script>
<![endif]-->
<link rel="stylesheet" href="./css/message.css">
</head>
<body>
<div class="container-fluid">
<ul class="breadcrumb">
<li class="breadcrumb-item"><a href="#">首页</a></li>
<li class="breadcrumb-item active">房间信息列表</li>
</ul>
<form class="form-inline">
<div class="form-group">
<label for="" class="control-label">房间编号</label>
<input id="room_name" type="text" class="form-control" placeholder="输入房间编号查询">
</div>
<div class="form-group ml-2">
<button id="btn-query" type="button" class="btn btn-success">
<span class="bi-search"></span>
查询
</button>
</div>
<div class="form-group ml-2">
<button type="button" class="btn btn-info">
<span class="bi-plus-circle"></span>
新增房间
</button>
</div>
<div class="form-group ml-2">
<button type="button" class="btn btn-dark">
<span class="bi-file-earmark-excel"></span>
导出为excel
</button>
</div>
</form>
<!-- 表格 -->
<div class="table-responsive mt-1">
<table id="table-roomInfo" class="table table-hover table-striped table-bordered">
<thead>
<tr class="bg-info text-light">
<th>
<input type="checkbox">全选
</th>
<th>房间编号</th>
<th>最大人数</th>
<th class="text-center">是否有空调</th>
<th class="text-center">是否有网络</th>
<th class="text-center">独立卫生间</th>
<th>房间面积 (m<sup>2</sup>)</th>
<th>操作</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div class="row">
<div class="col" id="page-list-info">
</div>
<div class="col">
<ul class="pagination justify-content-end">
</ul>
</div>
</div>
</div>
<script type="text/html" id="temp1">
{{each roomInfoList item index}}
<tr>
<td>
<input type="checkbox">
</td>
<td>{{item.room_name}}</td>
<td>{{item.max_count}}</td>
<td class="text-center">
<span class="p-1 text-light rounded {{item.kt==0?'bg-danger':'bg-success'}}">
{{item.kt==0?"无":"有"}}
</span>
</td>
<td class="text-center">
<span class="p-1 text-light rounded {{item.network==0?'bg-danger':'bg-success'}}">
{{item.network==0?"无":"有"}}
</span>
</td>
<td class="text-center">
<span class="p-1 text-light rounded {{item.washroom==0?'bg-danger':'bg-success'}}">
{{item.washroom==0?"无":"有"}}
</span>
</td>
<td>{{item.room_size}}</td>
<td>
<button type="button" class="btn btn-warning btn-sm">编辑</button>
<button type="button" class="btn btn-danger btn-sm">删除</button>
</td>
</tr>
{{/each}}
</script>
<!-- 渲染分页的页码 -->
<script type="text/html" id="temp2">
<li data-index="1" class="page-item"><a href="#" class="page-link">首页</a></li>
<%for(var i=1;i<=pageCount;i++){%>
<li data-index="{{i}}" class="page-item {{i==pageIndex?'active':null}}"><a class="page-link" href="#">{{i}}</a></li>
<%}%>
<li data-index="{{pageCount}}" class="page-item"><a href="#" class="page-link">尾页</a></li>
</script>
<script type="text/html" id="temp3">
当前第{{pageIndex}}页,共{{pageCount}}页,共{{totalCount}}条
</script>
</body>
<script src="./js/jquery-3.5.1.min.js"></script>
<script src="./js/template-web.js"></script>
<script src="./css/bootstrap/js/bootstrap.min.js"></script>
<script src="./js/base.js"></script>
<script src="./js/message.min.js"></script>
<script>
$(function() {
function getData(pageIndex) {
var loading = Qmsg.loading("正在加载数据...");
request.get("/roomInfo/getListByPage", {
room_name: $("#room_name").val(),
pageIndex: pageIndex
}).then(function(res) {
console.log(res);
if (res.status == "success") {
Qmsg.success("获取数据成功");
//渲染表格
var htmlStr = template("temp1", {
roomInfoList: res.data.listData
});
$("#table-roomInfo>tbody").html(htmlStr);
// 渲染页面
var htmlStr2 = template("temp2", {
pageCount: res.data.pageCount,
pageIndex: res.data.pageIndex
});
$(".pagination").html(htmlStr2);
//渲染左边的文字信息
var htmlStr3 = template("temp3", {
pageIndex: res.data.pageIndex,
pageCount: res.data.pageCount,
totalCount: res.data.totalCount
});
$("#page-list-info").html(htmlStr3);
} else {
Qmsg.error("获取数据失败");
}
}).catch(function(error) {
console.log(error);
Qmsg.error("服务器错误,请联系管理员");
}).finally(function() {
//无论是成功进入then还是失败进入catch,最后都会进入到finally
loading.close();
});
}
getData(1);
$("#btn-query").on("click", function() {
getData(1);
})
$(".pagination").on("click", "li", function(event) {
var index = $(this).attr("data-index");
getData(index);
});
});
</script>
</html>
十八、配置服务器POST请求
第一步:装包
post请求是需要第三方包来完成的
$ npm install body-parser --save
**第二步:在app.js中配置post请求接收值的方式 **
//配置post接收参数
const bodyParser = require("body-parser");
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json({ limit: "30mb" }));
第三步:接收值使用req.body
router.post("/add",async(req,resp)=>{
//接收值
console.log(req.body);
});
十九、新增房间的功能【POST请求】
按照我们相应的开发规范,如果有一个新的模块出来的时候应该是先后台,再前台
首先在后台的项目里,完成Service
的功能
RoomInfoService.js文件里面
/**
* @description 新增房间数据
* @param {{ room_name, max_count, kt, network, washroom, room_size }} param 新增的删除
* @returns {Promise<boolean>} true代表新增成功,false代表新增失败
*/
async add({ room_name, max_count, kt, network, washroom, room_size }) {
let strSql = ` INSERT INTO ${this.currentTableName} (room_name, max_count, kt, network, washroom, room_size) VALUES (?, ?, ?, ?, ?, ?); `;
let results = await this.executeSql(strSql, [room_name, max_count, kt, network, washroom, room_size]);
return results.affectedRows > 0 ? true : false;
}
roomInfoRoute.js文件
router.post("/add", async (req, resp) => {
try {
//请注意,因为是post主有求,所以要使用body来接收值
let results = await ServiceFactory.roomInfoService.add(req.body);
resp.json(new ResultJson(results, results ? "新增成功" : "新增失败"));
} catch (error) {
console.log(error);
resp.status(500).json(new ResultJson(false, error));
}
});
这里一定要注意,post请求传递过来的值,是通过
req.body
来接收的
前端代码addRoomInfo.html
<!DOCTYPE html>
<html lang="zh">
<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">
<meta name="renderer" content="webkit">
<title>新增房间信息</title>
<link rel="stylesheet" href="./css/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="./css/bootstrap/font/bootstrap-icons.css">
<!--[if lt ie 9]>
<script src="./css/bootstrap/html5shiv.min.js"></script>
<script src="./css/bootstrap/respond.min.js"></script>
<![endif]-->
<link rel="stylesheet" href="./css/message.css">
</head>
<body>
<div class="container-fluid">
<ul class="breadcrumb">
<li class="breadcrumb-item"><a href="#">首页</a></li>
<li class="breadcrumb-item active">新增房间信息</li>
</ul>
<div class="card">
<div class="card-header bg-info text-light">
新增房间信息
</div>
<div class="card-body">
<form id="form-addRoomInfo">
<div class="form-group row">
<label for="" class="col-form-label col-sm-2 text-right">房间编号</label>
<div class="col-sm-7">
<input id="room_name" name="room_name" type="text" placeholder="请输入房间编号" class="form-control"
data-rule-required="true"
data-msg-required="房间编号不能为空"
data-rule-regexp="^[东南西北中发白]\-[sbt]\-\d{4}$"
data-msg-regexp="房间编号的格式不正确">
</div>
<div class="col-sm-3 text-danger"></div>
</div>
<div class="form-group row">
<label for="" class="col-form-label col-sm-2 text-right">最大人数</label>
<div class="col-sm-7">
<input id="max_count" name="max_count" type="text" placeholder="请输入最大人数" class="form-control"
data-rule-required="true"
data-msg-required="最大人数不能为空"
data-rule-regexp="^\d+$"
data-msg-regexp="最大人数必须是整数">
</div>
<div class="col-sm-3 text-danger"></div>
</div>
<div class="form-group row align-items-center">
<label for="" class="col-form-label col-sm-2 text-right">是否有空调</label>
<div class="col-sm-7">
<div class="custom-radio custom-control custom-control-inline">
<input checked class="custom-control-input" type="radio" id="kt1" name="kt" value="1">
<label for="kt1" class="custom-control-label">有</label>
</div>
<div class="custom-radio custom-control custom-control-inline">
<input class="custom-control-input" type="radio" id="kt0" name="kt" value="0">
<label for="kt0" class="custom-control-label">没有</label>
</div>
</div>
</div>
<div class="form-group row align-items-center">
<label for="" class="col-form-label col-sm-2 text-right">是否有网络</label>
<div class="col-sm-7">
<div class="custom-radio custom-control custom-control-inline">
<input checked class="custom-control-input" type="radio" id="network1" name="network" value="1">
<label for="network1" class="custom-control-label">有</label>
</div>
<div class="custom-radio custom-control custom-control-inline">
<input class="custom-control-input" type="radio" id="network0" name="network" value="0">
<label for="network0" class="custom-control-label">没有</label>
</div>
</div>
</div>
<div class="form-group row align-items-center">
<label for="" class="col-form-label col-sm-2 text-right">是否有独卫</label>
<div class="col-sm-7">
<div class="custom-radio custom-control custom-control-inline">
<input checked class="custom-control-input" type="radio" id="washroom1" name="washroom" value="1">
<label for="washroom1" class="custom-control-label">有</label>
</div>
<div class="custom-radio custom-control custom-control-inline">
<input class="custom-control-input" type="radio" id="washroom0" name="washroom" value="0">
<label for="washroom0" class="custom-control-label">没有</label>
</div>
</div>
<div class="col-sm-3 text-danger"></div>
</div>
<div class="form-group row">
<label for="" class="col-form-label col-sm-2 text-right">房间大小</label>
<div class="col-sm-7">
<div class="input-group">
<input id="room_size" name="room_size" type="text" class="form-control" placeholder="请输入房间面积"
data-rule-required="true"
data-msg-required="房间面积不能为空"
data-rule-regexp="^\d+(\.\d+)?$"
data-msg-regexp="房间面积必须是数字类型">
<div class="input-group-append">
<span class="input-group-text">m<sup>2</sup></span>
</div>
</div>
</div>
<div class="col-sm-3 text-danger"></div>
</div>
<div class="form-group row">
<div class="col-sm-7 offset-sm-2">
<button id="btn-save" type="button" class="btn btn-success">
保存数据
</button>
<a href="./roomInfoList.html" class="btn btn-info">
返回列表
</a>
</div>
</div>
</form>
</div>
</div>
</div>
</body>
<script src="./js/jquery-3.5.1.min.js"></script>
<script src="./js/template-web.js"></script>
<script src="./css/bootstrap/js/bootstrap.min.js"></script>
<script src="./js/message.min.js"></script>
<script src="./js/jquery.validate.js"></script>
<script src="./js/messages_zh.js"></script>
<script src="./js/base.js"></script>
<script>
$(function() {
var validateForm = $("#form-addRoomInfo").validate({
errorClass: "text-danger",
errorPlacement: function(errorElement, currentElement) {
// currentElement.parent().next().append(errorElement);
currentElement.parentsUntil("form").find("div.text-danger").append(errorElement);
},
});
$("#btn-save").on("click", function() {
if (validateForm.form()) {
//验证通过以后就要提交数据了
addData();
}
});
function addData() {
var laoding = Qmsg.loading("正在保存数据.....")
request.post("/roomInfo/add", {
room_name: $("#room_name").val(),
max_count: $("#max_count").val(),
kt: $("[name='kt']:checked").val(),
network: $("[name='network']:checked").val(),
washroom: $("[name='washroom']:checked").val(),
room_size: $("#room_size").val()
}).then(function(res) {
if (res.status == "success") {
Qmsg.success("新增成功");
location.replace("./roomInfoList.html");
} else {
Qmsg.error("新增失败,请重试或联系管理员");
}
}).catch(function(error) {
console.log(error);
Qmsg.error("服务器错误");
}).finally(function() {
laoding.close();
})
}
})
</script>
</html>
二十、新增验证房间编号是否存在
当我们在新增数据的时候,我们不仅仅是要新增数据,还要在填写数据的时候告诉别人,这个数据是否已经存在了
RoomInfoService.js
/**
* @description 验证某一个房间编号是否存在
* @param {string} room_name 要验证的房间编号
* @returns {Promise<boolean>} true代表存在,false代表不存在
*/
async existsRoomName(room_name) {
let strSql = ` select * from ${this.currentTableName} where 1 and room_name = ? `;
let results = await this.executeSql(strSql, [room_name]);
return results.length > 0 ? true : false
}
在路由文件roomInfoRoute.js里面编写一个路径请求,接收页面传递过来的房间编号 ,判断这个房间编号是否存在
router.get("/existsRoomName", async (req, resp) => {
try {
console.log(req.query.room_name);
let results = await ServiceFactory.roomInfoService.existsRoomName(req.query.room_name);
resp.send(new ResultJson(true, "数据请求成功", results));
} catch (error) {
console.log(error);
resp.status(500).json(new ResultJson(false, error));
}
});
在上面的路由里面添加了这个请求的路由以后,我们在前端的页面上面就可以使用这个路由地址的路径去通过AJAX请求了
$("#room_name").on("blur", function() {
var room_name = $(this).val();
if (room_name) {
request.get("/roomInfo/existsRoomName", {
room_name: room_name
}).then(function(res) {
if (res.status == "success") {
if (res.data == true) {
Qmsg.warning("当前房间编号已存在,请换一个");
$("#room_name").val("");
}
}
}).catch(function(error) {
Qmsg.error("服务器错误");
console.log(error);
});
}
});
二十一、设置项目的自动重启
在进行nodejs项目开发的时候,当我们更改了项目的源代码,我们希望项目能够自动重新启动,这个技术叫项目的热重启技术,它需要借用于第三方包来完成nodemon
第一步:安装nodemon
$ npm install nodemon --save-dev
这里要注意,这里使用的是
--save-dev
--save
代表当前的包是一个生产环境下面的包--save-dev
代表当前的包是一个开发环境下面的包
**场景:**现在李光昊要做一碗饭给标哥吃,它需要如下的东西
- 米【原材料】
- 水【原材料】
- 电饭锅【工具】
工具一般是在开发环境的时候使用的,而原材料则是生产环境下面使用
$ npm install 米 水 --save
$ npm install 电锯锅 --save-dev
第二步:配置启动脚本
在package.json
的文件下面,找到scripts
,添加如下命令
"scripts": {
"start": "node app.js",
"dev": "nodemon --watch ./"
},
第三步:启动项目
$ npm run dev
二十二、编辑房间信息
上图就是编辑页面的布局,但是我们应该是在列表页面点击编辑按钮以后再跳这个页面
roomInfoRoute.js文件
router.get("/findById", async (req, resp) => {
try {
let { id } = req.query;
let results = await ServiceFactory.roomInfoService.findById(id);
let flag = results ? true : false;
resp.json(new ResultJson(flag, flag ? "获取数据成功" : "获取数据失败", results));
} catch (error) {
console.log(error);
resp.status(500).json(new ResultJson(false, error));
}
});
editRoomInfo.html
这个时候浏览器地址栏的值为:http://127.0.0.1:5500/editRoomInfo.html?id=6
function getById() {
//合到浏览器地址栏上面的id
var u = new URL(location.href);
var id = u.searchParams.get("id");
//将这个id发送到后台服务器,让后台服务器从数据库里面取这个这个记录,返回给我
var loading = Qmsg.loading("正在加载数据...");
request.get("/roomInfo/findById", {
id: id
}).then(function(res) {
console.log(res);
}).catch(function(error) {
console.log(error);
Qmsg.error("服务器错误");
}).finally(function() {
loading.close();
});
}
getById();
这个时候数据已经可以获取回来了,我们要把数据放在页面上面,这里有两种方法
二十三、渲染编辑页面
第一种:直接以DOM的方式操作,这种效率非常低
这一种方式非常简单,直接将值一个一个的赋值给DOM元素的value属性
第二种:直接通过模板引擎渲染
直接将需要渲染的部分使用模板引擎,然后用数据渲染就可以了
二十四、编辑房间信息页面保存数据
RoomInfoService.js
/**
* @description 修改房间信息
* @param {{ id, room_name, max_count, kt, network, washroom, room_size }} param 要修改的参数
* @returns {Promise<boolean>}
*/
async update({ id, room_name, max_count, kt, network, washroom, room_size }) {
let strSql = ` UPDATE rental_house.room_info SET room_name=?, max_count=?, kt=?, network=?, washroom=?, room_size=? WHERE id = ?;`
let results = await this.executeSql(strSql, [room_name, max_count, kt, network, washroom, room_size, id])
return results.affectedRows > 0 ? true : false;
}
roomInfoRoute.js
router.post("/update", async (req, resp) => {
try {
let results = await ServiceFactory.roomInfoService.update(req.body);
resp.json(new ResultJson(results, results ? "修改成功" : "修改失败"));
} catch (error) {
console.log(error);
resp.status(500).json(new ResultJson(false, error));
}
});
editRoomInfo.html
//这里一定要注意,使用事件委托
$("#form-addRoomInfo").on("click", "#btn-save", function() {
if (validateForm.form()) {
// 表单验证成功以后,数据重新传回后台
saveData();
}
});
function saveData() {
request.post("/roomInfo/update", {
id: $("#id").val(),
room_name: $("#room_name").val(),
max_count: $("#max_count").val(),
kt: $("[name='kt']:checked").val(),
network: $("[name='network']:checked").val(),
washroom: $("[name='washroom']:checked").val(),
room_size: $("#room_size").val()
}).then(function(res) {
if (res.status == "success") {
Qmsg.success("修改成功");
location.replace("./roomInfoList.html");
} else {
Qmsg.error("修改失败");
}
}).catch(function(error) {
console.log(error);
Qmsg.error("服务器错误");
}).finally(function() {
loading.close();
});
}
二十五、解决分页页码过多的问题
以之前的列表页面上面,我们可以看到这里有很多页面,我们不可以把所有的页面都显示出来 ,所以要在后台设置一下
PageList.js
class PageList {
/**
*
* @param {number} pageIndex 当前第几页
* @param {number} totalCount 共多少条数据
* @param {number} pageSize 每页显示多少
* @param {[]} listData 查询的分页列表数据
*/
constructor(pageIndex, totalCount, pageSize, listData) {
this.pageIndex = pageIndex;
this.totalCount = totalCount;
this.pageCount = Math.ceil(totalCount / pageSize);
this.listData = listData;
this.pageStart = this.pageIndex - 3 > 0 ? this.pageIndex - 3 : 1;
this.pageEnd = this.pageStart + 6 > this.pageCount ? this.pageCount : this.pageStart + 6;
}
}
module.exports = PageList;
在上面的构造函数里面,我们添加了两个属性,
pageStart
用于代表起始页码的位置 ,pageEnd
用于代表结束页码的位置
roomInfoList.html
<!-- 渲染分页的页码 -->
<script type="text/html" id="temp2">
<li data-index="1" class="page-item"><a href="#" class="page-link">首页</a></li>
<%for(var i=pageStart;i<=pageEnd;i++){%>
<li data-index="{{i}}" class="page-item {{i==pageIndex?'active':null}}"><a class="page-link" href="#">{{i}}</a></li>
<%}%>
<li data-index="{{pageCount}}" class="page-item"><a href="#" class="page-link">尾页</a></li>
</script>
二十六、删除房间信息
我们应该删除数据
roomInfoRoute.js
router.get("/deleteById", async (req, resp) => {
try {
let { id } = req.query;
let results = await ServiceFactory.roomInfoService.deleteById(id);
resp.json(new ResultJson(results, results ? "删除成功" : "删除失败"));
} catch (error) {
console.log(error);
resp.status(500).json(new ResultJson(false, error));
}
});
roomInfoList.html
//使用事件委托
$("#table-roomInfo").on("click", ".btn-delete", function() {
if (confirm("确定要删除当前项吗?")) {
var id = $(this).attr("data-id");
request.get("/roomInfo/deleteById", {
id: id
}).then(function(res) {
if (res.status == "success") {
Qmsg.success("删除成功");
getData(currentPageIndex);
} else {
Qmsg.success("删除失败");
}
}).catch(function(error) {
console.log(error);
Qmsg.error("服务器错误,请联系管理员");
}).finally(function() {
//无论是成功进入then还是失败进入catch,最后都会进入到finally
loading.close();
});
}
});
在上面的代码里面是没有问题的,它最大的问题在于数据库
存在的问题
-
在开发过程当中,DBA不会给开发人员
delete
的权限 -
在开发过程当中即使有了
delete
权限 ,但是因为主外键限制,如果这个表的某一项主键 被 另一个表使用了,则不能删除,因为主外键在约束的时候是默认不允许更新及删除的
二十七、逻辑删除(软删除)
因为在开发的过程当中,我们不能够直接删除数据,但是我们又想实现删除的效果,所以怎么办呢?
这个时候我们可以使用下面的方式完成,直接在所有表的后面添加一个字段叫isDel
,它的类型为tinyint(1)
这样所有的表在打开的时候都会当回事一列isDel
,它的默认值就是0
它有了默认值 ,则在添加数据的时候,即使我们不添加这一列它默认也是0
我们可以把isDel0
当成是这一列没有删除,我们可以把isDel为1
代表删除了
接下来在所有查询语句的后面将原来的where 1
替换成where isDel = false
这个时候原来的删除方法deleteById
就应该变成下面的方法
BaseService.js
/**
* @description 根据一个主键删除信息
* @param {number} id 主键
* @returns {Promise<boolean>} true代表删除成功,false代表删除失败
*/
async deleteById(id) {
// let strSql = ` delete from ${this.currentTableName} where id = ? `;
let strSql = ` update ${this.currentTableName} set isDel = true where id = ? `
let results = await this.executeSql(strSql, [id]);
return results.affectedRows > 0 ? true : false;
}
逻辑删除也叫软删除,它本质上面是没有删除数据的,只慢在数据表里面添加了一列,用于标识这一项数据是否被删除掉
当我们要删除这一 项数据的时候,我们可以通过
update
语句当这一列的值改true
或其它值
二十八、Excel的导出功能
前端文件roomInfoList.html
$(".btn-export-excel").on("click", function() {
//地址栏的传值是get传值 ,后面是通过req.query来得到的
window.open(baseURL + "/roomInfo/exportExcel?room_name=" + $("#room_name").val());
});
在这里同学们要注意,我们使用了window.open()
打开一个新的链接,并且在后面使用?
拼接了参数,传递到了服务器
RoomInfoService.js
/**
* @description 导出为excel的数据,但是不需要分页
* @param {{room_name}} param 查询参数
* @returns {Promise<[]>} 返回查询结果
*/
exportExcel({ room_name }) {
let strSql = ` select * from ${this.currentTableName} where isDel = false `;
let strWhere = ` `;
let ps = []
if (room_name) {
strWhere += ` and room_name like ? `;
ps.push(`%${room_name}%`);
}
strSql += strWhere;
return this.executeSql(strSql, ps);
}
roomInfoRoute.js
router.get("/exportExcel", async (req, resp) => {
try {
let results = await ServiceFactory.roomInfoService.exportExcel(req.query);
let excelPath = ExcelUtils.resultsToExcel(results);
if (excelPath) {
//生成好了
resp.sendFile(excelPath);
}
else {
resp.json(new ResultJson(false, "excel没有数据,不能下载"));
}
} catch (error) {
console.log(error);
resp.status(500).json(new ResultJson(false, error));
}
});
ExcelUtils.js
因为我们的excel导出是需要在其它的页面也使用的,所以我们单独封装了一个方法来使用,在utils
目录下面我们新建了一个ExcelUtils.js
的文件,代码如下
/**
* ExcelUtils.js 操作excel的对象
*/
const xlsx = require("node-xlsx");
const path = require("path");
const fs = require("fs");
class ExcelUtils {
/**
* @description 数据库的查询结果转变成excel文件
* @param {[]} results 数据库的查询结果
* @returns {string} 返回生成的excel的路径
*/
static resultsToExcel(results) {
if (results.length > 0) {
let headRow = Object.keys(results[0]);
let dataRows = results.map(item => Object.values(item));
dataRows.unshift(headRow);
let excelObj = [
{
name: "Sheet1",
data: dataRows
}
];
//设置一个保存路径
let savePath = path.join(__dirname, "../excelDir", `${Date.now()}-${parseInt(Math.random() * 1000)}.xlsx`);
let excelBuff = xlsx.build(excelObj);
fs.writeFileSync(savePath, excelBuff);
return savePath;
}
else {
return "";
}
}
}
module.exports = ExcelUtils;
在上面的代码里面,我们是通过node-xlsx
生成了一个excel文件,然后在express里面通过resp.sendFile
来发送一个文件到浏览器
二十九、定期删除生成的excel文件
这个时候要注意,因为导出excel是需要在服务器生成excel文件的,所以服务器运行时间长了以后,必须要定时清除掉这些文件,如下图所示
这个时候我们要在项目启动的时候添加一个定时器的功能,去完成上面的代码
app.js里面
server.listen(8080, "0.0.0.0", () => {
console.log("服务器启动成功");
const deleteExcelFile = () => {
let excelDirPath = path.join(__dirname, "./excelDir");
let arr = fs.readdirSync(excelDirPath);
for (let item of arr) {
let fileCreateTime = item.split("-")[0];
if (Date.now() - fileCreateTime > 1000 * 60 * 30) {
//超过30分钟,要删除
fs.unlinkSync(path.join(excelDirPath, item));
}
}
}
//在服务器启动的时候就执行一次
deleteExcelFile();
//接下来开启定时器,定时执行
setInterval(deleteExcelFile, 1000 * 60 * 30)
});
三十、全局异常的处理
在之前的代码里面,我们可以看到,我们对所有的代码都进行了try...catch
的异常捕获,这样做非常麻烦
在express
的框架里面,其实我们有一个全局异常处理的包叫express-async-errors
,这个包可以全局捕获express
的异常,这样我样就可以简化很多代码
第一步:安装包
$ npm install express-async-errors --save
第二步:在app.js程序启动的时候导入这个包
const express = require("express");
require("express-async-errors");
第三步:在程序的最后处理异常
//在处理全局的异常
app.use((error, req, resp, next) => {
console.log(error);
resp.status(500).json(new ResultJson(false, "服务器错误", error));
next();
});
为什么在程序的最后去处理,是因为如果前面的程序处理了,我们就不处理,如果前面没有程序处理异常,我们就在这里处理掉,相当于把它做了最后一次的防线
当我们使用了全局异常以后,这样在整个程序的内部,我们都不需要使用try...catch
了
三十一、总结一
到此为止,我们已经完成了room_info
这个数据表的增,删,改,查的功能了
- 前端的项目和后端的项目是分开的,前端与后端的数据交互使用的是
ajax
请求 ajax
默认不允许跨域,所以要在设置添加响应头CORS
- 路由就是用于控制请求的,ajax请求的其实是某一个路由地址
- 在请求的时候是可以传递数据到服务器的,
get
请求的请求参数在url
的地址后面,后台接收值的时候使用req.query
,post
不在地址栏,它是通过body
传递过去的,它在后台req.body
express
的框架默认是不支持post
请求的,需要借用于第三方插件body-parser
- 路由返回的数据我们统一使用了
ResultJson
的格式来处理,这样前端在接收到json字符串的时候统一处理 - 前端的页面渲染,目前我们使用的是
art-template
模板引擎,后期我们全转向vue/react
- 在开发的时候,数据库一般是默认不会开启
delete
权限,或因为主外键约束不能删除,所以我们在数据库里面要使用软删除的功能 resp.sendFile
用于向前台发送一个文件,我们的excel
的导出功能就是基于此完成的- 全局的异常处理我们使用了
express-async-errors
来完成,这样可以极大的简化我们的代码
评论区