Автоматически генерировать скелетные страницы H5

внешний интерфейс SVG HTML CSS
Автоматически генерировать скелетные страницы H5

Страница скелета относится к тому, когда вы открываете мобильную веб-страницу, прежде чем страница будет проанализирована и данные загружены, общий стиль страницы сначала отображается пользователю. На странице-скелете изображения, текст и значки будут отображаться через серые прямоугольные блоки.Прежде чем отобразится реальная страница, пользователь может воспринимать базовый стиль CSS и макет загружаемой страницы. Скелет мобильной веб-страницы Ele.me показан на заглавном изображении.

Эта статья объяснит читателю схему автоматического создания страницы-скелета, показанную выше.Благодаря этой схеме автоматическое создание страницы-скелета можно идеально сочетать с вашим процессом разработки.

Зачем нужны скелетные страницы

Сначала мы разобьем проблему на два подвопроса, во-первых, зачем нам скелетные страницы.

  • Как упоминалось выше, скелетная страница предназначена для показа стиля CSS и макета страницы пользователю до того, как страница будет фактически проанализирована и приложение будет запущено, и посредством светлых и темных изменений скелетной страницы он информирует пользователя. что страница пытается загрузиться, чтобы пользователь мог воспринимать страницу так, как будто загрузка идет быстрее, чем раньше. Когда приложение запускается и данные собираются, скелетная страница заменяется страницей с реальными данными.
  • До появления скелетной страницы многие приложения использовали значок «Загрузка», чтобы сообщить пользователю, что данные загружаются, подождите, но пользователь не мог воспринимать страницу, которая должна быть представлена ​​в это время, и не мог определить время ожидания. Иконка загрузки уже вызвала у пользователей эстетическую усталость, а длительное ожидание вызвало у пользователей страх перед ожиданием.Согласно исследованию Google Research, 53% пользователей предпочитают закрыть веб-страницу или приложение после ожидания загрузки в течение 3 секунд. , что приводит к потере пользователя. Страница скелета заставляет пользователей чувствовать, что данные были загружены, но они все еще находятся в процессе рендеринга, поэтому пользователи чувствуют, что страница загружается быстрее, чем раньше. В то же время, поскольку стиль и макет страницы-скелета и реальной страницы абсолютно одинаковы, страница-скелет может плавно переключаться на страницу, визуализируемую реальными данными с точки зрения визуального восприятия пользователя. если через Значок загрузки переключается на конечную страницу, которая будет резкой в ​​восприятии пользователя.
  • Глядя на текущий интерфейсный фреймворк, он ужеReact,Vue,AngularПри трехстороннем подходе большинство фронтенд-приложений на рынке основаны на этих трех фреймворках или библиотеках и соответствующей экосистеме.Фронтенд-проекты Ele.me не являются исключением, напримерМобильный H5используетсяVueбиблиотека. У этих трех фреймворков есть общая черта: все они основаны на JS: до парсинга JS-кода страница не будет отображать никакого контента, это так называемый белый экран. Пользователям не нравится видеть белый экран, ничего не отображается, и пользователи, скорее всего, заподозрят, что с сетью или приложением что-то не так. Возьмите Vue в качестве примера: при применении начальной загрузки Vue будет передавать данные в компоненте и значение состояния в вычисляемомObject.definePropertyМетод преобразуется в set и получает доступ к свойствам для отслеживания изменений данных. И этот процесс завершается при запуске приложения, что неизбежно приведет к тому, что фаза запуска страницы будет медленнее, чем страницы, которые не управляются JS (например, приложения jQuery).

Второй подвопрос, зачем нужно автоматизировать генерацию скелетных страниц.

На самом деле причина очень проста: все программисты «ленивые», и ни один программист не захочет повторять ту же или подобную работу, даже если они «добавят денег». А написание страниц-скелетов вручную — это именно такая работа, повторение без нововведений. Поскольку стиль и макет страницы-скелета такие же, как и у страницы, отображаемой с реальными данными, но нет заполнения изображениями, текстом и картинками, почему бы не повторно использовать стиль и макет страницы? Почему бы не создавать автоматически скелетные страницы на основе реальных страниц с помощью инструментов? Это экономит не только наше собственное время, но и экономит трудозатраты компании, так почему бы и не сделать!

