用AI从零搭建Node.js API服务:Express实战与工程化指南
1. 为什么我劝你亲手搭一个 API 服务很多人学编程卡在同一个地方语法都会一到要做一个“能跑起来、别人能访问”的东西就懵了。你让他写个循环、写个函数没问题但让他从零搭一个 API 服务把数据从数据库里取出来、包装成 JSON、再通过一个地址暴露出去他就不知道从哪下手了。这个项目标题“用 AI 从零搭一个 API 服务”说的就是解决这个问题——不是教你背 Express 的 API 文档而是让你在 AI 的辅助下真正把一个能用的服务跑起来。我自己带过不少新人也面试过很多自称“熟悉 Node.js”的候选人。一个很普遍的现象是简历上写着精通 Express结果让他现场写一个带参数校验和错误处理的接口写出来的代码连基本的异常捕获都没有。问题出在哪不是他不懂app.get()怎么写而是他从来没有完整地走过一遍“从需求到上线”的流程。他做的都是填空题不是从零搭建。所以这个实战项目的价值不在于 API 本身有多复杂而在于它逼你把整条链路走通环境怎么配、框架怎么选、路由怎么设计、参数怎么校验、错误怎么处理、日志怎么打、接口怎么测试。这些东西你看十篇文章都不如自己动手搭一遍。而 AI 在这个过程中的角色不是替你写代码而是像一个随时在线的结对伙伴——你卡住了它给你方向你写完了它帮你 review你不确定选型它给你对比分析。这篇文章适合谁看如果你是刚学完 JavaScript 基础、想找一个“不太大但足够完整”的项目来练手的人那这篇就是写给你的。如果你已经写过一些接口但总觉得自己的代码“不够工程化”想看看一个相对规范的 API 服务应该长什么样那这篇同样有参考价值。我会把我在实际搭建过程中踩过的坑、做过的取舍、以及 AI 在哪些环节真正帮上了忙都原原本本讲出来。2. 动手之前先把这几件事想清楚2.1 技术选型为什么是 Node.js Express选 Node.js 做 API 服务最直接的理由是JavaScript 一门语言从头用到尾。你不需要在 Python 和 JavaScript 之间来回切换前端后端都是同一套语言体系对于小项目来说心智负担最小。而且 Node.js 的非阻塞 I/O 模型天生适合处理大量并发的小请求API 服务正好就是这个场景——每个请求进来查一下数据返回 JSON结束。这种“短平快”的请求用 Node.js 来处理资源利用率很高。框架层面Express 是最稳妥的选择。它足够老、足够成熟、社区足够大你遇到的几乎所有问题都能搜到答案。有人会问为什么不选 FastifyFastify 性能确实更好但它的生态和中间件丰富度跟 Express 还有差距。对于一个小项目实战来说Express 的“够用且不添乱”比“性能极致”更重要。你先把东西跑起来性能优化是后面的事。提示如果你之前完全没接触过 Node.js建议先去官网下载 LTS 版本安装。不要用 Current 版本LTS 才是稳定版兼容性最好。安装完之后在终端里跑node -v和npm -v能正常输出版本号就说明环境没问题。2.2 项目结构别把所有代码塞进一个文件我见过太多新手写的 API 服务所有代码都在一个index.js里路由、数据库操作、业务逻辑、错误处理全部混在一起两三百行看下来眼睛都花了。这种写法在项目只有三五个接口的时候还能忍一旦接口数量超过十个维护成本就指数级上升。所以从一开始就要把目录结构规划好。我的建议是至少分出这几层routes放路由定义controllers放业务逻辑middlewares放中间件utils放工具函数config放配置文件。这样分的好处是当你需要改某个接口的逻辑时你知道去controllers里找当你需要加一个全局的日志中间件时你知道去middlewares里加。每个文件只负责一件事读代码的人不需要在脑子里维护一张“什么代码在什么地方”的地图。2.3 AI 在这个项目里到底扮演什么角色我得先把话说清楚AI 不是替你写代码的。如果你指望把需求丢给 AI它吐出来一堆代码你复制粘贴就能跑那你大概率会失望。AI 生成的代码经常有微妙的 bug比如漏掉await、参数校验不完整、错误处理只写了catch但没做任何有意义的处理。这些东西你不理解的话出了问题根本不知道怎么修。AI 真正有用的地方在于第一帮你快速生成项目骨架和样板代码省去你敲重复代码的时间第二当你遇到报错不知道怎么排查时把错误信息贴给它它能给你几个可能的排查方向第三帮你 review 代码指出你可能忽略的边界情况。把 AI 当成一个知识面很广但需要你最终把关的助手而不是一个全自动代码生成器这个心态摆正了效率提升会非常明显。3. 从零开始搭建的完整实操过程3.1 初始化项目与安装依赖第一步创建一个项目目录然后在目录里初始化 npm。打开终端执行下面这几条命令mkdir my-api-service cd my-api-service npm init -ynpm init -y会生成一个默认的package.json文件里面的字段都是默认值。你可以打开这个文件把name、description、author这些字段改成你自己的信息。接下来安装核心依赖npm install express npm install -D nodemonExpress 是运行时依赖nodemon 是开发依赖。nodemon 的作用是监听文件变化你改完代码保存后它会自动重启服务不用每次手动CtrlC再重新node index.js。这个工具在开发阶段能省你很多时间。安装完成后在package.json的scripts字段里加两行{ scripts: { start: node src/index.js, dev: nodemon src/index.js } }这样开发的时候跑npm run dev部署的时候跑npm start命令清晰不容易搞混。3.2 搭建基础服务骨架在项目根目录下创建src文件夹然后在里面创建index.js。这是整个服务的入口文件。先写一个最基础的版本const express require(express); const app express(); const PORT process.env.PORT || 3000; app.use(express.json()); app.get(/health, (req, res) { res.json({ status: ok, timestamp: Date.now() }); }); app.listen(PORT, () { console.log(Server is running on port ${PORT}); });这几行代码做了几件事引入 Express、创建应用实例、注册express.json()中间件用来解析请求体中的 JSON 数据、定义一个健康检查接口、启动服务监听端口。健康检查接口看起来没什么用但在实际部署中非常重要——负载均衡器或者监控系统会定期访问这个接口来判断服务是否还活着。跑npm run dev然后在浏览器或者用 curl 访问http://localhost:3000/health看到{status:ok,timestamp:...}就说明基础服务已经跑起来了。3.3 设计路由与控制器分层基础服务跑通之后接下来要把路由和业务逻辑分开。在src下创建routes和controllers两个文件夹。假设我们要做一个简单的用户管理接口支持查询用户列表和根据 ID 查询单个用户。先写控制器controllers/userController.jsconst users [ { id: 1, name: 张三, email: zhangsanexample.com }, { id: 2, name: 李四, email: lisiexample.com }, { id: 3, name: 王五, email: wangwuexample.com } ]; const getUsers (req, res) { res.json({ code: 0, data: users, message: success }); }; const getUserById (req, res) { const id parseInt(req.params.id, 10); const user users.find(u u.id id); if (!user) { return res.status(404).json({ code: 404, data: null, message: 用户不存在 }); } res.json({ code: 0, data: user, message: success }); }; module.exports { getUsers, getUserById };这里用了一个内存数组来模拟数据实际项目中你会从数据库里查。注意parseInt(req.params.id, 10)这一步——URL 参数拿到的永远是字符串不转成数字的话find里的比较会永远返回 false。这个坑我见过太多人踩了接口一直返回“用户不存在”查了半天才发现是类型不匹配。然后写路由routes/userRoutes.jsconst express require(express); const router express.Router(); const { getUsers, getUserById } require(../controllers/userController); router.get(/, getUsers); router.get(/:id, getUserById); module.exports router;最后在index.js里挂载路由const userRoutes require(./routes/userRoutes); app.use(/api/users, userRoutes);这样分层之后路由文件只负责定义“什么路径对应什么处理函数”控制器文件只负责“拿到请求后做什么处理”职责清晰改起来不容易出错。3.4 参数校验与错误处理中间件上面那个getUserById其实有个隐患如果用户传的 ID 不是数字比如/api/users/abcparseInt会返回NaN然后find找不到任何东西返回 404。虽然结果看起来没问题但错误信息不准确——用户传了非法参数你应该返回 400 而不是 404。所以需要加参数校验。我习惯在控制器里做业务层面的校验在中间件里做通用的错误捕获。先加一个全局错误处理中间件放在所有路由注册之后app.use((err, req, res, next) { console.error(err.stack); res.status(err.status || 500).json({ code: err.status || 500, data: null, message: err.message || 服务器内部错误 }); });注意这个中间件有四个参数Express 靠参数个数来识别它是错误处理中间件少一个都不行。然后改造getUserById加上参数校验const getUserById (req, res, next) { const id parseInt(req.params.id, 10); if (isNaN(id)) { const err new Error(用户 ID 必须是数字); err.status 400; return next(err); } const user users.find(u u.id id); if (!user) { const err new Error(用户不存在); err.status 404; return next(err); } res.json({ code: 0, data: user, message: success }); };这样改造之后非法参数会走 400找不到用户会走 404真正的服务器错误会走 500每种情况的语义都清晰了。用next(err)把错误交给全局中间件处理控制器里不用重复写res.status().json()代码更干净。3.5 用 AI 辅助排查一个真实报错搭建过程中我遇到一个报错这里完整记录一下排查过程因为这类问题太常见了。当时的情况是我写了一个 POST 接口用来创建用户用 Postman 发请求请求体里带了 JSON 数据但req.body打印出来是undefined。我先把错误信息贴给 AI它给了几个排查方向第一确认有没有注册express.json()中间件第二确认请求头的Content-Type是不是application/json第三确认中间件注册的顺序是不是在路由之前。我检查了一下express.json()确实注册了但注册的位置在路由挂载之后。Express 的中间件是按注册顺序执行的如果路由先注册请求进来先匹配到路由后面的express.json()根本不会执行req.body自然就是 undefined。把app.use(express.json())移到所有路由注册之前问题解决。这个坑的本质是 Express 中间件的执行顺序问题AI 给的排查方向是对的但它不知道我的代码长什么样所以只能给可能性最终定位还是得靠我自己去看代码。这就是我前面说的AI 是助手不是替身。4. 接口测试与工程化收尾4.1 用 curl 和 Postman 做接口验证接口写完了怎么确认它真的能用最简单的办法是用 curl。比如测试查询用户列表curl http://localhost:3000/api/users测试查询单个用户curl http://localhost:3000/api/users/1测试非法参数curl http://localhost:3000/api/users/abc你应该分别看到正常数据、单个用户数据、以及 400 错误信息。curl 的好处是轻量、快适合快速验证。但如果你要频繁测试各种请求体、请求头、认证信息Postman 会更方便。我一般用 curl 做冒烟测试用 Postman 做完整的接口测试。注意测试的时候一定要覆盖边界情况。正常路径能跑通不代表接口没问题非法参数、空数据、超大请求体、并发请求这些才是真正暴露问题的地方。我习惯每写完一个接口至少测四种情况正常请求、参数缺失、参数类型错误、资源不存在。4.2 日志记录别等出事了才后悔没打日志很多新手写 API 服务不打日志出了问题只能靠猜。我强烈建议在项目初期就把日志加上。最简单的做法是写一个日志中间件app.use((req, res, next) { const start Date.now(); res.on(finish, () { const duration Date.now() - start; console.log(${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms); }); next(); });这个中间件会记录每个请求的方法、路径、状态码和耗时。别小看这几行代码当你的接口突然变慢或者某个接口开始报错时日志能帮你快速定位是哪个请求出了问题。生产环境建议用 winston 或者 pino 这类专业的日志库支持日志分级、输出到文件、格式化等功能。4.3 环境变量与配置分离把端口号、数据库连接串这些配置硬编码在代码里是大忌。不同环境开发、测试、生产的配置不一样硬编码意味着你每次部署都要改代码。正确的做法是用环境变量。安装dotenvnpm install dotenv在项目根目录创建.env文件PORT3000 NODE_ENVdevelopment然后在index.js最顶部加载require(dotenv).config(); const PORT process.env.PORT || 3000;记得把.env加到.gitignore里不要提交到代码仓库。可以创建一个.env.example文件作为模板里面只写 key 不写 value这样别人拿到你的代码知道需要配哪些环境变量。5. 常见问题与排查技巧实录5.1 接口返回 404 但路由明明定义了这是最常见的问题之一。排查顺序是这样的先确认请求方法和路径是否完全匹配GET /api/users和GET /api/user是两个不同的路由再确认路由挂载的前缀是否正确app.use(/api/users, userRoutes)和app.use(/api, userRoutes)会导致完全不同的路径最后确认路由注册的顺序如果有通配符路由或者静态文件中间件注册在前面可能会拦截掉你的请求。5.2 req.body 始终是 undefined前面已经讲过一个原因express.json()中间件注册顺序不对。另一个常见原因是请求头没有设置Content-Type: application/jsonExpress 的 JSON 解析中间件只处理这个 Content-Type 的请求。如果你用的是 Postman检查一下 Body 选项卡里选的是不是 raw JSON。5.3 服务启动报错 EADDRINUSE这个错误的意思是端口被占用了。可能是你之前启动的服务没有正常关闭还在后台跑着。解决办法在终端里执行lsof -i :3000找到占用端口的进程然后kill -9 PID杀掉它。或者直接换个端口启动。我习惯在开发时用nodemon它会自动处理重启但偶尔也会遇到端口没释放干净的情况这时候手动杀一下进程就行。5.4 异步操作忘记 await 导致返回空数据这个坑非常隐蔽。比如你写了一个从数据库查询用户的函数它是异步的但你在控制器里调用的时候忘了加await那么user变量拿到的是一个 Promise 对象而不是实际数据返回给前端的 JSON 里 data 字段会是一个空对象或者奇怪的东西。排查方法在控制器里打印一下拿到的数据如果看到Promise { pending }就说明漏了await。问题现象可能原因排查方法接口 404路径不匹配、路由顺序问题检查请求方法和路径、确认路由注册顺序req.body 为 undefined中间件顺序错误、Content-Type 不对确认 express.json() 在路由之前、检查请求头端口占用旧进程未关闭lsof 查进程、kill 掉或换端口返回数据为空异步操作漏了 await打印中间变量、检查 Promise 状态参数类型错误URL 参数是字符串未转换用 parseInt 或 Number 转换后校验5.5 一个容易被忽略的细节JSON 响应格式统一我见过很多项目不同接口返回的 JSON 结构完全不一样。有的返回{ data: ... }有的直接返回数组有的错误返回{ error: ... }有的返回{ message: ... }。前端对接的时候要写一堆兼容逻辑非常痛苦。建议从一开始就定好统一的响应格式比如成功时返回{ code: 0, data: ..., message: success }错误时返回{ code: xxx, data: null, message: 错误描述 }。所有接口都遵循这个格式前端处理起来就简单多了。6. 后续可以怎么扩展这个项目目前用的是内存数组模拟数据下一步自然是接入真正的数据库。MongoDB 配合 Mongoose 是比较顺滑的选择因为都是 JavaScript 生态学习成本低。如果你想练 SQL 相关的技能可以试试 SQLite 或者 PostgreSQL。接入数据库之后你会发现控制器的写法需要调整——查询变成异步操作了错误处理也要相应变化。再往后可以加用户认证。最简单的方案是 JWT用户登录后服务端签发一个 token后续请求带上这个 token 来验证身份。这一步会涉及到中间件的编写、密码加密、token 验证等知识点是一个很好的进阶练习。如果你能把认证也跑通那这个项目就从一个“练手 demo”变成了一个“可以拿出去给人看的作品”。最后说一个我自己的体会搭 API 服务这件事看再多教程都不如自己从头写一遍。你会在过程中遇到各种教程里不会提到的问题——环境配置的坑、中间件顺序的坑、异步处理的坑。每踩一个坑并且解决它你对整个技术栈的理解就深一层。AI 在这个过程中能帮你加速但前提是你自己得知道方向在哪。

