Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
文件名稱|版本號|聯系人|qq|備注
[干貨]在線直播技術8分鐘入門|v0.0.1|飛豺|8416837|直播技術
Docker版類似
rtmp服務器
簡介
rtmp是Adobe的私有協議,傳輸速度快,畫質好,好用。
nginx里的配置
搭建nginx不是本文重點,若是安裝包安裝,簡要說一下:下載安裝包,解壓,在服務器安裝前提軟件,編譯,安裝nginx即可.
vim conf/nginx.conf
# 極簡配置,成功接收推流。可以搭配on_connect等回調接口. rtmp { server{ listen 1935; # rtmp專用端口必須是1935,便于映射到外網 access_log logs/rtmp_access.log; application rtmp { live on; } } }
重啟后,接收推流的地址即為:ip:1935/rtmp/自定義的文件名稱,之后使用ffmpeg推流。
ffmpeg推流工具
部署
# 安裝推流工具 git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg sh configure --enable-shared --prefix=/usr/local/ffmpeg --disable-yasm make && make install # 漫長的等待 cd /usr/local/ffmpeg sh bin/ffmpeg -version
執行sh configure時可能報錯↓
nasm/yasm not found or too old. Use --disable-x86asm for a crippled build=> 因為沒有裝匯編編譯器
下載匯編編譯器:http://www.tortall.net/projects/yasm/releases/yasm-1.3.0.tar.gz,解壓tar.gz包后執行configure make && make install,然后重新配置編譯ffmpeg即可.
ffmpeg安裝完畢,查看版本會報錯↓
在這里插入圖片描述
下述第一個方法于我的服務器無效。
方法一,vim /etc/ld.so.conf.d/ffmpeg.conf # 添加內容:/usr/loacl/ffmpeg/lib 保存退出 # 執行命令:ldconfig
換一種方法
方法二,
ldd bin/ffmpeg # 查看依賴庫 export LD_LIBRARY_PATH=/usr/local/ffmpeg/lib/ # 變量 # 查看少不少包↓ ldd ffmpeg
OK,不缺少包.
在這里插入圖片描述
牛刀小試↓
./bin/ffmpeg -version # 查看版本 # 添加軟連接 ln -s /usr/local/ffmpeg/bin/ffmpeg /usr/bin/ffmpeg # 配置環境變量 vim /etc/profile # 進入后添加ffmpeg的path路徑 # 嘗試,將rtmp視頻流保存成文件 ffmpeg -i rtmp://58.200.131.2:1935/livetv/hunantv -acodec copy -vcodec copy -f flv -y testHunantv.flv # 執行后,該測試視頻流即可存儲到當前目錄.
網絡rtmp流保存執行中↓
在這里插入圖片描述
看到了保存的文件,說明ffmpeg命令沒問題.
在這里插入圖片描述
發送文件到Windows播放試一下:sz testHunantv.flv <=發送命令
播放成功!
推流命令
# 靜態源 ffmpeg -re -i avatar1.rmvb -f flv rtmp://192.168.4.152:1935/rtmp/avatar1 # 推送靜態視頻流-1 # ————————————————————————分界線———————————————————————— ffmpeg -i "rtmp://58.200.131.2:1935/livetv/hunantv live=1" -acodec copy -vcodec copy -f flv rtmp://內網ip:1935/rtmp/hunantv # 推送實時流1 OK # ————————————————————————分界線———————————————————————— ffmpeg -i "rtmp://202.69.69.180:443/webcast/bshdlive-pc live=1" -acodec copy -vcodec copy -f flv rtmp://內網ip:1935/rtmp/hktv # 推送實時流2 OK # ————————————————————————分界線———————————————————————— ffmpeg -i "rtmp://live.chosun.gscdn.com/live/tvchosun1.stream live=1" -acodec copy -vcodec copy -f flv rtmp://內網ip:1935/rtmp/cstv # 推送實時流3 OK # ffmpeg亦可推送攝像頭流和內網平臺提供的視頻流
外網播放
將內網的ip:1935端口映射到外網某個端口,然后使用外網ip:外網端口/rtmp/文件路徑/密鑰訪問即可。
帶寬
nethogs命令監控帶寬
yum install gcc-c++ libpcap-devel.x86_64 libpcap.x86_64 ncurses* # 安裝預備組件 wget https://github.com/raboof/nethogs/archive/v0.8.3.tar.gz # 下載安裝包 tar -zxvf v0.8.3.tar.gz cd nethogs-0.8.3 make && make install nethogs # 執行該命令看流量
npm install --save vue-video-player@版本號 npm install --save videojs-flash@版本號
"videojs-flash": "^2.1.0-3", "vue-video-player": "^4.0.6",
// flash player - start import VideoPlayer from 'vue-video-player' require('video.js/dist/video-js.css') require('vue-video-player/src/custom-theme.css') require('videojs-flash') Vue.use(VideoPlayer) // flash player - end
```
在線直播
內容是《Web前端開發之Javascript視頻》的課件,請配合大師哥《Javascript》視頻課程學習。
文檔坐標和視口坐標:
元素的位置是以像素來度量的,向右代表x坐標的增加,向下代表y坐標的增加;但是,有兩個不同的點作為坐標系的原點:元素的x和y坐標可以相對于文檔的左上角或者相對于視口的左上角,也就是對于一個元素來說,會有兩種坐標:文檔坐標和視口坐標;視口坐標也被稱為窗口坐標;
在頂級窗口或標簽頁中,視口只是實際顯示文檔內容的瀏覽器的一部分,它不包括瀏覽器其他組件,如菜單、工具條等;
對于在框架中顯示的文檔,視口是定義了框架頁的<iframe>元素;
元素大小:
以下的屬性和方法不屬于DOM2級樣式規范,但卻與HTML元素的樣式息息相關;IE率先引用了一些屬性,目前,所有主流的瀏覽器都支持以下屬性;
1. 偏移量(offset dimension):
包括元素在屏幕上占用的所有可見的空間;元素的可見大小包括寬高,所有內邊距,滾動條和邊框的大小(不包括外邊距);
通過以下4個屬性可以取得元素的偏移量:
偏移量(offset dimension)
var mydiv=document.getElementById("mydiv");
console.log(mydiv.offsetWidth);
console.log(mydiv.offsetHeight);
console.log(mydiv.offsetLeft);
console.log(mydiv.offsetTop);
offsetLeft和offsetTop返回值與包含元素有關,對于有些元素,這些值是文檔坐標,但對于已定位元素的后代元素和一些其他元素(如表格單元格),這些屬性返回的坐標是相對于祖先元素而不是文檔;
offsetParent屬性:包含元素的引用,也就是相對的父元素;offsetParent不一定與parentNode的值相等;如:<td>元素的offsetParent是作為其祖先元素的<table>元素,因為<table>是在DOM層次中距<td>最近的一個具有大小的元素;
如果其offsetParent屬性為null,以上的屬性都是文檔坐標;
console.log(mydiv.offsetParent);
如果要想知道某個元素在頁面上的偏移量,將這個元素的offsetLeft和offsetTop與其offsetParent的相同屬性相加,如此循環直至根元素,就可以得到一個基本準確的值;
<style>
*{margin: 0; padding: 0;}
ul,li{list-style: none;}
#container{width: 500px; height:100px; position: absolute;left:50px;top:100px;border:10px solid;background-color: blue;}
#content{width: 400px; height:50px; position: relative; padding-left: 50px;background-color: red; }
#myul{width: 300px; position: relative; background-color: purple;}
#myli{width: 200px; margin-left:20px; background-color: pink;}
</style>
<body>
<div id="container">
<div id="content">
<ul id="myul">
<li id="myli">零點程序員</li>
</ul>
</div>
</div>
<script>
var myli=document.getElementById("myli");
console.log(myli.offsetWidth);
console.log(myli.offsetLeft);
console.log(myli.offsetParent);
var myul=document.getElementById("myul");
console.log(myul.offsetWidth);
console.log(myul.offsetLeft);
console.log(myul.offsetParent);
var content=document.getElementById("content");
console.log(content.offsetWidth);
console.log(content.offsetLeft);
console.log(content.offsetParent);
var container=document.getElementById("container");
console.log(container.offsetWidth);
console.log(container.offsetLeft);
console.log(container.offsetParent);
// 定義一個函數
function getElementPosition(element){
var x=0, y=0;
while(element !=null){
x +=element.offsetLeft;
y +=element.offsetTop;
element=element.offsetParent;
}
return {x:x, y:y};
}
var content=document.getElementById("content");
console.log(getElementPosition(content).x);
console.log(getElementPosition(content).y);
var myli=document.getElementById("myli");
console.log(getElementPosition(myli).x);
console.log(getElementPosition(myli).y);
</script>
但實際上這個函數返回的值是不正確的,因為沒有包括邊框的寬度;
既然offsetWidth和offsetHeight是包括border的寬度的,所以有些地方也把它稱為物理寬度,它的值就是該元素實際的尺寸,因此,這并不一定等于元素的width和height,只有元素的CSS設置了box-sizing:border-box時才相等;
對于一個元素來說,它的實際的寬高也并不一定等于它的內容的寬和高,也就是在擁有滾動條的情況下,如:
<style>
#mydiv{width: 300px; height: 100px;border: 1px solid; overflow: scroll;}
</style>
<div id="mydiv">Lorem more...</div>
<script>
var mydiv=document.getElementById("mydiv");
console.log(mydiv.offsetWidth); // 302
console.log(mydiv.offsetHeight); // 102
console.log(mydiv.scrollWidth); // 283
console.log(mydiv.scrollHeight); // 483
</script>
另外,這兩個屬性值也不包含元素的:before或:after等偽類的寬和高;
<style>
#mydiv{width: 300px; height: 100px;border: 1px solid;}
#mydiv::after{content: "web前端開發"; display: block; margin-top:100px;}
</style>
<div id="mydiv"></div>
<script>
var mydiv=document.getElementById("mydiv");
console.log(mydiv.offsetWidth); // 302
console.log(mydiv.offsetHeight); // 102
</script>
如果該元素的display:none,各屬性返回0,offsetParent返回null;
如果該元素的position為static或fixed,其offsetParent為null,此時返回的各個屬性值就是文檔坐標;
對于行盒元素(如 span),offsetTop和offsetLeft描述的是第一個邊界框的位置,offsetWidth和 offsetHeight描述的是邊界框的尺寸;因此,使用offsetLeft、offsetTop、offsetWidth、offsetHeight來對應left、top、width和height 的一個盒子將不會是文本容器 span 的盒子邊界;如:
<div style="width: 300px; border:1px solid blue;">
<span style="background-color: purple;">span element</span>
<span id="long">Lorem ... text more...</span>
</div>
<div id="box" style="position: absolute; border: 1px solid red; z-index: 10"></div>
<script>
var box=document.getElementById("box");
var long=document.getElementById("long");
box.style.left=long.offsetLeft + "px";
box.style.top=long.offsetTop + "px";
box.style.width=long.offsetWidth + "px";
box.style.height=long.offsetHeight + "px";
</script>
所有這些偏移量屬性都是只讀的,且每次訪問它們都需要重新計算;因此,應該盡量避免重復訪問這些屬性;如果要使用,可以將它們保存在局部變量中,以提高性能;
myul.offsetLeft=20;
console.log(myul.offsetLeft); // 50 靜默失敗
2.客戶區大小(client dimension):
指的是元素內容及其內邊距所占據的空間大小,相關屬性為:clientTop、clientLeft、clientWidth和clientHeight;
客戶區大小(client dimension)
客戶區大小就是元素內部的空間大小,其與offsetWidth和offsetHeight類似,只不過不包含邊框大小;也不包括滾動條占用的空間;
clientWidth=CSS width + CSS padding - 水平滾動條寬度 – CSS border(如果width不包括border的話);
clientHeight=CSS height + CSS padding - 水平滾動條高度 – CSS border(如果height不包括border的話);
var mydiv=document.getElementById("mydiv");
console.log(mydiv.clientWidth);
console.log(mydiv.clientHeight);
注意,它不是元素內容實際的尺寸,而是元素內部可見區域的大小;并且是不包括滾動條的,如:
<style>
#mydiv{width: 300px; height: 100px;padding: ;border: 1px solid; overflow: scroll;}
</style>
<div id="mydiv">Lorem more...</div>
<script>
var mydiv=document.getElementById("mydiv");
console.log(mydiv.offsetWidth); // 302
console.log(mydiv.offsetHeight); // 102
console.log(mydiv.clientWidth); // 300
console.log(mydiv.clientHeight); // 100
console.log(mydiv.scrollWidth); // 300
console.log(mydiv.scrollHeight); // 147
注意:對于行盒,如<i>、<code>或<span>等,包括塊盒被設置了display:block后,clientWidth和clientHeight總是返回0;
當元素的display:none時,返回的也是0;
var span=document.getElementsByTagName("span")[0];
console.log(span.clientWidth); // 0
console.log(span.clientHeight); // 0
最典型的應用就是之前講到的獲得瀏覽器視口大小,如:
function getViewPort(){
if(document.compatMode=="BackCompat"){
return{
width:document.body.clientWidth,
height:document.body.clientHeight
};
}else{
return {
width:document.documentElement.clientWidth,
height:document.documentElement.clientHeight
};
}
}
有一個特例:在文檔的根元素上使用這些屬性時,它們的返回值和窗口的innerWidth和innerHeight屬性值相等;但如果有滾動條的話,innerWidth和innerHeight包括滾動條占用空間的值;
console.log(document.documentElement.clientWidth, document.documentElement.clientHeight);
console.log(window.innerWidth, window.innerHeight);
獲得瀏覽器視口大小:
function getViewportSize(w){
// 使用指定的窗口,如果不帶參數則使用當前窗口
w=w || window;
if(w.innerWidth !=null) return {w: w.innerWidth, h: w.innerHeight};
var d=w.document;
if(document.compatMode=="CSS1Compat")
return {w: d.documentElement.clientWidth, h: d.documentElement.clientHeight};
// 怪異模式
return {w: d.body.clientWidth, h: d.body.clientHeight};
}
console.log(getViewportSize().h);
與偏移量相似,這兩個屬性該也為只讀,并且每次訪問都要重新計算;
clientTop屬性和clientLeft屬性:可以返回元素的上邊框和左邊框的大小,其值為一個整數,沒有單位,并且是只讀的;
var mydiv=document.getElementById("mydiv");
console.log(mydiv.clientTop); // 1
console.log(mydiv.clientLeft); // 1
其和getComputedStyle ()方法的borderTopWidth屬性存在一定的區別;
var mydiv=document.getElementById("mydiv");
console.log(mydiv.clientTop); // 12
console.log(getComputedStyle(mydiv,null).borderTopWidth); // 11.5px
可見,clientTop和clientLeft返回的是整數并且沒有單位,而borderTopWidth返回的是mydiv的上邊框的厚度,是精確的;兩者之間的關系是:
mydiv.clientTop=Math.round(parseFloat(getComputedStyle(mydiv,null).borderTopWidth));
但firefox是向下取整;
如果元素有滾動條,并且將這些滾動條放在左側或頂部,clientLeft和clientTop也就包含了滾動條的寬度;
對于行盒元素來說,clientLeft和clientTop值總是為0;當元素的display:none時,返回的也是0;
3.滾動大小(scroll dimension):
指的是包含滾動內容的元素大小;
有些元素(如<html>),即使沒有執行任何代碼也能自動地添加滾動條;但另外一些元素,則需要通過css的overflow屬性進行設置才能滾動;
滾動相關的屬性:
滾動大小(scroll dimension)
scrollWidth和scrollHeight主要用于確定元素內容的實際大小,它們是只讀的,不包括邊框和滾動條;
也就是說,返回的是元素的內容區域加上它的內邊距再加上任何溢出內容的尺寸;
<style>
#mydiv{width: 300px;height:100px; border: 1px solid; overflow: scroll;}
</style>
<div id="mydiv">Lorem more...</div>
<script>
var mydiv=document.getElementById("mydiv");
// 283=300 -17(滾動條的寬), 168=內容實際的高
console.log(mydiv.scrollWidth, mydiv.scrollHeight);
</script>
當內容正好和內容區域匹配而沒有溢出時,這些屬性與clientWidth與clientHeight是相等的;但當溢出時,它們就包含溢出的內容,返回值比clientWidth和clientHeight要大;
// 改變文字數量,觀察兩者的區別
console.log(mydiv.scrollWidth, mydiv.scrollHeight);
console.log(mydiv.clientWidth, mydiv.clientHeight);
通常認為<html>(混雜模式下為<body>)元素是在瀏覽器視口滾動的元素,因此帶有垂直滾動條的頁面總高度就是document.documentElement.scrollHeight;
對于不包含滾動條的頁面,scrollWidth、scrollHeight和clientWidth、clientHeight之間的關系基本是相等的;
console.log(document.documentElement.scrollWidth, document.documentElement.scrollHeight);
console.log(document.documentElement.clientWidth, document.documentElement.clientHeight);
scrollWidth和scrollHeight等于文檔內容區域的大小,而clientWidth和clientHeight等于視口大小;但低版本的瀏覽器有可能結果并不一致;
在確定文檔的總高度時,必須取得scrollWidth/clientWidth和scrollheight/clientHeight中的最大值,才能保證在跨瀏覽器的環境下得到精確的結果;
var docWidth=Math.max(document.documentElement.scrollWidth, document.documentElement.clientWidth);
var docHeight=Math.max(document.documentElement.scrollHeight, document.documentElement.clientHeight);
注:對于混雜混式下,需要使用document.body
scrollLeft和scrollTop屬性:
通過這兩個屬性可以取得元素當前滾動的狀態,也就是滾動位置;它們是可寫的,即能設置元素的滾動位置;
scrollLeft 屬性可以獲取或設置一個元素的內容水平滾動的像素數;scrollTop 屬性可以獲取或設置一個元素的內容垂直滾動的像素數;
在尚未滾動時,兩值均為0;如果垂直滾動,該值大于0,且表示元素上方不可見內容的像素高度,如果水平滾動,該值大于0,且表示元素左方不可見內容的像素寬;
console.log(document.documentElement.scrollLeft);
console.log(document.documentElement.scrollTop);
示例:滾動一個元素;
<style>
#mydiv{width: 300px; height:100px; border: 2px solid; overflow: scroll hidden; white-space: nowrap;}
</style>
<div id="mydiv">Lorem, more...</div>
<button id="btn">滾</button>
<script>
var mydiv=document.getElementById("mydiv");
var btn=document.getElementById("btn");
btn.onclick=function(e){
mydiv.scrollLeft +=20;
}
</script>
將元素的這兩個屬性設置為0,就可以重置元素的滾動位置;
function scrollToTop(element){
if(element.scrollTop !=0){
element.scrollTop=0;
}
}
// 應用
var btn=document.getElementById("btn");
btn.onclick=function(){
scrollToTop(document.documentElement);
}
判定元素是否滾動到底:如果元素滾動到底,下面等式返回true,否則返回false,如:element.scrollHeight - element.scrollTop===element.clientHeight;
var mydiv=document.getElementById("mydiv");
var timer=setInterval(scrollDiv, 100);
function scrollDiv(){
if(mydiv.scrollHeight - mydiv.scrollTop===mydiv.clientHeight)
clearInterval(timer);
else
mydiv.scrollTop +=5;
console.log("scroll");
}
檢查容器能否滾動:
// 加個判斷,條件也可以是:
// window.getComputedStyle(mydiv).overflow==='hidden'
var timer;
if(window.getComputedStyle(mydiv).overflowY==='scroll'){
timer=setInterval(scrollDiv, 100);
}
scrollLeft和scrollTop可以被設置為任何整數值,但有以下特點:
var mydiv=document.getElementById("mydiv");
mydiv.scrollTop=500; // 或設為負數
console.log(mydiv.scrollTop); // 0
這兩個屬性值有可能是小數(比如縮放了頁面的顯示大小),所以在取值時最好取整,例如:Math.ceil()或Math.floor;
function getElementPos(element){
var y=0, x=0;
var current=element;
for(var e=element; e !=null; e=e.offsetParent){
// 但是自己的邊框不能加進去
if(current==e){
x +=e.offsetLeft;
y +=e.offsetTop;
}else{
x +=e.offsetLeft + e.clientLeft;
y +=e.offsetTop + e.clientTop;
}
}
// 再次循環所有的祖先元素,減去滾動的偏移量,并轉換為視口坐標
for(var e=element.parentNode; e !=null && e.nodeType==1; e=e.parentNode){
y -=e.scrollTop;
x -=e.scrollLeft;
}
return {x:x, y:y};
}
var mydiv=document.getElementById("mydiv");
console.log(getElementPos(mydiv).x);
console.log(getElementPos(mydiv).y);
console.log(mydiv.offsetParent);
console.log(mydiv.getBoundingClientRect());
示例:返回到頂部:
var gotop=document.getElementById("gotop");
var timer;
gotop.onclick=function(){
timer=setInterval(goTop, 1);
}
function goTop(){
if(document.documentElement.scrollTop==0)
clearInterval(timer);
else{
// document.documentElement.scrollTop-=10;
document.documentElement.scrollTop-=document.documentElement.scrollTop / 100;
}
}
// 或者使用遞歸
gotop.onclick=goTop;
function goTop(){
console.log("a:" + document.documentElement.scrollTop);
if(document.documentElement.scrollTop==0)
return;
document.documentElement.scrollTop -=10;
setTimeout(goTop,1);
}
示例:判定用戶是否閱讀過文本,如:
<style>
.registration{
width: 600px; height: 200px; padding: 10px;
border: 2px solid purple; border-radius: 5px;
overflow-y: scroll;
}
</style>
<h1>同意協議</h1>
<div class="registration">
<p>Lorem more...</p>
</div>
<p>
<input type="checkbox" name="accept" id="agree" />
<label for="agree">我同意</label>
<input type="submit" id="nextstep" value="下一步" />
</p>
<script>
window.onload=function(){
var registration=document.querySelector(".registration");
var agree=document.getElementById("agree");
agree.disabled=true;
var nextstep=document.getElementById("nextstep");
nextstep.disabled=true;
var readed=false;
var noticeBox=document.createElement("h2");
noticeBox.id="notice";
noticeBox.innerText="請閱讀以下內容"
registration.parentNode.insertBefore(noticeBox, registration);
registration.onscroll=function(e){
if(readed) return;
readed=this.scrollHeight - this.scrollTop===this.clientHeight;
agree.disabled=nextstep.disabled=!readed;
noticeBox.innerText=readed ? "歡迎參加" : "請繼續閱讀";
}
}
</script>
示例:滾動文本,如:
<style>
*{margin: 0; padding: 0;}
#scrollBox{padding:10px;margin:100px auto;width: 300px; height: 150px; background: lightgray;overflow: hidden;}
</style>
<div id="scrollBox">
<ul id="con1">
<li>HTML</li>
<li>CSS</li>
<li>Javascript</li>
<li>更多的li</li>
<li>更多的li</li>
<li>更多的li</li>
<li>更多的li</li>
<li>更多的li</li>
</ul>
<ul id="con2"></ul>
</div>
<script>
var scrollBox=document.getElementById("scrollBox");
var con1=document.getElementById("con1");
var con2=document.getElementById("con2");
con2.innerHTML=con1.innerHTML;
function scrollUp(){
if(scrollBox.scrollTop >=con1.offsetHeight)
scrollBox.scrollTop=0;
else
scrollBox.scrollTop++;
}
var timer=setInterval(scrollUp, 50);
scrollBox.onmouseover=function(){
clearInterval(timer);
};
scrollBox.onmouseout=function(){
timer=setInterval(scrollUp, 50);
}
</script>
圖示匯總各個屬性:
元素坐標、幾何尺寸
windows對象的pageXOffset、pageYOffset和scrollX、scrollY:
pageXOffset 和 pageYOffset 屬性返回文檔在窗口左上角水平和垂直方向滾動的像素;這一對屬性等于scrollX和scrollY屬性,前者是后者的別稱;但IE不支持后者;這些屬性是只讀的;
window.scrollBy(100,200);
console.log(window.pageXOffset);
console.log(window.pageYOffset);
console.log(window.scrollX);
console.log(window.scrollY);
console.log(window.pageXOffset==window.scrollX); // true
與scrollLeft和scrollTop關系:返回值是一樣的;
window.scroll(100,300);
console.log(window.pageXOffset);
console.log(window.pageYOffset);
console.log(document.documentElement.scrollLeft);
console.log(document.documentElement.scrollTop);
為了跨瀏覽器兼容性,一般使用window.pageXOffset代替window.scrollX;另外,舊版本的 IE(<9)兩個屬性都不支持,必須通過其他的非標準屬性來解決此問題;
window.scrollBy(100,200);
var x=(window.pageXOffset !==undefined) ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body).scrollLeft;
var y=(window.pageYOffset !==undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
console.log(x,y);
// 或者
var isCSS1Compat=document.compatMode==="CSS1Compat";
var x=window.pageXOffset ? window.pageXOffset : isCSS1Compat ? document.documentElement.scrollLeft : document.body.scrollLeft;
var y=window.pageXOffset ? window.pageYOffset : isCSS1Compat ? document.documentElement.scrollTop : document.body.scrollTop;
console.log(x,y);
封裝一個函數:
// 以一個對象的x和y屬性的方式返回滾動條的偏移量
function getScrollOffset(w){
// 使用指定的窗口,如果不帶參數則使用當前窗口
w=w || window;
if(w.pageXOffset !=null) return {x: w.pageXOffset, y: w.pageYOffset};
var d=w.document;
if(document.compatMode=="CSS1Compat")
return {x: d.documentElement.scrollLeft, y: d.documentElement.scrollTop};
// 針對怪異模式
return {x: d.body.scrollLeft, y: d.body.scrollTop};
}
console.log(getScrollOffset().x);
console.log(getScrollOffset().y);
4. 確定元素大小:
瀏覽器為每個元素都提供了一個Element.getBoundingClientRect()([?ba?nd??] 邊界)方法;該方法是在IE5引入的;該方法不需要參數,返回一個矩形對象,類型為DOMRect,包含6個屬性:x、y、left、top、right、bottom、width和height;這些屬性給出了元素在頁面中相對于視口的位置和寬高;其中,x和left相同,y與top相同;right和bottom屬性表示元素的右下角的x和y坐標;
<div id="mydiv" style="width: 200px;height:100px;background-color:red; ">id is mydiv</div>
<div style="width: 2000px; background-color: purple;">div</div>
<script>
var mydiv=document.getElementById("mydiv");
var rect=mydiv.getBoundingClientRect();
console.log(rect);
rect=document.documentElement.getBoundingClientRect();
console.log(rect);
</script>
IE并沒有實現x和y;
DOMRect 中的 top, left, right, bottom 屬性是使用對象的其他屬性的值來計算獲得的;
在IE及老款的Edge中,該方法返回的并不是DOMRect類型,而是ClientRect類型;
console.log(ClientRect);
console.log(ClientRect.prototype); // [object ClientRectPrototype]
getBoundingClientRect()返回的數據是包括元素的border及padding;
<div id="mydiv" style="width: 200px;height:100px;background-color:red; border:20px solid black;">name is mydiv</div>
// …
var rect=mydiv.getBoundingClientRect();
console.log(rect);
這是標準盒子,如果是怪異盒子,比如在CSS中設置box-sizing:border-box,那么返回的數據中的寬和高就等于元素的width和height;
如果要轉化為文檔坐標,需要加上滾動的偏移量;
window.scrollBy(50,100);
var mydiv=document.getElementById("mydiv");
var rect=mydiv.getBoundingClientRect();
var x=rect.left + document.documentElement.scrollLeft;
var y=rect.top + document.documentElement.scrollTop;
// 或者使用上面定義的getScrollOffset()函數
var offsets=getScrollOffset();
var x=rect.left + offsets.x;
var y=rect.top + offsets.y;
console.log(x,y);
在布局中,文檔中的元素分為塊盒和行盒,塊盒總是為矩形,但行盒可能跨了多行,因此可能由多個矩形組成,可以把它稱為邊界矩形;
var span=document.getElementById("mydiv").getElementsByTagName("span")[0];
var rect=span.getBoundingClientRect();
console.log(rect);
在IE8及以下瀏覽器中,該方法返回的對象中并不包括width和height屬性;可以使用一個簡便的方式計算元素的width和height屬性:
var mydiv=document.getElementById("mydiv");
var rect=mydiv.getBoundingClientRect();
var w=rect.width || (rect.right - rect.left);
var h=rect.height || (rect.bottom - rect.top);
console.log(w,h);
示例:元素在頁面上的相對文檔的偏移量,如:
function getOffset(ele){
if (!ele || ele.nodeType !=1)
return;
var rect=ele.getBoundingClientRect(),
doc=ele.ownerDocument.documentElement;
return {
top: rect.top + window.pageYOffset - doc.clientTop,
left: rect.left + window.pageXOffset - doc.clientLeft
};
}
getClientRects()方法:
該方法返回一個指向客戶端中每一個盒子的邊界矩形的矩形集合;該矩形集合是一個只讀的類數組對象DOMRectList,可以稱為矩形列表對象,它的每個元素都是DOMRect對象;
var span=document.getElementsByTagName("span")[0];
var rects=span.getClientRects();
console.log(rects);
for(var i=0,len=rects.length; i<len; i++){
console.log(rects[i]);
}
當然,該方法也可以應用在塊盒中,此時它只返回包含一個元素的集合對象;
var mydiv=document.getElementById("mydiv");
console.log(mydiv.getClientRects()[0]);
在IE中返回的是ClientRectList類型,其中保存的是ClientRect類型的對象;
對于HTML area元素、自身不做任何渲染的SVG元素、display:none元素和不直接渲染出來的任何元素,都將會返回一個空列表;
小示例:
<style>
div{display: inline-block; width: 150px;}
div p,ol,table{border: 1px solid blue;}
span, li, th, td{border: 1px solid green;}
</style>
<div>
<strong>原始</strong>
<p><span>Web前端開發課程,包括HTML、CSS、Javascript等內容</span></p>
</div>
<div>
<strong>p的rect</strong>
<p class="rect"><span>Web前端開發課程,包括HTML、CSS、Javascript等內容</span></p>
</div>
<div>
<strong>span的rect</strong>
<p class="rect"><span>Web前端開發課程,包括HTML、CSS、Javascript等內容</span></p>
</div>
<hr />
<div>
<strong>原始</strong>
<ol>
<li>HTML</li>
<li>CSS</li>
</ol>
</div>
<div>
<strong>ol的rect</strong>
<ol class="rect">
<li>HTML</li>
<li>CSS</li>
</ol>
</div>
<div>
<strong>li的rect</strong>
<ol>
<li class="rect">HTML</li>
<li class="rect">CSS</li>
</ol>
</div>
<hr/>
<div>
<table>
<caption>原始</caption>
<thead><tr><th>thead</th></tr></thead>
<tbody><tr><td>tbody</td></tr></tbody>
</table>
</div>
<div>
<table class="rect">
<caption>table的rect</caption>
<thead><tr><th>thead</th></tr></thead>
<tbody><tr><td>tbody</td></tr></tbody>
</table>
</div>
<div>
<table>
<caption>td的rect</caption>
<thead><tr><th class="rect">thead</th></tr></thead>
<tbody><tr><td class="rect">tbody</td></tr></tbody>
</table>
</div>
<script>
function addClientRect(elt){
// 為了使邊框寬度與矩形寬度一致,這里給每個客戶矩形上方絕對定位一個 div。
// 注意:如果用戶改變大小或者縮放,繪圖將會重繪。
var rects=elt.getClientRects();
for(var i=0, len=rects.length; i<len; i++){
var rect=rects[i];
// console.log(rect);
var overlayDiv=document.createElement("div");
overlayDiv.className="overlay";
overlayDiv.style.position="absolute";
overlayDiv.style.border="1px solid red";
var scrollTop=document.documentElement.scrollTop || document.body.scrollTop;
var scrollLeft=document.documentElement.scrollLeft || document.body.scrollLeft;
overlayDiv.style.margin=overlayDiv.style.padding="0";
overlayDiv.style.top=(rect.top + scrollTop) + "px";
overlayDiv.style.left=(rect.left + scrollLeft) + "px";
// 希望rect.width作為邊框寬度,所以內容寬度減少2px
overlayDiv.style.width=(rect.width - 2) + "px";
overlayDiv.style.height=(rect.height - 2) + "px";
document.body.appendChild(overlayDiv);
}
}
var elts=document.querySelectorAll(".rect");
for(var i=0,len=elts.length; i<len; i++)
addClientRect(elts[i]);
</script>
對于NodeList等對象,它們是實時的,但getBoundingClientRect()和getClientRects()所返回的矩形對象或矩形列表對象并不是實時的,它們只是調用方法時文檔視覺狀態的靜態快照,在用戶滾動或改變瀏覽器窗口大小時不會更新它們;
document.elementFromPoint()方法:如果想在指定位置上判定有什么元素,可以使用該方法;參數需要傳遞x和y坐標,不需要單位px,該坐標是視口坐標,該方法返回在指定位置的一個元素;
如果在指定位置有多個元素,它返回的是里面和最上面的(即CSS的z-index屬性),如果指定的點在視口以外,該方法返回null;
典型的案例是將鼠標指針的坐標傳遞給它來判斷鼠標在哪個元素上,但是,在鼠標事件中的target屬性也包含了這些坐標信息,所以,elementFromPoint()方法并不常用;
var div=document.createElement("div");
div.id="divone";
div.setAttribute("style","width:200px;height:100px;position:absolute;left:50px;top:200px;border:solid 5px;");
document.documentElement.appendChild(div);
var innerDiv=document.createElement("div");
innerDiv.setAttribute("style","background-color:purple; width:100px; height:50px;");
div.appendChild(innerDiv);
var elt=document.elementFromPoint(100,200);
console.log(elt);
console.log(div.getBoundingClientRect());
// 鼠標事件中的坐標
var mydiv=document.getElementById("mydiv");
document.addEventListener("click",function(e){
console.log(e.target);
console.log(e.clientX, e.clientY);
},false);
document.elementsFromPoint()方法:該方法返還在特定坐標點下的HTML元素數組;IE與老版的Edge并不支持;
var elts=document.elementsFromPoint(100,250);
console.log(elts);
滾動:
Window.scroll()、Window.scrollBy()、Window.scrollTo()及Element.scroll()、Element.scrollBy()、Element.scrollTo();
scroll(x, y)或scrollTo(x, y)方法:
var btn=document.querySelector(".btn");
btn.onclick=function(){
var documentHeight=document.documentElement.offsetHeight;
var viewportHeight=window.innerHeight;
// 滾動到最后一屏
window.scrollTo(0, documentHeight - viewportHeight);
}
scrollBy(x, y)方法:
其與以上兩個方法類似,但是它的參數是相對的,并在當前滾動的偏移量上增加
window.scrollBy(5,5);
示例,閱讀文章時自動滾屏,如:
<style>
.autoscrollbtn{width: 50px; height: 50px; background-color: purple;
position: fixed; top:100px; right: 100px; color:#FFF}
</style>
<div>lorem</div>
<div class="autoscrollbtn">滾</div>
<script>
// 3874 4531
var btn=document.querySelector(".autoscrollbtn");
var timer;
var viewportHeight=window.innerHeight;
var stop=false;
btn.addEventListener("click",function(e){
if(!stop){
e.target.innerText="停";
timer=setInterval(function(){
if((viewportHeight + document.documentElement.scrollTop) >= document.documentElement.offsetHeight)
clearInterval(timer);
scrollBy(0,2);
},200);
}else{
e.target.innerText="滾";
clearInterval(timer);
}
stop=!stop;
});
</script>
以上方法,參數除了x和y坐標外,還可以是一個ScrollToOptions對象;
CSSOM View 規范的ScrollToOptions對象,用于指定一個元素應該滾動到哪里,以及滾動是否應該平滑;與我們之前講的scrollIntoView()方法的參數類似,但類型不一樣,其為ScrollIntoViewOptions,屬性為block及inline等;而ScrollToOptions對象擁有的是top、left和behavior屬性,其中behavior屬性值可能為:auto及smooth;該參數IE和Edge不支持;
如果不使用ScrollToOptions對象參數,也可以使用CSS指定,如:
html,body{
scroll-behavior:smooth;
}
但IE和Edge依然不支持;
var btn=document.getElementById("btn");
btn.addEventListener("click",function(e){
window.scroll(mydiv.offsetLeft,mydiv.offsetTop);
// 或,但IE與Edge不支持
window.scroll({left:mydiv.offsetLeft, top:mydiv.offsetTop, behavior:"smooth"});
},false);
Element.scroll()、Element.scrollBy()、Element.scrollTo();
這些方法是用于在給定的元素中滾動到某個特定坐標,其用法與window上的三個方法一致,但IE與Edge均不支持Element的方法;
<div id="mydiv" style="background-color: purple; width: 300px; height: 400px; overflow-y: scroll;">Lorem</div>
<script>
var mydiv=document.getElementById("mydiv");
mydiv.scroll(0,300);
</script>
Web前端開發之Javascript
—本文是對之前同名文章的修正,將所有webpack3的內容更新為webpack4,以及加入了筆者近期在公司工作中學習到的自動化思想,對文章內容作了進一步提升。
作為互聯網項目,最重要的便是用戶體驗。在舉國“互聯網+”的熱潮中,用戶至上也已經被大多數企業所接收,特別是在如今移動端快速發展的時代,我們的網頁不僅只是呈現在用戶的PC瀏覽器里,更多的時候,用戶是通過移動產品瀏覽我們的網頁。加之有越來越多的開發者投入到Web APP和Hybrid APP的開發隊伍中,性能這一問題又再一次被提上了程序員們重點關注的要素。我曾經看到過這樣一句話:一個網站的體驗,決定了用戶是否愿意去了解網站的功能;而網站的功能,決定了用戶是否會一票否決網站的體驗。這是改版自網絡上的一句流行語,但卻把網站性能這件事說的十分透徹,特別是在網站這樣的項目中,如果一個用戶需要超過5s才能看見頁面,他會毫不猶豫地關閉它。性能優化,作為工程師界的“上乘武功”,是我們在開發中老生常談的話題,也是一名開發者從入門向資深進階的必經階段,雖然我們看到過很多的標準、軍規,但在真正實踐中,卻常常力不從心,不知道落下了什么,不知道性能是否還有進一步優化的空間。
對于網站的性能,在行業內有很多既定的指標,但就以前端er而言,我們應該更加關注以下指標:白屏時間、首屏時間、整頁時間、DNS時間、CPU占用率。而我之前自己搭建的一個網站(網址:http://jerryonlyzrj.com/resume/ ,近日因域名備案無法打開,幾日后即恢復正常),完全沒做性能優化時,首屏時間是12.67s,最后經過多方面優化,終于將其降低至1.06s,并且還未配置CDN加速。其中過程我踩了很多坑,也翻了許多專業書籍,最后決定將這幾日的努力整理成文,幫助前端愛好者們少走彎路。文章更新可能之后不會實時同步在論壇上,歡迎大家關注我的Github,我會把最新的文章更新在對應的項目里,讓我們一起在代碼的海洋里策馬奔騰:https://github.com/jerryOnlyZRJ 。
今天,我們將從性能優化的三大方面工作逐步展開介紹,其中包括網絡傳輸性能、頁面渲染性能以及JS阻塞性能,系統性地帶著讀者們體驗性能優化的實踐流程。
在開始介紹網絡傳輸性能優化這項工作之前,我們需要了解瀏覽器處理用戶請求的過程,那么就必須奉上這幅神圖了:
這是navigation timing監測指標圖,從圖中我們可以看出,瀏覽器在得到用戶請求之后,經歷了下面這些階段:重定向→拉取緩存→DNS查詢→建立TCP鏈接→發起請求→接收響應→處理HTML元素→元素加載完成。不著急,我們對其中的細節一步步展開討論:
我們都知道,瀏覽器在向服務器發起請求前,會先查詢本地是否有相同的文件,如果有,就會直接拉取本地緩存,這和我們在后臺部署的Redis和Memcache類似,都是起到了中間緩沖的作用,我們先看看瀏覽器處理緩存的策略:
因為網上的圖片太籠統了,而且我翻過很多講緩存的文章,很少有將狀態碼還有什么時候將緩存存放在內存(memory)中什么時候緩存在硬盤中(disk)系統地整理出來,所以我自己繪制了一張瀏覽器緩存機制流程圖,結合這張圖再更深入地說明瀏覽器的緩存機制。
這里我們可以使用chrome devtools里的network面板查看網絡傳輸的相關信息:
(這里需要特別注意,在我們進行緩存調試時,需要去除network面板頂部的Disable cache 勾選項,否則瀏覽器將始終不會從緩存中拉取數據)
瀏覽器默認的緩存是放在內存內的,但我們知道,內存里的緩存會因為進程的結束或者說瀏覽器的關閉而被清除,而存在硬盤里的緩存才能夠被長期保留下去。很多時候,我們在network面板中各請求的size項里,會看到兩種不同的狀態:from memory cache 和 from disk cache,前者指緩存來自內存,后者指緩存來自硬盤。而控制緩存存放位置的,不是別人,就是我們在服務器上設置的Etag字段。在瀏覽器接收到服務器響應后,會檢測響應頭部(Header),如果有Etag字段,那么瀏覽器就會將本次緩存寫入硬盤中。
之所以拉取緩存會出現200、304兩種不同的狀態碼,取決于瀏覽器是否有向服務器發起驗證請求。 只有向服務器發起驗證請求并確認緩存未被更新,才會返回304狀態碼。
這里我以nginx為例,談談如何配置緩存:
首先,我們先進入nginx的配置文檔
$ vim nginxPath/conf/nginx.conf
在配置文檔內插入如下兩項:
etag on; //開啟etag驗證
expires 7d; //設置緩存過期時間為7天
打開我們的網站,在chrome devtools的network面板中觀察我們的請求資源,如果在響應頭部看見Etag和Expires字段,就說明我們的緩存配置成功了。
【!!!特別注意!!!】在我們配置緩存時一定要切記,瀏覽器在處理用戶請求時,如果命中強緩存,瀏覽器會直接拉取本地緩存,不會與服務器發生任何通信,也就是說,如果我們在服務器端更新了文件,并不會被瀏覽器得知,就無法替換失效的緩存。所以我們在構建階段,需要為我們的靜態資源添加md5 hash后綴,避免資源更新而引起的前后端文件無法同步的問題。
我們之前所做的瀏覽器緩存工作,只有在用戶第二次訪問我們的頁面才能起到效果,如果要在用戶首次打開頁面就實現優良的性能,必須對資源進行優化。我們常將網絡性能優化措施歸結為三大方面:減少請求數、減小請求資源體積、提升網絡傳輸速率。現在,讓我們逐個擊破:
結合前端工程化思想,我們在對上線文件進行自動化打包編譯時,通常都需要打包工具的協助,這里我推薦webpack,我通常都使用Gulp和Grunt來編譯node,Parcel太新,而且webpack也一直在自身的特性上向Parcel靠攏。
在對webpack進行上線配置時,我們要特別注意以下幾點:
①JS壓縮:(這點應該算是耳熟能詳了,就不多介紹了)
optimization: {
minimizer: [
new UglifyJsPlugin({
cache: true,
parallel: true,
sourceMap: true // set to true if you want JS source maps
}),
...Plugins
]
}
②HTML壓縮:
new HtmlWebpackPlugin({
template: __dirname + '/views/index.html', // new 一個這個插件的實例,并傳入相關的參數
filename: '../index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
chunksSortMode: 'dependency'
})
我們在使用html-webpack-plugin 自動化注入JS、CSS打包HTML文件時,很少會為其添加配置項,這里我給出樣例,大家直接復制就行。據悉,在Webpack5中,html-webpack-plugin的功能會像common-chunk-plugin那樣,被集成到webpack內部,這樣我們就不需要再install額外的插件了。
PS:這里有一個技巧,在我們書寫HTML元素的src 或 href 屬性時,可以省略協議部分,這樣也能簡單起到節省資源的目的。(雖然其目的本身是為了統一站內的所有協議)
③提取公共資源:
splitChunks: {
cacheGroups: {
vendor: { // 抽離第三方插件
test: /node_modules/, // 指定是node_modules下的第三方包
chunks: 'initial',
name: 'common/vendor', // 打包后的文件名,任意命名
priority: 10 // 設置優先級,防止和自定義的公共代碼提取時被覆蓋,不進行打包
},
utils: { // 抽離自定義公共代碼
test: /\.js$/,
chunks: 'initial',
name: 'common/utils',
minSize: 0 // 只要超出0字節就生成一個新包
}
}
}
④提取css并壓縮:
在使用webpack的過程中,我們通常會以模塊的形式引入css文件(webpack的思想不就是萬物皆模塊嘛),但是在上線的時候,我們還需要將這些css提取出來,并且壓縮,這些看似復雜的過程只需要簡單的幾行配置就行:
(PS:我們需要用到mini-css-extract-plugin ,所以還得大家自行npm install)
const MiniCssExtractPlugin=require('mini-css-extract-plugin')
module: {
rules: [..., {
test: /\.css$/,
exclude: /node_modules/,
use: [
_mode==='development' ? 'style-loader' : MiniCssExtractPlugin.loader, {
loader: 'css-loader',
options: {
importLoaders: 1
}
}, {
loader: 'postcss-loader',
options: {
ident: 'postcss'
}
}
]
}]
}
我這里配置預處理器postcss,但是我把相關配置提取到了單獨的文件postcss.config.js里了,其中cssnano是一款很不錯的CSS優化插件。
⑤將webpack開發環境修改為生產環境:
在使用webpack打包項目時,它常常會引入一些調試代碼,以作相關調試,我們在上線時不需要這部分內容,通過配置剔除:
devtool: 'false'
如果你能按照上述六點將webpack上線配置完整配置出來,基本能將文件資源體積壓縮到極致了,如有疏漏,還希望大家能加以補充。
最后,我們還應該在服務器上開啟Gzip傳輸壓縮,它能將我們的文本類文件體積壓縮至原先的四分之一,效果立竿見影,還是切換到我們的nginx配置文檔,添加如下兩項配置項目:
gzip on;
gzip_types text/plain application/javascriptapplication/x-javascripttext/css application/xml text/javascriptapplication/x-httpd-php application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml;
【!!!特別注意!!!】不要對圖片文件進行Gzip壓縮!不要對圖片文件進行Gzip壓縮!不要對圖片文件進行Gzip壓縮!我只會告訴你效果適得其反,至于具體原因,還得考慮服務器壓縮過程中的CPU占用還有壓縮率等指標,對圖片進行壓縮不但會占用后臺大量資源,壓縮效果其實并不可觀,可以說是“弊大于利”,所以請在gzip_types 把圖片的相關項去掉。針對圖片的相關處理,我們接下來會更加具體地介紹。
剛剛我們介紹了資源打包壓縮,只是停留在了代碼層面,而在我們實際開發中,真正占用了大量網絡傳輸資源的,并不是這些文件,而是圖片,如果你對圖片進行了優化工作,你能立刻看見明顯的效果。
很多開發者可能會有這樣的錯覺(其實我曾經也是這樣),我們會為了方便在一個200?200的圖片容器內直接使用一張400?400的圖片,我們甚至認為這樣能讓用戶覺得圖片更加清晰,其實不然,在普通的顯示器上,用戶并不會感到縮放后的大圖更加清晰,但這一切卻導致網頁加速速度下降,同時照成帶寬浪費,你可能不知道,一張200KB的圖片和2M的圖片的傳輸時間會是200m和12s的差距(親身經歷,深受其害(┬_┬))。所以,當你需要用多大的圖片時,就在服務器上準備好多大的圖片,盡量固定圖片尺寸。
雪碧圖的概念大家一定在生活中經常聽見,其實雪碧圖是減小請求數的顯著運用。而且很奇妙的是,多張圖片聘在一塊后,總體積會比之前所有圖片的體積之和小(你可以親自試試)。這里給大家推薦一個自動化生成雪碧圖的工具:https://www.toptal.com/developers/css/sprite-generator (圖片來自官網首頁)
只要你添加相關資源文件,他就會自動幫你生成雪碧圖以及對應的CSS樣式。
其實我們在工程中還有更為自動的方法,便是一款雪碧圖生成插件webpack-spritesmith。首先,先簡單介紹一下使用插件生成雪碧圖的思路:
首先,我們會把我們所需要的小圖標放置在一個文件夾內以便于管理:
(這里的@2x圖片是為了適配視網膜二倍屏的圖片資源,webpack-spritesmith內有專門為適配多倍屏提供的配置項,稍候將會講到)
然后,我們需要插件去讀取這個文件夾內的所有圖片資源文件,以文件夾名稱為圖片名稱生成一張雪碧圖到指定位置,并且輸出能夠正確使用這些雪碧圖的CSS文件。
如今,webpack-spritesmith這款插件能實現我們想要的一切,先奉上配置內容: (具體可參照webpack-spritesmith官方文檔: https://www.npmjs.com/package/webpack-spritesmith )
執行webpack之后,就會在開發目錄里生成上面兩張圖的結果,我們可以看看common.css里面的內容:
我們可以看到,所有我們之前放在common文件夾里的圖片資源都自動地生成了相應的樣式,這些都不需要我們手動處理,`webpack-spritesmith這款插件就已經幫我們完成了!
不論是壓縮后的圖片,還是雪碧圖,終歸還是圖片,只要是圖片,就還是會占用大量網絡傳輸資源。但是字體圖標的出現,卻讓前端開發者看到了另外一個神奇的世界。
我最喜歡用的是阿里矢量圖標庫(網址:http://www.iconfont.cn/ ),里面有大量的矢量圖資源,而且你只需要像在淘寶采購一樣把他們添加至購物車就能把它們帶回家,整理完資源后還能自動生成CDN鏈接,可以說是完美的一條龍服務了。(圖片來自官網首頁)
圖片能做的很多事情,矢量圖都能作,而且它只是往HTML里插入字符和CSS樣式而已,和圖片請求比起來資源占用完全不在一個數量級,如果你的項目里有小圖標,就是用矢量圖吧。
但如果我們做的是公司或者團隊的項目,需要使用到許多自定義的字體圖標,可愛的設計小姐姐們只是丟給你了幾份.svg圖片,你又該如何去做呢?
其實也很簡單,阿里矢量圖標庫就提供了上傳本地SVG資源的功能,這里另外推薦一個網站——icomoon。icomoon這個網站也為我們提供了將SVG圖片自動轉化成CSS樣式的功能。(圖片來自icomoon首頁)
我們可以點擊Import Icons按鈕導入我們本地的SVG資源,然后選中他們,接下來生成CSS的事情,就交給icomoon吧,具體的操作,就和阿里矢量圖標庫類同了。
WebP格式,是谷歌公司開發的一種旨在加快圖片加載速度的圖片格式。圖片壓縮體積大約只有JPEG的2/3,并能節省大量的服務器帶寬資源和數據空間。Facebook、Ebay等知名網站已經開始測試并使用WebP格式。
我們可以使用官網提供的Linux命令行工具對項目中的圖片進行WebP編碼,也可以使用我們的線上服務,這里我推薦叉拍云(網址:https://www.upyun.com/webp )。但是在實際的上線工作中,我們還是得編寫Shell腳本用命令行工具進行自動化編譯,測試階段用線上服務方便快捷。(圖片來自叉拍云官網)
除了network版塊,其實chrome還為我們準備好了一款監測網絡傳輸性能的插件——Page Speed,咱們的文章封面,就是用的Page Speed的官方宣傳圖(因為我覺得這張圖再合適不過了)。我們只需要通過下面步驟安裝,就可以在chrome devtools里找到它了:chrome菜單→更多工具→拓展程序→chrome網上應用商店→搜索pagespeed后安轉即可。
(PS:使用chrome應用商店需要翻墻,怎么翻墻我就不便多說了)
這就是Page Speed的功能界面:
我們只需要打開待測試的網頁,然后點擊Page Speed里的 Start analyzing按鈕,它就會自動幫我們測試網絡傳輸性能了,這是我的網站測試結果:
Page Speed最人性化的地方,便是它會對測試網站的性能瓶頸提出完整的建議,我們可以根據它的提示進行優化工作。這里我的網站已經優化到最好指標了(??????)??,Page Speed Score表示你的性能測試得分,100/100表示已經沒有需要優化的地方。
優化完畢后再使用chorme devtools的network版塊測量一下我們網頁的白屏時間還有首屏時間,是不是得到了很大的提升?
Last but not least,
再好的性能優化實例,也必須在CDN的支撐下才能到達極致。
如果我們在Linux下使用命令$ traceroute targetIp 或者在Windows下使用批處理 > tracert targetIp,都可以定位用戶與目標計算機之間經過的所有路由器,不言而喻,用戶和服務器之間距離越遠,經過的路由器越多,延遲也就越高。使用CDN的目的之一便是解決這一問題,當然不僅僅如此,CDN還可以分擔IDC壓力。
當然,憑著我們單個人的資金實力(除非你是王思聰)是必定搭建不起來CDN的,不過我們可以使用各大企業提供的服務,諸如騰訊云等,配置也十分簡單,這里就請大家自行去推敲啦。
其實大家應該對瀏覽器將的HTML渲染機制比較熟悉了,基本流程同上圖所述,大家在入門的時候,你的導師或者前輩可能會告訴你,在渲染方面我們要減少重排和重繪,因為他們會影響瀏覽器性能。不過你一定不知道其中原理是什么,對吧。今天我們就結合《Webkit技術內幕》(這本書我還是很推薦大家買來看看,好歹作為一名前端工程師,你得知道我們天天接觸的瀏覽器內核是怎樣工作的)的相關知識,給大家普及普及那些深層次的概念。
PS:這里提到了Webkit內核,我順帶提一下瀏覽器內部的渲染引擎、解釋器等組件的關系,因為經常有師弟或者一些前端愛好者向我問這方面的知識,分不清他們的關系,我就拿一張圖來說明:(如果你對著不感興趣,可以直接跳過)
瀏覽器的解釋器,是包括在渲染引擎內的,我們常說的Chrome(現在使用的是Blink引擎)和Safari使用的Webkit引擎,Firefox使用的Gecko引擎,指的就是渲染引擎。而在渲染引擎內,還包括著我們的HTML解釋器(渲染時用于構造DOM樹)、CSS解釋器(渲染時用于合成CSS規則)還有我們的JS解釋器。不過后來,由于JS的使用越來越重要,工作越來越繁雜,所以JS解釋器也漸漸獨立出來,成為了單獨的JS引擎,就像眾所周知的V8引擎,我們經常接觸的Node.js也是用的它。
如果我告訴你,一個頁面是有許多許多層級組成的,他們就像千層面那樣,你能想象出這個頁面實際的樣子嗎?這里為了便于大家想象,我附上一張之前Firefox的3D View插件的頁面Layers層級圖:
對,你沒看錯,頁面的真實樣子就是這樣,是由多個DOM元素渲染層(Layers)組成的,實際上一個頁面在構建完render tree之后,是經歷了這樣的流程才最終呈現在我們面前的:
①瀏覽器會先獲取DOM樹并依據樣式將其分割成多個獨立的渲染層
②CPU將每個層繪制進繪圖中
③將位圖作為紋理上傳至GPU(顯卡)繪制
④GPU將所有的渲染層緩存(如果下次上傳的渲染層沒有發生變化,GPU就不需要對其進行重繪)并復合多個渲染層最終形成我們的圖像
從上面的步驟我們可以知道,布局是由CPU處理的,而繪制則是由GPU完成的。
其實在chrome中,也為我們提供了相關插件供我們查看頁面渲染層的分布情況,以及GPU的占用率:(所以說,平時我們得多去嘗試嘗試chrome的那些莫名其妙的插件,真的會發現好多東西都是神器)
chrome開發者工具菜單→more tools→Layers(開啟渲染層功能模塊)
chrome開發者工具菜單→more tools→rendering(開啟渲染性能監測工具)
執行上面的操作后,你會在瀏覽器里看到這樣的效果:
太多東西了,分模塊講吧:
(一)最先是頁面右上方的小黑窗:其實提示已經說的很清楚了,它顯示的就是我們的GPU占用率,能夠讓我們清楚地知道頁面是否發生了大量的重繪。
(二)Layers版塊:這就是用于顯示我們剛提到的DOM渲染層的工具了,左側的列表里將會列出頁面里存在哪些渲染層,還有這些渲染層的詳細信息。
(三)Rendering版塊:這個版塊和我們的控制臺在同一個地方,大家可別找不到它。前三個勾選項是我們最常使用的,讓我來給大家解釋一下他們的功能(充當一次免費翻譯)
①Paint flashing:勾選之后會對頁面中發生重繪的元素高亮顯示
②Layer borders:和我們的Layer版塊功能類似,它會用高亮邊界突出我們頁面中的各個渲染層
③FPS meter:就是開啟我們在(一)中提到的小黑窗,用于觀察我們的GPU占用率
可能大家會問我,和我提到DOM渲染層這么深的概念有什么用啊,好像跟性能優化沒一點關系啊?大家應該還記得我剛說到GPU會對我們的渲染層作緩存對吧,那么大家試想一下,如果我們把那些一直發生大量重排重繪的元素提取出來,單獨觸發一個渲染層,那樣這個元素不就不會“連累”其他元素一塊重繪了對吧。
那么問題來了,什么情況下會觸發渲染層呢?大家只要記住:
video元素、WebGL、Canvas、CSS3 3D、CSS濾鏡、z-index大于某個相鄰節點的元素都會觸發新的Layer,其實我們最常用的方法,就是給某個元素加上下面的樣式:
transform: translateZ(0);
backface-visibility: hidden;
這樣就可以觸發渲染層啦(^__^) 。
我們把容易觸發重排重繪的元素單獨觸發渲染層,讓它與那些“靜態”元素隔離,讓GPU分擔更多的渲染工作,我們通常把這樣的措施成為硬件加速,或者是GPU加速。大家之前肯定聽過這個說法,現在完全清楚它的原理了吧。
現在到我們的重頭戲了,重排和重繪。先拋出概念:
①重排(reflow):渲染層內的元素布局發生修改,都會導致頁面重新排列,比如窗口的尺寸發生變化、刪除或添加DOM元素,修改了影響元素盒子大小的CSS屬性(諸如:width、height、padding)。
②重繪(repaint):繪制,即渲染上色,所有對元素的視覺表現屬性的修改,都會引發重繪。
我們習慣使用chrome devtools中的performance版塊來測量頁面重排重繪所占據的時間:
①藍色部分:HTML解析和網絡通信占用的時間
②黃色部分:JavaScript語句執行所占用時間
③紫色部分:重排占用時間
④綠色部分:重繪占用時間
不論是重排還是重繪,都會阻塞瀏覽器。要提高網頁性能,就要降低重排和重繪的頻率和成本,近可能少地觸發重新渲染。正如我們在2.3中提到的,重排是由CPU處理的,而重繪是由GPU處理的,CPU的處理效率遠不及GPU,并且重排一定會引發重繪,而重繪不一定會引發重排。所以在性能優化工作中,我們更應當著重減少重排的發生。
這里給大家推薦一個網站,里面詳細列出了哪些CSS屬性在不同的渲染引擎中是否會觸發重排或重繪:
https://csstriggers.com/ (圖片來自官網)
談了那么多理論,最實際不過的,就是解決方案,大家一定都等著急了吧,做好準備,一大波干貨來襲:
(一)CSS屬性讀寫分離:瀏覽器沒次對元素樣式進行讀操作時,都必須進行一次重新渲染(重排 + 重繪),所以我們在使用JS對元素樣式進行讀寫操作時,最好將兩者分離開,先讀后寫,避免出現兩者交叉使用的情況。最最最客觀的解決方案,就是不用JS去操作元素樣式,這也是我最推薦的。
(二)通過切換class或者style.csstext屬性去批量操作元素樣式
(三)DOM元素離線更新:當對DOM進行相關操作時,例、appendChild等都可以使用Document Fragment對象進行離線操作,帶元素“組裝”完成后再一次插入頁面,或者使用display:none 對元素隱藏,在元素“消失”后進行相關操作。
(四)將沒用的元素設為不可見:visibility: hidden,這樣可以減小重繪的壓力,必要的時候再將元素顯示。
(五)壓縮DOM的深度,一個渲染層內不要有過深的子元素,少用DOM完成頁面樣式,多使用偽元素或者box-shadow取代。
(六)圖片在渲染前指定大小:因為img元素是內聯元素,所以在加載圖片后會改變寬高,嚴重的情況會導致整個頁面重排,所以最好在渲染前就指定其大小,或者讓其脫離文檔流。
(七)對頁面中可能發生大量重排重繪的元素單獨觸發渲染層,使用GPU分擔CPU壓力。(這項策略需要慎用,得著重考量以犧牲GPU占用率能否換來可期的性能優化,畢竟頁面中存在太多的渲染層對與GPU而言也是一種不必要的壓力,通常情況下,我們會對動畫元素采取硬件加速。)
JavaScript在網站開發中幾乎已經確定了壟斷地位,哪怕是一個再簡單不過的靜態頁面,你都可能看到JS的存在,可以說,沒有JS,就基本沒有用戶交互。然而,腳本帶來的問題就是他會阻塞頁面的平行下載,還會提高進程的CPU占用率。更有甚者,現在node.js已經在前端開發中普及,稍有不慎,我們引發了內存泄漏,或者在代碼中誤寫了死循環,會直接造成我們的服務器奔潰。在如今這個JS已經遍布前后端的時代,性能的瓶頸不單單只是停留在影響用戶體驗上,還會有更多更為嚴重的問題,對JS的性能優化工作不可小覷。
在編程的過程中,如果我們使用了閉包后未將相關資源加以釋放,或者引用了外鏈后未將其置空(比如給某DOM元素綁定了事件回調,后來卻remove了該元素),都會造成內存泄漏的情況發生,進而大量占用用戶的CPU,造成卡頓或死機。我們可以使用chrome提供的JavaScript Profile版塊,開啟方式同Layers等版塊,這里我就不再多說了,直接上效果圖:
我們可以清除看見JS執行時各函數的執行時間以及CPU占用情況,如果我在代碼里增加一行while(true){}, 那么它的占用率一定會飆升到一個異常的指標(親測93.26%)。
其實瀏覽器強大的內存回收機制在大多數時候避免了這一情況的發生,即便用戶發生了死機,他只要結束相關進程(或關閉瀏覽器)就可以解決這一問題,但我們要知道,同樣的情況還會發生在我們的服務器端,也就是我們的node中,嚴重的情況,會直接造成我們的服務器宕機,網站奔潰。所以更多時候,我們都使用JavaScript Profile版塊來進行我們的node服務的壓力測試,搭配node-inspector 插件,我們能更有效地檢測JS執行時各函數的CPU占用率,針對性地進行優化。
(PS:沒修煉到一定水平,千萬別在服務端使用閉包,一個是真沒啥用,我們會有更多優良的解決辦法,二是真的很容易內存泄漏,造成的后果是你無法預期的)
之所以將負載均衡作為拓展內容,是因為如果是你自己搭建的個人網站,或者中小型網站,其實并不需要考慮多大的并發量,但是如果你搭建的是大型網站,負載均衡便是開發過程不可或缺的步驟。
現在的開發流程都注重前后端分離,也就是軟件工程中常提到的“高內聚低耦合”的思想,你也可以用模塊化的思想去理解,前后解耦就相當與把一個項目分成了前端和后端兩個大模塊,中間通過接口聯系起來,分別進行開發。這樣做有什么好處?我就舉最有實際效果的一點:“異步編程”。這是我自己想的名字,因為我覺得前后解耦的形式很像我們JS中的異步隊列,傳統的開發模式是“同步”的,前端需要等后端封裝好接口,知道了能拿什么數據,再去開發,時間短,工程大。而解耦之后,我們只需要提前約定好接口,前后兩端就可以同時開發,不僅高效而且省時。
我們都知道node的核心是事件驅動,通過loop去異步處理用戶請求,相比于傳統的后端服務,它們都是將用戶的每個請求分配異步隊列進行處理,推薦大家去看這樣一篇博文:https://mp.weixin.qq.com/s?__biz=MzAxOTc0NzExNg==&mid=2665513044&idx=1&sn=9b8526e9d641b970ee5ddac02dae3c57&scene=21#wechat_redirect 。特別生動地講解了事件驅動的運行機制,通俗易懂。事件驅動的最大優勢是什么?就是在高并發IO時,不會造成堵塞,對于直播類網站,這點是至關重要的,我們有成功的先例——快手,快手強大的IO高并發究其本質一定能追溯到node。
其實現在的企業級網站,都會搭建一層node作為中間層。大概的網站框架如圖所示:
我們都知道node的優劣,這里分享一份鏈接,找了挺久寫的還算詳細:https://www.zhihu.com/question/19653241/answer/15993549 。其實都是老套路,那些說node不行的都是指著node是單線程這一個軟肋開撕,告訴你,我們有解決方案了——pm2。這是它的官網:http://pm2.keymetrics.io/ 。它是一款node.js進程管理器,具體的功能,就是能在你的計算機里的每一個內核都啟動一個node.js服務,也就是說如果你的電腦或者服務器是多核處理器(現在也少見單核了吧),它就能啟動多個node.js服務,并且它能夠自動控制負載均衡,會自動將用戶的請求分發至壓力小的服務進程上處理。聽起來這東西簡直就是神器啊!而且它的功能遠遠不止這些,這里我就不作過多介紹了,大家知道我們在上線的時候需要用到它就行了,安裝的方法也很簡單,直接用npm下到全局就可以了$ npm i pm2 -g具體的使用方法還有相關特性可以參照官網。這里我在build文件夾內添加了pm2.json文件,這是pm2的啟動配置文件,我們可以自行配置相關參數,具體可參考github源碼,運行時我們只要在上線目錄下輸入命令$ pm2 start pm2.json即可。
下面是pm2啟動后的效果圖:
在開始搭建工作之前,首先得知道什么是反向代理。可能大家對這個名詞比較陌生,先上一張圖:
所謂代理就是我們通常所說的中介,網站的反向代理就是指那臺介于用戶和我們真實服務器之間的服務器(說的我都拗口了),它的作用便是能夠將用戶的請求分配到壓力較小的服務器上,其機制是輪詢。聽完這句話是不是感覺很耳熟,沒錯,在我介紹pm2的時候也說過同樣的話,反向代理起到的作用同pm2一樣也是實現負載均衡,你現在應該也明白了兩者之間的差異,反向代理是對服務器實現負載均衡,而pm2是對進程實現負載均衡。大家如果想深入了解反向代理的相關知識,我推薦知乎的一個貼子:https://www.zhihu.com/question/24723688 。但是大家會想到,配服務器是運維的事情啊,和我們前端有什么關系呢?的確,在這部分,我們的工作只有一些,只需要向運維提供一份配置文檔即可。
http {
upstream video {
ip_hash;
server localhost:3000;
}
server {
listen: 8080;
location / {
proxy_pass: http://video
}
}
}
也就是說,在和運維對接的時候,我們只需要將上面這幾行代碼改為我們配置好的文檔發送給他就行了,其他的事情,運維小哥會明白的,不用多說,都在酒里。
但是,這幾行代碼該怎么去改呢?首先我們得知道,在nginx中,模塊被分為三大類:handler、filter和upstream。而其中的upstream模塊,負責完成完成網絡數據的接收、處理和轉發,也是我們需要在反向代理中用到的模塊。接下來我們將介紹配置代碼里的內容所表示的含義
upstream關鍵字后緊跟的標識符是我們自定義的項目名稱,通過一對花括號在其中增添我們的配置信息。
ip_hash 關鍵字:控制用戶再次訪問時是否連接到前一次連接的服務器
server關鍵字:我們真實服務器的地址,這里的內容肯定是需要我們去填寫的,不然運維怎么知道你把項目放在那個服務器上了,也不知道你封裝了一層node而得去監聽3000端口。
server是nginx的基本配置,我們需要通過server將我們定義的upstream應用到服務器上。
listen關鍵字:服務器監聽的端口
location關鍵字:和我們之前在node層說到的路由是起同樣的功能,這里是把用戶的請求分配到對應的upstream上
網站的性能與監測是一項復雜的工作,還有很多很多后續的工作,我之前所提到的這些,也只能算是冰山一角,在熟悉開發規范的同時,也需要實踐經驗的積累。
在翻閱了許多與網站性能相關的書籍后,我還是更鐘情于唐文前輩編著的《大型網站性能監測、分析與優化》,里面的知識較新,切合實際,至少我讀完一遍后很有收獲、醍醐灌頂,我也希望對性能感興趣的讀者在看完我的文章后能去翻翻這本著作。
這里筆者還建議大家平時有事沒事可以多去看幾遍雅虎軍規,雖是老生常談,但卻字字珠璣。如果大家能熟記于心更是再好不過了,傳送門:https://www.cnblogs.com/xianyulaodi/p/5755079.html
原鏈接:http://imweb.io/topic/5b6fd3c13cb5a02f33c013bd
*請認真填寫需求信息,我們會在24小時內與您取得聯系。