const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone 6'];
const { Skeleton } = require('page-skeleton-webpack-plugin');
let skeleton = new Skeleton();
(async () => {
const browser = await (puppeteer.launch({
//設置超時時間
timeout: 15000,
//若是是訪問https頁面 此屬性會忽略https錯誤
ignoreHTTPSErrors: true,
// 打開開發者工具, 當此值爲true時, headless總爲false
devtools: true,
// 非headless模式,爲了能直觀看到頁面生成骨架屏的過程
headless: false
}));
const page = await browser.newPage();
// 由於是移動端,設置模擬iphone6
await page.emulate(iPhone);
// 打開m站首頁
await page.goto('https://m.to8to.com/sz');
// 等待首屏bannar加載完成
await page.waitForSelector('.ad-data-report-carousel');
// 開始build骨架屏
await skeleton.makeSkeleton(page);
})();
複製代碼
async makeSkeleton(page) {
const {defer} = this.options
// 把生成骨架屏代碼注入puppeteer同時執行初始化
await page.addScriptTag({content: this.scriptContent})
// 延遲邏輯,用於等待某些異步操做,圖1我已經使用waitForSelector,因此這個能夠不用管
await sleep(defer)
// 執行genSkeleton方法
await page.evaluate((options) => {
Skeleton.genSkeleton(options)
}, this.options)
}
複製代碼
const pluginDefaultConfig = {
port: '8989',
// 該配置對象能夠配置一個 color 字段,用於決定骨架頁面中文字塊的的顏色,顏色值支持16進制、RGB等。
text: {
color: '#EEEEEE'
},
// 該配置接受 3 個字段,color、shape、shapeOpposite。color 和 shape 用於肯定骨架頁面中圖片塊的顏色和形狀,
// 顏色值支持16 進制和 RGB等,形狀支持兩個枚舉值,circle (矩形)和 rect(圓形)。
// shapeOpposite 字段接受一個數組,數組中每一個元素是一個 DOM 選擇器,用於選擇 DOM 元素,
// 被選擇 DOM 的形狀將和配置的 shape 形狀相反,例如,配置的是 rect那麼,
// shapeOpposite 中的圖片塊將在骨架頁面中顯示成 circle 形狀(圓形),具體怎麼配置能夠參考該部分末尾的默認配置。
image: {
shape: 'rect', // `rect` | `circle`
color: '#EFEFEF',
shapeOpposite: []
},
// 該配置接受兩個字段,color 和 excludes。color 用來肯定骨架頁面中被視爲按鈕塊的顏色,
// excludes 接受一個數組,數組中元素是 DOM 選擇器,用來選擇元素,該數組中的元素將不被視爲按鈕塊
button: {
color: '#EFEFEF',
excludes: []
},
// 該配置接受 3 個字段,color、shape、shapeOpposite。color 和 shape 用於肯定骨架頁面中 svg 塊的顏色和形狀,
// 顏色值支持16 進制和 RGB等,同時也支持 transparent 枚舉值,設置爲 transparent 後,
// svg 塊將是透明塊。形狀支持兩個枚舉值,circle (矩形)和 rect(圓形)。
// shapeOpposite 字段接受一個數組,數組中每一個元素是一個 DOM 選擇器,用於選擇 DOM 元素,
// 被選擇 DOM 的形狀將和配置的 shape 形狀相反,例如,配置的是 rect那麼,
// shapeOpposite 中的 svg 塊將在骨架頁面中顯示成 circle 形狀(圓形),具體怎麼配置能夠參考該部分末尾的默認配置。
svg: {
color: '#EFEFEF',
shape: 'circle', // circle | rect
shapeOpposite: []
},
// 該配置接受兩個字段,color 和 shape。color 用來肯定骨架頁面中被視爲僞元素塊的顏色,
// shape 用來設置僞元素塊的形狀,接受兩個枚舉值:circle 和 rect。
pseudo: {
color: '#EFEFEF', // or transparent
shape: 'circle' // circle | rect
},
device: 'iPhone 6',
debug: false,
minify: {
minifyCSS: { level: 2 },
removeComments: true,
removeAttributeQuotes: true,
removeEmptyAttributes: false
},
defer: 5000,
// 若是你有不須要進行骨架處理的元素,那麼將該元素的 CSS 選擇器寫入該數組。
excludes: [],
// 不須要生成頁面骨架,且須要從 DOM 中移除的元素,配置值爲移除元素的 CSS 選擇器。
remove: [],
// 不須要移除,可是經過設置其透明度爲 0,來隱藏該元素,配置值爲隱藏元素的 CSS 選擇器。
hide: [],
// 該數組中元素是 CSS 選擇器,被選擇的元素將被被插件處理成一個色塊,色塊的顏色和按鈕塊顏色一致。內部元素將再也不作特殊處理,文字將隱藏。
grayBlock: [],
cookies: [],
// 其接受的枚舉值rem, vw, vh, vmin, vmax。
cssUnit: 'rem',
// 生成骨架頁面(shell.html)中 css 值保留的小數位數,默認值是 4。
decimal: 4,
logLevel: 'info',
quiet: false,
noInfo: false,
logTime: true
};
複製代碼
// ele 爲 document.documentElement; 遞歸遍歷DOM樹
;(function preTraverse(ele) {
// styles爲元素中全部可用的css屬性列表
const styles = getComputedStyle(ele);
// 檢查元素是否有僞元素
const hasPseudoEle = checkHasPseudoEle(ele);
// 判斷元素是否在可視區域內(是不是首屏元素),非首屏元素將要移除
if (!inViewPort(ele) || DISPLAY_NONE.test(ele.getAttribute('style'))) {
return toRemove.push(ele)
}
// 自定義要處理爲色塊的元素
if (~grayEle.indexOf(ele)) { // eslint-disable-line no-bitwise
return grayBlocks.push(ele)
}
// 自定義不須要處理爲骨架的元素
if (~excludesEle.indexOf(ele)) return false // eslint-disable-line no-bitwise
if (hasPseudoEle) {
pseudos.push(hasPseudoEle);
}
if (checkHasBorder(styles)) {
ele.style.border = 'none';
}
// 列表元素統一處理爲默認樣式
if (ele.children.length > 0 && /UL|OL/.test(ele.tagName)) {
listHandle(ele);
}
// 有子節點遍歷處理
if (ele.children && ele.children.length > 0) {
Array.from(ele.children).forEach(child => preTraverse(child));
}
// 將全部擁有 textChildNode 子元素的元素的文字顏色設置成背景色,這樣就不會在顯示文字了。
if (ele.childNodes && Array.from(ele.childNodes).some(n => n.nodeType === Node.TEXT_NODE)) {
transparent(ele);
}
// 統一文本下劃線的顏色
if (checkHasTextDecoration(styles)) {
ele.style.textDecorationColor = TRANSPARENT;
}
// 隱藏全部 svg 元素
if (ele.tagName === 'svg') {
return svgs.push(ele)
}
// 有背景色或背景圖的元素
if (EXT_REG.test(styles.background) || EXT_REG.test(styles.backgroundImage)) {
return hasImageBackEles.push(ele)
}
// 背景漸變元素
if (GRADIENT_REG.test(styles.background) || GRADIENT_REG.test(styles.backgroundImage)) {
return gradientBackEles.push(ele)
}
if (ele.tagName === 'IMG' || isBase64Img(ele)) {
return imgs.push(ele)
}
if (
ele.nodeType === Node.ELEMENT_NODE &&
(ele.tagName === 'BUTTON' || (ele.tagName === 'A' && ele.getAttribute('role') === 'button'))
) {
return buttons.push(ele)
}
if (
ele.childNodes &&
ele.childNodes.length === 1 &&
ele.childNodes[0].nodeType === Node.TEXT_NODE &&
/\S/.test(ele.childNodes[0].textContent)
) {
return texts.push(ele)
}
}(rootElement));
複製代碼
svgs.forEach(e => svgHandler(e, svg, cssUnit, decimal));
texts.forEach(e => {
textHandler(e, text, cssUnit, decimal)
});
buttons.forEach(e => buttonHandler(e, button));
hasImageBackEles.forEach(e => backgroundHandler(e, image));
imgs.forEach(e => imgHandler(e, image));
pseudos.forEach(e => pseudosHandler(e, pseudo));
gradientBackEles.forEach(e => backgroundHandler(e, image));
grayBlocks.forEach(e => grayHandler(e, button));
複製代碼
具體各塊的骨架結構如何生成的接下來會一一分析
javascript
// 寬高爲0或設置隱藏的元素直接移除(aria是爲殘疾人士等提供無障礙訪問動態、可交互Web內容的技術規範)
if (width === 0 || height === 0 || ele.getAttribute('aria-hidden') === 'true') {
return removeElement(ele)
}
複製代碼
非隱藏的元素,會把 svg 元素內部全部元素刪除,減小最終生成的骨架頁面體積,其次,設置svg 元素的寬、高和形狀等。
css
// 設置shapeOpposite的元素的最終形狀和shape配置的相反
const finalShape = shapeOpposite.indexOf(ele) > -1 ? getOppositeShape(shape) : shape;
// 清空元素的內部結構 innerHTML = ''
emptyElement(ele);
const shapeClassName = CLASS_NAME_PREFEX + shape;
// 根據rect or cirle設置border-radius屬性,同時set到styleCache
shapeStyle(shape);
Object.assign(ele.style, {
width: px2relativeUtil(width, cssUnit, decimal),
height: px2relativeUtil(height, cssUnit, decimal),
});
addClassName(ele, [shapeClassName]);
// color是自定義svg配置中的color屬性,可設置16進制設置及transparent枚舉值
if (color === TRANSPARENT) {
// 設置爲透明塊
setOpacity(ele);
} else {
// 設置背景色
const className = CLASS_NAME_PREFEX + 'svg';
const rule = `{ background: ${color} !important; }`;
addStyle(`.${className}`, rule);
ele.classList.add(className);
}
複製代碼
function buttonHandler(ele, {color, excludes}) {
if (excludes.indexOf(ele) > -1) return false
const classname = CLASS_NAME_PREFEX + 'button';
const rule = `{ color: ${color} !important; background: ${color} !important; border: none !important; box-shadow: none !important; }`;
addStyle(`.${classname}`, rule);
ele.classList.add(classname);
}
複製代碼
function backgroundHandler(ele, {color, shape}) {
const imageClass = CLASS_NAME_PREFEX + 'image';
const shapeClass = CLASS_NAME_PREFEX + shape;
const rule = `{ background: ${color} !important; }`;
addStyle(`.${imageClass}`, rule);
shapeStyle(shape);
addClassName(ele, [imageClass, shapeClass]);
}
複製代碼
function imgHandler(ele, {color, shape, shapeOpposite}) {
const {width, height} = ele.getBoundingClientRect();
const attrs = {
width,
height,
src: SMALLEST_BASE64 // 1*1像素透明gif圖
};
const finalShape = shapeOpposite.indexOf(ele) > -1 ? getOppositeShape(shape) : shape;
setAttributes(ele, attrs);
const className = CLASS_NAME_PREFEX + 'image';
const shapeName = CLASS_NAME_PREFEX + finalShape;
const rule = `{ background: ${color} !important; }`;
addStyle(`.${className}`, rule);
shapeStyle(finalShape);
addClassName(ele, [className, shapeName]);
if (ele.hasAttribute('alt')) {
ele.removeAttribute('alt');
}
}
複製代碼
function pseudosHandler({ele, hasBefore, hasAfter}, {color, shape, shapeOpposite}) {
if (!shapeOpposite) shapeOpposite = []
const finalShape = shapeOpposite.indexOf(ele) > -1 ? getOppositeShape(shape) : shape;
const PSEUDO_CLASS = `${CLASS_NAME_PREFEX}pseudo`;
const PSEUDO_RECT_CLASS = `${CLASS_NAME_PREFEX}pseudo-rect`;
const PSEUDO_CIRCLE_CLASS = `${CLASS_NAME_PREFEX}pseudo-circle`;
const rules = {
[`.${PSEUDO_CLASS}::before, .${PSEUDO_CLASS}::after`]: `{
background: ${color} !important;
background-image: none !important;
color: transparent !important;
border-color: transparent !important;
}`,
[`.${PSEUDO_RECT_CLASS}::before, .${PSEUDO_RECT_CLASS}::after`]: `{
border-radius: 0 !important;
}`,
[`.${PSEUDO_CIRCLE_CLASS}::before, .${PSEUDO_CIRCLE_CLASS}::after`]: `{
border-radius: 50% !important;
}`
};
Object.keys(rules).forEach(key => {
addStyle(key, rules[key]);
});
addClassName(ele, [PSEUDO_CLASS, finalShape === 'circle' ? PSEUDO_CIRCLE_CLASS : PSEUDO_RECT_CLASS]);
}
複製代碼
// 文本行數 =( 高度 - 上下padding ) / 行高
const lineCount = (height - parseFloat(paddingTop, 10) - parseFloat(paddingBottom, 10)) / parseFloat(lineHeight, 10) | 0; // eslint-disable-line no-bitwise
// 文本高度比 = 字體高度/行高
let textHeightRatio = parseFloat(fontSize, 10) / parseFloat(lineHeight, 10);
if (Number.isNaN(textHeightRatio)) {
textHeightRatio = 1 / 1.4; // default number
}
複製代碼
經過線性漸變生成條紋背景的文本塊:
html
const firstColorPoint = ((1 - textHeightRatio) / 2 * 100).toFixed(decimal);
const secondColorPoint = (((1 - textHeightRatio) / 2 + textHeightRatio) * 100).toFixed(decimal);
const backgroundSize = `100% ${px2relativeUtil(lineHeight, cssUnit, decimal)}`;
const className = CLASS_NAME_PREFEX + 'text-' + firstColorPoint.toString(32).replace(/\./g, '-');
const rule = `{ background-image: linear-gradient(transparent ${firstColorPoint}%, ${color} 0%, ${color} ${secondColorPoint}%, transparent 0%) !important; background-size: ${backgroundSize}; position: ${position} !important; }`;
複製代碼
單行文本須要計算文本寬度和text-aligin屬性
java
const textWidthPercent = textWidth / (width - parseInt(paddingRight, 10) - parseInt(paddingLeft, 10));
ele.style.backgroundSize = `${(textWidthPercent > 1 ? 1 : textWidthPercent) * 100}% ${px2relativeUtil(lineHeight, cssUnit, decimal)}`;
switch (textAlign) {
case 'left': // do nothing
break
case 'center':
ele.style.backgroundPositionX = '50%';
break
case 'right':
ele.style.backgroundPositionX = '100%';
break
}
複製代碼
我抽空把生成骨架屏的邏輯單獨抽出來,方便你們定製對骨架屏的工程化處理及調試node
github.com/wookaoer/pa…
webpack