Ajax GET:多个数据特定调用,还是较少的非特定调用?

时间:2019-07-05 10:24:28

标签: javascript node.js ajax

我正在使用Node.js / express后端和MongoDB作为数据库开发Web应用程序。

下面的示例适用于管理仪表板页面,在该页面中,我将显示带有与网站上用户相关的不同信息的卡片。我可能想在示例页面上显示-例如:

  1. 每种类型的用户数
  2. 每种用户类型最常见的位置
  3. 每月有多少次注册
  4. 最受欢迎的职位

我可以在一条路由中完成所有操作,在该路由中,我有一个控制器来执行所有这些任务,并将它们作为对象捆绑到url,然后可以使用ajax提取数据。或者,我可以将每个任务拆分为自己的路由/控制器,并分别对每个任务进行ajax调用。我要决定的是在单个页面上进行多个Ajax调用的最佳做法是什么


示例:

我正在建立一个页面,在其中我将使用DataTables为不同类型的用户(目前有两个:mentorsmentees)制作一个交互式表格。这个示例仅需要两个数据请求(每种用户类型一个),但是我的最终页面将更像10。

对于每种用户类型,我针对每种用户类型进行ajax get调用,并根据返回的数据构建表:

用户类型1-导航

$.get('/admin/' + id + '/mentees')
    .done(data => {
        $('#menteeTable').DataTable( {
            data: data,
            "columns": [
                { "data": "username"},
                { "data": "status"}
            ]
        });
    })

用户类型2-导师

$.get('/admin/' + id + '/mentors')
    .done(data => {
        $('#mentorTable').DataTable( {
            data: data,
            "columns": [
                { "data": "username"},
                { "data": "position"}
            ]
        });
    })

这然后在我的Node.js后端中需要两条路由:

router.get("/admin/:id/mentors", getMentors);
router.get("/admin/:id/mentees", getMentees);

和两个结构相同的控制器(但针对不同的用户类型过滤):

getMentees(req, res, next){
    console.log("Controller: getMentees");
    let query = { accountType: 'mentee', isAdmin: false };
    Profile.find(query)
        .lean()
        .then(users => {
            return res.json(users);
        })
        .catch(err => {
            console.log(err)
        })
}

这很好。但是,由于我需要发出多个数据请求,因此我想确保以正确的方式构建它。我可以看到几个选项:

  1. 为每种数据类型进行单独的ajax调用,并在后端进行繁重的操作(例如,统计用户类型并返回)-如上所述
  2. 为每种数据类型进行单独的ajax调用,但在前端进行繁重的操作。在上面的示例中,我可以很容易地过滤掉我的ajax调用返回的isAdmin上的data个用户
  3. 减少请求较少精炼数据的ajax调用。在上面的示例中,我可以对所有用户进行一次呼叫(仅需要一个路由/控制器),然后在前端过滤data来构建两个表

花费的数据采购时间

方面,我想就哪种策略最有效提供一些建议

更新

为澄清这个问题,我可以使用类似以下的控制器设置来达到与上述相同的结果:

Profile.find(query)
    .lean()
    .then(users => {
        let mentors = [],
        mentees = []

        users.forEach(user => {
            if(user.accountType === 'mentee') {
                mentees.push(user);
            } else if (user.accountType === 'mentor') {
                mentors.push(user);
            }
        });
        return res.json({mentees, mentors});
    })

然后进行一个ajax调用,并相应地拆分数据。我的问题是:哪个是首选选项?

6 个答案:

答案 0 :(得分:2)

TL; DR:选项1

IMO,我不会将未处理的数据提供给前端,这可能会出错,您可能会发现太多信息,未指定的客户端计算机可能要处理很多(可能是带宽有限的低功耗设备) (例如电池电量),您希望获得顺畅的用户体验,并且客户端上的javascript从大量数据中搅乱信息会对此有所影响。我使用后端进行处理(根据需要准备信息),使用JS来获取信息并将其(AJAX)放置在页面上,并使用诸如切换元素状态之类的东西,以及使用CSS来进行任何移动(动画和过渡等) ),然后再使用JS。 同样对于路由,我的方法是每个不同的信息包(dataTable)都有一条路由,因此您不会因为太多目的而使方法过载,请保持其简单性和可维护性。您始终可以提取出所有相同且经常重复的内容。

为回答您的问题,我将选择选项1。 您还可以提供一个“页面加载”端点,然后,如果有任何更改,以后将使用它们的不同端点来更新各个表。最初的“页面加载”调用可以整理来自后端端点的信息,并用作数据包以初始填充所有表。一个初始请求包含大量定义良好的数据,然后可以在用户请求时更新单个表(如果需要,则可以进行推送)。

答案 1 :(得分:2)

这是一个很好的问题。首先,您应该认识到应用程序如何处理接收到的数据。如果海量数据没有在前端更改,而是具有不同的视图,并且这些视图的全部数据需求,则可能会将其缓存到前端(例如用户设置数据-应用程序始终会读取但很少更改),那么您可以使用< em>您的第二个选择。在其他情况下,如果前端仅处理大量数据库数据(例如特定用户的日志数据)中的一小部分,则最好在服务器端您的第一个和第三个选项上进行预处理(过滤)。实际上,对于我来说,第二个选项仅适用于在前端缓存未更改的数据。

澄清问题后,您可以对请求和lodash库进行分组:

Profile.find(query)
    .lean()
    .then(users => {
        let result = [];    

       result = _(users)
        .groupBy((elem) => elem.accountType)
        .map((vals, key) => ({accountType: key, users: vals}))
        .value(); 
        });
        return res.json(result);
    });

当然,您可以根据需要映射数据。这种方式可以获取所有类型的帐户(不仅是“ mentee”和“ mentor”)

答案 2 :(得分:2)

通常,这种体系结构中包含三件事:

1. Client 
2. API Gateway
3. Micro services (Servers)

在您的情况下:

1. Client is JS application code
2. API Gateway + Server is Nodejs/express (Dual responsibility)

要注意的第1点

服务器仅提供核心API。因此,用于服务器的此API只能是用户api,例如:

