A, page – the skeleton – webpack – the plugin

page-skeleton-webpack-pluginElemeFE is a Webpack plug-in developed by ElemeFE team. The purpose of this plug-in is to generate corresponding skeleton screen pages according to different routing pages in your project, and pack the skeleton screen pages into the corresponding static routing pages through Webpack.

Second, the main principle of plug-in automatic skeleton screen generation



  1. Open the page to generate the skeleton screen using the headless browser puppeteer
  2. Inject a script to extract the skeleton screen after the page is rendered (note: it must wait until the page is completely rendered, otherwise the EXTRACTED DOM is incomplete)
  3. The elements in the page are deleted or added, and the existing elements are covered by cascading styles, so as to hide pictures and words without changing the page layout, and make them displayed as gray blocks by covering them with styles. The modified HTML and CSS styles are then extracted to generate a skeleton screen.
First demo shows how to automatically generate skeleton screen, and then through the code specific analysis of how to generate skeleton screen:



Install operating environment
Dependent environment:
  • puppeteer
  • nodejs v8.x
Install the puppeteer can refer to: www.jianshu.com/p/a9a55c03f…
Launch the Puppeteer and open the page where you want to generate the skeleton screen

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({
        // Set the timeout period
        timeout: 15000.// This property ignores HTTPS errors if you are accessing HTTPS pages
        ignoreHTTPSErrors: true.// Open developer tools. When this value is true, headless is always false
        devtools: true.// Non-headless mode, in order to visually see the process of creating a skeleton screen
        headless: false
    }));
    const page = await browser.newPage();
    // since it is mobile, set it to simulate iphone6
    await page.emulate(iPhone);
    // Open the homepage of m station
    await page.goto('https://m.to8to.com/sz');
    // Wait for the first screen bannar load to complete
    await page.waitForSelector('.ad-data-report-carousel');
    // Start building the skeleton screen
    awaitskeleton.makeSkeleton(page); }) ();Copy the code

MakeSkeleton generates skeleton screen code
Entry code in
page-skeleton-webpack-plugin/src/skeleton.js
  1. Through page. Injections into puppeteer addScriptTag script and initialization, the script path on the page – the skeleton – webpack – the plugin/SRC/script/index. Js.
  2. Execute the genSkeleton method to generate a skeleton screen

async makeSkeleton(page) {
    const {defer} = this.options
    // Inject the skeleton screen generation code into the puppeteer while performing the initialization
    await page.addScriptTag({content: this.scriptContent})
    I already used waitForSelector in Figure 1, so I can ignore this
    await sleep(defer)
    // Execute the genSkeleton method
    await page.evaluate((options) = > {
      Skeleton.genSkeleton(options)
    }, this.options)
  }
Copy the code

Initialize core logic:
  • Initialization parameters:

