IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 自动化生成骨架屏的技术方案设计与落地 -> 正文阅读

[JavaScript知识库]自动化生成骨架屏的技术方案设计与落地

?

个人文章集:https://github.com/Nealyang/PersonalBlog

主笔公众号:全栈前端精选

?

背景

性能优化,减少页面加载等待时间一直是前端领域永恒的话题。如今大部分业务合作模式都是前后端分离方案,便利性的同时也带来了非常多的弊端,比如 FCP 时间显著增加(多了更多的 HTTP 请求往返的时间消耗),这也就造成了我们所说的白屏时间较长,用户体验较差的情况。

当然,对此我们可以有很多种优化手段,即便是此文介绍的骨架屏也只是用户体验的优化而已,对性能优化的数据没有任何提升,但是其必要性,依然是不言而喻的。

本文主要介绍应用在拍卖源码工作台BeeMa 架构中的骨架屏自动生成方案。有一定的定制型,但是基本原理是相通的。

骨架屏 Skeleton

71d7bf9d870cba0629e94b9c03b31cc7.gif
Skeleton

骨架屏其实就是在页面加载内容之前,先给用户展示出页面的大致结构,再等拿到接口数据后在将内容替换,较传统的菊花 loading 效果会给用户一种“已经渲染一部分出来了”的错觉,在效果上可以一定程度的提升用户体验。本质上就是视觉过渡的一个效果,以此来降低用户在等待时候的焦灼情绪。

方案调研

