composition API重構mixin實踐

寫在前面

為什麼要用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)。

1.composition API 簡介

composition API,也叫做組合式API,它可以將同一個邏輯關注點相關的程式碼配置在一起,能夠解決大型套件因選項分離導致的碎片化問題、降低程式碼複雜度和定位單一邏輯問題的難度。

做了一夜影片,就為讓大家更好的理解Vue3的Composition Api。

為了形象理解 composition API,在這裡推薦大帥老師的文章

(https://juejin.cn/post/6890545920883032071)

2.composition API 對比 mixin

mixin缺點:

  • 渲染上下文中使用的屬性來源不清晰。(例如在閱讀一個運用了多個 mixin 的模板時,很難看出某個屬性是從哪個 mixin 中注入的)

  • 名稱空間衝突。(mixin 之間的屬性和方法可能有衝突)

composition API優點:

  • 暴露給模板的屬性來源十分清晰,因為它們都是被組合邏輯函式回傳的值。

  • 不存在名稱空間衝突,可以透過解構任意命名

  • 不需要為邏輯復用建立套件例項

  • 僅依賴它的引數和 Vue 全域匯出的 API,而不依賴 this 上下文

3.如何在vue2專案中使用composition API

這裡使用提供了 composition API 的vue2的外掛, @vue/composition-api

  • 專案中安裝 @vue/composition-api

1
2
3
npm install @vue/composition-api
# or
yarn add @vue/composition-api
  • 專案中使用

1
2
3
4
import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api'

Vue.use(VueCompositionAPI)

4.嘗試改造mixin---失敗範例

4.1 踩坑之旅開啟

這裡先是選擇了一段自認為比較簡單的 mixin 進行改造,事實證明,實踐才是檢驗真理的唯一標準,哈哈哈。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 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檔案中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 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套件中引用

1
2
3
4
5
6
7
8
9
10
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,升級會有哪些坑點,感覺可以再花費一段時間來研究下了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 升級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 屬性,改造成本高了不少。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 套件中
import placeOrderPlan from '@/app/help-sale/composables/placeOrderPlan.js'

export default {
  props: {
    query:{},  // 這裡傳入query
  },
  setup(props) {
    const { placeOrderPlan } = placeOrderPlan(props) // 這裡傳入props
    return {
      placeOrderPlan
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 套件中
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屬性的坑點,儲存執行,會看到瀏覽器里還在報錯,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 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 的過程就此終止了 ????

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 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程式碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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 中???? 這坨程式碼去哪裡了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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》?(https://zhuanlan.zhihu.com/p/320445941);

  • compostion API 的缺點:麵條程式碼,可以檢視《 簡明扼要聊聊 Vue3.0 的 Composition API 是啥東東》(https://zhuanlan.zhihu.com/p/320445941);

  • 思考:什麼樣的程式碼適合改造成(使用) composition API

感謝你的閱讀,有任何問題,歡迎評論區留言討論,互相學習。