作者:韓永豪 移動開發(fā)部 前端開發(fā)工程師
今年的11月初,我們公司參加了「2017年亞洲幼教年會(APEAC)」并取得了很不錯(cuò)的成果。本人有幸負(fù)責(zé)關(guān)于這次展示頁的前端開發(fā),特以此文記錄開發(fā)過程中的關(guān)鍵環(huán)節(jié)。
展示頁分為三大模塊:數(shù)據(jù)展示、動態(tài)展示和地圖展示。效果如下:
數(shù)據(jù)展示
此模塊展示我們公司至今為止的各項(xiàng)數(shù)據(jù),通過異步請求定時(shí)更新。
數(shù)字過渡的動態(tài)效果為類似于老虎機(jī)的效果,對應(yīng)數(shù)位的新數(shù)字從下至上替換舊數(shù)字,如果該位數(shù)的數(shù)字沒有發(fā)生變化,則沒有過渡效果。
要實(shí)現(xiàn)這種效果,第一步要把數(shù)字按位切分:
// 分離每個(gè)數(shù)字
function split(num) {
return (num || 0).toString().split('');
}
然后,增加千分位,即從個(gè)位開始,每隔三位插入一個(gè)逗號,實(shí)現(xiàn)代碼如下:
function toThousands(num) {
var num = (num || 0).toString(), result = '';
while (num.length > 3) {
result = ',' + num.slice(-3) + result;
num = num.slice(0, num.length - 3);
}
if (num) { result = num + result; }
return result;
}
最后利用樣式控制過渡動畫,關(guān)鍵代碼如下:
<ul id="main" class="number">
<li class="group">
<span class="old">1</span>
<span class="new">1</span>
</li>
<li class="group">
<span class="old">,</span>
<span class="new">,</span>
</li>
<li class="group">
<span class="old">4</span>
<span class="new">1</span>
</li>
<li class="group">
<span class="old">5</span>
<span class="new">5</span>
</li>
<li class="group">
<span class="old">6</span>
<span class="new">2</span>
</li>
</ul>
.number li {
width: 0.18rem;
height: 0.24rem;
line-height: 0.24rem;
display: inline-block;
overflow: hidden;
}
.number li span {
display: block;
transform: translateY(0%);
}
.number li.active span {
animation: move 0.3s;
animation-fill-mode: forwards; // 讓動畫結(jié)束后保持最后一幀
}
@keyframes move {
from {
transform: translateY(0);
}
to {
transform: translateY(-100%);
}
}
var $main = document.querySelector('#main');
// 填充數(shù)字
function update(fromArr, toArr) {
// 從個(gè)位數(shù)開始對齊位數(shù)
fromArr = fromArr.reverse();
toArr = toArr.reverse();
if (fromArr.length > toArr.length) {
toArr.length = fromArr.length
} else {
fromArr.length = toArr.length
}
fromArr = fromArr.reverse();
toArr = toArr.reverse();
// 渲染節(jié)點(diǎn)并激活動畫
var numberHTML = ''
for (var i = 0; i < toArr.length; i++) {
// 如果該位數(shù)的數(shù)字沒有發(fā)生變化,則沒有過渡效果
if (formArr[i] !== toArr[i]) {
numberHTML += ('<li class="group active">' +
'<span class="old">' + formArr[i] || '' + '</span>' +
'<span class="new">' + toArr[i] || '' + '</span>' +
'</li>');
} else {
numberHTML += ('<li class="group">' +
'<span class="old">' + formArr[i] || '' + '</span>' +
'<span class="new">' + toArr[i] || '' + '</span>' +
'</li>');
}
}
if (numberHTML) {
$main.innerHTML = numberHTML;
}
}
動態(tài)展示
此模塊是游客現(xiàn)場互動的區(qū)域,只要掃一下二維碼點(diǎn)贊,就會新增一條動態(tài)。
頁面運(yùn)行過程中,可能同時(shí)有多個(gè)人發(fā)動態(tài),所以要有一條線程定時(shí)請求數(shù)據(jù),并把數(shù)據(jù)保存在隊(duì)列中:
同時(shí),要有另一條線程讀取隊(duì)列的數(shù)據(jù)進(jìn)行渲染:
關(guān)鍵代碼如下:
var cacheList = []; // 隊(duì)列列表
var CHECK_INTERVAL = 2000; // 每個(gè)兩秒檢查一下隊(duì)列
var UPDATE_INTERVAL = 1000; // 插入數(shù)據(jù)間隔
var MIN_CACHE = 10; // 儲備數(shù)
// 檢查隊(duì)列
function checkCache() {
if (cacheList.length < MIN_CACHE) {
// 異步請求數(shù)據(jù)
ajax(function(res) {
if (res && res.length) {
cacheList = cacheList.concat(res); // 把新的數(shù)據(jù)合并到隊(duì)列列表
setTimeout(checkCache, CHECK_INTERVAL); // 輪詢檢查數(shù)據(jù)
}
}
}
}
// 開始加載
function loadData() {
if (cacheList.length > 0) {
render(cacheList[0]);
cacheList = cacheList.splice(0, 1);
}
setTimeout(start, UPDATE_INTERVAL); // 輪詢讀取數(shù)據(jù)
}
loadData(); // 數(shù)據(jù)啟動
checkCache(); // 隊(duì)列啟動
地圖展示
此模塊由中國地圖、固定的光點(diǎn)、閃爍的光點(diǎn)和動態(tài)氣泡構(gòu)成。
因?yàn)楣恻c(diǎn)和氣泡都與地理位置(精確到省份)有關(guān)聯(lián),所以首先要在地圖上劃分出每一個(gè)省份。然而,省份的占位是不規(guī)則的,劃分起來會有一定的難度。
剛開始的想法是每個(gè)省份有固定幾個(gè)點(diǎn),氣泡和光點(diǎn)只會固定出現(xiàn)在那幾個(gè)位置。雖然能實(shí)現(xiàn)效果,但是看起來比較僵硬,并沒有達(dá)到設(shè)計(jì)的效果。因此,又換了一種方案。
微積分在計(jì)算不規(guī)則圖形的面積時(shí),就是把一大塊不規(guī)則圖形切分成若干塊小矩形,以此鋪滿整個(gè)不規(guī)則圖形。同理,只需要按省份畫出其對應(yīng)的幾個(gè)小矩形,并記錄下來,就可以根據(jù)這些坐標(biāo)讓光點(diǎn)和氣泡出現(xiàn)在對應(yīng)省份的位置。為了便于畫出這些小矩形,我做了一個(gè)小工具,效果如下圖:
因?yàn)轫撁媸歉鶕?jù)屏幕分辨率自適應(yīng)寬高的,因此地圖在頁面上的尺寸是不固定的(但是比例是固定的),所以這里做了一點(diǎn)調(diào)整,生成的坐標(biāo)為百分比而不是像素值。例如:
// 記錄的省份數(shù)據(jù)
var positionData = {
新疆: [
{
startX: '6.7164179104477615%',
endX: '34.07960199004975%',
startY: '8.602941176470589%',
endY: '19.77941176470588%'
},
{
startX: '25.37313432835821%',
endX: '35.69651741293532%',
startY: '3.5049019607843137%',
endY: '8.602941176470589%'
},
...
],
廣東: [
{
...
},
...
],
...
};
最后,只需要在劃分好的小矩形中選取某一塊,然后從這塊小矩形的面積中隨機(jī)抽一個(gè)點(diǎn)顯示光點(diǎn)和氣泡:
// 地圖
var $map = document.querySelector('#map');
// 隨機(jī)獲取一個(gè)點(diǎn)
function getPosition(province) {
// 獲取省里面隨機(jī)一個(gè)小矩形
function getPositionArea(areaArray) {
return areaArray[Math.round(Math.random() * areaArray.length)];
}
// 選定一個(gè)小矩形
var area = getPositionArea(positionData[province]);
var x = parseFloat(area.endX) - parseFloat(area.startX);
var y = parseFloat(area.endY) - parseFloat(area.startY);
return {
x: parseFloat(area.startX) + (Math.random() * x) + '%',
y: parseFloat(area.startY) + (Math.random() * y) + '%'
};
}
// 獲取新的光點(diǎn)
function getPoint($point, province) {
if (!$point) {
$point = document.createElement('div');
// 如果動畫結(jié)束則移除光點(diǎn),并重新開始
$point.addEventListener('animationend', function() {
$point.classList.remove('active');
start($point);
});
}
// 更新節(jié)點(diǎn)坐標(biāo)和其他樣式
var position = getPosition(provice);
...
return $point;
}
// 刷新樣式并隨機(jī)加入地圖
function start($point) {
$point = getPoint($point);
setTimeout(function() {
$point.classList('active');
}, Math.round(Math.radom() * 1000))
}
// 創(chuàng)建六百個(gè)光點(diǎn)
var POINT_COUNT = 600;
for (var i = 0; i < POINT_COUNT; i++) {
start();
}
這樣就完成了。
應(yīng)急處理
由上可見,頁面上的數(shù)據(jù)和動畫都非常多,展會的設(shè)備未必能滿足這樣的性能要求。如果遇到性能比較低的機(jī)器,就需要減少數(shù)據(jù)或減弱動畫效果。為了能讓現(xiàn)場工作人員方便地調(diào)整,此頁面支持通過URL參數(shù)指定性能選項(xiàng)。例如:
/exhibition?pointCount=300
pointCount參數(shù)是用于修改光點(diǎn)數(shù)量,默認(rèn)為600個(gè)。此外還有其他參數(shù),就不再逐一列出了。
最后,一起來看看效果吧!