为什么要用composition API?
业务项目中的 mixin 代码逻辑繁杂,开发维护成本高,亟待重构, vue3 composition API 是 解决 mixin 现有问题的方案之一;
出于长远考虑,vue3 稳定后,项目也会逐步迁移到 vue3 版本(毕竟咱们都是技术的弄潮儿 ???? ),提前迁移部分功能也是在为后续的迁移做准备。
本文重点关注 composition API 改造 vue2 项目中的mixin,API的使用请参考 vue3官方文档-高阶指南-组合式API(https://v3.cn.vuejs.org/guide/composition-api-introduction.html)。
composition API,也叫做组合式API,它可以将同一个逻辑关注点相关的代码配置在一起,能够解决大型组件因选项分离导致的碎片化问题、降低代码复杂度和定位单一逻辑问题的难度。
做了一夜动画,就为让大家更好的理解Vue3的Composition Api。
![]()
为了形象理解 composition API,在这里推荐大帅老师的文章
(https://juejin.cn/post/6890545920883032071)
渲染上下文中使用的属性来源不清晰。(例如在阅读一个运用了多个 mixin 的模板时,很难看出某个属性是从哪个 mixin 中注入的)
命名空间冲突。(mixin 之间的属性和方法可能有冲突)
暴露给模板的属性来源十分清晰,因为它们都是被组合逻辑函数返回的值。
不存在命名空间冲突,可以通过解构任意命名
不需要为逻辑复用创建组件实例
仅依赖它的参数和 Vue 全局导出的 API,而不依赖 this 上下文
这里使用提供了 composition API 的vue2的插件, @vue/composition-api。
项目中安装 @vue/composition-api
npm install @vue/composition-api
# or
yarn add @vue/composition-api
项目中使用
import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api'
Vue.use(VueCompositionAPI)
这里先是选择了一段自认为比较简单的 mixin 进行改造,事实证明,实践才是检验真理的唯一标准,哈哈哈。。。
// placeOrderPlanMixin.js
import { getPreShuntTestAjax } from '@/apiData/helpsale'
export default {
data() {
return {
placeOrderPlan: 'D'
}
},
created() {
this.fetchPreShunt()
},
methods: {
// 获取订单预分流方案
fetchPreShunt() {
const { cateId } = this.$route.query || {}
getPreShuntTestAjax()
.then((res) => {
this.placeOrderPlan = res
})
.catch((e) => {
console.log(e)
})
.finally(() => {
this.$lego(
{
actiontype: 'bm-h2-place-order-preshunt',
cateId,
orderPlanType: this.placeOrderPlan
},
false
)
})
}
}
}
将上面 mixin 代码改造,抽离到 composables 文件夹下 placeOrderPlan.js文件中
// composables/placeOrderPlan.js
import { getPreShuntTestAjax } from '@/apiData/helpsale'
import { ref, onMounted } from '@vue/composition-api'
export default function() {
let placeOrderPlan = ref('D')
const fetchPreShunt = () => {
const { cateId } = this.$route.query || {}
getPreShuntTestAjax()
.then((res) => {
placeOrderPlan.value = res
})
.catch((e) => {
console.log(e)
})
.finally(() => {
this.$lego(
{
actiontype: 'bm-h2-place-order-preshunt',
cateId,
orderPlanType: placeOrderPlan.value
},
false
)
})
}
onMounted(() => {
fetchPreShunt()
})
return {
placeOrderPlan
}
}
vue组件中引用
import placeOrderPlan from '@/app/help-sale/composables/placeOrderPlan.js'
export default {
setup(props, context) {
const { placeOrderPlan } = placeOrderPlan()
return {
placeOrderPlan
}
}
}
保存运行,这里会看到浏览器控制台报错,因为 setup 里面是访问不到 this 的,也就是说我们无法通过 this 访问到 router 和 vuex 中的属性和方法,于是疑问点来了 ????
google了一下,基本上就是需要升级 vue-router@4 ,vuex,升级会有哪些坑点,感觉可以再花费一段时间来研究下了。
// 升级vue-router@4, vuex后的用法,举个栗子
import {useRouter} from 'vue-router'
import {useRoute} from 'vue-router'
import {useStore} from 'vuex'
export default {
setup() {
const router = useRouter()
const route = userRoute()
const store = userStore()
onMounted(() => {
store.dispatch('changeName', route.query.name)
})
const jumpUrl = () => {
router.push({
path:'/index',
query:route.query
})
}
return {
jumpUrl
}
}
}
当然也可以通过setup的props来获取 query 参数,但是需要在使用到setup的组件中都传入 query 属性,改造成本高了不少。
// 组件中
import placeOrderPlan from '@/app/help-sale/composables/placeOrderPlan.js'
export default {
props: {
query:{}, // 这里传入query
},
setup(props) {
const { placeOrderPlan } = placeOrderPlan(props) // 这里传入props
return {
placeOrderPlan
}
}
}
// composables/placeOrderPlan.js
import { getPreShuntTestAjax } from '@/apiData/helpsale'
import { ref, onMounted } from '@vue/composition-api'
export default function(props) {
let placeOrderPlan = ref('D')
const fetchPreShunt = () => {
// const { cateId } = this.$route.query || {}
// 这里就可以访问到组件的props参数
const { cateId } = props.query || {}
// ... 此处省略一万行代码
}
onMounted(() => {
fetchPreShunt()
})
return {
placeOrderPlan
}
}
这里出于好奇,在组件中打印了下 props,context,
// 组件中
import placeOrderPlan from '@/app/help-sale/composables/placeOrderPlan.js'
export default {
props: {
query:{},
},
setup(props, context) {
console.log('props: ', props)
console.log('context: ', context)
const { placeOrderPlan } = placeOrderPlan(props) // 这里传入props
return {
placeOrderPlan
}
}
}
可以看到,props 里确实可以取到组件传入的属性 query
但是这个 context 貌似提供的属性跟我在官网上看到的不太一样啊,官网上明明说的三个 property,并没有提到parent 和 root 这两个属性,为什么这里特殊提到 parent 和 root 属性,因为从打印结果看 root 完全就相当于暴露了 this,我可以通过 root/parent 属性,去访问到组件现有的 mixin 等数据
为了确认官网文档是不是少写了,也是出于好奇心,我初始化了个 vue3 的项目(当然这里正确的打开方式应当去扒源码,我偷个懒 ???? ),打印后,果然官网骗了我,暴露的不止3个属性,但是确实也没有 root 和 parent 属性。
也就是说我们当前引入@vue/composition-api改造项目后,将来迁移到 vue3 是要直接换成官方 vue@3 正式包的,通过 root 和 parent 调用方法是不可取的,直接替换就会有报错。
而 @vue/composition-api 文档上提到的可以直接迁移 vue@3 包还是有风险的,除非我们严格不使用 root 和 parent 属性
我们暂且忽略root和parent属性的坑点,保存执行,会看到浏览器里还在报错,
// composables/placeOrderPlan.js
import { getPreShuntTestAjax } from '@/apiData/helpsale'
import { ref, onMounted } from '@vue/composition-api'
export default function(props) {
let placeOrderPlan = ref('D')
const fetchPreShunt = () => {
const { cateId } = props.query || {}
getPreShuntTestAjax()
.then((res) => {
placeOrderPlan.value = res
})
.catch((e) => {
console.log(e)
})
.finally(() => {
this.$lego( // 这里报错
{
actiontype: 'bm-h2-place-order-preshunt',
cateId,
orderPlanType: placeOrderPlan.value
},
false
)
})
}
onMounted(() => {
fetchPreShunt()
})
return {
placeOrderPlan
}
}
我们定位到 $lego 在全局 mixin 方法中,因为setup中不支持访问this,挂在在this上的全局mixin方法该如何访问呢?问题又来了????
网上大家都是在讲 composition API 如何替代 mixin,难道全局的 mixin 也要替换成 composition API,然后在所有组件中引入?显然这种情况已经不适于改造成 compostion API,如果改造成工具函数呢,再看看我们的 $setCommonBackup 方法里的逻辑,获取埋点基础参数,这么多个绑定在 this 上的参数,需要通过传参的方式进行传入,改造成本巨大,至此,改造当前 mixin 的过程就此终止了 ????
// utils/mixin.js 全局mixin
export default {
methods: {
$lego({ actiontype, ...rest }, isClick = true) {
const type = isClick ? '__CLICK' : '__SHOW'
actiontype = actiontype.toUpperCase() + type
const urlRouterName = (this.$route && this.$route.name) || 'u-bmmain'
const backup = { ...rest, ...this.$setCommonBackup() } // 这里调用$setCommonBackup获取基础参数
lego.send({
actiontype,
backup
})
},
$setCommonBackup() {
const { name = '', query = {} } = this.$route || {}
// 此处省略一万行...
const logsMark = `U_BM-Main_${name}`
const urlRouterName = name || 'u-bmmain'
// 这里访问this上挂在的$route.query
const uFrom = this.$route.query.uFrom || ''
const cateId = this.$route.query.cateId || ''
const servicefrom = this.$route.query.servicefrom || ''
// 这里访问this上的方法
let planType = this.$ABPlanType()
const params = {
logsMark,
urlRouterName,
channel,
channnelSouce,
uFrom,
pageCateId: cateId,
planType,
servicefrom
}
// 这里访问this上的属性
if (this.cateId) {
Object.assign(params, { cateId: this.cateId })
}
// 这里访问this上的方法
if (this.$bmFrom()) {
Object.assign(params, { bmFrom: this.$bmFrom() })
}
return params
}
}
}
不抛弃,不放弃???? ,还是选了个真正简单的例子改造了一下。
有多简单
没有使用 vue-router,vuex 的场景
没有嵌套 mixin 或调用全局 mixin 的场景
也没有 watch,computed 的场景
// correlate-mixin/jumpEvaPlan.js
import { getCommonABTestAjax } from '@/apiData/common.js'
export default {
data() {
return {
evaluateType: ''
}
},
created() {
this.getEvaABTestChannel()
},
methods: {
// 获取估价页AB测跳转具体页面
getEvaABTestChannel() {
getCommonABTestAjax({
testId: 10171
})
.then((res) => {
this.evaluateType = res
})
.catch((e) => {
console.log('e: ', e)
})
},
getEvaRouteName(type) {
const evaluateType = type || this.evaluateType
let routeName = ''
switch (evaluateType) {
case 'D':
routeName = 'helpsale-evaluate-Dplan'
break
case 'B':
routeName = 'helpsale-evaluate-Bplan'
break
case 'C':
routeName = 'helpsale-evaluate-Cplan'
break
default:
routeName = 'helpsale-evaluate'
break
}
return routeName
}
}
}
功能抽象到 composables/getCommonABTest.js
// composables/getCommonABTest.js
import { getCommonABTestAjax } from '@/apiData/common.js'
import { ref, onMounted } from '@vue/composition-api'
export default function getABTest(testId) {
let planType = ref('')
const getCommonABTest = () => {
getCommonABTestAjax({
testId
})
.then((res) => {
planType.value = res
})
.catch((e) => {
console.log('e: ', e)
})
}
onMounted(() => {
getCommonABTest() // 接口请求
})
return {
planType // 对外暴露的响应式属性
}
}
原 mixin 引入的组件,都需要加上 setup
// fastType/index.vue
import getABTest from '@/app/help-sale/composables/getCommonABTest.js'
export default {
setup(props) {
const { planType } = getABTest(10171)
// 假如一个页面有多个AB测
const res = getABTest(123)
const res2 = getABTest(456)
return {
evaluateType: planType,
test: res.planType,
test2: res2.planType
}
}
}
可能细心的童鞋会发现原 mixin 中???? 这坨代码去哪里了
getEvaRouteName(type) {
const evaluateType = type || this.evaluateType
let routeName = ''
switch (evaluateType) {
case 'D':
routeName = 'helpsale-evaluate-Dplan'
break
case 'B':
routeName = 'helpsale-evaluate-Bplan'
break
case 'C':
routeName = 'helpsale-evaluate-Cplan'
break
default:
routeName = 'helpsale-evaluate' // 默认估价A方案
break
}
return routeName
}
它被改造成公共函数了(实际上这个方法没有必要挂载在 this 上,但是通过 mixin 方式挂载到 this 上,兜底的 this.evaluateType 就不用传入了,改造后就需要各个调用的地方传入 this.evaluateType)
// utils/getEvaRouteName.js
const getEvaRouteName = (type) => {
// ???? 原来这里兜底的this.evaluateType也变成必传的了
// const evaluateType = type || this.evaluateType 此行废弃
let routeName = ''
switch (type) {
case 'D':
routeName = 'helpsale-evaluate-Dplan'
break
case 'B':
routeName = 'helpsale-evaluate-Bplan'
break
case 'C':
routeName = 'helpsale-evaluate-Cplan'
break
default:
routeName = 'helpsale-evaluate' // 默认估价A方案
break
}
return routeName
}
export default getEvaRouteName
// fastType/index.vue
import getEvaRouteName from '@/app/help-sale/utils/getEvaRouteName.js'
export default {
methods: {
navEvaluatePage() {
// 此处代码省略...
// const routename = this.getEvaRouteName() 之前的调用方式
const routename = getEvaRouteName(this.evaluateType)
// 此处代码省略...
}
}
保存代码,完美运行????????????
composition API 重构 vue2 mixin:
可以在不升级vue3的条件下,使用 @vue/composition-api,但是跟官方 vue3 正式包的 compositon API 提供的能力有出入(root,parent),强行使用不利于后续的 vue3 升级;
改造的代码涉及 vue-router,vuex 的相关操作需要升级 vue-router,vuex,升级带来的风险和踩坑点,有待尝试;
获取 query 通过 props 注入的方式也可以实现,但是让所用到的组件都传入 query,改造成本较高;
mixin 的逻辑面向组件,使用 composition API 需要改成面向功能,可能需要剥离 mixin 中功能+工具方法;
mixin 的改造,拆入到 setup 中的功能逻辑相对简单,但是其他绑定在this上的偏工具类的逻辑方法,如果不放到 setup 中(绑定到 this上),就需要单独抽离成业务工具方法,需要通过传参替代原来的 this.参数 的获取,带来的是相应调用地方的改造成本,尤其是用到的全局 mixin;
composition API 跟 vuex 对比,有点像是一个个拆出的小 store,那么 composition API 会替代 vuex 吗?参考 《你是否应该使用Composition API替代Vuex》?(https://zhuanlan.zhihu.com/p/320445941);
compostion API 的缺点:面条代码,可以查看《 简明扼要聊聊 Vue3.0 的 Composition API 是啥东东》(https://zhuanlan.zhihu.com/p/320445941);
思考:什么样的代码适合改造成(使用) composition API?
感谢你的阅读,有任何问题,欢迎评论区留言讨论,互相学习。