请教一个 Vue 请求后端 API 顺序问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
imherer
V2EX    程序员

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

  •  
  •   imherer 2024-05-13 09:40:45 +08:00 4367 次点击
    这是一个创建于 582 天前的主题,其中的信息可能已经有所发展或是发生改变。

    有 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  
       2024-05-13 09:43:21 +08:00
    把 A 放在父组件能调用?通过 props 传入结果到你的分页组件?
    imherer
        2
    imherer  
    OP
       2024-05-13 09:45:07 +08:00
    @Baymaxbowen B 接口也是在父组件里调用的,分页组件它只处理页码或者页大小的变化,变化后会在父组件的 watch 里监控它 然后调用 B
    InDom
        3
    InDom  
       2024-05-13 09:45:41 +08:00   1
    再包一两层?把 A B 分别包成两个函数,onMounted 里面调用 C ,C 取判断如果有 A 就调用 B ,如果没有 A 就先调用 A 。

    这样你只需要找调用 B 的地方调用 C 即可。
    imherer /td>
        4
    imherer  
    OP
       2024-05-13 09:47:05 +08:00
    @InDom 有道理,这个方法可行
    7tp29JDfB88rpw51
        5
    7tp29JDfB88rpw51  
       2024-05-13 09:47:07 +08:00
    你可以考虑在你的分页组件中引入一个状态来表示 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  
       2024-05-13 09:50:13 +08:00
    A 返回数据再动态绑定 B 的属性?好像是 this.$set 啥的
    (但是好久不写 vue 了, 不知道是不是这么个思路
    Stlin
        7
    Stlin  
       2024-05-13 09:51:26 +08:00
    这个 A 接口是不是在系统中别的页面也需要用到的?就是这个角色是不是全局的,如果是这个就好办啦,直接放在入口( main.js 或者 router 钩子)请求后存 session 或 local storage ,这样你在别的页面直接取就行了。
    HTML001
        8
    HTML001  
       2024-05-13 09:51:42 +08:00
    提取一段公用代码,用来判断是否有 A 的数据,有就直接调用 B ,没有则先 A 后 B 。之前需要调用 B 方法的地方,都用这个公用代码就行
    imherer
        9
    imherer  
    OP
       2024-05-13 09:52:41 +08:00
    @corcre 对的,A 返回数据再动态绑定 B 的属性。

    我查查
    imherer
        10
    imherer  
    OP
       2024-05-13 09:54:27 +08:00
    @Stlin 就当前页面用得到。A 的数据虽然大概率不会改,但是还是有改的情况,所以想法还是在这个页面加装的时候请求一次最新的数据最好
    imherer
        11
    imherer  
    OP
       2024-05-13 09:54:51 +08:00
    @HTML001 嗯,有点类似于 3 楼的做法
    rcocco
        12
    rcocco  
       2024-05-13 09:55:29 +08:00
    上 tanstack query/vue ,你在任意地方调用一次,之后随便取就行了。
    想长时间有效不重新请求就设置失效时间,想提前失效重新加载也有对应的 API
    imherer
        13
    imherer  
    OP
       2024-05-13 09:56:16 +08:00
    @shenleiyin AI 回答的吧?这个有个问题,假如 A 请求超过 1s 的话会导致 B 不会执行啊
    sqlNice
        14
    sqlNice  
       2024-05-13 09:56:17 +08:00
    分页组件 props 接收一个 beforeSearch 。在 watch 监听到变化执行你的 getData 方法(也就是 B )时 async 调用 beforeSearch 方法。
    corcre
        15
    corcre  
       2024-05-13 09:57:30 +08:00
    imherer
        16
    imherer  
    OP
       2024-05-13 10:00:41 +08:00
    @rcocco 看了下,这玩儿挺强大呀,好像还可以做限流?
    lizy0329
        17
    lizy0329  
       2024-05-13 10:01:14 +08:00
    1. 用 watch 监控
    2. 用 vue-query
    imherer
        18
    imherer  
    OP
       2024-05-13 10:06:09 +08:00
    @lizy0329 vue-query 是不是和 12L 说的 tanstack query/vue 一个功能?
    zogwosh
        19
    zogwosh  
       2024-05-13 10:11:01 +08:00
    你可以在 onMounted 里调用 watch
    duanxianze
        20
    duanxianze  
       2024-05-13 10:11:43 +08:00
    b 执行的时候判断是否有 userid 就行,没有直接 return,不用搞那么复杂,watch 里添加对 userid 变化的监测
    7tp29JDfB88rpw51
        21
    7tp29JDfB88rpw51  
       2024-05-13 10:17:15 +08:00
    @imherer 对的,感觉就是这么回事,然后就贴这儿了。
    gitignore
        22
    gitignore  
       2024-05-13 10:41:37 +08:00   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  
       2024-05-13 12:02:29 +08:00
    watch 到变化 emit 一个事件让父组件件调用接口 B
    M003
        24
    M003  
       2024-05-13 12:18:53 +08:00
    是不是应该配置 immediate 为 false
    mwjz
        25
    mwjz  
       2024-05-13 12:24:39 +08:00
    我封装封装通用组件的思路,

    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  
       2024-05-13 13:20:21 +08:00
    你是不是给 watch 加了 immediate: true ? watch 默认是懒执行的:仅当数据源变化时,才会执行回调。
    ixixi
        27
    ixixi  
       2024-05-13 13:25:05 +08:00
    提供一个我们在用的一个思路 用 rxjs
    eurkidu
        28
    eurkidu  
       2024-05-13 13:26:29 +08:00
    按现有逻辑
    方案一,现有逻辑,类似 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  
       2024-05-13 13:41:14 +08:00
    @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  
       2024-05-13 14:05:33 +08:00
    A 在路由中请求也行
    qiaobeier
        31
    qiaobeier  
       2024-05-13 14:05:36 +08:00
    典型的异步编程的需求,custom event ,callback ,promise 甚至轮询都可以做到。
    imherer
        32
    imherer  
    OP
       2024-05-13 14:11:46 +08:00
    @eurkidu 确实,你说的有道理,如果有 url 带 page 的情况,确实会出现额外出发一次分页请求,不过目前这个页面的场景不会出现
    realJamespond
        33
    realJamespond  
       2024-05-13 14:12:39 +08:00
    等 A 执行完后放全局,再渲染分页 onMounted 中调 B ?
    imherer
        34
    imherer  
    OP
       2024-05-13 14:12:45 +08:00
    @M003
    @oouz 是的,immediate 设置为 false ,然后在 onMounted 里分别调用 A ,B 也能解决问题
    a4854857
        35
    a4854857  
       2024-05-13 14:47:16 +08:00
    最简单的做法就是把 A 放在 B 函数里面, 如果发现没有 A 数据就直接先 await A. mounted 不做任何操作
    vialon17
        36
    vialon17  
       2024-05-13 15:43:19 +08:00
    添加一个 A 的控制变量就行,watch B 的时候检查一下,
    有了就直接请求 B ,没了就请求一下 A 。
    或者既然 A 用户表不怎么改变,直接 created 的时候请求存一下不就可以了?
    zhouS9
        37
    zhouS9  
       2024-05-13 16:07:00 +08:00
    虽然数据上有依赖关系,但是请求可以各走各的,都返回了就正常显示,否则就等待,vue 的响应视图会自动处理好
    wzzx
        38
    wzzx  
       2024-05-13 17:40:06 +08:00
    rxjs 哪个页面依赖这个 A 数据 就订阅一下 这样可以保证拿到 A 数据后再执行后面的逻辑
    wzzx
        39
    wzzx  
       2024-05-13 17:40:56 +08:00
    const aData$ = new Subject()
    TangYuSen
        40
    TangYuSen  
       2024-05-13 18:02:30 +08:00
    最直接的,就是你在封装分页组件的时候,给这个组件添加一个属性 is_init,

    ```
    const is_init = ref(false)

    // watch 的时候判断一下
    if(is_init) return
    isInit.value = true
    //调用 B
    ```
    这样就可以避免第一次页面加载的时候调用 B 了,
    coderzhangsan
        41
    coderzhangsan  
       2024-05-13 19:28:14 +08:00
    题外话:本人 PHP 后端,我一般都是直接做好数据一个接口返给前端,即便是后续业务迭代,我也是这样,免得对接时候问来问去的,搞不好就出错。
    Foxii
        42
    Foxii  
       2024-05-14 09:00:34 +08:00
    @imherer 这俩就是一个东西 vue-query: 'This package was migrated to be a part of https://github.com/TanStack/query'
    关于     帮助文档     自助推广系统     博客   &bsp; API     FAQ     Solana     882 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 60ms UTC 21:12 PVG 05:12 LAX 13:12 JFK 16:12
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86