Express - crud
开发工具
- vsCode
起步
- 初始化
- 模板处理
路由设计
请求方法 | 请求路径 | get 参数 | post 参数 | 备注 |
---|---|---|---|---|
GET | /students | 渲染首页 | ||
GET | /students/new | 添加学生页面 | ||
POST | /students/new | name、age、gender、hobbies | 处理添加学生请求 | |
GET | /students/edit | id | 渲染编辑页面 | |
POST | /students/edit | id、name、age、gender、hobbies | 处理编辑学生请求 | |
GET | /students/delete | id | 处理删除学生请求 |
备注配置 art-template 模板引擎
- 安装
npm install -S art-template express-art-template
在 Express 获取表单 POST 请求体数据
npm install -S body-parser
文件信息
- db.json文件
本设计中没有运用数据库开发,所以在此用文件读取的方式来代替数据库,后续再更新增加了数据库的版本。 - 文件内容
{
"students": [
{
"id": 1,
"name": "张三",
"gender": 0,
"age": 18,
"hobbies": "吃饭,睡觉,敲代码"
},
{
"id": 2,
"name": "张三",
"gender": 0,
"age": 18,
"hobbies": "吃饭,睡觉,敲代码"
},
{
"id": 3,
"name": "张三",
"gender": 0,
"age": 18,
"hobbies": "吃饭,睡觉,敲代码"
},
{
"id": 5,
"name": "张三",
"gender": 0,
"age": 18,
"hobbies": "吃饭,睡觉,敲代码"
},
{
"id": 6,
"name": "张三",
"gender": 0,
"age": 18,
"hobbies": "吃饭,睡觉,敲代码"
},
{
"name": "李四",
"gender": "0",
"age": "22",
"hobbies": "敲代码,跳绳",
"id": 8
}
]
}
设计操作数据的文件模块
/**
* student.js
* 数据操作文件模块
* 职责:操作文件中的数据,只处理数据,不关心业务
*
* 这里才是我们学习 Node 的精华部分:奥义之所在
* 封装异步 API
*/
var fs = require('fs')
var dbPath = './db.json'
/**
* 获取学生列表
* @param {Function} callback 回调函数
*/
exports.find = function (callback) {
fs.readFile(dbPath, 'utf8', function (err, data) {
if (err) {
return callback(err)
}
callback(null, JSON.parse(data).students)
})
}
/**
* 根据 id 获取学生信息对象
* @param {Number} id 学生 id
* @param {Function} callback 回调函数
*/
exports.findById = function (id, callback) {
fs.readFile(dbPath, 'utf8', function (err, data) {
if (err) {
return callback(err)
}
var students = JSON.parse(data).students
var ret = students.find(function (item) {
return item.id === parseInt(id)
})
callback(null, ret)
})
}
/**
* 添加保存学生
* @param {Object} student 学生对象
* @param {Function} callback 回调函数
*/
exports.save = function (student, callback) {
fs.readFile(dbPath, 'utf8', function (err, data) {
if (err) {
return callback(err)
}
var students = JSON.parse(data).students
// 添加 id ,唯一不重复
student.id = students[students.length - 1].id + 1
// 把用户传递的对象保存到数组中
students.push(student)
// 把对象数据转换为字符串
var fileData = JSON.stringify({
students: students
})
// 把字符串保存到文件中
fs.writeFile(dbPath, fileData, function (err) {
if (err) {
// 错误就是把错误对象传递给它
return callback(err)
}
// 成功就没错,所以错误对象是 null
callback(null)
})
})
}
/**
* 更新学生
*/
exports.updateById = function (student, callback) {
fs.readFile(dbPath, 'utf8', function (err, data) {
if (err) {
return callback(err)
}
var students = JSON.parse(data).students
// 注意:这里记得把 id 统一转换为数字类型
student.id = parseInt(student.id)
// 你要修改谁,就需要把谁找出来
// EcmaScript 6 中的一个数组方法:find
// 需要接收一个函数作为参数
// 当某个遍历项符合 item.id === student.id 条件的时候,find 会终止遍历,同时返回遍历项
var stu = students.find(function (item) {
return item.id === student.id
})
// 这种方式你就写死了,有 100 个难道就写 100 次吗?
// stu.name = student.name
// stu.age = student.age
// 遍历拷贝对象
for (var key in student) {
stu[key] = student[key]
}
// 把对象数据转换为字符串
var fileData = JSON.stringify({
students: students
})
// 把字符串保存到文件中
fs.writeFile(dbPath, fileData, function (err) {
if (err) {
// 错误就是把错误对象传递给它
return callback(err)
}
// 成功就没错,所以错误对象是 null
callback(null)
})
})
}
/**
* 删除学生
*/
exports.deleteById = function (id, callback) {
fs.readFile(dbPath, 'utf8', function (err, data) {
if (err) {
return callback(err)
}
var students = JSON.parse(data).students
// findIndex 方法专门用来根据条件查找元素的下标
var deleteId = students.findIndex(function (item) {
return item.id === parseInt(id)
})
// 根据下标从数组中删除对应的学生对象
students.splice(deleteId, 1)
// 把对象数据转换为字符串
var fileData = JSON.stringify({
students: students
})
// 把字符串保存到文件中
fs.writeFile(dbPath, fileData, function (err) {
if (err) {
// 错误就是把错误对象传递给它
return callback(err)
}
// 成功就没错,所以错误对象是 null
callback(null)
})
})
}
app.js入门模块
/**
* app.js入门模块
* 职责:
* 创建服务
* 做一些服务相关配置
* 模板引擎
* body-parse 解析post请求体
* 提供静态服务资源
* 挂载路由
* 监听端口启动服务
*/
var express = require("express");
var router = require("./router");
const bodyParser = require("body-parser");
var app = express();
app.use("/public/", express.static("./public/")); //相当于读取静态文件
app.engine("html", require("express-art-template"));
//配置模板引擎和body-parser一定要在app.use(router)挂载路由之前
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// router(app);
//把路由容器挂载到app服务中
app.use(router);
app.listen(2000, function () {
console.log("running 2000......");
});
module.exports = app; //导出app
router.js 路由模块
这里的路由模块设计的思想是根据回调函数的思想来设计的。
/**
* router.js 路由模块
* 职责:
* 处理路由
* 根据不同的请求方法+请求路径设置具体的请求处理函数
* 模块职责要单一
*/
//把app加载进来
// var app = require("./app");
var fs = require("fs");
var Student = require("./student");
//express提供了一个很好的方式
//专门用来包装路由的
var express = require("express");
//1、创建一个路由容器
var router = express.Router();
//2、把路由挂载到router路由容器中
router.get("/students", function (req, res) {
//readFile的第二个参数是可选的,传入utf8 就是告诉它把读取到的文件直接按照utf8编码转成我们能认识的字符
//也可以通过data.toString()的方式
// fs.readFile("./db.json", "utf8", function (err, data) {
// if (err) {
// return res.status(500).send("Server Error!");
// }
// res.render("index.html", {
// fruits: ["苹果", "香蕉", "葡萄", "橘子"],
// students: JSON.parse(data).students,
// });
// });
Student.find(function (err, students) {
if (err) {
return res.status(500).send("Server Error!");
}
res.render("index.html", {
fruits: ["苹果", "香蕉", "葡萄", "橘子"],
students: students,
});
});
});
router.get("/students/new", function (req, res) {
res.render("new.html");
});
router.post("/students/new", function (req, res) {
//1、获取表单请求
//2、处理
//将数据保存到db.json文件中以持久化
//3、发送响应
//先读取处理,转成对象
//然后往数据中push数据
//将对象转换为字符串
//最后将字符串写入文件
Student.save(req.body, function (err) {
if (err) {
return res.status(500).send("Server Error!");
}
res.redirect("/students");
});
});
router.get("/students/edit", function (req, res) {
//1、在客户端的列表页中处理链接问题(需要id参数)
//2、获取需要编辑学生的id
//3、渲染编辑页面
Student.findById(parseInt(req.query.id), function (err, student) {
if (err) {
return res.status(500).send("Server Error!");
}
res.render("edit.html", {
student: student,
});
});
});
router.post("/students/edit", function (req, res) {
Student.updateById(req.body, function (err) {
if (err) {
return res.status(500).send("Server Error!");
}
res.redirect("/students");
});
});
router.get("/students/delete", function (req, res) {
//1、获取要删除的id
//2、根据id进行删除操作
//3、根据操作结果发送响应数据
Student.deleteById(req.query.id, function (err) {
if (err) {
return res.status(500).send("Server Error!");
}
res.redirect("/students");
});
});
//3、把router导出
module.exports = router;
//这样使用不方便
// module.exports = function (app) {
// app.get("/students", function (req, res) {
// //readFile的第二个参数是可选的,传入utf8 就是告诉它把读取到的文件直接按照utf8编码转成我们能认识的字符
// //也可以通过data.toString()的方式
// fs.readFile("./db.json", "utf8", function (err, data) {
// if (err) {
// return res.status(500).send("Server Error!");
// }
// res.render("index.html", {
// fruits: ["苹果", "香蕉", "葡萄", "橘子"],
// students: JSON.parse(data).students,
// });
// });
// });
// };
views页面展示效果
以下这些页面的实现都是在bootstrap官网上找的模板。
链接
https://v3.bootcss.com/examples/dashboard/
index.html首页
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
<meta name="description" content="" />
<meta name="author" content="" />
<!-- <link rel="icon" href="../../favicon.ico" /> -->
<title>Dashboard Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
/>
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<link
href="../../assets/css/ie10-viewport-bug-workaround.css"
rel="stylesheet"
/>
<!-- Custom styles for this template -->
<link rel="stylesheet" href="../public/css/main.css" />
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<button
type="button"
class="navbar-toggle collapsed"
data-toggle="collapse"
data-target="#navbar"
aria-expanded="false"
aria-controls="navbar"
>
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Project name</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li><a href="#">Dashboard</a></li>
<li><a href="#">Settings</a></li>
<li><a href="#">Profile</a></li>
<li><a href="#">Help</a></li>
</ul>
<form class="navbar-form navbar-right">
<input type="text" class="form-control" placeholder="Search..." />
</form>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li class="active">
<a href="#">Overview <span class="sr-only">(current)</span></a>
</li>
<li><a href="#">Reports</a></li>
<li><a href="#">Analytics</a></li>
<li><a href="#">Export</a></li>
</ul>
<ul class="nav nav-sidebar">
<li><a href="">Nav item</a></li>
<li><a href="">Nav item again</a></li>
<li><a href="">One more nav</a></li>
<li><a href="">Another nav item</a></li>
<li><a href="">More navigation</a></li>
</ul>
<ul class="nav nav-sidebar">
<li><a href="">Nav item again</a></li>
<li><a href="">One more nav</a></li>
<li><a href="">Another nav item</a></li>
</ul>
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header">Dashboard</h1>
<div class="row placeholders">
{{ each fruits }}
<div class="col-xs-6 col-sm-3 placeholder">
<img
src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="
width="200"
height="200"
class="img-responsive"
alt="Generic placeholder thumbnail"
/>
<h4>{{ $value }}</h4>
<span class="text-muted">Something else</span>
</div>
{{ /each }}
</div>
<h2 class="sub-header">Section title</h2>
<a class="btn btn-success" href="/students/new">添加学生</a>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>姓名</th>
<th>性别</th>
<th>年龄</th>
<th>爱好</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{{each students}}
<tr>
<td>{{$value.id}}</td>
<td>{{$value.name}}</td>
<td>{{$value.gender}}</td>
<td>{{$value.age}}</td>
<td>{{$value.hobbies}}</td>
<td>
<a href="/students/edit?id={{$value.id}}">编辑</a>
<a href="/students/delete?id={{$value.id}}">删除</a>
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
</div>
</body>
</html>
new.html添加学生页面
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
<meta name="description" content="" />
<meta name="author" content="" />
<!-- <link rel="icon" href="../../favicon.ico" /> -->
<title>Dashboard Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
/>
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<link
href="../../assets/css/ie10-viewport-bug-workaround.css"
rel="stylesheet"
/>
<!-- Custom styles for this template -->
<link rel="stylesheet" href="../public/css/main.css" />
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<button
type="button"
class="navbar-toggle collapsed"
data-toggle="collapse"
data-target="#navbar"
aria-expanded="false"
aria-controls="navbar"
>
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Project name</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li><a href="#">Dashboard</a></li>
<li><a href="#">Settings</a></li>
<li><a href="#">Profile</a></li>
<li><a href="#">Help</a></li>
</ul>
<form class="navbar-form navbar-right">
<input type="text" class="form-control" placeholder="Search..." />
</form>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li class="active">
<a href="#">Overview <span class="sr-only">(current)</span></a>
</li>
<li><a href="#">Reports</a></li>
<li><a href="#">Analytics</a></li>
<li><a href="#">Export</a></li>
</ul>
<ul class="nav nav-sidebar">
<li><a href="">Nav item</a></li>
<li><a href="">Nav item again</a></li>
<li><a href="">One more nav</a></li>
<li><a href="">Another nav item</a></li>
<li><a href="">More navigation</a></li>
</ul>
<ul class="nav nav-sidebar">
<li><a href="">Nav item again</a></li>
<li><a href="">One more nav</a></li>
<li><a href="">Another nav item</a></li>
</ul>
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h2 class="sub-header">添加学生</h2>
<form action="/students/new" method="POST">
<div class="form-group">
<label for="name">姓名</label>
<input
type="text"
class="form-control"
id="name"
name="name"
placeholder="Email"
/>
</div>
<div class="form-group">
<label>性别</label>
<div>
<label class="radio-inline">
<input type="radio" name="gender" id="gender1" value="0" /> 男
</label>
<label class="radio-inline">
<input type="radio" name="gender" id="gender2" value="1" /> 女
</label>
</div>
</div>
<div class="form-group">
<label for="age">年龄</label>
<input type="number" id="age" name="age" class="form-control" />
</div>
<div class="form-group">
<label>爱好</label>
<input
type="text"
id="hobbies"
name="hobbies"
class="form-control"
/>
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
</div>
</div>
</body>
</html>
edit.html编辑学生页面
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
<meta name="description" content="" />
<meta name="author" content="" />
<!-- <link rel="icon" href="../../favicon.ico" /> -->
<title>Dashboard Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
/>
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<link
href="../../assets/css/ie10-viewport-bug-workaround.css"
rel="stylesheet"
/>
<!-- Custom styles for this template -->
<link rel="stylesheet" href="../public/css/main.css" />
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<button
type="button"
class="navbar-toggle collapsed"
data-toggle="collapse"
data-target="#navbar"
aria-expanded="false"
aria-controls="navbar"
>
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Project name</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li><a href="#">Dashboard</a></li>
<li><a href="#">Settings</a></li>
<li><a href="#">Profile</a></li>
<li><a href="#">Help</a></li>
</ul>
<form class="navbar-form navbar-right">
<input type="text" class="form-control" placeholder="Search..." />
</form>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li class="active">
<a href="#">Overview <span class="sr-only">(current)</span></a>
</li>
<li><a href="#">Reports</a></li>
<li><a href="#">Analytics</a></li>
<li><a href="#">Export</a></li>
</ul>
<ul class="nav nav-sidebar">
<li><a href="">Nav item</a></li>
<li><a href="">Nav item again</a></li>
<li><a href="">One more nav</a></li>
<li><a href="">Another nav item</a></li>
<li><a href="">More navigation</a></li>
</ul>
<ul class="nav nav-sidebar">
<li><a href="">Nav item again</a></li>
<li><a href="">One more nav</a></li>
<li><a href="">Another nav item</a></li>
</ul>
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h2 class="sub-header">编辑学生</h2>
<form action="/students/edit" method="POST">
<input type="hidden" name="id" value="{{student.id}}" />
<div class="form-group">
<label for="name">姓名</label>
<input
type="text"
class="form-control"
id="name"
name="name"
placeholder="Email"
value="{{student.name}}"
/>
</div>
<div class="form-group">
<label>性别</label>
<div>
<label class="radio-inline active">
<input type="radio" name="gender" id="gender1" value="0" /> 男
</label>
<label class="radio-inline">
<input type="radio" name="gender" id="gender2" value="1" /> 女
</label>
</div>
</div>
<div class="form-group">
<label for="age">年龄</label>
<input
type="number"
id="age"
name="age"
class="form-control"
value="{{student.age}}"
/>
</div>
<div class="form-group">
<label>爱好</label>
<input
type="text"
id="hobbies"
name="hobbies"
class="form-control"
value="{{student.hobbies}}"
/>
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
</div>
</div>
</body>
</html>
最后有一个需要的css样式
/*
* Base structure
*/
/* Move down content because we have a fixed navbar that is 50px tall */
body {
padding-top: 50px;
}
/*
* Global add-ons
*/
.sub-header {
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
/*
* Top navigation
* Hide default border to remove 1px line.
*/
.navbar-fixed-top {
border: 0;
}
/*
* Sidebar
*/
/* Hide for mobile, show later */
.sidebar {
display: none;
}
@media (min-width: 768px) {
.sidebar {
position: fixed;
top: 51px;
bottom: 0;
left: 0;
z-index: 1000;
display: block;
padding: 20px;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
background-color: #f5f5f5;
border-right: 1px solid #eee;
}
}
/* Sidebar navigation */
.nav-sidebar {
margin-right: -21px; /* 20px padding + 1px border */
margin-bottom: 20px;
margin-left: -20px;
}
.nav-sidebar > li > a {
padding-right: 20px;
padding-left: 20px;
}
.nav-sidebar > .active > a,
.nav-sidebar > .active > a:hover,
.nav-sidebar > .active > a:focus {
color: #fff;
background-color: #428bca;
}
/*
* Main content
*/
.main {
padding: 20px;
}
@media (min-width: 768px) {
.main {
padding-right: 40px;
padding-left: 40px;
}
}
.main .page-header {
margin-top: 0;
}
/*
* Placeholder dashboard ideas
*/
.placeholders {
margin-bottom: 30px;
text-align: center;
}
.placeholders h4 {
margin-bottom: 0;
}
.placeholder {
margin-bottom: 20px;
}
.placeholder img {
display: inline-block;
border-radius: 50%;
}