vue3的composition-api實踐總結


因為向往已久vue3的開發方式,但是組內有很多歷史項目,並且我們受制於ie的支持,所以我們決定在vue2中引入composition-api,來使用他的新特性。在使用過程中,我們遇到了很多問題,也積累了一些經驗,所以記錄下。

composition-api

首先給大家介紹一下composition-api,他是通過函數的形式,將vue的功能特性暴露給我們使用

雖然一下子看上去很多,但是只需要先掌握加粗的這一部分,就可以體驗到組合式api的魅力了,其他的可以先做了解等到你真的需要使用它們的時候在做深入。

reactive 用來將對象轉變為響應式的,與vue2的observable類似,ref用來獲得單獨或者為基礎數據類型獲得響應性。為什會會有兩個獲得響應性的api呢稍后我們將具體說明。computed、watch,provide、inject不用懷疑和vue2中做的是一樣的事情。

你一定注意到下面這些加了on開頭的生命周期鈎子函數,沒錯在組合式api中,這就是他們注冊的方式。但是為什么不見了beforeCreate和created呢?因為setup就是在這個階段執行的,而setup就是打開組合式api世界的大門。你可以把setup理解為class的constructor,在vue組件的創建階段,把我們的相關邏輯執行,並且注冊相關的副作用函數。

現在我們說回ref和reactive。

  • reactive在官網中的說明,接受一個對象,返回對象的響應式副本。ref在官網中的描述"接受一個內部值並返回一個響應式且可變的 ref 對象。

  • ref 對象具有指向內部值的單個 property.value"。

聽着很繞口,簡單來講就是reactive可以為對象創建響應式而ref除了對象,還可以接收基礎數據類型,比如string、boolean等。
那為什么會有這種差異呢?在vue3當中響應式是基於proxy實現的,而proxy的target必須是復雜數據類型,也就是存放在堆內存中,通過指針引用的對象。其實也很好理解,因為基礎數據類型,每一次賦值都是全新的對象,所以根本無法代理。那么如果我們想取得簡單類型的響應式怎么辦呢?這時候就需要用到ref。

`class RefImpl<T> {  
  private _value: T  
  
  public readonly __v_isRef = true  
  
  constructor(private _rawValue: T, public readonly _shallow = false) {  
    this._value = _shallow ? _rawValue : convert(_rawValue)  
  }  
  
  get value() {  
    track(toRaw(this), TrackOpTypes.GET, 'value')  
    return this._value  
  }  
  
  set value(newVal) {  
    if (hasChanged(toRaw(newVal), this._rawValue)) {  
      this._rawValue = newVal  
      this._value = this._shallow ? newVal : convert(newVal)  
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)  
    }  
  }  
}  
...  
const convert = <T extends unknown>(val: T): T =>  
  isObject(val) ? reactive(val) : val  
...  
`

ref通過創建內部狀態,將值掛在value上,所以ref生成的對象,要通過value使用。重寫get/set獲得的監聽,同時對對象的處理,也依賴了reactive的實現。

由此,ref並不只是具有對基本數據類型的響應式處理能力,他也是可以處理對象的。所以我認為ref和reactive的區分並不應該只是簡單/復雜對象的區分,而是應該用編程思想區分的。我們應該避免,把reactive 當作data在頂部將所有變量聲明的想法,而是應該結合具體的邏輯功能,比如一個控制灰度的Flag那他就應該是一個ref,而分頁當中的頁碼,pageSize,total等就應該是一個reactive聲明的對象。也就是說一個setup當中可以有多出響應變量的聲明,而且他們應當是與邏輯緊密結合的.

接下來我先用一個分頁的功能,用選項式和組合式api給大家對比一下。

一些例子

