Bootstrap

vue-cli项目中使用Electron

一、安装

安装electron,electron-builder,vue-cli-plugin-electron-builder,electron-devtools

npm i --save-dev electron electron-builder vue-cli-plugin-electron-builder electron-devtools

二、创建background.js

vue-cli项目src下创建background.js文件

// Modules to control application life and create native browser window
const {app, BrowserWindow, screen, ipcMain, dialog, Tray, protocol} = require('electron')
import {createProtocol} from 'vue-cli-plugin-electron-builder/lib'

const fs = require('fs');
const path = require('path')
const electron = require('electron');
/*获取electron窗体的菜单栏*/
const Menu = electron.Menu;
/*隐藏electron创听的菜单栏*/
Menu.setApplicationMenu(null);
let mainWindow;
let appTray = null;
let preFix = './';
if (process.env.NODE_ENV === 'development') {
  preFix = "./bundled/";
}

// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([{
  scheme: 'app',
  privileges: {
    secure: true,
    standard: true,
    supportFetchAPI: true
  }
}])

// 隐藏主窗口,并创建托盘,绑定关闭事件
function setTray(mainWindow) {
  // 用一个 Tray 来表示一个图标,这个图标处于正在运行的系统的通知区
  // 通常被添加到一个 context menu 上.
  // 系统托盘右键菜单
  const trayMenuTemplate = [
    {
      type: 'checkbox',
      label: '开机启动',
      checked: app.getLoginItemSettings().openAtLogin,
      click: function () {
        if (!app.isPackaged) {
          app.setLoginItemSettings({
            openAtLogin: !app.getLoginItemSettings().openAtLogin,
            path: process.execPath
          })
        } else {
          app.setLoginItemSettings({
            openAtLogin: !app.getLoginItemSettings().openAtLogin
          })
        }
      }
    },
    {
      // 系统托盘图标目录
      label: '退出',
      click: () => {
        app.quit();
      }
    }
  ];
  // 设置系统托盘图标
  let filePath = preFix + 'favicon.ico';
  const iconPath = path.join(__dirname, filePath);
  appTray = new Tray(iconPath);
  const win = BrowserWindow.getFocusedWindow()
  // 图标的上下文菜单
  const contextMenu = Menu.buildFromTemplate(trayMenuTemplate);

  // 展示主窗口,隐藏主窗口 mainWindow.hide()
  mainWindow.show();

  // 设置托盘悬浮提示
  appTray.setToolTip('Saturn');

  // 设置托盘菜单
  appTray.setContextMenu(contextMenu);

  // 单击托盘小图标显示应用
  // appTray.on('click', () => {
  //     //显示主程序
  //     mainWindow.show();
  //     //关闭托盘显示
  //     // appTray.destroy();
  // });
  appTray.on('double-click', () => {
    win.isVisible() ? win.hide() : win.show()
  })
  return appTray;
}

function createWindow(scheme, handler) {
  // Create the browser window.
  const displayWorkAreaSize = screen.getAllDisplays()[0].workArea
  const width = displayWorkAreaSize.width;
  const height = displayWorkAreaSize.height;
  mainWindow = new BrowserWindow({
    width: width / 2 + 100,
    height: height - 10,
    x: width - (width / 2 + 100) - 5,
    y: 5,
    // width: 1920,
    // height: 1350,
    transparent: true,
    resizable: false,
    movable: true,
    frame: false,
    alwaysOnTop: false,
    backgroundColor: '#00000000',
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      //关闭web权限检查,允许跨域
      webSecurity: false,
      nodeIntegration: true,
      contextIsolation: false,
      enableRemoteModule: true,
    }
  })

  // and load the index.html of the app.
  // mainWindow.loadFile('index_bak.html')
  let filePath = preFix + 'index.html';
  mainWindow.loadFile(path.join(__dirname, filePath))

  // Open the DevTools.
  // mainWindow.webContents.openDevTools()
  setTray(mainWindow);

  mainWindow.setSkipTaskbar(true) // 使窗口不显示在任务栏中

  //程序崩溃后
  mainWindow.webContents.on('crashed', () => {
    const options = {
      type: 'error',
      title: '进程崩溃了',
      message: '这个进程已经崩溃.',
      buttons: ['重载', '退出'],
    };
    recordCrash().then(() => {
      dialog.showMessageBox(options, (index) => {
        if (index === 0) reloadWindow(mainWindow);
        else app.quit();
      });
    }).catch((e) => {
      console.log('err', e);
    });
  })

  function recordCrash() {
    return new Promise(resolve => {
      // 崩溃日志请求成功....
      resolve();
    })
  }

  function reloadWindow(mainWin) {
    if (mainWin.isDestroyed()) {
      app.relaunch();
      app.exit(0);
    } else {
      BrowserWindow.getAllWindows().forEach((w) => {
        if (w.id !== mainWin.id) w.destroy();
      });
      mainWin.reload();
    }
  }

}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
  createProtocol('app')
  createWindow()

  app.on('activate', function () {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})

