前端web项目使用electron和electron-builder生成桌面应用

背景

需求推进技术探索,技术实现需求设计。刚开始本来是用 vue 做一个 web 项目,随着开发推进,产品要求做成桌面端应用。。。
想到了用 Electron 框架构建桌面应用程序,查了一下大多都是使用 Electron 一步步构建项目,把已有 web 项目打包成桌面应用文档比较少,不过也找到一篇不错的文档,下面记录一下踩坑过程。

参考文章

Electron 文档
Electron API 文档
利用electron和electron-builder把前端web项目生成桌面程序

实现过程

准备工作

  1. 安装Node.js,使用最新的LTS版本
  2. 打包后的 web 项目
  3. 开发之前最好把 Electron 官方文档看一下,官方文档还是最权威的

配置淘宝镜像

原镜像下载可能太慢或下载失败,可将 npm 和 electron 的镜像地址都切换为淘宝镜像,cmd 命令输入:

npm config edit

将下面的地址复制、粘贴、保存、关闭

registry=http://registry.npm.taobao.org/
electron_mirror=https://npm.taobao.org/mirrors/electron/
electron-builder-binaries_mirror=https://npm.taobao.org/mirrors/electron-builder-binaries/

前端web项目使用electron和electron-builder生成桌面应用_第1张图片

Electron 项目结构

.
├─icons						# icon 图标目录
├─node_modules
├─h5						# vue 资源
├─gulpfile.js  				# gulp 配置文件
├─main.js  					# 应用程序的入口文件
├─package.json
├─README.md

创建应用程序

新建目录并进入

# windows
mkdir my-electron-app && cd my-electron-app

复制的官方文档,运行居然报错,PowerShell 提示:标记 “&&” 不是此版本中的有效语句分隔符
前端web项目使用electron和electron-builder生成桌面应用_第2张图片
将命令行语句中的 && 改为分号 ; 就好了

# windows
mkdir my-electron-app; cd my-electron-app

创建 package.json 文件

{
  "name": "my-electron-app",
  "version": "1.0.0",
  "author": "freeman",
  "description": "electron app",
  "private": false,
  "scripts": {
    "view": "cross-env ELECTRON_ENV=dev electron .",
    "start": "cross-env ELECTRON_ENV=dev .\\node_modules\\.bin\\gulp watch:electron",
    "build": "cross-env ELECTRON_ENV=dep electron-builder"
  },
  "main": "main.js",
  "build": {
    "productName": "my electron app",
    "appId": "com.wss.app",
    "directories": {
      "output": "builder"
    },
    "nsis": {
      "oneClick": false,
      "allowElevation": true,
      "allowToChangeInstallationDirectory": true,
      "installerIcon": "./h5/favicon.ico",
      "uninstallerIcon": "./h5/favicon.ico",
      "installerHeaderIcon": "./h5/favicon.ico",
      "createDesktopShortcut": true,
      "createStartMenuShortcut": true
    },
    "win": {
      "target": [
        "nsis",
        "zip"
      ]
    },
    "files": [
      "h5/**/*",
      "main.js"
    ]
  },
  "devDependencies": {
    "cross-env": "^7.0.3",
    "electron": "^16.0.7",
    "electron-builder": "^22.14.5",
    "electron-connect": "^0.6.3",
    "gulp": "^4.0.2"
  }
}

package.json 代码解析

  • 启动命令设置
    这里我安装了cross-env,运行开发环境和打包时方便指定环境变量,同时引入了gulp配置热更新,编辑代码自动刷新应用
    热更新参考
"scripts": {
  "view": "cross-env ELECTRON_ENV=dev electron .",
  "start": "cross-env ELECTRON_ENV=dev .\\node_modules\\.bin\\gulp watch:electron",
  "build": "cross-env ELECTRON_ENV=dep electron-builder"
},
  • 设置入口文件为main.js

  • 打包相关配置

"build": {
  "productName": "my electron app",  // 项目名 这也是生成的exe文件的前缀名
  "appId": "com.wss.app",  // 包名  
  "directories": {  // 输出文件夹
    "output": "builder"
  }, 
  "nsis": {
    "oneClick": false,  // 是否一键安装
    "allowElevation": true,  // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。
    "allowToChangeInstallationDirectory": true,  // 允许修改安装目录
    "installerIcon": "./h5/favicon.ico",  // 安装图标
    "uninstallerIcon": "./h5/favicon.ico",  // 卸载图标
    "installerHeaderIcon": "./h5/favicon.ico",  // 安装时头部图标
    "createDesktopShortcut": true,  // 创建桌面图标
    "createStartMenuShortcut": true  // 创建开始菜单图标
  },
  "win": {
    "target": [
      "nsis",
      "zip"
    ]
  },
  "files": [
    "h5/**/*",  // 要打包的目录
    "main.js"
  ]
}

关于Electron项目出现白屏,控制台输出 Failed to load resource: net::ERR_FILE_NOT_FOUND 问题

  • npm run start运行时出现:
    执行npm run start,窗口出现白屏,控制台输出找不到资源,可能是 vue 项目的publicPath路径配置不对(vue 项目通过 npm run build 打包以后页面没有内容)
  • npm run build打包之后出现白屏:
    files 里面要打包的目录可能指定错了,没有把我们要打包的目录指定对,electron-build 就不会将build文件夹打包进去 app.asar 文件里,这样安装完.exe文件之后,控制台可能输出Not allowed to load local resource:,桌面程序打开白屏。

参考:electron-builder 打包后 出现Not allowed to load local resource:

  • 安装 npm 包
    我是安装的最新版Node.jsnpm依赖我都是安装在devDependencies里,如果刚开始安装在dependencies,打包的时候也会提示放到devDependencies
"devDependencies": {
  "cross-env": "^7.0.3",
  "electron": "^16.0.7",
  "electron-builder": "^22.14.5",
  "electron-connect": "^0.6.3",
  "gulp": "^4.0.2"
}

创建 gulpfile.js

因为想要实现热更新,安装了gulp,还需要创建gulpfile.js配置文件,这里只监听了main.js

const gulp = require('gulp');
const electron = require('electron-connect').server.create();
 
gulp.task('watch:electron', function () {
    electron.start();
    gulp.watch(['./main.js'], electron.restart);
});

引入 web 项目

vue 项目路由模式使用hash,将打包后的 vue 资源放到 electron 项目根目录

创建 main.js 文件

const {
  electron, 
  app, 
  BrowserWindow, 
  Menu, 
  screen
} = require('electron')
const path = require('path')

// app.isPackaged 如果应用已打包,则返回的属性,此属性可用于区分开发和生产环境
const isDevelopment = !app.isPackaged;
if(isDevelopment) {
  try {
    const client = require('electron-connect').client;
  } catch (err) {
  }
}

// 屏蔽安全告警在console控制台的显示
process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'

if (process.mas) app.setName('My Electron')

let mainWindow

function createWindow() {
  const displayWorkAreaSize = screen.getAllDisplays()[0].workArea
  const windowOptions = {
    x: displayWorkAreaSize.x,  // 窗口相对于屏幕的左偏移位置.默认居中
    y: displayWorkAreaSize.y,  // 窗口相对于屏幕的顶部偏移位置.默认居中
    width: displayWorkAreaSize.width,
    height: displayWorkAreaSize.height,
    minWidth: 1220,
    minHeight: 780,
    show: false,
    // movable: false,  // 窗口是否可以拖动
    // maximizable: false,  // 窗口是否可以最大化
    // titleBarStyle: 'hidden',
    frame: true,  // 指定 false 来创建一个 Frameless Window
    title: app.getName(),
    icon: path.join(__dirname, '/icons/icon.png'),  // 托盘图标
    hasShadow: false,  // 窗口是否有阴影
    autoHideMenuBar: true,  // 自动隐藏菜单栏
    webPreferences: {
      devTools: process.env.ELECTRON_ENV == "dev",// 是否开启 DevTools
      webSecurity: true,  // //允许跨域
      plugins: true,  // 是否开启插件支持
      experimentalFeatures: true,  // 开启 Chromium 的 可测试 特性
      experimentalCanvasFeatures: true,  // 开启 Chromium 的 canvas 可测试特性
      minimumFontSize: 10,
    },
  }

  if (process.platform === 'linux') {
    windowOptions.icon = path.join(__dirname, '/h5/favicon.ico')
  }

  mainWindow = new BrowserWindow(windowOptions)
  mainWindow.loadURL(path.join('file://', __dirname, '/h5/index.html'))
  // 设置窗口默认最大化
  mainWindow.maximize()
  // 设置用户是否可以调节窗口尺寸
  // mainWindow.setResizable(false)
  mainWindow.show()

  mainWindow.on('closed', function () {
    mainWindow = null
  })

  process.env.ELECTRON_ENV == "dev" && client.create(mainWindow);
}

app.on('ready', function () {
  const menu = Menu.buildFromTemplate(template)
  Menu.setApplicationMenu(menu) // 设置菜单部分
  createWindow()
})

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})

app.on('activate', function () {
  if (mainWindow === null) createWindow()
})

app.on('browser-window-created', function () {
  let reopenMenuItem = findReopenMenuItem()
  if (reopenMenuItem) reopenMenuItem.enabled = false
})

app.on('window-all-closed', function () {
  let reopenMenuItem = findReopenMenuItem()
  if (reopenMenuItem) reopenMenuItem.enabled = true
  app.quit()
})

/**
 * 注册键盘快捷键
 */
