news 2026/10/17 21:47:07

Vue 3 组件测试实战:Vitest 与 Vue Test Utils 的稳定编码方式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue 3 组件测试实战:Vitest 与 Vue Test Utils 的稳定编码方式

最近给一个维护了大半年的中后台系统补单元测试,我发现在团队里搜“Vit常见编码方式”,翻出来的测试代码至少有四五种风格:有人mount一把梭,有人万事shallowMount,有人测异步靠sleep(2000),还有人把组件内部的状态管理函数全都 mock 掉。这些测试在写完的那一刻全是绿的,等到产品改需求,红起来也是整片整片地红。

先把工具链说清楚。标题里的 Vit,在我经历的项目里通常指 Vitest,它是基于 Vite 的测试框架;而“常见编码方式”的另一个主角是@vue/test-utils,也就是 Vue 的官方测试工具库,负责组件的挂载、交互和断言。大家平时说 Vit 测试,其实默认就是这两者搭配使用。这篇文章围绕 Vue 3 组件测试的常见编码方式展开,我会结合具体代码讲明白挂载、props、事件、v-model、异步和 mock 到底怎么写才稳定,适合刚接触 Vue 单测、或者写过一些但总是不稳定的同学参考。你不需要一次性读完,把它当成排查测试问题时的备查手册更合适。

1. 先厘清环境:Vitest 与 Vue Test Utils 在测试里各管什么

1.1 两者的职责边界

Vitest 负责提供describe、it、expect、mock、fake timers、覆盖率这些基础设施;@vue/test-utils负责把组件实例化,生成一个包装器(wrapper),让你能查询 DOM、触发事件、断言 props 和 emits。简单说,Vitest 是“考试环境”,Vue Test Utils 是“答题卡上的操作工具”。

很多新手容易把两者混着用。比如想断言组件的响应式更新,不通过 wrapper 去查 DOM,反而直接在外头操纵组件实例。Vue 3 里setup的状态对外是封闭的,外部拿不到内部ref,硬要去拿实例操作只会让测试和实现严重耦合。更合理的编码方式是:组件内部状态变化一律通过 DOM 表现来断言,组件对外通信一律通过emitted()事件来断言,组件输入一律通过 props 来驱动。

我举个例子,一个表单组件在输入非法邮箱时,内部会往errors里塞一条提示。测试时不需要访问errors,只需要断言页面上的错误文本有没有出现即可。这样测试不关心错误存在哪个字段里,只关心用户看没看到提示。这一点是后面所有编码方式的前提,一旦这个边界乱了,测试一定会越写越痛苦。

1.2 最小配置里容易被忽略的三个开关

Vitest 可以直接读取 Vite 配置文件,也可以用独立的vitest.config.ts。下面是一个我在实际项目中常用来起步的配置,很多团队都在这份配置上栽过跟头:

/// <reference types="vitest" /> import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], test: { environment: 'jsdom', globals: true, css: false, setupFiles: ['./test/setup.ts'], clearMocks: true, restoreMocks: true } })

其中三个开关是新手最容易忽略的。

第一个是environment: 'jsdom'。Vue 组件测试需要 DOM 环境,默认的 node 环境里没有document、window,只要组件模板一挂载就会报错。如果你发现mount刚执行就抛document is not defined,基本都是这个配置没写。

第二个是css: false。不关掉 CSS 处理的话,测试启动时还要去解析样式文件,遇到一些 CSS 新特性还会直接报错。组件测试关注的是逻辑和结构,样式交给浏览器和视觉回归去管,配置里关掉是合理的。

第三个是setupFiles。这个文件会在每个测试文件执行前运行一次,适合注册全局组件、mock 少量原生 API。我在团队里看到的最大坑是有人把“修改全局状态”的逻辑写进了 setup 文件,结果测试文件之间互相污染,跑单文件全绿,跑全量随机红。setup 里只放公共且“只读”的内容,动态状态一律在beforeEach/afterEach里管理。

1.3 一个完整的 setup.ts 雏形

import { config } from '@vue/test-utils' import { vi } from 'vitest' config.global.stubs = { transition: false } // 某些浏览器 API 在 jsdom 里不存在,统一 mock if (!window.matchMedia) { window.matchMedia = vi.fn().mockImplementation((query) => ({ matches: false, media: query, onchange: null, addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn() })) }

这里把Transition组件 stub 掉是一个经验活。Vue 内置的Transition在单测里经常带来不稳定行为,比如进入动画的状态没走完,导致断言时机不对。把它 stub 成false可以直接关闭过渡渲染,测试就干净很多。

2. 挂载方式定生死:mount、shallowMount 与 stub 的选择

2.1 从一次“改子组件导致父组件测试崩掉”说起

有次我维护一个列表页组件,里面渲染了一个性能很重的子组件,专门做长文本展示。父组件的测试用mount全量挂载,断言的是列表头几个字段是否渲染。后来子组件内部优化,把某个根节点类名改了,父组件的测试直接红了。

当时排查了半天,发现父组件逻辑一行没动,纯粹是被子组件的实现细节拖下水。这个问题的本质是:mount会渲染整棵真实组件树,子孙组件里的任何 DOM 变化都会成为父组件测试的噪声。单元测试追求的是“当前组件行为”的确定性,而不是整棵组件树的快照。从那以后我大多改用shallowMount,它会自动把除当前组件以外的所有子组件替换为 stub,父组件的用例只关心自己这一层的行为。

不过也不能矫枉过正。全项目无脑shallowMount也会出问题,比如某个操作按钮只有子组件真正渲染出来才能被找到,stub 掉之后按钮消失了,测试就会写成“假阳性”——断言的对象根本不是用户真实看到的界面。所以挂载方式的选择需要有个判断标准,而不是一刀切。

2.2 什么时候选 shallowMount,什么时候必须 mount

我根据踩坑经验整理了一张判断表:

场景推荐挂载方式原因
只验证当前组件自身的渲染逻辑shallowMount隔离子组件噪声,速度快
子组件之间需要真实交互mount需要真实 DOM 事件和层级
一个子组件的行为当前用例不关心shallowMount或手动 stub减少外部依赖
子组件会真实影响当前组件的存在性mount例如通过v-if切换子组件
验证 slot 内容是否被子组件正确接收mountslot 内容必须真实渲染才能断言

核心判断就三步:被测用例的断言对象是不是当前组件的对外行为?子组件会不会给这个用例带来不确定因素?测试速度能不能接受?

这三步里最容易被忽略的是第二步。很多人想当然用mount,结果子组件里发了一个请求、一个定时器,父组件测试不仅要 mock 子组件的接口,还要处理子组件的生命周期,用例越写越长。反过来,如果你要测的是一个弹窗的整体交互,包括打开、确认、关闭的完整链路,那shallowMount把弹窗内部按钮 stub 成了空壳,断言自然无从谈起。此时必须mount。

2.3 手写 stub 和全局组件的处理方式

shallowMount的默认 stub 是一个能接收 slot 的简单元素,但在某些情况下默认 stub 不够用。比如项目里用了router-link,组件里写的是:

<router-link to="/detail">查看详情</router-link>

shallowMount默认不会帮你把带路由跳转的组件 stub 得很干净,比较稳妥的做法是在global.stubs里显式指定:

import { shallowMount } from '@vue/test-utils' const wrapper = shallowMount(ArticleItem, { global: { stubs: { 'router-link': { template: '<a><slot /></a>' }, HeavyContent: true } } })

这里HeavyContent: true表示把它替换成纯占位 stub,router-link则提供了带 slot 的简化模板,让组件内部的链接文案还能正常渲染出来。这样既不影响当前组件对链接文案的断言,也不会真的触发路由解析。

另一个经验是attachTo。如果组件在onMounted里调用了getBoundingClientRect,或者需要监听全局事件,单纯的mount可能因为组件没有真正挂到 DOM 文档里而拿不到布局信息。这时可以:

const wrapper = mount(TableFooter, { attachTo: document.body }) // 用例结束后必须清理 wrapper.unmount()

attachTo之后组件会真实插入文档,影响范围比普通挂载更大,所以一定要在用例结束或afterEach里调用unmount,否则事件监听和 DOM 节点都会泄漏到下一个用例。这是常被忽略的坑。

3. 组件交互的常见编码方式:props、事件与 v-model

3.1 props 断言与 setProps 的正确姿势

props 是组件的输入,测试的第一步通常是验证输入能否正确反映到界面上。假设有个徽标组件:

<!-- Badge.vue --> <script setup> defineProps({ label: { type: String, default: '' } }) </script> <template> <span class="badge">{{ label }}</span> </template>

对应的测试代码:

import { mount } from '@vue/test-utils' import Badge from '@/components/Badge.vue' it('正确渲染 label 并响应 props 更新', async () => { const wrapper = mount(Badge, { props: { label: 'new' } }) expect(wrapper.find('.badge').text()).toBe('new') await wrapper.setProps({ label: 'hot' }) expect(wrapper.find('.badge').text()).toBe('hot') })

这里有两个关键点。第一,setProps必须await,因为 Vue 的 DOM 更新是异步的,不等待就断言可能拿到上一次渲染的结果。第二,setProps只能更新挂载时已经传入的 props;如果你在setProps里塞一个挂载时根本没传过的 prop,Vue 会把它当作 attribute 直接表现在 DOM 属性上,不会变成组件内部逻辑的输入。这两点写测试时经常会踩,尤其是第二点,看起来没报错,但实际上测试根本没驱动到组件内部。

3.2 trigger 与 emitted:对外事件的测试套路

组件的输出,除了渲染结果,就是事件。测试事件有一个固定套路:先通过trigger触发用户操作,再用emitted()断言组件派发了什么事件。

<!-- Counter.vue --> <script setup> import { ref } from 'vue' const count = ref(0) const emit = defineEmits(['change']) function increment() { count.value += 1 emit('change', count.value) } </script> <template> <button class="inc" @click="increment">{{ count }}</button> </template>
import { mount } from '@vue/test-utils' import Counter from '@/components/Counter.vue' it('点击按钮后更新数字并派发 change 事件', async () => { const wrapper = mount(Counter) await wrapper.find('.inc').trigger('click') expect(wrapper.text()).toContain('1') expect(wrapper.emitted('change')).toHaveLength(1) expect(wrapper.emitted('change')?.[0]).toEqual([1]) })

emitted()返回的对象结构是两层数组:外层数组的每一项对应一次事件触发,内层数组是那次触发时传入的参数。所以emitted('change')?.[0]拿到的是第一次触发的事件参数列表,toEqual([1])表示第一次change事件携带的参数是1。这个结构刚接触时容易绕晕,但记住“外层次数、内层参数”就够了。

trigger也可以传入第二个参数,用来模拟事件对象里的字段。比如组件内部读取了event.clientX,可以这样写:

await wrapper.find('.inc').trigger('click', { clientX: 100 })

这个能力在测试拖拽、点击坐标等场景里非常有用。

3.3 v-model 双向绑定的三种写法

自定义组件和外部v-model通信,是组件测试里容易出错的点。Vue 3.4 之后推荐用defineModel来简化写法,假设有个搜索输入框:

<!-- SearchInput.vue --> <script setup> const model = defineModel() </script> <template> <input v-model="model">import { mount } from '@vue/test-utils' import SearchInput from '@/components/SearchInput.vue' it('输入内容时向外派发 update:modelValue', async () => { const wrapper = mount(SearchInput, { props: { modelValue: '初始值' } }) await wrapper.get('[data-test="search"]').setValue('vue') expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['vue']) })

第二层,验证外部更新modelValue时视图能同步刷新:

it('外部更新 modelValue 时输入框能同步', async () => { const wrapper = mount(SearchInput, { props: { modelValue: '初始值' } }) await wrapper.setProps({ modelValue: '新值' }) const input = wrapper.get('[data-test="search"]').element as HTMLInputElement expect(input.value).toBe('新值') })

如果项目里还在用 Vue 3.3 或更早版本,defineModel不可用,通常会写update:modelValue事件和props.modelValue的组合。测试代码不需要变,核心断言仍然是两层:输入时发事件、外部刷新时视图同步。记住这一个原则,无论组件内部写法怎么变化,测试编码方式都是通用的。

4. 异步与 Mock 的编码套路:定时器、Promise 与远程接口

4.1 nextTick、flushPromises 与 fake timers 如何选

异步是 Vue 组件测试里最让人头疼的部分。组件里常见的异步场景有三种:Vue 自身渲染更新、Promise 链上的数据回调、setTimeout/setInterval定时器。这三种等待方式不能乱用,我用一个表格简单对比一下:

场景等待手段说明
普通 DOM 更新await nextTick()只等一个 Vue 更新周期
await某个接口后的数据渲染flushPromises把当前微任务队列里的 Promise 回调都执行完
setTimeout / setIntervalvi.useFakeTimers()+ 时间推进模拟时间流逝,而不是真等

很多人误以为nextTick能解决所有异步问题。实际上,如果组件在onMounted里发了一个await fetchData(),接下来赋值的ref才触发视图更新,单靠一个nextTick是不够的。因为nextTick只是等当前响应式更新队列,不等接口返回。

碰到这种情况,我常用一个辅助函数:

const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0))

这个实现不算严谨,但在多数“接口返回后立刻更新 DOM”的场景里够用。更严谨的方案是直接装一个flush-promises包,内部实现比手写版本可靠得多。如果项目里不允许随便引第三方测试依赖,也可以用vi.waitFor(Vitest 0.34 之后新增),它会更贴近“等待某个条件成立”的表达。我的经验是:先用flushPromises处理链式 Promise,再用await nextTick()处理最后一次视图更新,两者结合基本覆盖大部分异步组件场景。

4.2 定时器场景的两种模式

定时器组件用真实时间等待是最傻的写法,一个 10 秒的轮询组件会让测试真实跑 10 秒。正确做法是用 fake timers。

import { mount } from '@vue/test-utils' import { vi } from 'vitest' import ProgressTimer from '@/components/ProgressTimer.vue' describe('ProgressTimer', () => { afterEach(() => { vi.useRealTimers() }) it('倒计时结束后派发 finish 事件', async () => { vi.useFakeTimers() const wrapper = mount(ProgressTimer, { props: { duration: 3000 } }) await vi.advanceTimersByTimeAsync(3000) expect(wrapper.emitted('finish')).toHaveLength(1) }) })

这里必须用advanceTimersByTimeAsync而不是同步版本。原因很隐蔽:如果定时器回调里还有await语句,同步推进时间只会执行到第一个await之前,后续逻辑要到下一个微任务才会执行。advanceTimersByTimeAsync会在每次时间推进后等待微任务队列清空,更适合真实的定时器回调。

结束之后必须调用vi.useRealTimers(),否则后面所有测试文件的时间都会被 fake 掉,出现一些完全无法定位的随机失败。这也是 5.3 会展开讲的“测试污染”源头之一。

4.3 接口 Mock 的颗粒度:mock 模块还是 mock 请求层

组件测试里几乎绕不开接口请求。常见的编码方式是在 API 模块边界做 mock,也就是把整个接口模块替换掉,让被测组件拿到假数据。用 Vitest 的vi.mock有一个提升陷阱,先看正确写法:

const mocks = vi.hoisted(() => ({ fetchUser: vi.fn() })) vi.mock('@/api/user', () => ({ fetchUser: mocks.fetchUser })) import { mount } from '@vue/test-utils' import UserCard from '@/components/UserCard.vue' it('卡片展示接口返回的用户名', async () => { mocks.fetchUser.mockResolvedValue({ name: 'Tom', level: 3 }) const wrapper = mount(UserCard) await flushPromises() expect(wrapper.text()).toContain('Tom') })

vi.mock的工厂函数会被提升到文件顶部执行,这意味着你在文件中部声明const mocks = ...,工厂函数里引用它时,该变量还没有初始化,直接报ReferenceError。vi.hoisted就是专门解决这个问题的机制,它把变量声明提升到所有 mock 之前。很多新手写vi.mock遇到变量引用报错,第一反应是调整 import 顺序,其实问题根源是提升。

Mock 的颗粒度建议放在 API 模块层,不要深入 mock 组件内部的工具函数。比如组件里有个纯函数formatMoney,你为了“方便测试”把它 mock 成固定返回“¥100”,那测试的实际效果就很弱:格式化逻辑改坏了,测试照样绿。正确的思路是,断言组件最终对外呈现的行为时,被测组件内部的关键逻辑必须真实跑一遍;只有当前用例不关心的第三方依赖(如路由、全局组件、浏览器 API)才值得 mock。

4.4 定时器与 Promise 混合时的一个坑

有个组件在定时器回调里做了两步操作:先更新一个ref,再调用另一个异步方法。我一开始写的测试是这样的:

vi.useFakeTimers() const wrapper = mount(NotifyTimer) vi.advanceTimersByTime(3000) expect(wrapper.emitted('notify')).toHaveLength(1)

结果断言经常失败,有时候又偶发通过。问题出在advanceTimersByTime是同步的,它把定时器回调执行完之后,回调里的await还没结算,事件自然没发出来。改为await vi.advanceTimersByTimeAsync(3000)之后,测试就稳定了。

这个坑排查起来很折磨人,因为偶发绿、偶发红,很难怀疑到同步异步混合上。我的建议是:只要定时器回调里出现过await、.then()或Promise.resolve(),一律用advanceTimersByTimeAsync,不要赌同步执行顺序。

5. 不稳定测试的排查实录:从“全绿”到“偶尔红”

5.1 反模式一:事件触发后不 await,断言全靠运气

我见过最多的问题写法是把trigger当成同步方法:

wrapper.find('button').trigger('click') expect(wrapper.text()).toContain('1')

trigger返回的是一个 Promise,内部包含了 Vue 的异步更新流程。不await的话,断言执行时机不确定。当次点击后的 DOM 更新如果刚好在断言前完成,测试就是绿的;一旦事件队列顺序稍有变化,就会红。这个“全绿”到“偶尔红”的切换没有任何逻辑上的警告,最伤代码库信任感。

清洗写法很简单:

await wrapper.find('button').trigger('click') expect(wrapper.text()).toContain('1')

规则也简单:凡是和 Vue 更新、DOM 操作、事件触发相关的断言,前面一律带上await。这条规则比任何“深刻理解 Vue 更新机制”的建议都实用。

5.2 反模式二:过度 mock 掉被测对象的核心行为

另一个不稳定的来源不是代码不稳定,而是“测试根本没测到真东西”。有一次排查一个金额格式化组件,发现某位同事在测试里把组件内部引入的formatMoney整个 mock 掉了,mock 返回值写死为“¥100”,然后断言页面显示“¥100”。

这个用例永远绿,但其实是假绿。因为真正的格式化函数可能早就坏掉了,测试仍然显示通过。我后来和团队约定了一个判断标准:如果某个 mock 是为了“让测试快点通过”而存在的,那这个 mock 就值得警惕;更能说明问题的是,删掉这个 mock 后测试如果仍然能验证组件行为,那它就不该存在。

提示:mock 的合理边界是“你不关心它在真实环境里如何工作的外部依赖”,而不是“你懒得处理它返回值的内部逻辑”。

5.3 反模式三:测试之间共享状态导致的随机失败

有阵子跑测试总是偶发失败,单独跑一个文件全绿,跑完整个项目一定有几个文件红。最后定位到原因非常典型:前面某个测试文件用了vi.useFakeTimers(),但没调vi.useRealTimers(),后续测试文件里的setTimeout全被接管,那些依赖真实时间线的组件全部乱了。

排查过程里我建立了一套自检清单,现在也分享出来:

  • 每个文件afterEach里是否有vi.useRealTimers()?
  • 每次vi.fn()的调用记录是否被清除,用clearMocks还是restoreMocks?
  • 测试结束有没有调用wrapper.unmount()?
  • 用到全局 store、localStorage 时,每个用例前是否恢复到初始状态?
  • setup 文件里有没有修改可变全局对象?

其中clearMocks和restoreMocks有个容易混淆的差异。clearMocks只清空 mock 的调用记录,不还原被 spy 的方法实现;restoreMocks会把被vi.spyOn改写的方法恢复成原始实现。实际项目里我两个都开,行为最干净:

test: { clearMocks: true, restoreMocks: true }

5.4 稳定测试的“自检问题清单”

最后整理一份我在 code review 时逐条问的清单,基本上能套在大多数组件测试上:

  • 这个用例是否只断言当前组件的对外行为,而不是它的内部实现?
  • 是否存在 mock 掉核心逻辑、导致断言没有意义的假绿?
  • 异步操作是否都通过await等待完成,而不是“猜时机”?
  • 每个用例之间是否存在共享的全局状态没有清理?
  • 定时器场景是否用了 fake timers,并确保其他文件不受影响?
  • 断言的内容是否是用户可感知的 DOM 或事件,而不是直接读ref内部值?

这六个问题里,前两个决定测试的价值,后四个决定测试的稳定性。我把它们沉淀成团队的测试规范之后,改造后的测试代码从“偶尔红”变成了“长期绿”,遇到回归时定位问题也快了很多。

最后再分享一点项目里的实际体会

写完这套体系后,我对 Vit 的“常见编码方式”有了新的理解:它不是一个固定的语法清单,而是一套面向行为的测试思维。同样的组件,用mount还是shallowMount、先断言 DOM 还是先断言事件、mocking 到哪一层,这些选择背后的判断标准比代码本身更重要。我的建议是,新项目一开始就确定几条简单的测试约定,比如交互类组件必须有 “触发事件 + 断言 emitted” 的用例,有异步请求的组件必须在 API 边界 mock,凡是触发操作前一律await。把这些约定写进团队的 review 检查项,比等测试红了再补救要划算得多。

如果你正好在给 Vue 3 项目补测试,先从最容易写坏的两个地方开始改:把所有trigger加上await,把所有vi.mock的引用变量收进vi.hoisted。这两处稳定了,你的测试瞬间会可靠上一大截。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 21:46:52

前端文件处理利器:FileReader与Blob实战详解

1. 从一次文件上传说起&#xff1a;为什么前端必须掌握FileReader和Blob前两周有个朋友跟我抱怨&#xff0c;说他们公司做内部管理系统&#xff0c;遇到一个特别头疼的需求&#xff1a;用户要上传几百MB的压缩包&#xff0c;还要能预览图片、查看文本文件内容&#xff0c;甚至要…

作者头像 李华
网站建设 2026/10/17 21:45:33

win-acme v2.2.9.1701:Windows 下全自动部署 Let‘s Encrypt SSL 证书

简介&#xff1a;win-acme.v2.2.9.1701.x64.pluggable.zip 是一款面向Windows系统管理员与Web运维人员的免费SSL证书自动化管理工具包&#xff0c;专为解决Lets Encrypt证书在IIS、nginx等服务上的快速申请、安装与续订难题而设计&#xff0c;尤其适用于中小型网站、测试环境及…

作者头像 李华
网站建设 2026/10/17 21:43:34

自动驾驶轨迹规划:五阶多项式与LatticePlan采样策略详解

1. 五阶轨迹规划到底在解决什么问题1.1 从“能开”到“开得舒服”的分水岭做自动驾驶决策规划这一块的人&#xff0c;迟早会撞上同一个瓶颈&#xff1a;路径规划给出的结果在几何上是通的&#xff0c;但车坐起来就是不舒服。要么方向盘来回小幅修正&#xff0c;要么过弯时横向加…

作者头像 李华
网站建设 2026/10/17 21:42:30

西门子PLC梯形图编程实战:从环境搭建到电机互锁与调试

简介&#xff1a;《西门子PLC编程基础》PPT课件是一份面向工业自动化初学者与现场工程师的入门资料&#xff0c;围绕PLC的结构组成、循环扫描机制、工作特点以及存储器与寻址方式展开讲解。内容覆盖CPU、存储器、输入输出电路、编程装置等核心部件&#xff0c;并结合输入采样—…

作者头像 李华
网站建设 2026/10/17 21:41:29

数据库入门:从SPJ案例理解关系模型与SQL本质

简介&#xff1a;本资源是《数据库系统概论&#xff08;第5版&#xff09;》王珊、萨师煊主编教材的配套课后习题参考答案&#xff0c;面向高校计算机、软件工程、信息管理等专业本科生及数据库初学者&#xff0c;用于辅助理解关系模型、SQL语言、关系代数等核心理论与实践难点…

作者头像 李华
网站建设 2026/10/17 21:37:19

Ubuntu24 安装 ClaudeCode 后,把 settings 改到 TaoToken 的完整配置大纲

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

作者头像 李华