ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Vue3组件开发全链路实战:从环境搭建到疑难排查

Vue3组件开发全链路实战:从环境搭建到疑难排查 这几年我一直在项目里用 Vue3 做组件开发从最初的 Vite 搭工程、写几个弹窗组件到后来负责整个后台管理系统的架构、统一封装业务组件库、再往 Electron、可视化大屏这些方向延伸踩过的坑确实不少。网上关于 Vue3 的教程铺天盖地但大部分要么停留在官方文档复读要么只讲语法不讲为什么这么做。这篇内容我打算换个思路不按官方文档的顺序讲 API而是从组件开发的完整链路出发把环境搭建、组件设计、通信方案、第三方集成、疑难排查这些环节串起来结合我在 HoRain云 相关项目里的实际落地经验把每个环节背后的设计逻辑和常见坑位说明白。不管你是刚准备上手 Vue3 的新人还是已经从 Vue2 迁移过来、正在被各种工程化问题折磨的开发者这篇内容都值得花几分钟读完能帮你少走不少弯路。1. 从零搭建 Vue3 开发环境别在第一步就掉坑1.1 为什么我坚持用 Vite 而不用 Vue CLI每次有人问我 Vue3 开发环境怎么搭我第一句话都是不要再用vue create了。不是说 Vue CLI 不能用而是 Vue3 时代 Vite 才是更合理的选择。Vite 基于原生 ES Module开发服务器启动时不用像 Webpack 那样把所有模块打包一遍冷启动速度基本在毫秒级热更新也是按需编译改一个组件保存后页面几乎是瞬时就刷新了。我在一个上百个页面的后台管理系统里实测过Webpack 开发模式启动需要 30 多秒Vite 只需要 3 秒左右这个差距在日常开发中非常影响幸福指数。Vite 的配置也很简单项目根目录下的vite.config.ts就是一个 Node 环境的配置文件可以用vitejs/plugin-vue来处理.vue单文件组件用vitejs/plugin-legacy做低版本浏览器的兼容转换。需要注意的是Vite 默认只处理现代浏览器如果项目需要兼容到 Chrome 60 以下的旧版本必须引入vitejs/plugin-legacy插件否则打包出来的代码包含可选链、空值合并这些新语法旧浏览器会直接白屏。# 使用 create-vue 创建项目这是目前官方推荐的方式 npm create vuelatest执行这个命令后脚手架会问你几个问题比如是否使用 TypeScript、是否引入 Vue Router、是否引入 Pinia、是否开启 ESLint 和 Prettier 等。我的建议是如果项目不是特别小TypeScript 一定勾上Vue3 的defineProps和defineEmits配合 TS 泛型之后组件之间的数据协议会清晰非常多路由和状态管理直接勾上后面省得自己手动集成。1.2 版本选型里的门道Node、Vite、Vue 的版本匹配版本问题是我见过新手踩坑最多的地方。Vite 5 要求 Node 版本 18 以上Vue 3.4 以后对编译器和类型推导也有一些新要求如果你在 Windows 7 这种老系统上开发Node 版本可能根本装不到 18那就老老实实用 Vite 4 或者 3不要硬上最新版。还有人在网上搜到vue3 vite5 总是报 definecomponent is not defined这类问题多半不是因为 defineComponent 真的不存在而是因为装了某个自动导入插件后在script setup里混用了全局注册和局部引用的方式或者某个版本插件和 Vite 版本不匹配导致编译产物丢失了导入语句。我在 HoRain云 的一个中台项目里就遇到过类似情况刚开始把 Vite 从 4 升到 5结果页面里大量组件报错说defineComponent未定义排查了半天发现是unplugin-auto-import版本太老生成的自动导入文件里没有包含 Vue 的运行时 API升级插件版本后问题直接消失。所以遇到 Vite 相关报错第一步永远是把所有构建相关插件升级到和 Vite 匹配的版本然后再去查业务代码。1.3 我常用的基础工程目录结构不管项目大小我习惯在初始化之后立刻把目录结构理清楚。src下面分为api、assets、components、composables、directives、layouts、router、stores、styles、types、utils、views这几块。components里按base和business再分一层base放按钮、弹窗、表格这种通用组件business放和业务强绑定的组件。composables放组合式函数比如useTable、useForm、usePagination这是 Vue3 组件开发里非常重要的一个组织方式后面我会专门讲。组件命名上我也踩过坑。Vue3 的 SFC 组件建议统一用 PascalCase文件名用PascalCase或者kebab-case都行但在一个项目里必须统一。如果你用了unplugin-vue-components做组件自动按需注册它会以文件名来推断组件名这时候如果组件文件比较乱很容易出现组件名冲突。2. Vue3 组件开发的核心范式组合式 API 到底改变了什么2.1 为什么说script setup是组件开发的默认选项Vue3 最让老 Vue2 开发者不习惯的就是组合式 API 的引入。在 Vue2 里我们写组件就是把 data、computed、methods、watch、生命周期全部分开一个功能相关的代码散落在各个 option 里。比如一个搜索功能data 里要定义关键词、loading、列表数据、分页参数methods 里要写搜索方法、重置方法、翻页方法computed 里要写过滤后的数据当一个组件复杂到几百行的时候你经常需要在一个文件里上下跳来跳去心智负担特别重。组合式 API 的核心思想是把同一个业务关注点的代码放在一起更准确地说它是让逻辑复用和逻辑内聚变成了组件开发的第一优先级。script setup是组合式 API 的语法糖它让我们可以少写很多样板代码。在script setup里顶层变量、函数、import 进来的内容都可以直接在模板里使用不需要返回对象。更重要的是defineProps、defineEmits、defineExpose这些都是编译宏不需要 import 就能直接用类型推导也很好。script setup langts import { ref, computed } from vue interface Props { title: string count: number } const props definePropsProps() const emit defineEmits{ (e: update:count, value: number): void (e: click-item, id: number): void }() const doubleCount computed(() props.count * 2) function handleClick() { emit(update:count, props.count 1) emit(click-item, 1) } /script template div classmy-component clickhandleClick h3{{ title }}/h3 p{{ doubleCount }}/p /div /template2.2 defineProps 赋给 data 后为什么失去响应性热搜词里有一个很典型的问题vue3 props赋值给data。很多从 Vue2 转过来的同学习惯在watch或者created里把 props 赋值给 data这样做在 Vue2 里还能正常响应但在 Vue3 里直接赋值会导致新数据源失去响应性。原因其实不复杂props 本身是响应式对象你把它的某个属性取出来赋给一个普通变量这个变量自然就和原来的响应式链路断开了。再加上 Vue3 的 ref 和 reactive 都是基于 Proxy 实现的普通的赋值操作不会建立依赖收集。遇到这个问题正确的做法是优先考虑computed。如果你的新数据只是对 props 做派生比如把字符串首字母大写、把数组排序那computed就是最合适的地方。只有当新数据需要保留自己的可变状态比如一个基于 props 初始值但之后可以被用户修改的临时草稿时才需要用refwatch来手动同步而且要设置immediate: true。// 推荐方式computed 派生 const displayTitle computed(() 标题${props.title}) // 需要可修改的副本时ref watch 同步 const draft ref(props.title) watch(() props.title, (val) { draft.value val }, { immediate: true })这里还有个心得尽量避免在组件内部对 props 对象做深拷贝再修改否则会带来大量无意义的性能和内存开销。合理划分受控组件和非受控组件的边界比纠结 props 怎么赋值更重要。2.3 v-model 在组件上的新玩法也在说 Vue2 和 Vue3 的区别Vue3 的 v-model 在组件上的实现和 Vue2 相比变化很大。Vue2 的.sync修饰符在 Vue3 被移除了统一合并到 v-model 的机制里而且一个组件上可以写多个 v-model分别绑定不同的属性。默认的modelValue对应update:modelValue事件自定义的比如v-model:title就对应update:title事件。这让一个组件可以同时暴露多个双向绑定数据源不需要再手动写valueinput那套模式。script setup langts defineProps{ modelValue: string visible: boolean }() const emit defineEmits{ (e: update:modelValue, value: string): void (e: update:visible, value: boolean): void }() /script这种设计在封装表单类组件时特别香。比如一个筛选弹窗组件父组件用v-model:keyword绑定关键词、v-model:visible控制弹窗显示代码可读性和维护性都远胜 Vue2 时代一堆$emit手动拼装。我在 HoRain云 的运营后台里就用这种方式重构了一批筛选组件改动量不大但父组件的模板清爽了非常多。2.4 defineExpose父组件如何拿到子组件内部方法Vue2 里ref拿到子组件实例后可以任意访问子组件的任何数据和方法Vue3 因为script setup默认是关闭的子组件内部的数据和函数默认不对外暴露必须用defineExpose明确标注出来。这个变化让组件边界变得更加清晰避免父组件绕过 props/emit 直接操作子组件内部在多人协作团队里非常有必要。不过也要注意defineExpose暴露出去的是普通对象响应式数据会经过脱壳处理所以父组件拿到 ref 对象后访问属性时无法再触发细粒度的依赖更新必须调用它暴露的方法来更新状态这是设计上的预期不是 bug。3. 组件通信与状态管理数据流怎么设计才不容易乱3.1 props/emit 是骨架provide/inject 是血管组件通信选型的核心原则是优先用 props 向下传数据、用 emit 向上发事件只有跨多层级传值的时候才考虑provide和inject。很多初学者喜欢直接使用全局事件总线或者 Pinia 来通信省事但一个项目里如果到处都是全局状态调试起来会非常痛苦——你根本不知道某个数据是被谁改的。provide和inject是 Vue3 中非常好用的跨层级通信方案它不需要一层层传 props也不用引入外部状态库。比如一个TablePage组件内部又嵌套了工具栏、筛选表单、分页器如果工具栏要触发刷新列表事件你不需要一层层$emit透传直接在根组件provide一个refresh方法子组件任意需要的地方inject进来就行。// 在父级组件中 import { provide } from vue provide(tableRefesh, () { /* 刷新逻辑 */ }) // 在任意层级子组件中 const tableRefesh inject(val?: unknown) void(tableRefesh)需要注意provide和inject虽然方便但它会让数据流变得不那么显式。我建议在项目里只对组件内部实现细节使用 provide/inject比如ThemeProvider向下分发主题变量、ConfigProvider分发全局配置如果是真正需要跨页面的业务数据还是应该放进 Pinia。3.2 Pinia 为什么能替代 Vuex以及组件里该怎么用Vuex 时代那套 state、getters、mutations、actions 写起来确实繁琐尤其是 mutations 和 actions 的区分很多团队到最后都会出现直接在 action 里改 state的违规操作。Pinia 直接抛弃了 mutationsstate 可以同步改也可以异步改这让代码量直接减少一半以上。除此之外Pinia 对 TypeScript 的支持非常天然store 定义好后所有 state 和 getters 的类型都能自动推导这在大型团队里能省掉大量手写类型的工作。在组件里使用 Pinia 有一个值得养成的习惯不要在整个页面组件里到处storeToRefsstore.xxx混用。比较推荐的做法是在组件开头统一解构需要的数据和动作页面模板里尽量只出现store.xxx的数据而动作统一在事件回调里调用。比如import { storeToRefs } from pinia import { useUserStore } from /stores/user const userStore useUserStore() const { name, roles } storeToRefs(userStore) function logout() { userStore.logout() }storeToRefs的目的是让 Pinia 中的 state 变成 ref这样在模板里使用时还能保留响应性。如果不加这一步直接解构出来的name就是一个普通字符串模板里第一次渲染没问题后续更新全丢。这个细节是 Pinia 最容易被忽略的坑。3.3 组件状态粒度全局、页面局部、还是组件私有设计组件系统时我经常提醒团队不是所有状态都需要放到全局全局状态用多了组件复用性会断崖式下降。一个合理的状态分层大概是这样的全局状态用户信息、权限、主题、国际化、全局通知。这类状态几乎每个页面都要用或者会被多个互不相干的组件同时修改。页面状态列表页的筛选条件、查询参数、分页信息。这类状态应该局限在当前路由组件内部用ref管理不要让子组件直接去改父组件的状态。组件私有状态弹窗开关、展开收起、输入框的临时值、加载中。这类状态应该完全封装在组件内部对外只暴露必要的 props 和 events。我见过不少团队把画面上的每个loading都放进 Pinia最后 debug 的时候根本分不清这个 loading 是哪个接口带来的无一例外都在坑里挣扎。记住一个原则状态离使用者越近越好只有确实需要跨作用域共享的数据才往上层提升。4. 第三方生态集成这五个高频场景一定要会4.1 ECharts 封装成组件和组合式函数别再每次复制粘贴Vue3 项目里做可视化ECharts 依然是绕不开的选择。但很多人直接把init、setOption、resize、dispose塞进每个页面组件里一个项目下来少说几十份重复代码而且经常忘了在组件卸载时dispose内存泄漏和页面卡顿接踵而至。用封装好的useECharts组合式函数和BaseChart组件能让这件事干净很多。import * as echarts from echarts import { onMounted, onBeforeUnmount, shallowRef } from vue export function useECharts(domRef: RefHTMLElement | undefined) { const chart shallowRefecharts.ECharts() function initChart() { if (!domRef.value) return chart.value echarts.init(domRef.value) } function setOption(option: echarts.EChartsOption) { chart.value?.setOption(option) } function resize() { chart.value?.resize() } onMounted(initChart) onBeforeUnmount(() { chart.value?.dispose() }) return { chart, setOption, resize } }用shallowRef存 chart 实例是非常重要的细节因为 echarts 实例内部数据结构极其复杂如果用深层的ref去包装它代理操作的性能损耗非常大。关于 option 的更新我做了一个约定凡是主动拉接口获取的新数据一律调用setOption(newOption)并开启notMerge模式凡是用户交互导致的状态变化比如切换指标则用普通的合并更新。这样能避免因为数据变化而不断 fullMerge 导致的性能退化。4.2 可视化大屏的组件化思路和自适应方案热搜词里出现了vue3可视化大屏这算是一个很典型的商业项目场景。大屏开发的痛点是分辨率不确定、浏览器窗口比例千奇百怪、需要在电视端和拼接屏上跑。我的做法是把整个大屏页面拆成布局组件 指标卡片组件 图表组件三层布局组件负责绝对定位和百分比尺寸指标卡片组件负责数字滚动、翻牌动画图表组件内部统一封装了 echarts 初始化、resize 和销毁逻辑对外只接收option和占位尺寸。自适应方面我推荐用 rem 的方案。给 html 设置一个基准字号然后在 JS 里监听 resize 事件按设计稿宽度 1920 等比例计算 font-size。大屏内的所有横向尺寸单位都用 rem这样不同分辨率下页面整体等比缩放不会出现某个图表把旁边卡片挤下去的问题。实测在 1920x1080、2560x1440、3840x2160 三种分辨率下表现都比较稳定。注意 resize 事件要加防抖不然窗口拖拽时 CPU 占用率会飙得很高。4.3 扫码枪传入和 MQTT 实时消息这类数据进组件的方式扫码枪听起来和 Web 开发没什么关系但仓储、零售类的后台管理系统里经常要支持扫码录入。扫码枪本质上是一个 HID 键盘设备它把读到的条码内容模拟成键盘快速输入到当前聚焦的元素而且输入速度极快。所以在 Vue3 组件里接收扫码枪最靠谱的方案是监听全局keydown事件通过两个特征判断一次扫码完成字符之间间隔很短通常小于 30ms结尾是回车键。let barcodeBuffer let lastTime 0 function onKeydown(e: KeyboardEvent) { const now Date.now() if (now - lastTime 50) barcodeBuffer lastTime now if (e.key Enter) { if (barcodeBuffer.length 0) { handleBarcode(barcodeBuffer) barcodeBuffer } return } if (barcodeBuffer.length 64 (e.key 0 e.key 9) || /^[a-zA-Z-]$/.test(e.key)) { barcodeBuffer e.key } }这个方案有个很实用的地方不管当前焦点在输入框、按钮还是页面空白处扫码都能生效不依赖任何硬件厂商的 SDK。类似地MQTT 实时消息进入组件时也要注意生命周期在onMounted里建立连接并订阅主题在onBeforeUnmount里取消订阅并关闭连接防止组件卸载后仍然收到消息导致内存泄漏。收到的新消息如果想驱动界面展示建议先写入 Pinia 或组件内的ref再由模板层去响应不要在回调里直接操作 DOM。4.4 iframe 嵌入、Electron 桌面端、视频插件能稳定跑通的接入姿势热搜里有vue3嵌套iframe没有办法触发iframe外层div的点击事件这个问题。iframe 默认会捕获鼠标事件外层 div 的 click 事件确实触发不了这是 iframe 的天然行为不是 Vue3 的问题。解决方案通常是在 iframe 外面再套一层透明的遮罩层或者监听window上的blur事件来间接判断用户点了 iframe也可以使用window.postMessage和内部页面做通信让 iframe 内部主动发消息给外层。如果是 Electron 项目Vue3 组件和主进程通信要走 preload 暴露的window.api不要在渲染进程直接使用 Node 的ipcRenderer不然后续开启contextIsolation后会被安全策略直接拦截。视频类场景比如接入海康威视的 Web 插件这类插件通常会往页面上塞 ActiveX 或私有控件在 Vue3 里很容易出现组件加载顺序不对导致插件初始化失败。我给的建议是用defineAsyncComponent异步加载视频预览组件等插件初始化完再挂载业务面板给插件一个稳定的环境。4.5 懒加载列表和虚拟滚动长列表不卡顿的封装方案后台管理系统里经常有超长列表滚动如果直接渲染几千条 DOM页面会卡到怀疑人生。组件化方案大体分两种懒加载滚动到底再请求下一页和虚拟滚动只渲染视口内的行。vue-virtual-scroller在 Vue3 里用起来已经比较顺手核心是给列表容器一个固定高度通过v-virtual-scroll-item插槽逐行渲染。如果不想引入额外库也可以用 IntersectionObserver 自己做一个加载更多指令在元素进入视口时发出load-more事件由父组件决定是否追加数据。注意在卸载时一定要断开 observer否则组件回收后 observer 回调还会执行导致对已销毁组件做状态更新报错。5. 从组件到系统后台管理、商城大屏这类项目怎么组织5.1 动态路由和按钮级权限的组件化落地热搜里出现vue3后台管理系统vue3商城若依vue3 ts报错jeecgboot迁移antd4本质上都绕不开权限系统的前端落地。后台管理系统的权限一般分两层路由级权限某个用户能不能进入某个页面和按钮级权限某个用户能不能看到某个按钮。路由级权限用动态路由实现登录后用后端返回的权限码过滤本地路由表再调用router.addRoute()动态添加路由。按钮级权限我习惯封装成v-permission指令指令内部判断当前用户的权限码不通过就移除元素。import type { Directive } from vue import { useUserStore } from /stores/user const permission: DirectiveHTMLElement, string | string[] { mounted(el, binding) { const userStore useUserStore() const required Array.isArray(binding.value) ? binding.value : [binding.value] const hasPermission required.some((code) userStore.permissions.includes(code)) if (!hasPermission) { el.parentNode?.removeChild(el) } }, }指令的方案比 v-if 好的一点是一个按钮的权限判断逻辑只写一次后续新按钮就直接加指令不用在每处都引用useUserStore。若依和 JeecgBoot 这类开源框架的 Vue3 版本目前基本都基于 TypeScript script setup重构了如果遇到 ts 报错大概率是某个第三方库的类型声明缺失用declare module补一个类型补丁或者在tsconfig.json里关闭strict的相关选项不会有太复杂的玄学问题。5.2 中后台通用组件库的搭建思路从页面里抽离可复用逻辑做中后台项目时最高效的方式不是一开始就去封装一个完美的组件库而是按需沉淀。开发过程中如果你发现某个筛选条件、某个表格操作列、某个弹窗表单在多处重复就赶紧抽出来。我在 HoRain云 的项目里沉淀最多的是TablePage和ModalForm这两个基础组件。TablePage统一管理列表加载、分页、搜索参数对外暴露refresh方法子组件可以调用它刷新当前页数据。ModalForm把弹窗的开关、表单校验、提交按钮 loading 全部封装对外暴露open、close方法。这两个组件一旦沉淀好后续新页面能压缩一半代码量。这里想多说一句通用组件的 props 设计要克制。没必要把所有配置都做成 props一些不影响核心逻辑的样式或者交互细节可以留一个attrs或插槽让使用方自行扩展避免组件 API 越画越庞大到最后没人敢动。5.3 从 Vue2 迁移到 Vue3 最容易忽略的差异uniapp vue2转vue3方法也在热词榜上说明迁移需求确实很大。两个版本之间的差异远不止 Composition API 和$emit的变化还有几个容易忽略的细节$attrs的行为变了。Vue3 里$listeners被删除$attrs包含了 class、style 和事件监听器并且默认会继承到组件根节点。组件根节点如果有多个或者想要手动控制落到哪个子元素上就必须设置inheritAttrs: false。全局 API 改成实例方法。Vue.use()、Vue.mixin()、Vue.prototype.$xxx全部移到app.use()、app.mixin()、app.config.globalProperties.$xxx。如果没有迁移代码里会出现Vue is not a constructor之类莫名其妙的报错。过滤器filter被移除了。官方建议用计算属性或方法替代。如果一个大型项目里到处是过滤器迁移时工作量不小建议写个脚本把模板里的| filterName统一改成方法调用。v-for和v-if同时使用时Vue3 中v-if的优先级高于v-for这和 Vue2 正好相反。很多 V2 代码迁移后逻辑会静默翻转页面内容突然消失排查起来特别隐蔽。6. 疑难杂症排查实录高频报错和诡异的 UI 问题6.1 常见报错与排查速查表我把这些年遇到且被反复搜索的问题整理成一张速查表遇到对应情况直接对着处理。现象根本原因解决方案Vite 启动慢或报defineComponent is not defined自动导入类插件与 Vite 版本不匹配升级unplugin-auto-import、unplugin-vue-components为最新版本script setup里用 props 赋值到 ref 后页面不更新直接取 props 属性赋给 ref 会断开响应链优先用 computed或用 watch 监听 props 后同步 ref组件在 Edge 浏览器里关闭弹窗时按钮偶尔无响应全局 message/toast 容器层级覆盖了弹窗检查全局注册的 fixed 层级给弹窗加更高 z-index或用appendTo控制挂载节点tabs 标签页样式改不动组件内部样式使用 scoped且目标样式在子组件深层使用:deep()选择器覆盖或改用全局样式文件控制 tabs 类名iframe 外层 div 点击事件无法触发iframe 拦截了鼠标事件外部加遮罩层或通过 postMessage 通信翻译到 Electron 后window.api为 undefinedpreload 脚本未正确暴露或 contextIsolation 配置不一致检查 main 进程 preload 路径确保contextBridge.exposeInMainWorld正常执行MQTT 消息回调里更新组件状态不生效组件已卸载或回调不在作用域内在 onMounted 中建立连接、onBeforeUnmount 中清除订阅消息回调里使用 ref 更新6.2 一个真实案例Edge 浏览器右上角关闭按钮偶发失效热搜里有一条vue3项目在edge浏览器中有时候无法关闭浏览器右上角的最小化按钮这类问题和 Vue 本身没有直接关系但它确确实实会出现在 Vue3 项目的生产环境里。我后来定位过类似问题发现绝大部分是因为页面里有一个全屏遮罩层position: fixed且z-index非常大虽然视觉上是透明的但它已经在浏览器窗口的最上层导致浏览器自身的窗口按钮最小化、关闭无法被点击。这种情况在 message 组件、loading 遮罩、弹窗蒙层里非常常见。排查思路就是打开开发者工具把html和body的样式临时禁用或者用 Elements 面板里选择器查找哪个元素占满了整个视口。解决办法有三个给遮罩层设置pointer-events: none但要注意子元素需要点击的话得恢复在弹窗关闭动画结束后主动移除遮罩层或者把遮罩层高度控制在弹窗实际高度范围内。从工程视角看第三种方案最干净它能保证弹窗关闭后页面即刻恢复到可交互状态。6.3 右键菜单、拖拽自适应这类交互组件封装的正确姿势vue3创建右键菜单和元素容器与容器之间拖拽自适应也是常见交互题。右键菜单我建议封装成一个轻量组件接收x、y、menus三个 props用Teleport渲染到 body 下点击任意区域后触发close事件。右键菜单关闭逻辑要在 document 上监听 click但要注意阻止contextmenu事件本身的冒泡否则菜单一出现就被自己关掉。拖拽自适应场景比如调整列宽、调整面板间距本质上是在 mousedown 时记录起点坐标在 mousemove 时计算差值在 mouseup 时解除监听。这个逻辑也可以抽成组合式函数useDragResize暴露出startPosition、deltaX、deltaY让组件消费这些值去改变宽度或偏移。有一个容易忽略的点拖拽过程中如果鼠标移出了组件区域要监听window上的 mousemove 和 mouseup而不是绑定在组件元素上否则鼠标一快就断触视觉上就是拖不动。我自己常用的一条经验这类交互组件尽量避免在模板里写mousemovexxx而是用组合式函数 事件监听统一管理。组件卸载时记得把 window 上绑定的监听全部移除否则会出现页面不报错但事件被多次触发的诡异现象。6.4 Mock 数据和 Eventsource 实时推送调试数据流的技巧vue3 ts的mock使用教程这类搜索说明很多人都在为前后端并行开发的阶段发愁。我推荐用vite-plugin-mock做本地 Mock它支持 TypeScript 编写 mock 接口能在 dev 环境里直接拦截请求并返回模拟数据。Mock 的目录和真实接口路径保持一致这样联调时只需要把 mock 关掉业务代码不用动。关于eventsourcepolyfill或者 SSE 这类服务端推送在 Vue3 组件里要特别注意EventSource实例一旦创建会自动重连组件卸载时一定要手动close()否则浏览器会残留连接页面切走后后端还在持续推数据。这类连接建议用composables/useSse.ts统一管理对组件暴露connect、close、onMessage三个接口。实测下来配合 Pinia 把推送的数据落成全局 state多组件订阅同一份数据比每处都单独建一个 EventSource 要稳定得多。写到这里组件开发这条链路基本串完整了。从我自己的实战体验来说Vue3 组件开发真正难的不是某一条语法或者某一个 API而是有没有一套稳定可复用的架构习惯——目录怎么分、状态放哪一层、组件边界划到哪、第三方能力怎么封装、异常问题怎么快速定位这些东西没有捷径全靠一个个项目积累。如果你正在入坑 Vue3建议不要只在文档里打转找一个小模块亲手从搭建工程开始做一遍把组件通信、异步加载、生命周期清理这些环节全部过一遍踩过几次坑之后你对这套框架的理解会比看十篇教程都要深。后面我还会继续更新一些 Vue3 工程化的具体实战比如从零搭建一个适合团队的中后台组件库以及 Electron Vue3 的桌面端优化细节欢迎持续关注。
返回列表