主题
综合案例:图书管理
本页对应 1-4综合案例-图书管理,共 8 段视频,约 1 小时 30 分。
学习目标
- 能使用 axios 按接口文档携带 creator 参数获取图书列表数据,并通过
map、join、innerHTML将数据渲染到表格中,同时将图书 ID 存入自定义属性供后续操作使用。 - 能使用 Bootstrap 弹框的属性控制与 JavaScript 控制两种方式,分别实现新增、编辑弹框的显示与隐藏,并说明两种方式各自的适用场景。
- 能独立完成新增图书功能:使用表单序列化插件收集书名、作者、出版社,结合展开运算符合并 creator 后提交到服务器,并在成功后重置表单、关闭弹框、重新获取并渲染最新列表。
- 能使用事件委托为动态生成的删除按钮绑定点击事件,通过类名判断和
return阻断非目标点击,获取父元素上的data-id后调用 DELETE 路径参数接口删除数据,并刷新列表。 - 能完成编辑图书的数据回填:调用图书详情接口获取数据,使用 JavaScript 控制编辑弹框显示,并通过
Object.keys与forEach动态生成选择器、结合中括号语法将数据写入对应表单元素,包括使用隐藏表单元素保存 ID。 - 能完成编辑图书的保存修改:序列化编辑表单并解构所需字段,按 PUT 接口要求将 ID 拼接到 URL、将书名、作者、出版社和 creator 放入请求体提交,成功后关闭弹框并调用列表函数刷新页面。
知识点
本节要完成什么
本节是“图书管理”案例的介绍课,不急着写完整代码,而是先确认案例最终效果、业务功能和实现顺序。为什么需要先介绍?因为增删改查涉及弹框、接口、列表渲染和多次数据刷新,如果一上来就写代码,很容易不知道每一步在整体中的位置。怎么想:先看成品,再拆功能,最后排顺序。怎么做:打开备课代码运行案例,观察默认渲染的三条图书数据——西游记、三国演义、水浒传;点击右上角“添加”,输入书名、作者、出版社后提交;再点击某行末尾“删除”;最后点击“编辑”,修改出版社并保存。易错点:不要把“介绍课”当成“代码课”,本节重点是建立业务地图。
四个功能与业务本质
案例要完成四个功能:渲染图书列表、新增图书、删除图书、编辑图书。为什么需要这四个功能?因为多数项目都会涉及数据管理,虽然数据内容不同,但基本都有“渲染数据、新增数据、删除数据、修改数据”。怎么想:图书数据不在页面里,而在服务器里;前端通过 axios 调接口操作服务器数据,页面只是渲染结果。转写稿中反复强调“数据在服务器”,接口文档里的 creator 是数据所有者外号,通过 query 参数传给服务器,服务器返回属于这个外号的三本书。怎么做:获取数据后渲染列表;新增时收集表单数据提交服务器;删除时调删除接口;编辑时先获取详情,再填表单,最后提交修改。易错点:服务器数据变了,页面不会自动变,必须重新获取数据并渲染;编辑时不要只从页面拿数据,要向服务器获取详情,保证数据准确。
为什么先学弹框、先做列表
推荐顺序是:先学习 Bootstrap 弹框,再做渲染图书列表,然后做新增、删除、编辑。为什么需要先学弹框?因为添加图书和编辑图书都需要弹窗承载表单;不会控制弹框显示隐藏,新增和编辑就无法完整演示。怎么想:先做列表,是因为列表是后续功能的“观察窗口”;新增、删除、编辑后都要重新渲染列表,才能看到数据变化。怎么做:Bootstrap 弹框可用属性控制,如 data-bs-toggle="modal"、data-bs-target="选择器" 显示,data-bs-dismiss="modal" 隐藏;也可用 JS 实例化后调用 show()、hide()。属性控制适合点击后立刻显示或隐藏;JS 控制适合显示或隐藏前还要做别的事,例如新增提交成功后再关闭弹框。易错点:data-bs-target 要写 CSS 选择器;隐藏属性要放在弹框内部元素上;不要把新增弹框和编辑弹框混用——本节明确是两套弹框,后续项目再考虑复用同一个。
推荐实现顺序与调试思路
为什么需要排顺序?因为四个功能互相依赖。怎么想:先做“渲染列表”,让页面有数据;再做“新增”,提交后调用获取列表函数刷新;再做“删除”,通过事件委托绑定动态生成的删除按钮,拿到 ID 后调 delete 接口,再刷新列表;最后做“编辑”,点击编辑先获取该图书详情,弹出编辑框并填表,保存时调 put 接口,再刷新列表并关闭弹框。怎么做:转写稿中的调试过程可作为验证方法:打开浏览器网络面板,查看请求是否发出、预览返回数据、状态码是否成功;例如删除接口成功时可能返回 204,页面不自动更新时要手动调用获取列表函数。易错点:动态生成的删除、编辑按钮要用事件委托;ID 可存在自定义属性或隐藏表单元素中,编辑时常用 type="hidden" 保存 ID,避免用户修改;接口方法 get、post、delete、put 要按文档设置,参数位置要区分 query、path、body。
小结
做完这个案例,收获是掌握前端经典增删改查的业务和思路,对以后开发很有帮助。为什么需要总结?因为本节不是孤立案例,而是可迁移的管理业务模板。怎么想:把“获取数据—渲染数据—提交变更—重新获取—更新页面”作为主线。怎么做:后续遇到类似管理业务,可参考本案例顺序:先列表,再新增、删除、编辑;弹框按场景选择属性控制或 JS 控制。易错点:不要忘记变更后重新获取数据;不要忽略 ID 的保存与传递;不要混淆新增和编辑弹框的职责。
本节要解决的问题
Bootstrap 弹框(Modal)的作用是:不离开当前页面,显示单独内容,供用户操作。例如图书管理里点击“添加”后弹出新增表单,用户填写完再关闭。
这一节的目标很明确:使用属性方式控制 Bootstrap 弹框的显示和隐藏。也就是说,先不写 JS 逻辑,只靠 HTML 自定义属性完成开关弹框。
为什么需要属性控制?因为很多场景只是“点按钮→弹框出现”“点取消/小叉→弹框关闭”,没有必要额外写 JS。用属性控制更直接,结构清楚,调试也快。
怎么想?先确认依赖和弹框结构,再给“触发按钮”加显示属性,给“弹框内部关闭按钮”加隐藏属性。怎么做?按下面步骤实现。
准备依赖与弹框结构
为什么需要先准备依赖?Bootstrap 的弹框样式来自 bootstrap.css,交互行为来自 bootstrap.js。只引 CSS 可能样式对,但点击不会正常开关;只引 JS 又没有样式。
怎么想?先保证页面里已经引入 Bootstrap 的 CSS 和 JS,然后准备弹框标签。笔记中说明:可以从文档的 Modal 里复制基础例子,确认结构。案例里新增图书弹框已经准备好了,所以本节重点不是从零搭结构,而是学会控制它。
怎么做?检查页面是否引入 bootstrap.css 和 bootstrap.js,并找到弹框容器和触发按钮。弹框一般包含标题、内容区、关闭按钮、取消/确认按钮等。
易错点:不要只引一个文件;弹框结构不要自己随意删到缺少 Bootstrap 需要的层级;先确认目标弹框的选择器,再写属性。
用属性控制弹框显示
为什么需要?用户点击“添加”按钮时,需要让指定弹框出现。属性控制的核心是:告诉 Bootstrap“这是一个弹框触发器”,再告诉它“要打开哪个弹框”。
怎么想?显示弹框需要两个属性配合:
data-bs-toggle="modal":表示触发弹框;data-bs-target="CSS选择器":指定要显示哪个弹框。
笔记中的语法图如下:

怎么做?例如给“添加”按钮加上属性,目标选择器要和弹框自身的选择器对应。转写稿中演示时用的是类选择器,类似:
html
<button
class="btn btn-primary"
data-bs-toggle="modal"
data-bs-target=".add-modal"
>
添加
</button>对应弹框容器需要有 .add-modal 这个类。注意:data-bs-target 接收的是 CSS 选择器,不是随便写一段文字。目标用 id 就写 #addModal,目标用 class 就写 .add-modal,点不能漏。
易错点:data-bs-toggle 的值是 modal;data-bs-target 必须能选中目标弹框;属性名拼写要准确,如 data-bs-toggle、data-bs-target。
用属性控制弹框隐藏
为什么需要?弹框弹出后,用户点小叉或“取消”应该能关闭。隐藏属性解决的是“关闭当前所在弹框”。
怎么想?关闭按钮通常位于弹框内部,所以只需要一个属性:data-bs-dismiss="modal"。它关闭的是标签所在的当前提示框,不需要再写 target。
怎么做?给弹框里的小叉、取消按钮加属性:
html
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
取消
</button>转写稿的调试过程很清楚:先只给“添加”按钮加显示属性,点击后弹框能出现;此时点小叉、点取消没有反应。接着给小叉加 data-bs-dismiss="modal",再点小叉,弹框关闭;再给“取消”按钮补同样属性,点取消也能关闭。这说明显示和隐藏是两组属性,不能只加显示不加关闭。
易错点:data-bs-dismiss="modal" 要加在弹框内部的关闭元素上;关闭按钮如果放到弹框外,就不是“关闭所在弹框”的效果;属性值仍是 modal。
调试与小结
调试时建议按顺序来:先确认 CSS/JS 已引入;再确认弹框结构存在;然后只加显示属性,测试能否弹出;最后给关闭按钮补 data-bs-dismiss="modal",测试小叉和取消。这样出错时容易定位是“目标选择器错”还是“关闭属性没加”。
本节两个小结问题:
用哪个属性绑定来控制弹框显示?
答案:data-bs-toggle和data-bs-target。用哪个属性来控制隐藏弹框?
答案:data-bs-dismiss,关闭的是标签所在的当前提示框。
记住一句话:显示靠 toggle + target 找目标,隐藏靠 dismiss 关所在弹框。
目标与场景:为什么要用 JS 控制弹框
本节目标是:使用 JS 方式控制 Bootstrap 弹框的显示和隐藏。
属性方式也能控制弹框:显示时给按钮加 data-bs-toggle="modal" 和 data-bs-target="弹框选择器",隐藏时在弹框内部加 data-bs-dismiss="modal"。它适合“点了就立刻显示/隐藏”的场景。
但图书管理案例里,点击“增加”后并不是立刻关闭弹框:要先收集用户输入的书名、作者、出版社,提交到服务器,成功后再关闭。也就是说,在显示或隐藏之前还要执行一段 JS 逻辑,这时就适合用 JS 方式控制。笔记小结也强调:直接出现/隐藏用属性方式;如果需要先执行一段 JS 逻辑再显示/隐藏,就用 JS 方式。
语法:怎么想与怎么做
JS 控制的核心思路是:先找到弹框的 DOM 元素,把它传给 Bootstrap 提供的 Modal 构造函数,得到实例对象;再通过实例调用 show() 显示、hide() 隐藏。

