【Electron】vue+electron白屏问题的解决方案

4,289 阅读5分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

一、前言

在electron应用的使用过程中,经常会遇到白屏的情况。白屏出现的主要为中情况:

  1. 在应用使用过程中,程序白屏崩溃
  2. 在应用初次加载的时候,白屏时间过长

针对这两种情况,我们逐一进行解决分析。

二、方案

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的问题。

感谢观看!希望能帮助到你!