JavaScript实现网页加载进度条:从伪进度到仿真优化的完整方案

作者:巡夜人

时间:2026-10-22

浏览:133

本文详细解析如何使用JavaScript实现网页加载进度条。针对Web资源加载无法获取真实进度的特性,文章从基础的window.onload方案出发,逐步优化为包含随机增量、动态延迟、超时控制及多资源分段追踪的仿真进度条,旨在提升用户等待体验并减少流失。

文章目录

Loading动画的基础应用

Loading动画在各类应用中极为常见,例如移动端App的下拉刷新与上拉加载功能。为了向用户直观展示数据正在处理中,通常会采用过渡动画来传达加载状态。常见的表现形式包括旋转的圆圈或跳动的省略号等。

网页Loading主要用于展示页面加载进度或数据请求过程。对于数据加载而言,实现相对简单:只需在发起Ajax请求前显示Loading效果,并在请求完成后隐藏即可。

然而,实现页面整体的加载进度则更为复杂,需要特定的技术手段。

页面加载进度是一个常见但技术实现较为晦涩的需求。其常见性体现在对“重”网页(尤其是网页游戏)的重要性上;其晦涩性则源于Web技术的特性:由于资源分散且加载时机不可控,很难获取“真实”的加载进度。在逻辑代码完全加载前,无法统计具体进度,且逻辑代码本身的加载进度也难以量化。此外,监控所有资源加载情况在技术上几乎不可行。

因此,页面加载进度本质上是“假”的,其核心目的是提升用户体验,避免用户在打开页面后长时间面对空白页面,从而降低用户流失率。

既然是“假”的进度,就必须做到“仿真”才具有实际价值。用户通常不关心某一时刻是否精确加载了百分之几,而是关注还需要等待多久。接下来,我们将逐步实现一个页面加载进度的Loading效果。

基础Loading实现与局限

首先,我们需要准备一段用于展示Loading状态的HTML代码:


<!DOCTYPE html>
<html>
<head>
 <title>动手实现一个网页加载进度loading</title>
</head>
<body>
 <div class="loading" id="loading">
 <div class="progress" id="progress">0%</div>
 </div>
</body>
</html>

接着,添加相应的CSS样式以美化Loading效果:


.loading {
 display: table;
 position: fixed;
 top: 0;
 left: 0;
 width: 100%;
 height: 100%;
 background-color: #fff;
 z-index: 5;
}
.loading .progress {
 display: table-cell;
 vertical-align: middle;
 text-align: center;
}

假设该Loading仅在页面加载完成后隐藏,中间无需显示具体进度。此时,最直接的想法是利用window.onload事件:

(以下示例代码为了方便演示,默认使用jQuery,并采用了ES6的箭头函数语法)


var $loading = $('#loading')
var $progress = $('#progress')
window.onload = () => {
 $loading.hide()
}

这样便实现了基本的Loading流程。为了增加进度条效果,我们可以每隔100毫秒将进度自增1,直至达到100%。同时,在window.onload触发时隐藏Loading层。

我们进一步补充进度条的具体实现逻辑:


var $loading = $('#loading')
var $progress = $('#progress')
var prg = 0 // 初始化进度
var timer = window.setInterval(() => { // 设置定时器
 if (prg >= 100) { // 到达终点,关闭定时器
 window.clearInterval(timer)
 prg = 100
 } else { // 未到终点,进度自增
 prg++
 }
 $progress.html(prg + '%')
 console.log(prg)
}, 100)
window.onload = () => {
 $loading.hide()
}

虽然效果看似不错,但存在一个问题:如果window.onload触发较慢,导致进度条先显示加载到100%,而Loading层仍未隐藏,会造成体验上的割裂。因此,需要确保Loading在window.onload触发时才到达终点。在此之前,进度条可以保持等待状态,例如在80%时暂停,待loaded事件触发后快速推进至100%。这是目前大多数进度条采用的做法。


