☰
Vue组件化核心指南:从组件通信到插槽与异步加载
2026/10/7 12:15:38 网站建设 项目流程

你是不是也经历过这种状态:一个页面写了一千多行,从数据请求到按钮样式全堆在一起,改一个按钮颜色要翻半天代码,想复用又不知道从哪下手?我第一次用 Vue 做项目时,最大的困惑不是语法,而是“组件”和“组件化”这两个词——它们听起来很厉害,但到底什么时候该拆、怎么拆、拆完怎么拼回去,完全没人给我讲清楚。吃了几个月的亏之后,我才慢慢把 Vue 组件这套东西摸透。

这篇文章我想把 Vue 入门阶段最关键的组件及组件化知识梳理一遍,包括组件到底是什么、怎么创建一个组件、父子组件如何通信、插槽怎么用、动态组件和异步组件怎么玩,以及我在实际项目中沉淀下来的一些封装经验和踩坑教训。不管你是刚安装完 Vue 还在配置环境的新手,还是已经写过几个页面但总觉得组件设计得别扭的开发者,这篇文章应该能帮你把思路理顺。

1. 先想明白:组件到底是干什么的

1.1 组件化解决的是人的问题,不是技术问题

很多人一上来就盯着技术看:props 怎么传、emit 怎么写、注册用什么 API。但组件化解决的首先是人的问题。一个功能模块几千行代码如果全部摊在一个文件里,你维护它的时候大脑得同时装下数据请求、条件渲染、事件处理、样式覆盖,认知负担重得吓人。而组件化本质上是“分治”——把大问题拆成多个小问题,每个小问题在一个相对独立的文件里解决,最后再通过组合把整体拼起来。

我在团队里经常打一个比方:你不可能让一个厨子同时负责买菜、切菜、炒菜、洗碗、收银、管库存。所以餐厅要分后厨、前厅、采购。前端也是一样,页面是餐厅,组件是各个岗位,组件化就是在划分职责边界。

1.2 组件的本质:一个自带状态和行为的“页面片段”

从 Vue 的视角看,一个组件就是一段可复用的、带有自己的样式、数据和行为的页面片段。你可以把它理解成一个“带按钮的刷卡机”:它从外面接收指令(props),自己内部也有记忆(data/ref),操作它的时候它会通过屏幕告诉你结果(模板渲染),关键动作还会向外广播(emit 事件)。

组件和普通函数不太一样。函数是“输入进去、输出出来”,调完就结束了。组件是“长”在页面上的,它有自己的生命周期,有可以被外部影响的状态,还能在多个父组件之间复用。这是理解组件的最重要一步——它不是一段函数,而是一个独立运行的“小应用”。

为了直观理解,你可以把整个页面想象成一棵树:

App ├── Header │ ├── Logo │ └── UserMenu ├── Content │ ├── SearchBar │ └── ListItem │ ├── ListItemInfo │ └── ListItemAction └── Footer

上面每一层都是一个组件,每一个组件都只关心自己这一层的逻辑。Header 不关心 ListItem 怎么渲染,Footer 不关心搜索逻辑。谁依赖谁,一目了然。这也是“高内聚、低耦合”在组件设计里的具体含义:每个组件内部尽量把相关的东西聚在一起,组件与组件之间的牵连尽量少。

2. 从零写一个组件:定义、注册、使用的完整链路

2.1 三种定义组件的姿势,以及它们的差别

Vue 里定义组件有三种常见姿势。第一种是 Options API,选项式写法,Vue 2 和 Vue 3 都支持,数据、方法、生命周期各自放在 data、methods、mounted 等选项里。第二种是 Composition API,Vue 3 主推的方式,用 setup 函数把逻辑组织在一起。第三种是直接写单文件组件 SFC,也就是 .vue 文件,把模板、脚本、样式塞在同一个文件里。

这里要给刚入门的朋友一个建议:不要迷信某种写法,而是先理解两者解决的问题。Options API 的好处是结构固定,新手容易上手,但逻辑分散——同一个功能的代码可能被 data、methods、watch 拆到不同区块。Composition API 的好处是同一个功能的代码可以放在一起,尤其是一个组件里涉及多个业务逻辑时,阅读和后续抽离都更舒服。

下面是最基本的一个组件,用 Composition API 写的:

<template> <div class="counter"> <p>当前值:{{ count }}</p> <button @click="increment">加一</button> </div> </template> <script setup> import { ref } from 'vue' const count = ref(0) function increment() { count.value++ } </script> <style scoped> .counter { padding: 12px; border: 1px solid #e5e7eb; border-radius: 8px; } </style>

上面这段代码就定义了一个计数器组件。它有内部状态count,有行为increment,有UI模板,样式也带上了。组件要的就是这种“自包含”:状态、行为、UI 长在同一个地方。

2.2 全局注册和局部注册,选错会带来什么麻烦

组件定义好之后,需要注册才能用。注册分为全局注册和局部注册。

全局注册就是把组件挂到整个应用上,任何组件里都能直接用,最常见的是在 main.js/main.ts 里调用app.component:

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')

局部注册则是在某个组件的script setup里引入后,就只在这个组件里可用:

<script setup> import BaseButton from './components/BaseButton.vue' </script>

注意,script setup模式下,引入的组件会自动注册到当前组件,不需要手动写 components 选项。这个细节经常让从 Vue 2 转过来的同学困惑——在 Vue 2 里你必须在components: {}里注册一下。

我的建议是:除了真正意义上的通用基础组件(如按钮、输入框、弹窗),其他一律用局部注册。全局注册看着方便,但有两个隐患。第一,打包体积:全局注册的组件即使某些页面没用,也会被打进最终产物里。第二,命名污染和维护耦合:全局组件多了以后,改一个组件要留意全站影响。实际项目里,组件总数几十个甚至上百个,全局注册会让依赖关系变得非常不透明。

2.3 跑起来之后的第一个组件长什么样

新手第一次把组件跑起来,最重要的是理解“组件一旦注册,就可以像普通 HTML 标签一样使用”。比如在 App.vue 里:

<template> <main> <Counter /> <Counter /> </main> </template>

一个组件被使用多次,每次都是独立实例,它们各自维护自己的count,互不干扰。很多刚接触组件的人会误以为组件的ref是全局共享的——并不是。组件实例是独立的,这也是复用性的前提。

这里顺带提一下环境配置里常见的坑:如果你用的构建工具是 Vue CLI 或 Vite,新装的 Vue 项目里通常已经配置好了@vitejs/plugin-vue或vue-loader。如果跑.vue文件时报错,先查两件事:第一,依赖有没有装齐;第二,构建配置里有没有对应的 SFC 插件。大多数“组件跑不起来”的问题,不是组件写错了,而是环境没配对。

3. 父传子与子传父:数据流动的方向感

3.1 props 传参:只传数据,不传控制权

组件通信是组件化里最核心的环节。最常见的场景是父组件要往子组件里塞数据。比如父组件拿到用户信息,想传给子组件展示,这时候就要用 props。

<!-- 父组件 --> <script setup> import UserCard from './UserCard.vue' const user = { name: '张三', age: 28, email: 'zhangsan@example.com' } </script> <template> <UserCard :name="user.name" :age="user.age" :email="user.email" /> </template>
<!-- 子组件 UserCard.vue --> <script setup> const props = defineProps({ name: { type: String, required: true }, age: { type: Number, default: 0 }, email: { type: String, default: '' } }) </script> <template> <div class="user-card"> <h3>{{ props.name }}</h3> <p>年龄:{{ props.age }}</p> <p>邮箱:{{ props.email }}</p> </div> </template>

这里有几个关键点。第一,props 是只读的,子组件不能直接改props.name。第二,props 可以带类型校验和默认值,这不仅仅是规范,更是调试利器——传错类型时 Vue 会在控制台给出警告。第三,如果某个 props 不是必填,务必给默认值,否则渲染时可能出现undefined的报错。

3.2 emit 事件:子组件怎么“向上汇报”

数据是父传子,但子组件里发生的动作,父组件怎么知道?比如子组件里有个删除按钮,点了之后父组件需要发起删除请求,这就需要子组件向父组件发送事件。

<!-- 子组件 UserCard.vue --> <script setup> const emit = defineEmits(['delete', 'edit']) function handleDelete() { emit('delete', { id: 'user_123' }) } function handleEdit() { emit('edit', { id: 'user_123' }) } </script> <template> <div class="user-card"> <!-- 省略展示内容 --> <button @click="handleDelete">删除</button> <button @click="handleEdit">编辑</button> </div> </template>
<!-- 父组件 --> <script setup> import UserCard from './UserCard.vue' function onDelete(payload) { console.log('父组件收到删除事件,携带数据:', payload) // 在这里写真正的删除逻辑 } function onEdit(payload) { console.log('父组件收到编辑事件,携带数据:', payload) } </script> <template> <UserCard :name="user.name" :age="user.age" :email="user.email" @delete="onDelete" @edit="onEdit" /> </template>

这里最值得注意的一点是:子组件只负责“告知”,不负责“执行”。删除请求拉到哪、失败怎么提示,这些逻辑放在父组件里,因为删除动作影响的通常不只有子组件自身。这是一种角色分工:子组件做表现和交互,父组件做业务和状态管理。如果子组件把删除请求自己也发了,下一次另一个页面要用这个 UserCard,很可能因为接口地址不一样就废了。

3.3 为什么 Vue 坚持单向数据流

Vue 的 props 是单向数据流:数据从父组件流向子组件。子组件不能修改 props,只能通过向外发事件,让父组件去改。这个设计的背后逻辑是——如果子组件能随便改 props,那状态变化的位置就会变得不可追踪。

举个例子,三个子组件用了同一个user对象。如果每个子组件都能直接改user.name,谁改了?什么时候改的?完全没法查。单向数据流加上事件机制,保证了数据的“所有权”非常清晰:谁拥有数据,谁才有权修改。数据流只能从拥有者流向使用方,使用方想要变更,必须通过正式渠道“申请”。

这个设计还有一个实际好处:排查问题更容易。当页面上一个值不对,你可以顺藤摸瓜——值从父组件传下来的,那就先去父组件查;子组件改不了它,就排除了子组件篡改的可能。调试范围一下子缩小很多。

4. 跨层传递和兄弟通信:不绕远路也不写死

4.1 provide/inject:长辈给后辈发“资源包”

多层嵌套组件时,如果每一层都用 props 一层一层转发,会非常痛苦。爷爷传爸爸、爸爸传儿子、儿子传孙子……中间每一层都只是“过路”,却都得写一遍 props。这种情况适合用 provide 和 inject。

provide是在上层组件里提供数据,inject是在下层任意深度的组件里注入数据。

<!-- 祖组件 App.vue --> <script setup> import { provide, ref } from 'vue' import Header from './Header.vue' const theme = ref('dark') provide('theme', theme) </script>
<!-- 任意深层子组件 --> <script setup> import { inject } from 'vue' const theme = inject('theme', 'light') </script> <template> <div>当前主题:{{ theme }}</div> </template>

用 provide/inject 最大的好处是跨层直通,省掉中间层转发的样板代码。但也要小心:它让数据来源变得不再透明。任何一个深层的组件用了 inject,你都得往上看才知道数据是哪来的。所以我的建议是,provide/inject 只用来传递真正的“全局性上下文”,比如主题、当前登录用户信息、地区语言包这类几乎所有组件都可能用到的东西,不要用它来代替常规的 props 通信。

这里有一个 Vue 3 很容易踩的坑:provide传ref时,如果你在setup里直接写provide('theme', theme.value),你提供出去的是一个普通字符串,子组件拿到后不会响应式更新。正确做法是提供theme这个 ref 本身:

// 正确 provide('theme', theme) // 错误:丢失响应式 provide('theme', theme.value)

4.2 兄弟组件通信的几种常见处理方式

兄弟组件之间没有直接的父子关系,不能用 props 直接传,也不能靠 emit 直接接收。常见方案有三种。

第一种是“状态提升”:把两个兄弟都要用的数据放到共同的父组件里,父组件通过 props 传给两个兄弟,某个兄弟要修改时通过 emit 上报,父组件改了之后数据再通过 props 流回另一个兄弟。这是最符合 Vue 单向数据流体系的做法,也是我日常最常用的。

第二种是全局事件总线。Vue 3 里官方移除了$on/$off实例方法,但你可以引入一个轻量的 mitt 库来做。适合对响应性要求不高、只做一次性通知的场景。

第三种是引入 Pinia 这类状态管理库,把跨组件共享的状态放到集中式 store 里。当应用规模变大,多个组件都要读写同一份业务数据时,Pinia 是更清晰的选择。

我给个选择参考:偶尔一两个兄弟组件同步,用提升状态就够了;频繁多处组件都要同步,直接上 Pinia,不要用事件总线硬撑,越到后期越难维护。

4.3 组件通信最容易踩的坑

我见过最多的通信 bug 集中在两个地方。第一个是对象类型的 props 被误改。props 虽然是只读的,但如果你传的是对象,子组件里直接改props.user.name是能改成功的,因为对象是引用传递,这个修改会绕过 Vue 的警告直接污染父组件数据。这会让排查变得极其困难。所以约定俗成的规则是:props 一律只读,包括对象内部的属性。要修改,先复制一份再改。

第二个坑是 emit 事件名称大小写。Vue 3 中事件名如果在子组件里写的是驼峰myEvent,父组件模板里通常用@my-event监听,两种风格都能匹配,但你要是记混了就会监听不到。建议统一用 kebab-case:defineEmits(['my-event']),父组件写@my-event,一致性最强,也不会踩命名匹配的边界问题。

5. slot 与内容分发:组件不再是一个黑盒

5.1 默认插槽:一张嘴吃天下

如果你只靠 props 和 emit,你会发现组件只能展示自己写死的内容。但实际需求里,很多组件是“壳子”——布局是固定的,中间内容要由使用方自己填。这时候就要用插槽 slot。

默认插槽是最简单的插槽,子组件里留一个<slot />,父组件在子组件标签内写的任何内容都会放进去。

<!-- 子组件 Card.vue --> <template> <div class="card"> <div class="card__header">这是卡片标题</div> <div class="card__body"> <!-- 这里放父组件传进来的内容 --> <slot /> </div> </div> </template>
<!-- 父组件 --> <script setup> import Card from './Card.vue' </script> <template> <Card> <p>这一段内容会被渲染进卡片的 body 区域</p> <button>也可以是任意其他元素</button> </Card> </template>

插槽的出现让组件的复用层次提升了一级。不带插槽的组件是“完全按我的样式渲染”,带了插槽的组件是“我提供框架,你来填内容”,后者的适用面明显更广。

5.2 具名插槽与作用域插槽

当一个组件有多个需要填充的位置时,默认插槽就不够用了。比如一个布局组件,有 header、default、footer 三个区域,这时候要用具名插槽。

<!-- 子组件 Layout.vue --> <template> <div class="layout"> <header> <slot name="header" /> </header> <main> <slot /> </main> <footer> <slot name="footer" /> </footer> </div> </template>
<!-- 父组件 --> <template> <Layout> <template #header> <h1>标题栏</h1> </template> <p>主体内容,没有写 name 的 slot 默认就是 default</p> <template #footer> <p>底部说明</p> </template> </Layout> </template>

作用域插槽则更进一步:子组件可以在插槽向外传数据,让父组件根据这些数据来决定渲染什么内容。最典型的使用场景是表格列:子组件渲染了一个列表,把每一项的数据通过作用域插槽交给父组件自定义展示方式。

<!-- 子组件 DataTable.vue --> <script setup> const items = [ { id: 1, name: '苹果', price: 5 }, { id: 2, name: '香蕉', price: 3 } ] </script> <template> <div class="data-table"> <div v-for="item in items" :key="item.id" class="data-table__row"> <slot :item="item">{{ item.name }}</slot> </div> </div> </template>
<!-- 父组件 --> <script setup> import DataTable from './DataTable.vue' </script> <template> <DataTable> <template #default="{ item }"> <span>{{ item.name }} —— {{ item.price }} 元</span> </template> </DataTable> </template>

5.3 什么时候必须用插槽

我自己的判断标准是:如果一个组件的视觉结构固定,但内部某个区域的内容可能由使用方决定,那就必须留插槽。比如弹窗组件,你可以预置标题、底部取消确认按钮,但弹窗正文的内容只可能是“谁用谁知道”,所以正文必须用插槽。

反过来,如果某个区域的内容在所有使用场景下都是一样的,就别硬加插槽,不然父组件多了很多重复代码。设计插槽的本质是——把“变”的部分暴露出去,把“不变”的部分收拢在自己组件内部。这个边界找得越准,组件的可用性越高。

写 slot 时还有一个和 Vue 2 的差异要注意:Vue 2 里具名插槽用slot="header",Vue 3 里已经改成了#header或者v-slot:header。对 Vue 2 转 Vue 3 的人来说,这个是语法层面的语法糖,记清楚就顺了,搜“vue插槽”相关文章时留意一下版本差异就行。

6. 动态组件、异步组件与页面联想:组件也能“随叫随到”

6.1 is 属性和动态组件切换

动态组件指的是用<component :is="...">在运行期决定渲染哪个组件。典型场景是 Tab 标签页:

<script setup> import { ref, shallowRef } from 'vue' import HomePanel from './HomePanel.vue' import ListPanel from './ListPanel.vue' import ProfilePanel from './ProfilePanel.vue' const tabs = [ { name: '首页', comp: HomePanel }, { name: '列表', comp: ListPanel }, { name: '我的', comp: ProfilePanel } ] const currentTab = ref(tabs[0]) </script> <template> <div class="tabs"> <button v-for="tab in tabs" :key="tab.name" @click="currentTab = tab" > {{ tab.name }} </button> <component :is="currentTab.comp" /> </div> </template>

用一个currentTab.comp控制,比v-if写一堆分支要干净得多。这里一个性能小技巧是:如果动态切换的组件很多,用shallowRef而不是ref来存组件定义,可以避免深层响应化带来的性能开销。组件对象本来就只需要引用,不需要 Vue 去把它变成响应式对象。

6.2 异步组件与首屏加载优化

异步组件是用defineAsyncComponent定义的组件,它不会在首屏打包进主文件,而是在需要渲染时才动态加载。这里其实和“vue路由”里经常用的路由懒加载是同一种思路:按需加载,减少首屏体积。

import { defineAsyncComponent } from 'vue' const AsyncUserPanel = defineAsyncComponent(() => import('./UserPanel.vue'))

异步组件配上一个 loading 状态提示,体验会更好。比如用户点开一个管理后台的弹窗,里面的富文本编辑器和图表组件都比较重,异步加载能明显拉开首屏时间。你可以给异步组件加一个loadingComponent参数,在加载期间渲染一个轻量的占位组件。

6.3 KeepAlive 缓存组件状态

动态切换组件时,默认行为是组件被卸载、状态丢失。如果切走再切回来,Tab 页里用户已经填写的表单内容就没了,这种体验很糟糕。这时候用 KeepAlive 包一层:

<template> <KeepAlive> <component :is="currentTab.comp" /> </KeepAlive> </template>

KeepAlive 的意思是:组件切走时不销毁,而是缓存起来;切回来时直接复用之前的状态。它非常适合多 Tab 页面和列表到详情再返回列表的场景。要注意的是,KeepAlive 会改变组件的生命周期:组件第一次进入时走onMounted,之后重新进入只触发onActivated;离开时触发onDeactivated,而不是onUnmounted。如果组件里有定时器或 WebSocket,记得在onDeactivated里清理。

7. 把组件做得好用的心法:面向复用的封装设计

7.1 封装组件前先写使用文档

很多初学者封装组件是抄着一个需求就开始写。写完之后发现,换个页面用起来不对味,又要补 props、又要加事件,最后改得千疮百孔。

我现在的习惯是:动手之前先写“这个组件用起来会是什么样”,把期望的标签结构、props、事件、插槽先当成伪代码列出来。这一步理不顺,代码写再好也白搭。

比如我要封装一个选择器组件,我会先写:

<AsyncSelect :request-api="fetchUserList" v-model="selectedUserId" placeholder="请选择用户" />

然后再去想:request-api传什么?v-model绑的是单个值还是数组?要不要支持搜索?空数据时显示什么?这些设计问题在写使用文档的阶段成本最低,等到代码写一半再改,成本高很多。

组件粒度也是一个需要平衡的问题。拆得太细,文件爆炸,一个按钮都要抽个组件,维护成本反而高;拆得太粗,组件内部几百行,复用时带了一堆用不上的东西。我的判断标准是:一个组件如果超过两百行,先想想能不能拆;一个组件如果只在一个地方用一次,先不要急着抽,直接用,等第二个用得上的地方出现了再抽。

7.2 v-model 在组件封装里的高级用法

谈组件封装绕不开 v-model。v-model 本质上是 props 加事件的语法糖:父组件传value/modelValue,监听update:modelValue事件,子组件发起事件后父组件更新绑定的变量。

在自定义组件中使用 v-model,最典型的场景就是封装一个数字输入组件或日期选择组件:

<!-- 子组件 NumberInput.vue --> <script setup> const props = defineProps({ modelValue: { type: Number, default: 0 } }) const emit = defineEmits(['update:modelValue']) </script> <template> <input type="number" :value="props.modelValue" @input="emit('update:modelValue', Number($event.target.value))" /> </template>
<!-- 父组件 --> <script setup> import { ref } from 'vue' import NumberInput from './NumberInput.vue' const num = ref(0) </script> <template> <NumberInput v-model="num" /> </template>

Vue 3 里还支持多个 v-model,比如v-model:title和v-model:content同时绑定两个字段,这种写法在封装表单类组件时特别好用。它让父组件的调用代码变得简洁,也让子组件的对外接口更接近原生表单项的体验。

7.3 组件库:拿来主义与二次封装

实际业务中,大量组件可以直接用现成的第三方库,比如 Vant、Element Plus、Ant Design Vue。就拿热搜词里的“vant 做联级多选按照层级的组件”来说,Vant 自带级联选择器,你直接按文档配置列、绑定字段就行,没必要自己造轮子。

但组件库也不是永远能满足业务。常见做法是二次封装:拿库里的组件当底座,在外面包一层你自己的组件,统一项目内的 props 约定、默认样式和事件命名。这样以后组件库升级,你只需要改一个封装层,业务代码不需要大改。

做二次封装时最忌讳的是把底层组件的能力全都透传一遍——几十个 props 全列出来,没有任何意义。更好的思路是:想清楚你的团队在这个组件上实际用到哪几项能力,只暴露真正需要的那几个接口,内部再把参数透传给底层组件。接口少,反而好维护。团队协作时别人看一眼你的封装组件文档就能上手,而不是要去翻底层库的完整 API。

8. 实操中反复踩过的坑和我的个人经验

8.1 组件的 key 问题:为什么列表渲染必须加 key

v-for循环渲染组件时,给组件加:key是必须的,这几乎是我在团队 code review 里提得最多的一条。key 不仅是给 Vue 一个标识,更决定了组件是否被复用或重建。

如果不加 key,Vue 会通过“就地复用”策略来更新节点。列表里每一项的组件实例可能被错误地复用,导致组件内部状态错乱。最常见的情况:列表前插一条数据后,后面的组件的选中状态全跟着变了。

key 的最佳选择是数据里稳定唯一的 id,不要用数组下标 index。因为 index 在插入、删除时同样会错位。虽然大部分时候不加 key 也“看起来没事”,但那只是因为你列表项没有内部状态。一旦组件里有个复选框、有个 input,状态错乱的问题立刻冒出来。

8.2 组件命名与文件名大小写

组件命名有两个容易踩的坑。第一个是组件名必须用多个单词,避免和原生 HTML 标签冲突。Vue 官方风格指南里也要求这一点——如果你写一个<button>组件,它会在很多地方和原生 button 混淆,以后排查起来非常痛苦。

第二个是文件名和组件名不要用无意义名称。很多教程里爱用Demo.vue、Test1.vue,实际项目里千万别学。我建议文件名用 PascalCase,比如UserProfile.vue、DataTable.vue,组件内部通过defineOptions({ name: 'UserProfile' })显式声明姓名,方便调试时在 Vue DevTools 里直接定位到端到端组件名称。

8.3 用结构化设计梳理组件树,先画图再写码

我记得热搜词里有一条“组件图 uml”。这里想说:画组件图和写 UML 不是为了赶时髦,而是真的能省时间。动手写代码前,先在纸上把组件树画出来,标清楚哪个组件持有哪份数据、哪些组件之间需要通信、哪些数据需要提升到父级。

画图的过程中你会发现很多问题:这个表单里三个下拉框的数据源其实是一样的,完全可以提升到同一个父组件统一管理;那个弹窗的关闭逻辑居然拆到了六个组件里。这些问题在代码阶段发现会很难受,但要是在设计阶段,改动只是纸面上的几笔。

8.4 一个实用的自查清单

最后分享一个我在写完组件后反复使用的自查清单,也算是我这些年在组件化实践里沉淀出的核心经验:

  • 组件有没有超过两百行?超过的话考虑拆。
  • 组件接口(props、emit、slot)是不是最小必要集?有没有暴露多余的能力。
  • 所有传给子组件的对象 props,子组件内部有没有改动它的属性?
  • 组件名是否至少两个单词?和 HTML 原生标签是否冲突?
  • 列表渲染有没有加稳定的 key?
  • 状态是应该放在组件内部,还是应该提升到父组件、放进 Pinia?
  • 异步加载的重组件,有没有配 loading 占位?
  • KeepAlive 缓存组件里,有没有需要清理的定时器或事件监听?

你可能会发现,上面这些条目大多不是怎么“写代码”,而是怎么“做设计”。组件化从来不只是语法层面的问题,它更是一种组织代码和思路的方式。把组件拆对、把通信理顺、把边界划清楚,Vue 开发的一半功力其实就已经到位了。剩下的,就是靠实际项目里一遍遍打磨手感,踩过的坑多了,自然就会在动手之前多想一步。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询