如何在 Vue 3 中的组件中多次挂钩

How to hook multiple times in component in Vue 3

我在一节课中看到我们可以使用组合创建 api hook 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>

问题只是第一个 loading 引用从 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

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 等应该用作回调,例如在模板中,promise it returns 通常不需要显式处理和链接,因为它的当前状态已经反映在挂钩结果中。这表明挂钩中存在潜在缺陷,因为 createPromise 参数在结果可用时是未知的,这需要为删除结果明确提供参数。