七、Vue组件

📅 2026/8/14 15:12:27
七、Vue组件
一、组件定义你可以把“组件”想象成乐高积木。在 Vue 中我们不再是一笔一划地写一整个大网页而是把网页拆分成一个个独立、可复用的小积木比如导航栏、按钮、文章列表然后像搭积木一样把它们拼起来。1. 组件定义创建积木在实际开发中我们强烈推荐使用单文件组件SFC也就是以.vue结尾的文件。它把一个组件的“骨架HTML”、“肌肉JS”和“衣服CSS”都写在一个文件里非常清晰。实际开发例子假设我们要做一个“博客文章卡片”组件我们可以创建一个BlogPost.vue文件!-- BlogPost.vue -- script setup // 这里写组件的逻辑JS // 比如定义一个标题属性准备接收父组件传过来的数据 import { defineProps } from vue const props defineProps([title]) /script template !-- 这里写组件的结构HTML -- div classblog-card h3{{ title }}/h3 p这是一篇关于 Vue 3 基础的文章.../p /div /template style scoped /* 这里写组件的样式CSS */ .blog-card { border: 1px solid #ccc; padding: 10px; } /style二、组件注册在 Vue 应用中组件需要先注册才能使用。注册方式主要有两种局部注册和全局注册。下表清晰对比了它们的区别对比维度局部注册 (Local Registration)全局注册 (Global Registration)使用范围仅限当前导入它的父组件内部使用。整个项目的任何组件中都可以直接使用。代码写法在组件内import导入script setup下自动注册。在main.js中使用app.component()注册。打包体积支持 Tree-shaking按需打包。没用到的组件不会打包项目体积更小。不支持 Tree-shaking。无论是否使用都会被打包进最终文件可能导致体积变大。依赖关系依赖关系非常清晰看代码就知道当前页面用了哪些子组件。依赖关系隐藏。在页面代码里看不到import新人接手时可能不知道组件从哪来的。命名冲突冲突概率极低因为作用域被限制在当前组件内。如果全局注册的组件名字重复后注册的会覆盖前面的容易引发冲突。适用场景业务组件、页面级组件、只在少数地方用到的组件占 90% 以上场景。基础 UI 组件如自定义 Button、Input、全局弹窗、全局提示框等占 10% 以下场景。方式一局部注册最常用、最推荐局部注册的意思是这个组件我只在当前的父组件里用其他地方不用。假设我们在首页Home.vue要用到BlogPost组件。如果你使用的是 Vue 3 的script setup语法糖只要导入import就可以直接使用了连显式注册的代码都省了!-- Home.vue -- script setup // 1. 只需要导入组件Vue 3 会自动完成局部注册 import BlogPost from ./components/BlogPost.vue /script template div classhome-page !-- 2. 直接在模板中使用 -- BlogPost titleVue3 入门指南 / /div /template如果你没有使用script setup而是传统的写法则需要显式地把它放到components选项里importBlogPostfrom./components/BlogPost.vueexportdefault{components:{BlogPost// 相当于 BlogPost: BlogPost}}方式二全局注册全局注册的意思是这个组件太常用了比如自定义按钮、弹窗我希望在整个项目的任何地方都能直接用不用每次都去导入。我们需要在项目入口文件main.js或main.ts中进行配置// main.jsimport{createApp}fromvueimportAppfrom./App.vue// 1. 导入你要全局注册的组件importBlogPostfrom./components/BlogPost.vueconstappcreateApp(App)// 2. 使用 app.component() 方法进行全局注册app.component(BlogPost,BlogPost)app.mount(#app)全局注册后的好处配置好之后无论你是在Home.vue、About.vue还是其他任何组件里都不需要再写import可以直接在模板里使用!-- 任意页面比如 About.vue -- template div !-- 直接使用无需导入 -- BlogPost title关于我的博客 / /div /template三、组件使用注册完成后我们就可以在父组件的模板里像写普通 HTML 标签一样使用它了。假设我们在首页Home.vue要用到刚才写的博客卡片以下是局部注册的完整写法!-- Home.vue (父组件) -- script setup // 1. 导入子组件就像从工具箱里拿出这块积木 import BlogPost from ./components/BlogPost.vue /script template div classhome-page h1我的博客首页/h1 !-- 2. 使用子组件像写HTML标签一样使用 -- !-- 我们可以给它传不同的标题 -- BlogPost titleVue3 入门指南 / BlogPost title前端学习路线 / /div /template四、模版引用1. 什么是“模板引用”Vue 的核心思想是“声明式渲染”——你只管在代码里写数据Vue 会自动帮你把数据渲染成 HTML你不需要也不应该像写 jQuery 那样去手动操作 DOM 节点。但是总有那么一些特殊情况你必须直接拿到真实的 HTML 标签。比如页面加载完后让输入框自动获取焦点focus。在某个div上初始化一个第三方的图表库比如 ECharts。这时候我们就需要用到ref属性给 DOM 元素打个“标签”然后在 JS 里通过这个标签把元素揪出来。2. 以前是怎么做的Vue 3.4 及以前的痛点在 Vue 3.5 之前我们要获取 DOM通常是这样写的script setup import { ref, onMounted } from vue // 变量名必须和模板里的 ref 名字一模一样 const myInput ref(null) onMounted(() { // 必须等组件挂载后才能拿到元素 myInput.value.focus() }) /script template !-- 这里的名字必须是 myInput -- input refmyInput / /template痛点在哪强耦合模板里的refmyInput和 JS 里的变量名myInput必须完全一致。如果模板里写成了my-inputJS 里就会报错新手经常在这里踩坑。类型推断差在 TypeScript 中ref(null)的类型默认是null你没法直接点出.focus()必须手动加泛型HTMLInputElement | null非常麻烦。3. 终极必杀技Vue 3.5 的 useTemplateRef()为了解决上面的痛点Vue 3.5 推出了useTemplateRef()它就像原生 JS 的document.getElementById()一样丝滑。实战场景页面加载后让输入框自动获取焦点script setup import { useTemplateRef, onMounted } from vue // 1. 变量名可以随便起比如叫 inputRef // 2. 只要保证括号里的字符串和模板里的 ref 名字一致即可 const inputRef useTemplateRef(my-input) onMounted(() { // 3. 完美支持 TypeScript 类型推断 // IDE 会自动知道 inputRef.value 是一个 HTMLInputElement // 直接点出 .focus() 毫无压力 inputRef.value?.focus() }) /script template !-- 这里的 ref 值必须和上面的字符串匹配 -- input refmy-input placeholder我会自动获取焦点 / /template核心优势解耦命名JS 变量名和模板ref属性名不再强制绑定通过字符串参数关联逻辑更清晰。自动类型推断配合 TypeScript 和 IDE它能根据模板里的input标签自动推断出inputRef.value是HTMLInputElement类型智能提示极其好用。4. 在 v-for 列表中使用在实际开发中我们经常要渲染一个列表并想拿到列表中所有的 DOM 元素。useTemplateRef()同样完美支持script setup import { ref, useTemplateRef, onMounted } from vue const list ref([苹果, 香蕉, 橘子]) // 获取列表项的引用 const itemRefs useTemplateRef(list-items) onMounted(() { // itemRefs.value 是一个包含所有 li DOM 元素的数组 console.log(itemRefs.value) }) /script template ul !-- 注意在 v-for 中所有的 ref 名字都一样 -- li v-foritem in list reflist-items {{ item }} /li /ul /template5. 避坑指南时机问题模板引用只有在组件挂载后onMounted生命周期才有值。如果在初始化阶段就去访问拿到的会是null。条件渲染v-if如果元素被v-if隐藏或卸载了它的引用值也会变成null。所以在使用时建议加上可选链?.或进行判空处理。版本要求useTemplateRef()是Vue 3.5才引入的新 API。如果你的项目还在用 3.4 或更早的版本请继续使用传统的const myInput ref(null)写法。五、组件的 v-model在原生 HTML 中我们用input v-modelmsg可以轻松实现双向绑定。但在实际开发中我们经常需要封装自己的组件比如一个好看的自定义输入框、一个日期选择器怎么让自定义组件也能像原生标签一样支持v-model呢1. 核心原理v-model 其实是个“语法糖”首先你要明白v-model并不是什么黑魔法它本质上就是**“属性传值Props 事件通知Emits”**的简写。在 Vue 3 中当你在父组件写MyInput v-modelmessage /时Vue 在底层会自动帮你翻译成MyInput :modelValuemessage update:modelValuemessage $event /也就是说要实现自定义组件的v-model子组件只需要做两件事接收一个叫modelValue的 prop。当内部数据变化时触发一个叫update:modelValue的事件。2. 实战演练Vue 3.4 的“终极必杀技” defineModel如果你使用的是Vue 3.4 及以上版本官方提供了一个超级方便的宏defineModel()。它把上面那两步直接帮你省了实际开发例子封装一个带样式的自定义输入框CustomInput.vue!-- CustomInput.vue (子组件) -- script setup // 仅仅这一行就自动声明了 modelValue 属性并绑定了 update:modelValue 事件 // 它返回一个类似 ref 的响应式变量 const model defineModel() /script template !-- 直接把这个 model 绑定给原生的 input -- input classcustom-input v-modelmodel placeholder请输入内容... / /template在父组件中使用!-- Parent.vue -- script setup import { ref } from vue import CustomInput from ./CustomInput.vue const myText ref() /script template !-- 就像用原生 input 一样丝滑 -- CustomInput v-modelmyText / p你输入了{{ myText }}/p /template3. 进阶用法一个组件绑定多个 v-model在实际业务中比如做一个“用户信息编辑”弹窗我们可能需要同时双向绑定“用户名”和“年龄”。Vue 3 完美支持这一点子组件UserForm.vuescript setup // 通过传参定义多个双向绑定的属性 const firstName defineModel(firstName) const lastName defineModel(lastName) /script template input v-modelfirstName placeholder名 / input v-modellastName placeholder姓 / /template父组件使用template !-- 用冒号指定具体的绑定参数 -- UserForm v-model:first-nameuser.first v-model:last-nameuser.last / /template4. 避坑指南实战经验坑 1千万不要给defineModel乱设默认值如果你在子组件写了const model defineModel({ default: 默认值 })但父组件没有传值会导致父子组件数据不同步父组件是undefined子组件是默认值。正确做法默认值应该在父组件定义比如const myText ref(默认值)。坑 2如果你用的是 Vue 3.4 之前的版本怎么办那就只能老老实实写传统的 Props 和 Emitsscript setup // 1. 声明属性 const props defineProps([modelValue]) // 2. 声明事件 const emit defineEmits([update:modelValue]) // 3. 手动处理输入 const handleInput (e) { emit(update:modelValue, e.target.value) } /script template input :valuemodelValue inputhandleInput / /template六、透传 Attributes1. 什么是“透传 Attributes”简单来说就是父组件传给子组件的某些属性子组件自己并没有用props接收而是直接“透传”继承给了子组件内部的 HTML 标签。最常见的透传属性包括class、style、id、data-*属性以及事件监听器如click。当一个组件以单个元素为根作渲染时透传的 attribute 会自动被添加到根元素上。举例来说假如我们有一个MyButton组件它的模板长这样!-- MyButton 的模板 -- buttonClick Me/button一个父组件使用了这个组件并且传入了classMyButton classlarge /最后渲染出的 DOM 结果是buttonclasslargeClick Me/button这里MyButton并没有将class声明为一个它所接受的 prop所以class被视作透传 attribute自动透传到了MyButton的根元素上。2. 默认行为单根节点 vs 多根节点Vue 3 对透传的处理非常智能主要分两种情况假设你封装了一个自定义的按钮组件MyButton /。父组件在使用时给它加了一个class和一个点击事件MyButton classprimary-btn clickhandleSubmit提交/MyButton情况一单根节点组件自动透传如果你的组件只有一个根标签Vue 会自动把所有透传属性加在这个根标签上。!-- MyButton.vue -- template !-- class 和 click 会自动跑到这个 button 上 -- button提交/button /template情况二多根节点组件不会自动透传需手动指定如果你的组件有多个根标签Vue 就“懵”了不知道该把属性加给谁。这时候如果不手动处理控制台会抛出警告。你需要用v-bind$attrs明确告诉 Vue 加给谁!-- CustomLayout.vue -- template header头部/header !-- 明确把透传属性绑定到 main 标签上 -- main v-bind$attrs内容区/main footer底部/footer /template3. “劫持”并自定义透传在实际开发中我们经常会遇到这样的需求给MyButton /传了classprimary-btn但我希望这个class不要加在最外层的div上而是加在内部的button上。这时候我们需要禁用自动透传然后手动控制。实战例子给按钮套个外壳!-- MyButton.vue -- script setup // 核心禁用默认的自动透传行为 defineOptions({ inheritAttrs: false }) /script template !-- 外层 div 不会继承父组件传来的 class -- div classbtn-wrapper !-- 通过 v-bind$attrs把父组件传来的 class、click 等全部精准绑定到内部 button 上 -- button classbtn v-bind$attrs点击我/button /div /template4. 在 JS 逻辑中访问透传属性有时候我们不仅要在模板里用透传属性还需要在script setup的 JS 逻辑里拿到它们。这时候可以使用useAttrs()这个组合式 APIscript setup import { useAttrs } from vue // 获取所有透传过来的属性 const attrs useAttrs() const handleClick () { console.log(透传过来的属性:, attrs) // 比如手动触发父组件传过来的 click 事件 // 注意事件监听器在 attrs 中是以 onXxx 的形式存在的 if (attrs.onClick) { attrs.onClick() } } /script5. 避坑指南大小写保留透传属性在 JS 中保留了原始大小写。比如父组件传了foo-bar123你在 JS 里必须用attrs[foo-bar]来访问。非响应式useAttrs()返回的对象不是响应式的你不能用watch去监听它的变化。如果需要根据透传属性做响应式处理请老老实实使用props。性能考虑虽然透传很好用但不要滥用。如果一个组件透传了极其庞大的属性对象可能会带来微小的性能开销。七、异步组件1. 什么是异步组件想象一下你要开一家大型餐厅同步组件默认就像在客人进门之前你必须把所有厨房设备、所有食材、所有桌椅板凳一次性全部搬进店里。结果就是客人要在门外等很久才能进门。异步组件按需加载客人进门时店里只有基础的桌椅和菜单首屏内容。当客人点了“法式大餐”时你再去后厨把做这道菜专用的重型烤箱搬进来。在 Vue 中异步组件就是把那些不常用的、体积很大的组件比如复杂的图表、富文本编辑器单独打包只有当页面真正需要用到它时才去服务器下载它。这样能大大加快网页的首次打开速度2. 实际开发怎么写Vue 3 标准姿势在 Vue 3 中我们使用defineAsyncComponent这个 API 来定义异步组件。实战场景假设我们有一个后台管理系统里面有一个“数据大屏”页面用了一个非常庞大的 ECharts 图表组件HeavyChart.vue。这个组件只有在用户点击“查看大屏”时才需要展示。!-- Dashboard.vue -- script setup import { defineAsyncComponent } from vue // 1. 定义异步组件告诉 Vue 这个组件需要异步加载 const HeavyChart defineAsyncComponent(() import(./components/HeavyChart.vue) ) /script template h1后台管理面板/h1 !-- 2. 像普通组件一样使用它 -- HeavyChart / /templateimport()是 ES6 的动态导入语法Vite 或 Webpack 打包时会自动把HeavyChart.vue切分成一个单独的小文件chunk。3. 处理“加载中”和“加载失败”网络请求是需要时间的。如果用户的网速比较慢图表还没下载完页面总不能是一片空白吧我们需要给用户一个“Loading加载中”的提示。defineAsyncComponent提供了一个非常强大的高级配置对象script setup import { defineAsyncComponent } from vue import LoadingSpinner from ./LoadingSpinner.vue // 一个旋转的 loading 动画 import ErrorFallback from ./ErrorFallback.vue // 加载失败时的提示组件 const HeavyChart defineAsyncComponent({ // 核心加载函数 loader: () import(./components/HeavyChart.vue), // 正在加载时显示的组件 loadingComponent: LoadingSpinner, // 延迟显示 loading 的时间默认 200ms // 为什么要延迟因为如果网速极快0.01秒就加载完了 // 闪一下 loading 再显示图表反而会让用户觉得“闪屏”体验不好。 delay: 200, // 如果加载失败显示的组件 errorComponent: ErrorFallback, // 超时时间如果超过 5 秒还没加载出来就显示 errorComponent timeout: 5000 }) /script4. 最常见的应用场景路由懒加载虽然这严格来说是 Vue Router 的功能但它和异步组件的原理完全一样也是日常开发中用得最多的地方假设你的应用有很多页面千万不要一次性把它们全加载了// router/index.jsconstroutes[{path:/home,// 访问首页时才去加载 Home.vuecomponent:()import(/views/Home.vue)},{path:/settings,// 访问设置页时才去加载 Settings.vuecomponent:()import(/views/Settings.vue)}]5. 实战避坑指南首屏核心组件绝对不要异步化比如网页最顶部的导航栏Header、底部的版权信息。这些是用户一打开网页就要看到的东西如果把它们搞成异步组件用户会看到导航栏“闪”一下才出来体验极差。配合Suspense使用高级玩法如果你的异步组件内部又嵌套了异步组件或者组件内部有await请求数据你可以用 Vue 3 的Suspense组件在最外层包裹统一处理整个组件树的加载状态避免页面上出现好几个 loading 圈圈。不要滥用只有那些体积大且非首屏必需的组件才值得做成异步组件。如果一个按钮组件只有 1KB你非要搞异步加载网络请求本身的开销反而比直接加载代码还要大。八、component1. 什么是component想象一下你手机里的多标签页或者微信的底部导航栏。当你点击首页时页面显示首页组件点击我的时页面无缝切换成个人中心组件。如果在以前你可能会用v-if写一堆判断Home v-ifactiveTab home / Profile v-else-ifactiveTab profile / Settings v-else-ifactiveTab settings /这种方式虽然能工作但当标签页越来越多时模板会变得非常臃肿而且每新增一个标签页都需要手动修改模板。Vue 提供了一个更优雅的解决方案——内置的component组件它通过:is属性动态决定当前渲染哪个组件让组件切换变得像切换变量一样简单。2. 动态标签页Tab 切换实际开发场景我们要做一个简单的后台管理面板左侧有菜单点击不同的菜单右侧显示不同的内容。script setup import { ref, shallowRef } from vue import Home from ./Home.vue import Settings from ./Settings.vue import User from ./User.vue // 1. 记录当前激活的标签名 const activeTab ref(home) // 2. 建立一个映射表标签名 - 组件 // 注意这里推荐用 shallowRef因为组件对象不需要深度响应式性能更好 const componentMap shallowRef({ home: Home, settings: Settings, user: User }) /script template !-- 左侧菜单按钮 -- div classmenu button clickactiveTab home首页/button button clickactiveTab settings设置/button button clickactiveTab user用户/button /div !-- 右侧动态内容区 -- div classcontent !-- 核心魔法在这里:is 绑定的是一个组件对象 -- component :iscomponentMap[activeTab] / /div /template核心原理当activeTab改变时componentMap[activeTab]就会指向不同的组件对象component :is...就会自动销毁旧组件渲染新组件整个过程无需手写v-if判断代码简洁且易于扩展。3. 动态组件 KeepAlive性能优化必杀技在实际开发中用component切换组件时有一个痛点每次切换旧组件都会被销毁新组件都会被重新创建。比如用户在首页填了一个很长的表单刚填了一半不小心点到了设置再点回首页时表单数据全没了这时候我们需要请出 Vue 的另一个内置组件KeepAlive来配合component把切换过的组件缓存起来template !-- 用 KeepAlive 包裹 component -- KeepAlive component :iscomponentMap[activeTab] / /KeepAlive /template加上KeepAlive后组件在第一次被切换走时不会被销毁而是被冻结保存在内存里。下次再切回来瞬间恢复之前的状态包括表单数据、滚动条位置等体验极其丝滑此外KeepAlive还支持include和exclude属性让你可以精确控制哪些组件需要缓存、哪些不需要template !-- 只缓存 Home 和 User 组件Settings 组件每次都会重新创建 -- KeepAlive :include[Home, User] component :iscomponentMap[activeTab] / /KeepAlive /template4. 避坑指南:is可以传字符串吗可以但不推荐。在 Vue 3 中:is my-component 会先去解析全局注册的组件。为了代码的清晰和避免全局污染强烈建议直接传入组件对象如上面的componentMap[activeTab]。性能优化小技巧如果你动态渲染的组件非常庞大记得用shallowRef而不是ref来存储组件对象。因为组件本身的结构是不需要被 Vue 深度监听的这样可以减少不必要的性能开销。生命周期触发被KeepAlive缓存的组件不会触发常规的onMounted和onUnmounted。取而代之的是onActivated进入缓存和onDeactivated离开缓存。如果你需要在每次切回页面时刷新数据记得用这两个钩子