news 2026/10/17 6:52:31

Vue组件封装实战:从props、插槽到分页表格的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue组件封装实战:从props、插槽到分页表格的完整指南

做了几年前端,我几乎每天都在跟 Vue 打交道。从最开始照着官方文档写 Todo List,到后来维护几十万行代码的后台系统,我发现在 Vue 世界里,组件就是一切的地基。很多开发者学完语法、会写页面,但一到自己动手封装一个基础组件(按钮、弹窗、分页、表格)就卡壳:props 传了页面不更新,事件没反应,样式莫名其妙的串来串去。这篇文章我准备把自己在实际项目里积累的 Vue 基本组件心得完整写出来,从设计思路、注册方式,到 props、emits、插槽、组件通信,最后用一个分页表格组件的封装案例把所有知识点串起来。适合刚接触 Vue 的新手,也适合想系统性整理组件知识、准备从"会用"走向"会设计"的初中级前端。

1. 为什么说组件是 Vue 应用的地基

1.1 没有组件化的项目到底有多痛苦

先聊点实际的。我最早接手一个后台管理项目时,里面有一个订单管理页面,搜索表单、表格、分页、操作按钮全部堆在同一个文件里,一个文件 3000 多行。改一个弹窗的样式要 Ctrl+F 找半天,因为跟这个弹窗无关的表格代码也在同一层作用域里,变量名稍微冲突就出诡异 bug。

这种痛点我相信很多人都经历过。页面越来越大,逻辑越来越纠缠,一个人改起来都费劲,更别说多人协作。后来这个项目用了几年时间勉强重构,第一步就是把页面按功能拆成组件:搜索区抽成一个组件、表格抽成一个组件、分页抽成一个组件,再由页面组件把它们组合起来。拆完之后,每个文件的代码量降到了原来的十分之一,出 bug 的定位时间也从小时级降到了分钟级。

1.2 组件到底解决了什么问题

拆完之后你会发现,组件的价值主要在三件事上:

  • 复用性:同一套 UI 和逻辑可以在多个页面复用。比如一个日期范围选择器,订单页用、财务报表页也用,抽成组件之后只维护一份代码。
  • 职责隔离:每个组件只干一件事,搜索组件管输入、表格组件管展示、分页组件管页码,各自的作用域互不干扰。
  • 协作效率:团队开发时,可以按组件分人,每个人负责一个组件,通过 props 和事件约定好接口,互不阻塞。

所以不要觉得"组件就是一段模板代码"。组件本质上是一种代码组织和协作的单元,在 Vue 里,它的载体通常是单文件组件(SFC),一个.vue文件就是一个独立组件。组件设计得好不好,直接决定项目后期是越改越轻松,还是越改越崩溃。

2. 组件的注册与结构:先把骨架立住

2.1 全局注册还是局部注册,这个选择影响全局

注册组件有两种方式:全局注册和局部注册。很多新手图省事,全部用全局注册,结果项目组件一多,全局命名冲突和性能问题全出来了。

全局注册是这样写的,在入口文件里:

import { createApp } from 'vue' import App from './App.vue' import BaseButton from './components/BaseButton.vue' const app = createApp(App) app.component('BaseButton', BaseButton) app.mount('#app')

注册之后,在项目的任何模板里都能直接<BaseButton />使用。它的好处是方便,不用每次导入,适合真正通用的基础组件,比如按钮、输入框。

局部注册则是在当前组件里用 import 引入:

<template> <div> <Pagination :total="100" @change="handlePageChange" /> </div> </template> <script setup> import Pagination from './Pagination.vue' function handlePageChange(page) { console.log('当前页:', page) } </script>

区别我用一个表格给你列清楚:

对比项全局注册局部注册
使用方式注册后任意模板直接用,无需导入每个使用组件都要 import
命名冲突多组件同名时后注册的覆盖先注册的按文件引用,冲突概率低
打包体积全局注册的组件会被打进主包按需加载,配合分包更灵活
适用场景真正通用的基础组件(Button、Input)页面级组件、业务组件

我个人建议是:除非是BaseButton、BaseInput这种确定全项目通用的组件,其余全部用局部注册。局部注册最大的好处是依赖关系清楚,你看到这个文件顶部 import 了哪些组件,就知道它用了哪些东西,排查问题省很多事。

2.2 单文件组件的三大块是怎么分工的

一个.vue文件里通常有三块:<template>、<script setup>、<style scoped>。它们各司其职:

  • template负责结构:声明组件要渲染哪些 DOM,写布局和插值表达式。
  • script setup负责逻辑:定义数据、计算属性、事件处理函数,以及子组件需要用到的 props。
  • style scoped负责样式:scoped的作用是让这里的样式只作用于当前组件的元素,不会污染全局。

这里我想强调一下scoped的机制。很多人以为scoped就是把样式"锁"在组件内部,其实它的实现方式是给组件模板里的元素添加一个><template> <h2>标题</h2> <p>段落</p> </template>

这不是件小事。实际封装组件时,如果你要用 Vue 3 的原子组件(比如把多个 Fragment 组合起来返回),多根节点能省掉一层多余的外层 div,避免布局被破坏。

组件命名方面,文件用 PascalCase(大驼峰),比如PaginationTable.vue,模板里你可以用<PaginationTable />或<pagination-table />,Vue 都认。业务组件建议都统一成 PascalCase,避免跟原生 HTML 元素混淆,也更符合主流习惯。

3. props 与 emits:组件的对外接口

3.1 props 的三种传值方式

组件之间的信息传递,第一步就是 props。父组件往子组件传数据,有三种写法。

第一种,静态传值,直接写字符串:

<Pagination title="订单列表分页" />

第二种,动态绑定,用v-bind或它的简写::

<Pagination :total="totalCount" :page-size="pageSize" />

这里totalCount是父组件里的一个响应式变量,路由、接口返回的数据变化,子组件里接收到的 prop 也会实时变化。

第三种是传对象、数组这类引用类型,这其实是动态绑定的特殊写法:

<Pagination v-bind="{ total: 100, pageSize: 10, currentPage: 1 }" />

这种写法适合你手里已经有一个配置对象时,一次性把多个属性都传给子组件。我在封装组件时也经常用这种写法,能少写几行代码。

3.2 props 的校验与默认值,别偷懒

有些开发者封装组件时defineProps里直接写个数组就完事:

const props = defineProps(['total', 'pageSize'])

项目小的时候没问题,项目一复杂,传错类型、传漏参数的问题全来了。组件是对外开放的接口,接口就应该有协议。我习惯写成对象形式,带上类型、默认值和校验:

<script setup> const props = defineProps({ total: { type: Number, required: true, validator: (value) => value >= 0 }, pageSize: { type: Number, default: 10 }, currentPage: { type: Number, default: 1 }, loading: { type: Boolean, default: false } }) </script>

required: true表示必填,default指定默认值,validator可以加一个简单的校验函数。校验错误时 Vue 会在控制台给出警告提示,这在开发期能省掉大量 debug 时间。

另外一个很关键的细节:props 是单向数据流。子组件不能直接修改 props 的值,只能通过触发事件让父组件去改。这一点在所有的官方文档里都会强调,但实际项目中总有人不小心写了props.total++这种代码。Vue 会警告你违反了单向数据流,而且这种修改不会同步回父组件,很容易变成隐性 bug。

3.3 emits:把事件交还给父组件

子组件怎么通知父组件?靠的是emit。在单文件组件里定义事件有两个作用,一个是声明这个组件"会发出哪些事件",另一个是方便 Vue 帮你校验。

推荐写法:

<script setup> const emit = defineEmits(['change', 'update:currentPage']) function handlePageChange(page) { emit('change', page) emit('update:currentPage', page) } </script>

父组件监听:

<Pagination @change="handlePageChange" />

事件的命名要注意,推荐用 kebab-case(短横线)风格,比如page-change,因为 HTML 属性本身不区分大小写,@page-change和@pageChange在某些边缘情况下会踩坑,统一用短横线最稳。

3.4 v-model 的底层机制,封装表单组件关键

v-model在原生输入框上是语法糖,在组件上同样也是。它的完整写法是:

<Pagination :modelValue="currentPage" @update:modelValue="currentPage = $event" />

而v-model="currentPage"就是上面写法的简写。这意味着,子组件里如果想实现"v-model 联动",需要接收一个modelValueprop,并在值变化时触发update:modelValue事件。

一个最简单的封装示例,比如一个数字输入框:

<template> <input type="number" :value="modelValue" @input="$emit('update:modelValue', Number($event.target.value))" /> </template> <script setup> defineProps({ modelValue: { type: Number, default: 0 } }) defineEmits(['update:modelValue']) </script>

父组件就能直接这样用:

<NumberInput v-model="pageNum" />

多个 v-model 的写法是v-model:page="pageNum"对应update:page,这个技巧在实际封装复杂组件时非常有用。比如一个时间范围组件,可以定义v-model:start和v-model:end两个值,父组件用起来就像操作两个普通变量。

4. 插槽:把"坑位"留给父组件

4.1 默认插槽与具名插槽

props 是数据层面的接口,插槽是 DOM 层面的接口。封装一个弹窗或者卡片组件时,外层结构和样式是固定的,但内容区域可能要放什么由使用者决定,这时候就该用插槽。

最简单的方式是默认插槽,在子组件模板里写<slot />,父组件放入的内容会渲染在这个位置:

<!-- Card.vue --> <template> <div class="card"> <div class="card__header"> <slot name="header" /> </div> <div class="card__body"> <slot /> </div> </div> </template>

具名插槽就是给<slot>加一个 name 属性,父组件用template配合v-slot指定内容进哪个插槽:

<Card> <template #header> 订单统计 </template> <template #default> 这里显示内容区域 </template> </Card>

我在实际项目里总结了一个经验:如果组件里只有静态结构和少量可变位置,优先考虑用插槽而不是 props 传模板内容。插槽可以让父组件完全掌控这块区域的 DOM,传模板字符串会有 XSS 风险和灵活性差的问题,看起来麻烦但长期维护成本最低。

4.2 作用域插槽,让插槽内容拿到子组件的数据

作用域插槽稍微进阶一点,它能让父组件在写插槽内容时访问到子组件里的数据。典型场景是封装表格组件时,想让外部自定义某一列的渲染,但外部需要拿到当前行的数据。

子组件里这样暴露数据:

<template> <slot :row="rowData" :index="rowIndex" /> </template>

父组件这样使用:

<Table> <template #row="{ row }"> <span class="highlight">{{ row.status === 1 ? '启用' : '停用' }}</span> </template> </Table>

这里的{ row }是解构写法,取的就是子组件 slot 上传出来的rowData。这种模式在封装表格、列表、选择器等组件时几乎是必须掌握的技能,可以说没有作用域插槽,很多通用组件都封不出来。

4.3 插槽的备用内容与使用原则

插槽是可以写默认内容的,父组件不传时显示默认内容,传了就用父组件的内容替换:

<slot name="empty"> <p>暂无数据</p> </slot>

关于插槽使用原则,我个人的体会是:能用插槽实现的自定义,就不要用"传一个变量控制 if else"来实现。拿一个弹窗组件举例,如果你用confirmText这个 prop 控制确认按钮文字,那以后是传字符串、还是传按钮、还是传一组带图标的按钮?一旦扩展,props 会越加越多。而如果直接用插槽允许父组件自定义 footer 区域,整个组件的扩展性会好很多。

5. 组件通信:父子、兄弟、跨层级

5.1 父子通信,先掌握这三板斧

父子组件通信是最高频的场景,主要方式有三种:

  • props + emits:父传子用 props,子传父用 emit。适用场景:直接父子关系的数据流。
  • v-model:适用于父组件要双向控制子组件某个值的场景,本质上是 props + emits 的语法糖。
  • ref 调用子组件方法:父组件通过ref拿到子组件实例,直接调用子组件暴露的方法。

第三种写法要注意,在 Vue 3 组合式 API 里,子组件用defineExpose明确暴露内容,父组件才能拿到:

