浅谈前端骨架屏方案

在图片与前端体验优化中,最重要的莫过于「骨架屏」了,因为它和“首屏体验”息息相关。

目前来说骨架屏基本上有两种方式:

  1. HTML + CSS:主流。基本是自己在项目中以侵入式方式围绕html“定制”;微信小程序的骨架屏生成方案本质上也是这种。
  2. 自动生成。利用一些手段在业务代码之外生成骨架屏,但最终还要依托架构插入到业务中。

CSS实现骨架屏

在近期的业务中,我遇到了一个场景:
说明

图中红色框内容在接口中分为三种级别。首先每个级别的活动都是固定的,后端只返回状态值。所以前端是三个数组。
其次需要考虑一个问题:是默认展示第一级别,如果状态发生改变,再切换到第二/三级别?还是默认空白,等到接口拿到数据后根据状态展示级别?

需要明确的是,这个页面并不只有这一个接口。而且这个接口的“优先级”是低级别的。

后者效果展示:
先空白拿到数据后再展示效果

不管是从视觉上还是我想采用的技术手段上,我都认为这个场景应该选择前者 —— 这样的话,骨架屏就有了“基准”。我就不需要采用额外的元素去实现,只需要用伪元素覆盖默认文案并展示动效即可:

/** 给所有需要展示骨架的元素都添加这行代码,变量默认为false,待接口拿到数据后变为true */
:class="{'cate-skeleton': !showPOSTData}"
.cate-skeleton {
    position: relative;

    &::after {
        content: "";
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: lightgray;
        background: linear-gradient(100deg, rgba(220, 220, 220, 0) 40%, rgba(255, 255, 255, 0.2) 50%, rgba(220, 220, 220, 0) 60%) gainsboro;
        background-size: 200% 100%;
        background-position-x: 150%;
        animation: 1.5s loading ease-in-out infinite;
        opacity: 1 !important;
        z-index: 2;
    }
}

@keyframes loading {
    to {
        background-position-x: -50%;
    }
}

这段代码中最重要的就是linear-gradient了。它其实就是将 background 分为三段。然后延长其 width ,并不断改变位置 position-x
骨架屏展示数据

因为有了骨架屏,用户就知道这段时间内页面并不是什么也没做。体验也就提升了。对我们来说,我们甚至可以把接口放到组件created里面处理(笔者所在组已然按照笔者之前提的“大小组件原则”封装业务代码) —— 这里还会有一个问题:如果网络和接口实在给力,而你什么也不做,可能会出现骨架闪动的效果。这可不是什么小问题,它甚至比“从空白到数据突然展示”更加令人难受。

let res = await this.$http({
    //参数
})
if(!res.data && !res.result) {
    // 兜底
}
// 延时300ms,不然瞬间灰色闪动更难受了
await this.promiseTimeout(300);

为此,笔者决定故意延长loading的时间,给用户更好的体验:

async promiseTimeout (time) {
    return await new Promise(function(resolve,reject){
        setTimeout(function(){
            console.log('骨架屏加载ing');
            resolve(time);
        },time);
    });
},

setTimeout 微任务的异步和请求的异步不同(机制就不一样)。setTimeout 不能直接触发async-await

node实现非侵入式骨架屏生成

在复杂场景下,我们可以把业务和骨架屏分离。比如在某种身份下其实进来是B布局,如果你在开发业务时采用第一种方案的话要么骨架固定,要么CV两份 HTML 代码去书写样式。
这好么?这不好。

我们可以以页面为基准“自动”生成骨架屏,然后通过配置注入到项目源码中。
这样就可以在页面生成之后再去对指定class/id进行骨架样式生成。对其余元素可以采取定制化生成,或是直接隐藏。

这是一种“后处理”。

既如此,我们应当要求:

  • 使用和维护成本低
  • 配置灵活
  • 还原度高
  • 尽量不影响加载性能

node中的puppeteer给我们提供了很好的方案:通过 puppeteer 获取页面、做骨架处理、截屏或获取源码、默认采用 base64 输出。

Puppeteer 是一个控制 headless Chrome 的 Node.js API 。它是一个 Node.js 库,通过 DevTools 协议提供了一个高级的 API 来控制 headless Chrome。它还可以配置为使用完整的(非 headless)Chrome。
我们可以通过 puppeteer 操作网页:触发事件、截屏、爬取数据、检索 SPA 并生成预渲染内容(即 “SSR”)、甚至是创建一个能运行最新js特性的自动测试环境(浏览器)。

npm install puppeteer
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.setViewport({width: 骨架屏宽, height: 骨架屏高});

  // 事件监听,可用于事件通信
  page.on('console', msg => console.log('PAGE LOG:', msg.text()));
  page.on('warning', msg => console.log('PAGE WARN:', JSON.stringify(msg)));
  page.on('error', msg => console.log('PAGE ERR:', ...msg.args));
  
  // waitUntil:load/domcontentload/networkidle0/networkidle2
  await page.goto('页面的url!!!', {waitUntil: 'networkidle2'});

  // 对打开的页面进行操作

  // 将页面截图,输出为 pdf 或 图片
  await page.pdf({path: 'hn.pdf', format: 'A4'});
  await page.screenshot({path: 'example.png'});

  await browser.close();
})();

