對於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定義完成後,我們的頁面資料基本都已清楚,直接寫頁面就會清晰很多,且出錯概率會大大降低。