以案例中的新增弹框为例,代码可以写成下面这样(选择器以你页面中的实际类名为准):
js
// 1. 找到弹框 DOM,并实例化
const addModal = new bootstrap.Modal(document.querySelector('.add-modal'));
// 2. 显示弹框
document.querySelector('.btn-show').addEventListener('click', () => {
addModal.show();
});
// 3. 隐藏弹框
document.querySelector('.add-btn').addEventListener('click', () => {
// 隐藏前还要做别的事:收集用户数据并提交到服务器
// todo...
addModal.hide();
});转写稿中的调试过程是:先在页面上加一个“JS 显示弹框”的测试按钮,绑定点击事件后调用 addModal.show(),确认弹框能弹出;再给“增加”按钮绑定点击事件,调用 addModal.hide(),确认弹框能关闭。最后在 hide() 前留一个 todo:收集用户数据并提交到服务器。这样就把“JS 控制弹框”与后续新增业务衔接起来了。
易错点
new bootstrap.Modal(...)传入的是 DOM 元素,不是选择器字符串。要先document.querySelector(...)取到元素再传进去。Modal的M是大写,show()、hide()是实例方法,不要写成属性方式里的data-bs-*。- 隐藏前如果有业务逻辑,要把逻辑放在
hide()之前;否则弹框关了但数据还没提交。 - 属性方式适合立即显示/隐藏;JS 方式适合显示/隐藏前还要执行逻辑。不要为了简单场景强行写复杂 JS,也不要在需要先提交数据时只用属性关闭。
- 隐藏按钮如果使用属性方式,
data-bs-dismiss="modal"要写在弹框内部的标签上;JS 方式则要先拿到弹框实例再调用hide()。
小结
什么时候用属性控制,什么时候用 JS 控制 Bootstrap 弹框的显示/隐藏?
- 直接出现/隐藏:用属性方式控制。
- 如果需要先执行一段 JS 逻辑再显示/隐藏:用 JS 方式控制。
本节要记住的 JS 控制流程是:找到弹框 DOM → new bootstrap.Modal(dom元素) 创建实例 → 用 show() 显示、hide() 隐藏。
本节目标与两步主线
为什么需要:图书管理案例的第一个功能是“渲染图书列表”。页面打开后要能看到属于自己的三本图书,后续新增、删除、编辑才有操作对象。
怎么想:转写稿把核心拆成两步:先获取数据,再分析数据结构并渲染到页面。数据不在本地,而在服务器,所以不能手写死三条数据。
怎么做:先按接口文档调用图书列表接口,拿到数组;再对照页面结构,用模板字符串生成若干 <tr>,拼接后写入 tbody。
易错点:不要一上来就写渲染。先确认接口返回什么、数组里字段叫什么,再写模板。否则容易出现字段名拼错、页面空白。
为什么需要设置“外号”
为什么需要:所有同学的数据都在同一个服务器上,服务器必须知道“这份图书数据属于谁”,否则无法返回你的三本书。
怎么想:转写稿里的“外号”就是数据所有者。接口参数名应纠正为 creator,不是口音里的 create/creta。请求时通过查询参数告诉服务器。
怎么做:把外号提成全局常量,避免多处写死:
js
const creator = '你的外号'易错点:外号不要直接写死在多个请求里;后续新增、编辑也会用到,统一用常量最稳。
获取图书列表数据
为什么需要:渲染列表的前提是拿到数组数据。转写稿中先通过 axios 发 GET 请求,参数放在 params 中。
怎么想:先 console.log(res) 看返回结构,再右键复制属性路径,确认列表在 res.data.data。
怎么做:
js
async function getBooks() {
const res = await axios({
url: '接口文档中的图书列表地址',
params: { creator }
})
const list = res.data.data
console.log(list) // 调试:确认是数组,且有三条图书
}易错点:忘记 await 会拿到 Promise;参数写成 create 而不是 creator 会拿不到对应数据;不要把 res.data 和 res.data.data 混用。
分析结构并渲染列表
为什么需要:有数据后还要把数组转成页面标签。转写稿中先审查元素,确认每条数据对应一个 <tr>,序号、书名、作者、出版社分别对应 <td>。
怎么想:数组转 HTML 数组用 map,再 join('') 拼成一个字符串,最后用 innerHTML 写入 tbody。序号用 index + 1,因为索引从 0 开始。
怎么做:
js
const htmlArr = list.map((item, index) => `
<tr>
<td>${index + 1}</td>
<td>${item.bookname}</td>
<td>${item.author}</td>
<td>${item.publisher}</td>
<td>
<button class="del">删除</button>
<button class="edit">编辑</button>
</td>
</tr>
`)
const htmlStr = htmlArr.join('')
document.querySelector('.list').innerHTML = htmlStr易错点:字段名要按接口返回写,常见是 bookname/author/publisher;join 要传空字符串,避免标签间出现逗号;innerHTML 会覆盖原有内容;选择器要选到 tbody。
封装 getBooks 与默认调用
为什么需要:转写稿强调,图书列表逻辑后续新增、删除、编辑后都要重新调用,所以不能只写一次散代码。
怎么想:把“获取数据 + 渲染数据”抽到 getBooks 函数中,页面初始化时默认调用一次。
怎么做:在函数内完成 axios 请求、map/join、innerHTML,最后写 getBooks()。
易错点:封装后忘记默认调用,页面不会显示;后续操作后也要记得再次调用,页面才会更新。
调试过程与易错点
为什么需要:转写稿的调试顺序很值得照做:先看 Network 请求是否发出,再看 Preview 是否返回三条数据,再在控制台看 list 是否数组,再看 map 后是否 HTML 数组,最后看 join 后字符串和页面 <tr> 数量。
怎么想:每写一步就验证一步,不要等全部写完才查错。
怎么做:先确认三条默认数据是西游记、三国演义、水浒传;再逐格替换模板内容,序号、书名、作者、出版社依次对上。
易错点:外号写错、字段名拼错、join 漏写空串、选择器写错、索引忘记 +1,都会导致页面显示异常。
目标与整体思路
本节要完成“图书管理—新增图书”。转写稿反复强调:图书数据存在服务器,新增不是只改页面,而是“准备输入区域 → 收集表单数据提交 → 重新获取并渲染列表”。因此新增图书可以拆成三步:先控制新增弹框的显示与隐藏;再在点击添加确认时收集表单数据并提交;最后调用之前封装的 getBooks() 刷新图书列表。
为什么需要:只把数据插到页面,刷新后就会丢;只提交服务器,不重新获取列表,页面又不会自动更新。
怎么想:把“弹框控制”和“数据提交”分开处理,提交成功后再刷新列表。
怎么做:Bootstrap 弹框负责显示隐藏,axios 负责提交,getBooks() 负责刷新。
易错点:转写稿里的“弹簧”是口误,实际是“弹框(modal)”;“date”应理解为 data;“mus/mers”应理解为 method。
弹框控制:属性方式
为什么需要:新增表单默认不显示,点击“添加”才弹出;点“取消”或小叉要能关闭。
怎么想:如果只是点了立刻显示/隐藏,用 Bootstrap 属性最直接。
怎么做:显示弹框要两个属性:data-bs-toggle="modal" 表示弹框,data-bs-target 指定弹框的 CSS 选择器;关闭用 data-bs-dismiss="modal",且关闭按钮要放在弹框内部。
html
<button data-bs-toggle="modal" data-bs-target=".add-modal">添加</button>
<button data-bs-dismiss="modal">取消</button>易错点:data-bs-target 的值是选择器,类选择器不要漏点;取消、小叉必须位于对应弹框内部,否则 data-bs-dismiss 不知道关闭哪个弹框。
JS 控制弹框:为提交做准备
为什么需要:点击“添加确认”时不能立刻关闭弹框,而要先把数据提交到服务器,成功后再关闭。
怎么想:转写稿明确,JS 控制适合“显示或隐藏之前还要做一些别的事情”的场景。
怎么做:先拿到弹框 DOM,实例化 bootstrap.Modal,再用 show()、hide() 控制。
js
const addModal = new bootstrap.Modal(document.querySelector('.add-modal'))
// 提交成功后关闭
addModal.hide()易错点:bootstrap.Modal 的 M 是大写;构造函数接收的是 DOM 元素;隐藏方法是 hide(),不是 hiigh。
收集表单数据并提交
为什么需要:新增接口需要书名、作者、出版社和创建者外号。转写稿中表单的 name 已与接口字段对应,可以直接用表单插件收集。
怎么想:serialize 能拿到表单内三个字段,但接口还需要 creator,所以用展开运算符合并。
怎么做:转写稿里的“s yourize”实际是 serialize,第二个参数为 { hash: true, empty: true }。
js
// 点击“添加”确认按钮后
const data = serialize(addForm, { hash: true, empty: true })
await axios({
url: '新增图书接口地址',
method: 'post',
data: { creator, ...data }
})调试时可在网络面板查看请求,预览中服务器返回“添加图书成功”,提交数据应有 4 个字段。
易错点:漏掉 creator 会少一个参数;接口方法是 post;serialize 得到的只是表单三字段,不要忘记展开合并。
刷新列表与清空表单
为什么需要:服务器数据变了,页面不会自动变;表单不清空,下次打开还会残留上次输入。
怎么想:复用图书列表阶段封装好的 getBooks(),提交成功后重新获取并渲染;表单用 reset() 清空。
怎么做:提交成功后依次关闭弹框、刷新列表、重置表单。
js
addModal.hide()
getBooks()
addForm.reset()转写稿的调试过程很关键:只提交时,网络请求成功但页面不变;刷新浏览器后才看到新书,说明服务器已变、本地未重新获取;调用 getBooks() 后页面立即更新。
易错点:忘记 getBooks() 页面不更新;reset() 是表单元素的方法;不要只清空某个输入框。
小结
新增数据可记为三步:准备好数据标签和样式(本节是 Bootstrap 弹框与表单);收集表单数据提交保存;刷新列表。核心代码就是属性控制弹框、serialize 收集、axios 提交、getBooks() 刷新、reset() 清空。
先想清楚:删除按钮是“后来才有”的
删除图书的目标很直接:点某一行末尾的删除标签,把这一行的数据从服务器删掉,再让页面显示最新的列表。但动手前要先想明白一件事——页面上的删除标签不是写在 HTML 里的,而是渲染图书列表时用 map 循环拼出来的。也就是说,页面加载完成那一刻,这些标签还不存在。
为什么这件事重要?因为如果你直接 document.querySelector('.del').addEventListener(...),在脚本执行时根本选不到元素,会报错或者绑定无效。怎么想?元素是动态生成的,就要用事件委托:把点击事件绑在一直存在的父元素 tbody 上,利用事件冒泡,点内部任何一个元素都会冒泡到 tbody,再通过事件对象 e.target 判断到底点的是谁。怎么做?先审查元素确认 tbody 的类名是 list,然后绑定:
js
document.querySelector('.list').addEventListener('click', (e) => {
// 后续逻辑
})易错点:委托的父元素必须是页面一开始就存在的元素,绑在 tr 或删除标签上都会失效。
判断点的是不是删除标签:取反 + return
tbody 里有很多元素,点序号、点书名都会触发这个事件,所以必须过滤。删除标签的类名是 del,用 classList.contains('del') 判断。
这里有个写法上的推导:如果写成 if (e.target.classList.contains('del')) { ...一大段逻辑... },后面还要嵌套 then 回调,层级会越来越深,可读性差。转写稿里把判断取反,先 return:
js
if (!e.target.classList.contains('del')) return为什么可行?点的不是删除标签时,条件成立,直接 return 阻断后续代码;能执行到下面的,就一定点的是删除标签。这就是“卫语句”思路,减少嵌套。易错点:contains 里的类名不要带点(写 del 不是 .del),取反符号别漏。
第二步:拿到要删除的那条数据的 id
删除接口需要知道删哪一条,所以必须拿到 id。可是渲染出来的界面上并没有 id。怎么想?id 在 getBooks 里请求回来的数据中就有,那就在渲染时“顺手”存下来。存在哪里最方便?删除和以后的编辑都要用,所以存在删除标签的父元素 tr 上,用自定义属性 data-id:
js
`<tr data-id="${v.id}">`取值时,从触发事件的元素往上找父元素,再读 dataset:
js
const { id } = e.target.parentNode.dataset调试过程可以这样验证:右键检查元素,看 tr 上有没有 data-id;再在控制台打印 e.target.parentNode.dataset,确认拿到 { id: '...' }。易错点:dataset 取值是驼峰形式,data-id 对应 dataset.id;如果存的是 data-book-id,就要写 dataset.bookId。另外父元素别找错,删除标签的直接父元素是 tr。
第三步:调用删除接口
打开接口文档会发现,删除和之前的列表、新增有两处不同:请求方法是 delete;参数 id 的位置是 path(路径参数),要直接拼在 URL 末尾的 {id} 处,而不是放在 params 里。
js
axios({
url: `http://.../books/${id}`,
method: 'delete'
})为什么用模板字符串?因为要把变量拼进 URL。调试时打开网络面板,能看到多了一次请求;点开响应,状态码是 204。这里要特别注意:204 表示成功,但服务器不返回响应体,所以预览里是空的,不要误以为失败。易错点:方法写成 delete(大小写不敏感,但别写成 del);id 忘了拼进 URL。
第四步:重新获取列表
接口调用成功后,服务器数据变了,但页面不会自动变——页面上的数据只是渲染结果。所以要在 then 里调用之前封装好的 getBooks():
js
.then(() => {
getBooks()
})getBooks 内部既请求数据又渲染,调用一次页面就更新了。易错点:只写 axios 不刷新,会看到“删了但还在”;反过来,也别在请求还没成功时就去刷新。
小结
删除数据的步骤是:告知服务器要删除的数据 id,服务器删除后,重新获取并刷新列表。落到代码上:事件委托绑 tbody → 判断 del 类名(取反 + return)→ 从父元素 dataset 取 id → axios 用 delete 方法和路径参数调接口 → getBooks() 刷新。
目标
完成图书管理案例中的“编辑图书”需求:点击某一行的“编辑”,先获取该图书详情并回显到编辑弹框,用户修改后提交到服务器,最后重新获取列表刷新页面。
本节笔记把编辑图书拆成三步:获取正在编辑的数据并回显、收集编辑表单数据提交保存、重新获取并刷新列表。转写稿中编辑图书分了多个视频讲解,下面按核心步骤展开。
编辑数据的核心思路
为什么需要:页面上的数据只是渲染结果,真正准确的数据在服务器。删除、新增、修改最终改变的都是服务器数据,所以编辑时不能直接从页面文本里取旧数据,而要先按 id 查询详情。
怎么想:点击编辑时先拿到该行图书 id;用 id 调“查询图书详情”接口;把返回数据回显到编辑表单;用户点“修改”后收集表单数据,用 put 提交;服务器成功后调用 getBooks() 重新获取列表,并关闭弹框。
怎么做:编辑按钮是动态生成的,所以用事件委托绑定到 tbody,并判断点击的是不是编辑标签。转写稿里用“取反 + return”减少嵌套:
js
document.querySelector('.list').addEventListener('click', (e) => {
if (!e.target.classList.contains('editor')) return
// 能走到这里,说明点的是编辑
})易错点:不要把判断逻辑全写在 if 里层层嵌套;编辑标签类名要核对准确;事件委托的父元素要选一直存在的 tbody。
获取正在编辑的数据
为什么需要:编辑回显必须用服务器最新数据,保证准确性。转写稿中调试时也是先点编辑,再打开 Network 看“查询图书详情”的响应,确认 data 里有 id、bookname、author、publisher。
怎么想:id 在删除功能里已经存到编辑/删除按钮的父元素自定义属性 data-id 上,编辑时直接通过 e.target.parentNode.dataset.id 取,不必重复存。
怎么做:
js
const { id } = e.target.parentNode.dataset
axios({
url: `接口地址/${id}`,
method: 'get'
}).then(res => {
const book = res.data.data
// 回显 book
})易错点:dataset 取到的是对象,要解构 id;如果列表渲染时漏存 data-id,这里就拿不到 id;详情接口参数在路径里,不是 params。
回显到编辑弹框
为什么需要:编辑弹框要先拿到数据再显示,所以适合用 JS 控制 Bootstrap 弹框,而不是纯属性控制。转写稿中强调:点击编辑不是立刻弹框,而是“先拿数据,再弹框”。
怎么想:创建编辑弹框实例;把 book 的字段填到表单元素;ID 不能让用户改,放进 type="hidden" 的隐藏域。转写稿先逐个写,后来发现“表单元素类名”和“对象属性名”一致,于是用 Object.keys 遍历优化。
怎么做:
js
const editModel = new bootstrap.Modal(document.querySelector('.edit-modal'))
Object.keys(book).forEach(key => {
document.querySelector(`.edit-modal .${key}`).value = book[key]
})
editModel.show()易错点:选择器要限定在 .edit-modal 内,避免选到新增弹框;隐藏域用于存 id,不能让用户看到或修改;先填充再 show()。
保存修改并刷新列表
为什么需要:点“修改”后要把新数据提交服务器;服务器变了页面不会自动变,所以要重新调用列表函数;弹框也要关闭。
怎么想:用 serialize 收集编辑表单数据,解构出 id、bookname、author、publisher;id 拼到 URL 路径,其他字段放 data,并补上外号变量 creator;请求方法用 put。
怎么做:
js
document.querySelector('.edit-btn').addEventListener('click', () => {
const form = document.querySelector('.edit-modal form')
const data = serialize(form, { hash: true, empty: true })
const { id, bookname, author, publisher } = data
axios({
url: `接口地址/${id}`,
method: 'put',
data: { bookname, author, publisher, creator }
}).then(() => {
getBooks()
editModel.hide()
})
})易错点:id 要拼 URL,不要只放 data;creator 是获取列表时定义的外号变量;提交成功后别忘 getBooks() 和 editModel.hide();调试时先看 Network 的请求体是否正好四个字段,再看响应是否“修改图书成功”。
小结
编辑数据的步骤是:获取正在编辑数据并回显,收集编辑表单的数据提交保存,重新获取并刷新列表。转写稿中的 bushop/brap 均为 Bootstrap,ex/axx 均为 axios,s yourize/seize 为 serialize,写代码时注意纠正。
一、为什么先总结:管理类业务有共同模型
学完图书管理案例,真正要收获的不是“只会改图书”,而是理解一类管理业务的共同模型:数据存在服务器,页面通过 axios 调接口完成增删改查,操作成功后重新取数并渲染。
为什么需要:实际项目里数据字段会变,但“列表、新增、删除、修改”的流程高度相似。
怎么想:先判断当前操作改的是哪里的数据——服务器;再判断页面为什么不自动变——页面只是渲染结果,必须重新获取。
怎么做:把列表逻辑抽成 getBooks,新增、删除、修改成功后都调用它。
易错点:只提交成功却不刷新页面;把“页面数据”当成“服务器数据”。
二、渲染列表:先取数,再渲染
为什么需要:列表是后续删除、编辑的基础,ID 也应在渲染时顺手保存。
怎么想:第一步通过 creator 外号告诉服务器“我是谁”,拿到自己的图书;第二步把数组映射成 HTML 字符串并渲染。
怎么做:核心片段按转写稿整理如下:
js
async function getBooks() {
const res = await axios({
url: '图书列表接口',
params: { creator }
});
const list = res.data.data;
const html = list.map((book, index) => `
<tr data-id="${book.id}">
<td>${index + 1}</td>
<td>${book.bookname}</td>
<td>${book.author}</td>
<td>${book.publisher}</td>
</tr>
`).join('');
document.querySelector('.list').innerHTML = html;
}易错点:忘记 join('');模板里字段名写错;ID 没有在渲染时存到 data-id,导致删除、编辑拿不到 ID。
三、新增:先准备输入区,再提交并刷新
为什么需要:用户要输入书名、作者、出版社,必须有一个输入区域。案例使用 Bootstrap 弹框。
怎么想:弹框显示可用属性控制,也可用 JS 控制。点击“添加”立即显示,适合属性控制;提交成功后还要先收集数据再关闭,适合 JS 控制。
怎么做:属性控制用 data-bs-toggle="modal" 和 data-bs-target="选择器";隐藏用 data-bs-dismiss="modal"。JS 控制用 new bootstrap.Modal(dom),再调用 show()、hide()。提交时用 serialize 收集表单数据,再与 creator 合并:
js
const data = { creator, ...serialize(form, { hash: true, empty: true }) };
await axios({ url: '新增接口', method: 'post', data });
getBooks();
form.reset();
editModal.hide();易错点:serialize 只拿到三个字段,忘记合并 creator;新增成功后忘记 getBooks();忘记 reset() 清空表单。
四、删除:事件委托、路径参数、重新取数
为什么需要:删除按钮是动态生成的,直接绑事件绑不到。
怎么想:把点击事件委托给一直存在的 tbody,再判断点击的是不是删除按钮。
怎么做:用 e.target.classList.contains('del') 判断;为减少嵌套,可反向判断并 return。ID 从父元素的 data-id 取,拼到 URL 末尾:
js
tbody.addEventListener('click', async (e) => {
if (!e.target.classList.contains('del')) return;
const { id } = e.target.parentNode.dataset;
await axios({ url: `删除接口/${id}`, method: 'delete' });
getBooks();
});易错点:把事件绑在动态按钮上;忘记 return 导致非删除区域也执行;删除接口参数是路径参数,不是 query 参数。
五、编辑:取数、回填、提交并刷新
为什么需要:编辑要先拿到准确数据,再回填弹框。
怎么想:不要从页面文本取数,应问服务器要详情;回填时表单元素类名与对象属性名一致,可用 Object.keys 循环简化。
怎么做:
js
const res = await axios({ url: `详情接口/${id}` });
const book = res.data.data;
Object.keys(book).forEach(key => {
document.querySelector(`.edit-modal .${key}`).value = book[key];
});
editModal.show();ID 通常放在 type="hidden" 的表单元素里,用户看不到但不能改。保存时用 serialize 取表单值,解构出 id、bookname、author、publisher,再提交:
js
await axios({
url: `修改接口/${id}`,
method: 'put',
data: { bookname, author, publisher, creator }
});
getBooks();
editModal.hide();易错点:ID 让用户可见可改;回填时选择器写死;修改成功后忘记关闭弹框和刷新列表。
六、总结:四步流程可套用
渲染数据:获取数据 → 渲染;新增数据:准备输入区 → 提交 → 刷新;删除数据:触发删除 → 调删除接口 → 刷新;修改数据:获取详情 → 回填 → 提交 → 刷新并关弹框。
易错点总清单:混淆 query 参数与路径参数;忘记 creator;忘记 getBooks();事件委托判断不严;动态元素直接绑事件;表单提交后不清空、不关弹框。掌握这些,再遇到类似管理业务就能按同一思路下手。
课件图示