/users?type={mentor/mentee/*}&limit=10&pageNo=8

也就是说,任何人都可以使用类型查询字符串来请求所有数据或过滤后的数据。

要注意的第二点

由于网页由多个数据点组成,并且对同一服务器的每个数据点进行调用会增加往返次数,并使UX更糟,因此存在API网关。因此,在这种情况下,JS不会直接与核心服务器通信,而是与API Gateway进行通信,并且API类似于:

/home

上述API内部调用以下API,并将数据与指导者和受指导者列表汇总在一个json中

/users?type={mentor/mentee/*}&limit=10&pageNo=8

此API只是通过查询属性将调用传递给核心服务器

现在,由于在您的代码中,API网关和Core服务器已合并到单个层中,因此这是您应该设置代码的方式:

getHome(req, res, next){
    console.log("Controller: /home");
    let queryMentees = { accountType: 'mentee', isAdmin: false };
    let queryMentors = { accountType: 'mentor', isAdmin: true };

    mentes  = getProfileData(queryMentees);
    mentors = getProfileData(queryMentors);
    return res.json({mentes,mentors});
}

getUsers(req, res, next){
    console.log("Controller: /users");
    let query = {accountType:request.query.type,isAdmin:request.query.isAdmin};
    return res.json(getProfileData(query));
}

以及具有以下功能的常见ProfileService.js类:

getProfileData(query){
  Profile.find(query)
        .lean()
        .then(users => {
            return users;
        })
        .catch(err => {
            console.log(err)
        })
}

有关API网关模式here

的更多信息

答案 3 :(得分:1)

如果您无法估算应用程序需要多少类型,则需要使用参数, 如果我像这样编写应用程序,那么我不会编写用于调用ajax的多个函数,也不会编写多个路由和控制器,

像这样的客户端

!SESSION 2019-07-13 02:37:18.785
----------------------------------------------- eclipse.buildId=unknown java.fullversion=8.0.5.30 - pxa6480sr5fp30-20190207_01(SR5 FP30) JRE 1.8.0 Linux amd64-64-Bit Compressed References 20190124_408237 (JIT enabled, AOT enabled) OpenJ9   - 9c77d86 OMR      - dad8ba7 IBM      - e2996d1 BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=en_US Framework arguments:  -product com.ibm.rational.rbu.product.v97.rbu -buildfile /opt/IBM/SDP/samples/AutoBuild/AdderBuild.xml importAndBuildEverything Command-line arguments:  -product com.ibm.rational.rbu.product.v97.rbu
-data /tmp/ws -os linux -ws gtk -buildfile /opt/IBM/SDP/samples/AutoBuild/AdderBuild.xml importAndBuildEverything

!ENTRY org.eclipse.equinox.app 0 0 2019-07-13 02:37:20.092 !MESSAGE Product com.ibm.rational.rbu.product.v97.rbu could not be found.

!ENTRY org.eclipse.jetty.plus 2 0 2019-07-13 02:37:20.465 !MESSAGE Could not resolve module: org.eclipse.jetty.plus [2158]   Bundle was not resolved because of a uses contraint violation.   org.osgi.service.resolver.ResolutionException: Uses constraint violation. Unable to resolve resource org.eclipse.jetty.plus [osgi.identity; type="osgi.bundle"; version:Version="9.4.8.v20171121"; osgi.identity="org.eclipse.jetty.plus"] because it is exposed to package 'org.eclipse.jetty.util.log' from resources org.eclipse.jetty.util [osgi.identity; type="osgi.bundle"; version:Version="9.4.8.v20171121"; osgi.identity="org.eclipse.jetty.util"] and org.eclipse.jetty.util [osgi.identity; type="osgi.bundle"; version:Version="9.4.11.v20180605"; osgi.identity="org.eclipse.jetty.util"] via two dependency chains.

Chain 1:   org.eclipse.jetty.plus [osgi.identity; type="osgi.bundle"; version:Version="9.4.8.v20171121"; osgi.identity="org.eclipse.jetty.plus"]
    import: (&(osgi.wiring.package=org.eclipse.jetty.util.log)(&(version>=9.4.8)(!(version>=9.4.9))))
     |
    export: osgi.wiring.package: org.eclipse.jetty.util.log   org.eclipse.jetty.util [osgi.identity; type="osgi.bundle"; version:Version="9.4.8.v20171121"; osgi.identity="org.eclipse.jetty.util"]

Chain 2:   org.eclipse.jetty.plus [osgi.identity; type="osgi.bundle"; version:Version="9.4.8.v20171121"; osgi.identity="org.eclipse.jetty.plus"]
    import: (&(osgi.wiring.package=org.eclipse.jetty.jndi)(&(version>=9.4.8)(!(version>=9.4.9))))
     |
    export: osgi.wiring.package=org.eclipse.jetty.jndi; uses:=org.eclipse.jetty.util.log   org.eclipse.jetty.jndi [osgi.identity; type="osgi.bundle"; version:Version="9.4.8.v20171121"; osgi.identity="org.eclipse.jetty.jndi"]
    import: (&(osgi.wiring.package=org.eclipse.jetty.util.log)(&(version>=9.4.0)(!(version>=10.0.0))))
     |
    export: osgi.wiring.package: org.eclipse.jetty.util.log   org.eclipse.jetty.util [osgi.identity; type="osgi.bundle"; version:Version="9.4.11.v20180605"; osgi.identity="org.eclipse.jetty.util"]

!ENTRY org.eclipse.osgi 4 0 2019-07-13 02:37:20.466 !MESSAGE Application error !STACK 1 java.lang.RuntimeException: No application id has been found.
        at org.eclipse.equinox.internal.app.EclipseAppContainer.startDefaultApp(EclipseAppContainer.java:242)
        at org.eclipse.equinox.internal.app.MainApplicationLauncher.run(MainApplicationLauncher.java:29)
        at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:134)
        at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:104)
        at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:388)
        at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:243)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:90)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:55)
        at java.lang.reflect.Method.invoke(Method.java:508)
        at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:653)
        at org.eclipse.equinox.launcher.Main.basicRun(Main.java:590)
        at org.eclipse.equinox.launcher.Main.run(Main.java:1499)
        at org.eclipse.equinox.launcher.Main.main(Main.java:1472)

我更喜欢客户端

let getQuery = (id,userType)=>{
$.get('/admin/' + id + '/userType/'+userType)
.done(data => {
    let dataTable = null;
    switch(userType){
        case "mentee":
        dataTable = $('#menteeTable');
        break;
        case "mentor":
        dataTable = $('#mentorTable');
        break;
        //.. you can add more selector for datatables but I wouldn't prefer this way you can generate "columns" property on server like "data" so meaning that you can just use one datatable object on client side
    }
    dataTable.DataTable( {
        data: data,
        "columns": [
            { "data": "username"},
            { "data": "status"}
        ]
    });
})
}

在这种情况下,服务器响应应支持{data:[{} ...],列:[{} ....]} Datatables examples

这样的服务器端 路由器仅一个

let getQuery = (id,userType)=>{
$.get('/admin/' + id + '/userType/'+userType)
.done(data => {
    $('#dataTable').DataTable( {
        data: data.rows,
        "columns": data.columns
        ]
    });
})
}

控制器

router.get("/admin/:id/userType/:userType", getQueryFromDB);

对于我来说,关于您的问题的主要含义是,指导者,指导者等...是诸如“ id”之类的参数

  

确保您的身份验证检查了哪些用户可以访问我的代码示例和您的代码的userType数据,有人可以通过更改路由来访问您的数据

周末愉快

答案 4 :(得分:1)

  1. 从用户设备上ui的性能和平滑性来看: 当然,最好对所有核心数据执行1个ajax请求(这对于尽快显示很重要),并且可能会对优先级较低的数据执行更多请求,而延迟很小。或执行2个请求:一个用于“快速”数据,另一个用于“慢速”(如果适用),因为:

一方面,许多ajax请求可能会降低ui的速度,同时完成ajax请求的数量可能会受到限制(取决于浏览器,可能是2到10),因此如果是ex。在ie中将有2个限制,然后有10个ajax,将有一个等待ajax请求的队列

但是另一方面,如果要显示的数据太多或某些数据需要花费较长的准备时间,则可能会导致等待较长时间的后端响应以显示某些内容。

繁重的工作:无论如何都不能在UI端进行此类操作,因为: 用户设备的资源不足且速度慢。 Javascript是同步的,因此,任何长循环“冻结” UI都需要运行该循环所需的时间。

讨论过滤用户:

Profile.find(query)
    .lean()
    .then(users => {
        let mentors = [],
        mentees = []

        users.forEach(user => {
            if(user.accountType === 'mentee') {
                mentees.push(user);
            } else if (user.accountType === 'mentor') {
                mentors.push(user);
            }
        });
        return res.json({mentees, mentors});
    })

似乎有一个问题,可能查询将具有排序和限制,如果最终结果不一致,则最终可能只有受训者或只有受训者,我认为您仍然应该对数据存储进行2个单独的查询

  1. 从项目的结构,可维护性,灵活性,可重用性等方面来看,当然最好使事物尽可能地分离。

因此,最后,假设您做了: 1.许多微服务都类似于每个小部件1后端微服务,但是有一层允许聚合结果以优化1-2 ajax查询中来自UI的流量。 2.许多ui模块各自从某个服务接收到各自使用的数据,这些ui模块执行1-2调用以聚合后端,并将接收到的不同数据集分配给许多前端模块。

答案 5 :(得分:-1)

在后端只需制作一个动态参数方法API。您可以将mentor,mentee,admin等作为角色传递。您应该具有某种类型的用户身份验证和授权,以检查用户a是否可以看到角色B中的用户。 关于UI,取决于用户,他们希望页面具有下拉过滤器,或者希望URL进行书签。 像多个网址/ admin / mentor等。 或一个带有querystring和下拉列表的网址。/user?role=mentor,/user?role=admin。 基于URL,您必须使控制器。我通常更喜欢下拉并获取数据(默认情况下,所有指导者都可能是该选择)。 这是一种特别的邀请,适合浪漫性质的邀请(例如约会或订婚聚会)。