相关新闻

QuickFix Java消息收发与查看实战指南

QuickFix Java消息收发与查看实战指南

1. 项目概述:为什么FIX协议的收发与查看是Java金融系统开发的“呼吸感”环节QuickFix Java 讲解(五)消息的收发与查看——这个标题里藏着一个被很多Java初学者低估、却被高频交易系统、券商柜台、基金估值引擎等真实生产环境反复锤炼的核心能…

2026/10/4 6:51:39 阅读更多 →
告警是真是假?Nearby Glasses 误报率、VR头显干扰与检测边界完整分析

告警是真是假?Nearby Glasses 误报率、VR头显干扰与检测边界完整分析

告警是真是假?Nearby Glasses 误报率、VR头显干扰与检测边界完整分析 【免费下载链接】yj_nearbyglasses attempting to detect smart glasses nearby and warn you 项目地址: https://gitcode.com/gh_mirrors/yj/yj_nearbyglasses 概要:Nearby Gl…

2026/10/4 6:51:39 阅读更多 →
chrome-devtools-mcp:让AI编码助手通过MCP协议直接操控浏览器

chrome-devtools-mcp:让AI编码助手通过MCP协议直接操控浏览器

1. 为什么需要让 AI 编码助手“看见”浏览器1.1 一个真实到让人抓狂的场景前端开发里有个场景几乎每个人都遇到过:你让 AI 编码助手帮你改一个按钮的样式,它信心满满地给你返回了一段 CSS,你复制粘贴进去,刷新页面,发现…

2026/10/4 6:51:39 阅读更多 →

最新新闻

24天16万行代码:用Claude Code从零构建K12教育产品的实战工作流

24天16万行代码:用Claude Code从零构建K12教育产品的实战工作流

1. 先聊聊这个项目到底在做什么24 天,630 次提交,16 万行代码。这三个数字放在一起,任何一个写过代码的人都会先愣一下——平均每天 26 次提交、6600 多行代码产出,而且是在不到一个月的时间里从零搭出一个 K12 教育产品。如果这是…

2026/10/4 7:33:03 阅读更多 →
Codex++卡顿排查与提速:六个亲测有效的性能优化方案

Codex++卡顿排查与提速:六个亲测有效的性能优化方案

1. 卡顿先别急着换工具,先搞清楚Codex到底卡在哪我用Codex做日常开发也有小半年了,这工具的补全和对话能力都相当能打,尤其是结合多文件上下文理解之后,改起老项目来确实顺手。但最近这阵子,明显感觉到它变“钝”了&am…

2026/10/4 7:33:03 阅读更多 →
OpenRig:基于Node.js与tmux的Codex CLI本地化封装方案

OpenRig:基于Node.js与tmux的Codex CLI本地化封装方案

1. OpenRig 是什么:一个被误读但极具潜力的开发者工具链OpenRig 这个名字最近在开发者社区里频繁出现,但它既不是某个新发布的 AI 框架,也不是某家大厂推出的闭源 SDK。我第一次在 GitLab CI 日志里看到openrig被当作服务名调用时&#xff0c…

2026/10/4 7:33:03 阅读更多 →
别急着做完整SaaS:用AI Agent快速验证产品需求

别急着做完整SaaS:用AI Agent快速验证产品需求

1. 为什么“先做完整 SaaS”正在变成一条弯路过去几年,做产品的默认路径几乎是固定的:先想清楚需求,画原型,搭后端,做权限,接支付,再一点点把功能堆成一个完整的 SaaS。这套打法在移动互联网时代…

2026/10/4 7:33:03 阅读更多 →
openrig:统一管理Claude Code与Codex的YAML配置编排工具

openrig:统一管理Claude Code与Codex的YAML配置编排工具

1. openrig 到底在解决什么问题第一次看到 openrig 这个名字,很多人会以为是某个硬件机架项目,或者跟 rigging 动画绑定有关。但结合它周边的关键词——claude code、codex、yaml、node.js——基本可以判断,这是一个围绕 AI 编程助手做配置编…

2026/10/4 7:33:03 阅读更多 →
两千元预算本地部署Qwen3-27B:V100实战280 tok/s推理

两千元预算本地部署Qwen3-27B:V100实战280 tok/s推理

1. 两千块预算的本地AI部署,到底能跑出什么水平先说结论:两千多块钱,在二手市场上凑一套能跑Qwen3-27B级别模型、推理速度稳定超过280 tok/s的平台,这件事在2025年是完全可行的。我自己前前后后折腾了大概三周,从选卡、…

2026/10/4 7:32:02 阅读更多 →

日新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/2 10:36:31 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/3 9:42:35 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/3 9:42:36 阅读更多 →