let template = [
  {
    label: '操作',
    submenu: [
      {
        label: '回到系统入口',
        accelerator: 'CmdOrCtrl+Q',
        click: function (item, focusedWindow) {
          if (focusedWindow.id === 1) {
            BrowserWindow.getAllWindows().forEach(function (win) {
              if (win.id > 1) {
                win.close()
              }
            })
          }
          focusedWindow.loadURL(path.join('file://', __dirname, '/h5/index.html'))
        }
      }, {
        label: '复制',
        accelerator: 'CmdOrCtrl+C',
        role: 'copy'
      }, {
        label: '粘贴',
        accelerator: 'CmdOrCtrl+V',
        role: 'paste'
      }, {
        label: '重新加载',
        accelerator: 'CmdOrCtrl+R',
        click: function (item, focusedWindow) {
          if (focusedWindow) {
            // on reload, start fresh and close any old
            // open secondary windows
            if (focusedWindow.id === 1) {
              BrowserWindow.getAllWindows().forEach(function (win) {
                if (win.id > 1) {
                  win.close()
                }
              })
            }
            focusedWindow.reload()
          }
        }
      }
    ]
  },
  {
    label: '窗口',
    role: 'window',
    submenu: [
      {
        label: '切换全屏',
        accelerator: 'F11',
        role: 'togglefullscreen',
        // click: function (item, focusedWindow) { // 自定义 click 和使用 role 效果是一样的
        //   if (focusedWindow) {
        //     focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
        //   }
        // }
      }, {
        label: '最小化',
        accelerator: 'CmdOrCtrl+M',
        role: 'minimize'
      }, {
        label: '关闭',
        accelerator: 'CmdOrCtrl+W',
        role: 'close'
      }
    ]
  },
  {
    label: '帮助',
    role: 'help',
    submenu: [{
      label: '意见反馈',
      click: function () {
        electron.shell.openExternal('https://www.electronjs.org/zh/docs/latest/api/app')
      }
    }]
  }
]

function findReopenMenuItem() {
  const menu = Menu.getApplicationMenu()
  if (!menu) return

  let reopenMenuItem
  menu.items.forEach(function (item) {
    if (item.submenu) {
      item.submenu.items.forEach(function (item) {
        if (item.key === 'reopenMenuItem') {
          reopenMenuItem = item
        }
      })
    }
  })
  return reopenMenuItem
}

// 设置是否显示 DevTools
if(process.env.ELECTRON_ENV == "dev") {
  template[1].submenu.push({
    label: '切换开发者工具',
    role: 'toggleDevTools'
  })
}

// 针对Mac端的一些配置
if (process.platform === 'darwin') {
  const name = electron.app.getName()
  template.unshift({
    label: name,
    submenu: [{
      label: '退出',
      accelerator: 'Command+Q',
      click: function () {
        app.quit()
      }
    }]
  })

  // Window menu.
  template[3].submenu.push({
    type: 'separator'
  }, {
    label: 'Bring All to Front',
    role: 'front'
  })

  // addUpdateMenuItems(template[0].submenu, 1)
}

// 针对Windows端的一些配置
if (process.platform === 'win32') {
  const helpMenu = template[template.length - 1].submenu
  // addUpdateMenuItems(helpMenu, 0)
}

main.js 代码解析

main.js配置项可对照 Electron API 文档设置。本项目是把已有的 vue 项目打包,不是完全开发 Electron 应用,窗口菜单选项只能配置,不方便完全自定义。

  • 运行时启动热更新
// main.js
process.env.ELECTRON_ENV == "dev" && client.create(mainWindow);
  • 配置程序托盘图标
    不配置图标将使用系统默认的
function createWindow() {
  const windowOptions = {
    // ...
    icon: path.join(__dirname, '/icons/icon.png'),  // 托盘图标
    // ...
  }
}

配置图标这里需要注意,根据官方文档 nativeImage说明,eletron 可以设置大小为 32x32、64x64 。。。等一些尺寸的图标,但是同样指定一个 6464 的png,通过npm run build打包发现报错 (必需为256256),同时还发现只添加一张照片也不行,会报错,于是指定了三张不同尺寸的 icon 图片。
build 报错截图:前端web项目使用electron和electron-builder生成桌面应用_第3张图片
icons 目录截图:
前端web项目使用electron和electron-builder生成桌面应用_第4张图片

  • 指定index.html位置
mainWindow.loadURL(path.join('file://', __dirname, '/h5/index.html'))

启动和打包应用程序

预览

npm run start

打包,打包输入目录在package.json中配置

npm run build

前端web项目使用electron和electron-builder生成桌面应用_第5张图片

记录

打包之后,报错 A JavaScript error occurred in the main process
前端web项目使用electron和electron-builder生成桌面应用_第6张图片
查了好久没有发现相关的问题,那就一定是项目中的配置存在问题,

通过图片发现是打包之后electron-connect报错,报错原因是因为electron-connect在开发环境安装的包,打包之后是不会安装的,生产引入未安装的包会报这种错,所以引入时要先判断下环境。

// main.js

// app.isPackaged 如果应用已打包,则返回的属性,此属性可用于区分开发和生产环境
const isDevelopment = !app.isPackaged;
if(isDevelopment) {
  try {
    const client = require('electron-connect').client;
  } catch (err) {
  }
}

你可能感兴趣的:(vue,electron,gulp)