var $loading = $('#loading')
var $progress = $('#progress')
var prg = 0
var timer = window.setInterval(() => {
 if (prg >= 80) { // 到达第一阶段80%,关闭定时器,保持等待
 window.clearInterval(timer)
 prg = 100
 } else {
 prg++
 }
 $progress.html(prg + '%')
 console.log(prg)
}, 100)
window.onload = () => {
 window.clearInterval(timer)
 window.setInterval(() => {
 if (prg >= 100) { // 到达终点,关闭定时器
 window.clearInterval(timer)
 prg = 100
 $loading.hide()
 } else {
 prg++
 }
 $progress.html(prg + '%')
 console.log(prg)
 }, 10) // 时间间隔缩短
}

基于上述逻辑,我们可以提炼并封装代码,去除重复部分:


var $loading = $('#loading')
var $progress = $('#progress')
var prg = 0
var timer = 0
progress(80, 100)
window.onload = () => {
 progress(100, 10, () => {
 $loading.hide()
 })
}
function progress (dist, delay, callback) {
 window.clearInterval(timer)
 timer = window.setInterval(() => {
 if (prg >= dist) {
 window.clearInterval(timer)
 prg = dist
 callback && callback()
 } else {
 prg++
 }
 $progress.html(prg + '%')
 console.log(prg)
 }, delay)
}

通过封装,我们得到了一个progress函数,作为核心功能模块。该函数通过接收目标值和间隔时间,即可模拟进度的演化过程。

进度条优化:随机化与动态延迟

目前实现的进度条仍存在以下问题:

1.0、进度变化过于均匀,相同时间间隔和增量,不符合网络环境的波动特点;

2.0、window.onload触发过快,导致用户来不及看清100%的状态,Loading便已消失;

3.0、第一阶段总是在80%处固定暂停,显得不够自然;

针对第一个问题,我们需要让时间间隔和增量随机化;第二个问题较为简单,只需在最终隐藏前增加延迟即可;第三个问题则需要随机生成一个初始值。

实现增量随机较为容易,但如何使时间间隔随机?由于setInterval无法动态设置delay,我们需要将其改造为使用setTimeout实现。(关于setInterval与setTimeout的用法和区别,此处不再赘述。)


var $loading = $('#loading')
var $progress = $('#progress')
var prg = 0
var timer = 0
progress([80, 90], [1, 3], 100) // 使用数组来表示随机数的区间
window.onload = () => {
 progress(100, [1, 5], 10, () => {
 window.setTimeout(() => { // 延迟了一秒再隐藏loading
 $loading.hide()
 }, 1000)
 })
}
function progress (dist, speed, delay, callback) {
 var _dist = random(dist)
 var _delay = random(delay)
 var _speed = random(speed)
 window.clearTimeout(timer)
 timer = window.setTimeout(() => {
 if (prg + _speed >= _dist) {
 window.clearTimeout(timer)
 prg = _dist
 callback && callback()
 } else {
 prg += _speed
 progress (_dist, speed, delay, callback)
 }
 $progress.html(parseInt(prg) + '%') // 留意,由于已经不是自增1,所以这里要取整
 console.log(prg)
 }, _delay)
}
function random (n) {
 if (typeof n === 'object') {
 var times = n[1] - n[0]
 var offset = n[0]
 return Math.random() * times + offset
 } else {
 return n
 }
}

至此,我们基本完成了需求功能。

高级优化:超时控制与首屏加载

但是,还有一个隐蔽的问题:当前使用window.onload时,从进入页面到触发该事件的时间间隔非常短,用户几乎感受不到第一阶段进度(80%)。这本身没有问题——我们关注的是,如果页面加载资源数量多、体积大,从进入页面到window.onload的时间可能很长(5~20秒不等)。实际上,我们只需为首屏资源的加载争取时间,无需等待所有资源就绪,且更快呈现页面是提高用户体验的关键。

我们需要考虑Loading停留过久的情况,因此应为其设置一个超时时间。若超过该时间而window.onload仍未完成,也应将进度推至100%并结束Loading。