/**
 * 开机自动启动
 * */
const exeName = path.basename(process.execPath)
app.setLoginItemSettings({
  openAtLogin: true,
  openAsHidden: false,
  path: process.execPath,
  args: [
    '--processStart', `"${exeName}"`,
  ]
})

/**
 * 窗口控制
 * */
// 1. 窗口 最小化
ipcMain.on('window-min', function () { // 收到渲染进程的窗口最小化操作的通知,并调用窗口最小化函数,执行该操作
  mainWindow.minimize();
})

// 2. 窗口 最大化、恢复
ipcMain.on('window-max', function () {
  if (mainWindow.isMaximized()) { // 为true表示窗口已最大化
    mainWindow.restore();// 将窗口恢复为之前的状态.
  } else {
    mainWindow.maximize();
  }
})

// 3. 关闭窗口
ipcMain.on('window-close', function () {
  // mainWindow.close();
  mainWindow.setSkipTaskbar(true) // 使窗口不显示在任务栏中
  mainWindow.hide() // 隐藏窗口
})

/**
 * 读写文件
 * */
ipcMain.on('read-message', function (event, arg) {
  // arg是从渲染进程返回来的数据
  // 这里是传给渲染进程的数据
  fs.readFile(path.join(__dirname, "./loginInfo.json"), "utf8", (err, data) => {
    if (err) {
      event.sender.send('read-reply', "读取失败");
    } else {
      event.sender.send('read-reply', data);
    }
  })
});


ipcMain.on('write-message', function (event, arg) {
  // arg是从渲染进程返回来的数据
  fs.writeFile(path.join(__dirname, "./loginInfo.json"), JSON.stringify(arg), "utf8", (err) => {
      if (err) {
        event.sender.send('write-reply', "写入失败");
      } else {
        event.sender.send('write-reply', "写入成功");
      }
    }
  )
  // 通过event.sender.send给渲染进程传递数据
});

三、创建preload.js文件

在vue-cli 项目src下 创建 preload.js

import { contextBridge, ipcRenderer } from 'electron'
window.ipcRenderer = ipcRenderer

contextBridge.exposeInMainWorld('ipcRenderer', {
  // 异步向主进程 发送消息
  send: (channel, data) => {
    let validChannels = ['toMain', 'downLoad', 'judgeUse', 'hideMenu', 'showMenu', 'window-close', 'asyncGetLocalServer']
    if (validChannels.includes(channel)) {
      ipcRenderer.send(channel, data)
    }
  },
  // 同步向主进程 发送消息,
  sendSync: (channel, data) => {
    let validChannels = ['syncGetLocalServer']
    if (validChannels.includes(channel)) {
      return ipcRenderer.sendSync(channel, data)
    }
  },
  // 异步接收主进程返回的数据
  receive: async (channel) => {
    let validChannels = ['authorizeBack', 'asyncBackLocalServer']
    if (validChannels.includes(channel)) {
      return new Promise((resolve) => {
        ipcRenderer.on(channel, (event, ...args) => {
          resolve(...args)
        })
      });

    }
  }
})

四、修改package.json文件