const pluginDefaultConfig = {
    port: '8989'.// This configuration object can be configured with a color field, used to determine the skeleton page text block color, color value can be hexadecimal, RGB, etc.
    text: {
        color: '#EEEEEE'
    },
    // This configuration accepts three fields, color, Shape, and shapeOpposite. Color and Shape are used to determine the color and shape of the image blocks in the skeleton page.
    // Color values support hexadecimal and RGB etc. Shapes support two enumerated values, circle (rectangle) and rect (circle).
    // The shapeOpposite field takes an array, and each element in the array is a DOM selector used to select DOM elements,
    // The selected DOM shape will be the opposite of the configured shape, for example, rect.
    // Image blocks in shapeOpposite will appear as circles in the skeleton page, see the default configuration at the end of this section for details.
    image: {
        shape: 'rect'.// `rect` | `circle`
        color: '#EFEFEF'.shapeOpposite: []},// This configuration accepts two fields, color and excludes. Color is used to determine what color is considered a button block in the skeleton page,
    // Excludes accepts an array of elements that are DOM selectors, used to select elements that will not be treated as button blocks
    button: {
        color: '#EFEFEF'.excludes: []},// This configuration accepts three fields, color, Shape, and shapeOpposite. Color and Shape are used to determine the color and shape of SVG blocks in skeleton pages,
    // The color value can be hexadecimal and RGB, as well as transparent enumerated values.
    // SVG blocks will be transparent blocks. Shapes support two enumerated values, circle (rectangle) and RECt (circle).
    // The shapeOpposite field takes an array, and each element in the array is a DOM selector used to select DOM elements,
    // The selected DOM shape will be the opposite of the configured shape, for example, rect.
    // The SVG block in shapeOpposite will appear as a circle in the skeleton page. See the default configuration at the end of this section for details.
    svg: {
        color: '#EFEFEF'.shape: 'circle'.// circle | rect
        shapeOpposite: []
    },
    // This configuration accepts two fields, color and shape. Color is used to determine the color of a skeleton page that is considered a pseudo-element block,
    // Shape is used to set the shape of the pseudo-element block. It accepts two enumerated values: circle and 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.// If you have an element that doesn't need to be skeletoned, write the element's CSS selector to the array.
    excludes: [],
    // For elements that need to be removed from the DOM without generating a page skeleton, set the value to a CSS selector for removing the element.
    remove: [],
    // It does not need to be removed, but hides the element by setting its transparency to 0, setting the value to the CSS selector of the hidden element.
    hide: [],
    // The elements in this array are CSS selectors. The selected elements are processed into a color block by the plugin. The color block is the same color as the button block. Internal elements are no longer special and text is hidden.
    grayBlock: [],
    cookies: [].// It accepts enumeration values rem, vw, vh, vmin, vmax.
    cssUnit: 'rem'.// Generate the number of decimal places reserved for CSS values in the skeleton page (shell.html). The default value is 4.
    decimal: 4.logLevel: 'info'.quiet: false.noInfo: false.logTime: true
};
Copy the code

  • Recursively traverses the DOM tree, classifying the DOM into text blocks, button blocks, image blocks, SVG blocks, pseudo-class element blocks, and so on.

/ / ele for the document. The documentElement; Recursively traverses the DOM tree; (function preTraverse(ele) {
  // styles is a list of all available CSS attributes in the element
  const styles = getComputedStyle(ele);
  // Check if the element has false elements
  const hasPseudoEle = checkHasPseudoEle(ele);

  // Determine if the element is in the viewable area (if it is a first screen element), non-first screen elements will be removed
  if(! inViewPort(ele) || DISPLAY_NONE.test(ele.getAttribute('style'))) {
	return toRemove.push(ele)
  }

  // Customize the elements to be processed as color blocks
  if (~grayEle.indexOf(ele)) { // eslint-disable-line no-bitwise
	return grayBlocks.push(ele)
  }

  // Customize elements that do not need to be handled as skeletons
  if (~excludesEle.indexOf(ele)) return false // eslint-disable-line no-bitwise

  if (hasPseudoEle) {
	pseudos.push(hasPseudoEle);
  }

  if (checkHasBorder(styles)) {
	ele.style.border = 'none';
  }

  // List elements are treated as default
  if (ele.children.length > 0 && /UL|OL/.test(ele.tagName)) {
	listHandle(ele);
  }

  // There is child node traversal processing
  if (ele.children && ele.children.length > 0) {
	Array.from(ele.children).forEach(child= > preTraverse(child));
  }

  // Set the text color of all elements that have children of textChildNode to the background color so that no text is displayed.
  if (ele.childNodes && Array.from(ele.childNodes).some(n= > n.nodeType === Node.TEXT_NODE)) {
	transparent(ele);
  }

  // Unify the color of the text underline
  if (checkHasTextDecoration(styles)) {
	ele.style.textDecorationColor = TRANSPARENT;
  }
  // Hide all SVG elements
  if (ele.tagName === 'svg') {
	return svgs.push(ele)
  }

  // An element with a background color or background image
  if (EXT_REG.test(styles.background) || EXT_REG.test(styles.backgroundImage)) {
	return hasImageBackEles.push(ele)
  }
  // Background gradient element
  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));
Copy the code

  • Processed the classified text block, picture block and so on to generate skeleton structure code

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));
Copy the code

How does the skeleton structure of each piece come into being and then we’ll look at it one by one

1. SVG blocks generate skeleton structures

  • Determine if an SVG element is not visible, and delete the element if not

