中间有过改动的地方,想想还是留着之前的,记录一下这个思考、变化的过程

哇~终于到写博客的时候了

行为分析:分析什么(用户的行为)

前端是angular,借助其自带的指令收集用户行为;
后端借助ELK:存到elasticsearch中借助kibana强大的图形展示 以图表方式展示分析的结果;

思路:

1、分为二部分:整个系统为一个实体,具体网页 为一个实体,其实还有一个局部区域的实体,但是交接的时候三个对他们来说有点绕,那索性去一个好辣

2、进入时开始收集数据,离开区域时调用后端方法保存数据,单击时获取单击的相关信息;

如:进入系统界面时收集进入时数据、及浏览器的一些信息(给实体),离开或退出时(准备用组件销毁钩子)收集离开的相关信息;网页和区域同样的道理;

关于离开时调用后端,之前计划离开整个系统时一起提交到后端,但是跨指令我new实体,你知道,之前的数据就没有了,(⊙o⊙)…我可能陷里面了,目前没有找到更好的方法,欢迎大家多提宝贵意见;

前端:

大致思路如下

//定义一个指令:gatherComponentData;指令和组件是一样的道理
@Directive({
    selector: '[gatherComponentData]'
})

//考虑到后端地址可能会变,所以需要@input给指令传递后端的url以方便调用,这样改的话成本比较小
@Input('gatherComponentData') behaviorActionUrl: string;

这些三个指令是一样的,下面多少有一些差异,但是都差不多:

//界面加载完成之后 给webContent实体赋值,并存储一部分数据到localstoage中,系统界面销毁时存储到后端
    ngAfterViewInit() {

        let webContent = {
            webEnterTime: new Date(),browserLanguage: navigator.language || "en-*",browserColorDepth: screen.colorDepth.toString() + "像素/英寸",browserWidth: document.documentElement.scrollWidth.toString() || document.body.scrollWidth.toString() || screen.width.toString(),browserHeight: document.documentElement.scrollHeight.toString() || document.body.scrollHeight.toString() || screen.height.toString(),browserPlatform: navigator.platform || "",browserCookieEnabled: navigator.cookieEnabled.toString(),browserName: navigator.userAgent
        }

        this.localStorage.setobject('webContent',webContent);
    };

    /** * 监听 了点击事件,target是点击的目标:90%是监听对象 */
@HostListener('click',['$event.target'])
onMouseclick(btn: HTMLElement) {
        //模块名、网页名 根据id获取
        // iconfont icon-9 iconfont icon-jiantouxia iconfont icon-jiantouxia
     if (btn.id == "webPageId") {
            this.webPageContent.webpageName = btn.innerText.trim();//网页名
            localStorage.setItem("pageName",btn.innerText.trim());
        } else if (btn.id == "webModuleId") {
            this.webPageContent.webmoduleName = btn.innerText.trim();//网页所属模块名 需要添加
            localStorage.setItem("moduleName",btn.innerText.trim());
        } else {
            this.webPageContent.webmenuName = btn.innerHTML.trim();//网页一级菜单名
            localStorage.setItem("menuName",btn.innerText.trim());
        }
    };

根据上面的信息,我有一个判断,前端html界面上如果没有这个id的话是需要添加的,或者换成其他的如class也可以,目前是根据id存储到localstorage里面,方便网页指令、区域指令获取自己的网页名等

再展示一个离开事件吧:为了防止用户只是一划而过而收集到无效信息,对时间添加了一个判断:

/** * 鼠标离开 整个组件的 事件 */
@HostListener('mouseleave')
onMouseLeave() {
    if (this.childDiv) {
      //日期格式问题:后台实体内的日期是date类型,但是我如果转成字符串格式是正确的,在转成日期格式就又自动格式化了;停用
      // var startDate = new Date();
      // let startString = startDate.getFullYear() + '-' + (startDate.getMonth() + 1) + '-' + startDate.getDate() + ' ' + startDate.getHours() + ':' + startDate.getMinutes() + ':' + startDate.getSeconds();
      this.enterDivMsg.leaveTime = new Date();//离开组件的时间

      let staySecond: number = (this.enterDivMsg.leaveTime.getTime() - this.enterDivMsg.enterTime.getTime()) / 1000
      if (staySecond > 1) {//如果离开操作距离 进入操作 只隔 2s 则 此次离开事件不算
        this.enterDivMsg.webpageName = localStorage.getItem("pageName") || " ";//网页名
        this.enterDivMsg.webmoduleName = localStorage.getItem("moduleName") || " ";//网页所属模块名 需要添加
        //传给后端网页数据
        let body = JSON.stringify(this.enterDivMsg);
        this.enterDivMsg = new WebPageContent();
        let headers = new Headers({ 'Content-Type': 'application/json' });
        let options = new RequestOptions({ headers: headers });//,options
        let boolean: any = this.http.post(this.behaviorActionChildUrl,body)
          .map(res => <any>res.json());

      }
    }
  };