{
  "name": "ruoyi",
  "version": "3.1.0",
  "description": "若依管理系统",
  "author": "若依",
  "license": "MIT",
  "scripts": {
    "dev": "vue-cli-service serve",
    "build:prod": "vue-cli-service build",
    "build:stage": "vue-cli-service build --mode staging",
    "preview": "node build/index.js --preview",
    "lint": "eslint --ext .js,.vue src",
    "electron:build": "vue-cli-service electron:build",
    "electron:serve": "vue-cli-service electron:serve",
    "postinstall": "electron-builder install-app-deps",
    "postuninstall": "electron-builder install-app-deps"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "src/**/*.{js,vue}": [
      "eslint --fix",
      "git add"
    ]
  },
  "keywords": [
    "vue",
    "admin",
    "dashboard",
    "element-ui",
    "boilerplate",
    "admin-template",
    "management-system"
  ],
  "repository": {
    "type": "git",
    "url": "https://gitee.com/y_project/RuoYi-Cloud.git"
  },
  "dependencies": {
    "@riophae/vue-treeselect": "0.4.0",
    "axios": "0.21.0",
    "clipboard": "2.0.6",
    "core-js": "3.8.1",
    "echarts": "4.9.0",
    "el-tree-transfer": "^2.4.7",
    "element-ui": "2.15.5",
    "file-saver": "2.0.4",
    "fuse.js": "6.4.3",
    "highlight.js": "9.18.5",
    "js-beautify": "1.13.0",
    "js-cookie": "2.2.1",
    "jsencrypt": "3.0.0-rc.1",
    "nprogress": "0.2.0",
    "quill": "1.3.7",
    "screenfull": "5.0.2",
    "sortablejs": "1.10.2",
    "vue": "2.6.12",
    "vue-count-to": "1.0.13",
    "vue-cropper": "0.5.5",
    "vue-fullscreen": "^2.6.1",
    "vue-meta": "^2.4.0",
    "vue-router": "3.4.9",
    "vue-wxlogin": "^1.0.4",
    "vuedraggable": "2.24.3",
    "vuex": "3.6.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "4.4.6",
    "@vue/cli-plugin-eslint": "4.4.6",
    "@vue/cli-service": "4.4.6",
    "babel-eslint": "10.1.0",
    "chalk": "4.1.0",
    "connect": "3.6.6",
    "dhtmlx-gantt": "^7.1.7",
    "electron": "^16.0.5",
    "electron-builder": "^22.14.5",
    "electron-devtools": "^0.0.3",
    "eslint": "7.15.0",
    "eslint-plugin-vue": "7.2.0",
    "lint-staged": "10.5.3",
    "runjs": "4.4.2",
    "sass": "1.32.0",
    "sass-loader": "10.1.0",
    "script-ext-html-webpack-plugin": "2.1.5",
    "svg-sprite-loader": "5.1.1",
    "vue-cli-plugin-electron-builder": "^2.1.1",
    "vue-template-compiler": "2.6.12"
  },
  "engines": {
    "node": ">=8.9",
    "npm": ">= 3.0.0"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions"
  ],
  "__npminstall_done": false,
  "main": "background.js"
}

五、修改vue.config.js文件

'use strict'
const path = require('path')

const defaultSettings = require('./src/settings.js')

function resolve(dir) {
  return path.join(__dirname, dir)
}