var $loading = $('#loading')
var $progress = $('#progress')
var prg = 0
var timer = 0
progress([80, 90], [1, 3], 100) // 使用数组来表示随机数的区间
window.onload = () => {
 progress(100, [1, 5], 10, () => {
 window.setTimeout(() => { // 延迟了一秒再隐藏loading
 $loading.hide()
 }, 1000)
 })
}
window.setTimeout(() => { // 设置5秒的超时时间
 progress(100, [1, 5], 10, () => {
 window.setTimeout(() => { // 延迟了一秒再隐藏loading
 $loading.hide()
 }, 1000)
 })
}, 5000)
function progress (dist, speed, delay, callback) {
 var _dist = random(dist)
 var _delay = random(delay)
 var _speed = random(speed)
 window.clearTimeout(timer)
 timer = window.setTimeout(() => {
 if (prg + _speed >= _dist) {
 window.clearTimeout(timer)
 prg = _dist
 callback && callback()
 } else {
 prg += _speed
 progress (_dist, speed, delay, callback)
 }
 $progress.html(parseInt(prg) + '%') // 留意,由于已经不是自增1,所以这里要取整
 console.log(prg)
 }, _delay)
}
function random (n) {
 if (typeof n === 'object') {
 var times = n[1] - n[0]
 var offset = n[0]
 return Math.random() * times + offset
 } else {
 return n
 }
}

我们可以直接设置一个定时器,例如5秒作为超时时间。这种做法是可行的。

然而,仍存在一个问题:该定时器在JS加载完毕后才开始生效,这意味着我们忽略了JS加载前的时间。误差可能很大,例如设置的5秒超时,用户实际可能等待了8秒,这不符合用户体验优化的初衷。因此,开始时间需要提前。我们可以在head中记录页面进入时间,然后在JS中进行对比:如果时间差大于超时时间,则直接执行完成步骤;如果小于超时时间,则等待剩余时间后再完成进度。

首先在head中埋点,记录用户进入页面的时间loadingStartTime:


<!DOCTYPE html>
<html>
<head>
 <title>动手实现一个网页加载进度loading</title>
 <script>
 window.loadingStartTime = new Date()
 </script>
 <script src="index.js"></script>
</head>
<body>
 <div class="loading" id="loading">
 <div class="progress" id="progress">0%</div>
 </div>
</body>
</html>

然后,对比当前时间,判断是否超时:(为方便代码复用,我将完成部分封装为complete函数)


var $loading = $('#loading')
var $progress = $('#progress')
var prg = 0
var timer = 0
var now = new Date() // 记录当前时间
var timeout = 5000 // 超时时间
progress([80, 90], [1, 3], 100)
window.onload = () => {
 complete()
}
if (now - loadingStartTime > timeout) { // 超时
 complete()
} else {
 window.setTimeout(() => { // 未超时,则等待剩余时间
 complete()
 }, timeout - (now - loadingStartTime))
}
function complete () { // 封装完成进度功能
 progress(100, [1, 5], 10, () => {
 window.setTimeout(() => {
 $loading.hide()
 }, 1000)
 })
}
function progress (dist, speed, delay, callback) {
 var _dist = random(dist)
 var _delay = random(delay)
 var _speed = random(speed)
 window.clearTimeout(timer)
 timer = window.setTimeout(() => {
 if (prg + _speed >= _dist) {
 window.clearTimeout(timer)
 prg = _dist
 callback && callback()
 } else {
 prg += _speed
 progress (_dist, speed, delay, callback)
 }
 $progress.html(parseInt(prg) + '%')
 console.log(prg)
 }, _delay)
}
function random (n) {
 if (typeof n === 'object') {
 var times = n[1] - n[0]
 var offset = n[0]
 return Math.random() * times + offset
 } else {
 return n
 }
}

至此,我们完整实现了这一功能。

然而,事情并未结束,你或许过于天真了。

仿真进度:多资源分段追踪