`<template>  
    <div>  
        <ul class="article-list">  
            <li v-for="item in articleList" :key="item.id">  
                <div>  
                    <div class="title">{{ item.title }}</div>  
                    <div class="content">{{ item.content }}</div>  
                </div>  
            </li>  
        </ul>  
        <el-pagination  
            @size-change="handleSizeChange"  
            @current-change="handleCurrentChange"  
            :current-page="currentPage"  
            :page-sizes="pageSizes"  
            :page-size="pageSize"  
            layout="total, sizes, prev, pager, next, jumper"  
            :total="total"  
        >  
        </el-pagination>  
    </div>  
</template>  
  
<script>  
import { getArticleList } from '@/mock/index';  
export default {  
    data() {  
        return {  
            articleList: [],  
            currentPage: 1,  
            pageSizes: [5, 10, 20],  
            pageSize: 5,  
            total: 0,  
        };  
    },  
    created() {  
        this.getList();  
    },  
    methods: {  
        getList() {  
            const param = {  
                currentPage: this.currentPage,  
                pageSizes: this.pageSizes,  
                pageSize: this.pageSize,  
            };  
            getArticleList(param).then((res) => {  
                this.articleList = res.data;  
                this.total = res.total;  
            });  
        },  
        handleSizeChange(val) {  
            this.pageSize = val;  
            this.getList();  
        },  
        handleCurrentChange(val) {  
            this.currentPage = val;  
            this.getList();  
        },  
    },  
};  
</script>  
`

這還是我們熟悉到不能在熟悉的分頁流程,在data中聲明數據,在method中提供修分頁的方法。當我們用composition-api實現的時候他就成了下面的樣子。

`<script>  
import { defineComponent, reactive, ref, toRefs } from "@vue/composition-api";  
import { getArticleList } from "@/mock/index";  
export default defineComponent({  
  setup() {  
    const page = reactive({  
      currentPage: 1,  
      pageSizes: [5, 10, 20],  
      pageSize: 5,  
      total: 0,  
    });  
    function handleSizeChange(val) {  
      page.pageSize = val;  
      getList();  
    }  
    function handleCurrentChange(val) {  
      page.currentPage = val;  
      getList();  
    }  
  
    const articleList = ref([]);  
    function getList() {  
      getArticleList(page).then((res) => {  
        articleList.value = res.data;  
        page.total = res.total;  
      });  
    }  
    getList();  
    return {  
      ...toRefs(page),  
      articleList,  
      getList,  
      handleSizeChange,  
      handleCurrentChange,  
    };  
  },  
});  
</script>  
`

這是以composition-api的方式實現的分頁,你會發現原本的data,method,還有聲明周期等選項都不見了,所有的邏輯都放到了setup當中。通過這一個簡單的例子,我們可以發現原本分散在各個選項中的邏輯,在這里得到了聚合。這種變化在復雜場景下更為明顯。在復雜組件中,這種情況更加明顯。而且當邏輯完全聚集在一起,這時候,將他們抽離出來,而且抽離邏輯的可以在別處復用,至此hook就形成了。

hook形態的分頁組件

`// hooks/useArticleList.js  
import { ref } from "@vue/composition-api";  
import { getArticleList } from "@/mock/index"; // mock ajax請求  
  
function useArticleList() {  
  const articleList = ref([]);  
  function getList(page) {  
    getArticleList(page).then((res) => {  
      articleList.value = res.data;  
      page.total = res.total;  
    });  
  }  
  return {  
    articleList,  
    getList,  
  };  
}  
export default useArticleList;  
  
// hooks/usePage.js  
import { reactive } from "@vue/composition-api";  
  
function usePage(changeFn) {  
  const page = reactive({  
    currentPage: 1,  
    pageSizes: [5, 10, 20],  
    pageSize: 5,  
    total: 0,  
  });  
  function handleSizeChange(val) {  
    page.pageSize = val;  
    changeFn(page);  
  }  
  function handleCurrentChange(val) {  
    page.currentPage = val;  
    changeFn(page);  
  }  
  return {  
    page,  
    handleSizeChange,  
    handleCurrentChange,  
  };  
}  
export default usePage;  
  
// views/List.vue  
import { defineComponent, toRefs } from "@vue/composition-api";  
import usePage from "@/hooks/usePage";  
import useArticleList from "@/hooks/useArticleList";  
export default defineComponent({  
  setup() {  
    const { articleList, getList } = useArticleList();  
    const { page, handleSizeChange, handleCurrentChange } = usePage(getList);  
    getList(page);  
    return {  
      ...toRefs(page),  
      articleList,  
      getList,  
      handleSizeChange,  
      handleCurrentChange,  
    };  
  },  
});  
`

在hook使用過程中我們也踩過很多坑

1. hook中的異步問題

因為hook本質上就是函數,所以靈活度非常高,尤其是在涉及異步的邏輯中,考慮不全面就很有可能造成很多問題。hook是可以覆蓋異步情況的,但是必須在setup當中執行時返回有效對象不能被阻塞。

