2

围绕Vue 3 Composition API构建一个应用程序,包含一些优秀实践!

 2 years ago
source link: https://www.fly63.com/article/detial/11882
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

更新日期: 2022-07-11阅读: 18标签: API分享

扫一扫分享

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(':white_check_mark:')
});
class Foo {
  constructor() {
    this.registerOnMounted();
  }
  registerOnMounted() {
     //它也会注册! 它是在一个类方法中,但它是在 
     //在 setup 中同步执行
    onUpdated(() => { 
      console.log(':white_check_mark:')
    });
  }
}
new Foo();
// IIFE also works
(function () {
  onUpdated(() => {
    state.value += ":white_check_mark:";
  });
})();
const onClick = () => {
 /* 
 这不会被注册。这个钩子是在另一个函数里面。
 Vue不可能在setup 初始化中达到这个方法。
 最糟糕的是,你甚至不会得到一个警告,除非这个 
 函数被执行! 所以要注意这一点。
 */ 
  onUpdated(() => {
    console.log(':x:')
  });
};

// 异步IIFE也会不行 :(
(async function () {
  await Promise.resolve();
  onUpdated(() => {
    state.value += ":x:";
  });
})();
</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解锁的新功能,给出了各种辅助函数。就像我们写的 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来处理模板名称空间。
来源: 大迁世界

链接: https://www.fly63.com/article/detial/11882


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK