圍繞Vue 3 Composition API構建一個應用程式,包含一些最佳實踐!

前端小智發表於2022-07-18

有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

1. Vue 3和Composition API的狀況

Vue 3已經發布了一年,它的主要新功能是:Composition API。從2021年秋季開始,推薦新專案使用Vue 3的 script setup 語法,所以希望我們能看到越來越多的生產級應用程式建立在Vue 3上。

這篇文章旨在展示一些有趣的方法來利用Composition API,以及如何圍繞它來構造一個應用程式。

2. 可組合函式和程式碼重用

新的組合API釋放了許多有趣的方法來重用跨元件的程式碼。複習一下:以前我們根據元件選項API分割元件邏輯:data、methods、created 等。

//  選項API風格
data: () => ({
    refA: 1,
    refB: 2,
  }),
// 在這裡,我們經常看到500行的程式碼。
computed: {
  computedA() {
    return this.refA + 10;
  },
  computedB() {
    return this.refA + 10;
  },
},

有了Composition API,我們就不會受限於這種結構,可以根據功能而不是選項來分離程式碼。

setup() {
    const refA = ref(1);
        const computedA = computed(() => refA.value + 10);
        /* 
        這裡也可能是500行的程式碼。
        但是,這些功能可以保持在彼此附近!
        */
    const computedB = computed(() => refA.value + 10);
        const refB = ref(2);

    return {
      refA,
      refB,
      computedA,
      computedB,
    };
  },

Vue 3.2引入了<script setup>語法,這只是setup()函式的語法糖,使程式碼更加簡潔。從現在開始,我們將使用 script setup 語法,因為它是最新的語法。

<script setup>
import { ref, computed } from 'vue'

const refA = ref(1);
const computedA = computed(() => refA.value + 10);

const refB = ref(2);
const computedB = computed(() => refA.value + 10);
</script>

在我看來,這是一個比較大想法。我們可以把這些功能分成自己的檔案,而不是用通過放置 在script setup中的位置來保持它們的分離。下面是同樣的邏輯,把檔案分割開來的做法。

// Component.vue
<script setup>
import useFeatureA from "./featureA";
import useFeatureB from "./featureB";

const { refA, computedA } = useFeatureA();
const { refB, computedB } = useFeatureB();
</script>

// featureA.js 
import { ref, computed } from "vue";

export default function () {
  const refA = ref(1);
  const computedA = computed(() => refA.value + 10);
  return {
    refA,
    computedA,
  };
}

// featureB.js 
import { ref, computed } from "vue";

export default function () {
  const refB = ref(2);
  const computedB = computed(() => refB.value + 10);
  return {
    refB,
    computedB,
  };
}

注意,featureA.jsfeatureB.js匯出了RefComputedRef型別,因此所有這些資料都是響應式的。

然而,這個特定的片段可能看起來有點矯枉過正。

  • 想象一下,這個元件有500多行程式碼,而不是10行。通過將邏輯分離到use__.js檔案中,程式碼變得更加可讀。
  • 我們可以在多個元件中自由地重複使用.js檔案中的可組合函式 不再有無渲染元件與作用域槽的限制,也不再有混合函式的名稱空間衝突。因為可組合函式直接使用了Vue的refcomputed,所以這段程式碼可以與你專案中的任何.vue元件一起使用。

陷阱1:setup 中的生命週期鉤子

如果生命週期鉤子(onMountedonUpdated等)可以在setup裡面使用,這也意味著我們也可以在我們的可組合函式裡面使用它們。甚至可以這樣寫:

// Component.vue
<script setup>
import { useStore } from 'vuex';

const store = useStore();
store.dispatch('myAction');
</script>


// store/actions.js
import { onMounted } from 'vue'
// ...
actions: {
  myAction() {
    onMounted(() => {
            console.log('its crazy, but this onMounted will be registered!')
        })
  }
}
// ...

而且Vue甚至會在vuex內部註冊生命週期鉤子! (問題是:你應該??)

有了這種靈活性,瞭解如何以及何時註冊這些鉤子就很重要了。請看下面的片段。哪些onUpdated鉤子將被註冊?

<script setup lang="ts">
import { ref, onUpdated } from "vue";

// 這個鉤子將被註冊。我們在 setup 中正常呼叫它
onUpdated(() => {
  console.log('✅')
});

class Foo {
  constructor() {
    this.registerOnMounted();
  }

  registerOnMounted() {
     //它也會註冊! 它是在一個類方法中,但它是在 
     //在 setup 中同步執行
    onUpdated(() => { 
      console.log('✅')
    });
  }
}
new Foo();

// IIFE also works
(function () {
  onUpdated(() => {
    state.value += "✅";
  });
})();


const onClick = () => {
    /* 
    這不會被註冊。這個鉤子是在另一個函式裡面。
    Vue不可能在setup 初始化中達到這個方法。
    最糟糕的是,你甚至不會得到一個警告,除非這個 
    函式被執行! 所以要注意這一點。
    */ 
  onUpdated(() => {
    console.log('❌')
  });
};

// 非同步IIFE也會不行 :(
(async function () {
  await Promise.resolve();
  onUpdated(() => {
    state.value += "❌";
  });
})();
</script>

結論:在宣告生命週期方法時,應使其在setup初始化時同步執行。否則,它們在哪裡被宣告以及在什麼情況下被宣告並不重要。

陷阱2:setup 中的非同步函式

我們經常需要在我們的邏輯中使用async/await。天真的做法是嘗試這樣做:

<script setup lang="ts">
import { myAsyncFunction } from './myAsyncFunction.js
const data = await myAsyncFunction();
</script>

<template>
  Async data: {{ data }}
</template>

然而,如果我們嘗試執行這段程式碼,元件根本不會被渲染。為什麼?因為 Promise 不跟蹤狀態。我們給 data 變數賦了一個 promise,但是Vue不會主動更新它的狀態。幸運的是,有一些變通辦法:

解決方案1:使用.then語法的ref

為了渲染該元件,我們可以使用.then語法。

<script setup>
import { ref } from "vue";
import { myAsyncFunction } from './myAsyncFunction.js

const data = ref(null);
myAsyncFunction().then((res) =>
  data.value = fetchedData
);
</script>

<template>
  Async data: {{ data }}
</template>
  1. 一開始時,建立一個等於null的響應式ref
  2. 呼叫了非同步函式script setup 的上下文是同步的,所以該元件會渲染
  3. myAsyncFunction() promise 被解決時,它的結果被賦值給響應性 data ref,結果被渲染

這種方式有自己優缺點:

  • 優點是:可以使用
  • 缺點:語法有點過時,當有多個.then.catch鏈時,會變得很笨拙。

解決方案2:IIFE

如果我們把這個邏輯包在一個非同步IIFE裡面,我們就可以使用 async/await的語法。

<script setup>
import { ref } from "vue";
import { myAsyncFunction } from './myAsyncFunction.js'

const data = ref(null);
(async function () {
    data.value = await myAsyncFunction()
})();
</script>

<template>
  Async data: {{ data }}
</template>

這種方式也有自己優缺點:

  • 優點:async/await語法
  • 缺點:可以說看起來不那麼幹淨,仍然需要一個額外的引用

解決方案3:Suspense (實驗性的)

如果我們在父元件中用<Suspense>包裝這個元件,我們就可以自由在setup 中自由使用async/await!

// Parent.vue
<script setup lang="ts">
import { Child } from './Child.vue
</script>

<template>
  <Suspense>
        <Child />
    </Suspense>
</template>

// Child.vue
<script setup lang="ts">
import { myAsyncFunction } from './myAsyncFunction.js
const data = await myAsyncFunction();
</script>

<template>
  Async data: {{ data }}
</template>
  • 優點:到目前為止,最簡明和直觀的語法
  • 缺點:截至2021年12月,這仍然是一個實驗性的功能,它的語法可能會改變。

<Suspense> 元件在子元件 setup 中有更多的可能性,而不僅僅是非同步。使用它,我們還可以指定載入和回退狀態。我認為這是建立非同步元件的前進方向。Nuxt 3已經使用了這個特性,對我來說,一旦這個特性穩定下來,它可能是首選的方式

解決方案4:單獨的第三方方法,為這些情況量身定做(見下節)。

優點。最靈活

缺點:對package.json的依賴

3. VueUse

VueUse庫依靠Composition API解鎖的新功能,給出了各種輔助函式。就像我們寫的useFeatureAuseFeatureB一樣,這個庫可以讓我們匯入預製的實用函式,以可組合的風格編寫。下面是它的工作原理的一個片段。

<script setup lang="ts">
import {
  useStorage,
    useDark
} from "@vueuse/core";
import { ref } from "vue";

/* 
    一個實現localStorage的例子。 
    這個函式返回一個Ref,所以可以立即用`.value`語法來編輯它。
    用.value語法編輯,而不需要單獨的getItem/setItem方法。
*/
const localStorageData = useStorage("foo", undefined);
</script>

我無法向你推薦這個庫,在我看來,它是任何新的Vue 3專案的必備品。

  • 這個庫有可能為你節省很多行程式碼和大量的時間。
  • 不影響包的大小
  • 原始碼很簡單,容易理解。如果你發現該庫的功能不夠,你可以擴充套件該功能。這意味在選擇使用這個庫時,不會有太大的風險。

下面是這個庫如何解決前面提到的非同步呼叫執行問題。

<script setup>
import { useAsyncState } from "@vueuse/core";
import { myAsyncFunction } from './myAsyncFunction.js';

const { state, isReady } = useAsyncState(
    // the async function we want to execute
  myAsyncFunction,

  // Default state:
  "Loading...",

  // UseAsyncState options:
  {
    onError: (e) => {
      console.error("Error!", e);
      state.value = "fallback";
    },
  }
);
</script>

