在手機上打開頁面時,經常會因為網絡不好導致需要較長的加載時間,如果這段時間內只是顯示一個“白板”,用戶體驗非常不好。通常的解決方案就是完整打開頁面前給用戶展示一個加載動畫,讓用戶能夠看到頁面還活着呢。本以為是個很簡單的活,真做起來才發現【有不少學問】!
一、首先要搞清楚的問題是,為什么加載過程中會出現”白板“,下面用Chrome開發者工具的Timeline分析面板做了幾個實驗。
1、不加載資源文件
順序:加載、解析、渲染、繪制
2、加載CSS
加載外部的CSS文件會阻止渲染,不論link的標簽放在什么位置上。
順序:加載、解析、加載(阻塞了渲染)、渲染、繪制
3、加載JS
a、在body前加載外部的JS文件會阻止渲染
順序:加載、解析、渲染(?)、加載(阻塞)、解析(JS執行完多了個解析)、渲染、繪制
b、在body后加載外部的JS文件不會阻止渲染
順序:加載、解析、渲染、繪制、加載、解析、渲染、繪制
4、在<script>標簽中動態創建link標簽加載CSS
加載CSS中斷了頁面的渲染和繪制
5、在<script>標簽中通過setTimeout函數動態創建link標簽加載CSS
加載CSS沒有中斷。我理解這是因為JS是單線程的,放在timeout里創建的link去排隊,瀏覽器就先不管它了。
二、基於上面的測試,實現頁面加載動畫理想的方式是什么?
先定好目標:盡快讓用戶看到變化,不要讓用戶以為頁面已經不響應,再逐步加載內容。
最快的方式就是做一個空的頁面,不加載任何外部資源(包括:CSS和JS)。頁面上方加載動畫的CSS定義和頁面元素,提供異步加載頁面元素、CSS和JS文件的JS。通過JS加載各類資源成功后關閉動畫效果,清除不必要的內容。
這樣就來個新問題,如何實現動態加載問題?
外部文件的動態加載問題很多文章都深入分析過了,簡單說,就是用異步加載,但是要考慮到各個JS文件的依賴關系問題。綜合比較了一下,決定requirejs實現動態加載。因為最近一直用angular,所以下面的代碼是require.js+angular。
HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta content="width=device-width,user-scalable=no,initial-scale=1.0" name="viewport">
<base href='/'>
<title>loading</title>
<style id='loadingStyle' type="text/css">
/*省略了,這里是loading元素轉圈的樣式,為了快應該壓縮了再放里面*/
</style>
</head>
<body>
<div id="content" ng-app="app" ng-controller="ctrl">
<ul class='list-group'>
<li class='list-group-item' ng-repeat="d in data"><i ng-bind="d"></i></li>
</ul>
</div>
<div class="loading"><div class='loading-indicator'><i></i></div></div>
<script src="static/js/require.js" defer async data-main="test/loading/loader.js?_=11"></script>
</body>
</html>
loader.js
因為angular不是AMD,所以用shim引用成angular全局變量
require.config({
paths: {
"angular": "/static/js/angular.min",
},
shim: {
"angular": {
exports: "angular"
},
},
deps: ['/test/loading/app.js?_=10']
});
app.js
如果真的要動態添加樣式,建議先獲得數據,把數據展現出來,在加載樣式,這樣就能讓用戶今早看到變化。
define(["angular"], function(angular) {
'use strict';
angular.module('app', []).controller('ctrl', ['$scope', '$timeout', function($scope, $timeout) {
$scope.data = [];
// 模擬長時間獲得數據
$timeout(function() {
for (var i = 0; i < 100; i++) {
$scope.data.push('data:' + i);
}
// 模擬長時間獲得樣式
$timeout(function() {
var link, head;
link = document.createElement('link');
link.href = "/test/loading/app.css?_=" + (new Date()).getTime();
link.rel = 'stylesheet';
link.onload = function() {
var eleLoading, eleStyle;
eleLoading = document.querySelector('.loading');
eleLoading.parentNode.removeChild(eleLoading);
eleStyle = document.querySelector('#loadingStyle');
eleStyle.parentNode.removeChild(eleStyle);
};
head = document.querySelector('head');
head.appendChild(link);
}, 2000);
}, 2000);
}]);
});
app.css
#content{color:red;}
三、還可以做什么?
可以考慮頁面的布局也動態加載,這樣用戶可以先看見頁面的框架,然后再獲取數據填到框架中。但是還沒有想到成熟的解決方案。