如果目的仅是编写一个纯粹的障眼法伪Loading,则与其他Loading实现无异。我们做事应脚踏实地,能实现的尽量实现,不能实现的,为团队和谐不得已采用一些“坑蒙拐骗”的手段。那么,我们能否更贴近实际情况?答案是肯定的。

我们来分析一个场景:假设希望Loading更加真实,可以选择性地跟踪页面上几个大资源的加载情况,并拆分进度条。例如,页面有三张大图a、b、c,我们将进度条拆分为五段,每加载完一张图便推进一个进度:

随机初始化[10, 20] -> 图a推进20%的进度 -> 图b推进25%的进度 -> 图c推进30%的进度 -> 完成100%

这三张图将占据20% + 25% + 30% = 75%的进度。

问题是,如果图片加载按顺序进行,我们可以简单地实现:10(假设初始进度为10%) -> 30 -> 55 -> 85 -> 100。但事实是,图片加载顺序不确定,谁先谁后无法预知。因此,我们需要更合理的方式管理这些进度增量,避免相互覆盖。

1.0、我们需要一个能够累计增量的变量next;

2.0、由于我们的progress函数传递的是目标进度,我们需要另一个函数add来传递增量进度。


var $loading = $('#loading')
var $progress = $('#progress')
var prg = 0
var timer = 0
var now = new Date()
var timeout = 5000
var next = prg
add([30, 50], [1, 3], 100) // 第一阶段
window.setTimeout(() => { // 模拟图a加载完
 add(20, [1, 3], 200)
}, 1000)
window.setTimeout(() => { // 模拟图c加载完
 add(30, [1, 3], 200)
}, 2000)
window.setTimeout(() => { // 模拟图b加载完
 add(25, [1, 3], 200)
}, 2500)
window.onload = () => {
 complete()
}
if (now - loadingStartTime > timeout) {
 complete()
} else {
 window.setTimeout(() => {
 complete()
 }, timeout - (now - loadingStartTime))
}
function complete () {
 add(100, [1, 5], 10, () => {
 window.setTimeout(() => {
 $loading.hide()
 }, 1000)
 })
}
function add (dist, speed, delay, callback) {
 var _dist = random(dist)
 if (next + _dist > 100) { // 对超出部分裁剪对齐
 next = 100
 } else {
 next += _dist
 }
 progress(next, speed, delay, callback)
}
function progress (dist, speed, delay, callback) {
 var _delay = random(delay)
 var _speed = random(speed)
 window.clearTimeout(timer)
 timer = window.setTimeout(() => {
 if (prg + _speed >= dist) {
 window.clearTimeout(timer)
 prg = dist
 callback && callback()
 } else {
 prg += _speed
 progress (dist, speed, delay, callback)
 }
 $progress.html(parseInt(prg) + '%')
 console.log(prg)
 }, _delay)
}
function random (n) {
 if (typeof n === 'object') {
 var times = n[1] - n[0]
 var offset = n[0]
 return Math.random() * times + offset
 } else {
 return n
 }
}

此处为方便演示,使用setTimeout模拟图片加载,真实应用中应使用image.onload。

接下来整理略显杂乱的代码,并将其封装为一个插件,下载地址为:http://xiazai.jb51.net/201701/yuanma/ez-progress-master_jb51.rar

ez-progress是一个Web(伪)进度插件,使用ez-progress实现该功能非常简单:


var Progress = require('ez-progress')
var prg = new Progress()
var $loading = $('#loading')
var $progress = $('#progress')
prg.on('progress', function (res) {
 var progress = parseInt(res.progress) // 注意进度取整,不然有可能会出现小数
 $progress.html(progress + '%')
})
prg.go([60, 70], function (res) {
 prg.complete(null, [0, 5], [0, 50]) // 飞一般地冲向终点
}, [0, 3], [0, 200])
window.onload = function () {
 prg.complete(null, [0, 5], [0, 50]) // 飞一般地冲向终点
}

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章