本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
一、前言
在electron应用的使用过程中,经常会遇到白屏的情况。白屏出现的主要为中情况:
- 在应用使用过程中,程序白屏崩溃
- 在应用初次加载的时候,白屏时间过长
针对这两种情况,我们逐一进行解决分析。
二、方案
1.分析解决运行过程中出现的白屏问题
出现这种情况,主要的原因是代码编写不规范导致,例如内存泄漏、dom加载过多或某些地方陷入死循环等。针对这种情况,我们要做的就是软件运行监测,建立日志机制。 首先我们需要引入electron-log,这个插件对于前端来说,就类似于console.log()。我们自定义一个electron-log的实例.
安装
npm install electron-log
// or
yarn add electron-log
创建一个文件用于创建electron-log实例
// .ElectronLog.js
import log from 'electron-log'
import { app } from 'electron'
import path from 'path'
log.transports.file.level = 'debug'
log.transports.file.maxSize = 1002430 // 10M
log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}]{scope} {text}'
// ====重新定义日志输入的文件位置以及文件名====start
const currentDate = new Date().getFullYear() + '-' + (new Date().getMonth() + 1) + '-' + new Date().getDate()
const fileName = `${currentDate}@${app.getVersion()}.log`
const basePath = path.join(app.getPath('userData'), 'electron_log', fileName)
log.transports.file.resolvePath = () => basePath
// ====重新定义日志输入的文件位置以及文件名====end
export default {
info (param) {
log.info(param)
},
warn (param) {
log.warn(param)
},
error (param) {
log.error(param)
},
debug (param) {
log.debug(param)
},
verbose (param) {
log.verbose(param)
},
silly (param) {
log.silly(param)
}
}
这样我们的日志记录实例就创建完成了。 根据electron官方文档中,里面有关于进程崩溃的监听render-process-gone。官网内容如下
事件: 'render-process-gone'
返回:
-
event
Event -
details
Object-
reason
string - 渲染进程消失的原因。 可选值:clean-exit
- 以零为退出代码退出的进程abnormal-exit
- 以非零退出代码退出的进程killed
- 进程发送一个SIGTERM,否则是被外部杀死的。crashed
- 进程崩溃oom
- 进程内存不足launch-failed
- 进程从未成功启动integrity-failure
- 窗口代码完整性检查失败
-
exitCode
Integer - 进程的退出代码,除非在reason
是launch-failed
的情况下,exitCode
将是一个平台特定的启动失败错误代码。
-
渲染器进程意外消失时触发。 这种情况通常因为进程崩溃或被杀死。
我们在监听到程序已经崩溃(白屏了),之后可以通过系统通知告知用户,让用户重载或退出,给用户可选项,而不是不处理。
// 引入日志
import logger from './electron-config/libs/ElectronLog'
// 监听程序崩溃事件, win是窗口实例
win.webContents.on('render-process-gone', (e, details) => {
const options = {
type: 'error',
title: '进程崩溃了',
message: '这个进程已经崩溃.',
buttons: ['重载', '退出']
}
recordCrash(details).then(() => {
dialog.showMessageBox(options).then(({ response }) => {
console.log(response)
if (response === 0) reloadWindow()
else app.quit()
})
}).catch((e) => {
console.log('err', e)
})
})
function recordCrash (arg) {
return new Promise(resolve => {
// 崩溃日志请求成功....
log.info(arg)
resolve()
})
}
// 重载
function reloadWindow () {
app.relaunch()
app.exit(0)
}
可以在控制台输入process.crash()检测是否设置成功。 那我们如何发现错误呢?可以使用process.on捕捉全局uncaughtException异常
process.on('uncaughtException', function (err) {
log.error("=======捕捉异常=======start")
log.error(err)
log.error("=======捕捉异常=======end")
})
这样我们就可以通过记录日志的方式发现导致electron应用运行中白屏的问题,以及做了白屏后的处理机制。
2.解决初次加载白屏时间过长问题
初次加载白屏的主要原因就是项目首屏加载速度过慢,可以通过优化vue项目加载效率的方式提高首屏加载速率,例如路由懒加载、打包拆分、组件按需引入。
这些方法是可以解决初次白屏时间过长的。但今天这里要讲的不是这种方式,而是一种较为偷懒的方法,即给窗口加个loading过度加载白屏这段时间。
我的思路是给需要通过加载过度的窗口添加一个子窗口,这个子窗口里面仅展示一个loading,等父级窗口加载完成后,再关掉子窗口。
首先,我们先写一个html静态的loading页面,我将静态页面public文件中
<!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">
</head>
<body>
<div id="loading-page" class="loading-wrap">
<div id="title"></div>
<div id="closeBtn" class="close-icon" onclick="controlWindow()">
<span>x</span>
</div>
<!-- loading内容可自定义为图片之类的 -->
<div id="loadingImgPre">
loading...
</div>
</div>
</body>
<script>
const remote = require('electron').remote
const ipcRenderer = require('electron').ipcRenderer
// 判断当前系统若为windows,将关闭按钮显示出来
if (process.platform !== 'darwin') {
document.getElementById('closeBtn').style.display = 'block'
}
// windows环境下需要给子窗口添加关闭按钮,因为在loading过程中用户可能会选择关闭窗口
const controlWindow = () => {
remote.getCurrentWindow().close()
}
// 获取当前页面url
let url = window.location.href
let GetQueryString = name => {
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)')
var r = window.location.search.substr(1).match(reg)
if (r != null) return decodeURI(r[2])
return null
}
// 若需要显示标题,则在地址栏中获取 windowTitle 为约定窗口名称字段(见下方)
let windowTitle = GetQueryString('windowTitle')
if (windowTitle) {
document.getElementById('title').innerText = windowTitle
}
</script>
<style>
html {
width: 100%;
height: 100%;
}
body {
margin: 0;
margin: auto;
}
body, #loading-page{
width: 100%;
height: 100%;
}
#loading-page {
display: flex;
align-items: center;
justify-content: center;
background: #fff;
}
#loadingImgPre{
height:40px;
width:40px;
position: relative;
}
.close-icon {
display: none;
width: 40px;
height: 40px;
text-align: center;
line-height: 34px;
cursor: pointer;
-webkit-app-region: no-drag;
position: absolute;
pointer-events: auto;
top: 0;
right: 0;
font-size: 14px;
font-weight: 600;
color: #1f2329;
}
.close-icon:hover {
background: #FF6161 !important;
}
.close-icon:hover span {
color: #fff !important;
}
#title{
font-size: 14px;
color: #1f2329;
font-weight: 500;
height: 40px;
width: 200px;
text-align: center;
position: absolute;
top: 0;
line-height: 40px;
}
</style>
</html>
之后我们创建一个js文件,用于存放子窗口的实例
import { BrowserWindow } from 'electron'
const path = require('path')
const CreateProcessLoadingPage = function (win, data = {}) {
const child = new BrowserWindow({
width: data.width || 1024,
height: data.height || 640,
parent: win,
frame: false,
title: 'loading...',
show: false,
center: true,
fullscreenable: false,
transparent: true,
titleBarStyle: 'hidden',
trafficLightPosition: { x: 12, y: 18 },
visualEffectState: 'active',
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
})
// 加载本地html文件---loading页(静态页面放在了public文件中)
// eslint-disable-next-line no-undef
child.loadFile(path.join(__static, '/LoadingPage.html'), { query: data })
// 当子窗口已经准备完成,可以展示时
child.once('ready-to-show', () => {
// 开发调试使用
// child.webContents.openDevTools()
// ====设置子窗口的位置以及大小,与父窗口同步大小位置(完全覆盖)====start
child.setPosition(win.getPosition()[0], win.getPosition()[1])
const width = win.getSize()[0] < 1024 ? 1024 : win.getSize()[0]
const height = win.getSize()[1] < 640 ? 640 : win.getSize()[1]
child.setSize(width, height)
// ====设置子窗口的位置以及大小,与父窗口同步大小位置(完全覆盖)====
// 之后同步展示父窗口->子窗口
win.show()
child.show()
})
// 监听子窗口关闭,则父窗口关闭--同步操作
child.on('close', () => {
win.close()
})
child.on('maximize', () => {
win.maximize()
})
child.on('unmaximize', () => {
win.unmaximize()
})
child.on('minimize', () => {
win.minimize()
})
child.on('restore', () => {
win.restore()
})
// 监听父窗口已经准备完成,可以展示了
win.once('ready-to-show', () => {
// 现将父窗口获取焦点
win.focus()
// 此时判断子窗口有没有被销毁,没有被销毁的话
if (!child.isDestroyed()) {
setTimeout(() => { // 稍作1s的延迟,之后关闭子窗口
if (!child.isDestroyed()) {
child.destroy()
}
}, 1000)
}
})
}
export default CreateProcessLoadingPage
子窗口loading我们也创建完成了, 下面是使用它
// 引入子窗口实例
import CreateProcessLoadingPage from './CreateProcessLoadingPage'
// 在父窗口创建实例成功时,调用子窗口loading的实例方法,win为父窗口实例
CreateProcessLoadingPage(win)
最后我们就解决了electron应用首屏加载长时间的白屏问题。完结!撒花!
三、后记
如果你的loading 使用的是lottie插件,建议将lottie插件文件下载下来,作为静态文件,引入loading静态页中,但是会报错,全局变量找不到lottie。解决方法就是需要更改下lottie.js中的源代码,如下
// 引入子窗口实例
(typeof navigator !== 'undefined') && (function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(function () {
return factory(root)
})
}
// 在electron中本身有module的存在,所以会执行到这里,但是factory返回的lottie对象赋值到了错误的地方,导致全局lottie对象丢失
// else if (typeof module === "object" && module.exports) {
// module.exports = factory(root);
// }
else {
root.lottie = factory(root)
root.bodymovin = root.lottie
}
}((window || {}), function (window) {
这样就解决了,无法获取lottie的问题。
感谢观看!希望能帮助到你!