寫在前面
為什麼要用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 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?
感謝你的閱讀,有任何問題,歡迎評論區留言討論,互相學習。