Vue单页面骨架屏实践
github 地址: VV-UI/VV-UI
演示地址: vv-ui
文档地址:skeleton
关于骨架屏介绍
骨架屏的作用主要是在网络请求较慢时,提供基础占位,当数据加载完成,恢复数据展示。这样给用户一种很自然的过渡,不会造成页面长时间白屏或者闪烁等情况。 常见的骨架屏实现方案有ssr
服务端渲染和prerender
两种解决方案。这里主要通过代码为大家展示如何一步步做出这样一个骨架屏:
prerender 渲染骨架屏
本组件库骨架屏的实现也是基于预渲染去实现的,有关于预渲染更详细的介绍请参考这篇文章:处理 Vue 单页面 Meta SEO的另一种思路 下面我们主要介绍其实现步骤,首先我们也是需要配置webpack-plugin,不过已经有实现好的prerender-spa-plugin可用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var path = require( 'path' ) var PrerenderSpaPlugin = require( 'prerender-spa-plugin' ) module.exports = { // ... plugins: [ new PrerenderSpaPlugin( // Absolute path to compiled SPA path.join(__dirname, '../dist' ), // List of routes to prerender [ '/' ] ) ] } |
然后写好我们的骨架屏文件main.skeleton.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | < template > < div class="main-skeleton"> < w-skeleton height="80px"></ w-skeleton > < div > < div class="skeleton-container"> < div class="skeleton"> < w-skeleton height="300px"></ w-skeleton > </ div > < w-skeleton height="45px"></ w-skeleton > </ div > < div class="skeleton-bottom"> < w-skeleton height="45px"></ w-skeleton > </ div > </ div > </ div > </ template > |
当初次进入页面的时候我们需要显示骨架屏,数据加载完,我们需要移除骨架屏:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | < template > < div id="app"> < mainSkeleton v-if="!init"></ mainSkeleton > < div v-else> < div class="body"></ div > </ div > </ div > </ template > < script > import mainSkeleton from './main.skeleton.vue' export default { name: 'app', data () { return { init: false } }, mounted () { // 这里模拟数据请求 setTimeout(() => { this.init = true }, 250) }, components: { mainSkeleton } } </ script > |
ssr 渲染骨架屏
下面我用我灵魂画师的笔法,画出了大致的过程:
首先创建我们的skeleton.entry.js
1 2 3 4 5 6 7 8 9 | import Vue from 'vue' ; import Skeleton from './skeleton.vue' ; export default new Vue({ components: { Skeleton }, template: '<skeleton />' }); |
当然这里的skeleton.vue
使我们事先写好的骨架屏组件,看起来可能是这样:
1 2 3 4 5 6 | < template > < div class="skeleton-wrapper"> < header class="skeleton-header"></ header > < div class="skeleton-block"></ div > </ div > </ template > |
然后我们需要的是能把skeleton.entry.js
编译成服务端渲染可用的bundle
文件,所以我们需要有个编译骨架屏的webpack.ssr.conf.js
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | const path = require( 'path' ); const merge = require( 'webpack-merge' ); const baseWebpackConfig = require( './webpack.base.conf' ); const nodeExternals = require( 'webpack-node-externals' ); function resolve(dir) { return path.join(__dirname, dir); } module.exports = merge(baseWebpackConfig, { target: 'node' , devtool: false , entry: { app: resolve( './src/skeleton.entry.js' ) }, output: Object.assign({}, baseWebpackConfig.output, { libraryTarget: 'commonjs2' }), externals: nodeExternals({ whitelist: /\.css$/ }), plugins: [] }); |
接下来最终的步骤,就是编写我们的webpackPlugin,我们期望我们的webpackPlugin可以帮我们把入口文件编译成bundle,然后再通过vue-server-renderer
来render bundle,最终产出响应的html片段和css片段,这里贴出核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // webpack start to work var serverCompiler = webpack(serverWebpackConfig); var mfs = new MFS(); // output to mfs serverCompiler.outputFileSystem = mfs; serverCompiler.watch({}, function (err, stats) { if (err) { reject(err); return ; } stats = stats.toJson(); stats.errors.forEach( function (err) { console.error(err); }); stats.warnings.forEach( function (err) { console.warn(err); }); var bundle = mfs.readFileSync(outputPath, 'utf-8' ); var skeletonCss = mfs.readFileSync(outputCssPath, 'utf-8' ); // create renderer with bundle var renderer = createBundleRenderer(bundle); // use vue ssr to render skeleton renderer.renderToString({}, function (err, skeletonHtml) { if (err) { reject(err); } else { resolve({skeletonHtml: skeletonHtml, skeletonCss: skeletonCss}); } }); }); |
最后一步,我们对产出的html片段, css片段进行组装,产出最终的html,所以我们需要监听webpack 的编译挂载之前的事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | compiler.plugin( 'compilation' , function (compilation) { // add listener for html-webpack-plugin compilation.plugin( 'html-webpack-plugin-before-html-processing' , function (htmlPluginData, callback) { ssr(webpackConfig).then( function (ref) { var skeletonHtml = ref.skeletonHtml; var skeletonCss = ref.skeletonCss; // insert inlined styles into html var headTagEndPos = htmlPluginData.html.lastIndexOf( '</head>' ); htmlPluginData.html = insertAt(htmlPluginData.html, ( "<style>" + skeletonCss + "</style>" ), headTagEndPos); // replace mounted point with ssr result in html var appPos = htmlPluginData.html.lastIndexOf(insertAfter) + insertAfter.length; htmlPluginData.html = insertAt(htmlPluginData.html, skeletonHtml, appPos); callback( null , htmlPluginData); }); }); }); |
关于:
作者:monkeyWang
本文参考文章:为vue项目添加骨架屏
本文源码详见:VV-UI/VV-UI
本人主页:monkeyWang
微信公众号:前端知识铺
会不定期推送前端技术文章,欢迎关注
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· 展开说说关于C#中ORM框架的用法!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?