<!-- 子组件 --> <script setup> function showInfo() { console.log('子组件的方法') } defineExpose({ showInfo }) </script>
<!-- 父组件 --> <script setup> import { ref } from 'vue' import ChildComp from './ChildComp.vue' const childRef = ref(null) function callChild() { childRef.value.showInfo() } </script> <template> <ChildComp ref="childRef" /> </template>

我在早期写 Vue 2 时没有defineExpose,直接this.$refs.child就能拿所有方法。Vue 3 改成显式暴露,其实是对组件内部实现的一种保护,防止外部随意调用内部未公开的东西。刚开始觉得繁琐,后来发现这种约束能倒逼你把组件对外 API 想清楚。

5.2 跨层级通信,provide 和 inject 的妙用

当组件层级很深时,比如页面组件 -> 布局组件 -> 卡片组件 -> 按钮组件,如果每层都靠 props 往下传,中间组件就要写很多跟自己无关的透传代码。Vue 提供了provide和inject来解决这种问题。

父级提供数据:

<script setup> import { provide } from 'vue' const appConfig = { theme: 'dark', locale: 'zh-CN' } provide('appConfig', appConfig) </script>

任意后代组件注入使用:

<script setup> import { inject } from 'vue' const appConfig = inject('appConfig', { theme: 'light', locale: 'en' }) console.log(appConfig.theme) // 'dark' </script>

inject的第二个参数是默认值,当找不到 provide 时使用。实际使用中有两个坑:

一,如果 provide 的值不是响应式的,后代组件拿到的就是一个静态值,页面不会跟着更新。要注意provide('count', ref(0))要包ref或reactive。

二,滥用 provide 会让数据来源变得不清晰,就像全局变量一样难以追踪。我的经验是:跨两三层用 provide/inject 可以,跨四五层还用它,不如考虑直接引入状态管理工具。

父子通信和跨层级通信的区别,可以看下面这张对照表:

通信场景推荐方案注意事项
父传子props单向数据流,子组件不要直接改 props
子传父emits事件命名统一 kebab-case
双向同步v-model底层就是 modelValue + update:modelValue
祖孙/隔代传值provide / inject传响应式数据,否则不更新
全局状态共享Pinia / Vuex按需使用,别把 store 当垃圾桶

5.3 状态管理的取舍,别把组件通信搞复杂

不太熟的人以为只要组件之间要共享数据,就得上 Vuex/Pinia。我的经验是反过来:先用组件通信解决,解决不了再上状态管理。

比如一个页面里两个兄弟组件,一个负责筛选条件,一个负责展示列表。它们要共享筛选结果。最简单的方式是把筛选状态提升到它们共同的父组件里,通过 props 传给列表组件,筛选变化时触发事件让父组件更新状态。这种"状态提升"模式完全够用,代码逻辑也透明。

只有当前状态确实需要被多个页面共享(比如用户信息、权限、购物车)时,才应该引入全局状态库。因为全局状态有一个隐形成本:你在一个组件里改了 store 里的数据,完全不知道哪里会受影响。组件通信至少还是沿着组件树的,状态管理则是网络状的,一旦数据链路复杂,排查成本成倍上升。

6. 实战:从零封装一个分页表格组件

6.1 需求分析与接口设计

前面说了这么多理论,现在我们来实际做一个组件。我选择的分页表格组件是后台系统里最典型的基础组件,它同时用到了 props、emits、插槽、作用域插槽、事件定义等多项技术。

需求梳理一下:

  • 外层面板:标题栏(可选)+ 表格区域 + 分页区域。
  • 表格列:支持自定义,每列一个插槽,外部可以拿到当前行数据和列配置。
  • 分页:显示总条数、页码、每页条数,翻页和修改每页条数时通知外部。
  • 表格数据来源:由父组件传入数组,组件内部不做接口请求,做纯展示组件。

接口设计如下:

名称类型说明
columnsArray列配置数组,每项包含{ prop, label, width }
dataArray表格数据数组
totalNumber总记录数,分页计算用
page-sizeNumber每页条数,默认 10
current-pageNumber当前页码,默认 1
事件 update:current-page页码变化时触发用于配合 v-model:current-page
事件 update:page-size每页条数变化时触发用于配合 v-model:page-size
插槽 默认自定义单元格作用域参数包含{ row, column, index }

这样的接口设计基本覆盖了后台列表页的常见需求,而且数据流方向清晰:父组件喂数据,子组件只负责展示和输出用户操作,不做任何业务判断。

6.2 具体实现代码

组件完整代码如下:

<template> <div class="pagination-table"> <div class="pagination-table__title" v-if="title"> <slot name="header">{{ title }}</slot> </div> <table class="pagination-table__table"> <thead> <tr> <th v-for="col in columns" :key="col.prop" :style="{ width: col.width }"> {{ col.label }} </th> </tr> </thead> <tbody> <tr v-for="(row, rowIndex) in data" :key="rowIndex"> <td v-for="col in columns" :key="col.prop"> <slot :name="col.prop" :row="row" :column="col" :index="rowIndex"> {{ row[col.prop] }} </slot> </td> </tr> <tr v-if="!data.length"> <td :colspan="columns.length" class="pagination-table__empty"> <slot name="empty">暂无数据</slot> </td> </tr> </tbody> </table> <div class="pagination-table__footer"> <span>共 {{ total }} 条</span> <button :disabled="currentPage <= 1" @click="handlePageChange(currentPage - 1)" > 上一页 </button> <span>第 {{ currentPage }} / {{ totalPages }} 页</span> <button :disabled="currentPage >= totalPages" @click="handlePageChange(currentPage + 1)" > 下一页 </button> <select :value="pageSize" @change="handleSizeChange($event)"> <option v-for="size in pageSizeOptions" :key="size" :value="size"> {{ size }} 条/页 </option> </select> </div> </div> </template> <script setup> import { computed } from 'vue' const props = defineProps({ columns: { type: Array, required: true }, data: { type: Array, default: () => [] }, total: { type: Number, default: 0 }, currentPage: { type: Number, default: 1 }, pageSize: { type: Number, default: 10 }, pageSizeOptions: { type: Array, default: () => [10, 20, 50] }, title: { type: String, default: '' } }) const emit = defineEmits(['update:currentPage', 'update:pageSize']) const totalPages = computed(() => Math.max(1, Math.ceil(props.total / props.pageSize))) function handlePageChange(page) { if (page < 1 || page > totalPages.value) return emit('update:currentPage', page) } function handleSizeChange(event) { const size = Number(event.target.value) emit('update:pageSize', size) // 切页大小后,当前页码如果超出范围,重置到第一页 emit('update:currentPage', 1) } </script> <style scoped> .pagination-table__footer { display: flex; align-items: center; gap: 12px; margin-top: 12px; font-size: 14px; } </style>

这里有几个关键点:

第一,data的默认值用了() => [],而不是[]。因为 props 的默认值如果是对象或数组,必须写成函数返回新实例,否则所有组件实例会共享同一个引用,出现 A 组件的修改影响到 B 组件的诡异问题。

第二,分页的totalPages用了computed,它依赖 props 里的total和pageSize,当这两个值变化时,总页数会自动更新。这就是计算属性解决"多数据源联动"的价值。

第三,翻页按钮里我没有直接修改 currentPage,而是通过 emit 通知父组件更新。父组件拿到新页码后,去请求新数据,再通过 props 把新data传回来,整个循环是单向的,非常清晰。

6.3 在项目里如何使用这个组件

假设一个典型的列表页面:

<script setup> import { ref, onMounted } from 'vue' import PaginationTable from './components/PaginationTable.vue' const columns = [ { prop: 'orderNo', label: '订单号', width: '180px' }, { prop: 'amount', label: '金额' }, { prop: 'status', label: '状态' }, { prop: 'actions', label: '操作', width: '200px' } ] const tableData = ref([]) const total = ref(0) const currentPage = ref(1) const pageSize = ref(10) async function fetchData() { const res = await fetchOrderList({ page: currentPage.value, pageSize: pageSize.value }) tableData.value = res.list total.value = res.total } onMounted(fetchData) </script> <template> <PaginationTable v-model:current-page="currentPage" v-model:page-size="pageSize" :columns="columns" :data="tableData" :total="total" @update:current-page="fetchData" @update:page-size="fetchData" > <template #status="{ row }"> <span :class="row.status === 1 ? 'tag-success' : 'tag-default'"> {{ row.status === 1 ? '已支付' : '未支付' }} </span> </template> <template #actions="{ row }"> <button @click="handleView(row)">查看</button> <button @click="handleEdit(row)">编辑</button> </template> </PaginationTable> </template>

这个使用方式体现了我在第 3.4 和第 4.2 里讲的两点:通过v-model:current-page实现页码双向同步;通过具名作用域插槽#status、#actions自定义列内容,同时安全地拿到了当前行的row数据。

你可能会问,为什么操作按钮这种明显是当前页面的业务逻辑,要放在插槽里来定义?因为表格组件本身是一个通用基础组件,它不应该知道"订单"应该有查看、编辑按钮。插槽把这样的业务决策权留给了使用方,这才是组件复用的关键逻辑。

6.4 组件完成后一定要过一遍自查清单

组件写完不代表完事,我在实际项目中有一套自查清单:

  • props 校验写了吗:所有必要的字段有没有类型和默认值?漏了一个default,使用方一旦没传,运行起来可能就是undefined。
  • 单向数据流守住了吗:组件内部有没有直接改 props?如果有,改成 emit 事件。
  • 事件命名统一吗:全部用 kebab-case,避免大小写问题。
  • 插槽都有备用内容吗:至少空数据提示要用备用内容兜底。
  • style 都加 scoped 了:尤其注意全局样式是否意外泄漏。

这个清单我一般会让组件的使用者也会签一遍,哪怕项目很赶,这个流程基本能过滤掉 80% 的封装问题。

7. 常见问题与排查技巧实录

7.1 props 传了对象,但页面不更新

这个是我被问得最多的问题之一。父组件的 data 明明变了,子组件里拿到的对象却还是旧的。

排查思路是:props 是引用类型时,Vue 的响应式追踪的是引用本身,不是深层次属性。如果父组件里做了类似obj.name = '新名字'这种直接修改已有属性的操作,在某些情况下确实能触发更新,但如果是直接给对象新增/删除一个属性(Vue 3 里用reactive默认能追踪增删,但如果你用的是普通对象配合 ref,就可能失效),或者this.obj = newObj时引用变了,这种则没问题。

更隐蔽的场景是:子组件在onMounted里把 props 拷贝了一份到本地,然后一直用副本渲染。这样父组件每次更新 props,本地副本都不会变。解决方法是改用computed基于 props 计算,或者直接把渲染数据源指向 props。

我自己的习惯是:组件内尽量直接用 props 作为渲染数据源,不到万不得已不做本地副本。

7.2 事件不触发的三个常见原因

如果你发现子组件 emit 了,但父组件监听没反应,按顺序排查这三处:

  • 事件名对不对。如果子组件emit('page-change'),父组件写@pageChange,在 DOM 模板里就会对不上,因为 DOM 模板会把大小写归一化。解决办法是统一用 kebab-case。
  • 有没有在正确的组件实例上监听。看起来像废话,但对于使用v-for渲染多组组件时,经常有人监听了第一个组件,却以为第二个组件的事件会触发。
  • 是不是在原生元素上误用了组件事件。比如你在子组件根元素上直接@click,如果你用的是普通元素包装,那是原生事件,直接绑定即可;如果你把@click加在组件标签上,它会被当作自定义事件,需要子组件 emit('click') 才能触发。

这里有个特别容易踩的坑:给组件标签加@click,期望点组件内部触发。实际上如果不做attr透传或 emit,这个事件是接不到的。在 Vue 3 里,如果你的子组件根节点是单个元素,父组件的v-on监听器会被自动透传给根元素,所以很多场景碰巧能工作,但一旦子组件是多根节点,这种行为就失效了。为了稳定性,组件内部需要触发的主要交互事件,都应该显式 emit,不要依赖透传。

7.3 样式不生效和样式泄漏问题

scoped样式不生效,最常见的场景是你在父组件里想改子组件内部某元素的颜色:

<style scoped> /* 这里的样式不会作用到子组件内部元素 */ .child-content { color: red; } </style>

这是因为 scoped 会给选择器加上父组件的 data 属性,而子组件内部元素没有这个属性。解决思路是:

子组件主动提供样式接口,比如通过 props 接收一个headerClass,然后在子组件模板里挂到对应元素上。

或者使用 CSS 变量,父组件通过自定义属性传颜色进去。推荐这个方式,因为 CSS 变量天然能穿透组件边界。

真要暴力解决的话,Vue 3 也支持非 scoped 的样式块,但尽量少用。你会在排查 bug 时知道"全局样式污染"有多难受。

7.4 keep-alive 缓存组件后,数据不刷新

动态组件配合 keep-alive 时,页面可能从缓存恢复,onMounted不会重新执行,导致你预期的"每次进入都拉新数据"失效。

解决办法是用新增的onActivated钩子:

import { onActivated, onMounted } from 'vue' onMounted(() => { // 首次挂载时执行 }) onActivated(() => { // 每次从缓存激活时执行 })

这算不上 bug,是设计使然,但如果你不知道这个钩子,排查起来还挺费劲的。

7.5 我建议你养成的组件排查习惯

最后聊点个人习惯。从 Vue 2 转到 Vue 3 之后,我排查组件问题基本固定套路:先看 props 是不是响应式的、再看事件名和监听位置、再看样式隔离、最后看是不是被缓存了。搭配浏览器里的 Vue Devtools,几乎能定位到所有层级问题。

另外我还习惯给每个组件写一个README.md或注释块,把 props、emits、插槽的用途写清楚。别小看这个文件,半年后你回来看自己封装的组件,靠的就是这些记录,不然真的会忘。对于团队协作,这也是一份最小可用的组件文档,比专门写设计文档便宜得多。

组件封装这事,写到后面拼的不是 API 熟练度,而是你对"谁能改什么、数据怎么流、边界在哪里"这些设计问题的把握。多封装几个真实业务里要用的组件,多踩几次样式串扰和数据不同步的坑,你自然就体会到我说的这些经验了。

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

爆肝一个月,Java 从入门到精通01 - 简介及发展史

为什么要学Java&#xff1f; 因为Java是全球排名第一的编程语言&#xff0c;Java工程师也是市场需求最大的软件工程师&#xff0c;选择Java&#xff0c;就是选择了高薪。 为什么Java应用最广泛&#xff1f; 从互联网到企业平台&#xff0c;Java是应用最广泛的编程语言&#…

作者头像 李华
网站建设 2026/10/17 6:51:10

真正做工程项目要用的技能,已经29 万Star

需求刚讲完一半&#xff0c;AI 已经把代码写完了。跑起来报错&#xff0c;让它修&#xff0c;它改了三个文件&#xff0c;报错换了个样式。来回四轮&#xff0c;你可能开始怀疑自己描述得不够清楚&#xff0c;下次还是一样。mattpocock/skills 对这个循环给的解释是&#xff1a…

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

EMUI8开机执行sh脚本:修改ramdisk与init.rc的完整指南

做 Android 系统定制的人&#xff0c;应该都碰到过这类需求&#xff1a;手上的设备跑的是 EMUI8&#xff08;某厂商基于 Android 8.x 开源系统深度定制的 ROM&#xff09;&#xff0c;希望在设备每次开机时自动执行一段 sh 脚本。可能是要调整内核参数、挂载额外分区、拉一个本…

作者头像 李华
网站建设 2026/10/17 6:50:35

千问 LeetCode 343. 整数拆分 Java实现

Java 实现 class Solution { public int integerBreak(int n) { if (n < 3) { return n - 1; }int quotient n / 3;int remainder n % 3;if (remainder 0) {return (int) Math.pow(3, quotient);} else if (remainder 1) {return (int) Math.pow(3, quotient - 1) * 4;}…

作者头像 李华