尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Vue3组件开发实战指南:从组合式API到性能优化与部署

Vue3组件开发实战指南:从组合式API到性能优化与部署 说实话Vue3这个技术栈我已经在HoRain云上前后折腾过多个项目从最开始的管理后台到后来的可视化大屏、物联网设备管理端组件开发这块踩过的坑和沉淀下来的套路都算比较多了。每次带新人上手发现大家最迷茫的反而不是语法而是“组件到底该怎么拆、怎么写才不算烂代码”这件事。这篇指南我想按自己的实践路径来写从环境搭好一颗新项目到把通用组件一个个造出来再到第三方库集成和上线部署最后把高频报错一网打尽。内容会比较啰嗦但都是能直接拿去用的东西适合刚接触Vue3的初学者也适合写过一阵子但总觉得封装没谱的进阶选手。1. 组件开发前的环境与工程搭建1.1 版本选型与工具链选择先说版本。Vue3组件开发已经相对成熟现在新建项目可以直接选Vue 3.4搭配Vite 5组合式API加TypeScript这套组合是当下最主流的选择。Node.js这边记得装18以上最好直接上20 LTSnpm、pnpm、yarn任选一个包管理器我个人在HoRain云上部署习惯用pnpm依赖安装速度快磁盘占用也小。很多同学会纠结是不是还要用Webpack。我的观点很直接新项目别回头了。Vite基于esbuild预构建依赖开发模式冷启动基本秒开热更新也是毫秒级反馈写组件的时候频繁改代码Vite给人的体验提升不是一点半点。如果你维护的是老项目必须继续用Webpack那Vue3本身也能跑只是配置上要处理的东西多一些比如CustomElement、vue-loader版本对齐之类的。还有个容易忽略的点是TypeScript版本。Vue3的官方类型推导对TS版本比较敏感建议装最新稳定版避免因为版本过旧导致defineComponent的类型推断不准确这在后续开发里会省很多事。Vue Router建议装4.xPinia建议装2.x跟Vue3的版本大版本匹配上就行。1.2 用Vite搭建标准Vue3 TS工程组件开发的第一步是有一个结构干净的工程。用官方脚手架最省事一条命令就能拉起来npm create vuelatest执行后会有交互式选项建议按下面的方式选择TypeScript选Yes组件开发里面类型约束非常重要JSX看个人习惯如果用render函数多就选YesVue Router选Yes组件路由场景绕不开Pinia选Yes状态管理后面要用Vitest项目需要写单测再选ESLint / Prettier全部选Yes多人协作时代码规范能少吵很多架项目拉起来之后先装依赖npm install npm run dev我自己习惯在这个基础上再补几个开发依赖sass做样式预处理unplugin-auto-import和unplugin-vue-components做API和组件的自动导入。这两个插件配合Volar能省掉大量“每次写ref都要手动import”的重复工作尤其是写业务组件的时候少一两个import看起来不起眼但整个组件文件会清爽非常多。自动导入插件配置起来也不复杂在vite.config.ts里挂上即可import AutoImport from unplugin-auto-import/vite import Components from unplugin-vue-components/vite export default defineConfig({ plugins: [ vue(), AutoImport({ imports: [vue, vue-router, pinia] }), Components({ dirs: [src/components] }) ] })1.3 组件目录结构与命名规范目录结构直接决定后续维护体验。我在HoRain云上的项目一般这么组织src/ components/ base/ # 基础原子组件如按钮、输入框、弹窗 business/ # 业务组件如用户选择器、订单表格 layout/ # 布局组件如侧边栏、头部导航 charts/ # 图表封装组件 composables/ # 组合式函数逻辑复用层 views/ # 页面级组件 stores/ # Pinia状态 utils/ # 纯工具函数命名规范这个看似小事但直接影响组件的可读性和可检索性。组件文件名统一用大驼峰比如UserAvatar.vue、OrderStatusTag.vue。组件name属性也保持一致配合devtools调试的时候才能一眼认出是哪个组件。一个tips组件名尽量用两个以上单词避免与原生HTML标签重名也避免和第三方库未来新增的标签产生覆盖冲突。2. Vue3组件开发的底层心智模型2.1 组合式API才是组件的第二语言很多从Vue2过来的人一开始都别扭觉得Options API的data、methods、computed分开写挺清晰的。但真正开发组件后你会发现一个功能相关的逻辑被拆散在多个选项块里改一个需求要在文件里来回跳。组合式API最大的价值就是让“同一件事的代码待在一起”。以一个小计数器为例script setup langts import { ref, computed } from vue const count ref(0) const double computed(() count.value * 2) function increment() { count.value } /script template div p{{ count }} / {{ double }}/p button clickincrement加一/button /div /templatescript setup语法糖让组件代码极度精简变量和函数定义后直接能在template里用。实际工作中我90%的组件都是用script setup写的因为它少了一层export default的包裹心智负担小。但有个场景我会额外用defineComponent就是组件希望有更完善的类型推断选项比如props类型推导不够精确的时候或者需要给组件加name字段配合keep-alive时。热词里有人问“defineComponent创建组件”怎么用本质上就是给你一个显式定义组件对象的方式script langts import { defineComponent, ref } from vue export default defineComponent({ name: CounterBox, setup() { const count ref(0) const increment () count.value return { count, increment } } }) /script2.2 组件通信的九种姿势与选型组件通信永远是新手的痛。我把Vue3里的通信方式整理成了一张表日常开发对着选就行通信场景推荐方式说明父传子props最基础的数据下行声明类型和默认值子传父emit事件通过defineEmits声明类型安全父子双向绑定v-model本质是modelValue prop update:modelValue事件跨多级传递provide / inject适合深层嵌套无需层层透传兄弟组件共享Pinia store状态提升到store组件各自读写任意跨组件Pinia store全局唯一数据源避免事件总线父组件拿子组件方法ref绑定组件实例子组件用defineExpose暴露方法非必要全局事件不推荐用事件总线Vue3删了$on自己搞EventBus容易乱结构复用插槽slot组件只负责布局内容由父级决定选型原则就一句话能props就props能emit就emit别一上来就上全局状态。我见过不少项目一个弹窗组件里几个字段都要放Pinia结果刷新页面字段状态全乱了还得重新拉接口。组件通信的思路应该是“谁的数据谁持有谁需要谁下发”。2.3 组件拆分与设计原则拆组件不是越细越好拆到粒度合适才算好。我带团队的时候给过一个朴素标准一个组件文件超过300行且包含多个不相关功能或者同一个模板片段在项目里出现三次以上就该考虑拆了。核心设计原则有三条第一单一职责。一个组件只做一件事。比如头像组件只负责展示头像和处理头像加载失败不要让它兼职做上传。上传功能单独抽UploadAvatar组件由父组件去组合它们。第二受控与非受控分离。比如一个输入框组件支持外部传value的时候走受控模式外部不传就走内部维护状态的非受控模式。Vue3里可以用computed加setter来优雅实现const innerValue ref() const proxyValue computed({ get: () props.modelValue ?? innerValue.value, set: (val) { if (props.modelValue ! undefined) { emit(update:modelValue, val) } innerValue.value val } })第三配置项驱动。复杂组件尽量把可变化的部分收敛成一个config对象。比如表格组件列宽、对齐、自定义渲染函数都可以用columns数组配置而不是每一个属性单独传一个props这样调用方写起来更清晰组件内部逻辑也更统一。3. 通用业务组件从零实现3.1 封装一个带防抖的搜索框组件搜索框是后台管理系统里最常见的业务组件。直接写input加监听每次按键都会触发请求接口压力大还容易因为响应顺序导致旧结果覆盖新结果。封装组件时建议把防抖逻辑内置。script setup langts import { ref, watch } from vue const props defineProps{ modelValue: string delay?: number }() const emit defineEmits{ (e: update:modelValue, value: string): void (e: search, value: string): void }() const innerValue ref(props.modelValue) let timer: ReturnTypetypeof setTimeout | null null watch(() props.modelValue, (val) { innerValue.value val }) watch(innerValue, (val) { if (timer) clearTimeout(timer) timer setTimeout(() { emit(update:modelValue, val) emit(search, val) }, props.delay ?? 300) }) /script template input v-modelinnerValue typetext placeholder请输入搜索关键字 classsearch-input / /template style scoped .search-input { width: 100%; height: 36px; padding: 0 12px; border: 1px solid #dcdfe6; border-radius: 6px; outline: none; transition: border-color 0.2s; } .search-input:focus { border-color: #409eff; } /style这里有个关键细节外部modelValue变化的时候要用watch同步到innerValue否则外部重置搜索条件时组件内部显示不会刷新。这个坑我踩过不止一次搜索结果一栏重置后输入框里还留着旧关键字排查半天才发现是单向同步没做。延迟参数用props.delay ?? 300而不是props.delay || 300是为了兼容传入0的使用场景0也表示不防抖直接用逻辑或会把0变成默认值。3.2 列表懒加载与虚拟滚动热词里同时出现了“vue3 列表懒加载”和“vue-virtual-scroller vue3使用”这两个其实是不同场景的方案。列表懒加载解决的是“数据分段拿”的问题虚拟滚动解决的是“一次性渲染几千条DOM卡死”的问题。懒加载最轻量的实现是IntersectionObserver。以滚动到底部加载更多为例import { ref, onMounted, onUnmounted } from vue const loading ref(false) const list refItem[]([]) const sentinel refHTMLElement | null(null) let observer: IntersectionObserver | null null async function loadMore() { if (loading.value) return loading.value true try { const newData await fetchNextPage() list.value.push(...newData) } finally { loading.value false } } onMounted(() { observer new IntersectionObserver((entries) { if (entries[0].isIntersecting) { loadMore() } }, { rootMargin: 200px }) if (sentinel.value) observer.observe(sentinel.value) }) onUnmounted(() observer?.disconnect())模板底部放一个哨兵div高度为1px。rootMargin设200px的意思是提前200px触发避免用户滑到底部还要等白屏。虚拟滚动在Vue3里可以直接用vue-virtual-scroller这个库。安装时记住一定看版本Vue3对应的是vue-virtual-scroller的2.x分支跟Vue2的1.x用法差别挺大。核心思路是外层容器固定高度内部只渲染可视区域那一小部分列表项通过计算scrollTop和itemSize算出哪些item需要渲染。列表项高度不固定的场景库内部会自动测量但会有一定的计算开销。我自己用下来的经验是列表数据超过500条且行高固定时虚拟滚动性价比最高如果行高不固定且数据量只有100来条先做懒加载就够了别为了炫技引入复杂度。3.3 右键菜单组件右键菜单看起来简单但有几个细节很容易翻车。一是组件要渲染到body下避免被父级overflow裁剪二是点击页面其他地方要关闭三是菜单不要超出视口边界。实现上可以用Teleport把菜单内容传送到bodyscript setup langts import { onMounted, onUnmounted, ref } from vue const visible ref(false) const x ref(0) const y ref(0) function showMenu(event: MouseEvent) { event.preventDefault() x.value event.clientX y.value event.clientY visible.value true } function closeMenu() { visible.value false } onMounted(() { window.addEventListener(click, closeMenu) window.addEventListener(resize, closeMenu) }) onUnmounted(() { window.removeEventListener(click, closeMenu) window.removeEventListener(resize, closeMenu) }) /script template div contextmenu.preventshowMenu slot / /div Teleport tobody div v-ifvisible classcontext-menu :style{ left: x px, top: y px } slot namemenu / /div /Teleport /template边界判断可以通过一个computed在渲染前修正坐标。比如菜单宽度200px、高度160px当x 200 window.innerWidth时就把x改成window.innerWidth - 200 - 8留8px的视觉边距。这个细节不处理用户右键靠近右下角的时候菜单就会跑到屏幕外面点都点不到。右键菜单的关闭时机也要注意。contextmenu事件本身会携带一次mousedown如果监听的是全局click需要考虑事件冒泡顺序弹开菜单的那次点击不应该立即触发关闭。我的做法是对showMenu函数加上stopPropagation或者在showMenu时设置延迟监听。这个坑在Windows触摸板和双按键鼠标上尤其明显。3.4 可视化大屏组件的适配实践大屏项目在Vue3里有一套相对成熟的适配方案。我看到热词里有“vue3 可视化大屏”和“vue3 echarts”这两个经常是成对出现的。大屏适配最常用的是缩放方案设计稿按1920x1080制作页面根元素用transform的scale进行等比缩放。实现思路是监听视口尺寸变化计算缩放比例const scale ref(1) function handleResize() { scale.value Math.min( window.innerWidth / 1920, window.innerHeight / 1080 ) } onMounted(() { handleResize() window.addEventListener(resize, handleResize) })模板里给根容器套一层style做transform和transform-origin定位。这种方案的好处是所有内部组件的尺寸和字体大小都按设计稿写不用到处用vw/vhECharts的尺寸计算也不会因为rem换算导致偏差。但注意缩放方案下ECharts图形的tooltip位置可能会偏移。原因是tooltip默认基于canvas容器的坐标系transform缩放后鼠标坐标和canvas内部的坐标会产生换算差异。解决办法是在ECharts实例上监听zr事件用event.offsetX和event.offsetY除以scale得到真实的坐标再手动调用chart.dispatchAction里的showTip事件。4. 生态集成与高阶场景实战4.1 Pinia状态管理的最佳实践Pinia和Vue3的组合式API设计天然契合。定义一个store的setup写法很直观import { defineStore } from pinia import { ref, computed } from vue export const useUserStore defineStore(user, () { const token ref() const userInfo refUserInfo | null(null) const isLogin computed(() !!token.value) async function login(payload: LoginParams) { const res await request.post(/login, payload) token.value res.token userInfo.value res.info } function logout() { token.value userInfo.value null } return { token, userInfo, isLogin, login, logout } })store里最忌讳的是把所有数据都塞进去。我见过有人把表格的搜索条件、分页参数、当前选中的行、甚至loading状态全放store里结果一个页面多个组件互相挤占状态维护起来非常痛苦。我的经验是只有真正被多个组件共享的数据才进store单纯一个页面内部的临时状态就用ref在组件里管着别过度设计。Pinia在组件外使用要注意一点调用useUserStore()前必须确保Pinia实例已经安装否则会报getActivePinia错误。在路由守卫或者axios拦截器里使用store需要在main.ts中把pinia实例导出然后在外部直接传实例调用// main.ts export const pinia createPinia() app.use(pinia) // 路由守卫里 import { pinia } from /main import { useUserStore } from /stores/user const userStore useUserStore(pinia)4.2 基于ECharts的图表组件封装图表组件是业务项目里高频复用的典型场景。我一般封装一个BaseChart组件接收option和高度props内部处理初始化、更新和销毁script setup langts import * as echarts from echarts import { onMounted, onUnmounted, ref, watch, nextTick } from vue const props defineProps{ option: echarts.EChartsOption height?: string }() const chartRef refHTMLElement | null(null) let chart: echarts.ECharts | null null function initChart() { if (!chartRef.value) return chart echarts.init(chartRef.value) chart.setOption(props.option) } function handleResize() { chart?.resize() } onMounted(async () { await nextTick() initChart() window.addEventListener(resize, handleResize) }) onUnmounted(() { window.removeEventListener(resize, handleResize) chart?.dispose() chart null }) watch(() props.option, (val) { chart?.setOption(val) }, { deep: true }) /script template div refchartRef classchart :style{ height: height ?? 300px } / /template这里三个细节值得说option用深度监听。ECharts的setOption是增量合并同一个图表多次setOption不会清空之前的数据所以深度watch是安全的。组件卸载时必须调用dispose否则图表实例会一直占用内存。尤其是在路由切换频繁的后台系统里不销毁实例会导致页面越来越卡。容器初始化时如果display为none比如在tab页里echarts.init会拿到0宽度。解决办法是等tab切换完成后再手动调一次resize或者在组件内部增加一个可视状态判断。按需引入也是ECharts组件化的重要一环。全量引入echarts打包体积很大生产环境建议改为按需注册import { use } from echarts/core import { CanvasRenderer } from echarts/renderers import { LineChart, BarChart, PieChart } from echarts/charts import { GridComponent, TooltipComponent, LegendComponent } from echarts/components use([ CanvasRenderer, LineChart, BarChart, PieChart, GridComponent, TooltipComponent, LegendComponent ])按需引入后打包体积能降一半以上效果很明显。4.3 扫码枪、MQTT与硬件设备对接热词里的“vue3接收扫码枪”和“vue3 mqtt”其实都指向物联网和工业场景的前端开发。扫码枪在网页端本质是一个“快速输入的键盘”扫码枪扫码后会自动触发一段键盘事件然后以回车结尾。前端接收的核心是监听keydown事件import { onMounted, onUnmounted } from vue let codeBuffer let timestamp 0 function handleKeydown(e: KeyboardEvent) { if (e.key Enter) { if (codeBuffer) { console.log(扫码结果:, codeBuffer) codeBuffer } return } const now Date.now() // 如果距离上次输入超过30ms说明是人工键盘输入重置缓冲区 if (now - timestamp 30) { codeBuffer } timestamp now codeBuffer e.key } onMounted(() window.addEventListener(keydown, handleKeydown)) onUnmounted(() window.removeEventListener(keydown, handleKeydown))这个30ms时间窗口是区分扫码枪和人工输入的关键。扫码枪每次按键的间隔极短通常小于10ms而人工敲键盘的间隔一般都大于30ms。实测下来这个阈值在20到50ms之间都比较稳。MQTT接入Vue3用mqtt.js这个库核心是创建客户端和订阅主题。需要注意的是Vue组件卸载的时候要主动调用end方法断开连接否则浏览器会积攒大量无效连接导致服务端误判客户端在线。这类涉及设备接入的场景在云服务器上部署时还要注意安全组和鉴权配置。我在HoRain云上部署过类似的接入服务一般会把MQTT broker放在内网前端通过后端转发或者使用wss加token鉴权的方式连接避免明文传输。这块不是前端组件本身的问题但整体架构上要提前规划。4.4 iframe嵌套与事件穿透问题热词里有个很典型的场景“vue3嵌套iframe没办法触发iframe外层div的点击事件”。这个问题的本质是iframe有自己的事件处理机制外层div的click事件只能监听到iframe容器本身无法穿透进iframe内部的DOM。简单说iframe内部是一个独立的文档鼠标点在iframe内容上时事件不会冒泡到外层文档。最常用的方案是postMessage通信。外层页面监听iframe的load事件然后向iframe发送消息iframe内部通过window.parent.postMessage回传数据。具体代码// 外层父页面 const iframeRef refHTMLIFrameElement | null(null) function handleIframeLoad() { iframeRef.value?.contentWindow?.postMessage({ type: FROM_PARENT, data: hello }, *) } window.addEventListener(message, (e) { if (e.data?.type FROM_IFRAME) { // 处理iframe内部传来的事件 } })如果是同源iframe还可以直接通过contentWindow访问内部方法和属性。跨域场景就只能走postMessage。热词里“没办法触发iframe外层div的点击事件”的情况如果你的需求是实现“点击iframe内部任意区域时让外层知道”就用postMessage如果你只是想挡住iframe上层的弹层浮层的点击穿透可以给浮层添加pointer-events样式并在合适时机移除。顺带提一句OnlyOffice、地图这类Web应用都有各自的iframe嵌入机制集成前先查官方文档确认是否支持postMessage通信能少走很多弯路。5. 构建、部署与性能优化5.1 用Nginx部署Vue3项目Vue3项目构建产物是纯静态文件部署到Nginx是最常见的方案。热词里有“win服务器 nginx 部署vue3项目”Windows服务器上配置思路和Linux基本一致主要路径和命令有点区别。构建之前先改vite.config.ts的base配置。如果项目部署在域名根路径下base用默认的/如果部署在子路径如/admin/下base要改成/admin/否则静态资源会全部404。构建命令npm run build产物在dist目录打包上传到云服务器后Nginx配置如下server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files那行是关键。Vue Router用history模式时刷新子路由页面Nginx如果找不到对应的静态文件就会404try_files会把所有未命中的请求回退到index.html由前端路由接管。静态资源缓存建议单独配置location /assets/ { expires 30d; add_header Cache-Control public, immutable; }带hash的文件内容变化会生成新文件名可以放心用长缓存。index.html则设置成不缓存保证每次发布后用户能拿到最新的入口文件。这块配置不好常见的现象就是项目发版后用户还是老页面清缓存才恢复。5.2 组件级别性能优化的几个方向Vue3组件性能优化跟Vue2相比思路有了不少变化。首先是响应式粒度更细了Vue3的Proxy能精确追踪到数据字段级别的更新大部分场景不需要手动优化。但有几个场景我实际测下来收益明显。第一个是shallowRef。如果你的组件里维护了一个巨大的列表数据但更新时整体替换用shallowRef可以避免Vue对所有嵌套属性做响应式代理能显著降低初始化开销const bigList shallowRefItem[]([]) function loadList() { bigList.value fetchData() // 整体替换 }第二个是v-memo。在模板中有v-for且子项渲染成本较高的场景可以用v-memo传入依赖数组只有依赖变化时才重新渲染对应区块div v-foritem in list :keyitem.id v-memo[item.id, item.status] !-- 只有item.id或item.status变化时才重新渲染 -- /div第三个是异步组件。把首屏不需要立即渲染的组件改成异步加载配合Suspense能减少首屏包体积const HeavyChart defineAsyncComponent(() import(/components/HeavyChart.vue))组件销毁时记得清理副作用这是性能问题的头号来源。window上的监听、定时器、Observer实例都在onUnmounted里处理干净否则页面来回跳几次就会积累越来越多无效监听肉眼可见的卡顿。5.3 Edge浏览器与弹窗关闭的问题排查热词里有一条“vue3项目在edge浏览器中有时候无法关闭浏览器右上角的最小化按钮”这个描述其实有点模糊我理解应该是页面上的某个“关闭”按钮在Edge里偶尔点不动。遇到这种问题多数不是Vue3本身的bug而是页面上存在透明层遮挡了按钮。排查思路很明确打开DevTools在Elements面板选中那个关闭按钮看看有没有一个透明或半透明的元素覆盖在它上面。这种透明层最常见的来源有两个一个是弹窗组件销毁时动画未结束DOM其实还挂在body下只是opacity变成0了。旧版会有这个问题ECharts的tooltip、手动创建的浮层组件都可能留下残留节点。解决方法是确保动画结束后再卸载组件或者用v-if把浮层彻底移除。另一个是全局监听事件导致的判断冲突比如热词里提到的“没办法触发iframe外层div的点击事件”以及某些场景下鼠标事件被上层Element截获。在Edge、Chrome这类Chromium内核浏览器里鼠标事件命中检测依赖元素的pointer-events属性如果一个元素设置了pointer-events: none它就不会拦事件反之就会。如果关闭按钮被拦截优先检查外层浮层有没有设置pointer-events以及是否有负z-index的定位元素把按钮盖住。我实际排查过的一个案例是某项目中给顶层容器设置了全局的user-select: none同时一个自定义的拖拽指令在mouseup里没做坐标边界判断导致某些操作后透明遮罩状态位没复位按钮点击事件一直被吞掉。这种问题很难靠读代码发现最好在点击无响应时用DevTools的Event Listeners面板检查按钮上的事件监听器是否还处于挂载状态。6. 常见问题排查与避坑实录6.1 defineComponent is not defined热词里有一条“vue3 vite5总是报definecomponent is not defined”这个报错其实分几种情况。最直接的原因是变量没导入。如果你写了defineComponent但代码里没有import { defineComponent } from vue那必然报这个错。但如果是script setup语法理论上是不需要手动引入defineComponent的因为它是编译器宏。那为什么Vite5下还是会报我遇到过一次是项目里同时存在script setup的文件和一个单独用script langts定义组件对象的文件后者引用了defineComponent但tsconfig的types配置把vue/runtime-core的类型搞乱了IDE不报错但运行时被当成全局变量去解析。解决方法是显式加上importimport { defineComponent } from vue export default defineComponent({ name: MyComponent })以及检查依赖版本。Vite5要求Vue版本至少3.4以上如果项目是从Vite2升级上来的node_modules里可能残留旧的vue版本或者重复的vue安装。清掉依赖重装一遍问题会自动消失。6.2 Vue3 TS的常见报错整理Vue3加TS的组合在项目里报错大部分都出在几个固定位置。一是类型导入和值的导入混淆。运行时要用到的API必须用普通import比如import { ref } from vue只用做类型标注的用import type比如import type { PropType } from vue。如果eslint配置了verbatimModuleSyntax混着写会直接编译报错。二是tsconfig路径别名没配置。vite.config.ts里配了别名但tsconfig.json没有对应paths编辑器就会一路标红。两者必须同时维护{ compilerOptions: { baseUrl: ., paths: { /*: [src/*] } } }三是Volar和Vetur共存。Vue3项目请一定禁用Vetur只保留Vue Language FeaturesVolar。我见过不少接手老项目的同学两个插件同时在编辑器里生效类型推导乱成一锅粥什么神奇报错都能冒出来。6.3 props赋值给data导致响应式丢失热词里“vue3 props赋值给data”是个很典型的问题。很多从Vue2过来的人习惯在data里初始化一份props的值然后在组件内部直接修改。Vue3里如果写成这样const props defineProps{ count: number }() const localCount ref(props.count)localCount只在初始时取了一次值后续props.count变化localCount不会跟着变。这是因为ref的入参只在创建时计算一次。正确做法有两个。如果只是展示直接用computedconst displayCount computed(() props.count)如果确实需要在内部修改那就watch一下const localCount ref(props.count) watch(() props.count, (val) { localCount.value val })还有一种情况是父组件更新对象类型的prop时子组件内部的引用仍然指向旧对象这涉及到引用传递和可变数据的问题最好的解法是从设计上避免父组件的对象prop尽量用不可变更新方式每次都是新对象。6.4 Vue2转Vue3的迁移清单很多团队面临unispp vue2转vue3、老项目升级的情况。这里列几个最容易被忽略的差异点v-model的用法变了。Vue3的v-model可以传多个参数原来是model .sync组合的写法被废弃直接用多个v-model:xxx。filter选项被移除。原来是全局过滤器或者组件过滤器格式化数据现在统一用computed或函数调用。$children被移除。访问子组件实例只能通过ref或者配合provide/inject。$on、$off、$once被移除。EventBus的写法在Vue3里不再支持推荐用Pinia替代。异步组件API变了。Vue2的() import()写法在Vue3中要包一层defineAsyncComponent。全局API从Vue构造函数移动到了app实例上。Vue.use改成app.useVue.component改成app.component。自定义事件要声明。子组件上用defineEmits声明emit事件否则会收到警告某些场景行为还会异常。这些差异如果没提前梳理升级过程中会反复踩坑。我建议迁移前先跑一遍官方迁移工具把已知废弃语法扫出来再人工过一遍业务组件效率高很多也不容易遗漏。最后分享一个我个人的习惯写组件的时候先想清楚这个组件未来会被谁用、在什么场景下用然后才动手写代码。一个组件如果只能被当前页面使用直接写在页面里就好没必要强行抽出来如果一个组件预判要被三四个地方用那就值得花时间把props、事件、插槽设计完整。组件开发没有银弹靠的是持续整理、持续复盘把每次踩坑的教训沉淀成自己的组件规范时间久了自然就顺手了。
返回列表