(Исходный контент Ma Honeycomb Technology, общедоступный идентификатор: mfwtech)
Согласно исследовательскому отчету Akamai, после опроса 1048 онлайн-покупателей было обнаружено, что:
-
Около 47% пользователей ожидают, что их страницы загрузятся в течение двух секунд.
-
Около 40% пользователей захотят покинуть или закрыть страницу, если загрузка занимает более 3 секунд.
Долгое время, чтобы улучшить пользовательский опыт при загрузке страницы, фронтенд-разработчики проделали большую работу, будь то веб-приложения или iOS, Android-приложения. Помимо решения проблемы ускорения отображения веб-страниц, еще одним важным моментом является улучшение восприятия пользователями времени ожидания загрузки. "Диаграмма хризантемы" и производные от нее различные анимации загрузки - распространенное решение. Я думаю, что ни разработчикам, ни пользователям не знакома следующая иконка:
本文要介绍的「骨架屏」则被视为菊花图升级版的方案。 Inspired by the existing skeleton screen, the front-end R & D team of the horse's e-commerce has achieved a method of automating the generating skeleton screen, and the application is implemented in multiple pages of the horse nest shopping mall. It has achieved хорошие результаты.
1. Что такое каркасный экран
Экран скелета можно понимать как представление пользователю общей структуры текущей страницы, состоящей из серых и белых блоков, до того, как данные страницы не были возвращены или страница не была полностью визуализирована, так что пользователь может почувствовать, что страница постепенно рендерится, так что процесс загрузки визуально становится плавным. Сгенерированная скелетная страница экрана показана на следующем рисунке:
Основными преимуществами каркасного экрана являются:
- Пользователи избегают просмотра длинных белых страниц
- Вы можете знать общую структуру страницы, уменьшая вероятность того, что пользователи покинут страницу, потому что они думают, что страница неверна.
- По сравнению с диаграммой хризантемы зрение более плавное
2. Общие решения каркасного экрана переднего плана
Прежде чем выбрать каркасный экран, мы также рассмотрели некоторые другие методы, например, можно ли избежать внешнего белого экрана с помощью рендеринга на стороне сервера (SSR). Однако обнаруживается, что задействовано слишком много проектов, а также построение и развертывание сервисов; либо простой предварительный рендеринг обеспечивается через prerender-spa-plugin, который дружелюбен к поддержке SPA, но требует дополнительной настройки веб-пакета, и из-за проблем с исходным кодом пакета время загрузки слишком велико, иногда происходит необъяснимый сбой и т. д., от которых в конечном итоге отказываются по разным причинам.
После серии исследований мы кратко разобрали несколько распространенных в отрасли решений каркасных экранов, а также их преимущества и недостатки.
1. Скелет экрана пользовательского интерфейса
То есть предоставить изображение, которое соответствует стилю домашней страницы страницы через пользовательский интерфейс, чтобы действовать как каркасный экран, вставить изображение каркасного экрана base64 в корневой корневой узел и внедрить его в проект при упаковке веб-пакета.
Это простой и грубый метод, который относительно легко реализовать. Но недостаток также очевиден, то есть он требует поддержки дизайнера пользовательского интерфейса и вмешательства разработчиков и не может быть сгенерирован автоматически.
2. Скриншот рукописного ввода
То есть каркас экрана настраивается под целевую страницу с помощью рукописного HTML и CSS. Таким образом, реальный стиль страницы может быть скопирован. Однако после изменения стиля страницы по разным причинам необходимо снова изменить стиль и макет экрана скелета, что значительно увеличивает стоимость обслуживания.
3. Автоматически генерировать статический каркасный экран
В настоящее время все большее внимание привлекает плагин Ele.me с открытым исходным кодом page-skeleton-webpack-plugin, конкретный принцип реализации которого заключается в следующем:
- Создать экран скелета
Используйте Puppeteer для управления безручным Chrome, чтобы открыть страницу экрана скелета, которую необходимо создать.После ожидания загрузки страницы, исходя из предпосылки сохранения стиля макета страницы, путем добавления и удаления элементов на странице, существующие элементы покрыты каскадным стилем, так что они отображаются как серый блок. Затем извлеките измененный HTML и CSS, разделите страницу на различные области блоков, такие как текстовые блоки, блоки изображений, блоки кнопок, SVG, блоки элементов псевдокласса и т. д., и обработайте каждый блок отдельно, чтобы сделать его максимально близким. в блок. Исходная страница остается прежней. Здесь используется метод addScriptTag экземпляра страницы Puppeteer для вставки скрипта блока обработки в страницу, открытую безголовым Chrome.
Между фактически сгенерированной страницей экрана-скелета и исходной страницей все еще может быть разрыв.Плагин записывает экран-скелет в память через memory-fs, и сгенерированный экран-скелет можно редактировать и предварительно просматривать на странице предварительного просмотра. модификация завершена, нажмите кнопку «Создать», чтобы сгенерировать новый каркас экрана для записи в проект.
Сделайте фото для наглядности:
- Вставить экран скелета
После того, как структура DOM и CSS скелетного экрана сгенерированы в автономном режиме, они внедряются в узел в шаблоне (EJS) во время построения и вставляются в HTML в функции ловушки после отправки.
Схема генерации каркаса экрана page-skeleton-webpack-plguin может генерировать соответствующую страницу в соответствии с каркасом экрана проекта, различными страницами маршрутизации и каркасом экрана страницы, упакованными в соответствующие страницы с помощью веб-пакета статической маршрутизации.
Его недостатки:
-
Является ли время создания скелетного экрана точным из-за невозможности отслеживать возврат интерфейса во время фактического использования?
-
Сгенерированная страница напрямую связана с качеством структуры, написанной бизнес-персоналом, и часто требует вторичной ручной корректировки.
В связи с этим команда отдела исследований и разработок отдела сотовой электронной коммерции Ma надеется найти способ создания каркасного экрана, более удобного для разработки и улучшающего взаимодействие с пользователем.Он может автоматически генерировать аналогичные каркасные экраны для различных бизнес-сценариев. и реализовать автоматический впрыск. Для разработки вам нужно только выполнить команду или простую настройку для создания каркасного экрана, и вам не нужно учитывать последующие работы по обслуживанию.
В процессе исследования проекта,draw-page-structureВдохновил наши проекты.
4. draw-page-structure
- Сгенерировать каркасный экран:
// dps.config.js
{
url: 'https://baidu.com',
output: {
filepath: '/Users/famanoder/DrawPageStructure/example/index.html',
injectSelector: '#app'
},
background: '#eee',
animation: 'opacity 1s linear infinite;',
// ...
}
В соответствии с онлайн-адресом, указанным в URL-адресе, сотрудничайте с Puppeteer, чтобы получить структуру DOM текущей страницы, и сгенерируйте файл экрана скелета для узла элемента в файле, указанном путем к файлу, затем можно создать страницу экрана скелета, и результат показан на следующем рисунке:
-
Вставить экран скелета
Вставьте сгенерированный выше файл экрана-скелета в узел с id="app" под корневым узлом страницы, а затем предоставьте метод активного уничтожения экрана-скелета в общем инструменте, который может помочь в разработке активного управления или разрушение каркаса экрана и отображение реального содержимого страницы.
Идея дизайна draw-page-structure может удовлетворить наши потребности в значительной степени.Недостатком является то, что он может генерировать скелетные экраны только для URL-адресов, которые уже существуют в Интернете, и не поддерживает среды разработки. Кроме того, поскольку он генерируется автоматически, когда страница перенаправляется (если вы не вошли в систему для перенаправления на страницу входа), сгенерированный скелетный экран может быть не таким, как ожидалось. Кроме того, его внутренняя реализация несовершенна, что может привести к тому, что экран-скелет, созданный под некоторыми страницами со сложной структурой, потребует вторичной оптимизации и настройки.
Итак, мы начали исследовать дальше.
3. Схема реализации, более дружественная к разработке
1. Идеи дизайна
Основываясь на существующих решениях, мы подумали о том, чтобы указать URL-адрес страницы и каталог вывода файла для создания скелетного экрана в файле конфигурации, прочитать элементы конфигурации в файле конфигурации во время выполнения, открыть указанную страницу через Puppeteer и внедрить evalDom. js-метод. Поскольку этот JS выполняется в Puppeteer, можно получить полную DOM-структуру текущей страницы, что оставляет нам много возможностей для развития.
Первоначально мы начали с тега body в полученной DOM-структуре, рекурсивно обработали все узлы на странице и заменили исходную позицию элемента сгенерированным DIV после обработки. В первой версии схемы методы getBoundingClientRect и getComputedStyle используются для получения всех вычисляемых свойств элемента и ширины, высоты и положения относительно области просмотра, а затем рекурсивно рендерятся в сочетании со стилевыми свойствами элемента себя, сохраняя исходный уровень вложенности DOM страницы.
Однако, поскольку существует слишком много атрибутов, которые могут определять положение элемента, таких как позиция, z-индекс, ширина, высота, вершина, отображение, размер окна, гибкость и т. д., необходимо учитывать все, что приводит к невозможность сосредоточиться на логике обработки DOM-структуры страницы, и эти После обработки атрибута его необходимо добавить в стиль конечного сгенерированного скелетного узла экрана, поэтому файл скелетного экрана может быть больше, чем файл скелетного экрана оригинальная полная структура страницы, что определенно не то, что нам нужно.
Оптимизированное решение состоит в том, чтобы использовать getBoundingClientRect и getComputedStyle для получения свойств, связанных с элементом, а затем напрямую генерировать окончательный узел каркаса экрана посредством абсолютного позиционирования. Таким образом, свойствами, которые в конечном итоге требуются на странице, являются в основном позиция, z-index, top, left, width, height, background и border-radius. За исключением того, что исходная DOM-структура страницы не может быть гарантирована, другие требования в основном могут быть выполнены, и она больше ориентирована на обработку узлов.
Основной процесс реализации выглядит следующим образом:
Это решение в настоящее время в основном используется в многостраничных проектах сотовой электронной коммерции Ma, включая страницу заказа, страницу визы и т. Д. Следующая отдельная страница является примером, эффект отображения выглядит следующим образом:
2. Реализация
- Создать экран скелета
(1) конфигурация config.js
const dpsConfig = {
// 默认生成位置为当前项目目录skeleton文件夹,已有骨架屏页面不会再次生成,新页面配置只需要添加新条目即可
visa_guide: {
url: 'https://w.mafengwo.cn/sfe-app/visa_guide.html?mdd_id=10083', // 必填项
},
call_charge: {
url: 'http://localhost:8081/sfe-app/call_charge.html?rights_id=25', // 必填项 待生成骨架屏页面的地址,用百度(https://baidu.com)试试也可以
//url:'https://www.baidu.com',
device: 'pc', // 非必填,默认mobile
background: '#eee', // 非必填
animation: 'opacity 1s linear infinite;', // 非必填
headless:false, // 非必填
customizeElement: function(node) { // 非必填
//返回值枚举如果是true表示不会向下递归到这层为止,如果返回值是一个对象那么节点的档子就按照对象里面的样式来绘制
//如果返回值为0表示正常递归渲染
//如果返回值为1表示渲染当前节点不在向下递归
//如果返回值为2表示对当前节点不作任何处理
if(node.className === 'navs-bottom-bar'){
return 2;
}
return 0;
},
showInitiativeBtn: true,// 非必填 如果此值设置为true表示开发需要主动触发生成骨架屏了,此时headless需设置为false
writePageStructure: function(html) { // 非必填
// 自己处理生成的骨架屏
// fs.writeFileSync(filepath, html);
// console.log(html)
},
init: function() { // 非必填
// 生成骨架屏之前的操作,比如删除干扰节点
}
}
}
module.exports = dpsConfig;
(2) Puppeteer открывает новую страницу и возвращается к экземпляру браузера, openPage
const ppteer = require('puppeteer');
const { log, getAgrType } = require('./utils');
const insertBtn = require('../insertBtn');
const devices = {
mobile: [375, 667, 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'],
ipad: [1024, 1366, 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1'],
pc: [1200, 1000, 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1']
};
async function pp({device = 'mobile', headless = true, showInitiativeBtn = false}) {
const browser = await ppteer.launch({headless});//返回browser实例
async function openPage(url, extraHTTPHeaders) {
const page = await browser.newPage();
let timeHandle = null;
if(showInitiativeBtn){
browser.on('targetchanged', async ()=>{//监听页面路由变化,并获取当前标签页的最新的页面,在showInitiativeBtn为true时插入按钮由开发控制主动生成骨架屏
const targets = await browser.targets();
const currentTarget = targets[targets.length - 1]
const currentPage = await currentTarget.page();
clearTimeout(timeHandle)
setTimeout(()=>{
if(currentPage){
currentPage.evaluate(insertBtn);
}
},300)
})
}
try{
let deviceSet = devices[device];
page.setUserAgent(deviceSet[2]);
page.setViewport({width: deviceSet[0], height: deviceSet[1]});
if(extraHTTPHeaders && getAgrType(extraHTTPHeaders) === 'object') {
await page.setExtraHTTPHeaders(new Map(Object.entries(extraHTTPHeaders)));
}
await page.goto(url, {
waitUntil: 'networkidle0'//不再有网络连接时触发(至少500ms后)
});
}catch(e){
console.log('\n');
log.error(e.message);
}
return page;
}
return {
browser,
openPage
}
};
module.exports = pp;
(3) Выполнение основной логики обработки узлов узлов в evalDom.js и evalDom.js в среде браузера.
agrs.unshift(evalScripts);//evalScripts = require('../evalDOM');在puppeteer里执行evalDom.js并将config.js里配置的参数传递给evalDom
html = await page.evaluate.apply(page, agrs);
//evalDom.js主要逻辑
startDraw: function () {
const $this = this;
const nodes = this.rootNode.childNodes;
this.beforeRenderDomStyle();
function childNodesStyleConcat(childNodes) {
for (let i = 0; i < childNodes.length; i++) {
const currentChildNode = childNodes[i];//当前子节点
//有哪些节点要跳过绘制骨架屏的过程
if ($this.shouldIgnoreCurrentElement(currentChildNode)) { //是否应该忽略当前节点,不采取任何措施。后续这个地方可以由用户指定哪些节点应该被略去,todo
continue;
}
const backgroundHasurl = analyseIfHadBackground(currentChildNode);
const hasDirectTextChild = childrenNodesHasText(currentChildNode);//判断当前元素是不是有直接的子元素并且此元素是Text
if ($this.customizeElement && $this.customizeElement(currentChildNode) !== 0 && $this.customizeElement(currentChildNode) !== undefined) {
//开发者自定义节点需要渲染的样子,默认返回false表示使用正常递归的算法来处理。如果返回值是true表示不会在向下递归,如果返回值是一个对象那么表示开发需要自定义样式此时直接绘制就好。todo
if (getArgtype($this.customizeElement(currentChildNode)) === 'object') {
console.log('object');
//此处如果返回一个对象表示对象要自定义最后绘制的对象
} else if ($this.customizeElement(currentChildNode) === 1) {
//如果此时返回true,表示此节点要过滤
getRenderStyle(currentChildNode);
} else if ($this.customizeElement(currentChildNode) === 2){
continue ;
}
continue;
}
if (backgroundHasurl || analyseIsEmptyElement(currentChildNode) || hasDirectTextChild || shouldDrawCurrentNode(currentChildNode)) { //如果当前元素是内联元素或者当前元素非内联元素,但是不包含子节点或者子节点都是内联元素的话那么我们就在当前的骨架屏上绘制此节点。
getRenderStyle(currentChildNode, hasDirectTextChild);
} else if (currentChildNode.childNodes && currentChildNode.childNodes.length) { //如果当前节点包含子节点
//递归
childNodesStyleConcat(currentChildNode.childNodes);
}
}
}
childNodesStyleConcat(nodes);
return this.showBlocks();
},
-
Вышеупомянутый rootNode является корневым узлом, который по умолчанию имеет значение document.body или может быть указан при разработке.
-
Основная логика состоит в том, чтобы решить, нужно ли игнорировать текущий узел, задано ли фоновое изображение, содержит ли оно текстовую информацию, указывает ли разработка метод обработки текущего узла и т. д., и отрисовывает соответствующий скелет экрана узла который соответствует условиям, в противном случае обрабатывается дочерний элемент текущего узла.
-
После того, как все узлы обработаны, вызовите showBlocks для объединения сгенерированных узлов каркасного экрана в строки HTML для последующей обработки.
(4) getRenderStyle генерирует скелетный стиль экрана
const styles = [
'position: fixed',
`z-index: ${zIndex}`,
`top: ${top}%`,
`left: ${left}%`,
`width: ${width}%`,
`height: ${height}%`,
'background: '+(background || '#eee'),
];
const radius = getStyle(node, 'border-radius');
radius && radius != '0px' && styles.push(`border-radius: ${radius}`);
blocks.push(`<div style="${styles.join(';')}"></div>`);
- zIndex, top, left, width, height — это обрабатываемые свойства, а затем в массив блоков помещаются строки всех узлов каркасного экрана.
(5) Файл HTML, который, наконец, генерирует скелетный экран, выглядит следующим образом.:
<html><head></head>
<body><div style="position: fixed;z-index: 999;top: 89.805%;left: 4.267%;width: 91.467%;height: 11.994%;background: #eee"></div></body></html>
- Вставить экран скелета
Добавьте в файл index.html записи проекта
<body>
<div id="app">
</div>
<% if(htmlWebpackPlugin.options.hasSkeleton) { %>
<div id="skeleton"><!-- 骨架屏通过htmlWebpackPlugin在启动打包的时候自动注入 -->
<%= htmlWebpackPlugin.options.loading.html %>
</div>
<% } %>
<!-- built files will be auto injected -->
</body>
4. Резюме
В настоящее время это решение уже поддерживает активный контроль времени генерации экрана-скелета разработчиком, что позволяет избежать невозможности сгенерировать правильный экран-скелет в процессе перенаправления страницы, а также поддерживает генерацию экрана-скелета во время локальной разработки. В будущем мы будем поддерживать разработку пользовательских стилей узлов экрана скелета и генерацию экрана скелета компонента, а также оптимизировать алгоритм фильтрации и обработки внутренних узлов в evalDom.js. Следите за обновлениями!
Наконец, мы набираем старших инженеров по разработке интерфейса Заинтересованные студенты могут отправлять свои резюме по адресу: kangcenbo@mafengwo.com.
Авторы этой статьи: Кан Ченбо, Сунь Хаонань, фронтенд-инженеры по исследованиям и разработкам платформы электронной коммерции Mafengwo..