骨架屏技术方案上从实现上来说大致可以三类:

  • 手动维护骨架屏的代码(HTMLcss or vueReact

  • 使用图片作为骨架屏

  • 自动生成骨架屏

对于前两种方案有一定的维护成本比较费人力,这里主要介绍下自动生成骨架屏的方案。

目前市面上主要使用的是饿了么开源的 webpack 插件:page-skeleton-webpack-plugin。它根据项目中不同的路由页面生成相应的骨架屏页面,并将骨架屏页面通过 webpack 打包到对应的静态路由页面中。这种方式将骨架屏代码与业务代码隔离,通过 webpack 注入的方式骨架屏代码(图片)注入到项目中。优势非常明显但是缺点也显而易见:webpack配置成本(还依赖html-webpack-plugin)。

技术方案

综合如上的技术调研,我们还是决定采用最低侵入业务代码且降低配置成本的骨架屏自动生成的方案。参考饿了么的设计思路,基于 BeeMa 架构和vscode插件来实现一个新的骨架屏生成方案。

设计原则

参考目前使用骨架屏的业务团队,我们首先要明确下我们的骨架屏需要具有的一些原则:

  • 骨架屏基于 BeeMa 架构

  • 自动生成

  • 维护成本低

  • 可配置

  • 还原度高(适配能力强)

  • 性能影响低

  • 支持用户二次修订

基于如上原则和 beema 架构vscode 插件的特性,如下使我们最终的技术方案设计:

  • 基于 BeeMa framework插件,提供骨架屏生成配置界面

  • 选择基于 BeeMa 架构的页面,支持 SkeletonScreen height、ignoreHeight/width、通用头和背景色保留等

  • 基于 Puppeteer 获取预发页面(支持登陆)

  • 功能封装到 BeeMa Framework:https://marketplace.visualstudio.com/items?itemName=nealyang.devworks-beema 插件中

  • 骨架屏只吐出 HTML 结构,样式基于用户自动以的 CSSInModel 的样式

  • 骨架屏样式,沉淀到项目 global.scss中,避免行内样式重复体积增大

流程图

d0b2ebab5ef82da3f73883586755751d.png

技术细节

校验 Puppeteer、

/**
?*?检查本地?puppeteer
?*?@param?localPath?本地路径
?*/
export?const?checkLocalPuppeteer?=?(localPath:?string):?Promise<string>?=>?{
??const?extensionPuppeteerDir?=?'mac-901912';
??return?new?Promise(async?(resolve,?reject)?=>?{
????try?{
??????//?/puppeteer/.local-chromium
??????if?(fse.existsSync(path.join(localPath,?extensionPuppeteerDir)))?{
????????//?本地存在?mac-901912
????????console.log('插件内存在?chromium');
????????resolve(localPath);
??????}?else?{
????????//?本地不存在,找全局?node?中的?node_modules
????????nodeExec('tnpm?config?get?prefix',?function?(error,?stdout)?{
??????????//?/Users/nealyang/.nvm/versions/node/v16.3.0
??????????if?(stdout)?{
????????????console.log('globalNpmPath:',?stdout);
????????????stdout?=?stdout.replace(/[\r\n]/g,?'').trim();
????????????let?localPuppeteerNpmPath?=?'';
????????????if?(fse.existsSync(path.join(stdout,?'node_modules',?'puppeteer')))?{
??????????????//?未使用nvm,则全局包就在?prefix?下的?node_modules?内
??????????????localPuppeteerNpmPath?=?path.join(stdout,?'node_modules',?'puppeteer');
????????????}
????????????if?(fse.existsSync(path.join(stdout,?'lib',?'node_modules',?'puppeteer')))?{
??????????????//?使用nvm,则全局包就在?prefix?下的lib?下的?node_modules?内
??????????????localPuppeteerNpmPath?=?path.join(stdout,?'lib',?'node_modules',?'puppeteer');
????????????}
????????????if?(localPuppeteerNpmPath)?{
??????????????const?globalPuppeteerPath?=?path.join(localPuppeteerNpmPath,?'.local-chromium');
??????????????if?(fse.existsSync(globalPuppeteerPath))?{
????????????????console.log('本地 puppeteer 查找成功!');
????????????????fse.copySync(globalPuppeteerPath,?localPath);
????????????????resolve(localPuppeteerNpmPath);
??????????????}?else?{
????????????????resolve('');
??????????????}
????????????}?else?{
??????????????resolve('');
????????????}
??????????}?else?{
????????????resolve('');
????????????return;
??????????}
????????});
??????}
????}?catch?(error:?any)?{
??????showErrorMsg(error);
??????resolve('');
????}
??});
};

webView 打开后,立即校验本地 Puppeteer

useEffect(() => {
    (async () => {
      const localPuppeteerPath = await callService('skeleton', 'checkLocalPuppeteerPath');
      if(localPuppeteerPath){
        setState("success");
        setValue(localPuppeteerPath);
      }else{
        setState('error')
      }
    })();
  }, []);
?

「Puppeteer 安装到项目内,webpack 打包并不会处理 Chromium 的二进制文件,可以将 Chromium copy 到 vscode extension 的build中。」

「但是!!!导致 build 过大,下载插件会超时!!!所以只能考虑将 Puppeteer 要求在用户本地全局安装。」

?

puppeteer

/**
?*?获取骨架屏?HTML?内容
?*?@param?pageUrl?需要生成骨架屏的页面?url
?*?@param?cookies?登陆所需的?cookies
?*?@param?skeletonHeight?所需骨架屏最大高度(高度越大,生成的骨架屏?HTML?大小越大)
?*?@param?ignoreHeight?忽略元素的最大高度(高度低于此则从骨架屏中删除)
?*?@param?ignoreWidth?忽略元素的最大宽度(宽度低于此则从骨架屏中删除)
?*?@param?rootSelectId??beema?架构中?renderID,默认为?root
?*?@param?context?vscode?Extension?context
?*?@param?progress?进度实例
?*?@param?totalProgress?总进度占比
?*?@returns
?*/
export?const?genSkeletonHtmlContent?=?(
??pageUrl:?string,
??cookies:?string?=?'[]',
??skeletonHeight:?number?=?800,
??ignoreHeight:?number?=?10,
??ignoreWidth:?number?=?10,
??rootId:?string?=?'root',
??retainNav:?boolean,
??retainGradient:?boolean,
??context:?vscode.ExtensionContext,
??progress:?vscode.Progress<{
????message?:?string?|?undefined;
????increment?:?number?|?undefined;
??}>,
??totalProgress:?number?=?30,
):?Promise<string>?=>?{
??const?reportProgress?=?(percent:?number,?message?=?'骨架屏?HTML?生成中')?=>?{
????progress.report({?increment:?percent?*?totalProgress,?message?});
??};
??return?new?Promise(async?(resolve,?reject)?=>?{
????try?{
??????let?content?=?'';
??????let?url?=?pageUrl;
??????if?(skeletonHeight)?{
????????url?=?addParameterToURL(`skeletonHeight=${skeletonHeight}`,?url);
??????}
??????if?(ignoreHeight)?{
????????url?=?addParameterToURL(`ignoreHeight=${ignoreHeight}`,?url);
??????}
??????if?(ignoreWidth)?{
????????url?=?addParameterToURL(`ignoreWidth=${ignoreWidth}`,?url);
??????}
??????if?(rootId)?{
????????url?=?addParameterToURL(`rootId=${rootId}`,?url);
??????}
??????if?(isTrue(retainGradient))?{
????????url?=?addParameterToURL(`retainGradient=${'true'}`,?url);
??????}
??????if?(isTrue(retainNav))?{
????????url?=?addParameterToURL(`retainNav=${'true'}`,?url);
??????}
??????const?extensionPath?=?(context?as?vscode.ExtensionContext).extensionPath;
??????const?jsPath?=?path.join(extensionPath,?'dist',?'skeleton.js');
??????const?browser?=?await?puppeteer.launch({
????????headless:?true,
????????executablePath:?path.join(
??????????extensionPath,
??????????'/mac-901912/chrome-mac/Chromium.app/Contents/MacOS/Chromium',
????????),
????????//?/Users/nealyang/Documents/code/work/beeDev/dev-works/extensions/devworks-beema/node_modules/puppeteer/.local-chromium/mac-901912/chrome-mac/Chromium.app/Contents/MacOS/Chromium
??????});
??????const?page?=?await?browser.newPage();
??????reportProgress(0.2,?'启动BeeMa内置浏览器');
??????page.on('console',?(msg:?any)?=>?console.log('PAGE?LOG:',?msg.text()));
??????page.on('error',?(msg:?any)?=>?console.log('PAGE?ERR:',?...msg.args));
??????await?page.emulate(iPhone);
??????if?(cookies?&&?Array.isArray(JSON.parse(cookies)))?{
????????await?page.setCookie(...JSON.parse(cookies));
????????reportProgress(0.4,?'注入?cookies');
??????}
??????await?page.goto(url,?{?waitUntil:?'networkidle2'?});
??????reportProgress(0.5,?'打开对应页面');
??????await?sleep(2300);
??????if?(fse.existsSync(jsPath))?{
????????const?jsContent?=?fse.readFileSync(jsPath,?{?encoding:?'utf-8'?});
????????progress.report({?increment:?50,?message:?'注入内置JavaScript脚本'?});
????????await?page.addScriptTag({?content:?jsContent?});
??????}
??????content?=?await?page.content();
??????content?=?content.replace(/<!---->/g,?'');
??????//?fse.writeFileSync('/Users/nealyang/Documents/code/work/beeDev/dev-works/extensions/devworks-beema/src/index.html',?content,?{?encoding:?'utf-8'?})
??????reportProgress(0.9,?'获取页面?HTML?架构');
??????await?browser.close();
??????resolve(getBodyContent(content));
????}?catch?(error:?any)?{
??????showErrorMsg(error);
????}
??});
};
?

vscode 中的配置,需要写入到即将注入到 Chromium 中 p

age 加载的 js 中,这里采用的方案是将配置信息写入到要打开页面的 url 的查询参数中

?
66fa48647979f09183c3b828c9314aa5.png
scriptIndex

webView & vscode 通信(配置)

详见基于 monorepo 的 vscode 插件及其相关 packages 开发架构实践总结

vscode

export?default?(context:?vscode.ExtensionContext)?=>?()?=>?{
??const?{?extensionPath?}?=?context;
??let?pageHelperPanel:?vscode.WebviewPanel?|?undefined;
??const?columnToShowIn?=?vscode.window.activeTextEditord
??????vscode.window.activeTextEditor.viewColumn
????:?undefined;

??if?(pageHelperPanel)?{
????pageHelperPanel.reveal(columnToShowIn);
??}?else?{
????pageHelperPanel?=?vscode.window.createWebviewPanel(
??????'BeeDev',
??????'骨架屏',
??????columnToShowIn?||?vscode.ViewColumn.One,
??????{
????????enableScripts:?true,
????????retainContextWhenHidden:?true,
??????},
????);
??}
??pageHelperPanel.webview.html?=?getHtmlFroWebview(extensionPath,?'skeleton',?false);
??pageHelperPanel.iconPath?=?vscode.Uri.parse(DEV_WORKS_ICON);
??pageHelperPanel.onDidDispose(
????()?=>?{
??????pageHelperPanel?=?undefined;
????},
????null,
????context.subscriptions,
??);
??connectService(pageHelperPanel,?context,?{?services?});
};

connectSeervice

export?function?connectService(
??webviewPanel:?vscode.WebviewPanel,
??context:?vscode.ExtensionContext,
??options:?IConnectServiceOptions,
)?{
??const?{?subscriptions?}?=?context;
??const?{?webview?}?=?webviewPanel;
??const?{?services?}?=?options;
??webview.onDidReceiveMessage(
????async?(message:?IMessage)?=>?{
??????const?{?service,?method,?eventId,?args?}?=?message;
??????const?api?=?services?&&?services[service]?&&?services[service][method];
??????console.log('onDidReceiveMessage',?message,?{?api?});
??????if?(api)?{
????????try?{
??????????const?fillApiArgLength?=?api.length?-?args.length;
??????????const?newArgs?=
????????????fillApiArgLength?>?0???args.concat(Array(fillApiArgLength).fill(undefined))?:?args;
??????????const?result?=?await?api(...newArgs,?context,?webviewPanel);

??????????console.log('invoke?service?result',?result);
??????????webview.postMessage({?eventId,?result?});
????????}?catch?(err)?{
??????????console.error('invoke?service?error',?err);
??????????webview.postMessage({?eventId,?errorMessage:?err.message?});
????????}
??????}?else?{
????????vscode.window.showErrorMessage(`invalid?command?${message}`);
??????}
????},
????undefined,
????subscriptions,
??);
}

Webview 中调用 callService

//?@ts-ignore
export?const?vscode?=?typeof?acquireVsCodeApi?===?'function'???acquireVsCodeApi()?:?null;

export?const?callService?=?function?(service:?string,?method:?string,?...args)?{
??return?new?Promise((resolve,?reject)?=>?{
????const?eventId?=?setTimeout(()?=>?{});

????console.log(`WebView call vscode extension service:${service}?${method}?${eventId}?${args}`);

????const?handler?=?(event)?=>?{
??????const?msg?=?event.data;
??????console.log(`webview?receive?vscode?message:}`,?msg);
??????if?(msg.eventId?===?eventId)?{
????????window.removeEventListener('message',?handler);
????????msg.errorMessage???reject(new?Error(msg.errorMessage))?:?resolve(msg.result);
??????}
????};

????//?webview?接受?vscode?发来的消息
????window.addEventListener('message',?handler);

????//?WebView?向?vscode?发送消息
????vscode.postMessage({
??????service,
??????method,
??????eventId,
??????args,
????});
??});
};
const?localPuppeteerPath?=?await?callService('skeleton',?'checkLocalPuppeteerPath');

launchJs

本地 js 通过 rollup 打包

e72990b4e1208d41236fe46adb3999f1.png
src

rollupConfig

export?default?{
??input:?'src/skeleton/scripts/index.js',
??output:?{
????file:?'dist/skeleton.js',
????format:?'iife',
??},
};
0034d3834d4b657b699e0d51c3fb8e4b.png
addScriptTag

文本处理

?

这里我们统一将行内元素作为文本处理方式

?
import?{?addClass?}?from?'../util';
import?{?SKELETON_TEXT_CLASS?}?from?'../constants';

export?default?function?(node)?{
??let?{?lineHeight,?fontSize?}?=?getComputedStyle(node);
??if?(lineHeight?===?'normal')?{
????lineHeight?=?parseFloat(fontSize)?*?1.5;
????lineHeight?=?isNaN(lineHeight)???'18px'?:?`${lineHeight}px`;
??}
??node.style.lineHeight?=?lineHeight;
??node.style.backgroundSize?=?`${lineHeight}?${lineHeight}`;
??addClass(node,?SKELETON_TEXT_CLASS);
}

SKELETON_TEXT_CLASS的样式作为 beema 架构中的 global.scss 中。

const?SKELETON_SCSS?=?`

//?beema?skeleton
.beema-skeleton-text-class?{
??background-color:?transparent?!important;
??color:?transparent?!important;
??background-image:?linear-gradient(transparent?20%,?#e2e2e280?20%,?#e2e2e280?80%,?transparent?0%)?!important;
}
.beema-skeleton-pseudo::before,
.beema-skeleton-pseudo::after?{
??background:?#f7f7f7?!important;
??background-image:?none?!important;
??color:?transparent?!important;
??border-color:?transparent?!important;
??border-radius:?0?!important;
}
`;

/**
?*
?*?@param?proPath?项目路径
?*/
export?const?addSkeletonSCSS?=?(proPath:?string)?=>?{
??const?globalScssPath?=?path.join(proPath,?'src',?'global.scss');
??if?(fse.existsSync(globalScssPath))?{
????let?fileContent?=?fse.readFileSync(globalScssPath,?{?encoding:?'utf-8'?});
????if?(fileContent.indexOf('beema-skeleton')?===?-1)?{
??????//?本地没有骨架屏的样式
??????fileContent?+=?SKELETON_SCSS;
??????fse.writeFileSync(globalScssPath,?fileContent,?{?encoding:?'utf-8'?});
????}
??}
};

如果 global.scss 中没有相应骨架屏的样式 class,则自动注入进去

「这是因为如果作为行内元素的话,生成的骨架屏代码会比较大,重复代码多,这里是为了提及优化做的事情」

图片处理

import?{?MAIN_COLOR,?SMALLEST_BASE64?}?from?'../constants';

import?{?setAttributes?}?from?'../util';

function?imgHandler(node)?{
??const?{?width,?height?}?=?node.getBoundingClientRect();

??setAttributes(node,?{
????width,
????height,
????src:?SMALLEST_BASE64,
??});

??node.style.backgroundColor?=?MAIN_COLOR;
}

export?default?imgHandler;
export?const?SMALLEST_BASE64?=
??'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';

超链接处理

function?aHandler(node)?{
??node.href?=?'javascript:void(0);';
}

export?default?aHandler;

伪元素处理

//?Check?the?element?pseudo-class?to?return?the?corresponding?element?and?width
export?const?checkHasPseudoEle?=?(ele)?=>?{
??if?(!ele)?return?false;

??const?beforeComputedStyle?=?getComputedStyle(ele,?'::before');
??const?beforeContent?=?beforeComputedStyle.getPropertyValue('content');
??const?beforeWidth?=?parseFloat(beforeComputedStyle.getPropertyValue('width'),?10)?||?0;
??const?hasBefore?=?beforeContent?&&?beforeContent?!==?'none';

??const?afterComputedStyle?=?getComputedStyle(ele,?'::after');
??const?afterContent?=?afterComputedStyle.getPropertyValue('content');
??const?afterWidth?=?parseFloat(afterComputedStyle.getPropertyValue('width'),?10)?||?0;
??const?hasAfter?=?afterContent?&&?afterContent?!==?'none';

??const?width?=?Math.max(beforeWidth,?afterWidth);

??if?(hasBefore?||?hasAfter)?{
????return?{?hasBefore,?hasAfter,?ele,?width?};
??}
??return?false;
};
import?{?checkHasPseudoEle,?addClass?}?from?'../util';

import?{?PSEUDO_CLASS?}?from?'../constants';

function?pseudoHandler(node)?{
??if?(!node.tagName)?return;

??const?pseudo?=?checkHasPseudoEle(node);

??if?(!pseudo?||?!pseudo.ele)?return;

??const?{?ele?}?=?pseudo;
??addClass(ele,?PSEUDO_CLASS);
}

export?default?pseudoHandler;
?

伪元素的样式代码已经在上面 global.scss 中展示了

?

通用处理

//?移除不需要的元素
??Array.from($$(REMOVE_TAGS.join(','))).forEach((ele)?=>?removeElement(ele));

??//?移除容器外的所有?dom
??Array.from(document.body.childNodes).map((node)?=>?{
????if?(node.id?!==?ROOT_SELECTOR_ID)?{
??????removeElement(node);
????}
??});

??//?移除容器内非模块?element
??Array.from($$(`#${ROOT_SELECTOR_ID}?.contentWrap`)).map((node)?=>?{
????Array.from(node.childNodes).map((comp)?=>?{
??????if?(comp.classList?&&?Array.from(comp.classList).includes('compContainer'))?{
????????//?模块设置白色背景色
????????comp.style.setProperty('background',?'#fff',?'important');
??????}?else?if?(
????????comp.classList?&&
????????Array.from(comp.classList).includes('headContainer')?&&
????????RETAIN_NAV
??????)?{
????????console.log('保留通用头');
??????}?else?if?(
????????comp.classList?&&
????????Array.from(comp.classList).join().includes('gradient-bg')?&&
????????RETAIN_GRADIENT
??????)?{
????????console.log('保留了渐变背景色');
??????}?else?{
????????removeElement(comp);
??????}
????});
??});

??//?移除屏幕外的node
??let?totalHeight?=?0;
??Array.from($$(`#${ROOT_SELECTOR_ID}?.compContainer`)).map((node)?=>?{
????const?{?height?}?=?getComputedStyle(node);
????console.log(totalHeight);
????if?(totalHeight?>?DEVICE_HEIGHT)?{
??????//?DEVICE_HEIGHT?高度以后的node全部删除
??????console.log(totalHeight);
??????removeElement(node);
????}
????totalHeight?+=?parseFloat(height);
??});

??//?移除?ignore?元素
??Array.from($$(`.${IGNORE_CLASS_NAME}`)).map(removeElement);
?

这里有个计算屏幕外的 node,也就是通过用户自定义的最大高度,取到 BeeMa 中每一个模块的高度,然后相加计算,如果超过这个高度,则后续的模块直接 remove 掉,一次来减少生成出的 HTML 代码的大小问题

?

使用

基本使用


7281eac80c39c694085d1df06f071f56.png

beema

769a3048f1170f7fb11bbcd462628f3b.png

约束

需全局安装 「puppeteer@10.4.0 : tnpm i puppeteer@10.4.0 --g」

84df4cd602aac2d9f8e976268a86798b.png
local Puppeteer

全局安装后,插件会自动查找本地的 puppeteer 路径,如果找到插件,则进行 copy 到插件内的过程,否则需要用户自己手动填写路径puppeteer地址。(一旦查找成功后,后续则无需填写地址,全局 puppeteer 包也可删除)

目前仅支持 beema 架构源码开发

4b3cb59ace7a441d2d8d7b4d373bf62d.png
VSCode 插件

注意??

如果生成出来的代码片段较大,如下两种「优化方案」

「1、减少骨架屏的高度(配置界面中最大高度)」

「2、在源码开发中,对于首屏代码但是非首屏展示的元素添加beema-skeleton-ignore的类名(例如轮播图的后面几张图甚至视频)」

效果演示

普通效果

614137932e466dbe9398825f8e975b8b.png 1b698215c10834f8f480c8ffd4eb3128.gif

生成的代码大小:

6bb72f41cad9d61901a2a5a279dec0f0.png
5.37kb

带有通用头和渐变背景色

?

拍卖通用设计元素,在页面新建空页面配置中即可看到配置

?
07ab89942605981761cb153f28fd8e9c.png
通用配置

效果如下:

ee2a97ef70424a4ef6bf8e97277c79ed.gif
带头部和背景色
a6fa96df184a09db8741d1afc725af22.png
6.93

复杂元素的页面效果展示

默认全屏骨架屏

c8e42ff84969d3e29a55599454aee811.gif

生成代码大小

570cba5d489f9f565cfb0064e40da2a7.png
20kb
?

未做 skeleton-ignore 侵入式优化,略大🥺

?

另一种优化手段是减小生成骨架屏的高度!

半屏骨架屏

8897e7097ff734955b33371ca5e71550.png

半屏
?

Fast 3Gno throttling的网络情况下,公众号中 gif 帧数限制,只能放图片展示效果了。

?

生成代码大小

c6f2be4da317fd37b1923408968de09e.png
7kb

后续优化

  • 增加通用头样式定制型

  • 支持骨架屏样式配置(颜色等)

  • 减少生成代码的提及大小

  • ...

  • 持续解决团队内使用反馈

参考资料

  • page-skeleton-webpack-plugin:https://github.com/ElemeFE/page-skeleton-webpack-plugin

  • awesome-skeleton:https://github.com/kaola-fed/awesome-skeleton

  • Building Skeleton Screens with CSS Custom Properties:https://css-tricks.com/building-skeleton-screens-css-custom-properties/

  • Vue页面骨架屏注入实践:https://segmentfault.com/a/1190000014832185?spm=ata.21736010.0.0.1273641fkJNOGV

  • BeeMa:https://marketplace.visualstudio.com/search?term=beema&target=VSCode&category=All%20categories&sortBy=Relevance

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-11-24 07:51:19  更:2021-11-24 07:51:21 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/21 5:17:50-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码