V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
imherer
V2EX  ›  程序员

请教一个 Vue 请求后端 API 顺序问题

  •  
  •   imherer · 10 天前 · 2907 次点击

    有 A 、B 两个 API ,其中 B 的结果需要依赖 A 的结果来组装数据(类似于 B 是用户表存了角色 ID ,界面显示的时候需要依赖 A 角色表里的角色名称)

    之前的做法是在onMounted里先调用 A 然后再调用 B ,其中 A 被调用后我就把结果存起来了,只用调用这一次

    现在我封装了一个分页组件,于是我不在onMounted里调用 B 了,直接watch页码和页大小有变化的时候调用 B ,但是这样会导致一个问题:就是页面一加载的时候 watch 就会生效,导致 B 比 A 先执行。但是我又不想把 A 也放在watch里,因为它的数据几乎是不会变的,类似于角色表,页面加装的时候调用一次即可

    大佬们有什么思路吗?

    42 条回复    2024-05-14 09:00:34 +08:00
    Baymaxbowen
        1
    Baymaxbowen  
       10 天前
    把 A 放在父组件能调用?通过 props 传入结果到你的分页组件?
    imherer
        2
    imherer  
    OP
       10 天前
    @Baymaxbowen B 接口也是在父组件里调用的,分页组件它只处理页码或者页大小的变化,变化后会在父组件的 watch 里监控它 然后调用 B
    InDom
        3
    InDom  
       10 天前   ❤️ 1
    再包一两层?把 A B 分别包成两个函数,onMounted 里面调用 C ,C 取判断如果有 A 就调用 B ,如果没有 A 就先调用 A 。

    这样你只需要找调用 B 的地方调用 C 即可。
    imherer
        4
    imherer  
    OP
       10 天前
    @InDom 有道理,这个方法可行
    shenleiyin
        5
    shenleiyin  
       10 天前
    你可以考虑在你的分页组件中引入一个状态来表示 A 是否已经加载完成。这样,在组件加载时,你可以首先检查是否已经加载了 A 的数据。如果没有,那么首先加载 A ,然后再加载 B 。

    下面是一个简单的示例代码,说明了这个思路:

    import { ref, watch, onMounted } from 'vue';

    export default {
    setup() {
    // 用于存储 A 的数据
    const dataA = ref(null);
    // 用于表示 A 是否已经加载完成
    const isDataALoaded = ref(false);

    // 模拟加载 A 的数据
    function fetchDataA() {
    // 这里假设 fetchDataA 是异步操作
    setTimeout(() => {
    // 模拟从 API 获取到的 A 的数据
    const result = /* 调用 A 的 API */;
    // 存储 A 的数据
    dataA.value = result;
    // 标记 A 已加载完成
    isDataALoaded.value = true;
    }, 1000); // 假设加载 A 的数据需要 1 秒钟
    }

    // 模拟加载 B 的数据
    function fetchDataB(page, pageSize) {
    // 这里假设 fetchDataB 是异步操作
    setTimeout(() => {
    // 只有在 A 的数据加载完成后才调用 B 的 API
    if (isDataALoaded.value) {
    // 这里可以使用 A 的数据来组装 B 的数据
    const result = /* 调用 B 的 API ,依赖于 A 的数据 */;
    console.log(result);
    } else {
    // 如果 A 的数据尚未加载完成,则等待 A 加载完成后再调用 B 的 API
    console.log("Waiting for A to load...");
    }
    }, 500); // 假设加载 B 的数据需要 0.5 秒钟
    }

    // 监听页码和页大小的变化,当它们变化时调用 fetchDataB
    watch([currentPage, pageSize], ([newPage, newPageSize], [oldPage, oldPageSize]) => {
    fetchDataB(newPage, newPageSize);
    });

    // 在组件加载时,如果 A 的数据尚未加载,则先加载 A 的数据
    onMounted(() => {
    if (!isDataALoaded.value) {
    fetchDataA();
    }
    });

    return {
    dataA,
    isDataALoaded,
    };
    },
    };

    这样,无论在分页组件加载时还是在页码或页大小变化时,都会先检查 A 的数据是否已经加载完成,然后再决定是否加载 B 的数据。
    corcre
        6
    corcre  
       10 天前
    A 返回数据再动态绑定 B 的属性?好像是 this.$set 啥的
    (但是好久不写 vue 了, 不知道是不是这么个思路
    Stlin
        7
    Stlin  
       10 天前
    这个 A 接口是不是在系统中别的页面也需要用到的?就是这个角色是不是全局的,如果是这个就好办啦,直接放在入口( main.js 或者 router 钩子)请求后存 session 或 local storage ,这样你在别的页面直接取就行了。
    HTML001
        8
    HTML001  
       10 天前
    提取一段公用代码,用来判断是否有 A 的数据,有就直接调用 B ,没有则先 A 后 B 。之前需要调用 B 方法的地方,都用这个公用代码就行
    imherer
        9
    imherer  
    OP
       10 天前
    @corcre 对的,A 返回数据再动态绑定 B 的属性。

    我查查
    imherer
        10
    imherer  
    OP
       10 天前
    @Stlin 就当前页面用得到。A 的数据虽然大概率不会改,但是还是有改的情况,所以想法还是在这个页面加装的时候请求一次最新的数据最好
    imherer
        11
    imherer  
    OP
       10 天前
    @HTML001 嗯,有点类似于 3 楼的做法
    rcocco
        12
    rcocco  
       10 天前
    上 tanstack query/vue ,你在任意地方调用一次,之后随便取就行了。
    想长时间有效不重新请求就设置失效时间,想提前失效重新加载也有对应的 API
    imherer
        13
    imherer  
    OP
       10 天前
    @shenleiyin AI 回答的吧?这个有个问题,假如 A 请求超过 1s 的话会导致 B 不会执行啊
    sqlNice
        14
    sqlNice  
       10 天前
    分页组件 props 接收一个 beforeSearch 。在 watch 监听到变化执行你的 getData 方法(也就是 B )时 async 调用 beforeSearch 方法。
    corcre
        15
    corcre  
       10 天前
    imherer
        16
    imherer  
    OP
       10 天前
    @rcocco 看了下,这玩儿挺强大呀,好像还可以做限流?
    lizy0329
        17
    lizy0329  
       10 天前
    1. 用 watch 监控
    2. 用 vue-query
    imherer
        18
    imherer  
    OP
       10 天前
    @lizy0329 vue-query 是不是和 12L 说的 tanstack query/vue 一个功能?
    zogwosh
        19
    zogwosh  
       10 天前
    你可以在 onMounted 里调用 watch
    duanxianze
        20
    duanxianze  
       10 天前
    b 执行的时候判断是否有 userid 就行,没有直接 return,不用搞那么复杂,watch 里添加对 userid 变化的监测
    shenleiyin
        21
    shenleiyin  
       10 天前
    @imherer 对的,感觉就是这么回事,然后就贴这儿了。
    gitignore
        22
    gitignore  
       10 天前   ❤️ 1
    我可能会这样写:


    ```typescript
    type Role = { id: string; name: string };

    function usePage<T>() {
    const role = ref<Role | null>(null)
    const loading = ref(false);
    const page = reactive({ current: 1, size: 10 });
    const uri = computed(() => `/api/b?page=${page.current}&size=${page.size}`);
    const { data, error, execute } = useFetch(uri, { immediate: false }).json<T>()

    // 防抖避免快速点击翻页按钮发送无效请求
    const onLoadPageData = debounce(async () => {
    try {
    loading.value = true;
    await initRole();
    await execute();
    } finally {
    loading.value = false;
    }
    }, 200)

    async function initRole() {
    if (role.value) return;
    const { data, error } = await useFetch("/api/a").json<Role>();
    if (error.value) {
    /** @todo error handler */
    } else {
    role.value = data.value;
    onLoadPageData();
    }
    }

    initRole();
    watch(page, onLoadPageData, { immediate: true });

    return { page, data, error, loading, onLoadPageData }
    }
    ```
    freezebreze
        23
    freezebreze  
       10 天前
    watch 到变化 emit 一个事件让父组件件调用接口 B
    M003
        24
    M003  
       10 天前
    是不是应该配置 immediate 为 false
    mwjz
        25
    mwjz  
       10 天前
    我封装封装通用组件的思路,

    1. 传递一个获取数据的函数,而不是一个简单的路由,这个函数参数为页码条数据。
    props: {
    auto: Boolean
    dataFunc: Function as PropType<
    (page: PaginationRequest) => Promise<XOR<PaginationResponse, Array<any>>>
    >,
    }

    传递函数的原因是增加可控性,比如请求条件,数据过滤、转换,接口依赖等, 全部可以交给外部控制。

    过滤条件,不同的业务可以做到随便控制。
    const getData = (page: PaginationRequest) => {
    // if (xxx) {
    // return []
    // }
    return SupervisionService.load().filingList(page, unref(filter));


    };

    而如果使用传递条件,路由给通用组件方案,通用组件在业务变更、需求不同情况下,会越来越臃肿,相信我,后面代码没法维护的。。。。


    2. 提供了一个 auto 参数, 设置 auto ,会自动首次请求。 这个参数是因为业务上,首次请求时机 有时候会根据筛选条件决定,
    props: {
    auto: Boolean
    dataFunc: Function as PropType<
    (page: PaginationRequest) => Promise<XOR<PaginationResponse, Array<any>>>
    >,
    }
    oouz
        26
    oouz  
       10 天前
    你是不是给 watch 加了 immediate: true ? watch 默认是懒执行的:仅当数据源变化时,才会执行回调。
    ixixi
        27
    ixixi  
       10 天前
    提供一个我们在用的一个思路 用 rxjs
    eurkidu
        28
    eurkidu  
       10 天前
    按现有逻辑
    方案一,现有逻辑,类似 3 楼思路,合并 A ,B 调用为一个新函数 C ,里面对 A 做单例执行,如果 A 数据有值不重复请求 A 数据。

    方案二,把 onMounted 里触发的 A ,前置到 beforeRouteEnter ,保证 A 数据加载了之后,再 next 渲染页面,即把 页面 非表格数据的 loading ,前置到 路由的 loading 中,这也是一种常见的写法。

    方案三,一般个人写这种页面,不会去 watch 分页参数,或者即便 watch 也会去掉 immediate 触发,因为大概率 页面首次初始化逻辑是不同于切换分页组件的时候触发逻辑的(当然,对于简单的 CRUD 页面来说可能是一样的)。如果不 watch 分页参数,那逻辑就变成自然的,首次渲染 A + B ,切换分页组件的时候,触发 B 的逻辑。

    不 watch 分页的一些原因,主要因为 watch immediate 会在组件 created 的时候直接触发,加上如果修改 watch 参数的逻辑因为组件封装的复杂度,导致用户一次操作,同时修改了 watch 对象的 2 个值,并且 2 次修改有类似 nextTick 的分隔,会导致触发 2 次,如果是手动控制的 change ,就可以规避(依稀记得当年 element-ui 1.0 的时代,el-select 的 change 事件是直接类似 watch 的逻辑,每次修改 v-model 的值,都会触发 change ,写省市区级联选择框的时候,一言难尽,后面 v2 版本就直接改逻辑了,只有用户的操作才会触发 change )

    所以 OP 逻辑里面的 分页调用 B ,从封装组件逻辑角度出发,我是不建议直接 watch 触发逻辑的,应该是用户主动切换页码的时候,才触发 change ,可举个简单的例子,比如需要列表跳转 url 进入详情,详情返回列表还是在原来的页码,一般可通过 url 参数保留之前的页码,这时候 页面初始化会带有比如 ?page=2 这样的参数,如果内部分页组件直接 watch 不做特殊处理,那默认请求第 1 页,拿到 url 参数,再修改当前页为第 2 页,则会额外触发一次分页请求,但是如果不是 watch 实现的,分页触发只会在用户手动切换分页的时候触发,初始化的时候因为第一页逻辑自己控制,可以灵活的处理各类情况。
    eurkidu
        29
    eurkidu  
       10 天前
    @eurkidu 还有一个常见方案,即如果你这个 A 只是 B 表格数据显示的时候,比如某列是字典表,其他配置表,B 返回数据里面存的是 id ,显示的时候要显示成 A 返回里面对应的 name 。

    可以直接写一个计算属性,按照 A 返回的数据,生成新的显示数据
    ```
    get viewTableData() {
    // 如果能保证 roleData 一定有值的话,加这个可以让 loading 中的表格显示效果更好
    if (this.roleData.length === 0) return []
    const data = JSON.parse(JSON.stringify(this.tableData))
    data.forEach(row => {
    row.roleName = this.roleData.find(v => v.id === row.roleId)?.name ?? ''
    })
    return data
    }
    ```

    然后可以对表格加 loading ,A 数据未返回前,表格处于 loading 中,这样就可以不用关心 A ,B 数据的加载顺序,当然这个逻辑比较偏向固定这个逻辑的页面的写法,不适合通用组件封装的逻辑。
    Curtion
        30
    Curtion  
       10 天前
    A 在路由中请求也行
    qiaobeier
        31
    qiaobeier  
       10 天前
    典型的异步编程的需求,custom event ,callback ,promise 甚至轮询都可以做到。
    imherer
        32
    imherer  
    OP
       10 天前
    @eurkidu 确实,你说的有道理,如果有 url 带 page 的情况,确实会出现额外出发一次分页请求,不过目前这个页面的场景不会出现
    realJamespond
        33
    realJamespond  
       10 天前
    等 A 执行完后放全局,再渲染分页 onMounted 中调 B ?
    imherer
        34
    imherer  
    OP
       10 天前
    @M003
    @oouz 是的,immediate 设置为 false ,然后在 onMounted 里分别调用 A ,B 也能解决问题
    a4854857
        35
    a4854857  
       10 天前
    最简单的做法就是把 A 放在 B 函数里面, 如果发现没有 A 数据就直接先 await A. mounted 不做任何操作
    vialon17
        36
    vialon17  
       10 天前
    添加一个 A 的控制变量就行,watch B 的时候检查一下,
    有了就直接请求 B ,没了就请求一下 A 。
    或者既然 A 用户表不怎么改变,直接 created 的时候请求存一下不就可以了?
    zhouS9
        37
    zhouS9  
       10 天前
    虽然数据上有依赖关系,但是请求可以各走各的,都返回了就正常显示,否则就等待,vue 的响应视图会自动处理好
    wzzx
        38
    wzzx  
       10 天前
    rxjs 哪个页面依赖这个 A 数据 就订阅一下 这样可以保证拿到 A 数据后再执行后面的逻辑
    wzzx
        39
    wzzx  
       10 天前
    const aData$ = new Subject()
    TangYuSen
        40
    TangYuSen  
       10 天前
    最直接的,就是你在封装分页组件的时候,给这个组件添加一个属性 is_init,

    ```
    const is_init = ref(false)

    // watch 的时候判断一下
    if(is_init) return
    isInit.value = true
    //调用 B
    ```
    这样就可以避免第一次页面加载的时候调用 B 了,
    coderzhangsan
        41
    coderzhangsan  
       10 天前
    题外话:本人 PHP 后端,我一般都是直接做好数据一个接口返给前端,即便是后续业务迭代,我也是这样,免得对接时候问来问去的,搞不好就出错。
    Foxii
        42
    Foxii  
       9 天前
    @imherer 这俩就是一个东西 vue-query: 'This package was migrated to be a part of https://github.com/TanStack/query'
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1709 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 41ms · UTC 16:50 · PVG 00:50 · LAX 09:50 · JFK 12:50
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.