使用 Vite 插件自动化实现骨架屏

article/2025/9/15 3:41:57

大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

作者:橙红年代

原文:https://juejin.cn/post/7152406737100734495

骨架屏在SPA应用中有两个显著提升用户体验的作用

  • 避免页面初始化加载时的空白,体验介于SSR和完全等待页面初始化完成之间

  • 避免部分路由组件需要加载数据完成之后才渲染的空白

骨架屏会给用户一种内容已经返回的错觉,只要稍加等待就能看见完整内容了,因此骨架屏的定位就是真实内容准备好之前的替身。

之前研究过一种快速生成骨架屏的想法:使用Chrome扩展程序生成网页骨架屏[1],大概原理是通过Chrome扩展程序注入content.js修改页面DOM接口,最终导出带有骨架屏样式的HTML代码。

当时的这个想法并没有在生产中落地,最近在折腾用户体验相关的功能,发现还是有必要继续完善一下骨架屏相关的东西。

业界对于骨架屏的应用,也有好几种方案

  • 直接让设计师提供页面对应的骨架屏设计图

    • 导出svgbase64图片嵌入代码中,比较影响项目体积

    • 开发手动编写样式,工作量较大

  • 通过组件编写骨架屏

    • 诸如vue-content-loader[2]、react-content-loader[3]等组件,可以通过svg快速编写骨架屏内容,但输出的产物与真实页面有一定差距,不容易实现定制化骨架屏需求。

    • 一些组件库,如vant[4]、varlet[5]也提供了skeleton组件,通过配置参数的形式控制生成骨架屏内容,其缺点也是定制化程度较差

  • 自动生成骨架屏

    • page-skeleton-webpack-plugin等比较成熟的自动骨架屏方案,甚至有专门的UI界面来控制生成不同一面的骨架屏,缺点是生成的骨架屏代码较大,影响项目体积

    • 借助puppeteer无头浏览器渲染出页面对应的骨架屏内容,依赖较大

    • 借助Chrome扩展程序生成骨架屏内容,本质上和无头浏览器原理相似

骨架屏属于锦上添花的功能,理想状态下开发者应该是不需要过分关注的,因此从开发体验上来看,手动编写骨架屏并不是很好的解决方案。因此本文主要研究另外一种骨架屏自动生成方案:通过vite插件自动注入骨架屏。

本文所涉及的项目代码均上传至github[6]上面了。

先预览一下效果

点击生成骨架屏

aacbbfa777fdda715e9ec4fee1870a13.jpeg

首屏访问

7b0d77b015353723602de60cca6a9788.jpeg

vite插件生成骨架屏

参考

  • 前端智能化探索,骨架屏低代码自动生成方案实践[7]

  • vite-plugin-vue-inspector[8]这个插件的实现,将源代码的一些信息注入到页面上

  • 骨架屏 - 微信小程序开发文档[9],小程序开发者工具提供了类似快速生成当前页骨架屏的方案

首先需要探寻一种自动能够将设计图或真实页面转成骨架屏的方案。大概有下面几个思路

  • 通过编译工具,解析代码中编写的HTML模板,生成骨架屏

  • 从设计图来源出发,比如sketch、figma等,通过插件导出可以用骨架屏内容

  • 直接操作真实页面的DOM,然后生成骨架屏内容

利用现有样式

看起来第三种思路的实现成本最低,也最为熟悉。这也是使用Chrome扩展程序生成网页骨架屏[10]这个方案中采用的方案,因此具体的实现细节这里不再赘述,简单总结一下

  • 在开发环境下,通过手动触发某个开关,开始生成某个页面对应的骨架屏内容

  • 将页面按节点类型拆分成不同区块

  • 支持自定义节点类型、忽略或隐藏节点

  • 最后导出的是一段HTML代码,复用原始页面的结构和CSS布局代码

核心API只有一个,传入对应的入口节点,输出转换后的骨架屏代码

const {name, content} = renderSkeleton(sel, defaultConfig)

比如下面这段结构

<div class="card" data-skeleton-type="block"><div class="card_tt" data-skeleton-type="text">卡片标题</div><div class="card_ct" data-skeleton-type="text">卡片内容卡片内容</div>
</div>

生成的骨架屏代码是

<div class="sk-block card"><div class="sk-text card_tt">卡片标题</div><div class="sk-text card_ct">卡片内容卡片内容</div>
</div>

其中sk-blocksk-text等样式类都是在生成时追加上去的,用于覆盖原本的样式,从而展示骨架屏的灰色背景,但同时保留原本的布局样式。

renderSkeleton的调用时机由开发者自己控制,我们可以向页面注入一个按钮,点击时调用

function createTrigger() {const div: HTMLDivElement = document.createElement('div')div.setAttribute('style', 'position:fixed;right:0;bottom:20px;width:50px;height:50px;background:red;')div.addEventListener('click', function () {renderSkeleton('[data-skeleton-root]')})document.body.appendChild(div)
}if(process.end.NODE_ENV ==='development'){createTrigger()
}

在得到骨架屏代码之后,在业务代码中通过一个loading标志位控制展示的是骨架屏还是真实内容

<script setup lang="ts">
import {ref, onMounted} from "vue";const loading = ref(true);
const list = ref<number>([]);async function fetchList() {await sleep(1000)list.value = [1, 2, 3, 4, 5]loading.value = false
}onMounted(() => {fetchList()
})</script><template><div class="page"><div v-if="loading"><!--这里的都是骨架屏代码--><div class="sk-block card" v-for="item in 5" :key="item"><div class="sk-text card_tt">卡片标题</div><div class="sk-text card_ct">卡片内容卡片内容</div></div></div><div v-else class="card-list card-list-knowledge" data-skeleton-root="APP" data-skeleton-type="list"><div class="card" v-for="item in list" :key="item" data-skeleton-type="block"><div class="card_tt">卡片标题</div><div class="card_ct">卡片内容卡片内容</div></div></div></div></template><style lang="scss" scoped>
// 相关的样式
</style>

可以看到,v-if="loading"标签内部的代码,就是生成的骨架屏内容。需要注意的是,既然骨架屏与业务代码在一起,也会参与Vue的SFC编译,因此骨架屏标签上面的一些动态属性如scopeid等,需要移除。关于scopeid带来的其他问题,后面的篇幅会提到,这也会影响整个renderSkeleton的实现。

如果每次在调用renderSkeleton拿到骨架屏代码之后,手动修改业务代码替换loading展示的内容,无疑非常麻烦,现在来研究一下如何自动化解决这个问题。

前面提到,骨架屏主要应用在首屏渲染需要和路由页面切换时

  • SPA首屏渲染优化

  • 路由组件切换时的占位内容

接下来看看这两种场景下如何自动注入骨架屏代码

组件内渲染骨架屏

我们可以通过占位符来声明当前组件对应骨架屏代码的地方,比如

<div v-if="loading">__SKELETON_APP_CONTENT__</div>
<div v-else>真实业务代码</div>

在获得骨架屏代码之后,将__SKELETON_APP_CONTENT__这里的内容替换成真实的骨架屏代码即可。

如何替换呢?vite插件提供了一个transform的钩子

const filename = './src/skeleton/content.json'function SkeletonPlaceholderPlugin() {return {name: 'skeleton-placeholder-plugin',enforce: 'pre',transform(src, id) {if (/\.vue$/.test(id)) {const {content} = fs.readJsonSync(filename)// 约定对应的骨架屏占位符let code = src.replace(/__SKELETON_(.*?)_CONTENT__/igm, function (match) {return content})return {code,}}return src},} as Plugin
}

其中./skeleton.txt中的内容,就是在调用renderSkeleton后生成的骨架屏代码,通过transformpre,我们就可以在vue插件解析SFC之前,先将骨架屏占位符替换成真正的代码,再参与后续的编译流程。

这里还需要解决一个问题:renderSkeleton是在客户端触发的,而skeleton.txt是在开发服务器环境下的,需要有一个通信的机制将客户端生成的骨架屏代码发送到项目目录下面。

vite插件提供了一个configureServer钩子,用来配置vite开发服务器,我们可以加一个中间件,用来提供一个保存骨架屏代码的接口

function SkeletonApiPlugin() {async function saveSkeletonContent(name, content) {await fs.ensureFile(filename)const file = await fs.readJson(filename)file[name] = {content,}await fs.writeJson(filename, file)}return {name: 'skeleton-api-plugin',configureServer(server) {server.middlewares.use(bodyParser())server.middlewares.use('/update_skeleton', async (req, res, next) => {const {name, content, pathname} = req.bodyawait saveSkeletonContent(name, content, pathname)// 骨架屏代码更新之后,重启服务server.restart()res.end('success')})},}
}

然后在renderSkeleton之后,调用这个接口上传生成的骨架屏代码即可

async function sendContent(body: any) {const response = await fetch('/update_skeleton', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(body)})const data = await response.text()
}const __renderSkeleton = async function () {const {name, content} = renderSkeleton(".card-list", {})await sendContent({name,content})
}

bingo!大功告成。梳理一下流程

  • 开发者在某个时候手动调用__renderSkeleton,就会自动生成当前页面的骨架屏

  • 将骨架屏代码发送给vite接口,更新本地skeleton/content.json中的骨架屏代码,

  • vite重启服务后,重新触发pre队列中的skeleton-content-component插件,替换骨架屏占位符,注入骨架屏代码,完成整个骨架屏的插入流程。

整个过程中,开发者只需要完成下面两步操作即可

  • 声明骨架屏在业务代码中的占位符

  • 点击按钮,触发生成骨架屏代码

路由切换的骨架屏大多应用在路由组件上,可以考虑进一步封装,统一管理loading和骨架屏展示,这里比较细节,就不再一一展开了。

首屏渲染骨架屏

骨架屏对于SPA首屏渲染优化,需要在应用初始化之前就开始渲染,即需要在id="app"的组件内植入初始化的骨架屏代码

如果是服务端预渲染,可以直接返回填充后的代码;如果是客户端处理,可以通过document.write处理,我们这里只考虑纯SPA引用,由前端处理骨架屏的插入。

我们可以通过vite插件提供的transformIndexHtml钩子注入这段逻辑

function SkeletonApiPlugin() {return {name: 'skeleton-aip-plugin',transformIndexHtml(html) {let {content} = fs.readJsonSync(filename)const code = `
<script>
var map = ${JSON.stringify(content)}
var pathname = window.location.pathname
var target = Object.values(map).find(function (row){return row.pathname === pathname
})
var content = target && target.content || ''
document.write(content)
</script>`return html.replace(/__SKELETON_CONTENT__/, code)}}
}

对应的index.html代码为

<div id="app">__SKELETON_CONTENT__</div>

根据用户当前访问的url,读取该url对应的骨架屏代码,然后通过document.write写入骨架屏代码。这里可以看出,在生成骨架屏代码时,我们还需要保留对应页面url的映射,甚至需要考虑动态化路由的匹配问题。这个也比较简单,在提交到服务端保存时,加个当前页面的路径参数就行了

const {name, content} = renderSkeleton(sel, defaultConfig)// 如果是hash路由,就替换成fragmentconst {pathname} = window.locationawait sendContent({name,content,pathname // 保存骨架屏代码的时候顺道把pathname也保存了})

整理一下流程

  • 用户访问url

  • 根据页面url,加载对应的骨架屏代码,填充在根节点下

    • 如果是服务端预渲染,可以直接返回填充后的代码

    • 如果是客户端处理,可以通过document.write处理

  • 用户看见渲染的骨架屏内容

  • 初始化应用,加载页面数据,渲染出真实页面

开发者在点击生成当前页面的骨架屏时,保存的骨架屏代码,既可以用在路由组件切换时的骨架屏,也可以用在首屏渲染时的骨架屏,Nice~

存在的一些问题

利用vite插件注入骨架屏的代码,看起来是可行的,但在方案落地时,发现了一些需要解决的问题。

存在原始样式不生效的场景

由于生成的骨架屏代码是依赖原始样式的,

<div class="card" data-skeleton-type="block"></div>

对应的骨架屏代码

<div class="sk-block card"></div>

其中的sk-block只会添加一些灰色背景和动画,至于整体的尺寸和布局,还是card这个类来控制的。

这么设计的主要原因是:即使card的尺寸布局发生了变化,对应的骨架屏样式也会一同更新。

但在某些场景下,原始样式类无法生效,最具有代表性的问题就Vue项目的的scoped css

我们知道,vue-loader@vitejs/plugin-vue等工具解析SFC文件时,会为对应组件生成scopeId(参考之前的源码分析:从vue-loader源码分析CSS-Scoped的实现[11]),然后通过postcss插件,通过组合选择器实现了类似于css作用域的样式表

.card[data-v-xxx] {}

我们的生成骨架屏的时机是在开发环境下进行的,这就导致在生产环境下,看到的骨架屏并没有原始样式类对应的尺寸和布局。

下面是vite vue插件的源码

export function createDescriptor(filename: string,source: string,{ root, isProduction, sourceMap, compiler }: ResolvedOptions
): SFCParseResult {const { descriptor, errors } = compiler.parse(source, {filename,sourceMap})const normalizedPath = slash(path.normalize(path.relative(root, filename)))descriptor.id = hash(normalizedPath + (isProduction ? source : ''))cache.set(filename, descriptor)return { descriptor, errors }
}

vue-loader中生成scopeid的方法类似,看了一下貌似并没有提供自定义scopeid的API。

因此对于同一个文件而言,生产环境和非生产环境参与生产hash的参数是不一样的,导致最后得到的scopeid 也不一样。

对于组件内渲染骨架屏这种场景,我们也许可以不考虑scopeid,因为在SFC编译之前,我们就已经通过transform钩子注入了对应的骨架屏模板,对于SFC编译器而言,骨架屏代码和业务代码都在同一个组件内,也就是说他们最后都会获得相同的scopeid,这也是为什么生成的骨架屏代码,要擦除HTML标签上面的scopeid的原因。

但如果骨架屏依赖的外部样式并不在同一个SFC文件内,也会导致原始的骨架屏样式不生效。

<template><div class="page"><div v-if="loading"><div class="sk-block card" v-for="item in 5" :key="item"><div class="sk-text card_tt">卡片标题</div><div class="sk-text card_ct">卡片内容卡片内容</div></div></div><div v-else class="card-list card-list-knowledge" data-skeleton-root="APP" data-skeleton-type="list"><Card v-for="item in list" :key="item" data-skeleton-type="block"/></div></div></template><style lang="scss" scoped>
// card 样式不在这个SFC下面,但是插入的骨架屏代码却在这个SFC文件下
</style>

此外,对于首屏渲染骨架屏这种场景,就不得不考虑scopeid了。如果骨架屏依赖的原始样式是携带作用域的,那就必须要保证骨架屏代码与生产环境的样式表一致

.card[data-v-xxx] {}
<div id="app"><div class="sk-block card" data-v-xxx></div>
</div>

这样,首屏渲染依赖的骨架屏和组件内渲染的骨架屏就产生了冲突,前者需要携带scopeid,而后者又需要擦除scopeid。

为了解决这个冲突,有两种办法

  • 在保存骨架屏代码时,同时保存对应的scopeid,并在首屏渲染时,为每个标签上手动添加scopeid。

  • 原始骨架屏代码就携带scopeid,而在替换组件内的骨架屏占位符时再擦除scopeid

但不论通过何种方式保证两个环境下生成的scopeid 一致(甚至是通过修改插件源码的方式),可能也会存在旧版本的骨架屏携带的scopeid和新版本对应的scopeid 不一致的问题,即旧版本的class和新版本的class不一致。

要解决这个问题,除非在每次修改源码之后,都更新一下骨架屏,由于生成骨架屏这一步是手动的,这与自动化的目的背道而驰了。

因此,看起来利用原始类同步真实DOM的布局和尺寸,在scoped css中并不是一个十分完善的设计。

骨架屏代码质量

第二个不是那么重要的问题是生成的骨架屏代码,相较于手动编写,不够精简。

虽然在源代码中,骨架屏代码被占位符替代,但在编译阶段,骨架屏会编译到render函数中,可能造成代码体积较大,甚至影响页面性能的问题。

这个问题并不是一个阻塞性问题,可以后面考虑如何优化,比如骨架屏仍旧保留v-for等指令,组件可以正常编译,而首屏渲染的骨架屏需要通过自己解析生成完整的HTML代码。

解决方案

上面这两个问题的本质都是因为骨架屏生成方案导致的,跟后续保存骨架屏代码并自动替换并没有多大关系,因此我们只需要优化骨架屏生成方案即可。

既然依赖于原始样式生成的骨架屏代码存在这些缺点,有没有什么解决办法呢?

事实上,我们对于骨架屏是否更真实内容结构的还原程度并没有那么高的要求,也并没有要求骨架屏要跟业务代码一直保持一致,既然导出HTML骨架屏代码比较冗余和繁琐,我们可以换一换思路。

不使用scoped css

其他比较常用的CSS方案如css moudlecss-in-js或者是全局原子类css如tailwindwindicss等,如果输出的是纯粹的CSS代码,且生产环境和线上保持一致,理论上是不会出现scopeid这个问题的。

但Vue项目中,scoped css方案应该占据了半壁江山,加上我自己也比较喜欢scoped css,因此这是一个绕不过去的问题。

将骨架屏页面自动转成图片

第一种思路将骨架屏页面保存为图片,这样就不用再依赖原始样式了。

大概思路就是:在解析当前页面获得骨架屏代码之后,再通过html2canvas等工具,将已经渲染的HTML内容转成canvas,再导出base64图片。

import html2canvas from 'html2canvas'const __renderSkeleton = async function (sel = 'body') {const {name, content} = renderSkeleton(sel, defaultConfig)const canvas = await html2canvas(document.querySelector(sel)!)document.body.appendChild(canvas);const imgData = canvas.toDataURL()// 保存<img src="${imgData}" alt="">作为骨架屏代码
}

这种通过图片替代HTML骨架屏代码的优点在于兼容性好(对应的页面骨架屏甚至可以用在App或小程序中),容易迁移,不需要依赖项目代码中的样式类。

但是html2canvas生成的图片也不是百分百还原UI,需要足够纯净的代码原始结构才能生成符合要求的图片。此外图片也存在分辨率和清晰度等问题。

也许又要回到最初的起点,让设计大佬直接导出一张SVG?(开个玩笑,我们还是要走自动化的道路

复制一份独立的样式表

如果能够找到骨架屏代码中每个标签对应的class在样式表中定义的样式,类似于Chrome dev tools中的Elements Styles面板,我们就可以将这些样式复制一份,然后将scopeid替换成其他的选择器

88332e7b408418953b74f1dafd08000d.jpeg

开发环境下的样式都是通过style标签引入,因此可以拿到页面上所有的样式表对象,提取符合对应选择器的样式,包括.className.className[scopeId]这两类

写一个Demo

const { getClassStyle } = (() => {const styleNodes = document.querySelectorAll("style");const allRules = Array.from(styleNodes).reduce((acc, styleNode) => {const rules = styleNode.sheet.cssRules;acc = acc.concat(Array.from(rules));return acc;},[]);const getClassStyle = (selectorText) => {return allRules.filter((row) => row.selectorText === selectorText);};return {getClassStyle,};
})();const getNodeAttrByRegex = (node, re) => {const attr = Array.from(node.attributes).find((row) => {return re.test(row.name);});return attr && attr.name;
};const parseNodeStyle = (node) => {const scopeId = getNodeAttrByRegex(node, /^data-v-/);return Array.from(myBox.classList).reduce((acc, row) => {const rules = getClassStyle(`.${row}`);// 这里没有再考虑两个类.A.B之类的组合样式了,排列组合比较多return acc.concat(getClassStyle(`.${row}`)).concat(getClassStyle(`.${row}[${scopeId}]`));}, []);
};
const rules = parseNodeStyle(myBox);
console.log(rules);

这样就可以得到每个节点在scoped css的样式,然后替换成骨架屏依赖的样式。

但现在要保存的骨架屏代码的HTML结构之外,还需要保存对应的那份CSS代码,十分繁琐

提取必要的布局信息生成骨架屏

能否像html2canvas的思路一样,重新绘制一份骨架屏页面出来呢

通过getComputedStyle可以获取骨架屏每个节点的计算样式

const width = getComputedStyle(myBox,null).getPropertyValue('width');

复用页面结构,把所有布局和尺寸相关的属性都枚举出来,一一获取然后转成行内样式,看起来也是可行的。

但这个方案需要逐步尝试完善对应的属性列表,相当于复刻一下浏览器的布局规则,工作量较大,此外还需要考虑rem、postcss等问题,看起来也不是一个明智的选择。

postcss插件

既然scopeid是通过postcss插入的,能不能在对应的样式规则里面加一个分组选择器,额外支持一下骨架屏的呢

比如

.card[data-v-xxx] {}

修改为

.card[data-v-xxx], .sk-wrap .card {}

这样,只要解决生产环境和开发环境scopeid不一致的问题就可以了。

编写postcss插件可以参考官方文档:编写一个postcss 插件[12]

vue/compuler-sfc源码中发现,scopedPlugin插件位于传入的postcssPlugins之后,而我们编写的插件需要位于scopedPlugin之后才行,

4f3872c6aa8245bcbfa6e35eace0a010.jpeg

如果不能修改源码,只有继续从vite 插件的transform钩子入手了,在transform中手动执行postcss进行编译

继续编写一个SkeletonStylePlugin插件

const wrapSelector = '.sk-wrap'
export function SkeletonStylePlugin() {return {name: 'skeleton-style-plugin',transform(src: string, id: string) {const {query} = parseVueRequest(id)if (query.type === 'style') {const result = postcss([cssSkeletonGroupPlugin({wrapSelector})]).process(src)return result.css}return src}}
}

注意该插件要放在vue插件后面执行,因为此时得到的内容才是经过vue-compiler编译后的携带有scopeid 的样式。

其中cssSkeletonGroupPlugin是一个postcss插件

import {Rule} from 'postcss'const processedRules = new WeakSet<Rule>()type PluginOptions = {wrapSelector: string
}
const plugin = (opts: PluginOptions) => {const {wrapSelector} = optsfunction processRule(rule: Rule) {if (processedRules.has(rule)) {return}processedRules.add(rule)rule.selector = rewriteSelector(rule)}function rewriteSelector(rule: Rule): string {const selector = rule.selector || ''const group: string[] = []selector.split(',').forEach(sel => {// todo 这里需要排除不在骨架屏中使用的样式const re = /\[data-v-.*?\]/igmif (re.test(sel)) {group.push(wrapSelector + ' ' + sel.replace(re, ''))}})if(!group.length) return selectorreturn selector + ', ' + group.join(',')}return {postcssPlugin: 'skeleton-group-selector-plugin',Rule(rule: Rule) {processRule(rule)},}
}
plugin.postcss = trueexport default plugin

这个插件写的比较粗糙,只考虑了常规的选择器,并依次追加分组选择器。测试一下

.test1[data-v-xxx] {}

成功编译成了

.test1[data-v-xxx], .sk-wrap .test1 {}

这样,只需要将骨架屏代码外边包一层sk-wrap,骨架屏中的样式就可以正常生效了!

content && document.write('<div class="${wrapSelector.substr(1)}">' +content+'</div>')

看起来解决了一个困扰我很久的问题。

小结

至此,一个借助于Vite插件实现自动骨架屏的方案就实现了,总结一下整体流程

首先初始化插件

import {SkeletonPlaceholderPlugin, SkeletonApiPlugin} from '../src/plugins/vitePlugin'export default defineConfig({plugins: [SkeletonPlaceholderPlugin(),vue(),SkeletonApiPlugin(),],build: {cssCodeSplit: false}
})

然后填写占位符,对于首屏渲染的骨架屏

<div id="app">__SKELETON_CONTENT__</div>

对于组件内的骨架屏

<!--其中占位符格式为`__SKELETON_${data-skeleton-root}_CONTENT__`-->
<div v-if="loading">__SKELETON_APP_CONTENT__</div>
<div class="card-list" data-skeleton-root="APP" data-skeleton-type="list"></div>

接着初始化客户端触发器,同时向页面插入一个可以点击生成骨架屏的按钮

import '../../src/style/skeleton.scss'
import {initInject} from '../../src/inject'createApp(App).use(router).mount('#app')// 开发环境下才注入
if (import.meta.env.DEV) {setTimeout(initInject)
}

点击触发器,自动将当前页面转换成骨架屏

通过HTTP将骨架屏代码发送到插件接口,通过fs写入本地文件./src/skeleton/content.json中,然后自动重启vite server

页面内组件的占位符会通过SkeletonPlaceholderPlugin替换对应占位符的骨架屏代码,loading生效时展示骨架屏

首屏渲染页面时,通过location.pathname插入当前路径对应的骨架屏代码,直接看见骨架屏代码

所有骨架屏依赖的当前样式通过cssSkeletonGroupPlugin解析,通过分组选择器输出在css文件,不再依赖scopeid。

这样,一个基本自动的骨架屏工具就集成到项目中,需要进行的手动工作包括

  • 配置插件

  • 定义组件的骨架屏占位符,以及骨架屏入口data-skeleton-root="APP"

  • 必要时在标签上声明data-skeleton-type,定制骨架屏节点

整个项目比较依赖vite插件开发知识,也参考了vite@vitejs/plugin-vue@vue/compile-sfc等源码的实现细节。

所有Demo已经放在github[13]上面了,剩下要解决的就是优化生成骨架屏的效果和质量了,期待后续吧

Node 社群我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:1. 点个「在看」,让更多人也能看到这篇文章2. 订阅官方博客 www.inode.club 让我们一起成长点赞和在看就是最大的支持❤️

http://chatgpt.dhexx.cn/article/r7oGdB4e.shtml

相关文章

骨架屏技术讲解以及如何在Vue中实现骨架屏

骨架屏技术讲解以及如何在Vue中实现骨架屏 写在前面骨架屏是什么如何实现&#xff08;原理分析&#xff09;一个生动的例子 实现方式&#xff08;具体实现&#xff09;方案一、在模版中来实现骨架屏方案二、使用一个Base64的图片来作为骨架屏方案三、使用 .vue 文件来完成骨架屏…

正确使用uniapp搭配微信开发者工具自带的骨架屏功能,生成骨架屏

重点&#xff1a;把index.skeleton.wxml和index.skeleton.wxss文件中的&#xff08;is““和data-event-opts””&#xff09;的整个属性删掉 一、描述&#xff1a;解决使用uniapp开发微信小程序生成骨架屏 很多人都知道微信开发者工具自带生成骨架屏的功能&#xff0c;但是却不…

vue骨架屏介绍

骨架屏可以理解为是当数据还未加载进来前&#xff0c;页面的一个空白版本&#xff0c;一个简单的关键渲染路径。可以看一下下面Facebook的骨架屏实现&#xff0c;可以看到在页面完全渲染完成之前&#xff0c;用户会看到一个样式简单&#xff0c;描绘了当前页面的大致框架的骨架…

微信小程序:骨架屏的实现方法

骨架屏是为了展示一个页面骨架而不含有实际的页面内容&#xff0c;从渲染效率上来讲&#xff0c;骨架屏它并不能使首屏渲染加快。由于骨架屏的一些使用又向用户渲染了额外的一些内容&#xff0c;这些内容是额外添加的、本来是不需要渲染的&#xff0c;它反而从整体上加长了首屏…

客户端骨架屏详解

一直以来&#xff0c;无论是Web还是iOS、Android的应用中&#xff0c;为了提升应用的加载等待这段时间的用户感知体验&#xff0c;各种技术层出不穷。其中&#xff0c;尤以菊花图以及由它衍生各种加载动画最为突出。 对于菊花图我们自不必多说&#xff0c;现在对于加载的设计体…

微信小程序使用骨架屏

骨架屏的使用越来越广泛。在微信小程序中使用骨架屏如果自己实现&#xff0c;不同的页面对应不同的骨架屏&#xff0c;会有一定难度&#xff1b;不过&#xff0c;微信小程序已经提供生成骨架屏功能&#xff0c;使得我们在开发中非常方便&#xff0c;下面介绍如何生成 在生成骨架…

Skeleton Screen — 骨架屏

用户体验一直是前端开发需要考虑的重要部分&#xff0c;在数据请求时常见到锁屏的loading动画&#xff0c;而现在越来越多的产品倾向于使用Skeleton Screen Loading(骨架屏)替代&#xff0c;以优化用户体验。 一种自动生成网页骨架屏的方式 前端骨架屏方案小结 Skeleton Scree…

element-骨架屏

使用场景&#xff1a;在需要等待加载内容的位置设置骨架屏。 主要代码&#xff1a;&#xff08;其中loading控制骨架屏的显示不显示&#xff0c;为false时不显示&#xff0c;为true时显示&#xff09; <template><div v-show"!loading"><!-- 第一行…

浅谈前端骨架屏方案

在图片与前端体验优化中&#xff0c;最重要的莫过于「骨架屏」了&#xff0c;因为它和“首屏体验”息息相关。 目前来说骨架屏基本上有两种方式&#xff1a; HTML CSS&#xff1a;主流。基本是自己在项目中以侵入式方式围绕html“定制”&#xff1b;微信小程序的骨架屏生成方…

前端骨架屏应用

什么是骨架屏 骨架屏可以理解为在页面数据尚未返回或页面未完成完全渲染前&#xff0c;先给用户呈现一个由灰白块组成的当前页面大致结构&#xff0c;让用户产生页面正在逐渐渲染的感受&#xff0c;从而使加载过程从视觉上变得流畅。 生成后的骨架屏页面如下图所示&#xff1…

如何实现骨架屏效果?

今天我们来用原生js实现一个骨架屏的效果&#xff0c;效果如下&#xff1a; 首先思考如何实现 思考实现方式 骨架屏的原理是在数据没加载出来的时候&#xff0c;使用滚动的背景颜色去替代&#xff0c;等到加载完毕后则显示对应内容 那么我们的核心就是实现一个.skeleton的样…

啥是骨架屏

&#xff08;一&#xff09;什么是骨架屏 骨架屏可以理解为是当数据还未加载进来前&#xff0c;页面的一个空白版本。在页面完全渲染完成之前&#xff0c;用户会看到一个样式简单&#xff0c;描绘了当前页面的大致框架的页面&#xff0c;然后骨架屏中各个占位部分被实际资源完…

前端骨架屏方案与实践

对于依赖接口渲染的页面&#xff0c;在拿到数据之前页面往往是空白的&#xff0c;为了提示用户当前正在加载中&#xff0c;往往会使用进度条、loading图标或骨架屏的方式。 对于前两种方案而言&#xff0c;实现比较简单&#xff1b;本文主要研究骨架屏的实现方案。 骨架屏(Ske…

网页骨架屏自动生成方案(dps)

来源&#xff1a;花满楼 https://zhuanlan.zhihu.com/p/74403911 什么是骨架屏&#xff1f; 什么是骨架屏呢&#xff1f;骨架屏(Skeleton Screen)是指在页面数据加载完成前&#xff0c;先给用户展示出页面的大致结构&#xff08;灰色占位图&#xff09;&#xff0c;在拿到接口数…

骨架屏

&#xff08;一&#xff09;什么是骨架屏 骨架屏可以理解为是当数据还未加载进来前&#xff0c;页面的一个空白版本。在页面完全渲染完成之前&#xff0c;用户会看到一个样式简单&#xff0c;描绘了当前页面的大致框架的页面&#xff0c;然后骨架屏中各个占位部分被实际资源完…

Vue中实现骨架屏的多种方式

vue-cli项目首页加载缓慢想要使用骨架屏效果&#xff0c;经过几天的实践&#xff0c;这里学习并记录一下vue项目自动生成骨架屏方法。 前言&#xff1a;骨架屏的作用主要是在网络请求较慢时&#xff0c;提供基础占位&#xff0c;当数据加载完成&#xff0c;恢复数据展示。这样给…

骨架屏原理——面试别再被挨打了

目录 前言 骨架屏是什么 骨架屏原理 用途&#xff1a; &#xff08;一&#xff09;简单实现 &#xff08;二&#xff09; vue项目中的构建 &#xff08;三&#xff09;自动化方案 前言 同样是之前练手项目中的&#xff0c;emmm,知道干嘛用的&#xff0c;没了解过具体原理…

性能测试总结之内存泄露和内存溢出

刚刚做完了一个项目的性能测试&#xff0c;“有幸”也遇到了内存泄露的案例&#xff0c;所以在此和大家分享一下。 主要从以下几部分来说明&#xff0c;关于内存和内存泄露、溢出的概念&#xff0c;区分内存泄露和内存溢出&#xff1b;内存的区域划分&#xff0c;了解GC回收机…

内存泄露与内存溢出的区别及解决方法

内存溢出与泄露的区别 内存溢出 out of memory&#xff0c;是指程序在申请内存时&#xff0c;没有足够的内存空间供其使用&#xff0c;出现out of memory&#xff1b;比如申请了一个integer,但给它存了long才能存下的数&#xff0c;那就是内存溢出。 内存泄露 memory leak&…

jvm故障 内存泄露和内存溢出总结

目录 内存泄漏memory leak 内存泄漏的分类&#xff08;按发生方式来分类&#xff09; 内存泄露的场景 静态集合类 / 长生命周期的对象持有短生命周期对象的引用 / 单例模式 /类加载器 各种连接&#xff0c;如数据库连接、网络连接和IO连接等 变量不合理的作用域 内部类持…