// Set width and height to 0 or set hidden elements to be removed directly (ARIA is a technical specification for providing barrierless access to dynamic, interactive Web content for people with disabilities, etc.)
if (width === 0 || height === 0 || ele.getAttribute('aria-hidden') = = ='true') {
   return removeElement(ele)
}
Copy the code

For unhidden elements, all elements inside the SVG element are removed, reducing the resulting skeleton page size. Second, the width, height, shape, and so on of the SVG element are set.

// Sets the final shape of the element shapeOpposite to the shape configuration
const finalShape = shapeOpposite.indexOf(ele) > - 1 ? getOppositeShape(shape) : shape;

// Empty the inner structure of the element.
emptyElement(ele);

const shapeClassName = CLASS_NAME_PREFEX + shape;
// Set the border-radius property according to rect or cirle and set to styleCache
shapeStyle(shape);

Object.assign(ele.style, {
  width: px2relativeUtil(width, cssUnit, decimal),
  height: px2relativeUtil(height, cssUnit, decimal),
});

addClassName(ele, [shapeClassName]);

// color is the color attribute in your custom SVG configuration. You can set hexadecimal Settings and transparent enumeration values
if (color === TRANSPARENT) {
  // Set to transparent block
  setOpacity(ele);
} else {
  // Set the background color
  const className = CLASS_NAME_PREFEX + 'svg';
  const rule = `{
  background: ${color}! important; } `;
  addStyle(`.${className}`, rule);
  ele.classList.add(className);
}
Copy the code

2. The button block generates the skeleton structure

Button block processing is relatively simple, remove the border and shadow, set a uniform background color and text, button block processing is complete.

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);
}
Copy the code

3. The background block generates skeleton structure

A background block is an element that has a background image or background color. Set the background color uniformly.

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

4. Picture blocks generate skeleton structure

  • Set the element width and height, base64 encoded value of 1*1 pixel transparent GIF image to fill the image
  • Set the background color and shape
  • Remove useless attributes (Alt)

function imgHandler(ele, {color, shape, shapeOpposite}) {
    const {width, height} = ele.getBoundingClientRect();
    const attrs = {
        width,
        height,
        src: SMALLEST_BASE64 // 1 x 1 pixel transparent GIF image
    };

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

5, pseudo-element block processing skeleton structure

  • Pseudo-elements ::before and ::after remove the background image and unify to a transparent background color
  • Set shape (rectangle or rounded corner)

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

6. Text blocks handle skeleton structures

Text blocks are a little more complicated to work with, so I’ll leave it to the end.
Text block definition: Any element that contains a text node is a text block.
Calculate the number of text lines in the text block, the height of the text block (i.e. the height of the text block to draw =fontSize) :
  • Calculates the number of lines of text (element height – upper and lower padding)/line height
  • Calculate text height ratio = font height/line height (default 1/1.4)

// Number of lines of text = (height - upper and lower padding)/line height
const lineCount = (height - parseFloat(paddingTop, 10) - parseFloat(paddingBottom, 10)) / parseFloat(lineHeight, 10) | 0; // eslint-disable-line no-bitwise

// Text height ratio = font height/line height
let textHeightRatio = parseFloat(fontSize, 10) / parseFloat(lineHeight, 10);
if (Number.isNaN(textHeightRatio)) {
  textHeightRatio = 1 / 1.4; // default number}Copy the code

Generate a block of text with a linear gradient for a striped background:

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.The '-');

const rule = `{
    background-image: linear-gradient(transparent ${firstColorPoint}%, ${color} 0%, ${color} ${secondColorPoint}%, transparent 0%) ! important; background-size:${backgroundSize};
    position: ${position}! important; } `;
Copy the code

Single-line text requires calculation of the text width and text-aligin attributes

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
}
Copy the code

That’s the logic behind elementUI’s open-source skeleton screen plugin. Of course, the logic related to engineering is not posted here, and can be discussed later.

I took the time to generate the skeleton screen logic separate out, convenient for everyone to customize the skeleton screen engineering processing and debugging

Github.com/wookaoer/pa…



Reference article:
Github.com/Jocs/jocs.g…
My.oschina.net/reamd7/blog…