If you have dreams and dry goods, you can search for [Great Move to the World] on WeChat and pay attention to this Shawanzhi who is still washing dishes in the early hours of the morning.
This article GitHub https://github.com/qq449245884/xiaozhi has been included, there are complete test sites, materials and my series of articles for interviews with first-line manufacturers.
1. The state of Vue 3 and the Composition API
Vue 3 has been out for a year and its main new feature is: the Composition API. Starting in fall 2021, Vue 3's script setup
syntax is recommended for new projects, so hopefully we'll see more and more production-grade applications built on Vue 3.
This post aims to show some interesting ways to take advantage of the Composition API and how to structure an application around it.
2. Composable functions and code reuse
The new composition API unlocks many interesting ways to reuse code across components. To review: Previously we split component logic based on the component options API: data, methods, created, etc.
// 选项API风格
data: () => ({
refA: 1,
refB: 2,
}),
// 在这里,我们经常看到500行的代码。
computed: {
computedA() {
return this.refA + 10;
},
computedB() {
return this.refA + 10;
},
},
With the Composition API, we are not limited to this structure and can separate code based on functionality rather than options.
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 introduced the <script setup>
syntax, which is just a syntactic sugar for the setup()
function, making the code more concise. From now on, we will use the script setup syntax because it is the latest syntax.
<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>
In my opinion, this is a big idea. Instead of keeping them separate by placing them in the script setup, we can separate these functions into their own files. The following is the same logic, the practice of dividing the file.
// 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,
};
}
Note that featureA.js
and featureB.js
export Ref
and ComputedRef
of type response.
However, this particular snippet might seem like overkill.
- Imagine this component has 500+ lines of code instead of 10. By separating the logic
到use__.js
files, the code becomes more readable. - We can freely reuse in multiple components
.js
the composable functions in the file no longer have the restrictions of unrendered components and scope slots, and no more namespace conflicts for mixin functions. Because the composable function uses Vue'sref
andcomputed
, this code can be used with any.vue
component in your project.
Pitfall 1: Lifecycle hooks in setup
If lifecycle hooks ( onMounted
, onUpdated
etc.) can be used inside setup
, it also means we can use them inside our composable functions as well . It can even be written like this:
// 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!')
})
}
}
// ...
And Vue will even register lifecycle hooks inside vuex! (problem: you should 🤨🙂)
With this flexibility, it's important to know how and when to register these hooks. See the snippet below. Which onUpdated
hooks will be registered?
<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>
Conclusion: When declaring a lifecycle method, it should be executed synchronously when setup
is initialized. Otherwise, it doesn't matter where and under what circumstances they are declared.
Pitfall 2: Async functions in setup
We often need to use async/await
in our logic. The naive approach is to try this:
<script setup lang="ts">
import { myAsyncFunction } from './myAsyncFunction.js
const data = await myAsyncFunction();
</script>
<template>
Async data: {{ data }}
</template>
However, if we try to run this code , the component doesn't get rendered at all. Why? Because Promises don't track state. We assign a promise to the data variable, but Vue doesn't actively update its state. Fortunately, there are some workarounds:
Solution 1: Use .then
syntax for ref
To render the component, we can use the .then
syntax.
<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>
- At the beginning, create a reactive equal to null
ref
- The context in which the asynchronous function script setup is called is synchronous, so the component will render
- When the
myAsyncFunction()
promise is resolved, its result is assigned to the reactive data ref and the result is rendered
This approach has its own advantages and disadvantages:
- The advantage is: you can use
- Disadvantage: The syntax is a bit outdated and becomes unwieldy when there are multiple
.then
and.catch
chains.
Solution 2: IIFE
If we wrap this logic in an asynchronous IIFE, we can use the syntax 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>
This approach also has its own advantages and disadvantages:
- Pros: async/await syntax
- Cons: Arguably not as clean looking, still requires an extra citation
Solution 3: Suspense (experimental)
If we wrap this component with <Suspense>
in the parent component, we can freely use async/await
in setup!
// 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>
- Pros: By far the most concise and intuitive syntax
- Cons: As of December 2021, this is still an experimental feature and its syntax may change.
<Suspense>
Components have more possibilities in subcomponent setup than just async. Using it, we can also specify loading and fallback states. I think this is the way forward for creating asynchronous components. Nuxt 3 already uses this feature, for me it's probably the preferred way once this is stabilized
Solution 4: A separate third-party approach, tailored for these situations (see next section).
advantage. most flexible
Cons: Dependency on package.json
3. VueUse
The VueUse library relies on the new functionality unlocked by the Composition API, giving various helper functions. Just as we wrote useFeatureA
and useFeatureB
, this library lets us import pre-made utility functions, written in a composable style. Below is a snippet of how it works.
<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>
I can't recommend this library to you enough, it's a must-have for any new Vue 3 project in my opinion.
- This library has the potential to save you many lines of code and a lot of time.
- Does not affect package size
- The source code is simple and easy to understand. If you find that the library is not functional enough, you can extend the functionality. This means that there is not much risk when choosing to use this library.
Here's how this library solves the aforementioned asynchronous call execution problem.
<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>
This method lets you execute async functions inside setup
and gives you fallback options and loading state. Now, this is my preferred way to handle async.
4. If your project uses Typescript
New defineProps
and defineEmits
syntax
script setup brings a faster way to enter props and emits in Vue components.
<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>
Personally, I'd choose the generic style because it saves us an extra import and is more explicit about null and undefined types, rather than { required: false }
in the Vue 2 style syntax.
💡 Note that there is no need to manually import defineProps
and defineEmits
. This is because these are special macros used by Vue. These are processed into "normal options API syntax" at compile time. We may see more and more implementations of macros in the future Vue版本
.
Typing of composable functions
Since TypeScript requires the return value of a default input module, I mostly wrote TS compositions this way at first.
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
// ...
}
This way, I think it's a mistake. There is actually no need to type the function return, because it can be easily typed implicitly when writing composables. It can save us a lot of time and lines of code.
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
}
💡 If EsLint flags this as an error, it will `
'@typescript-eslint/explicit-module-boundary-types': 'error'
`
, into the EsLint config ( .eslintrc
).
Volar extension
Volar replaces Vetur as a Vue extension for VsCode and WebStorm. It is now officially recommended for use with Vue 3. For me, its main features are: typing props and emits out of the box . This works great, especially with Typescript.
Now, I always choose to use Volar in Vue 3 projects. For Vue 2, Volar still works because it requires less configuration.
5. Application architecture around composition APIs
Move logic out of .vue component files
Previously, there were some examples where all the logic was done in script setup. There are also examples of components using composable functions imported from the .vue
file.
The big code design question is: should we write all the logic out of the .vue
file? There are pros and cons.
All logic is placed in setup | Move to dedicated .js/.ts files |
---|---|
No need to write a composable, easy to modify directly | more scalable |
Refactoring is required when reusing code | No refactoring required |
more templates |
I chose this way:
- Use a hybrid approach in small/medium projects. Generally speaking, the logic is written in the setup. When the component is too large, or when it is clear that the code will be reused, put it in a separate
js/ts
file - For large projects, just write everything to be composable. Only use setup to handle template namespaces.
The bugs that may exist in editing cannot be known in real time. In order to solve these bugs afterwards, a lot of time is spent on log debugging. By the way, here is a useful BUG monitoring tool , Fundebug .
Author: Noveo Translator: Xiaozhi Source: noveogroup
Original: https://blog.noveogroup.com/2022/02/building-app-around-vue-3-composition-api
comminicate
If you have dreams and dry goods, you can search for [Great Move to the World] on WeChat and pay attention to this Shawanzhi who is still washing dishes in the early hours of the morning.
This article GitHub https://github.com/qq449245884/xiaozhi has been included, there are complete test sites, materials and my series of articles for interviews with first-line manufacturers.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。