Создание скелетных страниц с помощью puppeteer

Базовая схема генерации каркасных страниц

пройти черезpuppeteerКонтроль на сервереheadlessChrome открывает разрабатываемую страницу, которая должна создать скелетную страницу. После ожидания загрузки и отображения страницы, исходя из предпосылки сохранения стиля макета страницы, существующие элементы покрываются каскадными стилями путем удаления или добавления элементов в Таким образом, отображение рисунков, текста и рисунков можно скрыть без изменения макета страницы, и они могут отображаться в виде серых блоков посредством покрытия стиля. Затем модифицированный Извлекаются стили HTML и CSS, и это скелет страницы.

Приведенное выше описание принципа создания страницы-скелета может показаться немного расплывчатым, и последующее будет проходить через некоторыеpage-skeleton-webpack-plugin(называемый PSWP) в конкретных фрагментах кода, чтобы проиллюстрировать создание скелетных страниц. PSWP – это инструмент для создания каркасных страниц во внешнем интерфейсе Ele.me. Внутренняя команда уже использует его, и проект находится в активной разработке. Исходный код еще не открыт, так что следите за обновлениями.

Прежде чем объяснять конкретное создание страницы скелета, сначала разберитесь с puppeteer, который представлен на GitHub.

Puppeteer is a Node library which provides a high-level API to control headless Chrome or Chromium over theDevTools Protocol. It can also be configured to use full (non-headless) Chrome or Chromium.

Если вы относительно новичок в Puppeteer, рекомендуется узнать о нем, прежде чем продолжать читать эту статью.Puppeteer API, я буду ждать твоего возвращения ⏰.

Шаг 1: Запустите страницу через Puppeteer

Прежде чем начать генерировать страницу скелета, нужно запустить страницу через Puppeteer, в PSWP черезSkeletonкласс для инкапсуляции различных методов создания скелетных страниц. код показывает, как показано ниже:

// ./skeleton.js
class Skeleton {
  constructor(options = {}) {
    this.options = options
    this.browser = null
    this.page = null
  }

  async initPage() {
        // 第一步:用于启动一个页面
  }

  async makeSkeleton() {
      // 第二步:构建骨架页面
  }

  async genHtml(url) {
    // 第三步:根据构建的骨架页面,获取 HTML 和 CSS
  }
}
module.exports = Skeleton

Перед запуском страницы мы можем настроить мобильное устройство, на котором мы хотим создать страницу скелета.Дополнительные устройства можно настроить через проект Puppeteer.DeviceDescriptors. Значение по умолчанию в PSWP:iphone 6 Plus, конечно, вы можете выбрать устройство, которое чаще всего используют ваши целевые пользователи в настоящее время. PSWP поддерживает только конфигурацию с одним устройством. Код стартовой страницы выглядит следующим образом:

// ./skeleton.js 
async initPage() {
    const { device, headless, debug } = this.options
    const browser = await puppeteer.launch({ headless })
    const page = await browser.newPage()
        // 设置模拟设备
    await page.emulate(devices[device])
    this.browser = browser
    this.page = page
    if (debug) {
      page.on('console', (...args) => {
        // do something with args
      })
    }

    return this.page
  }

Как видно из приведенного выше кода, мы можем передатьheadlessНастройте, чтобы выбрать, открывать ли безголовый Chrome,debugНастраивает, следует ли печатать сообщения об ошибках на терминале.

Шаг 2: Создайте страницу скелета

На этом этапе наша основная работа заключается в покрытии стиля CSS разрабатываемой страницы и добавлении или удалении элементов для создания скелета страницы. Спасибо Puppeteer за хороший APIpage.addScriptTag, который может передавать код JavaScript черезScriptТег вставляется на предыдущем шаге, чтобы открыть страницу, поэтому мы можем вызывать атрибут объекта спецификации прямым методом, код выглядит следующим образом:

// ./skeleton.js
async makeSkeleton() {
   const { defer } = this.options
   const content = await genScriptContent()

   // 将生产骨架页面的 js 代码插入到 page 中
   await this.page.addScriptTag({ content })
   await sleep(defer)
   await this.page.evaluate(async (options) => {
     const { genSkeleton } = Skeleton
     genSkeleton(options)
   }, this.options)
}

genScriptContentМетод используется для получения исходного кода JS, вставленного в страницу.Также следует отметить, что существуетdeferКонфигурация используется, чтобы указать Puppeteer время ожидания после открытия страницы. Это связано с тем, что после открытия разрабатываемой страницы некоторый контент на странице не был загружен. Если скелетная страница создается до этого, это, вероятно, вызовет окончательное поколение. Страница-скелет не соответствует реальной странице. Делает генерацию страницы скелета неудачной.

На самом деле ядро ​​создания страницы-скелета — это JS-скрипт, вставленный в страницу. Ниже автор сосредоточится на том, как создать страницу-скелет.

Генерация каркасных страниц в основном происходит вgenSkeletonметод, который прописан в скрипте, вставленном на страницу и связанномwindowобъект, поэтому мы можем вызвать его напрямую.

В схеме создания страницы-скелета страница сначала делится на разные блоки в соответствии с разными элементами.Детали блока следующие:

  • текстовый блок: элементы DOM, содержащие уникальные текстовые узлы, обрабатываются как текстовые блоки.
  • блок изображения: элементы IMG или элементы с фоновым изображением обрабатываются как блоки изображения.
  • SVG-блоки: элементы SVG обрабатываются как блоки SVG.
  • Блок псевдоэлементов:::before,::afterПоскольку элементы псевдотипа также будут отображаться на странице, они также нуждаются в обработке и рассматриваются как блоки псевдоэлементов.
  • блок кнопок: Такие элементы, как BUTTON, INPUT [type=button], A [role=button], считаются блоками кнопок.role=buttonЭлемент A рассматривается как блок кнопок, фактически, если вам нужно рассматривать элемент A как кнопку, добавьтеrole=buttonФункции необходимы, что также отвечает требованиям доступности интерфейса.

После того, как элемент разделен на разные блоки, следующий шаг - обработка блоков по отдельности, включает в себя обложку и изменение стиля в элементах, цель только одна, которая состоит в том, чтобы сформировать скелет в блоки страниц, т.е. прямо на фиг название выглядит , из-за ограниченного объема статьи, эта статья толькотекстовый блока такжекартинаОбъясняет, как блок генерирует стиль скелета с помощью определенного алгоритма.

Алгоритм генерации текстового блока

Чтобы сгенерировать серые полосы текстового блока, сначала нам нужно знать высоту текстового блока, чтобы мы могли нарисовать высоту серых полос, высоту серых полос в текстовом блоке можно получить с помощьюfontSizeВ то же время, если это текстовый блок, сформированный многострочным текстом, такой текстовый блок также должен быть многострочным.Также нам нужно знать межстрочный интервал в текстовом блоке.К счастью, междустрочный интервал также легко чтобы получить.

lineHeight - fontSize - межстрочный интервал

При многострочном тексте, чтобы нарисовать серые полосы, нам также нужно знать, сколько строк текста, чтобы мы знали, сколько серых полос нужно нарисовать.Количество строк текста можно рассчитать по следующей формуле:

contentHeight = ClientHeight - paddingTop - paddingBottom
lineNumber = contentHeight / lineHeight

В приведенной выше формуле мы сначала вычисляем высоту содержимого текстового блока, вычитая paddingTop и paddingBottom из ClientHeight, а ClientHeight получаетсяgetBoundingClientRectПолучение API, paddingTop , paddingBottom и lineHeight можно получить черезgetComputedStyleНаконец, мы можем вычислить количество строк текста в текстовом блоке, разделив contentHeight на lineHeight.

имеютМежстрочный интервал,высота строки,так же какКоличество строк в текстовом блокеЗатем мы можем нарисовать наши серые полосы.

Я думаю многие читали@Lea VerouизCSS SecretsВ этой книге есть статья, посвященная тому, как создать полосатый фон с помощью линейных градиентов, и в этой статье на отрисовку серых полос в текстовом блоке также влияетCSS SecretsВдохновленный линейным градиентом для рисования серых текстовых полос. код показывает, как показано ниже:

const comStyle = window.getComputedStyle(ele)
const text = ele.textContent
let {
  lineHeight,
  paddingTop,
  paddingRight,
  paddingBottom,
  paddingLeft,
  position: pos,
  fontSize,
  textAlign,
  wordSpacing,
  wordBreak
} = comStyle

const height = ele.offsetHeight
// 向下取整
const lineCount = (height - parseInt(paddingTop, 10) -
     parseInt(paddingBottom, 10)) / parseInt(lineHeight, 10) | 0

let textHeightRatio = parseInt(fontSize, 10) / parseInt(lineHeight, 10)

Object.assign(ele.style, {
   backgroundImage: `linear-gradient(
     transparent ${(1 - textHeightRatio) / 2 * 100}%,
     ${color} 0%,
     ${color} ${((1 - textHeightRatio) / 2 + textHeightRatio) * 100}%,
     transparent 0%)`,
   backgroundOrigin: 'content-box',
   backgroundSize: `100% ${lineHeight}`,
   backgroundClip: 'content-box',
   backgroundColor: 'transparent',
   position,
   color: 'transparent',
   backgroundRepeat: 'repeat-y'
 })

Как упоминалось выше, сначала мы считаем количество строкlineCount, а отношение высоты текста ко всей высоте строки вычисляется по fontSize и lineHeight,textHeightRatioТаким образом, мы знаем точку разграничения градиента серых полос, как сказала @Lea Verou:

Взято из: Секреты CSS
«Если у цветовой остановки есть положение, которое меньше, чем у специализированного положения любой цветовой остановки перед ним в списке, установить его положение, которое будет равным наибольшему указанному положению любого цвета.
— CSS-изображения уровня 3 (Я 3.org/TR/CSS3-IMA…)

То есть в линейном градиенте, если мы установим начальную точку линейного градиента меньше, чем начальное значение предыдущей цветовой точки, или установим на 0%, то линейный градиент исчезнет и вместо этого будет два разных цвета. Полосы, что означает отсутствие линейных градиентов.

Когда мы рисуем текстовый блок, ширина backgroundSize равна 100%, а высота равна lineHeight, то есть высота серой полосы плюс прозрачная полоса равна lineHeight. Несмотря на то, что мы нарисовали серые полосы, наш текст все еще отображается. Прежде чем появится окончательный эффект стиля скелета, нам также нужно скрыть текст и установитьcolor:‘transparent’Таким образом, наш текст имеет тот же цвет, что и фон, а окончательный вид — серые полосы.

При обработке одной строки текста, так как ширина текста не имеет ширины всей строки, для отдельной строки текста нам также необходимо вычислить ширину текста, а затем установить ширину серого полосы в качестве ширины текста, так что эффект стиля скелета может быть лучше Близко к стилю текста.

Код для вычисления ширины текста выглядит следующим образом:

const getTextWidth = (text, style) => {
    let offScreenParagraph = document.querySelector(`#${MOCK_TEXT_ID}`)
    if (!offScreenParagraph) {
      const wrapper = document.createElement('p')
      offScreenParagraph = document.createElement('span')
      Object.assign(wrapper.style, {
        width: '10000px'
      })
      offScreenParagraph.id = MOCK_TEXT_ID
      wrapper.appendChild(offScreenParagraph)
      document.body.appendChild(wrapper)
    }
    Object.assign(offScreenParagraph.style, style)
    offScreenParagraph.textContent = text
    return offScreenParagraph.getBoundingClientRect().width
  }

Здесь используется небольшая хитрость: мы создаем элемент SPAN на странице, затем элементу SPAN присваиваем стиль исходного текста, а текстовое содержимое помещаем в элемент SPAN, чтобы ширина элемента SPAN равнялась ширине текста. . Наконец, мы рисуем ширину серых полос в соответствии с шириной текста.