<template>
  useAsyncState: {{ state }}
  Is the data ready: {{ isReady }}
</template>

這種方法可以讓你在setup裡面執行非同步函式,並給你回退選項和載入狀態。現在,這是我處理非同步的首選方法。

4. 如果你的專案使用Typescript

新的definePropsdefineEmits語法

script setup 帶來了一種在Vue元件中輸入 props 和 emits 的更快方式。

<script setup lang="ts">
import { PropType } from "vue";

interface CustomPropType {
  bar: string;
  baz: number;
}
//  defineProps的過載。
// 1. 類似於選項API的語法
defineProps({
  foo: {
    type: Object as PropType<CustomPropType>,
    required: false,
    default: () => ({
      bar: "",
      baz: 0,
    }),
  },
});

// 2. 通過一個泛型。注意,不需要PropType!
defineProps<{ foo: CustomPropType }>();

// 3.預設狀態可以這樣做。
withDefaults(
  defineProps<{
    foo: CustomPropType;
  }>(),
  {
    foo: () => ({
      bar: "",
      baz: 0,
    }),
  }
);

// // Emits也可以用defineEmits進行簡單的型別化
defineEmits<{ (foo: "foo"): string }>();
</script>

就個人而言,我會選擇通用風格,因為它為我們節省了一個額外的匯入,並且對null和 undefined 的型別更加明確,而不是Vue 2風格語法中的{ required: false }

? 注意,不需要手動匯入 definePropsdefineEmits。這是因為這些是Vue使用的特殊巨集。這些在編譯時被處理成 "正常 的選項API語法。我們可能會在未來的Vue版本中看到越來越多的巨集的實現。

可組合函式的型別化

因為typescript要求預設輸入模組的返回值,所以一開始我主要是用這種方式寫TS組合物。

import { ref, Ref, SetupContext, watch } from "vue";

export default function ({
  emit,
}: SetupContext<("change-component" | "close")[]>): 
// 下面的程式碼真的有必要嗎?
{
  onCloseStructureDetails: () => void;
  showTimeSlots: Ref<boolean>;
  showStructureDetails: Ref<boolean>;
  onSelectSlot: (arg1: onSelectSlotArgs) => void;
  onBackButtonClick: () => void;
  showMobileStepsLayout: Ref<boolean>;
  authStepsComponent: Ref<string>;
  isMobile: Ref<boolean>;
  selectedTimeSlot: Ref<null | TimeSlot>;
  showQuestionarireLink: Ref<boolean>;
} {
  const isMobile = useBreakpoints().smaller("md");
  const store = useStore();
    // and so on, and so on
    // ... 
}

這種方式,我認為這是個錯誤。其實沒有必要對函式返回進行型別化,因為在編寫可組合的時候可以很容易地對它進行隱式型別化。它可以為我們節省大量的時間和程式碼行。

import { ref, Ref, SetupContext, watch } from "vue";

export default function ({
  emit,
}: SetupContext<("change-component" | "close")[]>) {
  const isMobile = useBreakpoints().smaller("md");
  const store = useStore();
    // The return can be typed implicitly in composables
}

? 如果EsLint將此標記為錯誤,將`
'@typescript-eslint/explicit-module-boundary-types': 'error'
`,放入EsLint配置(.eslintrc)。

Volar extension

Volar是作為VsCode和WebStorm的Vue擴充套件來取代Vetur的。現在它被正式推薦給Vue 3使用。對我來說,它的主要特點是:typing props and emits out of the box。這很好用,特別是使用Typescript的話。

現在,我總是會選擇Vue 3專案中使用Volar。對於Vue 2, Volar仍然適用,因為它需要更少的配置 。

5. 圍繞組合API的應用架構

將邏輯從.vue元件檔案中移出

以前,有一些例子,所有的邏輯都是在script setup 中完成的。還有一些例子是使用從.vue檔案匯入的可組合函式的元件。

大程式碼設計問題是:我們應該把所有的邏輯寫在.vue檔案之外嗎?有利有弊。

所有的邏輯都放在 setup中移到專用的.js/.ts檔案
不需要寫一個可組合的,方便直接修改可擴充套件更強
重用程式碼時需要重構不需要重構
更多模板

我是這樣選擇的:

  • 在小型/中型專案中使用混合方法。一般來說,把邏輯寫在setup裡面。當元件太大時,或者當很清楚這些程式碼會被重複使用時,就把它放在單獨的js/ts檔案中
  • 對於大型專案,只需將所有內容編寫為可組合的。只使用setup來處理模板名稱空間。

編輯中可能存在的bug沒法實時知道,事後為了解決這些bug,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug

作者:Noveo 譯者:小智 來源:noveogroup

原文:https://blog.noveogroup.com/2...

交流

有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

相關文章