本讲是项目的收官阶段。前三讲分别完成了页面重构(Day 1)、数据库搭建(Day 2)、服务器与接口开发(Day 3),本讲将系统讲解新闻系统的整体架构与数据流,精读前台新闻展示页与后台管理系统两个页面的完整实现,掌握前后端联调的排错方法,并通过限时复刻与分组答辩完成项目验收。
《企业技能实习》· 新闻系统全栈项目实训
前置教程
- Day 3 · Node.js 与 Express 讲义,本讲假设服务器与四个接口已全部跑通。
资源下载
- 无需额外页面素材:
tmt/index.html与system/index.html的完整代码分别在 2.4 节与 3.7 节,可直接复制使用 - 需自备两个库文件(课程资料包内):
bootstrap.min.css拷入src/css、bootstrap.min.js拷入src/js - 成品参考图:
成品效果图/index.png、成品效果图/system.png
1. 项目架构总览
1.1 全栈数据流
以下主线图是整个项目的骨架(建议把这条链路记熟):
(新闻展示页)"] SYS["system/index.html
(新闻管理系统页)"] end subgraph S["服务器(Node+Express,8888)"] GET["GET /news"] CRUD["4 个接口
create / deleteOne / updateOne"] end subgraph D["数据库(MongoDB,27017)"] NL[("news.newsList")] end B ~~~ S S ~~~ D TMT -- "axios.get" --> GET SYS -- "axios.post" --> CRUD GET -- "dataModel.find()" --> NL CRUD -- "mongoose 遥控" --> NL NL -. "读:find 结果
写:写入确认" .-> S S -. "res.data.list → 页面渲染(jQuery append) / 操作结果" .-> B
1.2 一次"添加新闻"的完整链路
以管理员在后台新增一条新闻为例,数据经历以下八步旅行(答辩高频题):
- 管理员在后台模态框填写表单,点击"保存";
axios.post("/news", {date, title, author, imgSrc})发出请求,JSON 数据放在请求体中;- Express 在 8888 端口接收到请求,匹配到
myapp.post("/news", ...); - body-parser 将请求体解析为 JS 对象,放入
req.body; dataModel.create(newsObj)经 Mongoose 将数据写入 MongoDB 的 newsList 集合;- 数据库返回携带新
_id的完整文档,response.json({ news })返回给前端; - 前端在
.then中拿到res.data.news,向表格append一行新的<tr>; - 管理员看到新行;此时 Compass 刷新后同样多出一条——同一份数据的两个视图。
思考:第 6 步为什么必须把新文档传回前端?前端不能自行拼一个吗?(提示:新增的这一行之后还要编辑和删除,靠什么定位?)
1.3 两套页面的职责划分
- tmt 新闻展示页:面向读者,只读展示;
- system 管理系统页:面向管理员,提供增删改查;
- 两套页面共用同一批接口——页面可以随意更换,接口保持不变,这就是"前后端分离"的雏形;
- 同源请求:页面本身运行在 8888 端口上,因此
http://localhost:8888/news也可以简写为/news,两种写法效果相同。
2. 前台新闻展示页实现精读
2.1 Swiper 轮播图
var mySwiper = new Swiper(".swiper-container", {
loop: true, // 循环播放
autoplay: true, // 自动播放
pagination: { el: ".swiper-pagination" }
});
对应结构:.swiper-container > .swiper-wrapper > .swiper-slide,slide 内部包含遮罩 .mask > h2,分页器样式自行定制。
2.2 新闻列表渲染
axios.get("http://localhost:8888/news").then(function (res) {
res.data.list.forEach(function (item) { // item 即一条新闻文档
var li = $("<li class='slide-news'>");
li.html(`<img src="${item.imgSrc}" alt=""><div class="info"><h3 class="title">${item.title}</h3><a class="author" href="#">${item.author}</a><span class="time">${item.date}</span></div>`);
$(".news ul").append(li);
});
});
前端拿到的数据与后端代码的三层对应关系:
| 前端拿到的 | 来自后端的 |
|---|---|
res.data |
response.json({ list: ... }) |
res.data.list |
{ list: find 的结果数组 } |
item._id / item.title / ... |
MongoDB 文档字段 |
回顾 Day 1:当时提到"数组换成服务器请求,页面代码几乎不用改"。对照可见——
fakeNews.forEach换成res.data.list.forEach,其余一行未动。数据驱动页面。
2.3 验证:数据库与页面的联动
依次执行以下三步,观察数据变化传导到页面:
- 在 Compass 中手工修改一条新闻的标题 → 刷新 tmt 页面 → 标题随之变化;
- 在 Compass 中手工删除一条 → 刷新 → 前台少一条;
- 在后台 system 中添加一条 → Compass 中出现 → 前台刷新后出现。
结论:页面上的每一条新闻都不是代码,而是数据。
2.4 完整代码参考(tmt/index.html)
以下为前台页面的完整最终版(Day 1 页面 + 真实接口数据),未完成的同学可直接复制到 src/tmt/index.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>新闻</title>
<!-- 轮播图样式 -->
<link rel="stylesheet" href="../css/swiper.min.css" />
<style>
* { margin: 0; padding: 0; }
body { font-family: arial; }
/* ===== 顶栏 ===== */
.header-top {
width: 100%; height: 50px; padding: 14px 15px;
box-sizing: border-box; background-color: #fff;
overflow: hidden;
}
.header-top img { width: 60px; float: left; }
.header-top .nav { float: right; font-size: 16px; outline: none; }
.header-top .nav ul { list-style: none; float: left; }
.header-top .nav ul li { float: left; margin-right: 15px; }
.header-top .nav ul li a { color: #333; font-size: 14px; text-decoration: none; font-weight: bold; }
.header-top .nav img { float: right; width: 18px; margin-top: 1px; }
/* ===== 轮播图:遮罩与标题 ===== */
.swiper-container { width: 100vw; height: 60vw; position: relative; }
.swiper-container .swiper-slide { position: relative; }
.swiper-container .swiper-slide img { width: 100%; }
.swiper-container .swiper-slide .mask {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,.6) 100%);
}
.swiper-container .swiper-slide h2 {
color: #fff; position: absolute; left: 20px; bottom: 20px;
font-weight: normal; font-size: 16px;
display: -webkit-box; -webkit-box-orient: vertical; box-orient: vertical;
-webkit-line-clamp: 2; line-clamp: 2; overflow: hidden;
}
.swiper-pagination-bullet { width: 15px; height: 2px; background: #fff; border-radius: 2px; opacity: .49; }
.swiper-pagination-bullet-active { width: 30px; background: #49c2c8; opacity: 1; }
/* ===== 频道菜单:横向滚动 ===== */
.main .menu-list { background: #f4f4f4; box-sizing: border-box; overflow-x: auto; padding: 15px; }
.main .menu-list ul { list-style: none; white-space: nowrap; }
.main .menu-list ul li { display: inline-block; margin-right: 25px; }
.main .menu-list ul li a { color: #999; font-size: 16px; text-decoration: none; }
.main .menu-list ul li a.active { color: #333; }
/* ===== 新闻列表:左图右文 ===== */
.news ul { list-style: none; }
.news ul li { border-bottom: 1px solid #eeeeee; padding: 20px 15px; overflow: hidden; }
.news ul li img { width: 31.4vw; border-radius: 5px; float: left; }
.news ul li .info { float: right; width: 57vw; }
.news ul li .info .title {
font-weight: normal; color: #333; font-size: 16px; min-height: 2.63em;
display: -webkit-box; -webkit-box-orient: vertical; box-orient: vertical;
-webkit-line-clamp: 2; line-clamp: 2; overflow: hidden;
word-wrap: break-word; white-space: normal;
}
.news ul li .info .author { color: #999; font-size: 11px; display: block; margin: 10px 0; }
.news ul li .info .time { color: #999; font-size: 11px; }
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="header-top">
<img src="../images/h5_logo.png" alt="logo" />
<div class="nav">
<ul>
<li><a href="#">PRO</a></li>
<li><a href="#">科股宝</a></li>
<li><a href="#">视频</a></li>
<li><a href="#">音频</a></li>
<li><a href="#">EN</a></li>
</ul>
<img src="../images/h5_menu.png" alt="menu" />
</div>
</div>
<div class="banner swiper-container">
<div class="swiper-wrapper">
<div class="swiper-slide">
<img src="../images/1.jpg" alt="" />
<div class="mask"><h2>年轻人痴迷的玄学,是一门好生意吗?</h2></div>
</div>
<div class="swiper-slide">
<img src="../images/2.jpg" alt="" />
<div class="mask"><h2>蛋壳倒下,自如"割肉"止损,长租公寓还有救吗?</h2></div>
</div>
<div class="swiper-slide">
<img src="../images/3.jpg" alt="" />
<div class="mask"><h2>新造车势力让华为走开</h2></div>
</div>
<div class="swiper-slide">
<img src="../images/4.jpg" alt="" />
<div class="mask"><h2>寻找年度赢家,「EDGE Awards 全球评选」正式开启</h2></div>
</div>
</div>
<div class="swiper-pagination"></div>
</div>
</header>
<div class="main">
<div class="menu-list">
<ul>
<li><a class="active" href="#">推荐</a></li>
<li><a href="#">最新</a></li>
<li><a href="#">专题</a></li>
<li><a href="#">标识人物</a></li>
<li><a href="#">深度</a></li>
<li><a href="#">产业</a></li>
</ul>
</div>
<!-- 新闻列表:内容全部由接口数据动态生成 -->
<div class="news"><ul></ul></div>
</div>
</div>
<script src="../js/jquery-3.5.1.min.js"></script>
<script src="../js/axios.min.js"></script>
<script src="../js/swiper.min.js"></script>
<script>
// 轮播图
var mySwiper = new Swiper(".swiper-container", {
loop: true, // 循环播放
autoplay: true, // 自动播放
pagination: { el: ".swiper-pagination" }
});
// 向服务器请求新闻列表并渲染
axios.get("http://localhost:8888/news").then(function (res) {
res.data.list.forEach(function (item) {
var li = $("<li class='slide-news'>");
li.html(`<img src="${item.imgSrc}" alt=""><div class="info"><h3 class="title">${item.title}</h3><a class="author" href="#">${item.author}</a><span class="time">${item.date}</span></div>`);
$(".news ul").append(li);
});
});
</script>
</body>
</html>
3. 后台管理系统实现精读
3.1 Bootstrap 常用组件
引入 bootstrap.min.css 与 bootstrap.min.js(后者依赖 jQuery)后即可使用整套现成组件。请先把这两个文件分别拷入 src/css 与 src/js(课程资料包内有)。本页用到的类名速查:
| 组件 | 类名 |
|---|---|
| 表格 | table table-bordered table-hover |
| 主按钮(蓝) | btn btn-primary |
| 危险按钮(红) | btn btn-danger |
| 小尺寸按钮 | btn-sm |
| 模态框 | .modal > .modal-dialog > .modal-content(header/body/footer 三段) |
| 表单 | .form-group + .form-control |
对照后台设计稿与成品图:后台 90% 的外观由 Bootstrap 提供,我们只需编写少量微调样式。
3.2 难点一:为动态创建的元素绑定事件
列表中的删除、编辑按钮是请求返回后由 tr.html(...) 动态创建的。$('.del').click(...) 在页面加载时就查找按钮——彼时按钮尚不存在,事件绑定不上。
本项目的解法:创建行时直接将事件写入标签——
onclick='delFun("${item._id}", this)'
item._id作为参数传入(知道删除哪一条);this将按钮自身传入(知道删除哪一行);- 删除该行只需一句:
$(ele).parents("tr").remove()。
3.3 难点二:编辑与添加共用一个模态框
两个按钮打开的是同一个 .modal,保存时程序如何区分当前操作?依靠状态变量:
editNewsId = 点击编辑时记住"改哪一条"
isEditOrAdd = "edit" / "add",保存按钮据此分流
hide.bs.modal 关闭监听 → 清空表单,防止"先编辑后添加"串数据
思考:如果关闭时不清空表单,会引发什么事故?(提示:先编辑过 A,再点击添加,弹窗中显示的是谁的内容?直接保存会发生什么?)
3.4 列表渲染与删除
axios.get("http://localhost:8888/news").then(function (res) {
res.data.list.forEach(function (item, index) {
var tr = $(`<tr id='${item._id}'>`); // 行 id = 文档 _id,编辑删除全靠它定位
tr.html(`<td class='ind'>${index + 1}</td>
<td class='date'>${item.date}</td>
<td class='title'>${item.title}</td>
<td class='author'>${item.author}</td>
<td class='img'><img src='${item.imgSrc}' /></td>
<td>
<button onclick='editFun("${item._id}")' class="btn btn-primary btn-sm">编辑</button>
<button onclick='delFun("${item._id}",this)' class="btn btn-danger btn-sm">删除</button>
</td>`);
$(".table tbody").append(tr);
});
});
function delFun(id, ele) {
if (confirm("是否确定删除?")) {
axios.post(`http://localhost:8888/news/${id}`).then(function () {
$(ele).parents("tr").remove(); // 后端删除完成后,前端再删除行
});
}
}
数据库中删除一次(接口),页面中删除一次(DOM)——两个"消失"是两条路径,缺少任何一条都会出现"看似没了其实还在"或"还在却看不见"。
3.5 编辑回填与保存
var editNewsId = "";
var isEditOrAdd = "";
function editFun(id) {
editNewsId = id;
isEditOrAdd = "edit";
// 从当前行取出内容回填弹窗(页面本身就是数据,无需再次请求)
var date = $(`#${id} .date`).text();
var title = $(`#${id} .title`).text();
var author = $(`#${id} .author`).text();
var imgSrc = $(`#${id} .img img`).attr("src");
$(".modal-body #dateInput").val(date);
$(".modal-body #titleInput").val(title);
$(".modal-body #authorInput").val(author);
$(".modal-body #imgSrcInput").val(imgSrc);
$(".modal").modal("show");
}
$(".add").click(function () {
isEditOrAdd = "add";
$(".modal").modal("show");
});
$(".modal #save").on("click", function () {
var date = $(".modal-body #dateInput").val();
var title = $(".modal-body #titleInput").val();
var author = $(".modal-body #authorInput").val();
var imgSrc = $(".modal-body #imgSrcInput").val();
if (isEditOrAdd === "edit") {
// 编辑保存 → 发送更新请求 → 直接修改页面文字,不再重新请求列表
axios.post(`http://localhost:8888/news/${editNewsId}/update`,
{ date: date, title: title, author: author, imgSrc: imgSrc })
.then(function () {
$(".modal").modal("hide");
$(`#${editNewsId} .date`).text(date);
$(`#${editNewsId} .title`).text(title);
$(`#${editNewsId} .author`).text(author);
$(`#${editNewsId} .img img`).attr("src", imgSrc);
});
} else {
// 添加保存 → 发送新增请求 → 用返回的新文档(含 _id)append 一行
axios.post(`http://localhost:8888/news`,
{ date: date, title: title, author: author, imgSrc: imgSrc })
.then(function (res) {
$(".modal").modal("hide");
var news = res.data.news;
/* 按 3.4 同款模板生成 tr 并 append,完整写法见 3.7 完整代码 */
});
}
});
// 弹窗关闭时清空表单,避免编辑与添加互相串数据
$(".modal").on("hide.bs.modal", function () {
$(".modal-body #dateInput").val("");
$(".modal-body #titleInput").val("");
$(".modal-body #authorInput").val("");
$(".modal-body #imgSrcInput").val("");
});
3.6 验证:完整的增删改查闭环
依次执行:后台添加一条新闻 → 保存 → Compass 中出现 → 前台 tmt 刷新后出现 → 后台删除 → 前台消失。
增删改查四条链路至此全部打通。
3.7 完整代码参考(system/index.html)
以下为后台页面的完整代码(3.4 与 3.5 的片段在此合为一体),可直接复制到 src/system/index.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>新闻管理系统</title>
<!-- Bootstrap 样式:表格、按钮、模态框的外观都由它提供 -->
<link rel="stylesheet" href="../css/bootstrap.min.css" />
<style>
* { margin: 0; padding: 0; }
/* 页面居中容器 */
.wrap { width: 90%; margin: 0 auto; }
h1 { float: left; margin-top: 40px; margin-bottom: 20px; }
.add { float: right; margin-top: 50px; margin-bottom: 20px; }
.clear { clear: both; }
/* 表格整体 */
.table { width: 100%; color: #909399; }
.table img { width: 80px; } /* 新闻缩略图 */
/* 表头居中,标题列左对齐 */
.table.table-bordered > thead > tr > th { text-align: center; }
.table.table-bordered > thead > tr > th.title { text-align: left; }
/* 内容格:行高等于图片高度,实现垂直居中 */
.table.table-bordered > tbody > tr > td {
padding: 6px; text-align: center; line-height: 70px; color: #1f2d3d;
}
/* 标题列:可点击的蓝色文字 */
.table.table-bordered > tbody > tr > td.title {
cursor: pointer; color: #20a0ff; text-align: left;
}
</style>
</head>
<body>
<div class="wrap">
<h1>新闻管理系统</h1>
<button type="button" class="add btn btn-primary">添加</button>
<div class="clear"></div>
<!-- 新闻表格:表头写死,内容行由 JS 依据接口数据动态生成 -->
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>序号</th>
<th>日期</th>
<th class="title">标题</th>
<th>作者</th>
<th>图片</th>
<th>操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<!-- 模态框:编辑与添加共用同一个弹窗(Bootstrap 组件) -->
<div class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 class="modal-title">新闻编辑</h4>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<label for="dateInput">日期</label>
<input type="text" class="form-control" id="dateInput" placeholder="日期" />
</div>
<div class="form-group">
<label for="titleInput">标题</label>
<input type="text" class="form-control" id="titleInput" placeholder="标题" />
</div>
<div class="form-group">
<label for="authorInput">作者</label>
<input type="text" class="form-control" id="authorInput" placeholder="作者" />
</div>
<div class="form-group">
<label for="imgSrcInput">图片信息</label>
<input type="text" class="form-control" id="imgSrcInput" placeholder="图片链接" />
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
<button id="save" type="button" class="btn btn-primary">保存</button>
</div>
</div>
</div>
</div>
<!-- JS 库引入:jQuery(DOM 操作)、axios(请求)、Bootstrap(模态框) -->
<script src="../js/jquery-3.5.1.min.js"></script>
<script src="../js/axios.min.js"></script>
<script src="../js/bootstrap.min.js"></script>
<script>
// ===== 功能 1:进入页面就请求新闻列表,循环生成表格行 =====
axios.get("http://localhost:8888/news").then(function (res) {
res.data.list.forEach(function (item, index) {
var tr = $(`<tr id='${item._id}'>`); // 行 id = 文档 _id,编辑删除全靠它定位
tr.html(`<td class='ind'>${index + 1}</td>
<td class='date'>${item.date}</td>
<td class='title'>${item.title}</td>
<td class='author'>${item.author}</td>
<td class='img'><img src='${item.imgSrc}' /></td>
<td>
<button type="button" onclick='editFun("${item._id}")' class="btn btn-primary btn-sm edit">编辑</button>
<button type="button" onclick='delFun("${item._id}",this)' class="btn btn-danger btn-sm del">删除</button>
</td>`);
$(".table tbody").append(tr);
});
});
// ===== 功能 2:删除单条新闻 =====
function delFun(id, ele) {
if (confirm("是否确定删除?")) {
axios.post(`http://localhost:8888/news/${id}`).then(function () {
$(ele).parents("tr").remove(); // 后端删除完成后,前端再删除行
});
}
}
// ===== 功能 3:编辑单条新闻(回填弹窗并记住状态) =====
var editNewsId = ""; // 点击编辑时记住"改哪一条"
var isEditOrAdd = ""; // 保存按钮据此区分当前是编辑还是添加
function editFun(id) {
editNewsId = id;
isEditOrAdd = "edit";
// 从当前行取出内容回填弹窗(页面本身就是数据,无需再次请求)
var date = $(`#${id} .date`).text();
var title = $(`#${id} .title`).text();
var author = $(`#${id} .author`).text();
var imgSrc = $(`#${id} .img img`).attr("src");
$(".modal-body #dateInput").val(date);
$(".modal-body #titleInput").val(title);
$(".modal-body #authorInput").val(author);
$(".modal-body #imgSrcInput").val(imgSrc);
$(".modal").modal("show");
}
// ===== 功能 4:添加新闻(与编辑共用同一个弹窗) =====
$(".add").click(function () {
isEditOrAdd = "add";
$(".modal").modal("show");
});
// ===== 保存按钮:编辑、添加两条分支 =====
$(".modal #save").on("click", function () {
var date = $(".modal-body #dateInput").val();
var title = $(".modal-body #titleInput").val();
var author = $(".modal-body #authorInput").val();
var imgSrc = $(".modal-body #imgSrcInput").val();
if (isEditOrAdd === "edit") {
// 编辑保存 → 发送更新请求 → 直接修改页面文字,不再重新请求列表
axios
.post(`http://localhost:8888/news/${editNewsId}/update`, {
date: date, title: title, author: author, imgSrc: imgSrc
})
.then(function () {
$(".modal").modal("hide");
$(`#${editNewsId} .date`).text(date);
$(`#${editNewsId} .title`).text(title);
$(`#${editNewsId} .author`).text(author);
$(`#${editNewsId} .img img`).attr("src", imgSrc);
});
} else {
// 添加保存 → 发送新增请求 → 用返回的新文档(含 _id)append 一行
axios
.post(`http://localhost:8888/news`, {
date: date, title: title, author: author, imgSrc: imgSrc
})
.then(function (res) {
$(".modal").modal("hide");
var news = res.data.news;
var tr = $(`<tr id='${news._id}'>`);
// 序号 = 当前最后一行的序号 + 1
var index = $(".table tbody tr:last .ind").text() * 1;
tr.html(`<td class='ind'>${index + 1}</td>
<td class='date'>${news.date}</td>
<td class='title'>${news.title}</td>
<td class='author'>${news.author}</td>
<td class='img'><img src='${news.imgSrc}' /></td>
<td>
<button type="button" onclick='editFun("${news._id}")' class="btn btn-primary btn-sm edit">编辑</button>
<button type="button" onclick='delFun("${news._id}",this)' class="btn btn-danger btn-sm del">删除</button>
</td>`);
$(".table tbody").append(tr);
});
}
});
// ===== 弹窗关闭时清空表单,避免编辑与添加互相串数据 =====
$(".modal").on("hide.bs.modal", function () {
$(".modal-body #dateInput").val("");
$(".modal-body #titleInput").val("");
$(".modal-body #authorInput").val("");
$(".modal-body #imgSrcInput").val("");
});
</script>
</body>
</html>
4. 前后端联调排错方法
4.1 报错三看
出现异常时按固定顺序排查:一看浏览器 Console(有无红字报错)→ 二看 Network(请求是否发出、返回了什么)→ 三看服务器控制台;仍未定位则检查数据库窗口。 排错顺序与数据流方向相反:页面 → 请求 → 接口 → 数据库。
4.2 常见问题速查表
| 症状 | 第一排查点 | 第二排查点 |
|---|---|---|
| 页面 404 | 地址拼写 / 文件夹名大小写 | express.static("src") 路径 |
| 接口无响应 | 数据库窗口是否在运行 | 服务器控制台是否报连接失败 |
Cannot find module |
是否在项目目录执行过 npm i | package.json 是否存在 |
req.body 为 undefined |
myapp.use(bodyParser.json()) 位置 |
请求是否使用 JSON 格式 |
| 列表不显示 | F12 Network 查看 /news 的返回 |
Compass 中集合是否有数据 |
| 修改代码不生效 | Ctrl + C 后重新 node app.js |
浏览器 Ctrl + F5 强刷 |
| Compass 连接失败 | mongod 是否启动、27017 占用 | 连接串 mongodb://localhost:27017 |
| 增删改查无效 | 库名 / 集合名与代码是否一致(news / newsList) | _id 是否传对 |
5. 动手练习:限时复刻与分组答辩
5.1 复刻规则
- 从空文件夹出发(可参考笔记、课程资料与本讲 2.4 / 3.7 节的完整代码,不允许整目录拷贝);
- 达标线(60 分钟内):npm 安装依赖 → 服务器托管 → 连接数据库 → 4 个接口 → 两个页面可访问;
- 加分线:增删改查在页面上真实可用(不仅是接口测试);
- 卡住超过 15 分钟可举手,教师给予一次提示(计入记录)。
5.2 任务清单
| 层级 | 任务 |
|---|---|
| 基础必做 | 全站跑通:服务器 + 数据库 + tmt/system 两页面可访问,4 个接口测试通过 |
| 进阶选做 | 页面级增删改查全部可用;前台接入搜索或分页接口;替换为自己的配色与文案 |
| 挑战加分 | 新闻详情页 detail.html(列表点击携带 id 跳转、axios 渲染单条);删除确认改为 Bootstrap 模态框 |
复刻顺序参考(沿用 Day 3 实练的顺序感):
npm init → 安装依赖 → app.js 最小服务器 → 托管 src → 连库 + 模型 → 接口 1~4 → 页面联调
5.3 验收答辩流程
每组 3 分钟,流程固定为三步:
- 演示功能(1 分钟);
- 讲解架构图与数据流(1 分钟);
- 讲解本项目中印象最深的一个 bug 及解决过程(1 分钟)。
5.4 答辩预热题
每组将被抽取 1 题,请提前准备:
- 前台列表的数据从哪里来?中间经过了哪几站?
- 删除一条新闻,前后端各发生了什么?
req.params与req.body的区别?各举一例;- 编辑和添加共用一个弹窗,程序如何区分?
news与newsList分别是什么?写错了会怎样?
6. 学习总结与作品归档
- 学习总结(答辩结束后 24 小时内提交,三段式): - 本项目中的技术收获; - 最大的坑与解决过程; - 后续学习计划。
- 作品归档(同期交学习委员汇总): - 项目打包(打包前删除 node_modules,保留 package.json 即可随时还原); - Day 1–Day 4 学习日志 + 学习总结。
- 后续学习路线建议: - ES6+ 新语法 → Vue / React 框架; - Express 路由分层、登录鉴权(JWT); - MySQL 与关联查询; - Git 版本管理与项目部署上线。
7. 本讲小结
- 架构:浏览器 → axios → Express 接口 → Mongoose → MongoDB,数据沿这条链路双向流动;
- 前台:Swiper 轮播 + axios 渲染列表,页面由数据驱动;
- 后台:Bootstrap 组件 + 增删改查交互,重点在动态元素绑事件与共用模态框的状态管理;
- 排错:Console → Network → 服务器控制台,按数据流反查;
- 至此,一个从数据库到页面的完整全栈系统已经全部过手——下一站,框架。
举手提问