本讲是项目的收官阶段。前三讲分别完成了页面重构(Day 1)、数据库搭建(Day 2)、服务器与接口开发(Day 3),本讲将系统讲解新闻系统的整体架构与数据流,精读前台新闻展示页与后台管理系统两个页面的完整实现,掌握前后端联调的排错方法,并通过限时复刻与分组答辩完成项目验收。

《企业技能实习》· 新闻系统全栈项目实训

前置教程

  • Day 3 · Node.js 与 Express 讲义,本讲假设服务器与四个接口已全部跑通。

资源下载

  • 无需额外页面素材:tmt/index.htmlsystem/index.html 的完整代码分别在 2.4 节与 3.7 节,可直接复制使用
  • 需自备两个库文件(课程资料包内):bootstrap.min.css 拷入 src/cssbootstrap.min.js 拷入 src/js
  • 成品参考图:成品效果图/index.png成品效果图/system.png

1. 项目架构总览

1.1 全栈数据流

以下主线图是整个项目的骨架(建议把这条链路记熟):

flowchart TB subgraph B["用户浏览器"] TMT["tmt/index.html
(新闻展示页)"] 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 一次"添加新闻"的完整链路

以管理员在后台新增一条新闻为例,数据经历以下八步旅行(答辩高频题):

  1. 管理员在后台模态框填写表单,点击"保存";
  2. axios.post("/news", {date, title, author, imgSrc}) 发出请求,JSON 数据放在请求体中;
  3. Express 在 8888 端口接收到请求,匹配到 myapp.post("/news", ...)
  4. body-parser 将请求体解析为 JS 对象,放入 req.body
  5. dataModel.create(newsObj) 经 Mongoose 将数据写入 MongoDB 的 newsList 集合;
  6. 数据库返回携带新 _id 的完整文档response.json({ news }) 返回给前端;
  7. 前端在 .then 中拿到 res.data.news,向表格 append 一行新的 <tr>
  8. 管理员看到新行;此时 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 验证:数据库与页面的联动

依次执行以下三步,观察数据变化传导到页面:

  1. 在 Compass 中手工修改一条新闻的标题 → 刷新 tmt 页面 → 标题随之变化;
  2. 在 Compass 中手工删除一条 → 刷新 → 前台少一条;
  3. 在后台 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.cssbootstrap.min.js(后者依赖 jQuery)后即可使用整套现成组件。请先把这两个文件分别拷入 src/csssrc/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">&times;</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 分钟);
  2. 讲解架构图与数据流(1 分钟);
  3. 讲解本项目中印象最深的一个 bug 及解决过程(1 分钟)。

5.4 答辩预热题

每组将被抽取 1 题,请提前准备:

  1. 前台列表的数据从哪里来?中间经过了哪几站?
  2. 删除一条新闻,前后端各发生了什么?
  3. req.paramsreq.body 的区别?各举一例;
  4. 编辑和添加共用一个弹窗,程序如何区分?
  5. newsnewsList 分别是什么?写错了会怎样?

6. 学习总结与作品归档

  1. 学习总结(答辩结束后 24 小时内提交,三段式): - 本项目中的技术收获; - 最大的坑与解决过程; - 后续学习计划。
  2. 作品归档(同期交学习委员汇总): - 项目打包(打包前删除 node_modules,保留 package.json 即可随时还原); - Day 1–Day 4 学习日志 + 学习总结。
  3. 后续学习路线建议: - ES6+ 新语法 → Vue / React 框架; - Express 路由分层、登录鉴权(JWT); - MySQL 与关联查询; - Git 版本管理与项目部署上线。

7. 本讲小结

  • 架构:浏览器 → axios → Express 接口 → Mongoose → MongoDB,数据沿这条链路双向流动;
  • 前台:Swiper 轮播 + axios 渲染列表,页面由数据驱动;
  • 后台:Bootstrap 组件 + 增删改查交互,重点在动态元素绑事件与共用模态框的状态管理;
  • 排错:Console → Network → 服务器控制台,按数据流反查;
  • 至此,一个从数据库到页面的完整全栈系统已经全部过手——下一站,框架。