这种方案简化的并不是代码层面 —— 当然,你也可以封装成可视化。我们的处理思路和上面大致相同 —— 因为不是操作原页面,这里直接替换即可。以 Img 为例:

Array.from(document.body.querySelectorAll('img')).map(img => {
  img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
  img.style.backgroundColor = '#EEEEEE';
});

对于文字来说,也是如此:

await page.$eval('.xxx/#xxx(按class/id查找)',
	(el, value)=> el.setAttribute('style', value),
	'backgroundImage: linear-gradient(to bottom, #070b21, rgba(7, 11, 33, 0.5))'
)

或插入指定文案:

await page.$$eval('nav>ul>li>.wired-rendered',
	nodes => nodes.map(n => {
		n.innerHTML = `<span class="eval-puppeteer-bg" style="background-image: #EEEEEE">${n.innerHTML}</span>`
	// return n;
	}))

因为骨架屏主要目标是“首屏”,我们就可以移除非首屏节点:

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

    } catch (e) {
      return true;
    }
}

style也是如此:

const styles = Array.from(document.querySelectorAll('style')).map(style => style.innerHTML || style.innerText);
// 移除非首屏样式
function handleStyles(styles, html) {
    const ast = cssTree.parse(styles);
    const dom = new JSDOM(html);
    const document = dom.window.document;
    const cleanedChildren = [];
    let index = 0;
    ast && ast.children && ast.children.map((style) => {
        let slectorExisted = false,
            selector;
        switch (style.prelude && style.prelude.type) {
            case 'Raw':
                selector = style.prelude.value && style.prelude.value.replace(/,|\n/g, '');
                slectorExisted = selectorExistedInHtml(selector, document);
                break;
            case 'SelectorList':
                style.prelude.children && style.prelude.children.map(child => {
                    const children = child && child.children;
                    selector = getSelector(children);
                    if (selectorExistedInHtml(selector, document)) {
                        slectorExisted = true;
                    }
                });
                break;
        }
        if (slectorExisted) {
            cleanedChildren.push(style);
        }
    });
    ast.children = cleanedChildren;
    let outputStyles = cssTree.generate(ast);
    outputStyles = outputStyles.replace(/},+/g, '}');
    return outputStyles;
}

function selectorExistedInHtml(selector, document) {

    if (!selector) {
      return false;
    }

    // 查询当前样式在 html 中是否用到
    let selectorResult, slectorExisted = false;
    try {
      selectorResult = document.querySelectorAll(selector);

    } catch (e) {
      console.log('selector query error: ' + selector);
    }

    if (selectorResult && selectorResult.length) {
      slectorExisted = true;
    }

    return slectorExisted;
}
### 前端骨架的实现方式与示例 #### 使用 HTMLCSS 的简单骨架实现 可以通过纯 HTMLCSS 来创建一个基础的骨架效果。这种实现方式适用于静态页面或者简单的动态内容加载场景。 ```html <div class="skeleton"> <div class="skeleton-header"></div> <div class="skeleton-body"></div> </div> <style> .skeleton { width: 100%; } .skeleton-header { height: 50px; background-color: #e0e0e0; border-radius: 8px; margin-bottom: 16px; animation: pulse 1.5s infinite ease-in-out; } .skeleton-body { height: 200px; background-color: #e0e0e0; border-radius: 8px; animation: pulse 1.5s infinite ease-in-out; } @keyframes pulse { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } } </style> ``` 上述代码展示了如何利用 `animation` 属性模拟骨骼的效果,通过颜色渐变让其看起来像是正在加载的内容[^1]。 --- #### Vue.js 中的骨架实现 在 Vue.js 项目中可以借助第三方库来快速实现骨架功能。以下是基于 Element Plus 组件库的一个实例: ##### 安装依赖 首先需要安装 Element Plus 库: ```bash npm install element-plus --save ``` ##### 页面代码示例 ```vue <template> <el-skeleton :loading="loading" animated> <template #template> <div style="display: flex;"> <el-skeleton-item variant="image" style="width: 90px; height: 90px;" /> <div style="margin-left: 16px;"> <el-skeleton-item variant="h3" style="width: 80%;" /> <el-skeleton-item variant="text" style="width: 50%; margin-top: 16px;" /> </div> </div> </template> <!-- 替换真实数据 --> <div v-if="!loading">这里是实际内容...</div> </el-skeleton> </template> <script> export default { data() { return { loading: true, }; }, mounted() { setTimeout(() => { this.loading = false; }, 2000); // 模拟异步请求延迟 }, }; </script> ``` 此代码片段演示了如何使用 Element Plus 提供的 `<el-skeleton>` 组件以及子组件 `<el-skeleton-item>` 创建复杂的布局结构,并支持动画效果[^3]。 --- #### 自动化生成骨架方案 对于更复杂的应用场景,可以选择自动化工具来自动生成骨架。这些工具通常会扫描现有的 DOM 结构并提取样式信息用于构建对应的占位符版本。最终输出的形式可能是 Base64 编码图像或者是嵌入式的 HTML+CSS 片段[^2]。 例如,某些插件允许开发者仅需配置少量参数即可完成整个过程而无需手动调整每一处细节位置关系。 --- #### 骨架的优势分析 相比于传统的 Loading 动画,骨架能够给用户提供更加直观的感受,减少等待焦虑感的同时也提升了整体交互体验质量[^4]。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

恪愚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值