const name = defaultSettings.title //process.env.VUE_APP_TITLE || '若依管理系统' // 网页标题
const BASE_URL = process.env.NODE_EVN === 'production' ? './' : './'
const port = process.env.port || process.env.npm_config_port || 80 // 端口
// vue.config.js 配置说明
//官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions
// 这里只列一部分,具体配置参考文档
module.exports = {
  // 部署生产环境和开发环境下的URL。
  // 默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上
  // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
  publicPath: BASE_URL,
  // publicPath: process.env.NODE_ENV === "production" ? "/" : "/",
  // 在npm run build 或 yarn build 时 ,生成文件的目录名称(要和baseUrl的生产环境路径一致)(默认dist)
  outputDir: 'dist',
  // 用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下)
  assetsDir: 'static',
  // 是否开启eslint保存检测,有效值:ture | false | 'error'
  lintOnSave: process.env.NODE_ENV === 'development',
  // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
  productionSourceMap: false,
  // webpack-dev-server 相关配置
  devServer: {
    host: '0.0.0.0',
    port: port,
    open: true,
    proxy: {
      // detail: https://cli.vuejs.org/config/#devserver-proxy
      [process.env.VUE_APP_BASE_API]: {
        // target: `http://localhost:8080`,
        target: process.env.VUE_APP_SERVER,
        changeOrigin: true,
        /*pathRewrite: {
          ['^' + process.env.VUE_APP_BASE_API]: '/' + process.env.VUE_APP_BASE_API
        },*/
        pathRewrite: {
          ['^' + process.env.VUE_APP_BASE_API]: ''
        },
        ws: true,
      },
      '^/static': {
        target: `http://localhost:80`,
        changeOrigin: true,
        ws: true,
      }
    },
    disableHostCheck: true
  },
  configureWebpack: {
    name: name,
    resolve: {
      alias: {
        '@': resolve('src')
      }
    }
  },
  pluginOptions: {
    electronBuilder: {
      // nodeIntegration: true,
      removeElectronJunk: false,
      preload: './src/preload.js',
      builderOptions: {
        "appId": "com.electron.Saturn",
        "productName": "Saturn",//项目名,也是生成的安装文件名,即aDemo.exe
        "copyright": "Copyright © 2021",//版权信息
        "directories": {
          "output": "./build",      //打包后文件所在位置
          // "app": "./dist_electron"    //开始位置
        },
        "win": {//win相关配置
          "icon": "./src/assets/icons/favicon.ico",//图标,当前图标在根目录下,注意这里有两个坑
          "target": [
            {
              "target": "nsis",//利用nsis制作安装程序
              "arch": [
                "x64",//64位
              ]
            }
          ]
        },
        "asar": false,
        "nsis": {
          "oneClick": false, // 是否一键安装
          "allowElevation": true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。
          "allowToChangeInstallationDirectory": true, // 允许修改安装目录
          "installerIcon": "./src/assets/icons/favicon.ico",// 安装图标
          "uninstallerIcon": "./src/assets/icons/favicon.ico",//卸载图标
          "installerHeaderIcon": "./src/assets/icons/favicon.ico", // 安装时头部图标
          "createDesktopShortcut": true, // 创建桌面图标
          "createStartMenuShortcut": true,// 创建开始菜单图标
          "shortcutName": "Saturn", // 图标名称
        },
      }
    },
  },
  chainWebpack(config) {
    config.plugins.delete('preload') // TODO: need test
    config.plugins.delete('prefetch') // TODO: need test

    // set svg-sprite-loader
    config.module
      .rule('svg')
      .exclude.add(resolve('src/assets/icons'))
      .end()
    config.module
      .rule('icons')
      .test(/\.svg$/)
      .include.add(resolve('src/assets/icons'))
      .end()
      .use('svg-sprite-loader')
      .loader('svg-sprite-loader')
      .options({
        symbolId: 'icon-[name]'
      })
      .end()
    config
      .when(process.env.NODE_ENV !== 'development',
        config => {
          config
            .plugin('ScriptExtHtmlWebpackPlugin')
            .after('html')
            .use('script-ext-html-webpack-plugin', [{
              // `runtime` must same as runtimeChunk name. default is `runtime`
              inline: /runtime\..*\.js$/
            }])
            .end()
          config
            .optimization.splitChunks({
            chunks: 'all',
            cacheGroups: {
              libs: {
                name: 'chunk-libs',
                test: /[\\/]node_modules[\\/]/,
                priority: 10,
                chunks: 'initial' // only package third parties that are initially dependent
              },
              elementUI: {
                name: 'chunk-elementUI', // split elementUI into a single package
                priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
                test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
              },
              commons: {
                name: 'chunk-commons',
                test: resolve('src/components'), // can customize your rules
                minChunks: 3, //  minimum common number
                priority: 5,
                reuseExistingChunk: true
              }
            }
          })
          config.optimization.runtimeChunk('single'),
            {
              from: path.resolve(__dirname, './public/robots.txt'), //防爬虫文件
              to: './', //到根目录下
            }
        }
      )
  }
}

六、启动

1 本地启动

npm run electron:serve

2 打包

npm run electron:build

打包成功后在build文件夹会生成exe安装文件
在这里插入图片描述

补充:

  1. 启动发现资源请求不到,先执行npm run electron:build(会生成dist_electron文件夹及相关文件),再执行npm run electron:serve,然后将dist_electron/bundled/static 复制一份static放到dist_electron文件夹下
    (不要问我为什么,我也不知道……时间紧,任务重,有时间再去研究-_-||)
  2. 接口协议是File的参考上一篇解决 vue项目通过Electron生成桌面应用
;