有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。
本文 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.js
和featureB.js
匯出了Ref
和ComputedRef
型別,因此所有這些資料都是響應式的。
然而,這個特定的片段可能看起來有點矯枉過正。
- 想象一下,這個元件有500多行程式碼,而不是10行。通過將邏輯分離
到use__.js
檔案中,程式碼變得更加可讀。 - 我們可以在多個元件中自由地重複使用
.js
檔案中的可組合函式 不再有無渲染元件與作用域槽的限制,也不再有混合函式的名稱空間衝突。因為可組合函式直接使用了Vue的ref
和computed
,所以這段程式碼可以與你專案中的任何.vue
元件一起使用。
陷阱1:setup 中的生命週期鉤子
如果生命週期鉤子(onMounted
,onUpdated
等)可以在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>
- 一開始時,建立一個等於null的響應式
ref
- 呼叫了非同步函式script setup 的上下文是同步的,所以該元件會渲染
- 當
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解鎖的新功能,給出了各種輔助函式。就像我們寫的useFeatureA
和useFeatureB
一樣,這個庫可以讓我們匯入預製的實用函式,以可組合的風格編寫。下面是它的工作原理的一個片段。
<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
新的defineProps
和defineEmits
語法
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 }
。
? 注意,不需要手動匯入 defineProps
和 defineEmits
。這是因為這些是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 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。