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

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

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

          dom-to-image庫(kù)是如何將html轉(zhuǎn)換成圖片的

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

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

          雖然原理很簡(jiǎn)單,但是dom-to-image畢竟也有1000多行代碼,所以我很好奇它具體都做了哪些事情,本文就來(lái)詳細(xì)剖析一下,需要說(shuō)明的是dom-to-image庫(kù)已經(jīng)六七年前沒(méi)有更新了,可能有點(diǎn)過(guò)時(shí),所以我們要看的是基于它修改的dom-to-image-more庫(kù),這個(gè)庫(kù)修復(fù)了一些bug,以及增加了一些特性,接下來(lái)我們就來(lái)詳細(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,最后通過(guò)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) {// 通過(guò)canvas繪制圖片
                      // ...
                });
          }
          

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

          將節(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對(duì)象:

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

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

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

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

          檢查和包裝元素

          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)簽里,可以猜測(cè)這個(gè)主要是處理文本節(jié)點(diǎn),畢竟應(yīng)該沒(méi)有人會(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)

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

          cloneNode(clonee, options, null, ownerWindow)
          
          // 參數(shù):需要克隆的節(jié)點(diǎn)、選項(xiàng)、父節(jié)點(diǎn)的樣式、所屬window對(duì)象
          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)簽,或者需要過(guò)濾掉的節(jié)點(diǎn),那么會(huì)直接返回。

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

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

          處理canvas元素的克隆

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

          如果元素是canvas,那么會(huì)通過(guò)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 {
                          // 如果沒(méi)有window對(duì)象或者requestAnimationFrame方法,那么立即返回
                          resolve(image);
                      }
                  };
                  image.onerror = reject;
                  image.src = uri;
              });
          }
          

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

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

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

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

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

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

          接下來(lái)調(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,有必要先簡(jiǎn)單了解一下。

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

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

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

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

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

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

          一個(gè)普通的DOM元素可以使用attachShadow方法來(lái)添加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,我們就可以通過(guò)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;
              });
          }
          

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

          function getRenderedChildren(original) {
              // 如果是slot元素,那么通過(guò)assignedNodes方法返回該插槽中的節(jié)點(diǎn)
              if (util.isShadowSlotElement(original)) {
                  return original.assignedNodes();
              }
              // 普通元素直接通過(guò)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樹(shù)中
          function isInShadowRoot(value) {
              // 如果是普通節(jié)點(diǎn),getRootNode方法會(huì)返回document對(duì)象,如果是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;
          }
          

          這一連串的判斷,如果對(duì)于shadow DOM不熟悉的話大概率很難看懂,不過(guò)沒(méi)關(guān)系,跳過(guò)這部分也可以,反正就是獲取子節(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)的所有樣式和值。

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

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

          對(duì)于每個(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對(duì)象,和我們使用div.style獲取到的對(duì)象類型是一樣的,但是div.style對(duì)象只能獲取到元素的內(nèi)聯(lián)樣式,使用div.style.color = '#fff'設(shè)置的也能獲取到,因?yàn)檫@種方式設(shè)置的也是內(nèi)聯(lián)樣式,其他樣式是獲取不到的,但是window.getComputedStyle能獲取到所有css樣式。

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

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

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

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

          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è)置一遍呢,無(wú)法理解。

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

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

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

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

          1.1.先獲取原節(jié)點(diǎn)及祖先節(jié)點(diǎn)的元素標(biāo)簽列表,其實(shí)就是一個(gè)向上遞歸的過(guò)程,不過(guò)存在終止條件,就是當(dāng)遇到塊級(jí)元素的祖先節(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.接下來(lái)會(huì)創(chuàng)建一個(gè)沙箱,也就是一個(gè)iframe,這個(gè)iframe的DOCTYPE和charset會(huì)設(shè)置成和當(dāng)前頁(yè)面的一樣。

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

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

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

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

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

          if (
              sourceValue !== defaultValue ||
              (parentComputedStyles && sourceValue !== parentValue)
          ) {
              // 樣式優(yōu)先級(jí),比如important
              const priority = sourceComputedStyles.getPropertyPriority(name);
              // 將樣式設(shè)置到克隆節(jié)點(diǎn)的style對(duì)象上
              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)樣式,就不用管了,不得不說(shuō),還是挺巧妙的。

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

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

          克隆偽元素

          克隆完樣式,接下來(lái)就是處理偽元素了:

          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方法是可以獲取元素的偽元素的樣式的,通過(guò)第二個(gè)參數(shù)指定要獲取的偽元素即可。

          如果偽元素的content為空就不管了,總感覺(jué)有點(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ì)插入偽元素的樣式,通過(guò)formatPseudoElementStyle方法獲取偽元素的樣式字符串:

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

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

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

          但是前面說(shuō)了,這個(gè)屬性一般都是沒(méi)值的,所以會(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}`;
              }
          }
          

          很簡(jiǎn)單,遍歷樣式對(duì)象,然后拼接成css的樣式字符串。

          克隆輸入框

          對(duì)于輸入框的處理很簡(jiǎn)單:

          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è)置到克隆后的元素上即可。但是我測(cè)試發(fā)現(xiàn)克隆輸入框也會(huì)把它的值給克隆過(guò)去,所以這一步可能沒(méi)有必要。

          修復(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)添加命名空間,另外對(duì)于rect節(jié)點(diǎn),還把寬高的屬性設(shè)置成對(duì)應(yīng)的樣式,這個(gè)是何原因,我們也不得而知。

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

          嵌入字體

          節(jié)點(diǎn)克隆完后接下來(lái)會(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),接下來(lá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)簽和通過(guò)link標(biāo)簽引入的樣式,結(jié)果是一個(gè)類數(shù)組,數(shù)組的每一項(xiàng)是一個(gè)CSSStyleSheet對(duì)象。

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

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

          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語(yǔ)句,找出其中的@font-face語(yǔ)句,shouldProcess方法會(huì)判斷@font-face語(yǔ)句的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語(yǔ)句中定義的所有字體的url,然后通過(guò)XMLHttpRequest發(fā)起請(qǐng)求,將字體文件轉(zhuǎn)換成data:URL形式,然后替換css語(yǔ)句中的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語(yǔ)句的遠(yuǎn)程字體url都轉(zhuǎn)換成data:URL形式后再將它們拼接成css字符串即可完成嵌入字體的操作。

          說(shuō)實(shí)話,Promise鏈太長(zhǎng),看著容易暈。

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

          內(nèi)聯(lián)完了字體后接下來(lái)就是內(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è)方法也很簡(jiǎn)單,也是發(fā)個(gè)請(qǐng)求獲取圖片數(shù)據(jù),然后將它轉(zhuǎn)換成data:URL設(shè)置回圖片的src。

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

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

          圖片也處理完了接下來(lái)就可以將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對(duì)象來(lái)將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ù)包裝元素

          在最開(kāi)始的【檢查和包裝元素】步驟會(huì)替換掉節(jié)點(diǎn)類型不為1的節(jié)點(diǎn),這一步就是用來(lái)恢復(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)看過(guò)了,這里就不重復(fù)說(shuō)了。

          將圖片通過(guò)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);
           // 如果寬度高度都沒(méi)有,那么默認(rèn)設(shè)置成300
              if (util.isDimensionMissing(width)) {
                  width = util.isDimensionMissing(height) ? 300 : height * 2.0;
              }
              // 如果高度沒(méi)有,那么默認(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上后,就可以通過(guò)canvas.toDataURL()方法轉(zhuǎn)換成圖片的data:URL,你可以渲染到頁(yè)面,也可以直接進(jìn)行下載。

          總結(jié)

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

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

          限于篇幅,源碼中其實(shí)還要很多有意思的細(xì)節(jié)沒(méi)有介紹,比如為了修改iframe的DOCTYPE和charset,居然寫(xiě)了三種方式,雖然我覺(jué)得第一種就夠了,又比如獲取節(jié)點(diǎn)默認(rèn)樣式的方式,通過(guò)iframe創(chuàng)建同樣標(biāo)簽同樣層級(jí)的元素,說(shuō)實(shí)話我是從來(lái)沒(méi)見(jiàn)過(guò),再比如解析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)簽我也是從來(lái)沒(méi)有見(jiàn)過(guò)。等等。

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

          SS的布局有太多種方式,元素的表現(xiàn)也有很多的形式。

          像我們熟悉的那些:行內(nèi)元素、塊元素、列表元素、表格元素、絕對(duì)定位、固定定位、浮動(dòng)、彈性布局、網(wǎng)格布局等等等等。

          一個(gè)元素的具體渲染可能會(huì)受到父子元素、兄弟元素的影響。

          大多數(shù)情況我們都可以通過(guò)一些手段,來(lái)解決我們遇到的布局或表現(xiàn)問(wèn)題。

          比如給一個(gè)元素賦予了浮動(dòng),那么可以通過(guò)清除浮動(dòng)來(lái)消除影響,再比如通過(guò)絕對(duì)定位來(lái)調(diào)整元素的位置,也可以通過(guò)padding或margin等避免其他元素被覆蓋。

          但是也有那么一些情況,我們不太好處理,比如某一個(gè)元素希望相對(duì)于它的父級(jí)區(qū)域做固定定位,而不是基于整個(gè)頁(yè)面,能做到嗎?再比如給定一個(gè)盒容器,無(wú)論子元素怎么排序布局或者浮動(dòng),也不會(huì)影響其他相鄰盒子的渲染,該怎么做到?

          其實(shí)不止這些,還有很多的實(shí)際場(chǎng)景讓我們很棘手,你是否在設(shè)計(jì)和實(shí)現(xiàn)方面做過(guò)平衡和妥協(xié)?

          對(duì)于現(xiàn)在的瀏覽器來(lái)說(shuō),所支持的CSS功能不允許我們說(shuō):我不行,我做不到。

          別的不說(shuō),今天要講的contain就能用來(lái)解決上面的問(wèn)題,它能做的事情還有很多,基于自身天然的屬性,甚至能輕松提升你的性能!

          contain概述

          contain表明該元素要獨(dú)立于頁(yè)面中的其他元素,該元素中的所有內(nèi)容都被局限在一個(gè)獨(dú)立的區(qū)域,跟其他元素隔離開(kāi)來(lái),從而使得基于該元素的所有計(jì)算都是獨(dú)立的,被限制在該DOM子樹(shù)中,而不是整個(gè)頁(yè)面。這樣能夠讓頁(yè)面的性能提升。

          該元素構(gòu)成的容器,可以控制其產(chǎn)生的尺寸范圍、樣式作用域、布局方式、繪制區(qū)域。會(huì)生成新的包含區(qū)塊、新的層疊上下文、新的區(qū)塊格式化上下文。這些控制手段都對(duì)應(yīng)著不同的局限屬性,在容器內(nèi)對(duì)局限屬性的修改,不會(huì)影響容器外的部分,也就不會(huì)使得頁(yè)面經(jīng)常重新渲染,尤其在動(dòng)態(tài)修改頁(yè)面元素時(shí)會(huì)帶來(lái)更好的性能受益。

          我們理解它的時(shí)候,不要把它想成是包含的意思,理解成它的作用,是對(duì)包含內(nèi)容的一個(gè)局限,之后也會(huì)多次用到"局限"這個(gè)詞。

          分類

          一、關(guān)鍵詞

          通過(guò)關(guān)鍵詞,可以指定不同的局限屬性,從而產(chǎn)生不同的局限效果。

          1. none:不應(yīng)用任何局限。
          2. size:元素的尺寸無(wú)視子元素而單獨(dú)計(jì)算,在行向和塊向上都應(yīng)用該局限。
          3. inline-size:元素的尺寸無(wú)視子元素而單獨(dú)計(jì)算,只在行向上應(yīng)用該局限。
          4. layout:使得該元素的布局與元素外的任何內(nèi)容相互獨(dú)立,互不影響。即該元素所構(gòu)成的容器從頁(yè)面中隔離出來(lái),單獨(dú)計(jì)算布局。
          5. style:對(duì)于容器內(nèi)的某些可能影響外部屬性的值,被限制在容器內(nèi),不參與整體的計(jì)算,比如計(jì)數(shù)器和引號(hào)不會(huì)參與之前或者之后的計(jì)算,會(huì)有獨(dú)立的計(jì)算,對(duì)于外面來(lái)說(shuō)就好像這個(gè)元素沒(méi)存在過(guò)一樣,或者說(shuō)不知道有它的存在。
          6. paint:限制該元素的繪制區(qū)域范圍,子元素的渲染永遠(yuǎn)不會(huì)出現(xiàn)在容器外,容器的邊界限定了子元素的可見(jiàn)內(nèi)容。例如容器在屏幕外,那么就永遠(yuǎn)不用繪制,因?yàn)槠渥釉匾部隙ㄔ谄聊煌猓幌駇argin負(fù)值放在屏幕外那樣,子元素可能延伸到屏幕內(nèi)。
          7. content:相當(dāng)于設(shè)置了layout、paint、style,也就是除了size之外的所有值。
          8. strict:給該元素應(yīng)用所有局限規(guī)則,相當(dāng)于設(shè)置size、layout、paint、style。

          二、組合值

          也就是上面2-6關(guān)鍵字的任意組合,跟順序無(wú)關(guān),多個(gè)值之間用空格分隔開(kāi)。跟個(gè)數(shù)也無(wú)關(guān),可以設(shè)置任意的數(shù)量。不過(guò)要注意,size和inline-size同時(shí)只能設(shè)置一個(gè),因?yàn)樗鼈z是沖突的。

          三、全局值

          全局值的作用,可以參考我在font-size那篇文章中的解釋說(shuō)明,它們的作用機(jī)制和原理都是一樣的,這里不在重復(fù)贅述。

          示例

          我們來(lái)對(duì)上面所說(shuō)的內(nèi)容,做一些示例,來(lái)看看它們的實(shí)際工作方式。

          先構(gòu)建一個(gè)基本的代碼,之后都以這個(gè)為基礎(chǔ)改造和演示:

          <div style="background-color: bisque;">
            我是父元素
            <div style="background-color: coral;">
              我是子元素
            </div>
          </div>
          <div style="background-color: lightpink;">
            我是父兄弟元素
          </div>

          看下現(xiàn)在的效果:

          效果

          記住這個(gè)效果,因?yàn)槲覀兘酉聛?lái)就要改變contain屬性,觀察它所發(fā)生的變化。

          1. 通過(guò)尺寸來(lái)控制
          <div style="contain: size;background-color: bisque;">
            我是父元素
            <div style="background-color: coral;">
              我是子元素
            </div>
          </div>
          <div style="background-color: lightpink;">
            我是父兄弟元素
          </div>

          給父元素加上contain:size樣式:

          效果

          可以看到,"我是父元素"和"我是父兄弟元素"重合了,也就說(shuō),父兄弟元素的渲染,直接從父元素渲染開(kāi)始的位置開(kāi)始渲染的。就好像父元素不存在一樣。

          這里面的奧妙通過(guò)控制臺(tái)看一下,其實(shí)就很容易解開(kāi)了:

          效果

          就是因?yàn)楦冈氐母叨葹?了。在解釋之前先說(shuō)明一下,行向指的是我們書(shū)寫(xiě)的方向,就是指的從左往右,你就可以理解成是多個(gè)行內(nèi)元素排列的方向,一直往后面追加的方向。塊向指的是我們折行的方向,也就是指的從上往下,你就可以理解成是多個(gè)塊級(jí)元素排列的方向,一直往下追加的方向。

          理解了這兩個(gè)之后,我們就知道,由于size影響著這兩個(gè)方向上的局限,它會(huì)變得無(wú)視子元素。因?yàn)槿绻麤](méi)有主動(dòng)設(shè)置尺寸的話,就好像子元素不存在一樣,那么它就沒(méi)有高度,所以兄弟元素就自然而然的頂上來(lái)了。

          這時(shí),我們改一下設(shè)置,讓它只在行向上有局限:

          <div style="contain: inline-size;background-color: bisque;">
            我是父元素
            <div style="background-color: coral;">
              我是子元素
            </div>
          </div>
          <div style="background-color: lightpink;">
            我是父兄弟元素
          </div>

          通過(guò)設(shè)置contain:inline-size:

          效果

          可以看到,效果又回來(lái)了,這是因?yàn)槲覀儧](méi)有在塊向上做局限,因此高度會(huì)自然撐開(kāi)。

          既然說(shuō)到了這里,我們?cè)倏匆幌拢侨绾卧谛邢蛏线M(jìn)行局限的,構(gòu)造如下代碼:

          <div style="contain: inline-size;display: inline-block;background-color: bisque;">
            我是父元素
            <div style="background-color: coral;">
              我是子元素
            </div>
          </div>
          <div style="background-color: lightpink;">
            我是父兄弟元素
          </div>

          設(shè)置父元素為行內(nèi)塊,增加display:inline-block:

          效果

          瞬間又變成了這樣,這同樣是因?yàn)椋袃?nèi)局限使得父元素獨(dú)立計(jì)算尺寸,而我們又沒(méi)手動(dòng)指定,因此它的寬度為0,子元素也跟著寬度為0,所以就變成了一個(gè)字一換行。

          這時(shí)即使你給子元素加上寬度,在行向上父元素也會(huì)無(wú)視你:

          效果

          你看我們給子元素加上100px的寬度,但是鼠標(biāo)查看父元素,依然是寬度為0。

          1. 通過(guò)布局來(lái)控制

          指定contain:layout,可以讓該元素獨(dú)立計(jì)算它的內(nèi)部布局,不受外界影響,我們先將子元素設(shè)定一個(gè)固定定位:

          <div style="background-color: bisque;">
            我是父元素
            <div style="position: fixed;top: 10px;background-color: coral;">
              我是子元素
            </div>
          </div>
          <div style="background-color: lightpink;">
            我是父兄弟元素
          </div>

          看下現(xiàn)在的效果:

          效果

          完全沒(méi)毛病,子元素固定到頁(yè)面頂部的10px位置。

          現(xiàn)在應(yīng)用一下我們的布局限制:

          <div style="contain: layout;background-color: bisque;">
            我是父元素
            <div style="position: fixed;top: 10px;background-color: coral;">
              我是子元素
            </div>
          </div>
          <div style="background-color: lightpink;">
            我是父兄弟元素
          </div>

          再看下顯示的效果:

          效果

          咦?明明是固定定位,它的位置卻是相對(duì)于父元素的。這就是布局限制的作用,相當(dāng)于父元素告訴頁(yè)面,從現(xiàn)在開(kāi)始,這片的布局歸我管,所有的行為都向我請(qǐng)示,由我指揮。

          同樣,其他的position值,也都是基于父元素的布局限制來(lái)渲染的,這里就不做一一演示了。

          其實(shí),不光是position,只要是關(guān)于布局的,都會(huì)在此局限下生效,這里再演示一個(gè)浮動(dòng)的例子:

          <div style="height: 80px;padding: 5px;background-color: bisque;">
            <h2 style="margin-bottom: 7px;">我是父元素</h2>
            <p style="float: left;background-color: coral;">
              我是子元素
            </p>
          </div>
          <div style="height: 80px;padding: 5px;background-color: lightpink;">
            <h2>我是父兄弟元素</h2>
            <p style="background-color: coral;">
              我是父兄弟子元素
            </p>
          </div>

          兩個(gè)父元素各包含兩個(gè)子元素,其中第一個(gè)父元素的第二個(gè)子元素設(shè)置為左浮動(dòng),為了更好的演示,我把子元素改成了h2和p標(biāo)簽,看下現(xiàn)在的效果:

          效果

          父元素的兩個(gè)子元素正常顯示,但是父兄弟元素的第一個(gè)子元素里面的文字,被擠的偏移了,這是由于父元素的第二個(gè)子元素設(shè)置浮動(dòng)引起的,通過(guò)控制臺(tái)看下就明白了:

          效果

          紅線是我標(biāo)出來(lái)的,可以看到,父兄弟元素的h2和父元素的浮動(dòng)有一丁點(diǎn)的重合,導(dǎo)致文字被推開(kāi),這就是浮動(dòng)產(chǎn)生的影響。

          我們通過(guò)布局局限,來(lái)控制浮動(dòng)只發(fā)生在局限內(nèi),稍微改下代碼:

          <div style="contain: layout;height: 80px;padding: 5px;background-color: bisque;">
            <h2 style="margin-bottom: 7px;">我是父元素</h2>
            <p style="float: left;background-color: coral;">
              我是子元素
            </p>
          </div>
          <div style="height: 80px;padding: 5px;background-color: lightpink;">
            <h2>我是父兄弟元素</h2>
            <p style="background-color: coral;">
              我是父兄弟子元素
            </p>
          </div>

          給父元素添加了contain:layout,這是浮動(dòng)就不會(huì)影響后面的布局了:

          效果

          通過(guò)布局局限,我們可以把所有的關(guān)于布局的影響,都控制在容器內(nèi),這樣即使容器內(nèi)布局發(fā)生了改變,也完全不會(huì)影響頁(yè)面其他內(nèi)容,在動(dòng)態(tài)頁(yè)面中,如果需要頻繁的修改某些元素,通過(guò)這種方式管理和設(shè)計(jì)頁(yè)面,就能很有效的改善渲染的性能。

          1. 通過(guò)樣式來(lái)控制

          這里的樣式主要是針對(duì)計(jì)數(shù)器和引號(hào)的作用域,能控制它們只在所局限的范圍內(nèi)單獨(dú)計(jì)算,而不會(huì)影響全局的結(jié)果,就好像它們是單獨(dú)拿出來(lái)作為一個(gè)獨(dú)立的文檔一樣,看個(gè)例子:

          body {
            counter-reset: my-list;
          }
          div > div::before {
            counter-increment: my-list;
            content: "(" counter(my-list) "):";
          }
          <div>
            <div>第1行</div>
            <div>第2行</div>
            <div>第3行</div>
            <div>第4行</div>
            <div>第5行</div>
            <div>第6行</div>
          </div>

          我們?cè)O(shè)計(jì)這樣一個(gè)列表,通過(guò)自定義的計(jì)數(shù)器,設(shè)置一個(gè)前綴的顯示:

          效果

          如果我們希望第三行獨(dú)立出來(lái),進(jìn)行樣式局限,不參與外部計(jì)數(shù):

          <div>
            <div>第1行</div>
            <div>第2行</div>
            <div style="contain: style;">第3行</div>
            <div>第4行</div>
            <div>第5行</div>
            <div>第6行</div>
          </div>

          那么它就會(huì)展現(xiàn)成這個(gè)樣子:

          效果

          注意第三行的計(jì)數(shù),已經(jīng)重置為1,并且第四行從3開(kāi)始,接著第二行的值繼續(xù)計(jì)數(shù)。

          1. 通過(guò)繪制來(lái)控制

          這個(gè)也比較厲害,就是父元素啥樣就是啥樣,子元素永遠(yuǎn)不會(huì)在容器外渲染:

          <div style="background-color: bisque;width: 100px;height: 100px;">
            <div>
            海客談瀛洲,煙濤微茫信難求。
            越人語(yǔ)天姥,云霞明滅或可睹。
            天姥連天向天橫,勢(shì)拔五岳掩赤城。
              </div>
          </div>

          有這樣一個(gè)代碼塊,容器的寬高為100px,內(nèi)容有超出部分:

          效果

          我們可以通過(guò)繪制局限來(lái)使容器外的內(nèi)容不顯示:

          <div style="contain: style;background-color: bisque;width: 100px;height: 100px;">
            <div>
            海客談瀛洲,煙濤微茫信難求。
            越人語(yǔ)天姥,云霞明滅或可睹。
            天姥連天向天橫,勢(shì)拔五岳掩赤城。
            </div>
          </div>

          添加contain:paint,只讓繪制區(qū)域被限制在容器內(nèi)部:

          效果

          這個(gè)跟overflow:hidden有一點(diǎn)區(qū)別,就是繪制局限真的就是正常繪制,只不過(guò)不繪制容器外的部分,而hidden雖然隱藏,但是依然能通過(guò)js進(jìn)行滾動(dòng)。

          1. 通過(guò)組合值來(lái)控制

          我們也可以通過(guò)任意的值的組合來(lái)控制所需要的局限,也可以通過(guò)strict或content關(guān)鍵字來(lái)快速的做到這一點(diǎn)。這里就不再重復(fù)演示了

          最后

          合理的使用contain,不但能快速實(shí)現(xiàn)我們的需求,也能減少我們的修改量,而且會(huì)降低不理解現(xiàn)象的情況的出現(xiàn)頻率,更能提升頁(yè)面的性能。

          尤其是布局局限,提供給了我們更多的發(fā)揮空間,而且任意的復(fù)制到其他地方,也不會(huì)對(duì)外部元素有影響。

          每天一點(diǎn)小知識(shí),希望能夠幫助到你。

          . block(區(qū)塊)

          block面板主要是設(shè)置對(duì)象文本的文字間距、對(duì)齊方式、上標(biāo)、下標(biāo)、排列方式、首行縮進(jìn)等。

          (1)word-spacing:設(shè)置單詞之間的間距。可以設(shè)置負(fù)值。

          默認(rèn)值:normal

          (2)letter-spacing:設(shè)置字符之間的間距。可以指定負(fù)值。因?yàn)橹形囊彩亲址? 這個(gè)參數(shù)可以設(shè)置文字間的間距。

          默認(rèn)值:normal


          (3)vertical-align:指定元素的垂直對(duì)齊方式。針對(duì)<td>表格設(shè)置垂直對(duì)齊方法,但是對(duì)<div>設(shè)置無(wú)效,可以將display屬性設(shè)置為table-cell值;

          可以指定sub(下標(biāo))、super(上標(biāo))、top(與頂端對(duì)齊)、middle(居中)、bottom(與底端對(duì)齊)等。適用于行內(nèi)塊元素 img、input、td等

          baseline 默認(rèn)。元素放置在父元素的基線上。

          sub 垂直對(duì)齊文本的下標(biāo)。

          super 垂直對(duì)齊文本的上標(biāo)

          top 把元素的頂端與行中最高元素的頂端對(duì)齊

          text-top 把元素的頂端與父元素字體的頂端對(duì)齊

          middle 把此元素放置在父元素的中部。

          bottom 把元素的頂端與行中最低的元素的頂端對(duì)齊。

          text-bottom 把元素的底端與父元素字體的底端對(duì)齊。

          % 使用 "line-height" 屬性的百分比值來(lái)排列此元素。允許使用負(fù)值。

          inherit 規(guī)定應(yīng)該從父元素繼承 vertical-align 屬性的值。

          默認(rèn)值: baseline


          (4)text-align:設(shè)置文本的排列方式(適用于行內(nèi)元素和行內(nèi)塊元素, 如 a、span、input、img、label等元素)。left(左對(duì)齊)、right(右對(duì)齊)、center(居中)、justify(兩端對(duì)齊)。也可用于<hr/>下劃線對(duì)齊方式

          默認(rèn)值: 如果 direction 屬性是 ltr, 則默認(rèn)值是 left;如果 direction 是 rtl,則為 right。(關(guān)于CSS direction 屬性看下面的內(nèi)容介紹)


          text-align 和margin auto 區(qū)別

          text-align 可以讓塊級(jí)元素里面的文字內(nèi)容居中對(duì)齊.

          文字內(nèi)容 == (圖片 input 行內(nèi)元素 行內(nèi)塊元素)

          但是對(duì)于里面的塊級(jí)元素?zé)o效

          讓一個(gè)塊級(jí)元素居中對(duì)齊 : margin:0 auto;



          (5)text-indent:設(shè)置文本第一行的縮進(jìn)值。負(fù)值用于將文本第一行向外拉, 主要給<p>設(shè)置首行。

          要在每段前空兩格,可設(shè)置為2em,因?yàn)閑m是當(dāng)前字體尺寸,2em就是兩個(gè)字的大小。

          默認(rèn)值: not specified(沒(méi)有規(guī)定的)

          p{font-size:12px;text-indent:2em;}

          上述代碼就是可以實(shí)現(xiàn)段落首行縮進(jìn)24px(也就是兩個(gè)字體大小的距離)。



          (6)white-space:設(shè)置如何處理元素內(nèi)的空白符。有三個(gè)選項(xiàng)可選:

          normal 會(huì)將空白符全部壓縮;

          pre 則會(huì)如同處理pre標(biāo)簽內(nèi)的文本一樣處理這些空白符,也就是說(shuō),所有的空白符,包括空格,標(biāo)簽,回車(chē),等都會(huì)得以保留;

          nowrap 指定文本只有遇到br標(biāo)簽時(shí)才換行。

          默認(rèn)值: normal


          (7)display: none 此元素不會(huì)被顯示。

          block 此元素將顯示為塊級(jí)元素,此元素前后會(huì)帶有換行符。

          inline 默認(rèn)值。此元素會(huì)被顯示為內(nèi)聯(lián)元素,元素前后沒(méi)有換行符。

          inline-block 行內(nèi)塊元素。(CSS2.1 新增的值)

          list-item 此元素會(huì)作為列表顯示。

          run-in 此元素會(huì)根據(jù)上下文作為塊級(jí)元素或內(nèi)聯(lián)元素顯示。

          compact CSS 中有值 compact,不過(guò)由于缺乏廣泛支持,已經(jīng)從 CSS2.1 中刪除。

          marker CSS 中有值 marker,不過(guò)由于缺乏廣泛支持,已經(jīng)從 CSS2.1 中刪除。

          table 此元素會(huì)作為塊級(jí)表格來(lái)顯示(類似 <table>),表格前后帶有換行符。

          inline-table 此元素會(huì)作為內(nèi)聯(lián)表格來(lái)顯示(類似 <table>),表格前后沒(méi)有換行符。

          table-row-group 此元素會(huì)作為一個(gè)或多個(gè)行的分組來(lái)顯示(類似 <tbody>)。

          table-header-group 此元素會(huì)作為一個(gè)或多個(gè)行的分組來(lái)顯示(類似 <thead>)。

          table-footer-group 此元素會(huì)作為一個(gè)或多個(gè)行的分組來(lái)顯示(類似 <tfoot>)。

          table-row 此元素會(huì)作為一個(gè)表格行顯示(類似 <tr>)。

          table-column-group 此元素會(huì)作為一個(gè)或多個(gè)列的分組來(lái)顯示(類似 <colgroup>)。

          table-column 此元素會(huì)作為一個(gè)單元格列顯示(類似 <col>)

          table-cell 此元素會(huì)作為一個(gè)表格單元格顯示(類似 <td> 和 <th>)

          table-caption 此元素會(huì)作為一個(gè)表格標(biāo)題顯示(類似 <caption>)

          inherit 規(guī)定應(yīng)該從父元素繼承 display 屬性的值。

          默認(rèn)值: inline


          常用的屬性:

          display:none; //隱藏
          display:block //顯示(將行內(nèi)元素轉(zhuǎn)換為塊元素)
          dispaly:inline //將塊元素轉(zhuǎn)換行內(nèi)元素
          dispaly:inline-block //將塊元素轉(zhuǎn)換為行內(nèi)塊元素
          display:table-cell; //此元素會(huì)作為一個(gè)表格單元格顯示(類似 <td> 和 <th>) 用于多行文本、塊元素垂直居中

          以上轉(zhuǎn)換涉及行內(nèi)塊元素(img、input)轉(zhuǎn)換;

          經(jīng)過(guò)大量的測(cè)試證明:屬性display是不能用于轉(zhuǎn)換行內(nèi)塊元素(img、input)


          同義詞: 內(nèi)聯(lián)元素(行內(nèi)元素)



          4. box(方框或盒子) 主要針對(duì)圖片、表格、層、段落(p)、標(biāo)題(h1-h6)等

          box面板主要設(shè)置對(duì)象的邊界、間距、高度、寬度、和漂浮方式等。

          (1)width:定義元素的寬。

          默認(rèn)值:auto


          (2)height:定義元素的高。

          默認(rèn)值:auto


          以上width、height屬性如果是針對(duì)div標(biāo)簽設(shè)置css樣式,則與定位設(shè)置窗口width、height屬性一致(Dreamweaver會(huì)自動(dòng)填充數(shù)據(jù))


          在父元素和子元素都設(shè)置了高度和寬度的情況下:

          如果子元素的寬度超出了它的父元素的寬度, 則會(huì)父元素不會(huì)限制其顯示寬度;

          如果子元素的高度超出了它的父元素的高度, 則會(huì)父元素不會(huì)限制其顯示高度;

          (3)float:定義元素的漂浮方式。left 表示對(duì)象浮在左邊、right表示對(duì)象浮在右邊、none 表示對(duì)象不浮動(dòng)。

          默認(rèn)值:none


          一個(gè)span標(biāo)簽不需要轉(zhuǎn)成塊級(jí)元素, 就能夠設(shè)置寬度、高度了。所以能夠證明一件事兒, 就是所有標(biāo)簽已經(jīng)不區(qū)分行內(nèi)、塊了。

          也就是說(shuō), 一旦一個(gè)元素浮動(dòng)了, 那么, 將能夠并排了, 并且能夠設(shè)置寬高了。無(wú)論它原來(lái)是個(gè)div還是個(gè)span。

          span{
          float: left;
          width: 200px;
          height: 200px;
          background-color: orange;
          }
          

          (4)clear:不允許元素的漂浮,相對(duì)于前一個(gè)<div>設(shè)置浮動(dòng)時(shí),下一個(gè)有清除設(shè)置的元素就會(huì)移到它的下面。

          left表示不允許左邊有浮動(dòng)對(duì)象 right表示不允許右邊有浮動(dòng)對(duì)象

          none 表示允許兩邊都可以有浮動(dòng)對(duì)象 both 不允許有浮動(dòng)對(duì)象。

          默認(rèn)值:none


          浮動(dòng)的主要作用是將塊元素排在同一行(span也可以設(shè)置浮動(dòng));

          浮動(dòng)元素它會(huì)向左或者向右進(jìn)行浮動(dòng), 所謂浮動(dòng)可以理解"飄";

          浮動(dòng)元素向它遇到父元素的邊框就停止浮動(dòng);

          浮動(dòng)元素的層級(jí)會(huì)比普通元素要高, 并且它不會(huì)再占用原有的高度;

          浮動(dòng)元素只會(huì)影響后面的元素, 不會(huì)影響前面的元素:

          浮動(dòng)元素的后面的元素(可能是一個(gè)也可能是多個(gè)元素受到影響)也會(huì)繼承浮動(dòng)元素的特性, 它也會(huì)浮動(dòng)了要解決這個(gè)問(wèn)題有兩辦法;

          第一個(gè):需要清除浮動(dòng)

          第二個(gè):給父元素設(shè)置一個(gè)高度(子元素都設(shè)置了height高度條件下), 不建議使用

          清除浮動(dòng)后它不會(huì)影響到它后面的元素, 父元素會(huì)將所有的浮動(dòng)元素包圍者


          當(dāng)然浮動(dòng)元素對(duì)后面的元素的影響可以做首字母方法, 圖文混排(文字環(huán)繞圖片)等特效;


          (5)padding:定義元素內(nèi)容與其邊框的空距(如果元素沒(méi)有邊框就是指頁(yè)邊的空白)。

          可以分別設(shè)置分別設(shè)置上、右、下、左內(nèi)邊距。

          padding-top

          padding-right

          padding-bottom

          padding-left

          p {padding:20px} 設(shè)置設(shè)置上、右、下、左內(nèi)邊距都為20px

          p {margin: 20px 30px 30px 20px;} 設(shè)置padding-top padding-right padding-bottom padding-left分別為20px 30px 30px 20px

          默認(rèn)值:0。


          要懂得, 用小屬性層疊大屬性:

          padding: 20px;

          padding-left: 30px;

          注意:大屬性要寫(xiě)在前面, 小屬性寫(xiě)在后面;

          比如:

          div{
          width: 200px;
          height: 200px;
          padding-left: 10px;
          padding-right: 20px;
          padding:40px 50px 60px;
          padding-bottom: 30px;
          border: 1px solid #000;
          }

          padding-left:10px和padding-right:20px沒(méi)用, 因?yàn)楹竺娴膒adding大屬性, 層疊覆蓋了。


          (6)margin:定義元素的邊框與其他元素之間的距離(如果沒(méi)有邊框就是指內(nèi)容之間的距離)。

          可以分別設(shè)置上邊界、右邊界、下邊界、左邊界的值。

          margin-top

          margin-right

          margin-bottom

          margin-left

          p {margin:20px}

          p {margin:10px 0px 15px 5px;}

          默認(rèn)值:0

          marign:上邊界值 右邊界值 下邊界值 左邊界值

          margin屬性值必須按照上面順序進(jìn)行排列,以空格分開(kāi)。如果僅輸入一個(gè)值,則4個(gè)邊界值會(huì)同時(shí)設(shè)置為此值。

          如果僅輸入兩個(gè)的值,則缺少的值會(huì)以對(duì)邊的設(shè)置值進(jìn)行替代。例如:

          div{margin:5px 10px 15px 20px} /*上=5px,右=10px,下=15px,左=20px*/
          div{margin:5px} /*上=5px,右=5px,下=5px,左=5px*/
          div{margin:5px 10px} /*上=5px,右=10px,下=5px,左=10px*/
          div{margin:5px 10px 15px} /*上=5px,右=10px,下=15px,左=10px*/
          


          5. border(邊框) 針對(duì)段落(p標(biāo)簽)、圖片、表格、標(biāo)題(h1-h6)、form、input等(幾乎所有的元素都可以設(shè)置邊框)

          border面板可以設(shè)置對(duì)象邊框的寬度、顏色及樣式。

          (1)border-width:設(shè)置元素邊的寬度。可以分別設(shè)定top(上邊寬)、right(右邊寬)、bottom(下邊寬)、left(左邊寬)的值。

          border-top-width border-right-width border-bottom-width border-left-width

          thin 定義細(xì)的邊框。

          medium 默認(rèn)。定義中等的邊框。

          thick 定義粗的邊框。

          length 允許您自定義邊框的寬度。

          inherit 規(guī)定應(yīng)該從父元素繼承邊框?qū)挾取?/p>

          默認(rèn)值: medium


          (2)border-color:設(shè)置邊框的顏色。你可以分別對(duì)每條邊設(shè)置顏色。

          注意:我們可以通過(guò)設(shè)置不同的顏色做出亮邊和暗邊的效果,這樣元素看起來(lái)是立體的。

          border-top-color border-right-color border-bottom-color border-left-color

          默認(rèn)值: not specified(未規(guī)定的)


          (3)border-style:設(shè)置邊框樣式。

          border-top-style border-right-style border-bottom-style border-left-style

          可以設(shè)置為none(無(wú)邊框)、dotted(點(diǎn)線)、dashed(虛線)、solid(實(shí)線)、double(雙線)、

          groove(凹槽,3D凹線)、ridge(凸槽,3D凸線)、inset(凹邊,3D嵌入線)、outset(凸邊,3D浮出線)等邊框樣式。

          默認(rèn)值:none(無(wú)邊框)

          推薦:表單輸入框:inset(凹邊) 按鈕:outset(凸邊)


          兼容性問(wèn)題

          比如, border:10px ridge red; 在chrome和firefox、IE中有細(xì)微差別:

          如果公司里面的設(shè)計(jì)師, 處女座的, 追求極高的頁(yè)面還原度, 那么不能使用css來(lái)制作邊框。

          就要用到圖片, 就要切圖了。所以, 比較穩(wěn)定的就幾個(gè):solid、dashed、dotted, 其他的邊框樣式盡量不要用。


          border可以沒(méi)有: border: none;

          某一條邊沒(méi)有: border-left: none;

          也可以調(diào)整左邊邊框的寬度為0px: border-left-width: 0px;

          注意:border-bottom-style:可以修改a鏈接的"下劃線"的風(fēng)格

          border-bottom-width:可以修改a鏈接的"下劃線"的寬度(粗細(xì))

          border-bottom-color:可以修改a鏈接的"下劃線"的顏色


          主站蜘蛛池模板: 国产福利电影一区二区三区久久久久成人精品综合 | 中文字幕一区二区三区在线观看 | 日本中文字幕一区二区有码在线| 人妻av无码一区二区三区| 精品乱码一区内射人妻无码| 一区二区在线观看视频| 亚洲成av人片一区二区三区 | 中文字幕乱码一区久久麻豆樱花| 日韩AV片无码一区二区不卡| 亚洲一区二区三区高清不卡 | 日韩人妻精品一区二区三区视频 | 性色AV 一区二区三区| 国产对白精品刺激一区二区 | 亚洲一区二区三区无码中文字幕| 波霸影院一区二区| 波多野结衣AV一区二区三区中文| 国产剧情国产精品一区| 国产精品成人一区二区| 精品乱人伦一区二区| 日本高清天码一区在线播放| 中文字幕在线不卡一区二区| 91video国产一区| 亚洲欧洲一区二区| 国产成人一区二区三区视频免费| 国产美女露脸口爆吞精一区二区| 制服美女视频一区| 国产在线第一区二区三区| 国产精品无码一区二区在线| 中文激情在线一区二区| 国产乱码一区二区三区四| 亚洲av无码片区一区二区三区| 午夜福利一区二区三区高清视频 | 日韩人妻无码免费视频一区二区三区| 亚洲一区二区三区高清视频| 无码精品国产一区二区三区免费| 久久国产一区二区| 亚洲电影国产一区| 国模私拍福利一区二区| 无码一区二区三区AV免费| 成人久久精品一区二区三区| 日本一区二区三区精品视频|