對於Vue3和Ts的心得和思考

語言: CN / TW / HK

本文正在參加「金石計劃」

作者:京東物流 吳雲闊

1 前言

Vue3已經正式發佈了一段時間了,各種生態已經成熟。最近使用taro+vue3重構冷鏈的小程序,經過了一段時間的開發和使用,有了一些自己的思考。

總的來説,Vue3無論是在底層原理還是在實際開發過程中,都有了很大的進步。

從源碼層面來説,使用Proxy代替Object.defineProperty的API,一個是代理的對象,一個是遞歸監控的屬性,從而在性能上有了很大的進步,並且,也解決了對象動態屬性增加、數組改變監聽上的缺陷;對diff算法進行優化,增加了靜態標記,大大提高了Vue的執行效率;還有靜態提升、事件監聽緩存等一系列提升效率的手段。

從應用層面來説,主要的改變是將option API改成了composition API(組合式API),在業務中拋棄data、methods、生命週期函數隔離開的開發方式,使代碼相對於業務有更強的聚合性,在代碼開發、代碼閲讀、代碼維護方面對於開發者都是更加友好。

對於typescript有了更好的支持,我們知道,對於大型的前端項目來説,使用typescript的類型校驗,能使前端項目有更強的健壯性,這也使得Vue3對於大型項目的開發提供了更強的質量保證。

2 組合式API

所謂的組合式API,將Vue2中的data、methods、生命週期、數據監聽等option,都封裝成鈎子函數,然後組合到setup函數中,其核心就在於setup函數。setup函數存在的意義,就是為了使用這些新增的組合式API,並且這些API只能在setup函數中使用。

setup函數執行的時機是,props初始化之後,beforeCreate函數執行之前,所以在執行setup時,還沒有初始化Vue實例,因此在setup中不能使用this對象。setup函數的返回值會被注入到Vue實例中,供Vue組件使用,所以任何數據想在Vue組件的模板中使用,必須在setup函數中return出去。

組合式API的組合,體現在兩個層面。第一層的意思是,將某一業務相關數據和處理邏輯放到一起,這是一種關注點的聚合,更方便我們編寫代碼、處理業務邏輯,並且能更聚焦業務邏輯,更方便我們看代碼。第二層面的意思,當某個組件的業務邏輯足夠複雜,setup中的代碼足夠大的情況下,我們可以在setup內部,進一步提取相關的一塊業務,使代碼邏輯更加清晰,做到了進一步的聚合作用。

如下面代碼所示,將業務代碼塊A抽出來,則代碼塊A中return出來的數據就可以在組件中使用:

  1. // 組件
  2. import functionA from 'A'
  3. export default defineComponent({
  4. name: 'componentName',
  5. setup() {
  6. ...functionA()
  7. }
  8. })

  9. // 代碼塊A

  10. export default () => {
  11. return {
  12. a: 1
  13. }
  14. }

3 響應式API

在Vue3中響應式API,主要體現在ref和reactive兩個函數。對於響應式API,想説兩個問題,第一個是為什麼要增加響應式API,第二個是響應式API函數ref和reactive的異同點。

3.1 為什麼增加響應式API

在Vue2中所有數據都寫在data的option中,data中的數據都是響應式的,這樣產生的一個問題是,有些常量數據本身不需要監聽,從而造成了資源的浪費。所以在Vue3中增加了響應式API,只需要對需要動態更新dom的數據進行響應式,不需要動態更新dom的數據不進行響應式處理,從很大程度上節省了資源。這裏我覺得需要注意的是,寫代碼的時候一定要仔細思考一下,哪些數據需要進行響應式綁定,哪些數據不需要進行響應式綁定,而不是一股腦的全給綁定上,這樣即使代碼邏輯不能很清晰易懂,並且也會影響執行效率(寫慣了Vue2的同學需要注意)。

3.2 ref和reactive的異同點

在瞭解了為什麼要增加響應式API後,我們發現Vue3提供了兩個響應式API函數,ref和reactive。為什麼會提供兩個API呢? 一個不就可以了嗎?那麼這兩個API之間的區別是什麼呢?

