前言
骨架屏在很多应用中已经实现了,开源社区也有一些实现,但是有一些想要用的 Electron
还是比较困难。今天,我要给大家介绍如何为 Electron
应用生成 骨架屏。与其说是骨架屏,不如说是 骨架图。
骨架屏实现的几种方式
- 使用 ssr 预渲染
- 加载 js 后,再请求服务器获取数据时,先使用 ui 库的
Skeleton
组件去渲染部分内容 - 通过
puppetter
等工具去访问指定页面并截图,通过一定的算法识别后,使用纯html、css去组织一个骨架屏的内容,然后打包成离线包放在 App 中使用。
以上三种,应该涵盖了大多数实现骨架屏的方案。
在 Electron
应用的实际开发中,我基于第三种简化了下。总的思路是基于HtmlWebpackPlugin
和 Puppeteer
。在 HtmlWebpackPlugin
处理 html
模板时,通过替换模板中的 <!-- skeleton-css -->
和 <!-- skeleton-js -->
标记, 把渲染骨架图的样式和 js 逻辑插入到页面中。
我把这套逻辑封装成了一个 webpack 插件 —— skeleton-webpack-plugin
。
HTML 模板
先来看一下使用这个插件,html 模板需要改哪些部分。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/vnd.microsoft.icon" href="./favicon.ico">
<title>Electron</title>
<!-- skeleton-css -->
</head>
<body>
<div id="root">
<!-- skeleton-js -->
</div>
</body>
</html>
只需要添加 2个比较,<!-- skeleton-css -->
和 <!-- skeleton-js -->
,很简单吧。
然后我们看一下经过插件处理后的 html 模板
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" type="image/vnd.microsoft.icon" href="./favicon.ico">
<title>怡氧</title>
<style>
@keyframes skeletonLoading {
0% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
.skeleton-img {
animation: skeletonLoading 1s infinite;
}
</style>
</head>
<body>
<div id="root">
<script>var hash = location.hash.substring(2);
if (hash.includes("?")) {
hash = hash.substring(0, hash.indexOf('?'))
}
var skeletonName = hash.replaceAll('/', '--').replaceAll('\\', '--');
var image = document.createElement('img');
image.setAttribute("style", "position: fixed; top:0; left: 0;")
image.setAttribute("class", "skeleton-img")
image.setAttribute('alt', "")
image.src = './skeleton/' + skeletonName + '.png'
document.getElementById('root').appendChild(image)</script>
</div>
<script src="./static/js/index.fc7a1aa3.bundle.js"></script>
</body>
</html>
从 js 的逻辑中,相信大家已经看出来吧,主要是通过插入图片来显示骨架屏的。那么这个骨架图是如何实现的呢?我们等一会儿再讲。
Webpack Plguin 配置
先来看一下如何在 webpack 中使用插件
config.plugin('skeleton-plugin').use(SkeletonWebpackPlugin, [
{
outputPath: path.join(paths.appRoot, 'public'),
routes: ['/apps/recent', '/apps/new-file'],
},
])
上面是 webpack-chain 配置 webpack,不熟悉的可以看一下文档。
插件是支持多路由的,访问不同的路由有不同的骨架图。
骨架图又是如何生成的?
看代码的话其实很简单,但也有些麻烦。
这个方案缺点是侵入性强,所有需要绘制在骨架图的 DOM 元素都要添加一个特定的类名 —— skeleton
const path = require('path')
const { ensureDir } = require('fs-extra')
const HtmlWebpackPlugin = require('html-webpack-plugin')
class SkeletonWebpackPlugin {
constructor(options) {
this.options = Object.assign(
{},
{
fillStyle: '#eeeeee95',
cssMatch: '<!-- skeleton-css -->',
jsMatch: '<!-- skeleton-js -->',
publicPath: 'skeleton',
className: 'skeleton',
},
options
)
}
apply(compiler) {
this.options.context = compiler.context
this.options.devServer = compiler.options.devServer
this.options.mode = compiler.options.mode
compiler.hooks.compilation.tap('skeleton-webpack-plugin', (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).afterTemplateExecution.tapAsync(
'skeleton-webpack-plugin',
(htmlPluginData, callback) => {
if (htmlPluginData.html.includes(this.options.cssMatch)) {
htmlPluginData.html = htmlPluginData.html.replace(
this.options.cssMatch,
`
<style>
@keyframes skeletonLoading {
0% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
.skeleton-img {
animation: skeletonLoading 1s infinite;
}
</style>
`
)
}
if (htmlPluginData.html.includes(this.options.jsMatch)) {
const _path = this.options.publicPath
while (_path.startsWith('/') || _path.startsWith('.')) {
_path = _path.substring(1)
}
htmlPluginData.html = htmlPluginData.html.replace(
this.options.jsMatch,
`
<script>
var hash = location.hash.substring(2);
if (hash.includes("?")) {
hash = hash.substring(0, hash.indexOf('?'))
}
var skeletonName = hash.replaceAll('/', '--').replaceAll('\\\\', '--');
var image = document.createElement('img');
image.setAttribute("style", "position: fixed; top:0; left: 0;")
image.setAttribute("class", "skeleton-img")
image.setAttribute('alt', "")
image.src = './${_path}/' + skeletonName + '.png'
document.getElementById('root').appendChild(image)
</script>
`
)
}
callback(null, htmlPluginData)
}
)
})
compiler.hooks.done.tapAsync('skeleton-plugin', (compilation, callback) => {
if (this.options.mode == 'development') {
this.genSkeleton()
}
callback()
})
}
async sleep(time) {
await new Promise((resolve) => {
setTimeout(() => resolve(), time)
})
}
async genSkeleton() {
const browser = await require('puppeteer').launch({
defaultViewport: this.options.viewport,
})
const page = await browser.newPage()
let i = 1
const _path = this.options.publicPath
while (_path.startsWith('/') || _path.startsWith('.')) {
_path = _path.substring(1)
}
const parentDir = path.join(this.options.outputPath, _path)
await ensureDir(parentDir)
for (var route of this.options.routes) {
route = route.startsWith('/') ? route.substring(1) : route
await page.goto(
`http://localhost:${this.options.devServer.port}/#${route}`
)
await page.waitForNetworkIdle()
await this.sleep(1500)
if (!this.options.viewport) {
const availWidth = await page.evaluate('window.screen.availWidth')
const availHeight = await page.evaluate('window.screen.availHeight')
await page.setViewport({
width: availWidth,
height: availHeight,
})
}
const html = await page.evaluate(`
var eles = Array.from(document.getElementsByClassName("${this.options.className}"));
var rects = eles.map(function(n){
var rect = n.getBoundingClientRect()
return {
x: rect.x / window.innerWidth,
y: rect.y / window.innerHeight,
width: rect.width / window.innerWidth,
height: rect.height / window.innerHeight
}
})
rects;
`)
const imgPath = path.join(
parentDir,
route.replaceAll('/', '--').replaceAll('\\', '--') + '.png'
)
await page.screenshot({
path: imgPath,
})
await require('canvas')
.loadImage(imgPath)
.then((image) => {
const canvas = require('canvas').createCanvas(
image.width,
image.height
)
const ctx = canvas.getContext('2d')
ctx.fillStyle = this.options.fillStyle
html.forEach((rect) => {
ctx.fillRect(
rect.x * image.width,
rect.y * image.height,
rect.width * image.width,
rect.height * image.height
)
})
require('fs').writeFileSync(imgPath, canvas.toBuffer())
})
i++
}
await browser.close()
}
}
module.exports = SkeletonWebpackPlugin
总结
看一下实际使用的效果
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END
暂无评论内容