代码示例
代码示例 1:Bootstrap 弹框的两种控制方式(属性 + JS)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Bootstrap 弹框 - 属性控制 & JS 控制</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-3">
<!-- 属性方式:显示弹框(data-bs-toggle + data-bs-target 指定选择器) -->
<button class="btn btn-primary"
data-bs-toggle="modal"
data-bs-target=".add-modal">属性方式-显示弹框</button>
<!-- JS 方式:显示弹框 -->
<button class="btn btn-success btn-show">JS 方式-显示弹框</button>
</div>
<!-- 弹框结构:从 Bootstrap 文档 Modal 里复制基础例子 -->
<div class="modal add-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">新增图书</h5>
<!-- 属性方式:隐藏弹框(关闭的是标签所在的当前弹框) -->
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form class="add-form">
<input type="text" class="form-control mb-2" name="bookname" placeholder="书名">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary btn-add">添加</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 1. 创建弹框实例:构造函数里传的是 DOM 元素,不是选择器字符串
const addModal = new bootstrap.Modal(document.querySelector('.add-modal'))
// 2. JS 方式显示弹框
document.querySelector('.btn-show').addEventListener('click', () => {
addModal.show()
})
// 3. JS 方式隐藏弹框:隐藏之前还要做别的事(收集数据提交服务器)
document.querySelector('.btn-add').addEventListener('click', () => {
// to do:收集用户数据,并提交到服务器
console.log('提交数据……')
addModal.hide()
})
</script>
</body>
</html>关键行与易错点
data-bs-toggle="modal"只表示“我要弹框”,必须再配data-bs-target="选择器"告诉它弹哪一个,选择器前要带.(类选择器)。data-bs-dismiss="modal"关闭的是标签所在的当前弹框,所以小叉和“取消”只要写在弹框内部就能生效,不用写 JS。new bootstrap.Modal(dom元素)里必须是 DOM 元素;直接传字符串不会报错但拿不到实例。- 什么时候用属性、什么时候用 JS:点击立刻显示/隐藏用属性;显示或隐藏之前要先执行一段 JS 逻辑(比如先拿数据、先提交表单)就必须用 JS 的
show()/hide()。
代码示例 2:渲染图书列表 + 新增图书
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>图书管理 - 渲染列表 & 新增</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
<style>
.list { text-align: center; }
.list a { margin: 0 4px; }
</style>
</head>
<body>
<div class="container mt-3">
<h3>
图书管理
<button class="btn btn-primary btn-sm"
data-bs-toggle="modal"
data-bs-target=".add-modal">添加</button>
</h3>
<table class="table table-bordered list">
<thead>
<tr>
<th>序号</th><th>书名</th><th>作者</th><th>出版社</th><th>操作</th>
</tr>
</thead>
<tbody>
<!-- 数据由 JS 动态渲染 -->
</tbody>
</table>
</div>
<!-- 新增弹框 -->
<div class="modal add-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">新增图书</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form class="add-form">
<input type="text" class="form-control mb-2" name="bookname" placeholder="书名">
<input type="text" class="form-control mb-2" name="author" placeholder="作者">
<input type="text" class="form-control mb-2" name="publisher" placeholder="出版社">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary btn-add">添加</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
<script>
// 外号(数据所有者),换成自己的,别用我的
const creator = '西兰花'
// 接口地址(以接口文档为准)
const BASE_URL = 'http://hmajax.itheima.net/api/books'
// ========== 一、渲染图书列表(抽成函数,后面增删改都要复用) ==========
const getBooks = async () => {
const res = await axios({ url: BASE_URL, params: { creator } })
const list = res.data.data // 数组:每项是一本书
const html = list.map((item, index) => `
<tr>
<td data-id="${item.id}">${index + 1}</td>
<td>${item.bookname}</td>
<td>${item.author}</td>
<td>${item.publisher}</td>
<td data-id="${item.id}">
<a href="javascript:;" class="del">删除</a>
<a href="javascript:;" class="edit">编辑</a>
</td>
</tr>`).join('') // join('') 之间不能有任何连接符
document.querySelector('.list tbody').innerHTML = html
}
getBooks() // 默认调用一次
// ========== 二、新增图书 ==========
const addModal = new bootstrap.Modal(document.querySelector('.add-modal'))
const addForm = document.querySelector('.add-form')
document.querySelector('.btn-add').addEventListener('click', async () => {
// 1. 收集表单数据(课程用 serialize 插件,这里用 FormData 等价实现)
const data = {}
new FormData(addForm).forEach((value, key) => { data[key] = value })
// 2. 提交到服务器:表单只有 3 个字段,接口还要 creator,用展开运算符合并
await axios({ url: BASE_URL, method: 'post', data: { ...data, creator } })
// 3. 服务器数据变了,页面不会自动变 —— 必须重新获取并渲染
getBooks()
// 4. 清空表单 + 关闭弹框
addForm.reset()
addModal.hide()
})
</script>
</body>
</html>关键行与易错点
index + 1:map的索引从 0 开始,直接渲染会出现序号 0。join('')的参数必须是空字符串,否则表格行之间会多出逗号等连接符。- 列表逻辑一定要抽成
getBooks():新增、删除、修改后服务器数据变了,页面不会自己变,都得再调一次它。 creator提升为全局常量:获取列表和新增/修改都要用到,写死在函数里后面没法复用。{ ...data, creator }:serialize/FormData拿到的只有bookname/author/publisher,接口还要求creator,用展开运算符拼成一个完整对象再提交。- 提交成功后的顺序:先刷新列表 → 再
reset()清空表单 → 最后hide()关弹框;漏了reset(),下次点“添加”还会看到上次的旧内容。 form.reset()是原生表单方法,必须用 form 元素调,用 input 调没有效果。
代码示例 3:删除图书 + 编辑图书(回显 + 保存修改)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>图书管理 - 删除 & 编辑</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
<style>
.list { text-align: center; }
.list a { margin: 0 4px; }
</style>
</head>
<body>
<div class="container mt-3">
<h3>图书管理</h3>
<table class="table table-bordered list">
<thead>
<tr>
<th>序号</th><th>书名</th><th>作者</th><th>出版社</th><th>操作</th>
</tr>
</thead>
<tbody><!-- 动态渲染 --></tbody>
</table>
</div>
<!-- 编辑弹框(和新增是两套弹框) -->
<div class="modal edit-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑图书</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form class="edit-form">
<!-- 隐藏域存 ID:用户看不到,但数据提交时会带上 -->
<input type="hidden" name="id">
<input type="text" class="form-control mb-2" name="bookname" placeholder="书名">
<input type="text" class="form-control mb-2" name="author" placeholder="作者">
<input type="text" class="form-control mb-2" name="publisher" placeholder="出版社">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary btn-edit">修改</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
<script>
const creator = '西兰花'
const BASE_URL = 'http://hmajax.itheima.net/api/books'
// ========== 渲染列表(把 id 顺手存到父元素 td 的自定义属性上) ==========
const getBooks = async () => {
const res = await axios({ url: BASE_URL, params: { creator } })
const list = res.data.data
const html = list.map((item, index) => `
<tr>
<td>${index + 1}</td>
<td>${item.bookname}</td>
<td>${item.author}</td>
<td>${item.publisher}</td>
<td data-id="${item.id}">
<a href="javascript:;" class="del">删除</a>
<a href="javascript:;" class="edit">编辑</a>
</td>
</tr>`).join('')
document.querySelector('.list tbody').innerHTML = html
}
getBooks()
// ========== 一、删除图书(事件委托) ==========
document.querySelector('.list tbody').addEventListener('click', async (e) => {
// 点的不是删除标签,直接阻断后续代码(比套 if 可读性更好)
if (!e.target.classList.contains('del')) return
const id = e.target.parentNode.dataset.id // 从父元素 td 上取自定义属性
await axios({ url: `${BASE_URL}/${id}`, method: 'delete' })
getBooks() // 重新获取并渲染最新列表
})
// ========== 二、编辑图书:获取详情 + 回显 ==========
const editModal = new bootstrap.Modal(document.querySelector('.edit-modal'))
const editForm = document.querySelector('.edit-form')
document.querySelector('.list tbody').addEventListener('click', async (e) => {
if (!e.target.classList.contains('edit')) return
const id = e.target.parentNode.dataset.id
// 页面上看到的数据不够准,查看数据一律以服务器为准
const res = await axios({ url: `${BASE_URL}/${id}` })
const book = res.data.data
// 表单元素的 name 与对象属性名一一对应,循环设置值
Object.keys(book).forEach(key => {
const input = editForm.querySelector(`[name="${key}"]`)
if (input) input.value = book[key]
})
editModal.show() // 先拿数据,再弹框
})
// ========== 三、编辑图书:保存修改 ==========
document.querySelector('.btn-edit').addEventListener('click', async () => {
const data = {}
new FormData(editForm).forEach((value, key) => { data[key] = value })
// 解构:id 要拼在 URL 上,其余三个放 data 里
const { id, bookname, author, publisher } = data
await axios({
url: `${BASE_URL}/${id}`,
method: 'put',
data: { bookname, author, publisher, creator }
})
getBooks() // 刷新列表
editModal.hide() // 关闭弹框
})
</script>
</body>
</html>关键行与易错点
- 事件委托:列表是动态生成的,事件必须绑在一直存在的
tbody上;再用e.target.classList.contains('del')判断点的是不是目标按钮。 if (!xxx) return的取反写法:点错元素直接阻断,后面的代码不用层层嵌套,可读性更好。data-id存在 a 的父元素 td 上,所以取值是e.target.parentNode.dataset.id;如果存在tr上就要parentNode.parentNode,层级写错会拿到undefined。- 删除接口是
method: 'delete',参数是路径参数(直接拼在 URL 末尾${BASE_URL}/${id}),不是params;删除成功服务器通常不返回内容(204),别以为请求失败了。 - 编辑时先获取数据再
show(),所以只能用 JS 控制弹框,不能只靠data-bs-toggle。 - 隐藏域
<input type="hidden" name="id">用来存 ID:ID 不能让用户改,但提交时又要带上;如果写成普通输入框,用户改掉 ID 服务器就找不到要改哪条数据了。 Object.keys(book).forEach(...)依赖“表单 name 与接口字段名一致”这个规律,一次循环搞定所有字段;用中括号语法book[key]动态取值,别写成book.key。- 保存修改用
method: 'put',ID 拼在 URL,其余字段放data,且必须补上creator(它是全局常量,直接取用即可)。 - 提交成功后两步缺一不可:
getBooks()刷新列表 +editModal.hide()关闭弹框。
常见错误
点“添加”按钮弹框不出来 现象:按钮点击无任何反应,控制台无报错。 原因:只引入了
bootstrap.css没引入bootstrap.js;或data-bs-target写成了#add-modal而标签上根本没有 id(案例里用的是类名,要写.add-modal);或漏写data-bs-toggle="modal"。 修法:确认两个文件都引入且顺序在自定义脚本之前;打开审查元素核对 target 选择器与实际弹框类名/id 完全一致,点号/井号不能漏。新增提交成功后弹框关不掉 现象:数据提交成功、列表也刷新了,但弹框一直挂着。 原因:显示/隐藏只用了属性方式(
data-bs-dismiss),而提交按钮上的逻辑是 JS 执行的,属性方式无法在异步回调里关框。 修法:用 JS 方式const addModal = new bootstrap.Modal(document.querySelector('.add-modal')),在then里调addModal.hide();同时注意方法名是hide不是hidden、close。新增后列表不刷新,必须手动刷新页面才看到新数据 现象:接口返回“添加成功”,页面数据不变。 原因:只提交了数据,忘了服务器数据变了不等于页面变了;或者获取列表的逻辑写在默认执行处,没有抽成可复用函数。 修法:把“获取+渲染”抽成
getBooks()并在页面加载时调用一次;任何增删改成功后都再调一次getBooks()。删除/编辑时拿不到 id,请求 URL 里出现
undefined现象:请求发出去是/books/undefined,或dataset.id打印为undefined。 原因:渲染列表时忘了在tr上写data-id="${v.id}",或写了但取的是e.target.dataset.id(删除按钮本身没有该属性,属性在父级tr上)。 修法:渲染模板时补上data-id;取值统一走e.target.parentNode.dataset.id,并先console.log验证再拼接口。点表格任意位置都触发删除/编辑 现象:点空白处、点书名都会发请求。 原因:事件委托绑在
tbody上,只写了点击回调,没有过滤真正触发的元素。 修法:if (!e.target.classList.contains('del')) return(用取反 + return 阻断,减少嵌套);同时确认绑的是tbody这种始终存在的祖先,而不是动态生成的tr。编辑弹框里回显的数据错乱或为空 现象:点编辑后弹框弹出,但输入框是空的,或填到了新增弹框的输入框里。 原因:两个弹框里存在同名类名(如
.book-name),querySelector命中了页面上第一个匹配的元素(新增框里的);或先show()再异步拿数据,时序上看起来“没回显”。 修法:选择器加祖先限定.edit-modal .book-name;回显逻辑放在获取详情成功的回调里,赋值完成后再editModal.show()。serialize 拿到的不是对象,展开合并后参数不对 现象:
data是bookName=xx&author=yy这样的字符串,或提交后接口报缺少creator。 原因:serialize第二个参数忘了传{ hash: true, empty: true };或只提交了表单数据,没把外号creator合并进去。 修法:固定写serialize(form, { hash: true, empty: true });提交时用{ creator, ...data }合并,并注意展开顺序(后面的会覆盖前面的同名属性)。
练习
给新增加校验:书名输入为空时点击“添加”不发请求,弹框保持显示并给出提示。要求说明为什么这里必须用 JS 方式控制弹框,而不能靠
data-bs-dismiss。删除加二次确认:点击删除先弹
confirm('确定删除吗?'),取消则不调接口。写出事件委托里的完整判断链路(过滤元素 → 取 id → 确认 → 调接口 → 刷新)。合并弹框改造:把新增和编辑合并成同一个弹框(后续项目常见做法)。列出需要改动的位置,并说明如何用一个变量区分当前是“新增态”还是“编辑态”,以及点击“保存”时如何走不同分支。
扩展字段:给图书增加“价格”列,要求列表能展示、新增能录入、编辑能回显和修改。写出最少改动点,并说明为什么编辑回显那段
Object.keys(book).forEach(...)的写法可以几乎不改代码。回显健壮性:若详情接口返回的字段比表单元素多(比如多了
createTime),Object.keys循环回显会出什么问题?给出两种修法(提示:判断元素是否存在 / 只遍历表单元素)。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 图书管理-图书列表 | 12:45 |
| 2 | 图书管理-新增图书01 | 12:26 |
| 3 | 图书管理-新增图书02 | 10:43 |
| 4 | 图书管理-删除图书 | 14:08 |
| 5 | 图书管理-编辑图书-获取数据 | 09:35 |
| 6 | 图书管理-编辑图书-编辑弹框 | 14:01 |
| 7 | 图书管理-编辑图书-保存修改 | 09:59 |
| 8 | 图书管理-总结 | 06:23 |
小结
- 图书管理四个功能(渲染、新增、删除、编辑)本质是同一套模板:拿数据 → 改数据 → 重新拿数据刷新页面,只有“改”的那一步接口不同。
- 凡是服务器上的数据发生变化,本地页面都必须重新调用一次抽好的
getBooks(),不要指望刷新页面提示用户。 - 弹框控制记住选择标准:立刻显示/隐藏用属性(
data-bs-toggle/data-bs-target/data-bs-dismiss),显示或隐藏之前还要执行 JS 逻辑(校验、提交、回显)就必须用new bootstrap.Modal(el)+show()/hide()。 - 动态生成的元素一律用事件委托绑在始终存在的祖先上,并用“取反 + return”的方式过滤目标元素,代码嵌套更浅、可读性更好。
- id 要在渲染列表时顺手写进
tr的data-id,删除和编辑都从这里取;后期做任何行内操作都靠它,别等到用时再回来补。 - 编辑场景必须从服务器重新查详情再回显,不能从页面上抠文本;id 用
type="hidden"的输入框承载,既不污染用户视野又能被serialize一并收集。 - 请求方法(get/post/put/delete)统一写进
method,路径参数直接拼到 URL 末尾,query 参数放params——三者位置别混。 - 编辑回显用
Object.keys(book).forEach+ 模板字符串选择器 + 中括号取值,能让“字段数量变化”变成零成本,这个套路在后续表单类业务里会反复用到。