在使用層面,ref綁定的數據,需要使用[data].value進行數據更改。而reactive綁定的數據需要使用[data].[prpoerty]的方式進行數據更改。在使用場景方面,一般的,單個的普通數據,我們使用ref來定義響應式。而複雜數據,如:表單數據對象、某一模塊的一組數據等,使用reactive來定義響應式。

那麼,對象是不是必須用reactive來定義呢? 其實不是的,都可以。官方説法是:可以根據自身習慣使用不同的API。其實,我覺得,他們是有各自的使用場景的,ref更強調的是數據Value的改變,reactive更強調的是數據中某一屬性的改變。

4 treeShaking思想

當 Javascript 項目達到一定體積時,將代碼分成模塊會更易於管理。但是,當這樣做時,我們最終可能會導入實際上未使用的代碼。Tree Shaking 是一種通過消除最終文件中未使用的代碼來優化體積的方法。

Vue3使用了tree shaking的方法,將組件以及其所有的生命週期函數等方法進行分開,如果在組件中使用的代碼將不會出現在最終的打包文件中,如此,會減少大大Vue3項目的打包體積。由此造成的一個結果就是,使用方法的不同。

4.1 生命週期函數的使用方法

  1. import { defineComponent, ref, onMounted } from 'vue';
  2. export default defineComponent({
  3. name: 'Gift',
  4. setup() {
  5. const counter = ref(0);
  6. onMounted(() => {
  7. // 處理業務,一般進行數據請求
  8. })
  9. return {
  10. counter
  11. }
  12. }
  13. })

4.2 Vuex的使用方法

  1. import { useStore } from "vuex";
  2. import { defineComponent, ref, computed } from 'vue';
  3. export default defineComponent({
  4. name: 'Gift',
  5. setup() {
  6. const counter = ref(0);
  7. const store = useStore();
  8. const storeData = computed(() => store); // 配合computed,獲取store的值。
  9. return {
  10. counter,
  11. storeData
  12. }
  13. }
  14. })

4.3 Router的使用方法

  1. import { useStore } from "vuex";
  2. import { useRouter } from "vue-router";
  3. import { defineComponent, ref, computed } from 'vue';
  4. export default defineComponent({
  5. name: 'Gift',
  6. setup() {
  7. const counter = ref(0);
  8. const router = useRouter();
  9. const onClick = () => {
  10. router.push({ name: "AddGift" });
  11. }
  12. return {
  13. counter,
  14. onClick
  15. }
  16. }
  17. })

5 關於Typescript的使用

這一部分是關於Ts的內容,不過它與Vue3的開發息息相關。Vue3整體是使用Ts寫的,因此,開發Vue3的項目需要使用Ts,所以,我們還是要了解TS的。

關於Ts的使用這裏就不在細説了,在這裏想説的的是,在實際業務場景中是如何組織Ts代碼的。通過對TS的大量使用,我的一個體會是:Ts的核心思維是先關注數據結構,在根據數據結構進行頁面開發。而以前的前端開發模式是,先寫頁面,然後再關注數據。

比如説,我們要開發一個頁面,我們可能需要先定義一些interface。開發頁面的時候我們要關注:頁面數據的interface、接口返回數據的類型、請求參數的類型等等。

下面是開發一個列表頁面的例子:

  1. // 這是列表中每一項的數據類型
  2. interface IDataItem {
  3. id: string | number;
  4. name: string;
  5. desc: string;
  6. [key: string]: any;
  7. }

  8. // 接口返回值類型, 一般來説,我們不確定接口返回的數據的類型,因此使用泛型

  9. interface IRes<T> {
  10. code: number;
  11. msg: string;
  12. data: T
  13. }

  14. // 口返回數據類型定義

  15. interface IDataInfo {
  16. list: Array<IDataItem>;
  17. pageNum: number;
  18. pageSize: number;
  19. total: number;
  20. }

  21. // 請求

  22. export const getDatalist = (
  23. params: Record<string, any>
  24. ): Promise<IRes<IDataInfo>> => {
  25. return Http.get("/api/data/list", params);
  26. };

如上面代碼,當我們的interface定義完成後,我們的頁面數據基本都已清楚,直接寫頁面就會清晰很多,且出錯概率會大大降低。