后端

后端用的是bboss连接的elasticsearch并存储:只是存储

bbossgroups是国内首款集AOP、MVC、持久化、JSP标签库、分布式RPC服务、分布式事件框架于一身的企业级JavaEE开发框架,在Apache License Version 2.0 许可协议下开源,后有介绍:

和solr差不多,es需要建立映射,映射很像给实体里的字段定义类型:基本类型

<property name="createuserActionIndice">
        <![CDATA[{
            "settings": {
                "number_of_shards": 6,"index.refresh_interval": "5s"
            },{
                "mappings": {
                    "webContentComm": {
                        "properties": {
                            "userId": {//字段
                                "type": "text"//类型
                            },"userName": {
                                "type": "text"
                            },"webName": {
                                "type": "text"
                            },"userIp": {
                                "type": "ip"
                            },"browserWidth": {
                                "type": "text"
                            },"browserHeight": {
                                "type": "text"
                            },"browserDomain": {
                                "type": "text"
                            },"browserName": {
                                "type": "text"
                            },"browserColorDepth": {
                                "type": "text"
                            },"broserLanguage": {
                                "type": "text"
                            },"broserPlatform": {
                                "type": "keyword"
                            },"broserCookieEnabled": {
                                "type": "text"
                            },"webEnterTime": {
                                "type": "date"
                            },"webLeaveTime": {
                                "type": "date"
                            },"webStayTime": {
                                "type": "long"
                            },"webPageActionEntityList": {
                                "type": "nested","properties": {
                                    "webmoduleName": {
                                        "type": "text"
                                    },"webpageName": {
                                        "type": "text"
                                    },"webpageUrl": {
                                        "type": "text"
                                    },"browserReferrer": {
                                        "type": "text"
                                    },"webpageId": {
                                        "type": "text"
                                    },"webpageEnterTime": {
                                        "type": "date"
                                    },"webpageLeaveTime": {
                                        "type": "date"
                                    },"webpageStayTime": {
                                        "type": "long"
                                    },"btnNameList": {
                                        "type": "object"
                                    },"selectOptionList": {
                                        "type": "object"
                                    },"searchContentList": {
                                        "type": "object"
                                    },"divActionEntityList": {
                                        "type": "nested","properties": {
                                            "userId": {
                                                "type": "text"
                                            },"enterTime": {
                                                "type": "date"
                                            },"leaveTime": {
                                                "type": "date"
                                            },"stayDivTime": {
                                                "type": "long"
                                            },"divTitle": {
                                                "type": "text"
                                            },"btnNameList": {
                                                "type": "object"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }]]>
    </property>

映射其实很简单吧只是有些长而已,需要说明的是:
1、object是一个对象,对应单个JSON对象
JSON文档本质上是层次化的:文档可能包含内部对象,而内部对象本身可能包含内部对象;不过其会扁平化处理:
拿官网来说:

//为my_index这个索引的_doc这个type下的1的文档创建映射
PUT my_index/_doc/1
{ 
  "region": "US","manager": { 
    "age":     30,"name": { 
      "first": "John","last":  "Smith"
    }
  }
}
被处理成:
{
  "region":             "US","manager.age":        30,"manager.name.first": "John",//**
  "manager.name.last":  "Smith"//**
}

2、nested:嵌套用于JSON对象的数组,这样就立体了,嵌套的
嵌套字段也可以被单独查询了

3、映射其实还有父子关系,类似于一对多的数据库表,这个挺好研究的但是机会要留给后面的学习者嘛,以后再说吧,这个父子关系更加适合我的这三个实体

代码

刚才说了三个实体是依次传到后台的,所以也需要合起来传到es中,如果不和那岂不是很散,将来我们的系统做大了,那岂不是很乱;

先定义带有嵌套的实体,用于合数据提交到es中:

//region 定义list用来承接实体并提交给es
    private List<DivActionEntity> enterDivMsgEntitieList = new ArrayList<>();//接受所有的div元素信息
    private List<WebPageActionEntity> webPageActionEntityList = new ArrayList<>();//接受所有的网页信息,多个用户
    private List<WebActionEntity> webActionEntityList = new ArrayList<>();//接受所有用户关于网站的信息
    private String userIp;
    //endregion

方法中有实体这个,这个实体中只有自己的东西,没有延伸没有扩展

@RequestMapping(value = "/gatherWebData",method = RequestMethod.POST)
@CrossOrigin
public void gatherWebData(@RequestBody OnlyWebModel webContent,@PathVariable String webName,HttpServletRequest request) throws IOException {

//region 如果ip为空 则获取ip 最后退出走这个方法,之前获取网页数据时应该是有ip了,为防万一重新获取
        if (userIp.isEmpty()) {
            userIp = this.getUserIp(request);
        }
        //endregion

        //region 填充webContent网站实体 可能有多个用户,需要 传值给 list
        WebActionEntity webContentComm = new WebActionEntity();
        webContentComm.setUserId(webContent.getUserId());
        ……
        webContentComm.setUserIp(this.userIp);

        webActionEntityList.add(webContentComm);
        //endregion

        joinEntity();//合并实体
        //submitData();//提交

    }

最后的合并,筛选出一个用户的信息则提交:

public void joinEntity() {

        webActionEntityList.forEach(webItem -> {
            //region接受一个用户关于网页的信息
            List<WebPageActionEntity> singleWebPageEntityList = new ArrayList<>();
            webPageActionEntityList.forEach(pageItem -> {
                if (webItem.getUserId() == pageItem.getUserId()) {
                    //region 接受一个用户的div信息
                    List<DivActionEntity> singleUserDivActionEntities = new ArrayList<>();
                    enterDivMsgEntitieList.forEach(divItem -> {
                        if (pageItem.getUserId() == divItem.getUserId()) {
                            //如果div的用户id和网页的用户id相同
                            singleUserDivActionEntities.add(divItem);
                        }
                    });
                    //将用户的div信息 以用户为单位 保存到 网页信息中
                    pageItem.setDivActionEntityList(singleUserDivActionEntities);
                    singleUserDivActionEntities.clear();
                    //endregion
                    singleWebPageEntityList.add(pageItem);
                }
            });
            webItem.setWebPageActionEntityList(singleWebPageEntityList);
            singleWebPageEntityList.clear();
            //endregion
            submitData(webItem);//提交
        });
    }

kibana目前真正研究中,进度有点慢的原因是借助x-pack给自己挖了不少的坑,现在关闭了x-pack的安全验证,这样是不太好、我是不想在这纠结了,好长时间,es也由原来的集群变成了现在的单机【说多了都是泪,以后不要一个人研究对象了,折寿】

修改:2018年5月1日14:43:24
上面的代码和前端的代码是的,new一下原来的东西就没有了,所以重构(xie)了一下,思路是将原来的索引一分为二,映射为:

#网站:
PUT /web-action
{
"mappings": {
                    "webContentComm": {
                        "properties": {
                            "userId": {
                                "type": "text"
                            },"webStayTime": {
                                "type": "long"
                            }
                        }
                    }
}
}
#网页:
PUT /webpage-action
{
"mappings": {

              "webPageActionEntityList": {
                                "properties": {
                                    "webmoduleName": {
                                        "type": "text"
                                    },"searchContentList": {
                                        "type": "object"
                                    }
                                }
                            }
}
}
#Div局部区域:这般不用,还是写上吧
PUT /webdiv-action
{
"mappings": { 
                 "divActionEntityList": {
                              "properties": {
                                   "userId": {
                                           "type": "text"
                                     },"enterTime": {
                                         "type": "date"
                                     },"leaveTime": {
                                         "type": "date"
                                     },"stayDivTime": {
                                        "type": "long"
                                    },"divTitle": {
                                          "type": "text"
                                    },"btnNameList": {
                                        "type": "object"
                                  }
                              }
                     }
}
}

后端存储也改成了分别存储:

package com.dmsdbj.bebavior.controller;
import ……
/** * 单个存储,映射已经建立, * 网页单独存储,通过userId 逻辑上建立联系,es中并没有真正建立联系 * Created by phoebeM on 2018/04/22. */
@Controller
@RequestMapping("/getSingelActionData")
public class userActionSingle {

    private String userIp;

    /** * 用户退出 时 获取网站信息 并提交到es中 * * @param webContent 只是包含整体信息,里面的子实体 是webContentComm * @return */
    @RequestMapping(value = "/postWebData",method = RequestMethod.POST)
    @CrossOrigin
    public void gatherWebData(@RequestBody OnlyWebModel webContent,HttpServletRequest request) throws IOException {

        List<OnlyWebModel> onlyWebModels = new ArrayList<>();

        //region 如果ip为空 则获取ip 最后退出走这个方法,之前获取网页数据时应该是有ip了,为防万一重新获取
        if (userIp.isEmpty()) {
            userIp = this.getUserIp(request);
        }
        //endregion

        //region 填充webContent网站实体 可能有多个用户,需要 传值给 list
        //用户id作为标识 回覆盖 ,所以标识=用户id+时间
        Date day = new Date();
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
        webContent.setUserId(webContent.getUserId() + df.format(day));
        //用户ip
        webContent.setUserIp(this.userIp);
        //停留时长 s为单位
        webContent.setWebStayTime((webContent.getWebEnterTime().getTime() -
                webContent.getWebLeaveTime().getTime()) / 1000);

        onlyWebModels.add(webContent);
        //endregion
        //调用方法,提交到es
        //创建创建/修改/获取文档的客户端对象,单实例多线程安全
        ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
        //添加或者修改文档,如果Id已经存在做修改操作,否则做添加文档操作,返回处理结果
        String responseString = clientUtil.addDocuments("web-action",//索引表
                "webContentComm",//索引类型
                onlyWebModels);
        onlyWebModels.clear();

    }

    /** * 用户离开网页时获取网页信息 * * @param * @return */
    @RequestMapping(value = "/postWebPageData",method = RequestMethod.POST)
    @CrossOrigin
    public void gatherWebPageData(@RequestBody OnlyWebPageModel onlyWebPageModel,HttpServletRequest request,HttpServletResponse response) throws IOException {

        List<OnlyWebPageModel> onlyWebPageModels = new ArrayList<>();
        //获取用户ip
        userIp = this.getUserIp(request);

        //region 将参数中只包含网页级别信息的数据 需要传给 包含区域实体的网页实体
        Date day = new Date();
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
        onlyWebPageModel.setUserId(onlyWebPageModel.getUserId() + df.format(day));//加上日期,方便查询
        onlyWebPageModel.setUserIp(userIp);
        Long stayTime = (onlyWebPageModel.getWebpageEnterTime().getTime() - onlyWebPageModel.getWebpageLeaveTime().getTime()) / 1000;
        onlyWebPageModel.setWebpageStayTime(stayTime);
        onlyWebPageModels.add(onlyWebPageModel);
        //endregion

        //调用方法,提交到es
        //创建创建/修改/获取/删除文档的客户端对象,单实例多线程安全
        ClientInterface clientUtil = ElasticSearchHelper.getRestClientUtil();
        //添加或者修改文档,如果Id已经存在做修改操作,否则做添加文档操作,返回处理结果
        String responseString = clientUtil.addDocuments("webpage-action",//索引表
                "webPageActionEntityList",//索引类型
                onlyWebPageModels);
        onlyWebPageModels.clear();
    }


    /** * 获取用户ip * * @param request * @return */
    public String getUserIp(HttpServletRequest request) {
        userIp = request.getHeader("x-forwarded-for");
        if (userIp == null || userIp.length() == 0 || "unkNown".equalsIgnoreCase(userIp)) {
            userIp = request.getHeader("Proxy-Client-IP");
        }
        if (userIp == null || userIp.length() == 0 || "unkNown".equalsIgnoreCase(userIp)) {
            userIp = request.getHeader("WL-Proxy-Client-IP");
        }

        if (userIp == null || userIp.length() == 0 || "unkNown".equalsIgnoreCase(userIp)) {
            userIp = request.getRemoteAddr();
        }

        return userIp;
    }


}

关于bboss

官网地址:http://www.bbossgroups.com/
官方博客:http://yin-bp.iteye.com/

作者:尹标平,2001年大学毕业,一直从事JavaEE企业应用开发和架构设计工作,做过开发员、架构师、项目经理之类的,喜欢搞点开源方面的东东,比较拿得出手的开源项目只有bbossgroups。 (#^.^#)这个自我介绍有点谦虚了,大佬也是一位平易近人、乐于助人的好大佬

伊标平大佬主页:https://my.oschina.net/bboss

更多信息请访问文档:

高性能elasticsearch ORM开发库使用介绍-http://www.jb51.cc/article/p-xpuveyrf-bqr.html 里面的视频有增删改查的例子

bboss elasticsearch技术交流群:166471282,大佬挺忙的,大家先百度Google
bboss elasticsearch微信公众号:bbossgroups

基础映射的网址:

https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html

elk文档入口https://www.elastic.co/guide/index.html

中文权威指南:
https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html

【ELK+angular行为分析】的更多相关文章

  1. localStorage的过期时间设置的方法详解

    这篇文章主要介绍了localStorage的过期时间设置的方法详解的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  2. HTML5 Web缓存和运用程序缓存(cookie,session)

    这篇文章主要介绍了HTML5 Web缓存和运用程序缓存(cookie,session),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  3. html5超简单的localStorage实现记住密码的功能实现

    这篇文章主要介绍了html5超简单的localStorage实现记住密码的功能实现,非常具有实用价值,需要的朋友可以参考下

  4. 详解前端HTML5几种存储方式的总结

    本篇文章主要介绍了前端HTML5几种存储方式的总结 ,主要包括本地存储localstorage,本地存储sessionstorage,离线缓存(application cache),Web SQL,IndexedDB。有兴趣的可以了解一下。

  5. localstorage和sessionstorage使用记录(推荐)

    通过阅读各路大神对web存储locastorage和sessionstorage的用法解析,深有感触,下面小编把localstorage和sessionstorage使用记录分享到脚本之家平台,供大家参考

  6. ios – 如何在运行calabash测试时模拟后端交互

    解决方法由于直到今天我才收到其他反馈,我将回答我自己的问题.我们选择了为后端创建一个非常简单的模拟的方法.我们使用了Sinatra,但node.js或类似技术会产生相同的结果.可以通过简单的RESTAPI控制模拟.在步骤定义中,我们为运行场景适当地配置了模拟后端.这有点开销,因为模拟必须与真正的后端一起发展,但直到今天它仍然像一个强大的解决方案.

  7. ios – 在iPad Safari上的localStorage是否保持持续?

    我已经看到网络上的意见分歧.据说,从iOS5.1开始,iPad上的HTML5内容的本地数据存储不再保证是持久的,但是在苹果当前的Safari开发者页面(https://developer.apple.com/technologies/safari/html5.html)上,建议离线持久性被保证.有没有人有任何最近的经验与这个问题,并能评论如何可靠的数据库HTML5的功能与Safari在iPad上?

  8. ios8 – 如何追踪扩展中的事件?

    大多数iOS分析工具使用标准的uiapplication委托方法批量,每分钟或应用程序终止时上传事件.我没有看到任何专门的解决方案(今天,照片…)从这些家伙.在ios8中扩展分析方面你做了什么?

  9. iOS:Google Analytics(分析)用户计时报告未在我的Google Analytics(分析)帐户中更新

    我正在尝试使用GoogleAnalytics(分析)跟踪我的应用速度,但是在GoogleAnalytics(分析)帐户中我看不到任何应用程序的速度.我已经跟踪了其他参数,如事件,崩溃和异常.对于这些参数,我可以看到我的Google分析帐户中生成的报告.以下是我用来发送事件时间的代码.以下是控制台中打印的消息.GoogleAnalytics2.0b4-[GAIdispatcherdispatchCo

  10. 基于Swift语言开发微信、QQ和微博的SSO授权登录代码分析

    一,总体架构1,引入第三方库除了必须引入对应的登录SDK外,额外引入了SDWebImage,SVProgressHUD,看名字大家都明白吧,引入登录SDK请各自看官方的开发文档,需要加入什么系统库文件,需要配置OtherLinkerFlags等,请参考各自官方文档即可;2,配置连接桥文件因为创建的工程是基于Swift语言,目前官方SDK和其它三方库都是用OC写的,所以为了在swift中调用oc代码

随机推荐

  1. Angular2 innerHtml删除样式

    我正在使用innerHtml并在我的cms中设置html,响应似乎没问题,如果我这样打印:{{poi.content}}它给了我正确的内容:``但是当我使用[innerHtml]=“poi.content”时,它会给我这个html:当我使用[innerHtml]时,有谁知道为什么它会剥离我的样式Angular2清理动态添加的HTML,样式,……

  2. 为Angular根组件/模块指定@Input()参数

    我有3个根组件,由根AppModule引导.你如何为其中一个组件指定@input()参数?也不由AppModalComponent获取:它是未定义的.据我所知,你不能将@input()传递给bootstraped组件.但您可以使用其他方法来做到这一点–将值作为属性传递.index.html:app.component.ts:

  3. angular-ui-bootstrap – 如何为angular ui-bootstrap tabs指令指定href参数

    我正在使用角度ui-bootstrap库,但我不知道如何为每个选项卡指定自定义href.在角度ui-bootstrap文档中,指定了一个可选参数select(),但我不知道如何使用它来自定义每个选项卡的链接另一种重新定义问题的方法是如何使用带有角度ui-bootstrap选项卡的路由我希望现在还不算太晚,但我今天遇到了同样的问题.你可以通过以下方式实现:1)在控制器中定义选项卡href:2)声明一个函数来改变控制器中的散列:3)使用以下标记:我不确定这是否是最好的方法,我很乐意听取别人的意见.

  4. 离子框架 – 标签内部的ng-click不起作用

    >为什么标签标签内的按钮不起作用?>但是标签外的按钮(登陆)工作正常,为什么?>请帮我解决这个问题.我需要在点击时做出回复按钮workingdemo解决方案就是不要为物品使用标签.而只是使用divHTML

  5. Angular 2:将值传递给路由数据解析

    我正在尝试编写一个DataResolver服务,允许Angular2路由器在初始化组件之前预加载数据.解析器需要调用不同的API端点来获取适合于正在加载的路由的数据.我正在构建一个通用解析器,而不是为我的许多组件中的每个组件设置一个解析器.因此,我想在路由定义中传递指向正确端点的自定义输入.例如,考虑以下路线:app.routes.ts在第一个实例中,解析器需要调用/path/to/resourc

  6. angularjs – 解释ngModel管道,解析器,格式化程序,viewChangeListeners和$watchers的顺序

    换句话说:如果在模型更新之前触发了“ng-change”,我可以理解,但是我很难理解在更新模型之后以及在完成填充更改之前触发函数绑定属性.如果您读到这里:祝贺并感谢您的耐心等待!

  7. 角度5模板形式检测形式有效性状态的变化

    为了拥有一个可以监听其包含的表单的有效性状态的变化的组件并执行某些组件的方法,是reactiveforms的方法吗?

  8. Angular 2 CSV文件下载

    我在springboot应用程序中有我的后端,从那里我返回一个.csv文件WheniamhittingtheURLinbrowsercsvfileisgettingdownloaded.现在我试图从我的角度2应用程序中点击此URL,代码是这样的:零件:服务:我正在下载文件,但它像ActuallyitshouldbeBook.csv请指导我缺少的东西.有一种解决方法,但您需要创建一个页面上的元

  9. angularjs – Angular UI-Grid:过滤后如何获取总项数

    提前致谢:)你应该避免使用jQuery并与API进行交互.首先需要在网格创建事件中保存对API的引用.您应该已经知道总行数.您可以使用以下命令获取可见/已过滤行数:要么您可以使用以下命令获取所选行的数量:

  10. angularjs – 迁移gulp进程以包含typescript

    或者我应该使用tsc作为我的主要构建工具,让它解决依赖关系,创建映射文件并制作捆绑包?

返回
顶部