Code前端首页关于Code前端联系我们

Vue3还能用mixin吗?mixins和Composition API谁更好?踩坑指南

terry 3天前 阅读数 769 #Vue

很多人上手Vue3之后,第一反应就是“官方都推Composition API了,mixin是不是彻底凉了?”甚至不少新教程上来直接跳过mixin,只讲setup和hooks,但实际上,在Vue3的官方文档里,Options API依然支持mixin,连组合式API也保留了类似功能的封装思路——不过要换个叫法,今天咱们就拆碎了讲清楚:Vue3里mixin的现状、适用场景、和Composition API/hooks的区别,以及曾经踩过的Vue2/Vue3通用mixin坑。

mixin在Vue3里真的被抛弃了吗?

先给个准话:没有被完全废弃,只是被官方“降级”为非推荐方案,但代码照样跑,兼容性也没问题——只要你用的是Vue3的Options API或者是在组合式API的setup里,通过一些手段间接复用类似逻辑(不过不建议再硬套mixin的写法到组合式API里)。

为什么官方不推荐了?因为Vue2时代mixin暴露的核心问题——命名冲突、来源不明、逻辑耦合——在Options API里还是没解决,官方文档甚至直接给了替代建议:优先用组合式函数(也就是大家常说的Vue3 hooks),如果必须跨组件复用Options API的逻辑,再考虑用mixin或者更严格的extends

但这不代表mixin没有用武之地,举个实际例子:你维护的是一个用Vue2写的老电商项目,现在要升级到Vue3做渐进式重构,这个时候如果把所有复用的登录状态、路由守卫拦截提示都改成hooks,工程量太大,还容易出问题,这时候保留部分成熟稳定、没有冲突的mixin,慢慢用组合式函数替换,才是最稳妥的做法。

mixin和Composition API/hooks到底怎么选?

很多人会把mixin和组合式函数直接对比,但其实它们本质上是解决“代码复用”问题的两种不同思路:mixin是“横向扩展组件的Options对象”,组合式函数是“把可复用的逻辑拆成独立的函数,按需引入”,咱们从四个维度对比一下,你就能直接知道什么时候选哪个了。

代码的来源清晰度

这是mixin最被吐槽的点,也是组合式函数最大的优势。

比如你在Vue3的Options API里用了3个mixin:userMixincartMixinrouteMixin,每个都注册了data、methods、computed,当你在组件模板里看到{{ userName }}或者调用handleAddCart()的时候,根本不知道这个变量/方法是来自哪个mixin,还是组件本身定义的,维护的时候要翻3个mixin文件甚至更多,效率极低。

而用组合式函数就不一样了:你可以import { useUser, useCart, useRouteGuard } from '@/composables',然后在setup里明确地写const { userName, isLogin } = useUser()const { cartList, handleAddCart } = useCart(),调用的时候一眼就能看到来源,甚至可以用别名重命名冲突的变量,比如const { userName: buyerName } = useBuyerUser(),灵活度很高。

命名冲突的解决能力

刚才提到了别名,这是组合式函数解决冲突的核心方法,而mixin解决冲突的方式非常“粗暴”:如果是data、methods、computed,后引入的mixin会覆盖先引入的;如果是生命周期钩子,会按引入顺序依次执行。

举个踩过的Vue2升级Vue3的坑:老项目里有两个mixin,loadingMixin(有data里的isLoading,methods里的showLoading/hideLoading)和orderListMixin(刚好也有自己的isLoading,而且是后引入的),升级到Vue3之后,两个mixin的data合并逻辑没变,结果商品列表的加载状态被订单列表的覆盖了,导致商品页面一直显示“加载中”,排查了半天才发现是冲突的问题——因为代码维护了两年,后来加的mixin谁都没注意到重名了。

如果用组合式函数的话,这个问题直接就不存在了:两个逻辑里的isLoading是独立的响应式变量,你可以分别叫isGoodsLoadingisOrderLoading,完全不会互相影响。

逻辑的耦合度

mixin的另一个问题是“隐形的依赖”:比如cartMixin里可能依赖了userMixin里的userId,但是在引入的时候,你必须记得先引userMixin,再引cartMixin,不然userId就是undefined,项目直接报错,而且mixin之间的依赖关系很难梳理,项目越大,问题越严重。

组合式函数就可以明确地管理依赖:你可以在useCart里直接引入useUserconst { userId } = useUser(),不需要在使用组件的时候关心两个逻辑的引入顺序,组合式函数内部自己处理好了,这种“依赖内聚”的写法,逻辑更清晰,也更容易测试——你可以单独给useCart传一个模拟的userId来测试,不用依赖整个组件。

树形摇亲(Tree Shaking)的支持

现在的前端项目都很看重打包体积,树形摇亲就是打包工具帮你把没用到的代码“摇掉”的功能,组合式函数对树形摇亲的支持非常好:如果你只引入了useCart里的cartList,没用到handleAddCart,打包工具可能会把handleAddCart摇掉(当然要看具体的函数写法,尽量不要有副作用)。

