事有一个需求是,先读取网页中的数字,然后进行计算后再写到网页上。解决的方法是,得先用正确的id来标识这两个数字的元素。比如:<span id="sales">500</span> ,就是把500进行标识起来了。
然后把数字读出来。在JavaScript中,`document.getElementById`返回的是一个元素对象,而不是一个数字。这里需要获取元素的值才能进行计算。
计算后,结果会回写在 <span id="result"></span> 标识内。
如果计算回写之后,没有数字,请检查下js代码块是否放在最后。
<!DOCTYPE html>
<html>
<head>
<title>计算结果</title>
</head>
<body>
<span id="inventory">100</span> <!-- 今年社会日均库存 -->
<span id="sales">500</span> <!-- 今年以来社会销量 -->
<span id="result"></span>
<script>
// 获取页面上的元素
var inventoryElement = document.getElementById("inventory");
var salesElement = document.getElementById("sales");
var resultElement = document.getElementById("result");
// 获取元素的值
var inventory = parseInt(inventoryElement.textContent);
var sales = parseInt(salesElement.textContent);
// 计算结果
var result = inventory / sales;
// 在页面上显示计算结果
resultElement.textContent = result.toFixed(2); // 保留两位小数
</script>
</body>
</html>
关注我,我们将一起探讨如何将技术与业务有机结合。不必成为专家,只要具备基本的印象和理解,便能把握业务与技术的融合,实现二者的平衡发展。在这个过程中,我们将共同寻找如何在促进二者融合发挥最大价值的同时,确保技术与业务的协调一致。
页面停留时间(Time on Page)简称 Tp,是网站分析中很常见的一个指标,用于反映用户在某些页面上停留时间的长短,传统的Tp统计方法会存在一定的统计盲区,比如无法监控单页应用,没有考虑用户切换Tab、最小化窗口等操作场景。 基于上述背景,重新调研和实现了精确统计页面停留时长的方案,需要 兼容单页应用和多页应用,并且不耦合或入侵业务代码。
我们可以把一个页面生命周期抽象为三个动作: 「进入」、「活跃状态切换」、「离开」
如下图,计算页面停留时长既如何监控这三个动作,然后在对应触发的事件中记录时间戳,比如要统计活跃停留时长就把 active 区间相加即可,要统计总时长既 tn -t0 。
2.1 如何监听页面的进入和离开?
对于常规页面的 首次加载、页面关闭、刷新 等操作都可以通过 window.onload 和 window.onbeforeunload 事件来监听页面进入和离开,浏览器前进后退可以通过 pageshow 和 pagehide 处理。
对于单页应用内部的跳转可以转化为两个问题:1.监听路由变化,2.判断变化的URL是否为不同页面 。
2.1.1 监听路由变化
目前主流的单页应用大部分都是基于 browserHistory (history api) 或者 hashHistory 来做路由处理,我们可以通过监听路由变化来判断页面是否有可能切换。注意是有可能切换,因为URL发生变化不代表页面一定切换,具体的路由配置是由业务决定的(既URL和页面的匹配规则)。
browserHistory
路由的变化本质都会调用 History.pushState() 或 History.replaceState() ,能监听到这两个事件就能知道。通过 popstate 事件能解决一半问题,因为 popstate 只会在浏览器前进后退的时候触发,当调用 history.pushState() or history.replaceState() 的时候并不会触发。
The popstate event is fired when the active history entry changes. If the history entry being activated was created by a call to history.pushState() or was affected by a call to history.replaceState(), the popstate event's state property contains a copy of the history entry's state object.
Note that just calling history.pushState() or history.replaceState() won't trigger a popstate event. The popstate event will be triggered by doing a browser action such as a click on the back or forward button (or calling。history.back() or history.forward() in JavaScript).
这里需要通过猴子补丁(Monkeypatch)解决,运行时重写 history.pushState 和 history.replaceState 方法:
let _wr = function (type) { let orig = window.history[type] return function () { let rv = orig.apply(this, arguments) let e = new Event(type.toLowerCase()) e.arguments = arguments window.dispatchEvent(e) return rv } } window.history.pushState = _wr('pushState') window.history.replaceState = _wr('replaceState') window.addEventListener('pushstate', function (event) {}) window.addEventListener('replacestate', function (event) {})
hashHistory
hashHistory 的实现是基于 hash 的变化,hash 的变化可以通过 hashchange 来监听
2.1.2 判断URL是否为不同页面
问题本质是怎么定义一个页面,这里我们无法自动获取,因为不同业务场景定义不同,需要业务方在初始化的时候配置 rules 参数,默认不传入 rules 的情况取 location.pathname 为 key,key 不相同则判断为不同的页面,配置的语法:
new Tracer({ rules: [ { path: '/index' }, { path: '/detail/:id' }, { path: '/user', query: {tab: 'profile'} } ] )
对于页面进入和离开相关事件整理
2.2 如何监听页面活跃状态切换?
可以通过 Page Visibility API 以及在 window 上声明 onblur/onfocus 事件来处理。
2.2.1 Page Visibility API
一个网页的可见状态可以通过 Page Visibility API 获取,比如当用户 切换浏览器Tab、最小化窗口、电脑睡眠 的时候,系统API会派发一个当前页面可见状态变化的 visibilitychange 事件,然后在事件绑定函数中通过 document.hidden 或者 document.visibilityState 读取当前状态。
document.addEventListener('visibilitychange', function (event) { console.log(document.hidden, document.visibilityState) })
2.2.2 onblur/onfocus
2.3 什么时机上报数据?
2.3.1 页面离开时上报
对于页面刷新或者关闭窗口触发的操作可能会造成数据丢失
2.3.2 下次打开页面时上报
会丢失历史访问记录中的最后一个页面数据
目前采用的方案2,对于单页内部跳转是即时上报,对于单页/多页应用触发 window.onbeforeunload 事件的时候会把当前页面数据暂存在 localStorage 中,当用户下次进入页面的时候会把暂存数据上报。有个细节问题,如果用户下次打开页面是在第二天,对于统计当天的活跃时长会有一定的误差,所以在数据上报的同时会把该条数据的页面进入时间/离开时间带上。
3.1 UML类关系图
Tracer
核心类,用来实例化一个监控,对原生事件和自定义事件的封装,监听 enter activechange exit 事件来操作当前 Page 实例。
P.S. 取名来自暴雪旗下游戏守望先锋英雄猎空(Tracer),直译为:追踪者。
Page
页面的抽象类,用来实例化一个页面,封装了 enter exit active inactive 等操作,内部通过 state 属性来维护当前页面状态。
3.2 事件派发关系图
Desktop
Mobile
对于页面停留时长的定义可能在不同场景会有差异,比如内部业务系统或者OA系统,产品可能更关心用户在页面的活跃时长;而对于资讯类型的产品,页面可见时长会更有价值。单一的数据对业务分析是有限的,所以在具体的代码实过程中我们会把停留时长分三个指标,这样能更好的帮助产品/运营分析。
active 页面活跃时长
visible 页面可见时长 //仅支持Desktop
duration 页面总停留时长
移动端的兼容性目前还没完全覆盖;
对于页面的配置目前还不够灵活,考虑支持 react-router / vue-router 的配置;
byted-cg-tracer 待封装;开发中
https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onhashchange
https://developer.mozilla.org/en-US/docs/Web/Events/popstate
https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
https://stackoverflow.com/questions/4570093/how-to-get-notified-about-changes-of-the-history-via-history-pushstate
在统计用户在人数的时候,我们用到了监听器,监听器大致分为以下三种:
这种统计办法是错误的认为每次刷新页面后进行进行一次的count++运算
import javax.servlet.*;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import javax.servlet.http.HttpSessionBindingEvent;
@WebListener()
public class MyRequestListener implements ServletRequestListener{
private ServletContext sc;
private Integer count;
@Override
//请求被初始化 Request
public void requestInitialized(ServletRequestEvent sre) {
//获取全局域
sc = sre.getServletContext();
//将count从全局域中获取出来
count = (Integer) sc.getAttribute("count");
System.out.println(count);
count++;
System.out.println(count);
sc.setAttribute("count",count);
}
}
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import javax.servlet.http.HttpSessionBindingEvent;
@WebListener()
public class MyServletContextListener implements ServletContextListener{
private ServletContext sc;
@Override
//Application被初始化的时候创建
public void contextInitialized(ServletContextEvent sce) {
Integer count = 0;
//获取全局域
sc = sce.getServletContext();
//将count放入到全局域中
sc.setAttribute("count",count);
}
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<center><h1>You are the ${applicationScope.count} customer to visit. </h1></center>
</body>
</html>
这种错误地做法导致的是每刷新一次页面 就会导致count进行累加操作,最终产生错误的在线人数,所以此时想到不应该监听Request域,而应该监听Session域。
在第二次监听Session域之后,发现每次刷新页面后不改变count但是在启动不同的浏览器后count++会实现,但是,这样做并不是我们要统计的在线人数,所以此种做法错误。由于代码只是将原来写在Request监听器中的代码转移到Session监听器中,所以其他没变的代码将不重复。
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import javax.servlet.http.HttpSessionBindingEvent;
@WebListener()
public class MySessionListener implements HttpSessionListener{
private ServletContext sc;
private Integer count;
@Override
//当对话产生时激活此方法
public void sessionCreated(HttpSessionEvent se) {
sc = se.getSession().getServletContext();
count = (Integer) sc.getAttribute("count");
count++;
sc.setAttribute("count",count);
}
}
这时我们发现对于在线人数的统计,不是网页访问的次数,也不是浏览器打开的个数,对需求的理解的错误理解。所以正确的做法是统计其IP的数量,这样的话,不管你在一台电脑上开启多少客户端,都会只有一个。
统计其IP的数量,将IP的数量作为当前的在线人数,那么如何统计IP的数量呢?这样将会导出以下问题:
现在来解决这些问题:
到了这里又冒出来一个问题集合(List)放到哪个域里呢?
ServletContext域
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import javax.servlet.http.HttpSessionBindingEvent;
import java.util.ArrayList;
import java.util.List;
@WebListener()
public class MyServletContextListener implements ServletContextListener{
private ServletContext sc;
@Override
//Application被初始化的时候创建
public void contextInitialized(ServletContextEvent sce) {
//创建一个链表来存储IP
List<String> ips = new ArrayList<>();
sc = sce.getServletContext();
//将创建好的链表对象,放到Application域中
sc.setAttribute("ips",ips);
}
}
由于IP只能在Request域中获取,所以遍历判断在Request域中进行。
import javax.servlet.*;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.*;
import java.util.List;
@WebListener()
public class MyRequestListener implements ServletRequestListener{
private HttpServletRequest sr;
private String clientIp;
private ServletContext sc;
private List<String> ips;
private HttpSession session;
@Override
//请求被初始化 Request
public void requestInitialized(ServletRequestEvent sre) {
//从请求域中获取IP
sr = (HttpServletRequest) sre.getServletRequest();
clientIp = sr.getRemoteAddr();
session = sr.getSession();
session.setAttribute("clientIp",clientIp);
//测试
// System.out.println("clientIp = "+ clientIp);
//获取Application域中的List
sc = sre.getServletContext();
ips = (List<String>) sc.getAttribute("ips");
//遍历ips
for (String ip :
ips) {
if (clientIp.equals(ip))
return;
}
ips.add(clientIp);
sc.setAttribute("ips",ips);
}
}
因为要统计在线人数,所以要设置退出按钮,点击退出按钮之后,因为要从List域中移除,所以使用Session域监听器来判断session回话的关闭
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.*;
import java.util.List;
@WebListener()
public class MySessionListener implements HttpSessionListener{
private ServletContext sc;
private List<String> ips;
private HttpSession session;
private Object clientIp;
@Override
public void sessionDestroyed(HttpSessionEvent se) {
sc = se.getSession().getServletContext();
ips = (List<String>) sc.getAttribute("ips");
session = se.getSession();
clientIp = session.getAttribute("clientIp");
//删除ip,如何获取IP,但是不可以从session获取到IP
//因为Session获取不到Request
//一个Session包含多个Request
//一个Request只对应一个Session 所以获取不到,这时只能先从Request域中获取到的ips,放置到Session域
//然后从Session 域中读取
ips.remove(clientIp);
// session一失效就马上将此IP从链表中移除是错误的
//应该看此IP是否有另外的回话存在,如果有的话不能删除
}
}
此处代码是页面点击关闭后,激活的退出方法
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet(name = "LogoutServlet",urlPatterns = "/logoutServlet")
public class LogoutServlet extends HttpServlet {
private HttpSession session;
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//从域中获取一个session,设置为false 如果域中存在一个session,则直接获取,如果不存在,则返回一个空的session
session = request.getSession(false);
if (session != null){
//使session失效
session.invalidate();
//失效后,需要进行的操作,List链表中需要减去,用到了Session域监听器
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request,response);
}
}
在jsp页面进行读取的时候,因为ips是以List链表的形式存在的,所以要想判断当前在线人数,所以必须要判断链表的长度,所以是applicationScope.ips.size()
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<center><h1>You are the ${applicationScope.ips.size()} customer to visit. </h1><br>
<h3><a href="${pageContext.request.contextPath}/logoutServlet">
安全退出
</a></h3>
</center>
</body>
</html>
好了?,这时候,程序写完了,如何判断呢?
此时,我们的程序是部署在本地的Tomcat上的,对于本台电脑,只有一个IP,如何实现多个IP呢?其实啊我们的电脑可以有三个IP,在访问服务器的时候,服务器的IP多写几个,相当于本机的IP多出来几个。是哪三个IP呢?
1、默认clientIp : 0:0:0:0:0:0:0:1
2、127.0.0.1
这时大家可能会问127.0.0.1和localhost有什么区别呢,其实在这里要区分三个概念:
localhost、127.0.0.1 和 本机IP之间的区别:
3、IPv4地址:192.168.1.110
这样就很完美的实现了本地三个IP的测试。
写到这里,似乎已经可以简单的测试当前在线人数,也许仔细的人会发现在Session域被销毁的方法中的注释中发现一些猫腻。大家可以仔细想想,如果客户端用不同的浏览器,相同的IP去访问呢?点击退出后,会不会出现错误情况呢?答案是会的。演示结果如下图
所以在点击退出登录的按钮之后,不可以直接将IP移除,要判断有没有另外的回话存在,如果有另外的回话存在,此IP是不可以删掉的,问题由此变的复杂了,因为还要统计此IP所发出的会话有多少。
整体思路:
在全局域中,将不是直接将iP存放在List的链表中,而是以一个Map的形式存在,Map的键为String类型,Key为List类型,List中存放的是当前IP所激发的会话对象,这样就可以统计,一个IP触发的sessions有多少个。
通过调用Map的get方法,将当前IP最为参数,将可以获取到他所激发的会话集合。但是,此集合可能为空,因为有可能当前IP一次也没有访问此页面,所以在List为空的时候好要创建一个ArrayList来存放sessions,然后将变化后的List重新写回到Map,再将变化后的Map写回到全局域中 。这样创建过程基本完成。
然后考虑销毁过程,IP还需方法放到Session域中,当session被销毁的时候,应该把当前Session从List 中删除,但是Map中此sessions对应的IP可是不能直接删,要判断List中的sessions的个数(Entry对象),个数为1的时候才可以删除,不然就不可以删除。
所以,要将当前IP通过Request域存放到当前Session域中,
然后,要考虑的问题是,每次刷新页面后sessions的个数会增加,这是错误的,原因是什么?
答案是,因为在存放sessions的时候,创建数组直接进行的添加,这样的话,每次一刷新页面,就会导致sessions的添加,所以在此之前应该判断,sessions中是否有此session,有的话直接跳出。
这样添加就没问题了
在Map中,需要使用键值对的方式,Key为IP,Value为List,那么List中存放什么呢?存放的是此IP发出的所有回话的HttpSession的对象,所以List的泛型是HttpSession。
请求,在请求中,因为将当前Session 对象存放到List中, List在Map中,Map在全局域中,所以首先得从全局域获取到Map,然后,从Map中获取由当前IP所发出的所有Session的组成的List,判断当前的List是否为NULL,若为NULL,则创建List,否则,将当前SessioncurrentSession放入List中。
import javax.servlet.*;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@WebListener()
public class MyRequestListener implements ServletRequestListener{
private HttpServletRequest sr;
private String clientIp;
private ServletContext sc;
private List<String> ips;
private HttpSession currentSession;
private Map<String,List<HttpSession>> map;
private List<HttpSession> sessions;
@Override
//请求被初始化 Request
public void requestInitialized(ServletRequestEvent sre) {
//从请求域中获取IP
sr = (HttpServletRequest) sre.getServletRequest();
clientIp = sr.getRemoteAddr();
currentSession = sr.getSession();
//将当前Session 对象存放到List中, List在Map中,Map在全局域中,
sc = sre.getServletContext();
map = (Map<String, List<HttpSession>>) sc.getAttribute("map");
//从Map中获取由当前IP所发出的所有Session的组成的List
sessions = map.get(clientIp);
//判断当前的List是否为NULL,若为NULL,则创建List,否则,将当前Session放入List
if (sessions == null){
sessions = new ArrayList<>();
}
// 遍历List的session 对象,若有则不添加,若没有则添加
for (HttpSession session :
sessions) {
if (session == currentSession)
return;
}
sessions.add(currentSession);
//将变化过的List重新写回到Map
map.put(clientIp,sessions);
//再将变化的Map写回到全局域中
sc.setAttribute("map",map);
//将当前IP放入到当前Session
currentSession.setAttribute("clientIp",clientIp);
}
}
这里将不使用ips了,所以将其删除
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@WebListener()
public class MyServletContextListener implements ServletContextListener{
private ServletContext sc;
@Override
//Application被初始化的时候创建
public void contextInitialized(ServletContextEvent sce) {
//创建一个Map,key为IP,value为该IP上所发出的会话的对象
Map<String,List<HttpSession>> map = new HashMap<>();
sc = sce.getServletContext();
//将map放到全局域中
sc.setAttribute("map",map);
}
}
接下来剖析Session的删除工作,获取当前Session对象,这里有之前传递过来的IP,在进行删除操作的时候,要注意此处,删除的是List中的sessions,删除之后,还要判断其IP的是否要删除,如果List中没有该元素,则说明当前IP所发出的会话全部关闭,就可以从map中将当前IP对应的Entry对象删除,否则,当前IP所发出的会话任存在,那么使用put方法将变化过的List写回到map。
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.*;
import java.util.List;
import java.util.Map;
@WebListener()
public class MySessionListener implements HttpSessionListener{
private ServletContext sc;
private List<String> ips;
private HttpSession currentSession;
private String clientIp;
private Map<String,List<HttpSession>> map;
private List<HttpSession> sessions;
@Override
public void sessionDestroyed(HttpSessionEvent se) {
sc = se.getSession().getServletContext();
currentSession = se.getSession();
clientIp = (String) currentSession.getAttribute("clientIp");
map = (Map<String, List<HttpSession>>) sc.getAttribute("map");
//从Map中获取List
sessions = map.get(clientIp);
//从List中删除当前Session对象
sessions.remove(currentSession);
//如果List中没有该元素,则说明当前IP所发出的会话全部关闭,就可以从map中
//将当前IP对应的Entry对象删除
//若List中仍有元素,当前IP所发出的会话任存在,那么将变化过的List写回到map
if (sessions.size() == 0){
map.remove(clientIp);
}else {
map.put(clientIp,sessions);
}
sc.setAttribute("map",map);
}
}
因为处理的退出的页面/logoutServlet不需要做任何不同的处理,所以这里将不再重复。
因为在jsp用到了JSP标准库,所以到导两个包。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<center><h1>You are the ${applicationScope.map.size()} customer to visit. </h1><br>
<h3><a href="${pageContext.request.contextPath}/logoutServlet">
安全退出
</a><br></h3>
<h2>
<c:forEach items="${map}" var="entry">
${entry.key }=${entry.value.size()}<br>
</c:forEach>
</h2>
</center>
</body>
</html>
最后 测试成功,这就是一个完美的统计当前用户的在线人数。
来源:https://mp.weixin.qq.com/s/gM-lyh_9FeFKRRAJlPY83g
*请认真填写需求信息,我们会在24小时内与您取得联系。