拓冰建站拓冰建站
首页 / 资讯中心 / 正文

2026年Vue正确姿势:组合式API重构实战与关键细节

2026年还在写一堆methods、computed、watch各占一区的Options API代码说实话有点说不过去了。我自己从Vue 2时代一路写过来刚开始接触组合式APIComposition API时也觉得这不就是换了个写法吗直到在一个中大型后台项目里彻底重构了一遍之后才真正体会到组合式API不是换个语法糖而是从代码组织方式、复用模式到心智模型的全面升级。这篇文章不聊虚的直接从我实际项目的重构经历出发讲清楚为什么组合式API才是2026年写Vue的正确姿势以及你现在就可以开始动手落到代码里的细节。1. 先说清楚Options API到底错在哪1.1 业务逻辑被切碎成三段这是最疼的很多从Vue 2时代过来的朋友第一次学Vue的时候接触的都是Options APIdata里放数据、methods里放方法、computed里放计算属性、watch里放侦听器。这种按选项类型划分代码的方式在小组件里面完全没问题甚至可以说直观得一目了然。但是当组件超过200行、300行、500行的时候问题就来了——一个业务功能的数据、方法、计算属性和侦听器被硬生生拆散在四个不同的区块里。我举一个真实例子。之前做一个电商后台的订单列表页里面有搜索、筛选、分页、导出、批量操作、状态联动这几个模块。在Options API里这些模块的数据全堆在data里方法全堆在methods里computed里还混着好几个模块的计算逻辑watch里更是各种状态互相监听。改一个订单状态的功能你得在data里找到订单状态字段、在methods里找到修改状态的方法、在computed里找到依赖状态的计算属性、再在watch里找到跟状态相关的监听逻辑——一次简单的改动要在四个区块之间来回跳转。这不是代码风格问题这是实实在在的生产力损耗。1.2 逻辑复用全靠mixins埋了一堆隐性地雷Options API时代官方推荐的逻辑复用方案是mixins。我在项目里踩过太多mixins的坑了。最典型的一个问题是命名冲突项目里有两个mixins一个定义了initData方法另一个也定义了同名方法后引入的会静默覆盖先引入的排查的时候极其痛苦。还有一个问题是数据来源不透明组件里某个字段是data里自己定义的还是从某个mixin混进来的不点开mixin源码根本不知道。项目一大了组件和mixins之间的关系就像一张蜘蛛网看似复用了代码实际是给自己埋了一堆地雷。1.3 TypeScript支持是附加分而不是原生能力2026年TypeScript在Vue项目里已经算是标配了。Options API的问题在于this上的属性是通过类型合并推导出来的在复杂组件里编辑器的类型提示经常跟不上甚至出现this.xxx报错但运行正常的情况。而写类型定义的时候也很别扭——一个方法的参数类型、返回类型都要在选项对象里硬写跟JavaScript的写法差异很大。说到底Options API是为JavaScript时代设计的API形态TypeScript只是能用谈不上好用。2. 组合式API的核心设计哲学逻辑聚合与显式数据流2.1 从按选项分类到按业务领域聚合组合式API最核心的思维转变是把按选项类型划分代码变成了按业务领域聚合代码。同一个业务功能的数据、方法、计算属性、侦听器全部放在一起写。这样代码的结构就跟业务的边界对齐了而不是跟框架的语法对齐。我打个比方。Options API就像你把自己的工具按类型收纳所有螺丝钉放一个抽屉所有扳手放一个抽屉所有电钻放一个抽屉。但如果你的工作是组装一台柜子你得不停地在三个抽屉之间来回拿东西。而组合式API就像按项目收纳组装柜子的所有工具放一个箱子组装桌子的工具放另一个箱子。哪个项目需要用到什么打开对应的箱子就行。后者当然更贴合真实的工作场景。2.2 逻辑复用从mixins升级为composables组合式API把逻辑复用的方案从mixins升级成了自定义组合函数composables。一个composable就是一个普通的函数内部可以用ref、computed、watch这些API组织逻辑然后return出需要暴露给组件的东西。这种模式的好处是没有隐式的数据来源所有数据都是显式传入和返回的不会发生命名冲突函数内部的状态是封闭的TypeScript类型推导非常自然参数和返回值的类型一目了然可以自由组合多个composable之间互不干扰2.3 数据流是显式的不是魔法的在Options API里this.xxx的数据流向是很隐式的——它来自data、props、computed还是methods你得靠记忆。而在组合式API里你在setup函数中显式定义每个响应式数据显式声明每个函数显式return给模板使用。数据从哪里来、经过什么转换、到哪里去整个链路在代码里就能看明白。这种显式性在团队协作和代码评审中特别有价值——review代码的时候不需要跳来跳去猜了。3. 实战对比同一个功能Options API和组合式API分别怎么写3.1 需求描述一个带搜索和分页的用户列表为了让大家直观感受两者的差异我用一个真实项目里很常见的场景来做对比一个用户列表页需要支持搜索、分页、加载状态还有一个搜索条件变化时重置到第一页的逻辑。以下是Options API版本template div input v-modelkeyword placeholder搜索用户 / button clickhandleSearch搜索/button table tr v-foritem in filteredList :keyitem.id td{{ item.name }}/td td{{ item.email }}/td /tr /table button clickhandlePrev :disabledpage 1上一页/button span{{ page }} / {{ totalPage }}/span button clickhandleNext :disabledpage totalPage下一页/button p v-ifloading加载中.../p /div /template script export default { data() { return { keyword: , rawList: [], page: 1, pageSize: 10, loading: false, }; }, computed: { filteredList() { let list this.rawList; if (this.keyword.trim()) { list list.filter((item) item.name.toLowerCase().includes(this.keyword.toLowerCase()) ); } const start (this.page - 1) * this.pageSize; return list.slice(start, start this.pageSize); }, totalPage() { return Math.max( 1, Math.ceil( this.filteredList.length / this.pageSize ) ); }, }, watch: { keyword() { this.page 1; }, }, methods: { async fetchList() { this.loading true; // 模拟接口请求 this.rawList await new Promise((resolve) { setTimeout(() { resolve([ { id: 1, name: 张三, email: zhangsanexample.com }, { id: 2, name: 李四, email: lisiexample.com }, // 更多数据... ]); }, 500); }); this.loading false; }, handleSearch() { this.fetchList(); }, handlePrev() { if (this.page 1) this.page--; }, handleNext() { if (this.page this.totalPage) this.page; }, }, mounted() { this.fetchList(); }, }; /script这段代码功能上没问题但是业务逻辑的分布在四个区块中keyword、page、rawList这些字段在data里filteredList和totalPage在computed里fetchList和handleSearch在methods里keyword的监听逻辑在watch里。实际上这里至少有两个独立的业务模块——搜索过滤和分页管理——它们的代码是互相穿插的。3.2 组合式API版本逐行拆解再看看组合式API下同一个功能怎么写template div input v-modelkeyword placeholder搜索用户 / button clickfetchList搜索/button table tr v-foritem in paginatedList :keyitem.id td{{ item.name }}/td td{{ item.email }}/td /tr /table button clickprevPage :disabledpage 1上一页/button span{{ page }} / {{ totalPage }}/span button clicknextPage :disabledpage totalPage下一页/button p v-ifloading加载中.../p /div /template script setup import { ref, computed, watch, onMounted } from vue; // 搜索模块 const keyword ref(); const rawList ref([]); const loading ref(false); const filteredList computed(() { if (!keyword.value.trim()) return rawList.value; const kw keyword.value.toLowerCase(); return rawList.value.filter((item) item.name.toLowerCase().includes(kw) ); }); watch(keyword, () { page.value 1; }); async function fetchList() { loading.value true; rawList.value await new Promise((resolve) { setTimeout(() { resolve([ { id: 1, name: 张三, email: zhangsanexample.com }, { id: 2, name: 李四, email: lisiexample.com }, // 更多数据... ]); }, 500); }); loading.value false; } onMounted(fetchList); // 分页模块 const page ref(1); const pageSize ref(10); const paginatedList computed(() { const start (page.value - 1) * pageSize.value; return filteredList.value.slice(start, start pageSize.value); }); const totalPage computed(() Math.max(1, Math.ceil(filteredList.value.length / pageSize.value)) ); function prevPage() { if (page.value 1) page.value--; } function nextPage() { if (page.value totalPage.value) nextPage; } /script这段代码最大的变化是搜索模块和分页模块的逻辑在物理位置上分开了各有各的数据、计算属性和方法。如果你以后要单独把分页逻辑抽出去给别的组件用直接把这十几行代码剪贴到一个usePagination函数里就行几乎不用改逻辑。3.3 为什么说组合式API更利于读和改有人可能会说这不就是重新排了一下代码顺序吗功能不是一样的吗表面上看确实如此但在实际编码过程中这个重新排序的价值非常大。以我的经验来看代码的阅读时间一般是编写时间的三到五倍。在Options API里读一个功能你要同时关注四个区块大脑需要建立一张隐性的映射表。而在组合式API里读一个功能只需要顺着从上到下读一遍就行——这个模块的数据、逻辑、界面反馈全在这一块区域里。对于后续改动组合式API的优势更明显。比如项目里后来又要加一个按用户角色筛选的条件。在Options API里你得在data里加一个roleFilter字段、在methods里调整筛选逻辑、在computed里修改filteredList的依赖、可能还要在watch里加一条监听。来回折腾四个地方。而在组合式API里大概率只需要在过滤逻辑那里加几行代码就够了。4. 现在开始实操Options API迁移到组合式API的关键路径4.1 迁移第一步先搭好Vue环境确认版本如果你还没有把项目升级到Vue 3那组合式API暂时是体验不到的。检查一下你的项目的package.json确认Vue版本。如果还在Vue 2就需要先考虑升级。Vue 3的环境搭建其实很简单我一般直接用官方推荐的构建工具npm create vuelatest这条命令会引导你创建一个基于Vite的Vue 3项目默认就支持script setup语法。如果是已有项目升级我建议分两步走先把构建工具链Vite或者Webpack的Vue 3插件升级到位再逐个组件迁移。Vue 3的Options API是兼容Vue 2的写法的所以即使你暂时不想用组合式API升级到Vue 3后你的代码依然能跑——这给了我们很大的缓冲空间。4.2 写第一个composable把独立逻辑抽出来我的迁移经验是不要一上来就大改特改找一个逻辑相对独立的组件开始把它的一块业务逻辑抽成composable。拿用户列表页举例可以写一个useUserListimport { ref, computed, onMounted } from vue; export function useUserList() { const keyword ref(); const rawList ref([]); const loading ref(false); const page ref(1); const pageSize ref(10); const filteredList computed(() { if (!keyword.value.trim()) return rawList.value; const kw keyword.value.toLowerCase(); return rawList.value.filter((item) item.name.toLowerCase().includes(kw) ); }); const paginatedList computed(() { const start (page.value - 1) * pageSize.value; return filteredList.value.slice(start, start pageSize.value); }); const totalPage computed(() Math.max(1, Math.ceil(filteredList.value.length / pageSize.value)) ); async function fetchList() { loading.value true; // 模拟接口请求 rawList.value await new Promise((resolve) { setTimeout(() { resolve([ { id: 1, name: 张三, email: zhangsanexample.com }, { id: 2, name: 李四, email: lisiexample.com }, ]); }, 500); }); loading.value false; } function prevPage() { if (page.value 1) page.value--; } function nextPage() { if (page.value totalPage.value) page.value; } onMounted(fetchList); return { keyword, loading, paginatedList, totalPage, page, fetchList, prevPage, nextPage, }; }然后在组件里使用script setup import { useUserList } from /composables/useUserList; const { keyword, loading, paginatedList, totalPage, page, fetchList, prevPage, nextPage, } useUserList(); /script模板部分几乎不用改。关键的是useUserList这个函数变成了一个可复用的模块。以后项目里任何一个页面需要列表搜索分页的能力直接import这个函数就行了完全是复制粘贴自己的代码的升级版。4.3 生命周期和this的替代方案setup上下文很多人在迁移时最不习惯的是生命周期钩子和this的差异。在script setup里你是拿不到this的——因为setup函数根本不在组件实例上执行。但绝大多数原本依赖this.xxx的场景都可以通过在setup中直接使用响应式变量和函数来解决。生命周期钩子的变化很简单mounted变成了onMountedcreated直接不需要了因为setup本身就是在实例创建过程中执行。beforeUnmount对应onBeforeUnmountunmounted对应onUnmounted。还有watch变成了可单独导入的函数computed也变成了可单独导入的API。那this.$router、this.$route、this.$store怎么办Vue 3里用useRouter()、useRoute()、useStore()如果是Pinia则是useXxxStore()替换。这些都是组合式API里的辅助函数用法上其实就是从一个全局的上下文里取东西比之前的this.$xxx更直观也更利于TypeScript类型推导。4.4ref和reactive怎么选一条踩出来的经验组合式API里有两个创建响应式数据的基础APIref和reactive。很多新手在这里会纠结我一开始也纠结了很久。实际经验是ref适用于所有基本类型和需要整体替换的场景比如keyword是字符串page是数字用ref最合适。操作的时候要通过.value访问。reactive适用于对象、数组这类嵌套数据且你经常要操作内部字段的场景。不过reactive在解构时会丢失响应式所以如果你要解构出来用最好还是用ref。我个人的习惯是默认都用ref除非遇到一个明确的、内部字段经常被单独修改的深层对象才用reactive。ref有个好处是强制你通过.value来访问某种程度上也在提醒你这是一个响应式变量心智负担反而更小。补充一个2026年很实用的经验如果是在列表类数据上ref([])在替换整个数组时非常方便——直接list.value newArray就能触发更新。而如果用reactive管理数组替换整个数组时要小心直接赋值可能不会触发响应式更新需要用splice或者整体替换的方式。这个坑我踩过不止一次。5. 组合式API实战路由、视频播放、状态管理多场景应用5.1 组合式API Vue Router把路由逻辑也组合起来Vue Router 4在Vue 3里已经是一个标准的组合式API风格的库了。以前在Options API里获取路由参数是这样this.$route.params.id在组合式API里是这样script setup import { useRoute, useRouter } from vue-router; import { watch } from vue; const route useRoute(); const router useRouter(); const id route.params.id; // 监听路由参数变化重新拉取数据 watch( () route.params.id, (newId) { fetchDetail(newId); } ); function goBack() { router.back(); } /script一个跟路由相关的业务逻辑参数解析、参数变化监听、跳转操作在组合式API下天然地聚合在了一起。而且如果你有多个页面都需要根据路由参数拉取数据的逻辑可以直接抽一个useRouteDetail出来。这比在Options API里每个组件都重复写一遍watchmounted要优雅得多。5.2 组合式API 视频播放封装一个m3u8播放器逻辑这几年我在项目里经常遇到视频播放的需求尤其是m3u8流媒体格式。很多朋友一上来就引入video.js这种大而全的库其实完全可以用组合式API把这套逻辑封装成自己的composable。先说一个简单的背景m3u8是基于HTTP Live Streaming协议的视频流格式浏览器原生video标签不支持直接播放需要在JavaScript层做一层转换。社区常用hls.js来处理。组合式API很适合做这种有状态、有生命周期、有副作用的封装// useHlsPlayer.js import { ref, onMounted, onBeforeUnmount, watch } from vue; import Hls from hls.js; export function useHlsPlayer(videoElement, src) { const playing ref(false); const error ref(null); let hls null; async function init() { if (!videoElement.value || !src.value) return; // 如果浏览器原生支持HLS比如Safari直接用原生播放 if (videoElement.value.canPlayType(application/vnd.apple.mpegurl)) { videoElement.value.src src.value; } else if (Hls.isSupported()) { hls new Hls(); hls.loadSource(src.value); hls.attachMedia(videoElement.value); hls.on(Hls.Events.ERROR, (_, data) { error.value data; }); } else { error.value new Error(当前环境不支持HLS播放); } } watch(src, () { if (hls) { hls.destroy(); hls null; } init(); }); onMounted(init); onBeforeUnmount(() { if (hls) { hls.destroy(); hls null; } }); return { playing, error }; }这个composable封装了m3u8播放的全部核心逻辑初始化、兼容性判断、错误捕获、资源切换、销毁释放。使用它的组件代码非常干净template div video refvideoRef controls/video p v-iferror{{ error.message }}/p /div /template script setup import { ref } from vue; import { useHlsPlayer } from /composables/useHlsPlayer; const videoRef ref(null); const src ref(https://example.com/path/to/playlist.m3u8); const { playing, error } useHlsPlayer(videoRef, src); /script这里有个细节值得注意videoRef和src都用了ref这样在composable内部就可以用watch来响应它们的后续变化——比如用户切换了视频源播放器会自动销毁旧的Hls实例重新初始化新的实例。这比在Options API里手动调用播放器实例的方法要简洁可靠得多。5.3 组合式API 状态管理Pinia是天然搭档回到2026年Vue社区的状态管理方案已经基本统一到Pinia了。Pinia本身就是围绕组合式API设计的它的store定义方式跟composable几乎没有区别// stores/user.js import { defineStore } from pinia; import { ref, computed } from vue; export const useUserStore defineStore(user, () { const name ref(); const role ref(guest); const isAdmin computed(() role.value admin); async function login(username) { name.value username; role.value admin; } function logout() { name.value ; role.value guest; } return { name, role, isAdmin, login, logout }; });在组件中使用时script setup import { useUserStore } from /stores/user; const userStore useUserStore(); /script注意Pinia store的响应式数据在解构后会丢失响应式所以必须用storeToRefs来解构才能保持响应式import { storeToRefs } from pinia; const { name, role } storeToRefs(userStore);这是我在实际项目里踩过的坑之一。刚开始以为store解构出来就能用结果发现UI不更新查了半天才发现是解构导致响应式丢失。用storeToRefs之后就好了。5.4 面试题视角组合式API的高频考点这些年帮团队面了不少前端候选人发现组合式API已经成了Vue面试必问的方向。常见的几个高频题目我建议你在学的时候就要有意识地准备Options API和组合式API的区别是什么核心考点是逻辑聚合、复用方式、类型推导这三个维度而不是背定义。能结合一个具体的逻辑复杂组件说明就更好了。ref和reactive怎么选面试官真正想听的是你对响应式原理的理解而不只是使用偏好。能说出ref基于value的包装、reactive基于Proxy、以及解构后响应式丢失的原因基本都是加分项。composable的设计原则有哪些这里可以聊聊单一职责、命名以use开头、内部状态对外隔离、返回对象避免过度解构等。能结合自己项目里抽过的composable来谈会比纯理论加分很多。6. 组合式API的坑和排查技巧真实踩过的路6.1ref和reactive解构后响应式丢失这个问题前面提过但我想再展开一下因为它太常见了。很多从Options API转过来的人会习惯性地写const state reactive({ name: , age: 0 }); const { name, age } state; // 丢掉响应式然后发现模板里改变name页面不更新。原因很简单reactive返回的是Proxy对象解构出来的是原始值的拷贝不是Proxy代理。解决办法是用toRefs把reactive对象里的每个属性变成refimport { reactive, toRefs } from vue; const state reactive({ name: , age: 0 }); const { name, age } toRefs(state);这样name和age都是ref了在模板里自动解包在JavaScript里需要通过.value访问。我用下来的建议是如果要在setup里解构就直接用ref定义如果不解构整个对象用reactive也行。千万不要在解构和不解构之间反复横跳代码风格要统一。6.2watch的坑监听ref对象和reactive对象的方式不一样watch监听ref的时候直接传ref变量就行const keyword ref(); watch(keyword, (newVal, oldVal) { console.log(keyword changed:, newVal, oldVal); });监听reactive对象的某个属性时要传一个getter函数const state reactive({ keyword: }); watch( () state.keyword, (newVal, oldVal) { console.log(keyword changed:, newVal, oldVal); } );这个区别看起来不大但是如果你把reactive对象的属性直接传给watch它是不会正常触发的。另外watch默认是懒执行的不会立即运行回调。如果你需要进入页面时先执行一次回调要加immediate: true。这是我在项目中经常用到的一个小技巧。注意watchEffect和watch的区别也要搞清楚。watchEffect会自动追踪回调里用到的所有响应式依赖依赖变化时立即重新执行且默认立即执行一次。适合根据多个状态做副作用的场景。而watch适合监听某一个具体状态变化再做操作的场景。我一般70%的场景用watch30%用watchEffect。6.3script setup中组件自引用和递归组件的坑在script setup里组件默认是局部注册的不需要再在components选项里注册了——这是好事省了很多样板代码。但有一个坑如果组件要递归引用自己直接在模板里写组件名是可以的因为Vue会自动把文件名推断成组件名。如果你在项目里开了ESLint的vue/multi-word-component-names规则可能单文件组件名只能有一个词的情况下会报错需要调整规则或者重命名。6.4 组合式函数中副作用清理必须养成习惯Composable里面如果注册了全局事件监听、定时器、WebSocket链接等副作用一定要在组件卸载时清理。Options API时代你在beforeDestroy里手动清组合式API里对应的是onBeforeUnmount。但更优雅的方式是直接在composable内部用onScopeDispose——这个API能在组合式函数的作用域被销毁时自动调用清理逻辑不需要外部组件显式去处理。看一个例子// useCountdown.js import { ref, onScopeDispose } from vue; export function useCountdown(initial 60) { const count ref(initial); const timer setInterval(() { if (count.value 0) count.value--; }, 1000); onScopeDispose(() { clearInterval(timer); }); return { count }; }这样用useCountdown的组件完全不需要关心清理逻辑组件卸载时定时器自动被清除。这是组合式API里一个非常实用但很多人没注意到的API。6.5 排查组合式API里模板渲染不更新的通用思路如果遇到数据变了但模板不更新的情况按这个顺序排查确认变量是ref或reactive创建的而不是普通变量。确认在script setup中通过return或者在setup函数中暴露给了模板。检查是否对reactive对象进行了解构解构后响应式丢失。检查是否有非响应式的外部数据混入了响应式数据。最后检查是否在同一事件循环中同步修改了多个响应式变量有时需要nextTick配合。7. 给还在观望的朋友组合式API是趋势也是基本功最后再说一点个人体会。我见过很多开发者在Vue 3发布了好几年之后依然坚持用Options API理由通常是习惯了项目已经这样了不想折腾。我完全理解这种心态因为我自己也经历过。但2026年这个时间节点上我的建议是如果你还在写Vue组合式API不是新技能而是基本功。原因有三第一Vue 3生态里越来越多的库和工具默认使用组合式API你不熟悉就看不懂别人的代码。第二团队协作中组合式API的代码结构更统一逻辑边界更清晰code review成本明显低。第三从职业发展的角度面试几乎必问组合式API已掌握和能熟练讲解是两个不同的层次。从一个实际的迁移路径来看你不必一次性把整个项目推翻重写。可以先从新功能开始新写的组件一律用script setup然后挑一两个复杂度适中的老组件把独立的逻辑抽成composable。这样慢慢过渡你会发现自己回过头来再也不想用Options API写新组件了。代码是给人读的而组合式API让代码的组织方式更贴合人的思维习惯这大概就是它被称为正确姿势的原因。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门