而mixin做不到这一点:只要你引入了一个mixin,不管你用没用它里面的data、methods、computed,整个mixin的代码都会被打包进去,增加了项目的体积。

看到这里,你应该已经有答案了:

  • 新项目/新功能:100%优先用组合式函数,不要用mixin;
  • Vue2升级Vue3的老项目:成熟稳定、没有冲突、依赖简单的mixin可以保留,但是新功能一定要用组合式函数,慢慢替换老的mixin;
  • 必须跨组件复用Options API逻辑的特殊场景:可以用mixin或者更严格的extendsextends只能继承一个组件的Options,冲突风险比mixin小一点)。

Vue2/Vue3通用的mixin踩坑指南

如果你现在不得不用mixin(比如老项目重构),这几个坑一定要避开,都是血的教训。

强制命名规范,减少冲突风险

虽然命名规范不能完全避免冲突,但能大大降低概率。

  • 给mixin里的data、methods、computed加上统一的前缀,比如userMixin里的变量叫mixin_userName,方法叫mixin_handleLogin
  • 不要在mixin里定义通用的名字,比如isLoadinghandleClick,这些太容易重名了。

严格限制mixin的功能范围

一个mixin最好只做一件事,比如userMixin只负责登录状态和用户信息,loadingMixin只负责全局/局部的加载状态,不要把登录、购物车、路由拦截都塞到一个mixin里,不然逻辑耦合会爆炸。

生命周期钩子的执行顺序要记牢

不管是Vue2还是Vue3,mixin的生命周期钩子都是按引入顺序执行的,然后才是组件本身的生命周期钩子,比如你在组件里先引loadingMixin(有created),再引userMixin(有created),然后组件自己也有created,执行顺序就是:loadingMixin.createduserMixin.created组件.created

升级Vue3的时候要注意一点:Vue2的beforeDestroydestroyed在Vue3里改名为beforeUnmountunmounted,如果你老项目的mixin用了旧的钩子名,在Vue3里会被自动映射,但建议还是手动改成新的,避免以后版本升级出问题。

不要在mixin里修改组件的props或者其他mixin的data

虽然技术上可以做到,但这是非常糟糕的做法:会导致数据的流向不清晰,不知道是谁修改了数据,排查bug的时候会非常痛苦,如果mixin需要修改组件的props,应该通过emit触发组件的事件,让组件自己处理;如果mixin之间需要共享数据,应该用Vuex/Pinia(全局状态)或者Provide/Inject(局部跨层级状态),不要直接互相修改。

尽量用纯函数式的mixin

虽然mixin里可以有副作用(比如直接修改DOM、调用API),但尽量把副作用放到组件本身或者组合式函数里,mixin只提供纯函数或者响应式数据的封装,这样mixin的复用性更高,也更容易测试。

有没有办法在组合式API里用类似mixin的逻辑?

如果你习惯了mixin的“横向扩展”思路,但又想享受组合式函数的好处,其实可以用组合式函数的“组合”特性来模拟,比如你可以把多个小的组合式函数合并成一个大的“逻辑块”,然后在组件里一次性引入:

// composables/useProductDetail.js
import { useUser } from './useUser'
import { useCart } from './useCart'
import { useRouteGuard } from './useRouteGuard'
import { ref, onMounted } from 'vue'
export function useProductDetail(productId) {
  // 组合其他组合式函数
  const { userId, isLogin } = useUser()
  const { handleAddCart, cartCount } = useCart()
  const { requireLogin } = useRouteGuard()
  // 自己的逻辑
  const productDetail = ref(null)
  const getProductDetail = async () => {
    // 调用API获取商品详情
  }
  onMounted(() => {
    getProductDetail()
  })
  // 对外暴露的变量和方法
  return {
    userId,
    isLogin,
    handleAddCart,
    cartCount,
    requireLogin,
    productDetail,
    getProductDetail
  }
}

然后在组件里:

import { useRoute } from 'vue-router'
import { useProductDetail } from '@/composables/useProductDetail'
export default {
  setup() {
    const route = useRoute()
    const { productId } = route.params
    const { productDetail, handleAddCart, isLogin, requireLogin } = useProductDetail(productId)
    // 这里可以直接用,来源也很明确
    return {
      productDetail,
      handleAddCart,
      isLogin,
      requireLogin
    }
  }
}

这种写法既保留了组合式函数的所有优势,又有点像mixin的“横向扩展”,不过比mixin更清晰、更灵活,是官方推荐的做法。

写在最后

Vue3里的mixin没有凉,但确实已经不是主流了,组合式函数的出现,完美解决了mixin的所有核心问题,不管是代码清晰度、冲突解决能力、逻辑耦合度,还是树形摇亲的支持,都比mixin强得多。

如果你现在正在做新项目,直接跳过mixin学组合式函数就好;如果你正在维护老项目,也不用急着把所有mixin都换掉,慢慢用组合式函数替换就行,技术选型从来没有“最好”,只有“最适合”——适合你的项目场景、适合你的团队技术栈的,才是最好的。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门