javascript實(shí)現(xiàn)一個(gè)網(wǎng)頁(yè)加載進(jìn)度loading
loading隨處可見,比如一個(gè)app經(jīng)常會(huì)有下拉刷新,上拉加載的功能,在刷新和加載的過程中為了讓用戶感知到 load 的過程,我們會(huì)使用一些過渡動(dòng)畫來表達(dá)。最常見的比如“轉(zhuǎn)圈圈”,“省略號(hào)”等等。
網(wǎng)頁(yè)loading有很多用處,比如頁(yè)面的加載進(jìn)度,數(shù)據(jù)的加載過程等等,數(shù)據(jù)的加載loading很好做,只需要在加載數(shù)據(jù)之前(before ajax)顯示loading效果,在數(shù)據(jù)返回之后(ajax completed)結(jié)束loading效果,就可以了。
但是頁(yè)面的加載進(jìn)度,需要一點(diǎn)技巧。
頁(yè)面加載進(jìn)度一直以來都是一個(gè)常見而又晦澀的需求,常見是因?yàn)樗谀承爸亍本W(wǎng)頁(yè)(特別是網(wǎng)頁(yè)游戲)的應(yīng)用特別重要;晦澀是因?yàn)閣eb的特性,各種零散資源決定它很難是“真實(shí)”的進(jìn)度,只能是一種“假”的進(jìn)度,至少在邏輯代碼加載完成之前,我們都不能統(tǒng)計(jì)到進(jìn)度,而邏輯代碼自身的進(jìn)度也無法統(tǒng)計(jì)。另外,我們不可能監(jiān)控到所有資源的加載情況。
所以頁(yè)面的加載進(jìn)度都是“假”的,它存在的目的是為了提高用戶體驗(yàn),使用戶不至于在打開頁(yè)面之后長(zhǎng)時(shí)間面對(duì)一片空白,導(dǎo)致用戶流失。
既然是“假”的,我們就要做到“仿真”才有用。仿真是有意義的,事實(shí)上用戶并不在乎某一刻你是不是真的加載到了百分之幾,他只關(guān)心你還要load多久。所以接下來我們就來實(shí)現(xiàn)一個(gè)頁(yè)面加載進(jìn)度loading。
首先準(zhǔn)備一段loading的html:
<!DOCTYPE html> <html> <head> <title>動(dòng)手實(shí)現(xiàn)一個(gè)網(wǎng)頁(yè)加載進(jìn)度loading</title> </head> <body> <div class="loading" id="loading"> <div class="progress" id="progress">0%</div> </div> </body> </html>
來點(diǎn)樣式裝扮一下:
.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; }
我們先假設(shè)這個(gè)loading只需要在頁(yè)面加載完成之后隱藏,中間不需要顯示進(jìn)度。那么很簡(jiǎn)單,我們第一時(shí)間想到的就是window.onload:
(以下內(nèi)容為了方便演示,默認(rèn)使用jQuery,語(yǔ)法有es6的箭頭函數(shù))
var $loading = $('#loading') var $progress = $('#progress') window.onload = () => { $loading.hide() }
ok,這樣基本的loading流程就有了,我們?cè)黾右粋€(gè)進(jìn)度的效果,每隔100ms就自增1,一直到100%為止,而另一方面window loaded的時(shí)候,我們把loading給隱藏。
我們來補(bǔ)充一下進(jìn)度:
var $loading = $('#loading') var $progress = $('#progress') var prg = 0 // 初始化進(jìn)度 var timer = window.setInterval(() => { // 設(shè)置定時(shí)器 if (prg >= 100) { // 到達(dá)終點(diǎn),關(guān)閉定時(shí)器 window.clearInterval(timer) prg = 100 } else { // 未到終點(diǎn),進(jìn)度自增 prg++ } $progress.html(prg + '%') console.log(prg) }, 100) window.onload = () => { $loading.hide() }
效果不錯(cuò),但是有個(gè)問題,萬一window loaded太慢了,導(dǎo)致進(jìn)度顯示load到100%了,loading還沒有隱藏,那就打臉了。所以,我們需要讓loading在window loaded的時(shí)候才到達(dá)終點(diǎn),在這之前,loading可以保持一個(gè)等待的狀態(tài),比如在80%的時(shí)候,先停一停,然后在loaded的時(shí)候快速將進(jìn)度推至100%。這個(gè)做法是目前絕大部份進(jìn)度條的做法。
var $loading = $('#loading') var $progress = $('#progress') var prg = 0 var timer = window.setInterval(() => { if (prg >= 80) { // 到達(dá)第一階段80%,關(guān)閉定時(shí)器,保持等待 window.clearInterval(timer) prg = 100 } else { prg++ } $progress.html(prg + '%') console.log(prg) }, 100) window.onload = () => { window.clearInterval(timer) window.setInterval(() => { if (prg >= 100) { // 到達(dá)終點(diǎn),關(guān)閉定時(shí)器 window.clearInterval(timer) prg = 100 $loading.hide() } else { prg++ } $progress.html(prg + '%') console.log(prg) }, 10) // 時(shí)間間隔縮短 }
ok,這差不多就是我們想要的功能了,我們來提煉一下代碼,把重復(fù)的代碼給封裝一下:
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) }
我們得到了一個(gè)progress函數(shù),這個(gè)函數(shù)就是我們主要的功能模塊,通過傳入一個(gè)目標(biāo)值、一個(gè)時(shí)間間隔,就可以模擬進(jìn)度的演化過程。
目前來看,這個(gè)進(jìn)度還是有些問題的:
1.0、進(jìn)度太平均,相同的時(shí)間間隔,相同的增量,不符合網(wǎng)絡(luò)環(huán)境的特點(diǎn);
2.0、window.onload太快,我們還來不及看清100%,loading就已經(jīng)不見了;
3.0、每次第一階段都是在80%就暫停了,露餡兒了;
第一個(gè)點(diǎn),我們要讓時(shí)間間隔隨機(jī),增量也隨機(jī);第二個(gè)點(diǎn)很簡(jiǎn)單,我們延遲一下就好了;第三點(diǎn)也需要我們隨機(jī)產(chǎn)生一個(gè)初始值。
增量隨機(jī)很好辦,如何讓時(shí)間間隔隨機(jī)?setInterval是無法動(dòng)態(tài)設(shè)置delay的,那么我們就要把它改造一下,使用setTimeout來實(shí)現(xiàn)。(setInterval跟setTimeout的用法和區(qū)別就不細(xì)說了吧?)
var $loading = $('#loading') var $progress = $('#progress') var prg = 0 var timer = 0 progress([80, 90], [1, 3], 100) // 使用數(shù)組來表示隨機(jī)數(shù)的區(qū)間 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) + '%') // 留意,由于已經(jīng)不是自增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 } }
至此,我們差不多完成了需求。
but,還有一個(gè)比較隱蔽的問題,我們現(xiàn)在使用window.onload,發(fā)現(xiàn)從進(jìn)入頁(yè)面,到window.onload這中間相隔時(shí)間十分短,我們基本是感受不到第一階段進(jìn)度(80%)的,這是沒有問題的——我們?cè)谝獾氖?,如果?yè)面的加載資源數(shù)量很多,體積很大的時(shí)候,從進(jìn)入頁(yè)面,到window.onload就不是這么快速了,這中間可能會(huì)很漫長(zhǎng)(5~20秒不等),但事實(shí)上,我們只需要為 首屏資源 的加載爭(zhēng)取時(shí)間就可以了,不需要等待所有資源就緒,而且更快地呈現(xiàn)頁(yè)面也是提高用戶體驗(yàn)的關(guān)鍵。
我們應(yīng)該考慮頁(yè)面loading停留過久的情況,我們需要為loading設(shè)置一個(gè)超時(shí)時(shí)間,超過這個(gè)時(shí)間,假設(shè)window.onload還沒有完成,我們也要把進(jìn)度推到100%,把loading結(jié)束掉。
var $loading = $('#loading') var $progress = $('#progress') var prg = 0 var timer = 0 progress([80, 90], [1, 3], 100) // 使用數(shù)組來表示隨機(jī)數(shù)的區(qū)間 window.onload = () => { progress(100, [1, 5], 10, () => { window.setTimeout(() => { // 延遲了一秒再隱藏loading $loading.hide() }, 1000) }) } window.setTimeout(() => { // 設(shè)置5秒的超時(shí)時(shí)間 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) + '%') // 留意,由于已經(jīng)不是自增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 } }
我們直接設(shè)置了一個(gè)定時(shí)器,5s的時(shí)間來作為超時(shí)時(shí)間。這樣做是可以的。
but,還是有問題,這個(gè)定時(shí)器是在js加載完畢之后才開始生效的,也就是說,我們忽略了js加載完畢之前的時(shí)間,這誤差可大可小,我們?cè)O(shè)置的5s,實(shí)際用戶可能等待了8s,這是有問題的。我們做用戶體驗(yàn),需要從實(shí)際情況去考慮,所以這個(gè)開始時(shí)間還需要再提前一些,我們?cè)趆ead里來記錄這個(gè)開始時(shí)間,然后在js當(dāng)中去做對(duì)比,如果時(shí)間差大于超時(shí)時(shí)間,那我們就可以直接執(zhí)行最后的完成步驟,如果小于超時(shí)時(shí)間,則等待 剩余的時(shí)間 過后,再完成進(jìn)度。
先在head里埋點(diǎn),記錄用戶進(jìn)入頁(yè)面的時(shí)間loadingStartTime:
<!DOCTYPE html> <html> <head> <title>動(dòng)手實(shí)現(xiàn)一個(gè)網(wǎng)頁(yè)加載進(jìn)度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>
然后,我們對(duì)比 當(dāng)前的時(shí)間 ,看是否超時(shí):(為了方便復(fù)用代碼,我把完成的部分封裝成函數(shù)complete)
var $loading = $('#loading') var $progress = $('#progress') var prg = 0 var timer = 0 var now = new Date() // 記錄當(dāng)前時(shí)間 var timeout = 5000 // 超時(shí)時(shí)間 progress([80, 90], [1, 3], 100) window.onload = () => { complete() } if (now - loadingStartTime > timeout) { // 超時(shí) complete() } else { window.setTimeout(() => { // 未超時(shí),則等待剩余時(shí)間 complete() }, timeout - (now - loadingStartTime)) } function complete () { // 封裝完成進(jìn)度功能 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 } }
至此,我們算是完整地實(shí)現(xiàn)了這一功能。
然而,事情還沒有結(jié)束,少年你太天真。
如果目的是為了寫一個(gè)純粹障眼法的偽loading,那跟其他loading的實(shí)現(xiàn)就沒什么區(qū)別了,我們做事講究腳踏實(shí)地,能實(shí)現(xiàn)的實(shí)現(xiàn),不能實(shí)現(xiàn)的,為了團(tuán)隊(duì)和諧,我們不得已坑蒙拐騙。那么我們還能更貼近實(shí)際情況一點(diǎn)嗎?其實(shí)是可以的。
我們來分析一個(gè)場(chǎng)景,假設(shè)我們想讓我們的loading更加真實(shí)一些,那么我們可以選擇性地對(duì)頁(yè)面上幾個(gè)比較大的資源的加載進(jìn)行跟蹤,然后拆分整個(gè)進(jìn)度條,比如我們頁(yè)面有三張大圖a、b、c,那么我們將進(jìn)度條拆成五段,每加載完一張圖我們就推進(jìn)一個(gè)進(jìn)度:
隨機(jī)初始化[10, 20] ->
圖a推進(jìn)20%的進(jìn)度 ->
圖b推進(jìn)25%的進(jìn)度 ->
圖c推進(jìn)30%的進(jìn)度 ->
完成100%
這三張圖要占20% + 25% + 30% = 75%的進(jìn)度。
問題是,如果圖片加載完成是按照順序來的,那我們可以很簡(jiǎn)單地:10(假設(shè)初始進(jìn)度是10%) -> 30 -> 55 -> 85 -> 100,但事實(shí)是,圖片不會(huì)按照順序來,誰早到誰晚到是說不準(zhǔn)的,所以我們需要更合理的方式去管理這些進(jìn)度增量,使它們不會(huì)互相覆蓋。
1.0、我們需要一個(gè)能夠替我們累計(jì)增量的變量next;
2.0、由于我們的progress都是傳目的進(jìn)度的,我們需要另外一個(gè)函數(shù)add,來傳增量進(jìn)度。
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) { // 對(duì)超出部分裁剪對(duì)齊 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來模擬圖片的加載,真實(shí)應(yīng)用應(yīng)該是使用image.onload。
接下來整理一下有點(diǎn)亂的代碼,封裝成一個(gè)插件,下載地址為:http://xiazai.jb51.net/201701/yuanma/ez-progress-master_jb51.rar
ez-progress 是一個(gè)web(偽)進(jìn)度插件,使用 ez-progress 實(shí)現(xiàn)這個(gè)功能非常簡(jiǎn)單:
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) // 注意進(jìn)度取整,不然有可能會(huì)出現(xiàn)小數(shù) $progress.html(progress + '%') }) prg.go([60, 70], function (res) { prg.complete(null, [0, 5], [0, 50]) // 飛一般地沖向終點(diǎn) }, [0, 3], [0, 200]) window.onload = function () { prg.complete(null, [0, 5], [0, 50]) // 飛一般地沖向終點(diǎn) }
以上就是本文的全部?jī)?nèi)容,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來一定的幫助,同時(shí)也希望多多支持腳本之家!
相關(guān)文章
javascript-TreeView父子聯(lián)動(dòng)效果保持節(jié)點(diǎn)狀態(tài)一致
javascript-TreeView父子聯(lián)動(dòng)效果保持節(jié)點(diǎn)狀態(tài)一致...2007-08-08使用遞歸遍歷對(duì)象獲得value值的實(shí)現(xiàn)方法
下面小編就為大家?guī)硪黄褂眠f歸遍歷對(duì)象獲得value值的實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-06-06JS 自動(dòng)完成 AutoComplete(Ajax 查詢)
實(shí)現(xiàn)類似于百度或谷歌的搜索下拉列表的,就是打開百度往里輸入你要查詢的條件,只要你一輸入他就自動(dòng)彈出一個(gè)下拉列表框,并顯示相關(guān)所有搜索內(nèi)容2009-07-07js中的window.open返回object的錯(cuò)誤的解決方法
系統(tǒng)中用javascript中的window.open后,頁(yè)面返回了一個(gè)[object]。因?yàn)橄到y(tǒng)的原因,必需使用href="javascript:window.open()"這樣的格式。所以只能通過以下辦法解決。2009-08-08Javascript & DHTML DOM基礎(chǔ)和基本API
DOM是文檔對(duì)象模型(Document Object Model,是基于瀏覽器編程(在本教程中,可以說就是DHTML編程)的一套API接口,W3C出臺(tái)的推薦標(biāo)準(zhǔn),每個(gè)瀏覽器都有一些細(xì)微的差別,其中以Mozilla的瀏覽器最與標(biāo)準(zhǔn)接近。2008-07-07IE瀏覽器IFrame對(duì)象內(nèi)存不釋放問題解決方法
IFrame對(duì)象占用的內(nèi)存資源在窗體關(guān)閉后不會(huì)釋放。彈出關(guān)閉反復(fù)多次后,IE瀏覽器內(nèi)存占用可超過數(shù)百M(fèi),嚴(yán)重時(shí)IE瀏覽器報(bào)錯(cuò)2014-08-08