考试JS倒计时客户端和服务器时间同步问题

考试JS倒计时客户端和服务器时间同步问题

需求实现考试时间页面倒计时。

这个需求以前在刀具大赛的时候也遇到过,当时是使用前端每秒定时请求后台返回倒计时时间。这样的缺点就是当用户量大的时候,会有的大量的请求造成性能下降(其实用户少或者使用场景少的时候也没啥事),优点就是时间比较准确,没有浏览器的兼容问题。

还有一种解决方案就是第一次请求的时候返回时间,然后就在客户端倒计时就好(当然为了防止客户端改时间作弊,提交请求的时间要在服务器端检查)。这种做的优点服务端没有请求的压力,实现起来也比较简单。

一、存在问题的实现方式:

复制粘贴拿起键盘,啪啪啪 倒计时代码就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var time = 60;//服务端返回的剩余时间

var set = setInterval(function() {
time--;
console.log(time)
if(time === 0) {
clearInterval(set);
}
}, 1000);

执行结果
59
58
57
省略其他。。。

存在的问题:你这东西不准啊,我看着几分钟,有好几秒的延迟

其实是setTimeout/setInterval误差的问题,我们可通过减少误差,通过对下一次任务的调用时间进行修正。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
let count = 0;
let countdown = 5000; //服务器返回的倒计时时间
let interval = 1000;
let startTime = new Date().getTime();
let timer = setTimeout(countDownStart, interval); //首次执行
//定时器测试
function countDownStart() {
count++;
const offset = new Date().getTime() - (startTime + count * 1000);
const nextInterval = interval - offset; //修正后的延时时间
if (nextInterval < 0) {
nextInterval = 0;
}
countdown -= interval;
console.log("误差:" + offset + "ms,下一次执行:" + nextInterval + "ms后,离活动开始还有:" + countdown + "ms");
if (countdown <= 0) {
clearTimeout(timer);
} else {
timer = setTimeout(countDownStart, nextInterval);
}
}


执行结果
误差:11ms,下一次执行:989ms后,离活动开始还有:4000ms
误差:4ms,下一次执行:996ms后,离活动开始还有:3000ms
误差:2ms,下一次执行:998ms后,离活动开始还有:2000ms
误差:4ms,下一次执行:996ms后,离活动开始还有:1000ms
误差:9ms,下一次执行:991ms后,离活动开始还有:0ms
省略其他。。。

存在的问题:你这东西有问题啊,浏览器切换网页后,在回来看页面,这段过程是暂停的,延迟了几分钟 没考虑浏览器的”休眠”,浏览器切换回来,倒计时是暂停的

综上所述:

浏览器中的定时器任务是有误差的,也就是我们常说的 setTimeout 为什么不准的问题,这里涉及到 js 单线程以及运行机制,具体运行原理可参考 2019-11-04-JS倒计时setTimeout为什么会出现误差

二、优化后的实现方式:

即使利用setTimeout()模拟setInterval(),还是会因为其余脚本的执行,造成误差。所以,我认为JS定时函数setInterval、setTimeout的弊端无法避免,只能通过多次与服务器沟通,来矫正时间。

封装后的countDown.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
(function () {
function timer(delay) {
console.log('timer' + delay);
var self = this;
this._queue = [];
setInterval(function () {
for (var i = 0; i < self._queue.length; i++) {
self._queue[i]();
}
},
delay);
}

timer.prototype = {
constructor: timer,
add: function (cb) {
this._queue.push(cb);
return this._queue.length - 1;
},
remove: function (index) {
this._queue.splice(index, 1);
}
};

var delayTime = 1000;

var msInterval = new timer(delayTime);

function countDown(config) {
//默认配置
var defaultOptions = {
fixNow: 3 * 1000,
fixNowDate: true,
now: new Date().valueOf(),
template: '{d}:{h}:{m}:{s}',
render: function (outstring) {
console.log(outstring);
},
end: function () {
console.log('the end!');
},
endTime: new Date().valueOf() + 5 * 1000 * 60
};
for (var i in defaultOptions) {
this[i] = config[i] || defaultOptions[i];
}
this.init();
}

countDown.prototype = {
constructor: countDown,
init: function () {
console.log('countDown init');
var self = this;
//是否开启服务器时间校验
if (this.fixNowDate) {
var fix = new timer(this.fixNow);
fix.add(function () {
self.getNowTime(function (now) {
console.log('服务器时间校准,' + self.now + '----------' + now);
self.now = now;
});
});
}
//倒计时
var index = msInterval.add(function () {
self.now += delayTime;
if (self.now >= self.endTime) {
msInterval.remove(index);
self.end();
} else {
self.render(self.getOutString());
}
});
},
getBetween: function () {
return _formatTime(this.endTime - this.now);
},
getOutString: function () {
var between = this.getBetween();
return this.template.replace(/{(\w*)}/g, function (m, key) {
return between.hasOwnProperty(key) ? between[key] : "";
});
},
getNowTime: function (cb) {
var xhr = new XMLHttpRequest();
xhr.open('get', '/', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 3) {
var now = xhr.getResponseHeader('Date');
cb(new Date(now).valueOf());
}
};
xhr.send(null);
}
};

function _cover(num) {
var n = parseInt(num, 10);
return n < 10 ? '0' + n : n;
}

function _formatTime(ms) {
var s = ms / 1000,
m = s / 60;
return {
d: _cover(m / 60 / 24),
h: _cover(m / 60 % 24),
m: _cover(m % 60),
s: _cover(s % 60)
};
}

var now = Date.now();

//new countDown({});

window.$countDown = countDown;

})();

使用方法

首先要引入countDown.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 //倒计时10秒
new window.$countDown({
fixNow: 3 * 1000, //3秒一次服务器时间校准
template: '{d}天{h}:{m}:{s}',
render: function (outstring) {
console.log(outstring);
if (outstring.indexOf('00天') > -1) {
outstring = outstring.substring(3);
}
$("#timebox").text(outstring);
},
end: function () {
console.log('the end!');
},
endTime: new Date().valueOf() + 10 * 1000 * 60 //时间戳
});

经测试通过服务器时间校准,可以避免时间不准的问题而且还大大减轻了服务器端的压力。即使浏览器切到后台运行,倒计时停止也没有关系。

参考文档:

https://segmentfault.com/q/1010000000698541/a-1020000000698620

https://juejin.im/post/5bcd89d5e51d4579bb1c5e22

https://www.zhihu.com/question/28896402

-------------已经触及底线 感谢您的阅读-------------

本文标题:考试JS倒计时客户端和服务器时间同步问题

文章作者:趙小傑~~

发布时间:2019年11月04日 - 19:49:01

最后更新:2019年11月04日 - 19:49:04

原始链接:https://cnsyear.com/posts/abf82317.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%