const textWidth = getTextWidth(text, { fontSize, lineHeight, wordBreak, wordSpacing })
const textWidthPercent = textWidth / (width - parseInt(paddingRight, 10) - parseInt(paddingLeft, 10))
ele.style.backgroundSize = `${textWidthPercent * 100}% ${px2rem(lineHeight)}`
switch (textAlign) {
   case 'left': // do nothing
      break
   case 'center':
      ele.style.backgroundPositionX = '50%'
      break
   case 'right':
      ele.style.backgroundPositionX = '100%'
      break
 }

По ширине текста рассчитывается отношение текста к ширине содержимого всего элемента, по этому соотношению можно задать ширину серой полосы. Есть еще один момент, который требует особой обработки, нам нужноtextAlignустановить смещение фоновых полос по оси X, чтобы нарисованные серые полосы могли полностью совпадать с исходным текстом. Выше приведены все алгоритмы отрисовки всего текстового блока.Конечно некоторые детали опущены.Например в реальных проектах мы используемremединиц, поэтому нам также нужно преобразовать px в rem. То есть в коде вышеpx2remметод.

Алгоритм генерации блока изображения

Отрисовка графических блоков гораздо проще, чем текстовых, но я тоже наступил на некоторые ямы в процессе заказа плана.Здесь просто делюсь опытом добычи ям.

Первоначальный план состоит в том, чтобы заменить элемент IMG элементом DIV, а затем установить фон элемента DIV в серый цвет, а ширина и высота элемента DIV равны ширине и высоте исходного элемента IMG. серьезный недостаток: стиль, установленный на элементе IMG, не может быть применен к элементу DIV, что приводит к несоответствию эффекта скелета конечного блока изображения и реального изображения в стиле страницы, особенно он не может быть адаптирован к различным мобильным устройствам. , потому что ширина и высота DIV ограничены.

Затем мы попробовали, казалось бы, «продвинутый» метод, используя Canvas для рисования серого блока того же размера, что и исходное изображение, а затем преобразовав Canvas в dataUrl и присвоив его атрибуту src элемента IMG, чтобы элемент IMG отображается как Есть серый блок, который кажется идеальным. Когда мы генерируем файл HTML из сгенерированной страницы-скелета, мы ошеломлены. Размер файла превышает 200 КБ. Важной причиной для нас, чтобы отобразить страницу-скелет, чтобы надеюсь, что пользователи поймут, что я чувствую, что страница загружается быстрее.Если скелет страницы имеет размер более 200 КБ, это определенно приведет к тому, что страница будет загружаться медленнее, чем раньше, что противоречит нашему первоначальному замыслу, поэтому от этого решения можно только отказаться.

Окончательное предложение, мы решили преобразовать прозрачное изображение gif размером 1 * 1 пиксель в dataUrl, а затем присвоить его атрибуту src элемента IMG и установить характеристики ширины и высоты изображения равными ширине и высоте предыдущего изображения, и установите тон цвета фона.Что касается значений цвета, настроенных для стиля скелета, это решение отлично решает вышеуказанные проблемы.

// минимум 1 * 1 пиксель прозрачное изображение в формате gif
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'

Выше показан формат base64 размером 1*1 пиксель, что, очевидно, намного меньше, чем картинка, нарисованная через Canvas ранее.

Алгоритмы отрисовки SVG-блоков, блоков элементов псевдотипа и блоков кнопок повторно описывать не буду, если интересно, можете их установить.page-skeleton-webpack-pluginПрочтите исходный код.

Шаг 3: Получите HTML и CSS со скелетной страницы, созданной Puppeteer.

На втором этапе мы завершили отрисовку страницы-скелета, следующим шагом является получение HTML и CSS, а затем запись вshell.htmlв файле.

function getHtmlAndStyle() {
    const root = document.documentElement
    const rawHtml = root.outerHTML
    const styles = Array.from(?('style')).map(style => style.innerHTML || style.innerText)
    // ohter code
    const cleanedHtml = document.body.innerHTML
    return { rawHtml, styles, cleanedHtml }
}

