一、整体工作概述
今天主要完成了一点bug修复,高并发点击的时候列表会乱序,以及ai coding实现文档的CRUD,同时文档列表要同步渲染。 这一大块是ai写的,我只是看懂了代码,整体技术难度不高,而且一部分实现方式比较简单粗暴,后面还有优化空间。 简单记录一下开发过程,作为个人学习与复盘。
二、列表顺序随机跳变 bug 修复
- 服务端非确定性行为与前端过度请求交织产生的 Bug 服务端存在非确定性行为,导致前端频繁点击时,骨架屏上的列表顺序会突然打乱,出现数据顺序随机跳变的 bug。
问题的本质是:
后端数据库查询时没有显式指定排序(例如按照什么sort),所以数据库返回数据的顺序没有强制保证,在高并发多次连接或缓存刷新时,顺序就会随机跳变。bug解决方案是是把下列useEffect删掉就好了。
useEffect(() => {
if (currentId) {
fetchDocs(false);
}
}, [currentId, fetchDocs]);
为什么删除那个 useEffect 之后,选中文档列表的高亮还在,但乱序消失了?
高亮逻辑的真相: isActive = doc._id === currentId。
这里的 currentId 来自 URL(useParams)。当点击 Maps 时,URL 变了,React 会触发 WikiList 组件重新渲染。此时 docs 数组依然存在于内存中(State 未变),React 只需要重新计算每个 Item 的 isActive 布尔值并刷新 DOM 即可。根本不需要重新拉取数据。
乱序消失的真相: 既然不再触发 getAllDocuments,也就没有了后端随机排序的干扰,前端始终持有第一次加载时(第一个 useEffect)拿到的那份数据。
也就是说,前端连击触发 map,导致 current ID 改变,没法正常拿到后端数据,后端又重新返回一遍列表数据,把顺序打乱了。我每次都会触发全量数据拉取,后端又没有强制排序,数据顺序本身就不可靠;再加上 useEffect 频繁监听请求,拿到新顺序后重新渲染,最终导致列表顺序错乱。
这里我用 Network 面板验证,看了名为 documents 的请求响应,联机时能观察到同一个 ID 的元素位置在变化。
删掉这个 useEffect,不再触发 getAllDocuments 请求,不重新拉取数据,避开后端随机排序的影响,直接保留第一次加载的数据就行。
三、删除文档功能实现与疑问
在这之后,我做了第二个功能,这个功能是 AI 实现的,我只是把代码看了一遍,看懂后做了删除相关的操作。
我最初理解的全栈开发思路:首先在前端 API 服务层新增一个删除文档的接口供调用;前端 UI 事件触发时,在 onClick 里绑定这个 API 服务,向后端发请求。后端收到请求后,在 controller 层执行删除文件的指令,并新增对应的删除路由。
简化流程即:
前端 UI 绑定事件 → 事件触发请求 → 请求经前端 API 服务层发给后端 → 后端 controller 响应 → 路由分发 → 对数据库做 CRUD 操作。
这里我有个疑问:为什么后端新增路由就能直接操作数据库?是怎么直接连上数据库的?为什么现在后端的 POST、PUT、DELETE 这些路由都能直接操作数据库?
经过梳理,完整的全栈开发思路是:
前端 UI 绑定事件并触发 API 请求;请求经网络到达后端,由 “预设” 好的路由进行分发并指派给对应的控制器(Controller);控制器调用 “启动时已建立” 的数据库连接(借助.env 配置加载),最终完成对数据库的 CRUD 操作。
1. 前端层:定义与触发
API 服务层定义:先在前端 src/api 里写好“删除文档”的接口函数(比如用 axios.delete),明确告诉浏览器:我们要去哪个 URL、带哪个 id、用什么姿势(DELETE 方法)发请求。
UI 事件绑定:在 React 组件的按钮 onClick 里,直接绑定这个 API 函数。当用户一点,封装好的请求就带着数据“起飞”,穿过网络直奔后端。
2. 后端层:接线与派发
路由分发 (Routing):这里要注意,后端是预先埋伏好路由的。请求一到,路由就像个“接线员”,看一眼方法(DELETE)和路径,立刻把这个请求“踢”给对应的 Controller。
Controller 响应:Controller 才是真正的大脑。它负责把请求里的 id 摘出来,检查用户有没有权限删,然后下达最终的“处决指令”。
3. 持久层:指令执行
数据库 CRUD:Controller 调用数据库工具(比如 Mongoose 或 Prisma),利用早已建立好的连接管道,对数据库执行真正的删除动作。
Q1:为什么后端“配个路由”就能直接操作数据库?
路由只是“门牌号”,不是“推土机”。 你之所以觉得“配了路由就能删”,是因为你在写 router.delete() 的时候,紧跟着在回调函数里写了操作数据库的代码。 路由负责:确定“哪个接口对应哪个功能”。 Controller 负责:在这个功能里真正去改数据库。
Q2:后端是怎么“直接”连上数据库的?
连接发生在服务器启动的一瞬间,而不是请求来的时候。 当你运行后端项目(比如 npm run dev)时,程序会立刻读取 .env 里的数据库地址和密码,跟数据库建立一个持久的连接池。 平时:连接管道是通的,处于待命状态。 请求来时:Controller 只是顺着这个现成的“水管”发了一条指令,速度极快。
Q3:为什么现在的 POST、PUT、DELETE 路由都能操作数据库?
这是一种语义化约定 (RESTful),而不是物理限制。 从技术底层看,它们都是 TCP 数据包,本质没区别。但为了让代码好维护: DELETE:约定用来删,逻辑上直观。 POST/PUT:约定用来增和改,因为它们可以往请求体(Body)里塞进复杂的 JSON 结构,方便 Controller 拿到完整的数据去更新数据库。
四、细节优化与体验改进
还有一个细节问题:新增和删除文件时,左侧 wiki list 整个文档列表没有跟着数据动态更新。
当前处理方式是在 window 上挂载事件监听器,定义 DOCUMENTS_CHANGED_EVENT 处理文档变化。
export const DOCUMENTS_CHANGED_EVENT = "documents:changed";
export const notifyDocumentsChanged = () => {
window.dispatchEvent(new Event(DOCUMENTS_CHANGED_EVENT));
};
useEffect(() => {
const handleDocumentsChanged = () => {
fetchDocs();
};
window.addEventListener(DOCUMENTS_CHANGED_EVENT, handleDocumentsChanged);
return () => {
window.removeEventListener(
DOCUMENTS_CHANGED_EVENT,
handleDocumentsChanged,
);
};
}, [fetchDocs]);
我觉得这个方式虽然清晰,但直接操作 DOM、挂载到 window 上,跳出了 React 框架,算不上优雅,实现简单但有点粗暴,后面可能会再改,想想怎么放到现有框架里,方便调试和维护。
另外还有 UX 细节比如:
- 删除当前文件后,不想显示空白占位 UI,希望自动打开列表里下一个文档。这里用了 if 判断,定义了 remainingDocs,逻辑大概是:如果还有数据,就从数组里依次读 ID 做路由跳转
const remainingDocs = await getAllDocuments();
if (remainingDocs && remainingDocs.length > 0) {
navigate(`/wiki/${remainingDocs[0]._id}`);
} else {
navigate("/wiki");
}
只有数据全空时,才展示空白占位 UI。
- 还有一些警示弹窗,就是调用组件库组件Modal再配合国际化,没什么难度。
五、小结
今天做的工作难度不算大,解决问题的一些方案也比较粗暴,不够优雅。后面我会再想想更合适的实现方式,对架构的理解要更多一些,加油,加油!
参考文档
使用 Fetch - Web API | MDN
String.prototype.localeCompare() - JavaScript | MDN
Array.prototype.sort() - JavaScript | MDN
EventTarget.dispatchEvent() - Web API | MDN