我們總結了兩種異步的風格,通過一個簡單的hook為例

  • 外部沒有其他依賴,只是交付渲染的響應變量 對於這種情況,可以通過聲明、對外暴露響應變量,在hook中異步修改的方式
`// hooks/useWarehouse.js  
import { reactive,toRefs } from '@vue/composition-api';  
import { queryWarehouse } from '@/mock/index';  // 查詢倉庫的請求  
import getParam from '@/utils/getParam'; // 獲得一些參數的方法  
function useWarehouse(admin) {  
    const warehouse = reactive({ warehouseList: [] });  
    const param = { id: admin.id, ...getParam() };  
    const queryList = async () => {  
        const { list } = await queryWarehouse(param);  
        list.forEach(goods=>{  
        // 一些邏輯...  
          return goods  
        })  
        warehouse.warehouseList = list;  
    };  
    return { ...toRefs(warehouse), queryList };  
}  
`
`export default useWarehouse;// components/Warehouse.vue  
<template>  
    <div>  
        <button @click="queryList">queryList</button>  
        <ul>  
            <li v-for="goods in warehouseList" :key="goods.id">  
                {{goods}}  
            </li>  
        </ul>  
    </div>  
</template>  
  
<script>  
import { defineComponent } from '@vue/composition-api';  
import useWarehouse from '@/hooks/useWarehouse';  
export default defineComponent({  
    setup() {  
        // 倉庫保管員  
        const admin = {  
            id: '1234',  
            name: '張三',  
            age: 28,  
            sex: 'men',  
        };  
        const { warehouseList, queryList } = useWarehouse(admin);  
        return { warehouseList, queryList };  
    },  
});  
</script>  
`
  • 外部具有依賴,需要在使用側進行加工的 可以通過對外暴露Promise的方式,使外部獲得同步操作的能力 在原有例子上拓展,增加一個需要處理的更新時間屬性
`// hooks/useWarehouse.js  
function useWarehouse(admin) {  
    const warehouse = reactive({ warehouseList: [] });  
    const param = { id: admin.id, ...getParam() };  
    const queryList = async () => {  
        const { list, updateTime } = await queryWarehouse(param);  
            list.forEach(goods=>{  
        // 一些邏輯...  
          return goods  
        })  
        warehouse.warehouseList = list;  
        return updateTime;  
    };  
    return { ...toRefs(warehouse), queryList };  
}  
`
`export default useWarehouse;// components/Warehouse.vue  
<template>  
    <div>  
       ...  
        <span>nextUpdateTime:{{nextUpdateTime}}</span>  
    </div>  
</template>  
  
<script>  
...  
import dayjs from 'dayjs';  
export default defineComponent({  
    setup() {  
    ...  
       // 倉庫保管員  
        const admin = {  
            id: '1234',  
            name: '張三',  
            age: 28,  
            sex: 'men',  
        };  
        const { warehouseList, queryList } = useWarehouse(admin);  
        const nextUpdateTime = ref('');  
        const interval = 7; // 假設更新倉庫的時間間隔是7天  
        const queryHandler = async () => {  
            const updateTime = await queryList();  
            nextUpdateTime.value = dayjs(updateTime).add(interval, 'day');  
        };  
        return { warehouseList, nextUpdateTime, queryHandler };  
    },  
});  
</script>  
`

2. this的問題

因為setup是beforecreate階段,不能獲取到this,雖然通過setup的第二個參數context可以獲得一部分的能力。是我們想要操作諸如路由,vuex這樣的能力就收到了限制,最新的router@4、vuex@4都提供了組合式的api。

但是由於vue2的底層限制我們沒有辦法使用這些hook,但是我們可以通過引用實例的方式獲得一定的操縱能力,也可以通過getCurrentInstance獲得組件實例,上面掛載的對象。

由於composition-api中的響應式雖然底層原理與vue相同都是通過object.defineproperty改寫屬性實現的,但是具體實現方式存在差異,所以在setup當中與vue原生的響應式並不互通。這也導致即使我們拿到了相應的實例,也沒有辦法監聽它們的響應式。如果有這方面的需求,只能在選項配置中使用。

總結

通過vue3組合式、與hook的能力。我們的代碼風格有了很大的轉變,邏輯更加聚合、純粹。復用性能力得到了提升。項目整體的維護性有了顯著的提高。這也是我們即便在vue2的項目中,也要使用composition-api引入vue3新特性的原因。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM