Vue组件化核心:从页面思维到组件通信的实战指南

很多初学者学Vue,前四天还跟得上节奏,一到第五天就有点懵——因为从这一天开始,你不再只是"写页面",而是开始写"组件"了。我自己的感受是:模板语法、指令、状态管理这些学起来都还算直观,真正让脑子转个弯的,恰恰是组件化这一关。Day5这个节点,就是把"页面思维"切换到"组件思维"的关键一天。这篇文章就围绕第五天的核心内容展开,把组件化这条主线讲透,配合能直接跑的代码示例和踩坑记录,帮还在组件门口徘徊的朋友顺利迈过去。

1. 组件化到底改变了什么

1.1 从"整块页面"到"搭积木"的思维切换

很多小白一开始写Vue,脑子里还是传统网页开发的模式:一个页面就是一个大文件,从上到下堆结构、堆样式、堆逻辑。模板写了一大段,data里挂着十几个变量,methods里躺着二十多个函数,页面稍微复杂一点,文件就到上千行,改一个按钮可能要在文件里来回滚半天。

组件化要解决的,恰恰就是这个痛点。你可以把页面拆成一块一块独立的积木,每块积木有自己的结构、自己的样式、自己的逻辑,互不干扰。比如一个电商首页,可以拆成头部导航、轮播图、商品列表、购物车入口、底部Tab栏,每个部分独立成一个组件,组合在一起就是一个完整页面。这个思路在真实项目中非常实用。我在某公司做项目评审时见过一个刚转正的前端同事,把整个后台管理页面全写在一个单文件组件里,结果需求每次变更,他都要在那个文件里大海捞针。后来花了一个下午把页面拆成表格组件、筛选表单组件、分页组件,他自己都说,改动范围一下子缩小到原来的十分之一。

Vue的组件化还有一个好处:组件是自带边界的。只要约定了对外暴露的数据和事件,内部怎么实现几乎不影响外部使用。这就意味着一个团队里,A同学负责商品卡片组件,B同学负责购物车组件,两个人可以并行开发,最后把组件一拼就能跑通。这种协作方式,和传统一人改一处、全局受牵连的开发模式比起来,效率差别不是一点半点。

1.2 组件封装的第一条铁律:data必须是一个函数

初学者最容易犯的第一个组件层面的错误,就是用对象形式写data。在Vue 2时代,根实例的data可以是对象,但组件里必须写成函数,否则多个组件实例会共享同一份数据,改一个全变。Vue 3组合式API里虽然变成了ref和reactive,但"每个组件实例的数据必须独立"这个原则没有变。

举个例子:

// 错误示范 data() { return { count: 0 } } // 这样写就是错的: data() { count: 0 }

你可能觉得这不就是少了return吗?但组件被复用时,每个实例都需要一份自己的count。写成函数,每次调用data()都会返回一个全新的对象;直接写对象字面量的话,所有实例拿到的都是同一个内存地址。这个细节,就是组件化和小白之前写页面最大的不同——你不再只有一个页面实例,而是可能同时存在几十个组件实例,数据隔离必须在一开始就做好。

我在实际教学和带人过程中,发现很多人会在这里栽跟头,但奇怪的是,报错往往不明显,表现是"我改了A组件的count,B组件怎么也跟着变了"。排查很久才发现是data没写成函数。所以这一条值得刻在脑子里。

2. 第一个组件和父子组件的组合方式

2.1 用defineProps给组件传参

今天要掌握的第一块硬核内容,是父组件怎么把数据传给子组件。官方叫法是通过props传递属性。这个概念可以理解成:父组件给子组件递了一张纸条,纸条上写好了子组件需要显示的内容。

看一个最简示例,一个展示用户信息的子组件:

<!-- UserCard.vue --> <template> <div class="user-card"> <h3>{{ name }}</h3> <p>{{ email }}</p> </div> </template> <script setup> const props = defineProps({ name: { type: String, required: true }, email: { type: String, default: '暂无邮箱' } }) </script>

父组件里这样使用:

<template> <UserCard name="张三" email="zhangsan@example.com" /> </template> <script setup> import UserCard from './UserCard.vue' </script>

defineProps是Vue 3组合式API里专门用来声明props的编译宏,不需要手动引入,直接在<script setup>里用就行。声明的时候可以用简单数组形式(defineProps(['name', 'email'])),也可以用对象形式带上类型、默认值、必填校验。实际开发中我建议用对象形式,理由很简单:三五个props的时候数组写法看着清爽,等组件复杂到十几个props时,没有类型校验和默认值兜底,调用方稍不注意就会传错,排查起来非常痛苦。

2.2 父子组件的嵌套与插槽

有了props,父子组件之间能传数据了,但还有一个场景:父组件想往子组件里塞一段自己的模板内容。比如一个通用弹窗组件,弹窗的边框、关闭按钮、背景遮罩都是公共的,但弹窗里面是什么,每个页面都不一样。这种"骨架相同、内容不同"的需求,用slot来解决。

<!-- Modal.vue --> <template> <div class="modal-mask"> <div class="modal-content"> <slot></slot> <button @click="$emit('close')">关闭</button> </div> </div> </template>

使用方:

<Modal @close="showModal = false"> <p>这里的内容完全由父组件控制</p> </Modal>

<slot></slot>像一个占位符,父组件塞进来的内容最终会渲染到这个位置。这就是组件插槽的魅力——它让组件的复用性又上了一个台阶。我对小白的建议是,先掌握默认插槽,把具名插槽、作用域插槽放到后面再深入,Day5能理解默认插槽的机制就够了,否则信息量太大容易劝退。

2.3 组件拆分到什么粒度才合适

这个问题没有绝对标准,但有一点可以分享:如果一个组件模板超过80行、逻辑里职责超过一件事,就值得考虑拆分了。我见过有的项目把整个登录页拆成一个组件文件,几百行代码塞在一起,虽然也跑得动,但维护性其实和没拆一样。反过来,如果把一个按钮的样式都拆成组件,那就过度设计了,组件数量爆炸,反而拖累开发效率。

一个简单实用的判断标准:单个组件是否可以被多次复用,或者是否承担了明确的单一职责。比如"用户头像"组件,可能在导航栏、评论区、消息列表里都用得上,值得拆;"登录表单"里的用户名输入框,只在这个表单里出现,就没必要单独拆。Day5你只需要建立"该拆就拆、不该拆别硬拆"的基本感觉,等写过几个项目,这个粒度判断自然就熟练了。

3. 子组件和父组件如何对话

3.1 用defineEmits发送事件

数据从父到子,靠props;数据从子到父,靠事件。这就是Vue组件通信的"单向数据流"原则:父组件可以给子组件数据,但子组件不能直接修改父组件的值,只能通过触发事件通知父组件"你去改"。

<!-- ChildCounter.vue --> <template> <button @click="increase">+1</button> </template> <script setup> const emit = defineEmits(['increase']) function increase() { emit('increase', 1) } </script>

父组件:

<template> <ChildCounter @increase="handleIncrease" /> <p>当前计数:{{ count }}</p> </template> <script setup> import { ref } from 'vue' import ChildCounter from './ChildCounter.vue' const count = ref(0) function handleIncrease(num) { count.value += num } </script>

这里子组件并没有直接去改父组件的count,而是通过emit('increase', 1)告诉父组件"我这边发生了增加事件,附带数据是1"。父组件监听@increase,决定接下来怎么处理。这个过程,像极了实际工作中下级向上级汇报,但决定权始终在上级手里。之所以设计成这种单向模式,是因为如果允许子组件随意改父组件的数据,那数据流向就乱了,一个页面几十个组件互相改来改去,排查一个bug需要翻遍所有组件,谁开发谁知道那个痛苦。

3.2 自定义事件的事件名规范

我在Review代码时发现,很多人习惯把事件名写成驼峰式,比如emit('increaseCount'),父组件里也监听@increaseCount。这在Vue 3里能用,但官方推荐的是kebab-case,即emit('increase-count'),父组件写成@increase-count。原因是HTML标签对大小写不敏感,模板里写@increaseCount,浏览器解析时可能会把它当成@increasecount,导致事件对不上。虽然Vue内部做了兼容处理,但这种"可能踩坑但不确定什么时候踩"的问题,最好从源头避免。

3.3 用v-model实现父子双向绑定

props加emits已经能覆盖大部分通信场景,但有一种特殊情况值得单独讲:表单项。比如一个输入框组件,父组件需要知道用户输入了什么,同时还要能外部重置输入框的值,这就涉及真正的"双向绑定"。

Vue 3里自定义组件支持v-model,本质上是modelValue这个props加update:modelValue这个事件的语法糖。下面这个输入框组件就是标准写法:

<!-- MyInput.vue --> <template> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </template> <script setup> defineProps(['modelValue']) const emit = defineEmits(['update:modelValue']) </script>

父组件:

<MyInput v-model="username" />

这一行v-model展开后,等价于:

<MyInput :modelValue="username" @update:modelValue="username = $event" />

也就是说,子组件负责通知父组件"输入框的内容变了",父组件更新username后,再把新值通过modelValue传回子组件。看起来绕了一圈,但核心逻辑没变:数据流的源头和终点始终在父组件手里。对于小白来说,Day5先把这个标准写法记住,知道它是props加emits的组合即可,后续要在组件里封装表单控件,这个基础就够用了。

3.4 一个常见误区:直接修改props

和"data必须是函数"齐名的另一个坑,就是子组件内部直接修改props的值。比如这样:

<!-- 错误示范 --> <script setup> const props = defineProps(['visible']) props.visible = false // 报错或警告 </script>

Vue会给出警告,严格模式下会直接报错。原因前面已经说过了,props是父组件传给子组件的"只读快照",子组件没有权限修改。正确做法是:要么触发事件让父组件改,要么把props值复制一份到子组件内部状态,再在本地修改。这个限制会让很多从原生JS转过来的人觉得麻烦,但它正是Vue组件化维护性的基石——数据流向清晰可追踪,出错时能顺着箭头很快定位。

4. 组件生命周期和你的第一道实战坑

4.1 四个最常用的生命周期钩子

Vue组件的生命周期钩子,就是组件从创建到销毁过程中特定时机触发的事件。Day5不要求把所有钩子背下来,但至少要掌握四个:onMounted、onUpdated、onBeforeUnmount、onUnmounted。

<script setup> import { onMounted, onUpdated, onBeforeUnmount, onUnmounted } from 'vue' onMounted(() => { // 组件挂载到页面上之后 // 适合做接口请求、DOM操作、定时器初始化 }) onUpdated(() => { // 组件因数据变化重新渲染之后 }) onBeforeUnmount(() => { // 组件即将卸载,清理定时器、取消事件监听的好时机 }) onUnmounted(() => { // 组件已经卸载 }) </script>

初学阶段,onMounted用的频率最高,因为小白第一批实战项目里,基本都要在页面加载后去请求数据。但这里有个隐藏的坑:如果你在onMounted里初始化了一个定时器,却忘了清理它,组件卸载后定时器依然在跑,会引发一系列诡异问题——比如页面已经切走了,控制台还在报错,甚至内存泄漏。这个问题的典型解法就是在onBeforeUnmount或onUnmounted里clearInterval。

4.2 一个真实的小白排查实录

我自己带过一位刚入门的朋友,他写了个列表页,需求是点击按钮打开一个弹窗,弹窗里展示当前选中项的数据。他做法是:点按钮时,弹窗组件的props接收到当前数据,然后弹窗组件内部在onMounted里发起数据请求。表面看起来没问题,但实际跑起来,第一次打开弹窗数据正常,第二次打开弹窗数据还是旧的。

排查了一圈,原因很简单:弹窗组件没有被销毁,它只是被v-if控制显隐。同一个组件实例,在第一次打开后挂载,第二次打开时压根不会重新触发onMounted,自然也就不会重新请求数据。这就是我前面说的"组件生命周期"和"页面生命周期"的差别——你以为弹窗重新渲染了,但实际上它还是那个组件,状态一直留着。

解决办法有两种。第一种,用v-if让弹窗每次都重建,挂载钩子每次都会触发;第二种,改用watch监听props的变化,在数据更新时重新请求。前者简单直接,后者更接近真实项目的常用做法。

<script setup> import { watch } from 'vue' const props = defineProps(['currentItem']) watch(() => props.currentItem, (newVal) => { // 每次传入的currentItem变化时,重新拉取详情 }) </script>

这个案例很典型,我经常拿它来说说明白"组件挂载不等于页面加载"这件事。很多小白的思维惯性是"打开页面=挂载",一旦遇到弹窗、抽屉这种不销毁的组件,就容易踩到生命周期不会重复触发的坑。

4.3 常见问题速查表

现象可能原因排查方向
组件数据改了但页面没变修改了props而不是声明响应式数据检查是否直接改了props,用ref或reactive声明本地数据
弹窗第二次打开数据是旧的组件实例未销毁,onMounted不重复触发用v-if重建组件,或watch监听触发更新
多个相同组件互相影响data()没有写成函数,或组合式API里复用了同一个ref确保每个组件都有独立的数据实例
子组件事件触发了但父组件没收到事件名大小写或命名不一致统一用kebab-case命名并检查defineEmits声明
定时器在页面切换后一直报错卸载时没清理定时器在onBeforeUnmount里clearInterval或clearTimeout
列表渲染时每项组件需要不同状态直接用了循环索引作为组件唯一标识给每项传入唯一的key,不要用index

表格里最后一条我要特别说一句:Vue依赖key来区分列表中的每一项,很多时候大家在列表渲染时图省事直接用index做key,一旦列表发生增删、排序,组件状态就会出现串位。如果你在给列表项做复选框、输入框这类带交互的组件,这个坑几乎必踩。正确做法是用数据自带的唯一字段,比如用户ID、订单号,实在没有的话,可以用一个自增的ID生成函数,总之别用index。

5. 我的实操心得:Day5阶段应该怎么练

光看不练,组件化思维是建立不起来的。我给初学者的建议是:不要急着去做大型项目,而是找你手头已经写过的某个页面,把它拆成至少三个组件,哪怕只是"页头、内容区、页脚"这种最朴素的分法,也能让你体会到组件化的意义。拆完之后,试着把某些数据从父组件传给子组件,再试着让子组件在某个事件触发时通知父组件。这比我布置100道填空题都管用。

还有一个好用的练习法,是"组件接口契约"练习。你定义一个组件,先声明它接收哪些props、暴露哪些事件,然后问自己:如果别人要用这个组件,他需要知道什么信息?先把这个"使用说明"写出来,再去实现内部逻辑。这个习惯能倒逼你把组件设计得干净清晰,而不是写着写着就什么都往一个文件里塞。

另外,建议从Day5开始,建立自己的"组件问题清单"。遇到一个坑,就把它记下来,后面再碰到相同症状就直接查表。我自己早年写前端时就有这个习惯,很多经验不是靠天赋,而是靠记录和复盘慢慢积累起来的。等到组件通信、生命周期、插槽这些概念都熟练了,再回看Day5这一天的内容,你会发现自己已经在不知不觉中跨过了"写页面"到"写应用"的分水岭。

相关新闻

提示组装与提示缓存:LLM请求时延优化的关键路径

提示组装与提示缓存:LLM请求时延优化的关键路径

做LLM服务的人应该都有同感&#xff1a;一次请求进来&#xff0c;真正让模型跑起来之前的那些准备动作&#xff0c;反而是最容易出问题的地方。尤其是提示组装&#xff0c;系统提示、历史消息、工具定义、用户最新的问题&#xff0c;全都要在短短几十毫秒里拼成一个结构正确的上…

2026/10/20 23:37:22 阅读更多 →
Claude Code自动模式实战:从审批式对话到任务式执行的安全迁移指南

Claude Code自动模式实战:从审批式对话到任务式执行的安全迁移指南

最近我在一个内部项目里把 Claude Code 升级到带“自动模式”的版本&#xff0c;连着用了两周多处理日常编码任务&#xff0c;整体感受很复杂&#xff1a;一方面觉得 AI 编码工具的自主性终于到了能真正落地的程度&#xff0c;另一方面又在某些深夜看着终端里一串自动动作时忍不…

2026/10/20 23:37:22 阅读更多 →
SSM+Vue考研互助辅导平台:从需求拆解到部署全链路实战解析

SSM+Vue考研互助辅导平台:从需求拆解到部署全链路实战解析

这个“SSM278的考研互助辅导平台vue”是不少计算机相关专业的同学在课程设计或毕业设计阶段会遇到的一类典型题目。单看名字&#xff0c;它同时点名了后端框架&#xff08;SSM&#xff09;、项目业务方向&#xff08;考研互助辅导&#xff09;和前端技术栈&#xff08;Vue&…

2026/10/20 23:37:22 阅读更多 →

最新新闻

深入解析镜像加载原理:分层、联合文件系统与内容寻址

深入解析镜像加载原理:分层、联合文件系统与内容寻址

1. 从一次镜像拉取失败说起&#xff1a;为什么需要搞懂镜像加载原理很多人第一次接触容器技术&#xff0c;卡住的地方不是写配置文件&#xff0c;也不是跑命令&#xff0c;而是某天执行拉取镜像时突然报错&#xff0c;或者发现本地磁盘被一堆看不懂的层占满了&#xff0c;删也不…

2026/10/21 0:20:29 阅读更多 →
装完WorkBuddy不会用?从数据到复盘报告的环境搭建全流程

装完WorkBuddy不会用?从数据到复盘报告的环境搭建全流程

装完WorkBuddy&#xff0c;打开界面一片空白&#xff0c;左边几个菜单点进去也没什么反应&#xff0c;你知道它肯定能做点什么&#xff0c;但就是不知道从哪下手。这个状态我太熟了——这不是你一个人的问题&#xff0c;是绝大多数刚接触WorkBuddy的人都卡在同一个地方&#xf…

2026/10/21 0:20:29 阅读更多 →
深入理解 JavaScript 原型链与 super:避开 this 递归与继承的那些坑

深入理解 JavaScript 原型链与 super:避开 this 递归与继承的那些坑

前几天排查一个表单校验服务时&#xff0c;我盯着屏幕上的堆栈溢出看了很久。子类重写了父类的validate方法&#xff0c;里面顺手写了一行this.validate()&#xff0c;本来是想“复用父类的校验逻辑”&#xff0c;结果方法自己调自己&#xff0c;一路递归到爆栈。旁边同事说了句…

2026/10/21 0:20:29 阅读更多 →
OFDM+QAM仿真对比:16QAM与64QAM的BER与星座图解析

OFDM+QAM仿真对比:16QAM与64QAM的BER与星座图解析

简介&#xff1a;这是一份面向本硕博通信类教研学习的OFDM16QAM与OFDM64QAM对比仿真资料&#xff0c;使用MATLAB完成&#xff0c;重点对比两种高阶正交幅度调制在OFDM系统中的误码率表现&#xff0c;并绘制星座图、分析基带波形差异&#xff0c;适合课程设计、毕业设计以及数字…

2026/10/21 0:20:29 阅读更多 →
全球数字贸易协定结构化数据库:172国395份协定的政策量化分析基座

全球数字贸易协定结构化数据库:172国395份协定的政策量化分析基座

简介&#xff1a;本资源是一套覆盖2000—2024年全球数字贸易规则演进的实证研究数据集与配套学术文献&#xff0c;面向国际贸易、数字经济、国际法及产业政策领域的研究者、高校师生与政策分析人员&#xff0c;用于支撑计量建模、区域协定比较、制造业数字化影响评估等深度研究…

2026/10/21 0:20:29 阅读更多 →
Snipaste实操指南:截图即贴图,告别窗口反复切换

Snipaste实操指南:截图即贴图,告别窗口反复切换

我第一次认真用 Snipaste&#xff0c;不是因为它截图特别快&#xff0c;而是因为一张截图截完之后&#xff0c;它居然可以不落地、不打开看图软件、不新建文档&#xff0c;直接像便利贴一样钉在桌面上。这个“截图之后可以直接固定在桌面”的动作&#xff0c;解决了我拖延很久的…

2026/10/21 0:18:29 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 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/20 10:02:08 阅读更多 →
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/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练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/20 9:50:45 阅读更多 →