Принцип автоматической генерации каркасного экрана на основе анализа page-skeleton-webpack-plugin

Webpack

1. page-skeleton-webpack-plugin

page-skeleton-webpack-pluginЭто подключаемый модуль веб-пакета, разработанный командой ElemeFE.Целью этого подключаемого модуля является создание соответствующих страниц экрана скелета в соответствии с различными страницами маршрутизации в вашем проекте и упаковка страниц экрана скелета в соответствующие статические страницы маршрутизации через веб-пакет. .

2. Основной принцип плагина автоматически генерировать каркас экрана


  1. Откройте страницу, чтобы сгенерировать экран-скелет через безголовый кукловод браузера.
  2. После рендеринга страницы введите скрипт для извлечения скелетного экрана (примечание: вы должны дождаться полного рендеринга страницы, иначе извлеченный DOM будет неполным)
  3. Удаляйте или добавляйте элементы на страницу и покрывайте существующие элементы каскадными стилями, чтобы скрыть изображения и текст без изменения макета страницы, и покрывайте их стилями, чтобы они отображались в виде серых блоков. Затем извлеките измененные стили HTML и CSS, чтобы создать каркас экрана.
Во-первых, демонстрация показывает, как автоматически генерировать экран-скелет, а затем анализирует, как сгенерировать экран-скелет с помощью кода:


Установите среду выполнения
Зависимая среда:
  • puppeteer
  • nodejs v8.x
Чтобы установить Puppeteer, обратитесь к:Эй, краткое введение .com / боялся / ах 9 ах 55 от 03 ...
Запустите puppeteer и откройте страницу, на которой вы хотите создать скелетный экран.

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

Затем проанализируйте, как makeSkeleton генерирует скелетный код экрана.
Код входа находится вpage-skeleton-webpack-plugin/src/skeleton.js
  1. Вставьте скрипты в puppeteer через page.addScriptTag и инициализируйте их, путь к скрипту находится вpage-skeleton-webpack-plugin/src/script/index.js.
  2. Выполните метод genSkeleton для создания каркасного экрана.

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

  • Рекурсивно перемещайтесь по дереву DOM и классифицируйте DOM на текстовые блоки, блоки кнопок, блоки изображений, блоки SVG, блоки элементов псевдокласса и т. д.

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

То, как генерируется скелетная структура каждого блока, будет проанализировано один за другим.

1. Блоки SVG генерируют скелетную структуру

  • Определите, является ли элемент svg невидимым, и если он не виден, удалите элемент напрямую

// 宽高为0或设置隐藏的元素直接移除(aria是为残疾人士等提供无障碍访问动态、可交互Web内容的技术规范)
if (width === 0 || height === 0 || ele.getAttribute('aria-hidden') === 'true') {
   return removeElement(ele)
}

Для не скрытых элементов все элементы внутри элемента SVG будут удалены, уменьшая громкость окончательного генерируемого каркаса, а во-вторых, устанавливая ширину, высоту и форму элемента SVG.

// 设置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);
}

2. Блок кнопок создает каркасную структуру

Обработка блока кнопок относительно проста, убираем границу и тень, устанавливаем однородный цвет фона и текста, и блок кнопок обрабатывается.

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

3. Фоновый блок для создания каркасной структуры

Фоновый блок относится к элементу с фоновым изображением или фоновым цветом. Установите цвет фона равномерно.

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

4. Блок изображения для создания каркасной структуры

  • Установите ширину и высоту элемента, закодированное в base64 значение прозрачного изображения gif размером 1 * 1 пиксель, чтобы заполнить изображение.
  • Установить цвет фона, форму
  • Удалить бесполезные атрибуты (alt)

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

5. Блоки псевдоэлементов имеют дело со структурой скелета.

  • Псевдоэлементы ::before и ::after удаляют фоновое изображение и унифицируют цвет фона до прозрачного фона.
  • Установите форму (прямоугольник или закругленные углы)

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

6. Каркасная структура обработки текстового блока

С текстовыми блоками работать сложнее, поэтому поместим их в конец.
Определение текстового блока: любой элемент, содержащий текстовый узел, является текстовым блоком.
Вычислить количество текстовых строк и высоту текста текстового блока (то есть высоту отрисовываемого текстового блока = fontSize):
  • Рассчитать количество строк текста (высота элемента - верхний и нижний отступы)/высота строки
  • Рассчитать соотношение высоты текста = высота шрифта / высота строки (по умолчанию 1 / 1,4)

// 文本行数 =( 高度 - 上下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
}

Создайте текстовый блок с полосатым фоном с помощью линейного градиента:

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.

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
}

Вышеуказанное является основная логика Plug of Elementui с открытым исходным исходным платежом. Конечно, есть также логика, связанная с техникой, которая здесь не размещена, а также можно обсудить позже.

Я потратил время на извлечение логики генерации скелетного экрана отдельно, чтобы можно было настроить инженерную обработку и отладку скелетного экрана.

GitHub.com/Я, о Каур/Боюсь…


Справочная статья: