【问题标题】:How to hook multiple times in component in Vue 3如何在 Vue 3 中的组件中多次挂钩
【发布时间】:2021-05-11 19:31:46
【问题描述】:

我在一节课中看到我们可以使用组合 api 挂钩 usePromise 创建,但问题是我有一个带有待办事项列表的简单 crud 应用程序,我在其中创建、删除、获取 API 调用,但我不明白我是如何可以将此钩子用于一个组件中的所有 api。所有调用都正确,但加载不正确,它仅在第一次调用 PostService.getAll() 时才起作用,然后加载程序没有被触发。感谢您的回复。

usePromise.js

import { ref } from 'vue';

export default function usePromise(fn) {
    const results = ref(null);
    const error = ref(null);
    const loading = ref(false);

    const createPromise = async (...args) => {
        loading.value = true;
        error.value = null;
        results.value = null;

        try {
            results.value = await fn(...args);
        } catch (err) {
            error.value = err;
        } finally {
            loading.value = false;
        }
    };

    return { results, loading, error, createPromise };
}

apiClient.js

import axios from 'axios';

export default axios.create({
    baseURL: 'https://jsonplaceholder.typicode.com/',
    withCredentials: false,
    headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
    },
});

PostService.js

import apiClient from './apiClient';
const urlPath = '/posts';

export default {
    getAll() {
        return apiClient.get(urlPath);
    },

    add(post) {
        return apiClient.post(urlPath, post);
    },

    delete(id) {
        return apiClient.delete(`${urlPath}/${id}`);
    },
};

List.vue

<template>
    <div>
        <VLoader v-if="loading" />
        <template v-else>
            <table class="table">
                <thead>
                    <tr>
                        <th>Id</th>
                        <th>Title</th>
                        <th></th>
                    </tr>
                </thead>
                <tbody>
                    <tr v-for="post in posts" :key="post.id">
                        <td>{{ post.id }}</td>
                        <td>{{ post.title }}</td>
                        <td>
                            <button class="btn btn-danger ml-1" @click="deletePost(post.id)">Delete</button>
                        </td>
                    </tr>
                </tbody>
            </table>
        </template>
    </div>
</template>

<script>
import { ref, computed, watch, unref } from 'vue';
import PostService from '@/services/PostService';
import usePromise from '@/use/usePromise';

export default {
    setup() {
        const posts = ref([]);
        const post = ref({
            title: '',
            body: '',
        });

        const {
            results: postsResultRef,
            loading: postsLoadingRef,
            createPromise: getAllPosts,
        } = usePromise(() => PostService.getAll());
        
        getAllPosts(); //get all posts by initialize component

        const {
            results: postDeleteResultRef,
            loading: postDeleteLoadingRef,
            createPromise: deletePost,
        } = usePromise((id) => PostService.delete(id).then((result) => ({ ...result, removedId: id })));

        watch(postsResultRef, (postsResult) => {
            posts.value = postsResult.data;
        });

        watch(postDeleteResultRef, (postDeleteResult) => {
        if (postDeleteResult.status === 200) {
            posts.value = posts.value.filter((item) => item.id != postDeleteResult.removeId);
            // unref(posts).splice(/* remove postDeleteResult.removedId */);
        }
    });

        const loading = computed(() => [postsLoadingRef, postDeleteLoadingRef].map(unref).some(Boolean));

        return { posts, post, loading };
    },
};
</script>

【问题讨论】:

  • 钩子被误用了。应该在初始化设置时调用钩子,而不是在 deletePost 等内部。
  • 但是如果我们在设置中多次调用这个钩子会很混乱,当它在我认为它更具可读性的方法中时,不是吗?
  • 重点不在于可读性,而在于正确使用钩子。那样使用它是不正确的,它不是为这样使用而设计的。它使用可以绑定在模板中并与计算值结合的 refs。如果您仍然到处都有createPromise().then(() =&gt;...,则用钩子包裹承诺不会受益,您可以在不使用引用的情况下在then 中获得相同的结果。

标签: vue.js vuejs3 vue-composition-api


【解决方案1】:

一个 ref 保持对一个应该存在于整个组件生命周期中的值的反应性引用。它在组件的其他地方保持响应 - 模板、计算属性、观察者等。

usePromise 之类的钩子应该设置在 setup 函数内部(因此得名):

const { results, loading, createPromise } = usePromise(() => PostService.getAll()

对于多个请求,可以组合多个hook结果:

const posts = ref([]);

const { results: postsResultRef, loading: postsLoadingRef, createPromise: getAllPosts } = usePromise(() =>
  PostService.getAll()
);

const { results: postDeleteResultRef, loading: postDeleteLoadingRef, createPromise: deletePost } = usePromise(id =>
  PostService.delete(id).then(result => ({...result, removedId: id }))
);

...

watch(postsResultRef, postsResult => {
  posts.value = postsResult.data
});

watch(postDeleteResultRef, postDeleteResult => {
  if (postDeleteResult.status === 200)
    unref(posts).splice(/* remove postDeleteResult.removedId */)
});

...

const loading = computed(() => [postsLoadingRef, postDeleteLoadingRef, ...].map(unref).some(Boolean))

getAllPosts 等应该用作回调,例如在模板中,它返回的承诺通常不需要显式处理和链接,因为它的当前状态已经反映在钩子结果中。这表明钩子中存在潜在缺陷,因为createPromise 参数在结果可用时是未知的,这需要为删除结果显式提供参数。

【讨论】:

  • 我用你的答案更新了我的问题,在第一个钩子之后我调用 getAllPosts(); - 这是正确的称为钩子的乳清吗?
  • 一个更适合非阻塞异步效果的地方是onMounted钩子。但是,是的,可以像你一样在setup body 中调用它。
  • 你能解释一下为什么我不能使用 posts.value = posts.value.filter(...) 删除帖子,以及在你的代码中使用 unref(posts) 的目的是什么
  • unref 是获取posts.value 值的惯用方式,splice 是过滤元素的更有效方式,因为 Vue 支持可变数组。你可以使用那些你想要的。
  • 非常感谢您的帮助,我弄错了 posts.value.filter((item) => item.id != postDeleteResult.removeId) = removedId - 这就是它无法正常工作的原因
【解决方案2】:

问题只是第一个loading ref 是从setup() 返回的。其他的在每个方法中是隐藏和未使用的。

一种解决方案是跟踪state 中的活动loading ref,从setup() 返回:

  1. 声明state.loading

    export default {
      setup() {
        const state = reactive({
          //...
          loading: null,
        })
    
        //...
      }
    }
    
  2. state.loading 设置为每个方法中的loading 引用。

    const fetchPosts = () => {
      const { results, loading, createPromise } = usePromise(/*...*/)
      state.loading = loading
      //...
    }
    
    const deletePost = (id) => {
      const { results, loading, createPromise } = usePromise(/*...*/)
      state.loading = loading;
      //...
    }
    
    const onSubmit = () => {
      const { results, loading, createPromise } = usePromise(/*...*/)
      state.loading = loading
      //...
    }
    
  3. 删除最初从setup() 返回的loading ref,因为我们已经有state.loading,而toRefs(state) 将把loading 暴露给模板:

    export default {
      setup() {
        //...
    
        //return { toRefs(state), loading }
        //                        ^^^^^^^
        return { toRefs(state) }
      }
    }
    

demo

【讨论】:

  • 多个加载状态会以这种方式相互覆盖。
  • @EstusFlask 假设多个 Promise 同时运行(例如,同时添加帖子和删除帖子)。问题中没有任何迹象表明这一点。
猜你喜欢
  • 2021-05-08
  • 1970-01-01
  • 2021-01-25
  • 2021-04-22
  • 1970-01-01
  • 2020-07-01
  • 2021-12-24
  • 2021-02-13
  • 2021-10-20
相关资源
最近更新 更多