composition API重构mixin实践

写在前面

为何要用composition APIjavascript

  • 业务项目中的 mixin 代码逻辑繁杂,开发维护成本高,亟待重构, vue3 composition API 是 解决 mixin 现有问题的方案之一。
  • 出于长远考虑,vue3 稳定后,项目也会逐步迁移到 vue3 版本(毕竟我们都是技术的弄潮儿 🤘 ),提早迁移部分功能也是在为后续的迁移作准备。

本文重点关注 composition API 改造 vue2 项目 mixin,API的使用还请参考 vue3官方文档-高阶指南-组合式APIhtml

1 composition API 简介

composition API,也叫作组合式API,它能够将同一个逻辑关注点相关的代码配置在一块儿,从而解决大型组件因选项分离致使的碎片化问题,下降代码复杂度和定位某个单一逻辑问题的难度。vue

为了形象理解 composition API,在这里推荐强烈推荐大帅老师的文章

作了一晚上动画,就为让你们更好的理解Vue3的Composition Apijava

2 composition API 对比 mixin

mixin缺点:

  • 渲染上下文中使用的属性来源不清晰。(例如在阅读一个运用了多个 mixin 的模板时,很难看出某个属性是从哪个 mixin 中注入的)
  • 命名空间冲突。(mixin 之间的属性和方法可能有冲突)

composition API优势:

  • 暴露给模板的属性来源十分清晰,由于它们都是被组合逻辑函数返回的值。
  • 不存在命名空间冲突,能够经过解构任意命名
  • 再也不须要仅为逻辑复用而建立组件实例
  • 仅依赖它的参数和 Vue 全局导出的 API,而不是依赖 this 上下文

3 如何在vue2项目中使用composition API

这里使用提供了 composition API 的vue2的插件, @vue/composition-apivue-router

  • 项目中安装 @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)
复制代码

4 尝试改造mixin---失败案例

4.1 踩坑之旅开启

这里先是选择了一段自认为比较简单的 mixin 进行改造,事实证实,实践才是检验真理的惟一标准,哈哈哈。。。vuex

// 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
          )
        })
    }
  }
}
复制代码

4.2 开始改造

将上面 mixin 代码改造,抽离到 composables 文件夹下 placeOrderPlan.js文件中npm

// 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组件中引用api

import placeOrderPlan from '@/app/help-sale/composables/placeOrderPlan.js'

export default {
  setup(props, context) {
    const { placeOrderPlan } = placeOrderPlan()
    return {
      placeOrderPlan
    }
  }
}
复制代码

保存运行,这里会看到浏览器中报错,由于 setup 里面是访问不到 this 的,也就是咱们没法经过 this 访问到 router 和 vuex 中的属性和方法,因而疑问点来了 🤔浏览器

4.3 setup里面访问route,router, vuex要怎么用呢

google了一下,基本上就是须要升级 vue-router@4 ,vuex,升级会有哪些坑点,感受能够再花费一段时间来研究下了。bash

// 升级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方法该如何访问,因而,问题又来了,🤔

4.4 怎么在setup中访问到全局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
      }
  }
}
复制代码

5 尝试改造mixin---成功案例

不抛弃,不放弃💪 ,仍是选了个真正简单的例子改造了一下。

有多简单

  • 没有使用 vue-router,vuex 的场景
  • 没有嵌套 mixin 或调用全局 mixin 的场景
  • 也没有 watch,computed 的场景
待改造mixin代码以下:
// 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)
      
   // 此处代码省略...

  }
}
复制代码

保存代码,完美运行👏👏👏

6 总结

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 APIvuex 对比,有点像是一个个拆出的小 store,那么 composition API 会替代 vuex 吗?你是否应该使用Composition API替代Vuex?
  • compostion API 的缺点:面条代码,能够查看 简明扼要聊聊 Vue3.0 的 Composition API 是啥东东!
  • 思考:什么样的代码适合改形成(使用) composition API