Electron开发实践--封装一个python命令行工具的GUI

3,343 阅读2分钟

背景

公司需要制作一个桌面程序,之前有一个python写的cli工具,需要封装一个GUI跨平台使用,技术采用Electron + Vue技术栈,模板使用electron-vue,特殊之处是应用中使用pyinstaller打包成跨平台应用使用node调用。

基础项目中遇到的问题

上面提到该项目是根据electron-vue模板创建,在创建时,选用了electron-builder作为打包工具,打包后会在build目录下生成*-unpacked目录和对应的.exe.deb等可安装文件。下面说下基于该模板开发时遇到的问题。

升级electron版本

  • 模板使用的electron版本是2.X版本,版本较低,故升级到了11.3.0版本,以方便使用最新的框架特性
  • 升级后模板自带的vuex-electron插件不好使故去掉了

系统中的路径分隔符的问题

  • 由于历史原因路径的分割符在WindowsPOSIX不同
    • \ 反斜杠,在Windows,如C:\tmp\test.txt
    • / 斜杆,在POSIX,如/tmp/test.txt
  • 各个编程语言均有获取当前系统分隔符方法的api,node中可以使用:
    const sep = require('path').sep
    
  • 故在手动添加路径的地方需要通过上面提到的api获取路径分割符,动态进行拼接:
    import path from 'path'
    const downloadPath = `${this.$electron.remote.app.getPath('downloads')}${path.sep}合并表格.xlsx`
    

关于使用electron-builder打包遇到的问题

  • 据说使用electron-builder可以打包出跨操作系统的应用程序,保险起见,在WindowsDeepin系统分别进行打包
  • 打包的配置基本通过package.json即可配置完成:
    "build": {
      "productName": "excel-util",
      "appId": "com.excelUtil.xxx",
      "directories": {
        "output": "build"
      },
      "files": [
        "dist/electron/**/*"
      ],
      "dmg": {
        "contents": [
          {
            "x": 410,
            "y": 150,
            "type": "link",
            "path": "/Applications"
          },
          {
            "x": 130,
            "y": 150,
            "type": "file"
          }
        ]
      },
      "mac": {
        "icon": "build/icons/icon.icns"
      },
      "win": {
        "icon": "build/icons/icon.ico"
      },
      "nsis": {
        "shortcutName": "ExcelUtil",
        "oneClick": false,
        "allowToChangeInstallationDirectory": true,
        "include": "./installer.nsh"
      },
      "asar": false,
      "linux": {
        "icon": "build/icons",
        "target": "deb"
      }
    }
    
  • 以上是electron-builder中关于打包的所有配置,下面拆分一下
    • Windows打包配置,指定图标路径和nsis配置Windows应用安装和卸载的行为
      "win": {
        "icon": "build/icons/icon.ico"
      },
      "nsis": {
        "shortcutName": "ExcelUtil",
        "oneClick": false,
        "allowToChangeInstallationDirectory": true,
        "include": "./installer.nsh"
      }
      
    • Linux打包配置,指定图标和打包目标格式等
      "linux": {
        "icon": "build/icons",
        "target": "deb"
      }
      
  • 打包使用的依赖包的镜像问题
    • 打包会依赖很多外部的安装包,默认都是通过github或者npm官方仓库,会有下载缓慢或者下载失败的问题
    • 需要增加.yarnrc配置文件在根目录下,配置下载涉及到依赖的的仓库地址
      registry "https://registry.npm.taobao.org"
      sass_binary_site "https://npm.taobao.org/mirrors/node-sass/"
      phantomjs_cdnurl "http://cnpmjs.org/downloads"
      electron_mirror "https://npm.taobao.org/mirrors/electron/"
      sqlite3_binary_host_mirror "https://foxgis.oss-cn-shanghai.aliyuncs.com/"
      profiler_binary_host_mirror "https://npm.taobao.org/mirrors/node-inspector/"
      chromedriver_cdnurl "https://cdn.npm.taobao.org/dist/chromedriver"
      

开发中遇到的问题

主进程和渲染进程

  • electron本质是一个nodejs runtime和一个开源的chromium浏览器,开发时分为主进程和渲染进程
  • 主进程的代码在src/main目录下
  • 渲染进程的代码在src/renderer目录下
  • 同时会通过webpack分别对mainrenderer进行打包构建
  • 进程间通信可以使用ipcMainipcRenderer中的事件进行通信,以达到渲染进程可以调用主进程中的原生资源

主进程窗口配置

  • 由于本次开发涉及的主进程开发相对简单,只涉及到一个src/main/index.js,那就直接拿文件中BrowserWindow配置代码说事儿吧

    new BrowserWindow({
      height: 563,
      useContentSize: true,
      width: 1000,
      fullscreen: false, // 是否允许全屏
      fullscreenable: false,
      titleBarStyle: 'hidden',
      maximizable: false,
      webPreferences: {
        nodeIntegration: true, // 是否引入node api 这样渲染进程就可以i使用node api了
        nodeIntegrationInWorker: true,
        enableRemoteModule: true // 是否引入remote模块 这样渲染进程就可以使用remote模块api了
      }
    })
    
  • 当然其中还涉及到引用本地数据库lowdb,这里是参考掘友的文章

  • 本应用还涉及到了单窗口实现,使用的是requestSingleInstanceLock api来获取是否有窗口正在运行

    const gotTheLock = app.requestSingleInstanceLock()
    // 获取单体实例锁,设置应用单开
    if (!gotTheLock) {
      app.quit()
    } else {
      app.on('second-instance', (event, commandLine, workingDirectory) => {
        // 当运行第二个实例时,将会聚焦到mainWindow这个窗口
        if (mainWindow) {
          if (mainWindow.isMinimized()) mainWindow.restore()
          mainWindow.focus()
        }
      })
      app.on('ready', createWindow)
      app.on('window-all-closed', () => {
        if (process.platform !== 'darwin') {
          app.quit()
        }
      })
      app.on('activate', () => {
        if (mainWindow === null) {
          createWindow()
        }
      })
    }
    

调用python应用程序时遇到的问题

  • 首先需要通过pyinstaller分别打包出各操作系统环境中使用的cli应用程序
  • 将打包后的cli工具未压缩的包放入static/cli目录下
  • 调用时即可通过webpack吐出的全局__static变量获取对应的路径供node子进程调用
  • 这里需要对打包配置进行修改才能达到打包后仍然可以正确调用cli
    • package.jsonbuild字段中增加"asar": false,来保证打包后的应用不被压缩,node仍可以正常访问cli资源

    • 对于Windows来说,通过配置nsis的默认安装路径来保证路径可用,因为默认安装到C:\\Program Files下,有空格的路径会造成调用不通,故需要增加一下配置

      "nsis": {
        "some": "other config",
        "include": "./installer.nsh"
      }
      

      package.json中增加一个nsis安装脚本installer.nsh,让应用默认安装到C:\excel-utils目录下

      !macro preInit
          SetRegView 64
          WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\excel-utils"
          WriteRegExpandStr HkCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\excel-utils"
          SetRegView 32
          WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\excel-utils"
          WriteRegExpandStr HkCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\excel-utils"
      !macroend
      
    • 对于Deepin系统来说,由于在webpack打包复制static/cli目录时会丢失cli程序的可执行权限,导致打包后命令不能执行,故增加一个增加权限的脚本和命令 package.json

      "scripts": {
        "build:linux": "node .electron-vue/build.js && .electron-vue/changeMainMod.sh && electron-builder",
      }
      

      .electron-vue中增加changeMainMod.sh,并使其在业务代码打包完成后调用

      #!/bin/bash
      for main in `ls ./dist/electron/static/cli/*/main`
      do
        echo "正在设置$main为可执行权限"
        chmod +x $main
      done
      
  • webpack.renderer.config白名单的问题
    • 由于使用的vue2.X,UI库使用的则是element-ui,开发时发现使用el-table组件不正确显示,查阅资料后发现需要增加到打包白名单里面,注释写的也很详细,需要指定UI libraries来让编译.vue文件
    /**
     * List of node_modules to include in webpack bundle
     *
     * Required for specific packages like Vue UI libraries
     * that provide pure *.vue files that need compiling
     * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals
     */
    let whiteListedModules = ['vue', 'element-ui']
    

番外

技术选型时有两个选择一个是Electron,一个是NW.js,这里有一个文档来说明二者的异同,主要区别就是:

  • Electron有很多成功案例AtomVScode等等耳熟能详的大众应用,当然NW.js也有钉钉和微信开发者工具这样的巨头应用
  • 总体来说Electron有更多的用户群,更多三方工具包,更活跃的社区,有问题可以很快找到解决方案
  • NW.js保持了对Windows xp系统的支持,而Electron并出于安全性考虑并不支持
  • 再就是对node的集成,Electron集成了libuv在两个进程中,均可以直接使用,避免了对Chromium的hack,而NW.js则需要通过派发的方式使用

搬砖