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 精品久久久久久国产,久久手机精品视频,久久亚洲国产

          整合營銷服務(wù)商

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

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

          HTML中的圖片區(qū)域鏈接方法詳解-零基礎(chǔ)自學(xué)網(wǎng)頁制作

          圖片添加區(qū)域鏈接的基本寫法


          首先我們看看效果:

          實(shí)現(xiàn)這樣的功能需要學(xué)習(xí)以下幾點(diǎn)內(nèi)容。

          1.認(rèn)識<img/><map><area/></map>基本結(jié)構(gòu)

          首先復(fù)制一個(gè)html框架,命名為“圖片區(qū)域鏈接.html”,示例代碼如下:

          <!DOCTYPE HTML> 
            <html>  
            <head>   
            <title>圖片區(qū)域鏈接</title>  
            <meta charset="utf-8">  
            </head>   
            <body>  
          
            </body>   
            </html>

          向<body></body>中添加<img><map><area/></map>基本結(jié)構(gòu),示例代碼如下:

           <body>  
           <img/>
             <map>
             <area/>
             </map>
           </body>  

          指定要添加區(qū)域鏈接的圖片的路徑,如下:

           <img src="img/image1.jpg"/>
             <map>
             <area/>
             </map>

          讓<img>標(biāo)簽通過<map>的名字來驅(qū)使<map>為自己工作。

          需要兩步,第一,給<map>起名字,name=“map”,為了兼容所有的瀏覽器,還要加上id=“map”(有的瀏覽器只認(rèn)id)。

          第二,讓<img>叫出<map>的名字或id,usemap="#map"。大家要注意,叫名字時(shí)要加#。這個(gè)在之前的教程中也經(jīng)常出現(xiàn)。

          示例代碼如下:

           <img src="img/image1.jpg" usemap="#map"/>
             <map name="map" id="map">
             <area/>
             </map>

          下面來劃分區(qū)域。

          2.為圖片劃分區(qū)域的方法

          <area>是用來劃分區(qū)域的標(biāo)簽,area也是“”區(qū)域“”的意思。

          默認(rèn)的shape(形狀)屬性有“矩形(rect)”、“圓形(circ)”、“多邊形(poly)”三個(gè)值。

          分別添加三個(gè)形狀,示例代碼如下:

           <img src="img/image1.jpg" usemap="#map"/>
             <map name="map" id="map">
             <area shape="rect"/>
             <area shape="circ"/>
             <area shape="poly"/>
             </map>

          下面我們就要為區(qū)域規(guī)定參數(shù),也就是在圖像上的位置和范圍大小。

          為<area>添加coords屬性可以指定區(qū)域的位置和范圍。

          如果shape="rect" 則coords由四個(gè)參數(shù)組成。例如coords="0,0,50,50"。從左到右,兩兩一組,組成兩個(gè)平面坐標(biāo),即(0,0)和(50,50),單位是“像素”,矩形區(qū)域如下:

          如果shape=“circ”,coords=“50,50,10”。(50,50)定義了圓心,10是半徑。如圖:

          如果shape=“poly”,coords的參數(shù)不少于3對!注意是“”!從左到右,兩個(gè)數(shù)就是一組坐標(biāo),三組坐標(biāo)可以確定一個(gè)三角形,多組坐標(biāo)可以確定多邊形。例如

          這組參數(shù)畫出了下圖中殲20的邊框線(600像素*400像素,如果圖像的長寬像素?cái)?shù)變了,參數(shù)就不正確了),如圖:

          這時(shí),大家會(huì)有一個(gè)問題:如何才能知道圖像中某個(gè)像素點(diǎn)的坐標(biāo)呢?

          3.使用Gimp軟件精準(zhǔn)定位圖片區(qū)域

          使用Gimp軟件可以解決這個(gè)問題。

          Gimp是一款類似于Photoshop的數(shù)字圖像處理軟件,不同的是,Gimp是開源免費(fèi)的。

          下載地址:https://www.gimp.org/

          雙擊安裝即可,注意選擇一下安裝目錄。

          完成安裝之后打開,界面如下:

          點(diǎn)擊“文件”找到“打開”:

          選擇要打開的圖片名字:

          點(diǎn)擊名稱后,右邊會(huì)有圖像預(yù)覽,點(diǎn)擊“打開”即可:

          打開后如圖:

          把鼠標(biāo)放到圖像的任意位置,看左下角:

          這里就會(huì)顯示我們鼠標(biāo)所在的像素坐標(biāo)數(shù)值。

          這樣我們就能方便地寫“poly”的coords了。

          請?jiān)诳臻e時(shí)找一張圖片演練一下吧!

          4.為區(qū)域添加鏈接

          在<area/>標(biāo)簽中添加href屬性即可指定鏈接路徑,如下:

          href="https://www.zhihu.com/question/284642168" 

          添加title屬性可以在鼠標(biāo)滑過鏈接區(qū)域時(shí)提示讀者,如下:

          title="殲20氣動(dòng)外形分析"

          今天的內(nèi)容結(jié)束了,圖像區(qū)域鏈接在使用時(shí)還有一些注意事項(xiàng),我們下次再詳細(xì)討論。

          使用碎片時(shí)間,學(xué)習(xí)完整知識!關(guān)注大魚師兄,一起精研技藝。

          目錄

          HTML序章(學(xué)習(xí)目的、對象、基本概念)——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          HTML是什么?——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          第一個(gè)HTML頁面如何寫?——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          HTML頁面中head標(biāo)簽有啥用?——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          初識meta標(biāo)簽與SEO——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          HTML中的元素使用方法1——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          HTML中的元素使用方法2——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          HTML元素中的屬性1——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          HTML元素中的屬性2(路徑詳解)——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          使用HTML添加表格1(基本元素)——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          使用HTML添加表格2(表格頭部與腳部)——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          使用HTML添加表格3(間距與顏色)——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          使用HTML添加表格4(行顏色與表格嵌套)——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          16進(jìn)制顏色表示與RGB色彩模型——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          HTML中的塊級元素與內(nèi)聯(lián)元素——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          初識HTML中的<div>塊元素——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          在HTML頁面中嵌入其他頁面的方法——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          封閉在家學(xué)網(wǎng)頁制作!為頁面嵌入PDF文件——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          HTML表單元素初識1——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          HTML表單元素初識2——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          HTML表單3(下拉列表、多行文字輸入)——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          HTML表單4(form的action、method屬性)——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          HTML列表制作講解——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          為HTML頁面添加視頻、音頻的方法——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          音視頻格式轉(zhuǎn)換神器與html視頻元素加字幕——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          HTML中使用<a>標(biāo)簽實(shí)現(xiàn)文本內(nèi)鏈接——零基礎(chǔ)自學(xué)網(wǎng)頁制作

          om-to-image庫可以幫你把dom節(jié)點(diǎn)轉(zhuǎn)換為圖片,它的核心原理很簡單,就是利用svg的foreignObject標(biāo)簽?zāi)芮度雋tml的特性,然后通過img標(biāo)簽加載svg,最后再通過canvas繪制img實(shí)現(xiàn)導(dǎo)出,好了,本文到此結(jié)束。

          另一個(gè)知名的html2canvas庫其實(shí)也支持這種方式。

          雖然原理很簡單,但是dom-to-image畢竟也有1000多行代碼,所以我很好奇它具體都做了哪些事情,本文就來詳細(xì)剖析一下,需要說明的是dom-to-image庫已經(jīng)六七年前沒有更新了,可能有點(diǎn)過時(shí),所以我們要看的是基于它修改的dom-to-image-more庫,這個(gè)庫修復(fù)了一些bug,以及增加了一些特性,接下來我們就來詳細(xì)了解一下。

          將節(jié)點(diǎn)轉(zhuǎn)換成圖片

          我們用的最多的api應(yīng)該就是toPng(node),所以以這個(gè)方法為入口:

          function toPng(node, options) {
              return draw(node, options).then(function (canvas) {
                  return canvas.toDataURL();
              });
          }
          

          toPng方法會(huì)調(diào)用draw方法,然后返回一個(gè)canvas,最后通過canvas的toDataURL方法獲取到圖片的base64格式的data:URL,我們就可以直接下載為圖片。

          看一下draw方法:

          function draw(domNode, options) {
              options = options || {};
              return toSvg(domNode, options)// 轉(zhuǎn)換成svg
                  .then(util.makeImage)// 轉(zhuǎn)換成圖片
                  .then(function (image) {// 通過canvas繪制圖片
                      // ...
                });
          }
          

          一共分為了三個(gè)步驟,一一來看。

          將節(jié)點(diǎn)轉(zhuǎn)換成svg

          toSvg方法如下:

          function toSvg(node, options) {
              const ownerWindow = domtoimage.impl.util.getWindow(node);
              options = options || {};
              copyOptions(options);
              let restorations = [];
              return Promise.resolve(node)
                  .then(ensureElement)// 檢查和包裝元素
                  .then(function (clonee) {// 深度克隆節(jié)點(diǎn)
                      return cloneNode(clonee, options, null, ownerWindow);
                  })
                  .then(embedFonts)// 嵌入字體
                  .then(inlineImages)// 內(nèi)聯(lián)圖片
                  .then(makeSvgDataUri)// svg轉(zhuǎn)data:URL
                  .then(restoreWrappers)// 恢復(fù)包裝元素
          }
          

          node就是我們要轉(zhuǎn)換成圖片的DOM節(jié)點(diǎn),首先調(diào)用了getWindow方法獲取window對象:

          function getWindow(node) {
              const ownerDocument = node ? node.ownerDocument : undefined;
              return (
                  (ownerDocument ? ownerDocument.defaultView : undefined) ||
                  global ||
                  window
              );
          }
          

          說實(shí)話前端寫了這么多年,但是ownerDocument和defaultView兩個(gè)屬性我完全沒用過,ownerDocument屬性會(huì)返回當(dāng)前節(jié)點(diǎn)的頂層的 document對象,而在瀏覽器中,defaultView屬性會(huì)返回當(dāng)前 document 對象所關(guān)聯(lián)的 window 對象,如果沒有,會(huì)返回 null。

          所以這里優(yōu)先通過我們傳入的DOM節(jié)點(diǎn)獲取window對象,可能是為了處理iframe嵌入之類的情況把。

          接下來合并了選項(xiàng)后,就通過Promise實(shí)例的then方法鏈?zhǔn)降恼{(diào)用一系列的方法,一一來看。

          檢查和包裝元素

          ensureElement方法如下:

          function ensureElement(node) {
              // ELEMENT_NODE:1
              if (node.nodeType === ELEMENT_NODE) return node;
              const originalChild = node;
              const originalParent = node.parentNode;
              const wrappingSpan = document.createElement('span');
              originalParent.replaceChild(wrappingSpan, originalChild);
              wrappingSpan.append(node);
              restorations.push({
                  parent: originalParent,
                  child: originalChild,
                  wrapper: wrappingSpan,
              });
              return wrappingSpan;
          }
          

          html節(jié)點(diǎn)的nodeType有如下類型:

          值為1也就是我們普通的html標(biāo)簽,其他的比如文本節(jié)點(diǎn)、注釋節(jié)點(diǎn)、document節(jié)點(diǎn)也是比較常用的,如果我們傳入的節(jié)點(diǎn)的類型為1,ensureElement方法什么也不做直接返回該節(jié)點(diǎn),否則會(huì)創(chuàng)建一個(gè)span標(biāo)簽替換掉原節(jié)點(diǎn),并把原節(jié)點(diǎn)添加到該span標(biāo)簽里,可以猜測這個(gè)主要是處理文本節(jié)點(diǎn),畢竟應(yīng)該沒有人會(huì)傳其他類型的節(jié)點(diǎn)進(jìn)行轉(zhuǎn)換了。

          同時(shí)它還把原節(jié)點(diǎn),原節(jié)點(diǎn)的父節(jié)點(diǎn),span標(biāo)簽都收集到restorations數(shù)組里,很明顯,這是為了后面進(jìn)行還原。

          克隆節(jié)點(diǎn)

          接下來執(zhí)行了cloneNode方法:

          cloneNode(clonee, options, null, ownerWindow)
          
          // 參數(shù):需要克隆的節(jié)點(diǎn)、選項(xiàng)、父節(jié)點(diǎn)的樣式、所屬window對象
          function cloneNode(node, options, parentComputedStyles, ownerWindow) {
              const filter = options.filter;
              if (
                  node === sandbox ||
                  util.isHTMLScriptElement(node) ||
                  util.isHTMLStyleElement(node) ||
                  util.isHTMLLinkElement(node) ||
                  (parentComputedStyles !== null && filter && !filter(node))
              ) {
                  return Promise.resolve();
              }
              return Promise.resolve(node)
                  .then(makeNodeCopy)// 處理canvas元素
                  .then(function (clone) {// 克隆子節(jié)點(diǎn)
                      return cloneChildren(clone, getParentOfChildren(node));
                  })
                  .then(function (clone) {// 處理克隆的節(jié)點(diǎn)
                      return processClone(clone, node);
                  });
          }
          

          先做了一堆判斷,如果是script、style、link標(biāo)簽,或者需要過濾掉的節(jié)點(diǎn),那么會(huì)直接返回。

          sandbox、parentComputedStyles后面會(huì)看到。

          接下來又調(diào)用了幾個(gè)方法,沒辦法,跟著它一起入棧把。

          處理canvas元素的克隆

          function makeNodeCopy(original) {
              if (util.isHTMLCanvasElement(original)) {
                  return util.makeImage(original.toDataURL());
              }
              return original.cloneNode(false);
          }
          

          如果元素是canvas,那么會(huì)通過makeImage方法將其轉(zhuǎn)換成img標(biāo)簽:

          function makeImage(uri) {
              if (uri === 'data:,') {
                  return Promise.resolve();
              }
              return new Promise(function (resolve, reject) {
                  const image = new Image();
                  if (domtoimage.impl.options.useCredentials) {
                      image.crossOrigin = 'use-credentials';
                  }
                  image.onload = function () {
                      if (window && window.requestAnimationFrame) {
                          // 解決 Firefox 的一個(gè)bug (webcompat/web-bugs#119834) 
                          // 需要等待一幀
                          window.requestAnimationFrame(function () {
                              resolve(image);
                          });
                      } else {
                          // 如果沒有window對象或者requestAnimationFrame方法,那么立即返回
                          resolve(image);
                      }
                  };
                  image.onerror = reject;
                  image.src = uri;
              });
          }
          

          crossOrigin屬性用于定義一些元素如何處理跨域請求,主要有兩個(gè)取值:

          anonymous:元素的跨域資源請求不需要憑證標(biāo)志設(shè)置。

          use-credentials:元素的跨域資源請求需要憑證標(biāo)志設(shè)置,意味著該請求需要提供憑證。

          除了use-credentials,給crossOrigin設(shè)置其他任何值都會(huì)解析成anonymous,為了解決跨域問題,我們一般都會(huì)設(shè)置成anonymous,這個(gè)就相當(dāng)于告訴服務(wù)器,你不需要返回任何非匿名信息過來,例如cookie,所以肯定是安全的。不過在使用這兩個(gè)值時(shí)都需要服務(wù)端返回Access-Control-Allow-Credentials響應(yīng)頭,否則肯定無法跨域使用的。

          非canvas元素的其他元素,會(huì)直接調(diào)用它們的cloneNode方法進(jìn)行克隆,參數(shù)傳了false,代表只克隆自身,不克隆子節(jié)點(diǎn)。

          克隆子節(jié)點(diǎn)

          接下來調(diào)用了cloneChildren方法:

          cloneChildren(clone, getParentOfChildren(node));
          

          getParentOfChildren方法如下:

          function getParentOfChildren(original) {
              // 如果該節(jié)點(diǎn)是Shadow DOM的附加節(jié)點(diǎn),那么返回附加的Shadow DOM的根節(jié)點(diǎn)
              if (util.isElementHostForOpenShadowRoot(original)) {
                  return original.shadowRoot; 
              }
              return original;
          }
          function isElementHostForOpenShadowRoot(value) {
              return isElement(value) && value.shadowRoot !== null;
          }
          

          這里涉及到了shadow DOM,有必要先簡單了解一下。

          shadow DOM是一種封裝技術(shù),可以將標(biāo)記結(jié)構(gòu)、樣式和行為隱藏起來,比如我們熟悉的video標(biāo)簽,我們看到的只是一個(gè)video標(biāo)簽,但實(shí)際上它里面有很多我們看不到的元素,這個(gè)特性一般會(huì)和Web components結(jié)合使用,也就是可以創(chuàng)建自定義元素,就和Vue和React組件一樣。

          先了解一些術(shù)語:

          Shadow host:一個(gè)常規(guī) DOM 節(jié)點(diǎn),Shadow DOM 會(huì)被附加到這個(gè)節(jié)點(diǎn)上。

          Shadow tree:Shadow DOM 內(nèi)部的 DOM 樹。

          Shadow boundary:Shadow DOM 結(jié)束的地方,也是常規(guī) DOM 開始的地方。

          Shadow root: Shadow tree 的根節(jié)點(diǎn)。

          一個(gè)普通的DOM元素可以使用attachShadow方法來添加shadow DOM:

          let shadow = div.attachShadow({ mode: "open" });
          

          這樣就可以給div元素附加一個(gè)shadow DOM,然后我們可以和創(chuàng)建普通元素一樣創(chuàng)建任何元素添加到shadow下:

          let para = document.createElement('p');
          shadow.appendChild(para);
          

          當(dāng)mode設(shè)為open,我們就可以通過div.shadowRoot獲取到Shadow DOM,如果設(shè)置的是closed,那么外部就獲取不到。

          所以前面的getParentOfChildren方法會(huì)判斷當(dāng)前節(jié)點(diǎn)是不是一個(gè)Shadow host節(jié)點(diǎn),是的話就返回它內(nèi)部的Shadow root節(jié)點(diǎn),否則返回自身。

          回到cloneChildren方法,它接收兩個(gè)參數(shù):克隆的節(jié)點(diǎn)、原節(jié)點(diǎn)。

          function cloneChildren(clone, original) {
              // 獲取子節(jié)點(diǎn),如果原節(jié)點(diǎn)是slot節(jié)點(diǎn),那么會(huì)返回slot內(nèi)的節(jié)點(diǎn),
              const originalChildren = getRenderedChildren(original);
              let done = Promise.resolve();
              if (originalChildren.length !== 0) {
                  // 獲取原節(jié)點(diǎn)的計(jì)算樣式,如果原節(jié)點(diǎn)是shadow root節(jié)點(diǎn),那么會(huì)獲取它所附加到的普通元素的樣式
                  const originalComputedStyles = getComputedStyle(
                      getRenderedParent(original)
                  );
                  // 遍歷子節(jié)點(diǎn)
                  util.asArray(originalChildren).forEach(function (originalChild) {
                      done = done.then(function () {
                          // 遞歸調(diào)用cloneNode方法
                          return cloneNode(
                              originalChild,
                              options,
                              originalComputedStyles,
                              ownerWindow
                          ).then(function (clonedChild) {
                              // 克隆完后的子節(jié)點(diǎn)添加到該節(jié)點(diǎn)
                              if (clonedChild) {
                                  clone.appendChild(clonedChild);
                              }
                          });
                      });
                  });
              }
              return done.then(function () {
                  return clone;
              });
          }
          

          首先通過getRenderedChildren方法獲取子節(jié)點(diǎn):

          function getRenderedChildren(original) {
              // 如果是slot元素,那么通過assignedNodes方法返回該插槽中的節(jié)點(diǎn)
              if (util.isShadowSlotElement(original)) {
                  return original.assignedNodes();
              }
              // 普通元素直接通過childNodes獲取子節(jié)點(diǎn)
              return original.childNodes;
          }
          // 判斷是否是html slot元素
          function isShadowSlotElement(value) {
              return (
                  isInShadowRoot(value) && value instanceof getWindow(value).HTMLSlotElement
              );
          }
          // 判斷一個(gè)節(jié)點(diǎn)是否處于shadow DOM樹中
          function isInShadowRoot(value) {
              // 如果是普通節(jié)點(diǎn),getRootNode方法會(huì)返回document對象,如果是Shadow DOM,那么會(huì)返回shadow root
              return (
                  value !== null &&
                  Object.prototype.hasOwnProperty.call(value, 'getRootNode') &&
                  isShadowRoot(value.getRootNode())
              );
          }
          // 判斷是否是shadow DOM的根節(jié)點(diǎn)
          function isShadowRoot(value) {
              return value instanceof getWindow(value).ShadowRoot;
          }
          

          這一連串的判斷,如果對于shadow DOM不熟悉的話大概率很難看懂,不過沒關(guān)系,跳過這部分也可以,反正就是獲取子節(jié)點(diǎn)。

          獲取到子節(jié)點(diǎn)后又調(diào)用了如下方法:

          const originalComputedStyles = getComputedStyle(
              getRenderedParent(original)
          );
          function getRenderedParent(original) {
              // 如果該節(jié)點(diǎn)是shadow root,那么返回它附加到的普通的DOM節(jié)點(diǎn)
              if (util.isShadowRoot(original)) {
                  return original.host;
              }
              return original;
          }
          

          調(diào)用getComputedStyle獲取原節(jié)點(diǎn)的樣式,這個(gè)方法其實(shí)就是window.getComputedStyle方法,會(huì)返回節(jié)點(diǎn)的所有樣式和值。

          接下來就是遍歷子節(jié)點(diǎn),然后對每個(gè)子節(jié)點(diǎn)再次調(diào)用cloneNode方法,只不過會(huì)把原節(jié)點(diǎn)的樣式也傳進(jìn)去。對于子元素又會(huì)遞歸處理它們的子節(jié)點(diǎn),這樣就能深度克隆完整棵DOM樹。

          處理克隆的節(jié)點(diǎn)

          對于每個(gè)克隆節(jié)點(diǎn),又調(diào)用了processClone(clone, node)方法:

          function processClone(clone, original) {
              // 如果不是普通節(jié)點(diǎn),或者是slot節(jié)點(diǎn),那么直接返回
              if (!util.isElement(clone) || util.isShadowSlotElement(original)) {
                  return Promise.resolve(clone);
              }
              return Promise.resolve()
                  .then(cloneStyle)// 克隆樣式
                  .then(clonePseudoElements)// 克隆偽元素
                  .then(copyUserInput)// 克隆輸入框
                  .then(fixSvg)// 修復(fù)svg
                  .then(function () {
                      return clone;
                  });
          }
          

          又是一系列的操作,穩(wěn)住,我們繼續(xù)。

          克隆樣式

          function cloneStyle() {
              copyStyle(original, clone);
          }
          

          調(diào)用了copyStyle方法,傳入原節(jié)點(diǎn)和克隆節(jié)點(diǎn):

          function copyStyle(sourceElement, targetElement) {
              const sourceComputedStyles = getComputedStyle(sourceElement);
              if (sourceComputedStyles.cssText) {
                 // ...
              } else {
                 // ...
              }
          }
          

          window.getComputedStyle方法返回的是一個(gè)CSSStyleDeclaration對象,和我們使用div.style獲取到的對象類型是一樣的,但是div.style對象只能獲取到元素的內(nèi)聯(lián)樣式,使用div.style.color = '#fff'設(shè)置的也能獲取到,因?yàn)檫@種方式設(shè)置的也是內(nèi)聯(lián)樣式,其他樣式是獲取不到的,但是window.getComputedStyle能獲取到所有css樣式。

          div.style.cssText屬性我們都用過,可以獲取和批量設(shè)置內(nèi)聯(lián)樣式,如果要設(shè)置多個(gè)樣式,比單個(gè)調(diào)用div.style.xxx方便一點(diǎn),但是cssText會(huì)覆蓋整個(gè)內(nèi)聯(lián)樣式,比如下面的方式設(shè)置的字號是會(huì)丟失的,內(nèi)聯(lián)樣式最終只有color:

          div.style.fontSize = '23px'
          div.style.cssText = 'color: rgb(102, 102, 102)'
          

          但是window.getComputedStyle方法返回的對象的cssText和div.style.cssText不是同一個(gè)東西,即使有內(nèi)聯(lián)樣式,window.getComputedStyle方法返回對象的cssText值也是空,并且它無法修改,所以不清楚什么情況下它才會(huì)有值。

          假設(shè)有值的話,接下來的代碼我也不是很能理解:

          if (sourceComputedStyles.cssText) {
              targetElement.style.cssText = sourceComputedStyles.cssText;
              copyFont(sourceComputedStyles, targetElement.style);
          }
          
          function copyFont(source, target) {
              target.font = source.font;
              target.fontFamily = source.fontFamily;
              // ...
          }
          

          為什么不直接把原節(jié)點(diǎn)的style.cssText復(fù)制給克隆節(jié)點(diǎn)的style.cssText呢,另外為啥文本相關(guān)的樣式又要單獨(dú)設(shè)置一遍呢,無法理解。

          我們看看另外一個(gè)分支:

          else {
              copyUserComputedStyleFast(
                  options,
                  sourceElement,
                  sourceComputedStyles,
                  parentComputedStyles,
                  targetElement
              );
              // ...
          }
          

          先調(diào)用了copyUserComputedStyleFast方法,這個(gè)方法內(nèi)部非常復(fù)雜,就不把具體代碼放出來了,大致介紹一下它都做了什么:

          1.首先會(huì)獲取原節(jié)點(diǎn)的所謂的默認(rèn)樣式,這個(gè)步驟也比較復(fù)雜:

          1.1.先獲取原節(jié)點(diǎn)及祖先節(jié)點(diǎn)的元素標(biāo)簽列表,其實(shí)就是一個(gè)向上遞歸的過程,不過存在終止條件,就是當(dāng)遇到塊級元素的祖先節(jié)點(diǎn)。比如原節(jié)點(diǎn)是一個(gè)span標(biāo)簽,它的父節(jié)點(diǎn)也是一個(gè)span,再上一個(gè)父節(jié)點(diǎn)是一個(gè)div,那么獲取到的標(biāo)簽列表就是[span, span, div]。

          ? 1.2.接下來會(huì)創(chuàng)建一個(gè)沙箱,也就是一個(gè)iframe,這個(gè)iframe的DOCTYPE和charset會(huì)設(shè)置成和當(dāng)前頁面的一樣。

          ? 1.3.再接下來會(huì)根據(jù)前面獲取到的標(biāo)簽列表,在iframe中創(chuàng)建對應(yīng)結(jié)構(gòu)的DOM節(jié)點(diǎn),也就是會(huì)創(chuàng)建這樣一棵DOM樹:div -> span -> span。并且會(huì)給最后一個(gè)節(jié)點(diǎn)添加一個(gè)零寬字符的文本,并返回這個(gè)節(jié)點(diǎn)。

          ? 1.4.使用iframe的window.getComputedStyle方法獲取上一步返回節(jié)點(diǎn)的樣式,對于width和height會(huì)設(shè)置成auto。

          ? 1.5.刪除iframe里前面創(chuàng)建的節(jié)點(diǎn)。

          ? 16.返回1.4步獲取到的樣式對象。

          2.遍歷原節(jié)點(diǎn)的樣式,也就是sourceComputedStyles對象,對于每一個(gè)樣式屬性,都會(huì)獲取到三個(gè)值:sourceValue、defaultValue、parentValue,分別來自原節(jié)點(diǎn)的樣式對象sourceComputedStyles、第一步獲取到的默認(rèn)樣式對象、父節(jié)點(diǎn)的樣式對象parentComputedStyles,然后會(huì)做如下判斷:

          if (
              sourceValue !== defaultValue ||
              (parentComputedStyles && sourceValue !== parentValue)
          ) {
              // 樣式優(yōu)先級,比如important
              const priority = sourceComputedStyles.getPropertyPriority(name);
              // 將樣式設(shè)置到克隆節(jié)點(diǎn)的style對象上
              setStyleProperty(targetStyle, name, sourceValue, priority);
          }
          

          如果原節(jié)點(diǎn)的某個(gè)樣式值和默認(rèn)的樣式值不一樣,并且和父節(jié)點(diǎn)的也不一樣,那么就需要給克隆的節(jié)點(diǎn)手動(dòng)設(shè)置成內(nèi)聯(lián)樣式,否則其實(shí)就是繼承樣式或者默認(rèn)樣式,就不用管了,不得不說,還是挺巧妙的。

          copyUserComputedStyleFast方法執(zhí)行完后還做了如下操作:

          if (parentComputedStyles === null) {
              [
                  'inset-block',
                  'inset-block-start',
                  'inset-block-end',
              ].forEach((prop) => targetElement.style.removeProperty(prop));
              ['left', 'right', 'top', 'bottom'].forEach((prop) => {
                  if (targetElement.style.getPropertyValue(prop)) {
                      targetElement.style.setProperty(prop, '0px');
                  }
              });
          }
          

          對于我們傳入的節(jié)點(diǎn),parentComputedStyles是null,本質(zhì)相當(dāng)于根節(jié)點(diǎn),所以直接移除它的位置信息,防止發(fā)生偏移。

          克隆偽元素

          克隆完樣式,接下來就是處理偽元素了:

          function clonePseudoElements() {
              const cloneClassName = util.uid();
              [':before', ':after'].forEach(function (element) {
                  clonePseudoElement(element);
              });
          }
          

          分別調(diào)用clonePseudoElement方法處理兩種偽元素:

          function clonePseudoElement(element) {
              // 獲取原節(jié)點(diǎn)偽元素的樣式
              const style = getComputedStyle(original, element);
              // 獲取偽元素的content
              const content = style.getPropertyValue('content');
              // 如果偽元素的內(nèi)容為空就直接返回
              if (content === '' || content === 'none') {
                  return;
              }
              // 獲取克隆節(jié)點(diǎn)的類名
              const currentClass = clone.getAttribute('class') || '';
              // 給克隆元素增加一個(gè)唯一的類名
              clone.setAttribute('class', `${currentClass} ${cloneClassName}`);
           // 創(chuàng)建一個(gè)style標(biāo)簽
              const styleElement = document.createElement('style');
              // 插入偽元素的樣式
              styleElement.appendChild(formatPseudoElementStyle());
              // 將樣式標(biāo)簽添加到克隆節(jié)點(diǎn)內(nèi)
              clone.appendChild(styleElement);
          }
          

          window.getComputedStyle方法是可以獲取元素的偽元素的樣式的,通過第二個(gè)參數(shù)指定要獲取的偽元素即可。

          如果偽元素的content為空就不管了,總感覺有點(diǎn)不妥,畢竟我經(jīng)常會(huì)用偽元素渲染一些三角形,content都是設(shè)置成空的。

          如果不為空,那么會(huì)給克隆的節(jié)點(diǎn)新增一個(gè)唯一的類名,并且創(chuàng)建一個(gè)style標(biāo)簽添加到克隆節(jié)點(diǎn)內(nèi),這個(gè)style標(biāo)簽里會(huì)插入偽元素的樣式,通過formatPseudoElementStyle方法獲取偽元素的樣式字符串:

          function formatPseudoElementStyle() {
              const selector = `.${cloneClassName}:${element}`;
              // style為原節(jié)點(diǎn)偽元素的樣式對象
              const cssText = style.cssText
              ? formatCssText()
              : formatCssProperties();
          
              return document.createTextNode(`${selector}{${cssText}}`);
          }
          

          如果樣式對象的cssText有值,那么調(diào)用formatCssText方法:

          function formatCssText() {
              return `${style.cssText} content: ${content};`;
          }
          

          但是前面說了,這個(gè)屬性一般都是沒值的,所以會(huì)走formatCssProperties方法:

          function formatCssProperties() {
              const styleText = util
                  .asArray(style)
                  .map(formatProperty)
                  .join('; ');
              return `${styleText};`;
          
              function formatProperty(name) {
                  const propertyValue = style.getPropertyValue(name);
                  const propertyPriority = style.getPropertyPriority(name)
                  ? ' !important'
                  : '';
                  return `${name}: ${propertyValue}${propertyPriority}`;
              }
          }
          

          很簡單,遍歷樣式對象,然后拼接成css的樣式字符串。

          克隆輸入框

          對于輸入框的處理很簡單:

          function copyUserInput() {
              if (util.isHTMLTextAreaElement(original)) {
                  clone.innerHTML = original.value;
              }
              if (util.isHTMLInputElement(original)) {
                  clone.setAttribute('value', original.value);
              }
          }
          

          如果是textarea或者input元素,直接將原節(jié)點(diǎn)的值設(shè)置到克隆后的元素上即可。但是我測試發(fā)現(xiàn)克隆輸入框也會(huì)把它的值給克隆過去,所以這一步可能沒有必要。

          修復(fù)svg

          最后就是處理svg節(jié)點(diǎn):

          function fixSvg() {
              if (util.isSVGElement(clone)) {
                  clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
                  if (util.isSVGRectElement(clone)) {
                      ['width', 'height'].forEach(function (attribute) {
                          const value = clone.getAttribute(attribute);
                          if (value) {
                              clone.style.setProperty(attribute, value);
                          }
                      });
                  }
              }
          }
          

          給svg節(jié)點(diǎn)添加命名空間,另外對于rect節(jié)點(diǎn),還把寬高的屬性設(shè)置成對應(yīng)的樣式,這個(gè)是何原因,我們也不得而知。

          到這里,節(jié)點(diǎn)的克隆部分就結(jié)束了,不得不說,還是有點(diǎn)復(fù)雜的,很多操作其實(shí)我們也沒有看懂為什么要這么做,開發(fā)一個(gè)庫就是這樣,要處理很多邊界和異常情況,這個(gè)只有遇到了才知道為什么。

          嵌入字體

          節(jié)點(diǎn)克隆完后接下來會(huì)處理字體:

          function embedFonts(node) {
              return fontFaces.resolveAll().then(function (cssText) {
                  if (cssText !== '') {
                      const styleNode = document.createElement('style');
                      node.appendChild(styleNode);
                      styleNode.appendChild(document.createTextNode(cssText));
                  }
                  return node;
              });
          }
          

          調(diào)用resolveAll方法,會(huì)返回一段css字符串,然后創(chuàng)建一個(gè)style標(biāo)簽添加到克隆的節(jié)點(diǎn)內(nèi),接下來看看resolveAll方法都做了什么:

          function resolveAll() {
              return readAll()
                  // ...
          }
          

          又調(diào)用了readAll方法:

          function readAll() {
              return Promise.resolve(util.asArray(document.styleSheets))
                  .then(getCssRules)
                  .then(selectWebFontRules)
                  .then(function (rules) {
                      return rules.map(newWebFont);
                  });
          }
          

          document.styleSheets屬性可以獲取到文檔中所有的style標(biāo)簽和通過link標(biāo)簽引入的樣式,結(jié)果是一個(gè)類數(shù)組,數(shù)組的每一項(xiàng)是一個(gè)CSSStyleSheet對象。

          function getCssRules(styleSheets) {
              const cssRules = [];
              styleSheets.forEach(function (sheet) {
                  if (
                      Object.prototype.hasOwnProperty.call(
                          Object.getPrototypeOf(sheet),
                          'cssRules'
                      )
                  ) {
                      util.asArray(sheet.cssRules || []).forEach(
                          cssRules.push.bind(cssRules)
                      );
                  }
              });
              return cssRules;
          }
          

          通過CSSStyleSheet對象的cssRules屬性可以獲取到具體的css規(guī)則,cssRules的每一項(xiàng)也就是我們寫的一條css語句:

          function selectWebFontRules(cssRules) {
              return cssRules
                  .filter(function (rule) {
                      return rule.type === CSSRule.FONT_FACE_RULE;
                  })
                  .filter(function (rule) {
                      return inliner.shouldProcess(rule.style.getPropertyValue('src'));
                  });
          }
          

          遍歷所有的css語句,找出其中的@font-face語句,shouldProcess方法會(huì)判斷@font-face語句的src屬性是否存在url()值,找出了所有存在的字體規(guī)則后會(huì)遍歷它們調(diào)用newWebFont方法:

          function newWebFont(webFontRule) {
              return {
                  resolve: function resolve() {
                      const baseUrl = (webFontRule.parentStyleSheet || {}).href;
                      return inliner.inlineAll(webFontRule.cssText, baseUrl);
                  },
                  src: function () {
                      return webFontRule.style.getPropertyValue('src');
                  },
              };
          }
          

          inlineAll方法會(huì)找出@font-face語句中定義的所有字體的url,然后通過XMLHttpRequest發(fā)起請求,將字體文件轉(zhuǎn)換成data:URL形式,然后替換css語句中的url,核心就是使用下面這個(gè)正則匹配和替換。

          const URL_REGEX = /url\(['"]?([^'"]+?)['"]?\)/g;
          

          繼續(xù)resolveAll方法:

          function resolveAll() {
              return readAll()
                  .then(function (webFonts) {
                      return Promise.all(
                          webFonts.map(function (webFont) {
                              return webFont.resolve();
                          })
                      );
                  })
                  .then(function (cssStrings) {
                      return cssStrings.join('\n');
                  });
          }
          

          將所有@font-face語句的遠(yuǎn)程字體url都轉(zhuǎn)換成data:URL形式后再將它們拼接成css字符串即可完成嵌入字體的操作。

          說實(shí)話,Promise鏈太長,看著容易暈。

          內(nèi)聯(lián)圖片

          內(nèi)聯(lián)完了字體后接下來就是內(nèi)聯(lián)圖片:

          function inlineImages(node) {
              return images.inlineAll(node).then(function () {
                  return node;
              });
          }
          

          處理圖片的inlineAll方法如下:

          function inlineAll(node) {
              if (!util.isElement(node)) {
                  return Promise.resolve(node);
              }
              return inlineCSSProperty(node).then(function () {
                  // ...
              });
          }
          

          inlineCSSProperty方法會(huì)判斷節(jié)點(diǎn)background和 background-image屬性是否設(shè)置了圖片,是的話也會(huì)和嵌入字體一樣將遠(yuǎn)程圖片轉(zhuǎn)換成data:URL嵌入:

          function inlineCSSProperty(node) {
              const properties = ['background', 'background-image'];
              const inliningTasks = properties.map(function (propertyName) {
                  const value = node.style.getPropertyValue(propertyName);
                  const priority = node.style.getPropertyPriority(propertyName);
                  if (!value) {
                      return Promise.resolve();
                  }
                  // 如果設(shè)置了背景圖片,那么也會(huì)調(diào)用inliner.inlineAll方法將遠(yuǎn)程url的形式轉(zhuǎn)換成data:URL形式
                  return inliner.inlineAll(value).then(function (inlinedValue) {
                      // 將樣式設(shè)置成轉(zhuǎn)換后的值
                      node.style.setProperty(propertyName, inlinedValue, priority);
                  });
              });
              return Promise.all(inliningTasks).then(function () {
                  return node;
              });
          }
          

          處理完節(jié)點(diǎn)的背景圖片后:

          function inlineAll(node) {
              return inlineCSSProperty(node).then(function () {
                  if (util.isHTMLImageElement(node)) {
                      return newImage(node).inline();
                  } else {
                      return Promise.all(
                          util.asArray(node.childNodes).map(function (child) {
                              return inlineAll(child);
                          })
                      );
                  }
              });
          }
          

          會(huì)檢查節(jié)點(diǎn)是否是圖片節(jié)點(diǎn),是的話會(huì)調(diào)用newImage方法處理,這個(gè)方法也很簡單,也是發(fā)個(gè)請求獲取圖片數(shù)據(jù),然后將它轉(zhuǎn)換成data:URL設(shè)置回圖片的src。

          如果是其他節(jié)點(diǎn),那么就遞歸處理子節(jié)點(diǎn)。

          將svg轉(zhuǎn)換成data:URL

          圖片也處理完了接下來就可以將svg轉(zhuǎn)換成data:URL了:

          function makeSvgDataUri(node) {
              let width = options.width || util.width(node);
              let height = options.height || util.height(node);
          
              return Promise.resolve(node)
                  .then(function (svg) {
                      svg.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
                      return new XMLSerializer().serializeToString(svg);
                  })
                  .then(util.escapeXhtml)
                  .then(function (xhtml) {
                      const foreignObjectSizing =
                            (util.isDimensionMissing(width)
                             ? ' width="100%"'
                             : ` width="${width}"`) +
                            (util.isDimensionMissing(height)
                             ? ' height="100%"'
                             : ` height="${height}"`);
                      const svgSizing =
                            (util.isDimensionMissing(width) ? '' : ` width="${width}"`) +
                            (util.isDimensionMissing(height) ? '' : ` height="${height}"`);
                      return `<svg xmlns="http://www.w3.org/2000/svg"${svgSizing}> 
              <foreignObject${foreignObjectSizing}>${xhtml}</foreignObject>
             </svg>`;
                  })
                  .then(function (svg) {
                      return `data:image/svg+xml;charset=utf-8,${svg}`;
                  });
          }
          

          其中的isDimensionMissing方法就是判斷是否是不合法的數(shù)字。

          主要做了四件事。

          一是給節(jié)點(diǎn)添加命名空間,并使用XMLSerializer對象來將DOM節(jié)點(diǎn)序列化成字符串。

          二是轉(zhuǎn)換DOM字符串中的一些字符:

          function escapeXhtml(string) {
              return string.replace(/%/g, '%25').replace(/#/g, '%23').replace(/\n/g, '%0A');
          }
          

          第三步就是拼接svg字符串了,將序列化后的字符串使用foreignObject標(biāo)簽包裹,同時(shí)會(huì)計(jì)算一下DOM節(jié)點(diǎn)的寬高設(shè)置到svg上。

          最后一步是拼接成data:URL的形式。

          恢復(fù)包裝元素

          在最開始的【檢查和包裝元素】步驟會(huì)替換掉節(jié)點(diǎn)類型不為1的節(jié)點(diǎn),這一步就是用來恢復(fù)這個(gè)操作:

          function restoreWrappers(result) {
              while (restorations.length > 0) {
                  const restoration = restorations.pop();
                  restoration.parent.replaceChild(restoration.child, restoration.wrapper);
              }
              return result;
          }
          

          這一步結(jié)束后將節(jié)點(diǎn)轉(zhuǎn)換成svg的操作就結(jié)束了。

          將svg轉(zhuǎn)換成圖片

          現(xiàn)在我們可以回到draw方法:

          function draw(domNode, options) {
                  options = options || {};
                  return toSvg(domNode, options)
                      .then(util.makeImage)
                      .then(function (image) {
                          // ...
                      });
          }
          

          獲取到了svg的data:URL后會(huì)調(diào)用makeImage方法將它轉(zhuǎn)換成圖片,這個(gè)方法前面我們已經(jīng)看過了,這里就不重復(fù)說了。

          將圖片通過canvas導(dǎo)出

          繼續(xù)draw方法:

          function draw(domNode, options) {
                  options = options || {};
                  return toSvg(domNode, options)
                      .then(util.makeImage)
                      .then(function (image) {
                          const scale = typeof options.scale !== 'number' ? 1 : options.scale;
                          const canvas = newCanvas(domNode, scale);
                          const ctx = canvas.getContext('2d');
                          ctx.msImageSmoothingEnabled = false;// 禁用圖像平滑
                          ctx.imageSmoothingEnabled = false;// 禁用圖像平滑
                          if (image) {
                              ctx.scale(scale, scale);
                              ctx.drawImage(image, 0, 0);
                          }
                          return canvas;
                      });
          }
          

          先調(diào)用newCanvas方法創(chuàng)建一個(gè)canvas:

          function newCanvas(node, scale) {
              let width = options.width || util.width(node);
              let height = options.height || util.height(node);
           // 如果寬度高度都沒有,那么默認(rèn)設(shè)置成300
              if (util.isDimensionMissing(width)) {
                  width = util.isDimensionMissing(height) ? 300 : height * 2.0;
              }
              // 如果高度沒有,那么默認(rèn)設(shè)置成寬度的一半
              if (util.isDimensionMissing(height)) {
                  height = width / 2.0;
              }
           // 創(chuàng)建canvas
              const canvas = document.createElement('canvas');
              canvas.width = width * scale;
              canvas.height = height * scale;
           // 設(shè)置背景顏色
              if (options.bgcolor) {
                  const ctx = canvas.getContext('2d');
                  ctx.fillStyle = options.bgcolor;
                  ctx.fillRect(0, 0, canvas.width, canvas.height);
              }
              return canvas;
          }
          

          把svg圖片繪制到canvas上后,就可以通過canvas.toDataURL()方法轉(zhuǎn)換成圖片的data:URL,你可以渲染到頁面,也可以直接進(jìn)行下載。

          總結(jié)

          本文通過源碼詳細(xì)介紹了dom-to-image-more的原理,核心就是克隆節(jié)點(diǎn)和節(jié)點(diǎn)樣式,內(nèi)聯(lián)字體、背景圖片、圖片,然后通過svg的foreignObject標(biāo)簽嵌入克隆后的節(jié)點(diǎn),最后將svg轉(zhuǎn)換成圖片,圖片繪制到canvas上進(jìn)行導(dǎo)出。

          可以看到源碼中大量的Promise,很多不是異步的邏輯也會(huì)通過then方法來進(jìn)行管道式調(diào)用,大部分情況會(huì)讓代碼很清晰,一眼就知道大概做了什么事情,但是部分地方串聯(lián)了太長,反倒不太容易理解。

          限于篇幅,源碼中其實(shí)還要很多有意思的細(xì)節(jié)沒有介紹,比如為了修改iframe的DOCTYPE和charset,居然寫了三種方式,雖然我覺得第一種就夠了,又比如獲取節(jié)點(diǎn)默認(rèn)樣式的方式,通過iframe創(chuàng)建同樣標(biāo)簽同樣層級的元素,說實(shí)話我是從來沒見過,再比如解析css中的字體的url時(shí)用的是如下方法:

          function resolveUrl(url, baseUrl) {
              const doc = document.implementation.createHTMLDocument();
              const base = doc.createElement('base');
              doc.head.appendChild(base);
              const a = doc.createElement('a');
              doc.body.appendChild(a);
              base.href = baseUrl;
              a.href = url;
              return a.href;
          }
          

          base標(biāo)簽我也是從來沒有見過。等等。

          所以看源碼還是挺有意思的一件事,畢竟平時(shí)寫業(yè)務(wù)代碼局限性太大了,很多東西都了解不到,強(qiáng)烈推薦各位去閱讀一下。

          前面一篇文章:「高頻面試題」瀏覽器從輸入url到頁面展示中間發(fā)生了什么 中,我們有對瀏覽器的渲染流程做了一個(gè)概括性的介紹,今天這篇文章我們將深入學(xué)習(xí)這部分內(nèi)容。

          對于很多前端開發(fā)來說,平常做工主要專注于業(yè)務(wù)開發(fā),對瀏覽器的渲染階段可能不是很了解。實(shí)際上這個(gè)階段很重要,了解瀏覽器的渲染過程,能讓我們知道我們寫的HTML、CSS、JS代碼是如何被解析,并最終渲染成一個(gè)頁面的,在頁面性能優(yōu)化的時(shí)候有相應(yīng)的解決思路。

          我們先來看一個(gè)問題:

          HTML、CSS、JS文件在瀏覽器中是如何轉(zhuǎn)化成頁面的?

          如果你回答不上來,那就往下看吧。

          按照渲染的時(shí)間順序,渲染過程可以分為下面幾個(gè)子階段:構(gòu)建DOM樹、樣式計(jì)算、布局階段、分層、柵格化和合成顯示。

          下面詳細(xì)看下每個(gè)階段都做了哪些事情。

          1. 構(gòu)建DOM樹

          HTML文檔描述一個(gè)頁面的結(jié)構(gòu),但是瀏覽器無法直接理解和使用HTML,所以需要通過HTML解析器將HTML轉(zhuǎn)換成瀏覽器能夠理解的結(jié)構(gòu)——DOM樹。

          HTML文檔中所有內(nèi)容皆為節(jié)點(diǎn),各節(jié)點(diǎn)之間有層級關(guān)系,彼此相連,構(gòu)成DOM樹。

          構(gòu)建過程:讀取HTML文檔的字節(jié)(Bytes),將字節(jié)轉(zhuǎn)換成字符(Chars),依據(jù)字符確定標(biāo)簽(Tokens),將標(biāo)簽轉(zhuǎn)換成節(jié)點(diǎn)(Nodes),以節(jié)點(diǎn)為基準(zhǔn)構(gòu)建DOM樹。參考下圖:

          打開Chrome的開發(fā)者工具,在控制臺輸入 document 后回車,就能看到一個(gè)完整的DOM樹結(jié)構(gòu),如下圖所示:

          在控制臺打印出來的DOM結(jié)構(gòu)和HTML內(nèi)容幾乎一樣,但和HTML不同的是,DOM是保存在內(nèi)存中的樹狀結(jié)構(gòu),可以通過JavaScript來查詢或修改其內(nèi)容。

          2. 樣式計(jì)算

          樣式計(jì)算這個(gè)階段,是為了計(jì)算出DOM節(jié)點(diǎn)中每個(gè)元素的表現(xiàn)樣式。

          2.1 解析CSS

          CSS樣式可以通過下面三種方式引入:

          • 通過link引用外部的CSS文件
          • style 標(biāo)簽內(nèi)的CSS
          • 元素的style屬性內(nèi)嵌的CSS

          和HTML一樣,瀏覽器無法直接理解純文本的CSS樣式,需要通過CSS解析器將CSS解析成 styleSheets 結(jié)構(gòu),也就是我們常說的 CSSOM樹

          styleSheets結(jié)構(gòu)同樣具備查詢和修改功能:

          document.styleSheets

          2.2 屬性值標(biāo)準(zhǔn)化

          屬性值標(biāo)準(zhǔn)化看字面意思有點(diǎn)不好理解,我們通過下面一個(gè)例子來看看什么是屬性值標(biāo)準(zhǔn)化:

          在寫CSS樣式的時(shí)候,我們在設(shè)置color屬性值的時(shí)候,經(jīng)常會(huì)用white、red等,但是這種值瀏覽器的渲染引擎不容易理解,所以需要將所有值轉(zhuǎn)換成渲染引擎容易理解的、標(biāo)準(zhǔn)化的計(jì)算值,這個(gè)過程就是屬性值標(biāo)準(zhǔn)化。

          white標(biāo)準(zhǔn)化后的值為 rgb(255, 255, 255)

          2.3 計(jì)算DOM樹中每個(gè)節(jié)點(diǎn)的樣式

          完成樣式的屬性值標(biāo)準(zhǔn)化后,就需要計(jì)算每個(gè)節(jié)點(diǎn)的樣式屬性,這個(gè)階段CSS有兩個(gè)規(guī)則我們需要清楚:

          • 繼承規(guī)則:每個(gè)DOM節(jié)點(diǎn)都包含有父節(jié)點(diǎn)的樣式
          • 層疊規(guī)則:層疊是CSS的一個(gè)基本特征,是一個(gè)定義了如何合并來自多個(gè)源的屬性值的算法。

          樣式計(jì)算階段是為了計(jì)算出DOM節(jié)點(diǎn)中每個(gè)元素的具體樣式,在計(jì)算過程中需要遵守CSS的繼承和層疊兩個(gè)規(guī)則。

          該階段最終輸出的內(nèi)容是每個(gè)DOM節(jié)點(diǎn)的樣式,并被保存在 ComputedStyle 的結(jié)構(gòu)中。

          3. 布局階段

          經(jīng)過上面的兩個(gè)步驟,我們已經(jīng)拿到了DOM樹和DOM樹中元素的樣式,接下來需要計(jì)算DOM樹中可見元素的幾何位置,這個(gè)計(jì)算過程就是布局。

          3.1 創(chuàng)建布局樹

          在DOM樹中包含了一些不可見的元素,例如 head 標(biāo)簽,設(shè)置了 display:none 屬性的元素,所以我們需要額外構(gòu)建一棵只包含可見元素的布局樹。

          構(gòu)建過程:從DOM樹的根節(jié)點(diǎn)開始遍歷,將所有可見的節(jié)點(diǎn)加到布局樹中,忽略不可見的節(jié)點(diǎn)。

          3.2 布局計(jì)算

          到這里我們就有了一棵構(gòu)建好的布局樹,就可以開始計(jì)算布局樹節(jié)點(diǎn)的坐標(biāo)位置了。從根節(jié)點(diǎn)開始遍歷,結(jié)合上面計(jì)算得到的樣式,確定每個(gè)節(jié)點(diǎn)對象在頁面上的具體大小和位置,將這些信息保存在布局樹中。

          布局階段的輸出是一個(gè)盒子模型,它會(huì)精確地捕獲每個(gè)元素在屏幕內(nèi)的確切位置與大小。

          4. 分層

          現(xiàn)在我們已經(jīng)有了布局樹,也知道了每個(gè)元素的具體位置信息,但是還不能開始繪制頁面,因?yàn)轫撁嬷袝?huì)有像3D變換、頁面滾動(dòng)、或者用 z-index 進(jìn)行z軸排序等復(fù)雜效果,為了更方便實(shí)現(xiàn)這些效果,渲染引擎還需要為特定的節(jié)點(diǎn)生成專用的圖層,并生成一棵對應(yīng)的圖層樹(LayerTree)。

          在Chrome瀏覽器中,我們可以打開開發(fā)者工具,選擇 Elements-Layers 標(biāo)簽,就可以看到頁面的分層情況,如下圖所示:

          瀏覽器的頁面實(shí)際上被分成了很多圖層,這些圖層疊加后合成了最終的頁面。

          到這里,我們構(gòu)建了兩棵樹:布局樹和圖層樹。下面我們來看下這兩棵樹之間的關(guān)系:

          正常情況下,并不是布局樹的每個(gè)節(jié)點(diǎn)都包含一個(gè)圖層,如果一個(gè)節(jié)點(diǎn)沒有對應(yīng)的圖層,那么這個(gè)節(jié)點(diǎn)就從屬于父節(jié)點(diǎn)的圖層。

          那節(jié)點(diǎn)要滿足什么條件才會(huì)被提升為一個(gè)單獨(dú)的圖層?只要滿足下面其中一個(gè)條件即可:

          • 擁有層疊上下文屬性的元素會(huì)被提升為單獨(dú)的一個(gè)圖層
          • 需要剪裁(clip)的地方也會(huì)被創(chuàng)建為圖層。

          5. 圖層繪制

          構(gòu)建好圖層樹之后,渲染引擎就會(huì)對圖層樹中的每個(gè)圖層進(jìn)行繪制。

          渲染引擎實(shí)現(xiàn)圖層繪制,會(huì)把一個(gè)圖層的繪制拆分成很多小的繪制指令,然后將這些指令按照順序組成一個(gè)繪制列表。

          6. 柵格化(raster)操作

          繪制一個(gè)圖層時(shí)會(huì)生成一個(gè)繪制列表,這只是用來記錄繪制順序和繪制指令的列表,實(shí)際上繪制操作是由渲染引擎中的合成線程來完成的。

          通過下圖來看下渲染主線程和合成線程之間的關(guān)系:

          當(dāng)圖層的繪制列表準(zhǔn)備好后,主線程會(huì)把該繪制列表提交給合成線程,合成線程開始工作。

          首先合成線程會(huì)將圖層劃分為圖塊(tile),圖塊大小通常是 256256 或者 512512。

          然后合成線程會(huì)按照視口附近的圖塊來優(yōu)先生成位圖,實(shí)際生成位圖的操作是由柵格化來執(zhí)行的。所謂柵格化,是指將圖塊轉(zhuǎn)換為位圖。而圖塊是柵格化執(zhí)行的最小單位。渲染進(jìn)程維護(hù)了一個(gè)柵格化的線程池,所有的圖塊柵格化都是在線程池內(nèi)執(zhí)行的,運(yùn)行方式如下圖所示:

          7. 合成和顯示

          一旦所有圖塊都被光柵化,合成線程就會(huì)生成一個(gè)繪制圖塊的命令——“DrawQuad”,然后將該命令提交給瀏覽器進(jìn)程。

          瀏覽器進(jìn)程里面有一個(gè)名字叫做 viz 的組件,用來接收合成線程發(fā)過來的 DrawQuad 命令,然后根據(jù)命令執(zhí)行。 DrawQuad 命令,將其頁面內(nèi)容繪制到內(nèi)存中,最后再將內(nèi)存顯示在屏幕上。

          多年開發(fā)老碼農(nóng)福利贈(zèng)送:網(wǎng)頁制作,網(wǎng)站開發(fā),web前端開發(fā),從最零基礎(chǔ)開始的的HTML+CSS+JavaScript。jQuery,Vue、React、Ajax,node,angular框架等到移動(dòng)端小程序項(xiàng)目實(shí)戰(zhàn)【視頻+工具+電子書+系統(tǒng)路線圖】都有整理,需要的伙伴可以私信我,發(fā)送“前端”等3秒后就可以獲取領(lǐng)取地址,送給每一位對編程感興趣的小伙伴

          8. 總結(jié)

          一個(gè)完整的渲染流程可以總結(jié)如下:

          • 1、渲染進(jìn)程將HTML內(nèi)容轉(zhuǎn)換為瀏覽器能夠讀懂的DOM樹結(jié)構(gòu)。
          • 2、渲染引擎將CSS樣式表轉(zhuǎn)化為瀏覽器可以理解的styleSheets,計(jì)算出DOM節(jié)點(diǎn)的樣式。
          • 3、創(chuàng)建布局樹,并計(jì)算所需元素的布局信息。
          • 4、對布局樹進(jìn)行分層,并生成分層樹。
          • 5、為每個(gè)圖層生成繪制列表,并將其提交到合成線程。
          • 6、合成線程將圖層分圖塊,并柵格化將圖塊轉(zhuǎn)換成位圖。
          • 7、合成線程發(fā)送繪制圖塊命令給瀏覽器進(jìn)程。瀏覽器進(jìn)程根據(jù)指令生成頁面,并顯示到顯示器上。

          渲染過程中還有兩個(gè)我們經(jīng)常聽到的概念:重排和重繪。在這篇文章中就不細(xì)說了,下一篇文章再詳細(xì)介紹。


          主站蜘蛛池模板: 无码国产精品一区二区免费 | 波多野结衣高清一区二区三区| 一区二区三区视频在线播放| 亚洲av区一区二区三| 亚洲国产福利精品一区二区| 日韩色视频一区二区三区亚洲| 国产一区二区中文字幕| 后入内射国产一区二区| 国产SUV精品一区二区四| 亚洲午夜一区二区电影院| 另类免费视频一区二区在线观看| 亚洲一区二区三区在线视频| 国产一区二区三区在线免费 | 无码人妻久久一区二区三区| 国产一区玩具在线观看| 丝袜人妻一区二区三区网站| 一本色道久久综合一区| 琪琪see色原网一区二区| 日韩免费无码一区二区视频| 在线播放国产一区二区三区 | 国产另类ts人妖一区二区三区| 国产视频一区在线播放| 国产精品538一区二区在线| 久久毛片一区二区| 国产手机精品一区二区| 亚洲国产欧美国产综合一区| 国产精品99精品一区二区三区| 日韩久久精品一区二区三区| 无码一区二区三区中文字幕| 欧洲亚洲综合一区二区三区| 色一情一乱一伦一区二区三区| 天堂va视频一区二区| 立川理惠在线播放一区| 波多野结衣中文字幕一区| 一区二区高清在线观看| 天堂Aⅴ无码一区二区三区| 日本一区二区三区在线观看| 精品无码一区二区三区电影| 成人无码精品一区二区三区| 国产成人AV区一区二区三| 久久国产午夜精品一区二区三区|