Получить HTML и CSS относительно просто. Вы можете понять это, взглянув на приведенный выше код, но все еще есть проблема с полученными таким образом CSS и HTML. Не все стили HTML и CSS требуются для каркасных страниц, таких как элементы за пределами сгиба. Для страницы-скелета он вообще не нужен. Так как мы удалили некоторые элементы в процессе генерации страницы-скелета, а стили этих элементов все еще сохраняются на странице, то эти стили CSS тоже не нужны. Поэтому на этом этапе ключевым моментом является устранение ненужных элементов и стилей CSS, также известное как извлечение.Критический CSS.

Удалить элементы за пределами сгиба

const inViewPort = (ele) => {
  const rect = ele.getBoundingClientRect()
  return rect.top < window.innerHeight
    && rect.left < window.innerWidth
}

По вышеприведенному методу оценивается, находится ли элемент внутри первого экрана, если он внутри первого экрана, оставляем его, иначе удаляем.

Извлечь ключевой CSS

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

Сначала получите стиль CSS из элемента стиля, извлеките стиль из элемента ссылки, а затем передайтеcss-treeПроанализируйте извлеченные стили, проанализируйте все селекторы и правила CSS и передайтеquerySelectorметод для выбора селектора CSS, извлеченного выше, и удаления его, если результат querySelector равен нулю Правило, если вы можете его выбрать, сохраните его. код показывает, как показано ниже:

const checker = (selector) => {
  // other code
  if (/:{1,2}(before|after)/.test(selector)) {
    return true
  }
  try {
    const keep = !!document.querySelector(selector)
    return keep
  } catch (err) {
    const exception = err.toString()
    console.log(`Unable to querySelector('${selector}') [${exception}]`, 'error')
    return false
  }
}

Приведенный выше код используется для определения необходимости сохранения стиля CSS.Следует отметить, что все стили элементов псевдокласса сохраняются, поскольку querySelector не может выбирать элементы псевдокласса, а в процессе генерации скелетных страниц элементы псевдокласса также сохраняются.

Идеально подходит для Webpack

Чтобы реализовать автоматическую генерацию страниц-скелетов, необходимо объединить вышеуказанные шаги с нашим процессом разработки.В процессе разработки мы можем активно запускать генерацию страниц-скелетов, а на этапе упаковки и выпуска сгенерированный скелет страницы могут быть упакованы в окончательный проект. Это одна из причин, по которой скелет страницы превращается в плагин веб-пакета.

PSWP зависит отhtml-webpack-plugin, В настоящее время большинство интерфейсных проектов используют этот плагин. Важной причиной является то, что этот плагин избавляет нас от повторяющейся работы по ручной вставке JS и CSS в html. PSWP вставляет страницу скелета в index.html перед созданием проекта index.html, код выглядит следующим образом:

compilation.plugin('html-webpack-plugin-before-html-processing', async (htmlPluginData, callback) => {
  // replace `<!-- shell -->` with `shell code`
  try {
    const code = await getShellCode(this.options.pathname)
    htmlPluginData.html = htmlPluginData.html.replace('<!-- shell -->', code)
  } catch (err) {
    log(err.toString(), 'error')
  }
  callback(null, htmlPluginData)
})

Как следует из приведенного выше кода, на заключительном этапе упаковки он заменен каркасным шаблоном страницы, сгенерированным shell.html.<!—- shell —->Заметки, чтобы при повторном открытии страницы можно было увидеть скелет страницы.

Последние мысли

В процессе работы над проектом PSWP, наступив на некоторые ямы, резюмируя, когда мы пишем HTML, мы пытаемся выбрать семантические теги и добавить функции доступности, и пишем HTML в соответствии со спецификацией HTML. Поскольку HTML, в конце концов, является языком разметки, с помощью тегов с различной семантикой также могут быть переданы некоторые расширенные значения обернутого текстового содержимого, такие как тег LI для идентификации содержимого списка,role=buttonАтрибут означает, что элемент является кнопкой, так что мы можем отрисовать скелетный стиль в соответствии со спецификацией в процессе генерации скелетной страницы.

Ввиду ограниченного объема статьи эта статья не полностью охватывает все детали PSWP. Если вам интересно, пожалуйста, прочитайте исходный код напрямую. PSWP является экспериментальным проектом. Любые вопросы можно обсудить в области комментариев. .