整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          「干貨」在線直播技術(shù)8分鐘入門

          「干貨」在線直播技術(shù)8分鐘入門
          • 創(chuàng)內(nèi)容,轉(zhuǎn)載請(qǐng)注明出處^_^
          • 服務(wù)器搭建-Linux安裝包版
          • rtmp服務(wù)器
          • 簡(jiǎn)介
          • nginx里的配置
          • ffmpeg推流工具
          • 部署
          • 牛刀小試↓
          • 推流命令
          • 外網(wǎng)播放
          • 監(jiān)控
          • 帶寬
          • nethogs命令監(jiān)控帶寬
          • 播放
          • rtmp流的播放
          • flash player - npm版

          文件名稱|版本號(hào)|聯(lián)系人|qq|備注

          [干貨]在線直播技術(shù)8分鐘入門|v0.0.1|飛豺|8416837|直播技術(shù)

          原創(chuàng)內(nèi)容,轉(zhuǎn)載請(qǐng)注明出處^_^

          服務(wù)器搭建-Linux安裝包版

          Docker版類似

          rtmp服務(wù)器

          簡(jiǎn)介

          rtmp是Adobe的私有協(xié)議,傳輸速度快,畫質(zhì)好,好用。

          nginx里的配置

          搭建nginx不是本文重點(diǎn),若是安裝包安裝,簡(jiǎn)要說一下:下載安裝包,解壓,在服務(wù)器安裝前提軟件,編譯,安裝nginx即可.

          vim conf/nginx.conf

          # 極簡(jiǎn)配置,成功接收推流。可以搭配on_connect等回調(diào)接口.
          rtmp {
           server{
           listen 1935; # rtmp專用端口必須是1935,便于映射到外網(wǎng)
           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
          # 漫長(zhǎng)的等待
          cd /usr/local/ffmpeg
          sh bin/ffmpeg -version
          

          執(zhí)行sh configure時(shí)可能報(bào)錯(cuò)↓

          nasm/yasm not found or too old. Use --disable-x86asm for a crippled build=> 因?yàn)闆]有裝匯編編譯器

          下載匯編編譯器:http://www.tortall.net/projects/yasm/releases/yasm-1.3.0.tar.gz,解壓tar.gz包后執(zhí)行configure make && make install,然后重新配置編譯ffmpeg即可.

          ffmpeg安裝完畢,查看版本會(huì)報(bào)錯(cuò)↓

          在這里插入圖片描述

          下述第一個(gè)方法于我的服務(wù)器無效。

          方法一,vim /etc/ld.so.conf.d/ffmpeg.conf
          # 添加內(nèi)容:/usr/loacl/ffmpeg/lib 保存退出
          # 執(zhí)行命令:ldconfig
          

          換一種方法

          方法二,

          ldd bin/ffmpeg # 查看依賴庫(kù)
          export LD_LIBRARY_PATH=/usr/local/ffmpeg/lib/ # 變量
          # 查看少不少包↓
          ldd ffmpeg
          

          OK,不缺少包.

          在這里插入圖片描述

          牛刀小試↓

           ./bin/ffmpeg -version # 查看版本
           # 添加軟連接
           ln -s /usr/local/ffmpeg/bin/ffmpeg /usr/bin/ffmpeg
           # 配置環(huán)境變量
           vim /etc/profile # 進(jìn)入后添加ffmpeg的path路徑
           # 嘗試,將rtmp視頻流保存成文件
           ffmpeg -i rtmp://58.200.131.2:1935/livetv/hunantv -acodec copy -vcodec copy -f flv -y testHunantv.flv # 執(zhí)行后,該測(cè)試視頻流即可存儲(chǔ)到當(dāng)前目錄.
          

          網(wǎng)絡(luò)rtmp流保存執(zhí)行中↓

          在這里插入圖片描述

          看到了保存的文件,說明ffmpeg命令沒問題.

          在這里插入圖片描述

          發(fā)送文件到Windows播放試一下:sz testHunantv.flv <=發(fā)送命令

          播放成功!

          推流命令

          # 靜態(tài)源
          ffmpeg -re -i avatar1.rmvb -f flv rtmp://192.168.4.152:1935/rtmp/avatar1 # 推送靜態(tài)視頻流-1
          # ————————————————————————分界線————————————————————————
          ffmpeg -i "rtmp://58.200.131.2:1935/livetv/hunantv live=1" -acodec copy -vcodec copy -f flv rtmp://內(nèi)網(wǎng)ip:1935/rtmp/hunantv # 推送實(shí)時(shí)流1 OK
          # ————————————————————————分界線————————————————————————
          ffmpeg -i "rtmp://202.69.69.180:443/webcast/bshdlive-pc live=1" -acodec copy -vcodec copy -f flv rtmp://內(nèi)網(wǎng)ip:1935/rtmp/hktv # 推送實(shí)時(shí)流2 OK
          # ————————————————————————分界線————————————————————————
          ffmpeg -i "rtmp://live.chosun.gscdn.com/live/tvchosun1.stream live=1" -acodec copy -vcodec copy -f flv rtmp://內(nèi)網(wǎng)ip:1935/rtmp/cstv # 推送實(shí)時(shí)流3 OK
          # ffmpeg亦可推送攝像頭流和內(nèi)網(wǎng)平臺(tái)提供的視頻流
          

          外網(wǎng)播放

          將內(nèi)網(wǎng)的ip:1935端口映射到外網(wǎng)某個(gè)端口,然后使用外網(wǎng)ip:外網(wǎng)端口/rtmp/文件路徑/密鑰訪問即可。

          監(jiān)控

          帶寬

          nethogs命令監(jiān)控帶寬

          • CENTOS7 nethogs
          yum install gcc-c++ libpcap-devel.x86_64 libpcap.x86_64 ncurses* # 安裝預(yù)備組件
          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 # 執(zhí)行該命令看流量
          

          播放

          • rtmp流的播放
          • flash player - npm版
          • 普通js版的配置類似,引入js文件到html即可。
          • 強(qiáng)調(diào):筆者撰文之時(shí),vue-video-player 必須以npm安裝!??!(201909161813記錄)
          • 代碼
          • npm安裝依賴
          npm install --save vue-video-player@版本號(hào)
          npm install --save videojs-flash@版本號(hào)
          
          • package.json里查看版本號(hào):
          "videojs-flash": "^2.1.0-3",
          "vue-video-player": "^4.0.6",
          
          • main.js里新增
          // 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
          
          • 播放代碼video\index.vue
          • ```html
          • <template>
          • <video-player class="vjs-custom-skin" :options="playerOptions">
          • </video-player>
          • </template>
          • <script>
          • import ‘videojs-flash’
          • export default {
          • data() {
          • return {
          • playerOptions: {
          • height: ‘360’,
          • sources: [{
          • type: ‘rtmp/flv’,
          • src: ‘rtmp://media3.sinovision.net:1935/live/livestream’ // 測(cè)試播放地址 - flv
          • }],
          • techOrder: [‘flash’],
          • autoplay: true,
          • controls: true,
          • poster: ‘https://surmon-china.github.io/vue-quill-editor/static/images/surmon-9.jpg‘ // 封面圖片
          • }
          • }
          • }
          • }
          • </script>

          ```

          • 播放效果↓

          在線直播


          • *
          • 至此,一個(gè)從無到有的在線直播功能完成。
          • 進(jìn)階內(nèi)容待續(xù)

          內(nèi)容是《Web前端開發(fā)之Javascript視頻》的課件,請(qǐng)配合大師哥《Javascript》視頻課程學(xué)習(xí)。

          文檔坐標(biāo)和視口坐標(biāo):

          元素的位置是以像素來度量的,向右代表x坐標(biāo)的增加,向下代表y坐標(biāo)的增加;但是,有兩個(gè)不同的點(diǎn)作為坐標(biāo)系的原點(diǎn):元素的x和y坐標(biāo)可以相對(duì)于文檔的左上角或者相對(duì)于視口的左上角,也就是對(duì)于一個(gè)元素來說,會(huì)有兩種坐標(biāo):文檔坐標(biāo)和視口坐標(biāo);視口坐標(biāo)也被稱為窗口坐標(biāo);

          在頂級(jí)窗口或標(biāo)簽頁(yè)中,視口只是實(shí)際顯示文檔內(nèi)容的瀏覽器的一部分,它不包括瀏覽器其他組件,如菜單、工具條等;

          對(duì)于在框架中顯示的文檔,視口是定義了框架頁(yè)的<iframe>元素;

          元素大?。?/strong>

          以下的屬性和方法不屬于DOM2級(jí)樣式規(guī)范,但卻與HTML元素的樣式息息相關(guān);IE率先引用了一些屬性,目前,所有主流的瀏覽器都支持以下屬性;

          1. 偏移量(offset dimension):

          包括元素在屏幕上占用的所有可見的空間;元素的可見大小包括寬高,所有內(nèi)邊距,滾動(dòng)條和邊框的大小(不包括外邊距);

          通過以下4個(gè)屬性可以取得元素的偏移量:

          • offsetWidth:元素在水平方向占用的空間大小,以像素計(jì),包括元素的寬度,(可見的)垂直滾動(dòng)條的寬度,左右邊框?qū)挾龋?/li>
          • offsetHeight:元素在垂直方向占用的空間大小,以像素計(jì),包括元素的高度,(可見的)水平滾動(dòng)條的高度,上下邊框高度;
          • offsetLeft:元素的左外邊框到包含元素的左內(nèi)邊框之間的像素距離,即元素的x坐標(biāo);
          • offsetTop:元素的上外邊框到包含元素的上內(nèi)邊框之間的像素距離,即元素的y坐標(biāo);

          偏移量(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返回值與包含元素有關(guān),對(duì)于有些元素,這些值是文檔坐標(biāo),但對(duì)于已定位元素的后代元素和一些其他元素(如表格單元格),這些屬性返回的坐標(biāo)是相對(duì)于祖先元素而不是文檔;

          offsetParent屬性:包含元素的引用,也就是相對(duì)的父元素;offsetParent不一定與parentNode的值相等;如:<td>元素的offsetParent是作為其祖先元素的<table>元素,因?yàn)?lt;table>是在DOM層次中距<td>最近的一個(gè)具有大小的元素;

          如果其offsetParent屬性為null,以上的屬性都是文檔坐標(biāo);

          console.log(mydiv.offsetParent);

          如果要想知道某個(gè)元素在頁(yè)面上的偏移量,將這個(gè)元素的offsetLeft和offsetTop與其offsetParent的相同屬性相加,如此循環(huán)直至根元素,就可以得到一個(gè)基本準(zhǔn)確的值;

          <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">零點(diǎn)程序員</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);
          // 定義一個(gè)函數(shù)
          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>

          但實(shí)際上這個(gè)函數(shù)返回的值是不正確的,因?yàn)闆]有包括邊框的寬度;

          既然offsetWidth和offsetHeight是包括border的寬度的,所以有些地方也把它稱為物理寬度,它的值就是該元素實(shí)際的尺寸,因此,這并不一定等于元素的width和height,只有元素的CSS設(shè)置了box-sizing:border-box時(shí)才相等;

          對(duì)于一個(gè)元素來說,它的實(shí)際的寬高也并不一定等于它的內(nèi)容的寬和高,也就是在擁有滾動(dòng)條的情況下,如:

          <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>

          另外,這兩個(gè)屬性值也不包含元素的:before或:after等偽類的寬和高;

          <style>
          #mydiv{width: 300px; height: 100px;border: 1px solid;}
          #mydiv::after{content: "web前端開發(fā)"; 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,此時(shí)返回的各個(gè)屬性值就是文檔坐標(biāo);

          對(duì)于行盒元素(如 span),offsetTop和offsetLeft描述的是第一個(gè)邊界框的位置,offsetWidth和 offsetHeight描述的是邊界框的尺寸;因此,使用offsetLeft、offsetTop、offsetWidth、offsetHeight來對(duì)應(yīng)left、top、width和height 的一個(gè)盒子將不會(huì)是文本容器 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>

          所有這些偏移量屬性都是只讀的,且每次訪問它們都需要重新計(jì)算;因此,應(yīng)該盡量避免重復(fù)訪問這些屬性;如果要使用,可以將它們保存在局部變量中,以提高性能;

          myul.offsetLeft=20;
          console.log(myul.offsetLeft);  // 50 靜默失敗

          2.客戶區(qū)大小(client dimension):

          指的是元素內(nèi)容及其內(nèi)邊距所占據(jù)的空間大小,相關(guān)屬性為:clientTop、clientLeft、clientWidth和clientHeight;

          客戶區(qū)大小(client dimension)


          客戶區(qū)大小就是元素內(nèi)部的空間大小,其與offsetWidth和offsetHeight類似,只不過不包含邊框大?。灰膊话L動(dòng)條占用的空間;

          clientWidth=CSS width + CSS padding - 水平滾動(dòng)條寬度 – CSS border(如果width不包括border的話);

          clientHeight=CSS height + CSS padding - 水平滾動(dòng)條高度 – CSS border(如果height不包括border的話);

          var mydiv=document.getElementById("mydiv");
          console.log(mydiv.clientWidth);
          console.log(mydiv.clientHeight);

          注意,它不是元素內(nèi)容實(shí)際的尺寸,而是元素內(nèi)部可見區(qū)域的大?。徊⑶沂遣话L動(dòng)條的,如:

          <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

          注意:對(duì)于行盒,如<i>、<code>或<span>等,包括塊盒被設(shè)置了display:block后,clientWidth和clientHeight總是返回0;

          當(dāng)元素的display:none時(shí),返回的也是0;

          var span=document.getElementsByTagName("span")[0];
          console.log(span.clientWidth);  // 0
          console.log(span.clientHeight);  // 0

          最典型的應(yīng)用就是之前講到的獲得瀏覽器視口大小,如:

          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
                  };
              }
          }

          有一個(gè)特例:在文檔的根元素上使用這些屬性時(shí),它們的返回值和窗口的innerWidth和innerHeight屬性值相等;但如果有滾動(dòng)條的話,innerWidth和innerHeight包括滾動(dòng)條占用空間的值;

          console.log(document.documentElement.clientWidth, document.documentElement.clientHeight);
          console.log(window.innerWidth, window.innerHeight);

          獲得瀏覽器視口大?。?/p>

          function getViewportSize(w){
              // 使用指定的窗口,如果不帶參數(shù)則使用當(dāng)前窗口
              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);

          與偏移量相似,這兩個(gè)屬性該也為只讀,并且每次訪問都要重新計(jì)算;

          clientTop屬性和clientLeft屬性:可以返回元素的上邊框和左邊框的大小,其值為一個(gè)整數(shù),沒有單位,并且是只讀的;

          var mydiv=document.getElementById("mydiv");
          console.log(mydiv.clientTop);  // 1
          console.log(mydiv.clientLeft);  // 1

          其和getComputedStyle ()方法的borderTopWidth屬性存在一定的區(qū)別;

          var mydiv=document.getElementById("mydiv");
          console.log(mydiv.clientTop);  // 12
          console.log(getComputedStyle(mydiv,null).borderTopWidth);  // 11.5px

          可見,clientTop和clientLeft返回的是整數(shù)并且沒有單位,而borderTopWidth返回的是mydiv的上邊框的厚度,是精確的;兩者之間的關(guān)系是:

          mydiv.clientTop=Math.round(parseFloat(getComputedStyle(mydiv,null).borderTopWidth));

          但firefox是向下取整;

          如果元素有滾動(dòng)條,并且將這些滾動(dòng)條放在左側(cè)或頂部,clientLeft和clientTop也就包含了滾動(dòng)條的寬度;

          對(duì)于行盒元素來說,clientLeft和clientTop值總是為0;當(dāng)元素的display:none時(shí),返回的也是0;

          3.滾動(dòng)大小(scroll dimension):

          指的是包含滾動(dòng)內(nèi)容的元素大??;

          有些元素(如<html>),即使沒有執(zhí)行任何代碼也能自動(dòng)地添加滾動(dòng)條;但另外一些元素,則需要通過css的overflow屬性進(jìn)行設(shè)置才能滾動(dòng);

          滾動(dòng)相關(guān)的屬性:

          • scrollHeight:在沒有滾動(dòng)條的情況下,元素內(nèi)容的總高度;
          • scrollWidth:在沒有滾動(dòng)條的情況下,元素內(nèi)容的總寬度;(這兩個(gè)屬性前面用過了)
          • scrollLeft:被隱藏在內(nèi)容區(qū)域左側(cè)的像素?cái)?shù);通過設(shè)置該屬性可以改變?cè)氐臐L動(dòng)位置;
          • scrollTop:被隱藏在內(nèi)容區(qū)域上方的像素?cái)?shù);通過設(shè)置這個(gè)屬性可以改變?cè)氐臐L動(dòng)位置;

          滾動(dòng)大小(scroll dimension)

          scrollWidth和scrollHeight主要用于確定元素內(nèi)容的實(shí)際大小,它們是只讀的,不包括邊框和滾動(dòng)條;

          也就是說,返回的是元素的內(nèi)容區(qū)域加上它的內(nèi)邊距再加上任何溢出內(nèi)容的尺寸;

          <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(滾動(dòng)條的寬), 168=內(nèi)容實(shí)際的高 
          console.log(mydiv.scrollWidth, mydiv.scrollHeight);
          </script>

          當(dāng)內(nèi)容正好和內(nèi)容區(qū)域匹配而沒有溢出時(shí),這些屬性與clientWidth與clientHeight是相等的;但當(dāng)溢出時(shí),它們就包含溢出的內(nèi)容,返回值比clientWidth和clientHeight要大;

          // 改變文字?jǐn)?shù)量,觀察兩者的區(qū)別
          console.log(mydiv.scrollWidth, mydiv.scrollHeight);
          console.log(mydiv.clientWidth, mydiv.clientHeight);

          通常認(rèn)為<html>(混雜模式下為<body>)元素是在瀏覽器視口滾動(dòng)的元素,因此帶有垂直滾動(dòng)條的頁(yè)面總高度就是document.documentElement.scrollHeight;

          對(duì)于不包含滾動(dòng)條的頁(yè)面,scrollWidth、scrollHeight和clientWidth、clientHeight之間的關(guān)系基本是相等的;

          console.log(document.documentElement.scrollWidth, document.documentElement.scrollHeight);
          console.log(document.documentElement.clientWidth, document.documentElement.clientHeight);

          scrollWidth和scrollHeight等于文檔內(nèi)容區(qū)域的大小,而clientWidth和clientHeight等于視口大??;但低版本的瀏覽器有可能結(jié)果并不一致;

          在確定文檔的總高度時(shí),必須取得scrollWidth/clientWidth和scrollheight/clientHeight中的最大值,才能保證在跨瀏覽器的環(huán)境下得到精確的結(jié)果;

          var docWidth=Math.max(document.documentElement.scrollWidth, document.documentElement.clientWidth);
          var docHeight=Math.max(document.documentElement.scrollHeight, document.documentElement.clientHeight);

          注:對(duì)于混雜混式下,需要使用document.body

          scrollLeft和scrollTop屬性:

          通過這兩個(gè)屬性可以取得元素當(dāng)前滾動(dòng)的狀態(tài),也就是滾動(dòng)位置;它們是可寫的,即能設(shè)置元素的滾動(dòng)位置;

          scrollLeft 屬性可以獲取或設(shè)置一個(gè)元素的內(nèi)容水平滾動(dòng)的像素?cái)?shù);scrollTop 屬性可以獲取或設(shè)置一個(gè)元素的內(nèi)容垂直滾動(dòng)的像素?cái)?shù);

          在尚未滾動(dòng)時(shí),兩值均為0;如果垂直滾動(dòng),該值大于0,且表示元素上方不可見內(nèi)容的像素高度,如果水平滾動(dòng),該值大于0,且表示元素左方不可見內(nèi)容的像素寬;

          console.log(document.documentElement.scrollLeft);
          console.log(document.documentElement.scrollTop);

          示例:滾動(dòng)一個(gè)元素;

          <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>

          將元素的這兩個(gè)屬性設(shè)置為0,就可以重置元素的滾動(dòng)位置;

          function scrollToTop(element){
              if(element.scrollTop !=0){
                  element.scrollTop=0;
              }
          }
          // 應(yīng)用
          var btn=document.getElementById("btn");
          btn.onclick=function(){
              scrollToTop(document.documentElement);
          }

          判定元素是否滾動(dòng)到底:如果元素滾動(dòng)到底,下面等式返回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");
          }

          檢查容器能否滾動(dòng):

          // 加個(gè)判斷,條件也可以是:
          // window.getComputedStyle(mydiv).overflow==='hidden'
          var timer;
          if(window.getComputedStyle(mydiv).overflowY==='scroll'){
              timer=setInterval(scrollDiv, 100);
          }

          scrollLeft和scrollTop可以被設(shè)置為任何整數(shù)值,但有以下特點(diǎn):

          • 如果一個(gè)元素不能被滾動(dòng)(例如,它沒有溢出),scrollTop將被設(shè)置為0;
          • 設(shè)置的值小于0,這兩個(gè)屬性值被設(shè)為0;
          • 如果設(shè)置了超出這個(gè)容器可滾動(dòng)的值,,這兩個(gè)屬性會(huì)被設(shè)為最大值;
          var mydiv=document.getElementById("mydiv");
          mydiv.scrollTop=500; // 或設(shè)為負(fù)數(shù)
          console.log(mydiv.scrollTop); // 0

          這兩個(gè)屬性值有可能是小數(shù)(比如縮放了頁(yè)面的顯示大?。栽谌≈禃r(shí)最好取整,例如:Math.ceil()或Math.floor;

          function getElementPos(element){
              var y=0, x=0;
              var current=element;
              for(var e=element; e !=null; e=e.offsetParent){
                  // 但是自己的邊框不能加進(jìn)去
                  if(current==e){
                      x +=e.offsetLeft;
                      y +=e.offsetTop;
                  }else{
                      x +=e.offsetLeft + e.clientLeft;
                      y +=e.offsetTop + e.clientTop;
                  }
              }
              // 再次循環(huán)所有的祖先元素,減去滾動(dòng)的偏移量,并轉(zhuǎn)換為視口坐標(biāo)
              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>同意協(xié)議</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="請(qǐng)閱讀以下內(nèi)容"
              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 ? "歡迎參加" : "請(qǐng)繼續(xù)閱讀";
              }
          }
          </script>

          示例:滾動(dòng)文本,如:

          <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>

          圖示匯總各個(gè)屬性:

          元素坐標(biāo)、幾何尺寸

          windows對(duì)象的pageXOffset、pageYOffset和scrollX、scrollY:

          pageXOffset 和 pageYOffset 屬性返回文檔在窗口左上角水平和垂直方向滾動(dòng)的像素;這一對(duì)屬性等于scrollX和scrollY屬性,前者是后者的別稱;但I(xiàn)E不支持后者;這些屬性是只讀的;

          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關(guān)系:返回值是一樣的;

          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)兩個(gè)屬性都不支持,必須通過其他的非標(biāo)準(zhǔn)屬性來解決此問題;

          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);

          封裝一個(gè)函數(shù):

          // 以一個(gè)對(duì)象的x和y屬性的方式返回滾動(dòng)條的偏移量
          function getScrollOffset(w){
              // 使用指定的窗口,如果不帶參數(shù)則使用當(dāng)前窗口
              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};
              // 針對(duì)怪異模式
              return {x: d.body.scrollLeft, y: d.body.scrollTop};
          }
          console.log(getScrollOffset().x);
          console.log(getScrollOffset().y);

          4. 確定元素大小:

          瀏覽器為每個(gè)元素都提供了一個(gè)Element.getBoundingClientRect()([?ba?nd??] 邊界)方法;該方法是在IE5引入的;該方法不需要參數(shù),返回一個(gè)矩形對(duì)象,類型為DOMRect,包含6個(gè)屬性:x、y、left、top、right、bottom、width和height;這些屬性給出了元素在頁(yè)面中相對(duì)于視口的位置和寬高;其中,x和left相同,y與top相同;right和bottom屬性表示元素的右下角的x和y坐標(biāo);

          <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并沒有實(shí)現(xiàn)x和y;

          DOMRect 中的 top, left, right, bottom 屬性是使用對(duì)象的其他屬性的值來計(jì)算獲得的;

          在IE及老款的Edge中,該方法返回的并不是DOMRect類型,而是ClientRect類型;

          console.log(ClientRect);
          console.log(ClientRect.prototype);  // [object ClientRectPrototype]

          getBoundingClientRect()返回的數(shù)據(jù)是包括元素的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);

          這是標(biāo)準(zhǔn)盒子,如果是怪異盒子,比如在CSS中設(shè)置box-sizing:border-box,那么返回的數(shù)據(jù)中的寬和高就等于元素的width和height;

          如果要轉(zhuǎn)化為文檔坐標(biāo),需要加上滾動(dòng)的偏移量;

          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()函數(shù)
          var offsets=getScrollOffset();
          var x=rect.left + offsets.x;
          var y=rect.top + offsets.y;
          console.log(x,y);

          在布局中,文檔中的元素分為塊盒和行盒,塊盒總是為矩形,但行盒可能跨了多行,因此可能由多個(gè)矩形組成,可以把它稱為邊界矩形;

          var span=document.getElementById("mydiv").getElementsByTagName("span")[0];
          var rect=span.getBoundingClientRect();
          console.log(rect);

          在IE8及以下瀏覽器中,該方法返回的對(duì)象中并不包括width和height屬性;可以使用一個(gè)簡(jiǎn)便的方式計(jì)算元素的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);

          示例:元素在頁(yè)面上的相對(duì)文檔的偏移量,如:

          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()方法:

          該方法返回一個(gè)指向客戶端中每一個(gè)盒子的邊界矩形的矩形集合;該矩形集合是一個(gè)只讀的類數(shù)組對(duì)象DOMRectList,可以稱為矩形列表對(duì)象,它的每個(gè)元素都是DOMRect對(duì)象;

          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]);
          }

          當(dāng)然,該方法也可以應(yīng)用在塊盒中,此時(shí)它只返回包含一個(gè)元素的集合對(duì)象;

          var mydiv=document.getElementById("mydiv");
          console.log(mydiv.getClientRects()[0]);

          在IE中返回的是ClientRectList類型,其中保存的是ClientRect類型的對(duì)象;

          對(duì)于HTML area元素、自身不做任何渲染的SVG元素、display:none元素和不直接渲染出來的任何元素,都將會(huì)返回一個(gè)空列表;

          小示例:

          <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前端開發(fā)課程,包括HTML、CSS、Javascript等內(nèi)容</span></p>
          </div>
          <div>
              <strong>p的rect</strong>
              <p class="rect"><span>Web前端開發(fā)課程,包括HTML、CSS、Javascript等內(nèi)容</span></p>
          </div>
          <div>
              <strong>span的rect</strong>
              <p class="rect"><span>Web前端開發(fā)課程,包括HTML、CSS、Javascript等內(nèi)容</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){
              // 為了使邊框?qū)挾扰c矩形寬度一致,這里給每個(gè)客戶矩形上方絕對(duì)定位一個(gè) div。
              // 注意:如果用戶改變大小或者縮放,繪圖將會(huì)重繪。
              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作為邊框?qū)挾?,所以?nèi)容寬度減少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>

          對(duì)于NodeList等對(duì)象,它們是實(shí)時(shí)的,但getBoundingClientRect()和getClientRects()所返回的矩形對(duì)象或矩形列表對(duì)象并不是實(shí)時(shí)的,它們只是調(diào)用方法時(shí)文檔視覺狀態(tài)的靜態(tài)快照,在用戶滾動(dòng)或改變?yōu)g覽器窗口大小時(shí)不會(huì)更新它們;

          document.elementFromPoint()方法:如果想在指定位置上判定有什么元素,可以使用該方法;參數(shù)需要傳遞x和y坐標(biāo),不需要單位px,該坐標(biāo)是視口坐標(biāo),該方法返回在指定位置的一個(gè)元素;

          如果在指定位置有多個(gè)元素,它返回的是里面和最上面的(即CSS的z-index屬性),如果指定的點(diǎn)在視口以外,該方法返回null;

          典型的案例是將鼠標(biāo)指針的坐標(biāo)傳遞給它來判斷鼠標(biāo)在哪個(gè)元素上,但是,在鼠標(biāo)事件中的target屬性也包含了這些坐標(biāo)信息,所以,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());
          // 鼠標(biāo)事件中的坐標(biāo)
          var mydiv=document.getElementById("mydiv");
          document.addEventListener("click",function(e){
              console.log(e.target);
              console.log(e.clientX, e.clientY);
          },false);

          document.elementsFromPoint()方法:該方法返還在特定坐標(biāo)點(diǎn)下的HTML元素?cái)?shù)組;IE與老版的Edge并不支持;

          var elts=document.elementsFromPoint(100,250);
          console.log(elts);

          滾動(dòng):

          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;
              // 滾動(dòng)到最后一屏
              window.scrollTo(0, documentHeight - viewportHeight);
          }

          scrollBy(x, y)方法:

          其與以上兩個(gè)方法類似,但是它的參數(shù)是相對(duì)的,并在當(dāng)前滾動(dòng)的偏移量上增加

          window.scrollBy(5,5);

          示例,閱讀文章時(shí)自動(dòng)滾屏,如:

          <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>

          以上方法,參數(shù)除了x和y坐標(biāo)外,還可以是一個(gè)ScrollToOptions對(duì)象;

          CSSOM View 規(guī)范的ScrollToOptions對(duì)象,用于指定一個(gè)元素應(yīng)該滾動(dòng)到哪里,以及滾動(dòng)是否應(yīng)該平滑;與我們之前講的scrollIntoView()方法的參數(shù)類似,但類型不一樣,其為ScrollIntoViewOptions,屬性為block及inline等;而ScrollToOptions對(duì)象擁有的是top、left和behavior屬性,其中behavior屬性值可能為:auto及smooth;該參數(shù)IE和Edge不支持;

          如果不使用ScrollToOptions對(duì)象參數(shù),也可以使用CSS指定,如:

          html,body{
              scroll-behavior:smooth;
          }

          但I(xiàn)E和Edge依然不支持;

          var btn=document.getElementById("btn");
          btn.addEventListener("click",function(e){
          window.scroll(mydiv.offsetLeft,mydiv.offsetTop);
          // 或,但I(xiàn)E與Edge不支持
              window.scroll({left:mydiv.offsetLeft, top:mydiv.offsetTop, behavior:"smooth"});
          },false);
          Element.scroll()、Element.scrollBy()、Element.scrollTo();

          這些方法是用于在給定的元素中滾動(dòng)到某個(gè)特定坐標(biāo),其用法與window上的三個(gè)方法一致,但I(xiàn)E與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前端開發(fā)之Javascript

          —本文是對(duì)之前同名文章的修正,將所有webpack3的內(nèi)容更新為webpack4,以及加入了筆者近期在公司工作中學(xué)習(xí)到的自動(dòng)化思想,對(duì)文章內(nèi)容作了進(jìn)一步提升。

          0.引言

          作為互聯(lián)網(wǎng)項(xiàng)目,最重要的便是用戶體驗(yàn)。在舉國(guó)“互聯(lián)網(wǎng)+”的熱潮中,用戶至上也已經(jīng)被大多數(shù)企業(yè)所接收,特別是在如今移動(dòng)端快速發(fā)展的時(shí)代,我們的網(wǎng)頁(yè)不僅只是呈現(xiàn)在用戶的PC瀏覽器里,更多的時(shí)候,用戶是通過移動(dòng)產(chǎn)品瀏覽我們的網(wǎng)頁(yè)。加之有越來越多的開發(fā)者投入到Web APP和Hybrid APP的開發(fā)隊(duì)伍中,性能這一問題又再一次被提上了程序員們重點(diǎn)關(guān)注的要素。我曾經(jīng)看到過這樣一句話:一個(gè)網(wǎng)站的體驗(yàn),決定了用戶是否愿意去了解網(wǎng)站的功能;而網(wǎng)站的功能,決定了用戶是否會(huì)一票否決網(wǎng)站的體驗(yàn)。這是改版自網(wǎng)絡(luò)上的一句流行語(yǔ),但卻把網(wǎng)站性能這件事說的十分透徹,特別是在網(wǎng)站這樣的項(xiàng)目中,如果一個(gè)用戶需要超過5s才能看見頁(yè)面,他會(huì)毫不猶豫地關(guān)閉它。性能優(yōu)化,作為工程師界的“上乘武功”,是我們?cè)陂_發(fā)中老生常談的話題,也是一名開發(fā)者從入門向資深進(jìn)階的必經(jīng)階段,雖然我們看到過很多的標(biāo)準(zhǔn)、軍規(guī),但在真正實(shí)踐中,卻常常力不從心,不知道落下了什么,不知道性能是否還有進(jìn)一步優(yōu)化的空間。

          對(duì)于網(wǎng)站的性能,在行業(yè)內(nèi)有很多既定的指標(biāo),但就以前端er而言,我們應(yīng)該更加關(guān)注以下指標(biāo):白屏?xí)r間、首屏?xí)r間、整頁(yè)時(shí)間、DNS時(shí)間、CPU占用率。而我之前自己搭建的一個(gè)網(wǎng)站(網(wǎng)址:http://jerryonlyzrj.com/resume/ ,近日因域名備案無法打開,幾日后即恢復(fù)正常),完全沒做性能優(yōu)化時(shí),首屏?xí)r間是12.67s,最后經(jīng)過多方面優(yōu)化,終于將其降低至1.06s,并且還未配置CDN加速。其中過程我踩了很多坑,也翻了許多專業(yè)書籍,最后決定將這幾日的努力整理成文,幫助前端愛好者們少走彎路。文章更新可能之后不會(huì)實(shí)時(shí)同步在論壇上,歡迎大家關(guān)注我的Github,我會(huì)把最新的文章更新在對(duì)應(yīng)的項(xiàng)目里,讓我們一起在代碼的海洋里策馬奔騰:https://github.com/jerryOnlyZRJ 。

          今天,我們將從性能優(yōu)化的三大方面工作逐步展開介紹,其中包括網(wǎng)絡(luò)傳輸性能、頁(yè)面渲染性能以及JS阻塞性能,系統(tǒng)性地帶著讀者們體驗(yàn)性能優(yōu)化的實(shí)踐流程。

          1.網(wǎng)絡(luò)傳輸性能優(yōu)化

          在開始介紹網(wǎng)絡(luò)傳輸性能優(yōu)化這項(xiàng)工作之前,我們需要了解瀏覽器處理用戶請(qǐng)求的過程,那么就必須奉上這幅神圖了:

          這是navigation timing監(jiān)測(cè)指標(biāo)圖,從圖中我們可以看出,瀏覽器在得到用戶請(qǐng)求之后,經(jīng)歷了下面這些階段:重定向→拉取緩存→DNS查詢→建立TCP鏈接→發(fā)起請(qǐng)求→接收響應(yīng)→處理HTML元素→元素加載完成。不著急,我們對(duì)其中的細(xì)節(jié)一步步展開討論:

          1.1.瀏覽器緩存

          我們都知道,瀏覽器在向服務(wù)器發(fā)起請(qǐng)求前,會(huì)先查詢本地是否有相同的文件,如果有,就會(huì)直接拉取本地緩存,這和我們?cè)诤笈_(tái)部署的Redis和Memcache類似,都是起到了中間緩沖的作用,我們先看看瀏覽器處理緩存的策略:

          因?yàn)榫W(wǎng)上的圖片太籠統(tǒng)了,而且我翻過很多講緩存的文章,很少有將狀態(tài)碼還有什么時(shí)候?qū)⒕彺娲娣旁趦?nèi)存(memory)中什么時(shí)候緩存在硬盤中(disk)系統(tǒng)地整理出來,所以我自己繪制了一張瀏覽器緩存機(jī)制流程圖,結(jié)合這張圖再更深入地說明瀏覽器的緩存機(jī)制。

          這里我們可以使用chrome devtools里的network面板查看網(wǎng)絡(luò)傳輸?shù)南嚓P(guān)信息:

          (這里需要特別注意,在我們進(jìn)行緩存調(diào)試時(shí),需要去除network面板頂部的Disable cache 勾選項(xiàng),否則瀏覽器將始終不會(huì)從緩存中拉取數(shù)據(jù))

          瀏覽器默認(rèn)的緩存是放在內(nèi)存內(nèi)的,但我們知道,內(nèi)存里的緩存會(huì)因?yàn)檫M(jìn)程的結(jié)束或者說瀏覽器的關(guān)閉而被清除,而存在硬盤里的緩存才能夠被長(zhǎng)期保留下去。很多時(shí)候,我們?cè)趎etwork面板中各請(qǐng)求的size項(xiàng)里,會(huì)看到兩種不同的狀態(tài):from memory cache 和 from disk cache,前者指緩存來自內(nèi)存,后者指緩存來自硬盤。而控制緩存存放位置的,不是別人,就是我們?cè)诜?wù)器上設(shè)置的Etag字段。在瀏覽器接收到服務(wù)器響應(yīng)后,會(huì)檢測(cè)響應(yīng)頭部(Header),如果有Etag字段,那么瀏覽器就會(huì)將本次緩存寫入硬盤中。

          之所以拉取緩存會(huì)出現(xiàn)200、304兩種不同的狀態(tài)碼,取決于瀏覽器是否有向服務(wù)器發(fā)起驗(yàn)證請(qǐng)求。 只有向服務(wù)器發(fā)起驗(yàn)證請(qǐng)求并確認(rèn)緩存未被更新,才會(huì)返回304狀態(tài)碼。

          這里我以nginx為例,談?wù)勅绾闻渲镁彺?

          首先,我們先進(jìn)入nginx的配置文檔

          $ vim nginxPath/conf/nginx.conf

          在配置文檔內(nèi)插入如下兩項(xiàng):

          etag on;   //開啟etag驗(yàn)證
          expires 7d;    //設(shè)置緩存過期時(shí)間為7天
          

          打開我們的網(wǎng)站,在chrome devtools的network面板中觀察我們的請(qǐng)求資源,如果在響應(yīng)頭部看見Etag和Expires字段,就說明我們的緩存配置成功了。

          【!!!特別注意!!!】在我們配置緩存時(shí)一定要切記,瀏覽器在處理用戶請(qǐng)求時(shí),如果命中強(qiáng)緩存,瀏覽器會(huì)直接拉取本地緩存,不會(huì)與服務(wù)器發(fā)生任何通信,也就是說,如果我們?cè)诜?wù)器端更新了文件,并不會(huì)被瀏覽器得知,就無法替換失效的緩存。所以我們?cè)跇?gòu)建階段,需要為我們的靜態(tài)資源添加md5 hash后綴,避免資源更新而引起的前后端文件無法同步的問題。

          1.2.資源打包壓縮

          我們之前所做的瀏覽器緩存工作,只有在用戶第二次訪問我們的頁(yè)面才能起到效果,如果要在用戶首次打開頁(yè)面就實(shí)現(xiàn)優(yōu)良的性能,必須對(duì)資源進(jìn)行優(yōu)化。我們常將網(wǎng)絡(luò)性能優(yōu)化措施歸結(jié)為三大方面:減少請(qǐng)求數(shù)、減小請(qǐng)求資源體積、提升網(wǎng)絡(luò)傳輸速率。現(xiàn)在,讓我們逐個(gè)擊破:

          結(jié)合前端工程化思想,我們?cè)趯?duì)上線文件進(jìn)行自動(dòng)化打包編譯時(shí),通常都需要打包工具的協(xié)助,這里我推薦webpack,我通常都使用Gulp和Grunt來編譯node,Parcel太新,而且webpack也一直在自身的特性上向Parcel靠攏。

          在對(duì)webpack進(jìn)行上線配置時(shí),我們要特別注意以下幾點(diǎn):

          ①JS壓縮:(這點(diǎn)應(yīng)該算是耳熟能詳了,就不多介紹了)

          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 一個(gè)這個(gè)插件的實(shí)例,并傳入相關(guān)的參數(shù)
                      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'
                  })
          

          我們?cè)谑褂胔tml-webpack-plugin 自動(dòng)化注入JS、CSS打包HTML文件時(shí),很少會(huì)為其添加配置項(xiàng),這里我給出樣例,大家直接復(fù)制就行。據(jù)悉,在Webpack5中,html-webpack-plugin的功能會(huì)像common-chunk-plugin那樣,被集成到webpack內(nèi)部,這樣我們就不需要再install額外的插件了。

          PS:這里有一個(gè)技巧,在我們書寫HTML元素的src 或 href 屬性時(shí),可以省略協(xié)議部分,這樣也能簡(jiǎn)單起到節(jié)省資源的目的。(雖然其目的本身是為了統(tǒng)一站內(nèi)的所有協(xié)議)

          ③提取公共資源:

          splitChunks: {
                cacheGroups: {
                  vendor: { // 抽離第三方插件
                    test: /node_modules/, // 指定是node_modules下的第三方包
                    chunks: 'initial',
                    name: 'common/vendor', // 打包后的文件名,任意命名    
                    priority: 10 // 設(shè)置優(yōu)先級(jí),防止和自定義的公共代碼提取時(shí)被覆蓋,不進(jìn)行打包
                  },
                  utils: { // 抽離自定義公共代碼
                    test: /\.js$/,
                    chunks: 'initial',
                    name: 'common/utils',
                    minSize: 0 // 只要超出0字節(jié)就生成一個(gè)新包
                  }
                }
              }
          

          ④提取css并壓縮:

          在使用webpack的過程中,我們通常會(huì)以模塊的形式引入css文件(webpack的思想不就是萬物皆模塊嘛),但是在上線的時(shí)候,我們還需要將這些css提取出來,并且壓縮,這些看似復(fù)雜的過程只需要簡(jiǎn)單的幾行配置就行:

          (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'
                              }
                          }
                      ]
                  }]
              }
          

          我這里配置預(yù)處理器postcss,但是我把相關(guān)配置提取到了單獨(dú)的文件postcss.config.js里了,其中cssnano是一款很不錯(cuò)的CSS優(yōu)化插件。

          ⑤將webpack開發(fā)環(huán)境修改為生產(chǎn)環(huán)境:

          在使用webpack打包項(xiàng)目時(shí),它常常會(huì)引入一些調(diào)試代碼,以作相關(guān)調(diào)試,我們?cè)谏暇€時(shí)不需要這部分內(nèi)容,通過配置剔除:

          devtool: 'false'
          

          如果你能按照上述六點(diǎn)將webpack上線配置完整配置出來,基本能將文件資源體積壓縮到極致了,如有疏漏,還希望大家能加以補(bǔ)充。

          最后,我們還應(yīng)該在服務(wù)器上開啟Gzip傳輸壓縮,它能將我們的文本類文件體積壓縮至原先的四分之一,效果立竿見影,還是切換到我們的nginx配置文檔,添加如下兩項(xiàng)配置項(xiàng)目:

          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;
          

          【!??!特別注意!?。 ?/strong>不要對(duì)圖片文件進(jìn)行Gzip壓縮!不要對(duì)圖片文件進(jìn)行Gzip壓縮!不要對(duì)圖片文件進(jìn)行Gzip壓縮!我只會(huì)告訴你效果適得其反,至于具體原因,還得考慮服務(wù)器壓縮過程中的CPU占用還有壓縮率等指標(biāo),對(duì)圖片進(jìn)行壓縮不但會(huì)占用后臺(tái)大量資源,壓縮效果其實(shí)并不可觀,可以說是“弊大于利”,所以請(qǐng)?jiān)趃zip_types 把圖片的相關(guān)項(xiàng)去掉。針對(duì)圖片的相關(guān)處理,我們接下來會(huì)更加具體地介紹。

          1.3.圖片資源優(yōu)化

          剛剛我們介紹了資源打包壓縮,只是停留在了代碼層面,而在我們實(shí)際開發(fā)中,真正占用了大量網(wǎng)絡(luò)傳輸資源的,并不是這些文件,而是圖片,如果你對(duì)圖片進(jìn)行了優(yōu)化工作,你能立刻看見明顯的效果。

          1.3.1.不要在HTML里縮放圖像

          很多開發(fā)者可能會(huì)有這樣的錯(cuò)覺(其實(shí)我曾經(jīng)也是這樣),我們會(huì)為了方便在一個(gè)200?200的圖片容器內(nèi)直接使用一張400?400的圖片,我們甚至認(rèn)為這樣能讓用戶覺得圖片更加清晰,其實(shí)不然,在普通的顯示器上,用戶并不會(huì)感到縮放后的大圖更加清晰,但這一切卻導(dǎo)致網(wǎng)頁(yè)加速速度下降,同時(shí)照成帶寬浪費(fèi),你可能不知道,一張200KB的圖片和2M的圖片的傳輸時(shí)間會(huì)是200m和12s的差距(親身經(jīng)歷,深受其害(┬_┬))。所以,當(dāng)你需要用多大的圖片時(shí),就在服務(wù)器上準(zhǔn)備好多大的圖片,盡量固定圖片尺寸。

          1.3.2.使用雪碧圖(CSS Sprite)

          雪碧圖的概念大家一定在生活中經(jīng)常聽見,其實(shí)雪碧圖是減小請(qǐng)求數(shù)的顯著運(yùn)用。而且很奇妙的是,多張圖片聘在一塊后,總體積會(huì)比之前所有圖片的體積之和小(你可以親自試試)。這里給大家推薦一個(gè)自動(dòng)化生成雪碧圖的工具:https://www.toptal.com/developers/css/sprite-generator (圖片來自官網(wǎng)首頁(yè))

          只要你添加相關(guān)資源文件,他就會(huì)自動(dòng)幫你生成雪碧圖以及對(duì)應(yīng)的CSS樣式。

          其實(shí)我們?cè)诠こ讨羞€有更為自動(dòng)的方法,便是一款雪碧圖生成插件webpack-spritesmith。首先,先簡(jiǎn)單介紹一下使用插件生成雪碧圖的思路:

          首先,我們會(huì)把我們所需要的小圖標(biāo)放置在一個(gè)文件夾內(nèi)以便于管理:

          (這里的@2x圖片是為了適配視網(wǎng)膜二倍屏的圖片資源,webpack-spritesmith內(nèi)有專門為適配多倍屏提供的配置項(xiàng),稍候?qū)?huì)講到)

          然后,我們需要插件去讀取這個(gè)文件夾內(nèi)的所有圖片資源文件,以文件夾名稱為圖片名稱生成一張雪碧圖到指定位置,并且輸出能夠正確使用這些雪碧圖的CSS文件。

          如今,webpack-spritesmith這款插件能實(shí)現(xiàn)我們想要的一切,先奉上配置內(nèi)容: (具體可參照webpack-spritesmith官方文檔: https://www.npmjs.com/package/webpack-spritesmith )

          執(zhí)行webpack之后,就會(huì)在開發(fā)目錄里生成上面兩張圖的結(jié)果,我們可以看看common.css里面的內(nèi)容:

          我們可以看到,所有我們之前放在common文件夾里的圖片資源都自動(dòng)地生成了相應(yīng)的樣式,這些都不需要我們手動(dòng)處理,`webpack-spritesmith這款插件就已經(jīng)幫我們完成了!

          1.3.3.使用字體圖標(biāo)(iconfont)

          不論是壓縮后的圖片,還是雪碧圖,終歸還是圖片,只要是圖片,就還是會(huì)占用大量網(wǎng)絡(luò)傳輸資源。但是字體圖標(biāo)的出現(xiàn),卻讓前端開發(fā)者看到了另外一個(gè)神奇的世界。

          我最喜歡用的是阿里矢量圖標(biāo)庫(kù)(網(wǎng)址:http://www.iconfont.cn/ ),里面有大量的矢量圖資源,而且你只需要像在淘寶采購(gòu)一樣把他們添加至購(gòu)物車就能把它們帶回家,整理完資源后還能自動(dòng)生成CDN鏈接,可以說是完美的一條龍服務(wù)了。(圖片來自官網(wǎng)首頁(yè))

          圖片能做的很多事情,矢量圖都能作,而且它只是往HTML里插入字符和CSS樣式而已,和圖片請(qǐng)求比起來資源占用完全不在一個(gè)數(shù)量級(jí),如果你的項(xiàng)目里有小圖標(biāo),就是用矢量圖吧。

          但如果我們做的是公司或者團(tuán)隊(duì)的項(xiàng)目,需要使用到許多自定義的字體圖標(biāo),可愛的設(shè)計(jì)小姐姐們只是丟給你了幾份.svg圖片,你又該如何去做呢?

          其實(shí)也很簡(jiǎn)單,阿里矢量圖標(biāo)庫(kù)就提供了上傳本地SVG資源的功能,這里另外推薦一個(gè)網(wǎng)站——icomoon。icomoon這個(gè)網(wǎng)站也為我們提供了將SVG圖片自動(dòng)轉(zhuǎn)化成CSS樣式的功能。(圖片來自icomoon首頁(yè))

          我們可以點(diǎn)擊Import Icons按鈕導(dǎo)入我們本地的SVG資源,然后選中他們,接下來生成CSS的事情,就交給icomoon吧,具體的操作,就和阿里矢量圖標(biāo)庫(kù)類同了。

          1.3.4.使用WebP

          WebP格式,是谷歌公司開發(fā)的一種旨在加快圖片加載速度的圖片格式。圖片壓縮體積大約只有JPEG的2/3,并能節(jié)省大量的服務(wù)器帶寬資源和數(shù)據(jù)空間。Facebook、Ebay等知名網(wǎng)站已經(jīng)開始測(cè)試并使用WebP格式。

          我們可以使用官網(wǎng)提供的Linux命令行工具對(duì)項(xiàng)目中的圖片進(jìn)行WebP編碼,也可以使用我們的線上服務(wù),這里我推薦叉拍云(網(wǎng)址:https://www.upyun.com/webp )。但是在實(shí)際的上線工作中,我們還是得編寫Shell腳本用命令行工具進(jìn)行自動(dòng)化編譯,測(cè)試階段用線上服務(wù)方便快捷。(圖片來自叉拍云官網(wǎng))

          1.4.網(wǎng)絡(luò)傳輸性能檢測(cè)工具——Page Speed

          除了network版塊,其實(shí)chrome還為我們準(zhǔn)備好了一款監(jiān)測(cè)網(wǎng)絡(luò)傳輸性能的插件——Page Speed,咱們的文章封面,就是用的Page Speed的官方宣傳圖(因?yàn)槲矣X得這張圖再合適不過了)。我們只需要通過下面步驟安裝,就可以在chrome devtools里找到它了:chrome菜單→更多工具→拓展程序→chrome網(wǎng)上應(yīng)用商店→搜索pagespeed后安轉(zhuǎn)即可。

          (PS:使用chrome應(yīng)用商店需要翻墻,怎么翻墻我就不便多說了)

          這就是Page Speed的功能界面:

          我們只需要打開待測(cè)試的網(wǎng)頁(yè),然后點(diǎn)擊Page Speed里的 Start analyzing按鈕,它就會(huì)自動(dòng)幫我們測(cè)試網(wǎng)絡(luò)傳輸性能了,這是我的網(wǎng)站測(cè)試結(jié)果:

          Page Speed最人性化的地方,便是它會(huì)對(duì)測(cè)試網(wǎng)站的性能瓶頸提出完整的建議,我們可以根據(jù)它的提示進(jìn)行優(yōu)化工作。這里我的網(wǎng)站已經(jīng)優(yōu)化到最好指標(biāo)了(??????)??,Page Speed Score表示你的性能測(cè)試得分,100/100表示已經(jīng)沒有需要優(yōu)化的地方。

          優(yōu)化完畢后再使用chorme devtools的network版塊測(cè)量一下我們網(wǎng)頁(yè)的白屏?xí)r間還有首屏?xí)r間,是不是得到了很大的提升?

          1.5.使用CDN

          Last but not least,

          再好的性能優(yōu)化實(shí)例,也必須在CDN的支撐下才能到達(dá)極致。

          如果我們?cè)贚inux下使用命令$ traceroute targetIp 或者在Windows下使用批處理 > tracert targetIp,都可以定位用戶與目標(biāo)計(jì)算機(jī)之間經(jīng)過的所有路由器,不言而喻,用戶和服務(wù)器之間距離越遠(yuǎn),經(jīng)過的路由器越多,延遲也就越高。使用CDN的目的之一便是解決這一問題,當(dāng)然不僅僅如此,CDN還可以分擔(dān)IDC壓力。

          當(dāng)然,憑著我們單個(gè)人的資金實(shí)力(除非你是王思聰)是必定搭建不起來CDN的,不過我們可以使用各大企業(yè)提供的服務(wù),諸如騰訊云等,配置也十分簡(jiǎn)單,這里就請(qǐng)大家自行去推敲啦。

          2.頁(yè)面渲染性能優(yōu)化

          2.1.瀏覽器渲染過程(Webkit)

          其實(shí)大家應(yīng)該對(duì)瀏覽器將的HTML渲染機(jī)制比較熟悉了,基本流程同上圖所述,大家在入門的時(shí)候,你的導(dǎo)師或者前輩可能會(huì)告訴你,在渲染方面我們要減少重排和重繪,因?yàn)樗麄儠?huì)影響瀏覽器性能。不過你一定不知道其中原理是什么,對(duì)吧。今天我們就結(jié)合《Webkit技術(shù)內(nèi)幕》(這本書我還是很推薦大家買來看看,好歹作為一名前端工程師,你得知道我們天天接觸的瀏覽器內(nèi)核是怎樣工作的)的相關(guān)知識(shí),給大家普及普及那些深層次的概念。

          PS:這里提到了Webkit內(nèi)核,我順帶提一下瀏覽器內(nèi)部的渲染引擎、解釋器等組件的關(guān)系,因?yàn)榻?jīng)常有師弟或者一些前端愛好者向我問這方面的知識(shí),分不清他們的關(guān)系,我就拿一張圖來說明:(如果你對(duì)著不感興趣,可以直接跳過)

          瀏覽器的解釋器,是包括在渲染引擎內(nèi)的,我們常說的Chrome(現(xiàn)在使用的是Blink引擎)和Safari使用的Webkit引擎,F(xiàn)irefox使用的Gecko引擎,指的就是渲染引擎。而在渲染引擎內(nèi),還包括著我們的HTML解釋器(渲染時(shí)用于構(gòu)造DOM樹)、CSS解釋器(渲染時(shí)用于合成CSS規(guī)則)還有我們的JS解釋器。不過后來,由于JS的使用越來越重要,工作越來越繁雜,所以JS解釋器也漸漸獨(dú)立出來,成為了單獨(dú)的JS引擎,就像眾所周知的V8引擎,我們經(jīng)常接觸的Node.js也是用的它。

          2.2.DOM渲染層與GPU硬件加速

          如果我告訴你,一個(gè)頁(yè)面是有許多許多層級(jí)組成的,他們就像千層面那樣,你能想象出這個(gè)頁(yè)面實(shí)際的樣子嗎?這里為了便于大家想象,我附上一張之前Firefox的3D View插件的頁(yè)面Layers層級(jí)圖:

          對(duì),你沒看錯(cuò),頁(yè)面的真實(shí)樣子就是這樣,是由多個(gè)DOM元素渲染層(Layers)組成的,實(shí)際上一個(gè)頁(yè)面在構(gòu)建完render tree之后,是經(jīng)歷了這樣的流程才最終呈現(xiàn)在我們面前的:

          ①瀏覽器會(huì)先獲取DOM樹并依據(jù)樣式將其分割成多個(gè)獨(dú)立的渲染層

          ②CPU將每個(gè)層繪制進(jìn)繪圖中

          ③將位圖作為紋理上傳至GPU(顯卡)繪制

          ④GPU將所有的渲染層緩存(如果下次上傳的渲染層沒有發(fā)生變化,GPU就不需要對(duì)其進(jìn)行重繪)并復(fù)合多個(gè)渲染層最終形成我們的圖像

          從上面的步驟我們可以知道,布局是由CPU處理的,而繪制則是由GPU完成的。

          其實(shí)在chrome中,也為我們提供了相關(guān)插件供我們查看頁(yè)面渲染層的分布情況,以及GPU的占用率:(所以說,平時(shí)我們得多去嘗試嘗試chrome的那些莫名其妙的插件,真的會(huì)發(fā)現(xiàn)好多東西都是神器)

          chrome開發(fā)者工具菜單→more tools→Layers(開啟渲染層功能模塊)

          chrome開發(fā)者工具菜單→more tools→rendering(開啟渲染性能監(jiān)測(cè)工具)

          執(zhí)行上面的操作后,你會(huì)在瀏覽器里看到這樣的效果:

          太多東西了,分模塊講吧:

          (一)最先是頁(yè)面右上方的小黑窗:其實(shí)提示已經(jīng)說的很清楚了,它顯示的就是我們的GPU占用率,能夠讓我們清楚地知道頁(yè)面是否發(fā)生了大量的重繪。

          (二)Layers版塊:這就是用于顯示我們剛提到的DOM渲染層的工具了,左側(cè)的列表里將會(huì)列出頁(yè)面里存在哪些渲染層,還有這些渲染層的詳細(xì)信息。

          (三)Rendering版塊:這個(gè)版塊和我們的控制臺(tái)在同一個(gè)地方,大家可別找不到它。前三個(gè)勾選項(xiàng)是我們最常使用的,讓我來給大家解釋一下他們的功能(充當(dāng)一次免費(fèi)翻譯)

          ①Paint flashing:勾選之后會(huì)對(duì)頁(yè)面中發(fā)生重繪的元素高亮顯示

          ②Layer borders:和我們的Layer版塊功能類似,它會(huì)用高亮邊界突出我們頁(yè)面中的各個(gè)渲染層

          ③FPS meter:就是開啟我們?cè)冢ㄒ唬┲刑岬降男『诖?,用于觀察我們的GPU占用率

          可能大家會(huì)問我,和我提到DOM渲染層這么深的概念有什么用啊,好像跟性能優(yōu)化沒一點(diǎn)關(guān)系?。看蠹覒?yīng)該還記得我剛說到GPU會(huì)對(duì)我們的渲染層作緩存對(duì)吧,那么大家試想一下,如果我們把那些一直發(fā)生大量重排重繪的元素提取出來,單獨(dú)觸發(fā)一個(gè)渲染層,那樣這個(gè)元素不就不會(huì)“連累”其他元素一塊重繪了對(duì)吧。

          那么問題來了,什么情況下會(huì)觸發(fā)渲染層呢?大家只要記?。?/p>

          video元素、WebGL、Canvas、CSS3 3D、CSS濾鏡、z-index大于某個(gè)相鄰節(jié)點(diǎn)的元素都會(huì)觸發(fā)新的Layer,其實(shí)我們最常用的方法,就是給某個(gè)元素加上下面的樣式:

          transform: translateZ(0);
          backface-visibility: hidden;
          

          這樣就可以觸發(fā)渲染層啦(^__^) 。

          我們把容易觸發(fā)重排重繪的元素單獨(dú)觸發(fā)渲染層,讓它與那些“靜態(tài)”元素隔離,讓GPU分擔(dān)更多的渲染工作,我們通常把這樣的措施成為硬件加速,或者是GPU加速。大家之前肯定聽過這個(gè)說法,現(xiàn)在完全清楚它的原理了吧。

          2.3.重排與重繪

          現(xiàn)在到我們的重頭戲了,重排和重繪。先拋出概念:

          ①重排(reflow):渲染層內(nèi)的元素布局發(fā)生修改,都會(huì)導(dǎo)致頁(yè)面重新排列,比如窗口的尺寸發(fā)生變化、刪除或添加DOM元素,修改了影響元素盒子大小的CSS屬性(諸如:width、height、padding)。

          ②重繪(repaint):繪制,即渲染上色,所有對(duì)元素的視覺表現(xiàn)屬性的修改,都會(huì)引發(fā)重繪。

          我們習(xí)慣使用chrome devtools中的performance版塊來測(cè)量頁(yè)面重排重繪所占據(jù)的時(shí)間:

          ①藍(lán)色部分:HTML解析和網(wǎng)絡(luò)通信占用的時(shí)間

          ②黃色部分:JavaScript語(yǔ)句執(zhí)行所占用時(shí)間

          ③紫色部分:重排占用時(shí)間

          ④綠色部分:重繪占用時(shí)間

          不論是重排還是重繪,都會(huì)阻塞瀏覽器。要提高網(wǎng)頁(yè)性能,就要降低重排和重繪的頻率和成本,近可能少地觸發(fā)重新渲染。正如我們?cè)?.3中提到的,重排是由CPU處理的,而重繪是由GPU處理的,CPU的處理效率遠(yuǎn)不及GPU,并且重排一定會(huì)引發(fā)重繪,而重繪不一定會(huì)引發(fā)重排。所以在性能優(yōu)化工作中,我們更應(yīng)當(dāng)著重減少重排的發(fā)生。

          這里給大家推薦一個(gè)網(wǎng)站,里面詳細(xì)列出了哪些CSS屬性在不同的渲染引擎中是否會(huì)觸發(fā)重排或重繪:

          https://csstriggers.com/ (圖片來自官網(wǎng))

          2.4.優(yōu)化策略

          談了那么多理論,最實(shí)際不過的,就是解決方案,大家一定都等著急了吧,做好準(zhǔn)備,一大波干貨來襲:

          (一)CSS屬性讀寫分離:瀏覽器沒次對(duì)元素樣式進(jìn)行讀操作時(shí),都必須進(jìn)行一次重新渲染(重排 + 重繪),所以我們?cè)谑褂肑S對(duì)元素樣式進(jìn)行讀寫操作時(shí),最好將兩者分離開,先讀后寫,避免出現(xiàn)兩者交叉使用的情況。最最最客觀的解決方案,就是不用JS去操作元素樣式,這也是我最推薦的。

          (二)通過切換class或者style.csstext屬性去批量操作元素樣式

          (三)DOM元素離線更新:當(dāng)對(duì)DOM進(jìn)行相關(guān)操作時(shí),例、appendChild等都可以使用Document Fragment對(duì)象進(jìn)行離線操作,帶元素“組裝”完成后再一次插入頁(yè)面,或者使用display:none 對(duì)元素隱藏,在元素“消失”后進(jìn)行相關(guān)操作。

          (四)將沒用的元素設(shè)為不可見:visibility: hidden,這樣可以減小重繪的壓力,必要的時(shí)候再將元素顯示。

          (五)壓縮DOM的深度,一個(gè)渲染層內(nèi)不要有過深的子元素,少用DOM完成頁(yè)面樣式,多使用偽元素或者box-shadow取代。

          (六)圖片在渲染前指定大小:因?yàn)閕mg元素是內(nèi)聯(lián)元素,所以在加載圖片后會(huì)改變寬高,嚴(yán)重的情況會(huì)導(dǎo)致整個(gè)頁(yè)面重排,所以最好在渲染前就指定其大小,或者讓其脫離文檔流。

          (七)對(duì)頁(yè)面中可能發(fā)生大量重排重繪的元素單獨(dú)觸發(fā)渲染層,使用GPU分擔(dān)CPU壓力。(這項(xiàng)策略需要慎用,得著重考量以犧牲GPU占用率能否換來可期的性能優(yōu)化,畢竟頁(yè)面中存在太多的渲染層對(duì)與GPU而言也是一種不必要的壓力,通常情況下,我們會(huì)對(duì)動(dòng)畫元素采取硬件加速。)

          3.JS阻塞性能

          JavaScript在網(wǎng)站開發(fā)中幾乎已經(jīng)確定了壟斷地位,哪怕是一個(gè)再簡(jiǎn)單不過的靜態(tài)頁(yè)面,你都可能看到JS的存在,可以說,沒有JS,就基本沒有用戶交互。然而,腳本帶來的問題就是他會(huì)阻塞頁(yè)面的平行下載,還會(huì)提高進(jìn)程的CPU占用率。更有甚者,現(xiàn)在node.js已經(jīng)在前端開發(fā)中普及,稍有不慎,我們引發(fā)了內(nèi)存泄漏,或者在代碼中誤寫了死循環(huán),會(huì)直接造成我們的服務(wù)器奔潰。在如今這個(gè)JS已經(jīng)遍布前后端的時(shí)代,性能的瓶頸不單單只是停留在影響用戶體驗(yàn)上,還會(huì)有更多更為嚴(yán)重的問題,對(duì)JS的性能優(yōu)化工作不可小覷。

          在編程的過程中,如果我們使用了閉包后未將相關(guān)資源加以釋放,或者引用了外鏈后未將其置空(比如給某DOM元素綁定了事件回調(diào),后來卻remove了該元素),都會(huì)造成內(nèi)存泄漏的情況發(fā)生,進(jìn)而大量占用用戶的CPU,造成卡頓或死機(jī)。我們可以使用chrome提供的JavaScript Profile版塊,開啟方式同Layers等版塊,這里我就不再多說了,直接上效果圖:

          我們可以清除看見JS執(zhí)行時(shí)各函數(shù)的執(zhí)行時(shí)間以及CPU占用情況,如果我在代碼里增加一行while(true){}, 那么它的占用率一定會(huì)飆升到一個(gè)異常的指標(biāo)(親測(cè)93.26%)。

          其實(shí)瀏覽器強(qiáng)大的內(nèi)存回收機(jī)制在大多數(shù)時(shí)候避免了這一情況的發(fā)生,即便用戶發(fā)生了死機(jī),他只要結(jié)束相關(guān)進(jìn)程(或關(guān)閉瀏覽器)就可以解決這一問題,但我們要知道,同樣的情況還會(huì)發(fā)生在我們的服務(wù)器端,也就是我們的node中,嚴(yán)重的情況,會(huì)直接造成我們的服務(wù)器宕機(jī),網(wǎng)站奔潰。所以更多時(shí)候,我們都使用JavaScript Profile版塊來進(jìn)行我們的node服務(wù)的壓力測(cè)試,搭配node-inspector 插件,我們能更有效地檢測(cè)JS執(zhí)行時(shí)各函數(shù)的CPU占用率,針對(duì)性地進(jìn)行優(yōu)化。

          (PS:沒修煉到一定水平,千萬別在服務(wù)端使用閉包,一個(gè)是真沒啥用,我們會(huì)有更多優(yōu)良的解決辦法,二是真的很容易內(nèi)存泄漏,造成的后果是你無法預(yù)期的)

          4.【拓展】負(fù)載均衡

          之所以將負(fù)載均衡作為拓展內(nèi)容,是因?yàn)槿绻悄阕约捍罱ǖ膫€(gè)人網(wǎng)站,或者中小型網(wǎng)站,其實(shí)并不需要考慮多大的并發(fā)量,但是如果你搭建的是大型網(wǎng)站,負(fù)載均衡便是開發(fā)過程不可或缺的步驟。

          4.1.Node.js處理IO密集型請(qǐng)求

          現(xiàn)在的開發(fā)流程都注重前后端分離,也就是軟件工程中常提到的“高內(nèi)聚低耦合”的思想,你也可以用模塊化的思想去理解,前后解耦就相當(dāng)與把一個(gè)項(xiàng)目分成了前端和后端兩個(gè)大模塊,中間通過接口聯(lián)系起來,分別進(jìn)行開發(fā)。這樣做有什么好處?我就舉最有實(shí)際效果的一點(diǎn):“異步編程”。這是我自己想的名字,因?yàn)槲矣X得前后解耦的形式很像我們JS中的異步隊(duì)列,傳統(tǒng)的開發(fā)模式是“同步”的,前端需要等后端封裝好接口,知道了能拿什么數(shù)據(jù),再去開發(fā),時(shí)間短,工程大。而解耦之后,我們只需要提前約定好接口,前后兩端就可以同時(shí)開發(fā),不僅高效而且省時(shí)。

          我們都知道node的核心是事件驅(qū)動(dòng),通過loop去異步處理用戶請(qǐng)求,相比于傳統(tǒng)的后端服務(wù),它們都是將用戶的每個(gè)請(qǐng)求分配異步隊(duì)列進(jìn)行處理,推薦大家去看這樣一篇博文:https://mp.weixin.qq.com/s?__biz=MzAxOTc0NzExNg==&mid=2665513044&idx=1&sn=9b8526e9d641b970ee5ddac02dae3c57&scene=21#wechat_redirect 。特別生動(dòng)地講解了事件驅(qū)動(dòng)的運(yùn)行機(jī)制,通俗易懂。事件驅(qū)動(dòng)的最大優(yōu)勢(shì)是什么?就是在高并發(fā)IO時(shí),不會(huì)造成堵塞,對(duì)于直播類網(wǎng)站,這點(diǎn)是至關(guān)重要的,我們有成功的先例——快手,快手強(qiáng)大的IO高并發(fā)究其本質(zhì)一定能追溯到node。

          其實(shí)現(xiàn)在的企業(yè)級(jí)網(wǎng)站,都會(huì)搭建一層node作為中間層。大概的網(wǎng)站框架如圖所示:

          4.2.pm2實(shí)現(xiàn)Node.js“多線程”

          我們都知道node的優(yōu)劣,這里分享一份鏈接,找了挺久寫的還算詳細(xì):https://www.zhihu.com/question/19653241/answer/15993549 。其實(shí)都是老套路,那些說node不行的都是指著node是單線程這一個(gè)軟肋開撕,告訴你,我們有解決方案了——pm2。這是它的官網(wǎng):http://pm2.keymetrics.io/ 。它是一款node.js進(jìn)程管理器,具體的功能,就是能在你的計(jì)算機(jī)里的每一個(gè)內(nèi)核都啟動(dòng)一個(gè)node.js服務(wù),也就是說如果你的電腦或者服務(wù)器是多核處理器(現(xiàn)在也少見單核了吧),它就能啟動(dòng)多個(gè)node.js服務(wù),并且它能夠自動(dòng)控制負(fù)載均衡,會(huì)自動(dòng)將用戶的請(qǐng)求分發(fā)至壓力小的服務(wù)進(jìn)程上處理。聽起來這東西簡(jiǎn)直就是神器??!而且它的功能遠(yuǎn)遠(yuǎn)不止這些,這里我就不作過多介紹了,大家知道我們?cè)谏暇€的時(shí)候需要用到它就行了,安裝的方法也很簡(jiǎn)單,直接用npm下到全局就可以了$ npm i pm2 -g具體的使用方法還有相關(guān)特性可以參照官網(wǎng)。這里我在build文件夾內(nèi)添加了pm2.json文件,這是pm2的啟動(dòng)配置文件,我們可以自行配置相關(guān)參數(shù),具體可參考github源碼,運(yùn)行時(shí)我們只要在上線目錄下輸入命令$ pm2 start pm2.json即可。

          下面是pm2啟動(dòng)后的效果圖:

          4.3.nginx搭建反向代理

          在開始搭建工作之前,首先得知道什么是反向代理。可能大家對(duì)這個(gè)名詞比較陌生,先上一張圖:

          所謂代理就是我們通常所說的中介,網(wǎng)站的反向代理就是指那臺(tái)介于用戶和我們真實(shí)服務(wù)器之間的服務(wù)器(說的我都拗口了),它的作用便是能夠?qū)⒂脩舻恼?qǐng)求分配到壓力較小的服務(wù)器上,其機(jī)制是輪詢。聽完這句話是不是感覺很耳熟,沒錯(cuò),在我介紹pm2的時(shí)候也說過同樣的話,反向代理起到的作用同pm2一樣也是實(shí)現(xiàn)負(fù)載均衡,你現(xiàn)在應(yīng)該也明白了兩者之間的差異,反向代理是對(duì)服務(wù)器實(shí)現(xiàn)負(fù)載均衡,而pm2是對(duì)進(jìn)程實(shí)現(xiàn)負(fù)載均衡。大家如果想深入了解反向代理的相關(guān)知識(shí),我推薦知乎的一個(gè)貼子:https://www.zhihu.com/question/24723688 。但是大家會(huì)想到,配服務(wù)器是運(yùn)維的事情啊,和我們前端有什么關(guān)系呢?的確,在這部分,我們的工作只有一些,只需要向運(yùn)維提供一份配置文檔即可。

          http {
              upstream video {
                  ip_hash;
                  server localhost:3000;
              }
              server {
                  listen: 8080;
                  location / {
                      proxy_pass: http://video
                  }
              }
          }
          

          也就是說,在和運(yùn)維對(duì)接的時(shí)候,我們只需要將上面這幾行代碼改為我們配置好的文檔發(fā)送給他就行了,其他的事情,運(yùn)維小哥會(huì)明白的,不用多說,都在酒里。

          但是,這幾行代碼該怎么去改呢?首先我們得知道,在nginx中,模塊被分為三大類:handler、filter和upstream。而其中的upstream模塊,負(fù)責(zé)完成完成網(wǎng)絡(luò)數(shù)據(jù)的接收、處理和轉(zhuǎn)發(fā),也是我們需要在反向代理中用到的模塊。接下來我們將介紹配置代碼里的內(nèi)容所表示的含義

          4.3.1.upstream配置信息:

          upstream關(guān)鍵字后緊跟的標(biāo)識(shí)符是我們自定義的項(xiàng)目名稱,通過一對(duì)花括號(hào)在其中增添我們的配置信息。

          ip_hash 關(guān)鍵字:控制用戶再次訪問時(shí)是否連接到前一次連接的服務(wù)器

          server關(guān)鍵字:我們真實(shí)服務(wù)器的地址,這里的內(nèi)容肯定是需要我們?nèi)ヌ顚懙?,不然運(yùn)維怎么知道你把項(xiàng)目放在那個(gè)服務(wù)器上了,也不知道你封裝了一層node而得去監(jiān)聽3000端口。

          4.3.2.server配置信息

          server是nginx的基本配置,我們需要通過server將我們定義的upstream應(yīng)用到服務(wù)器上。

          listen關(guān)鍵字:服務(wù)器監(jiān)聽的端口

          location關(guān)鍵字:和我們之前在node層說到的路由是起同樣的功能,這里是把用戶的請(qǐng)求分配到對(duì)應(yīng)的upstream上

          5.拓展閱讀

          網(wǎng)站的性能與監(jiān)測(cè)是一項(xiàng)復(fù)雜的工作,還有很多很多后續(xù)的工作,我之前所提到的這些,也只能算是冰山一角,在熟悉開發(fā)規(guī)范的同時(shí),也需要實(shí)踐經(jīng)驗(yàn)的積累。

          在翻閱了許多與網(wǎng)站性能相關(guān)的書籍后,我還是更鐘情于唐文前輩編著的《大型網(wǎng)站性能監(jiān)測(cè)、分析與優(yōu)化》,里面的知識(shí)較新,切合實(shí)際,至少我讀完一遍后很有收獲、醍醐灌頂,我也希望對(duì)性能感興趣的讀者在看完我的文章后能去翻翻這本著作。

          這里筆者還建議大家平時(shí)有事沒事可以多去看幾遍雅虎軍規(guī),雖是老生常談,但卻字字珠璣。如果大家能熟記于心更是再好不過了,傳送門:https://www.cnblogs.com/xianyulaodi/p/5755079.html


          原鏈接:http://imweb.io/topic/5b6fd3c13cb5a02f33c013bd


          主站蜘蛛池模板: 国产精品电影一区二区三区| 一区在线免费观看| 亚洲日韩中文字幕无码一区| 国产一区二区草草影院| 怡红院一区二区三区| 大屁股熟女一区二区三区| 亚洲一区二区三区免费在线观看| 成人区人妻精品一区二区不卡| 国产av福利一区二区三巨| 亚洲一区免费在线观看| 亚洲国产综合无码一区二区二三区| 亚洲国产综合精品中文第一区| 人妻AV一区二区三区精品 | 一区二区三区四区无限乱码 | 日本精品一区二区久久久| 精品国产免费一区二区三区| 日本一区二区三区四区视频| 国产品无码一区二区三区在线| 国产情侣一区二区三区| 日本一区二区三区免费高清| 国产一区二区三区久久| 日韩精品一区二区三区在线观看| 末成年女A∨片一区二区| 国内精品无码一区二区三区| 午夜福利无码一区二区| 亚洲av无码一区二区三区人妖| 国产av天堂一区二区三区| 亚洲国产精品一区二区九九 | 天堂一区人妻无码| 国产一区二区电影在线观看| 精品国产亚洲第一区二区三区| 精品深夜AV无码一区二区| 国产日韩一区二区三免费高清| 国产在线精品一区在线观看| 日本免费一区二区三区四区五六区| 国产美女视频一区| 一区二区三区免费精品视频 | 亚洲色精品vr一区二区三区| 国产丝袜视频一区二区三区| 中文字幕AV一区中文字幕天堂| 91成人爽a毛片一区二区|