1

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's ref and computed , 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>
  1. At the beginning, create a reactive equal to null ref
  2. The context in which the asynchronous function script setup is called is synchronous, so the component will render
  3. 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.


王大冶
68.1k 声望105k 粉丝