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
相關文章
相關標籤/搜索