vue项目打包成exe桌面程序

总结

主要执行以下命令

全局安装electron:npm install electron -g

安装electron-builder依赖:vue add electron-builder

运行桌面程序:npm run electron:serve

打包应用程序:npm run electron:build

更新应用程序:npm i electron-updater --save

electron-builder 教程

一、本人比较笨,拷贝原始项目后,安转依赖并启动

执行命令:npm install

执行命令:npm run serve

二、全局安装electron:

执行命令:npm install electron -g

三、安装electron-builder依赖:

执行命令:vue add electron-builder

electron-builder安装成功后项目目录如下:

electron-builder安装成功后项目目录

会发现目录里多了一个background.js 的文件,并且package.json 里多了依赖和执行命令其中这个“main”: “background.js” 是electron的入口文件

四、运行桌面程序

执行命令:npm run electron:serve

执行以上命令后项目启动速度较慢,可以注释掉background.js文件中await installExtension(VUEJS_DEVTOOLS) 这一句代码

注释掉background.js文件中内容

项目运行后显示界面如下:

运行后的桌面程序


五、打包

执行命令:npm run electron:build

执行打包命令后报错:"directories" in the root is deprecated, please specify in the "build",如下图显示:

打包报错

解决办法,删掉“directories”这部分内容:

删除红线圈住内容

打包成功后exe程序所在位置:

根目录/dist_electron/win-unpacked/vue-antd-pro.exe ,如下图所示:

打包成功后exe程序所在位置

六、在vue.config.js中module.exports中添加pluginOptions配置:更换exe应用程序中的名称和图标

打包教程地址:https://blog.csdn.net/Assassin_EZI0/article/details/107144377

pluginOptions配置

执行打包命令:npm run electron:build重新打包后

七、更新应用程序 参考网址

客户安装应用以后,如果有新功能更新,那客户那边如何更新?解决方案,安装:electron-updater

执行命令:npm i electron-updater --save

介绍一下原理:使用electron-updater, 打包后会生成一个latest.yml 的文件,里面包含了版本号等描述信息,

并且electron-updater 提供了一系列监听事件,允许应用向服务器检测当前版本是否可以更新,

如果可以更新,在对应的监听事件里做了相关处理(下载最新版本,下载完成后退出并重新安装)

 在目录下新增一个目录_main, 里面有两个文件,events.js(定义了事件名称) 和 updater.js (处理接收)和 helper.js(是封装的持久化数据相关操作方法)。

helper.js

import { join } from 'path'

import fs from 'fs'

import { app } from 'electron'

const dataPath = join(app.getPath('userData'), 'data.json')

export function getLocalData(key) {

  if (!fs.existsSync(dataPath)) {

    fs.writeFileSync(dataPath, JSON.stringify({}), { encoding: 'utf-8' })

  }

  let data = fs.readFileSync(dataPath, { encoding: 'utf-8' })

  let json = JSON.parse(data)

  return key ? json[key] : json

}

export function setLocalData(key, value) {

  let args = [...arguments]

  let data = fs.readFileSync(dataPath, { encoding: 'utf-8' })

  let json = JSON.parse(data)

  if (args.length === 0 || args[0] === null) {

    json = {}

  } else if (args.length === 1 && typeof key === 'object' && key) {

    json = {

      ...json,

      ...args[0],

    }

  } else {

    json[key] = value

  }

  fs.writeFileSync(dataPath, JSON.stringify(json), { encoding: 'utf-8' })

}

export async function sleep(ms) {

  return new Promise((resolve) => {

    const timer = setTimeout(() => {

      resolve()

      clearTimeout(timer)

    }, ms)

  })

}

events.js:

// ipc通信事件, main 和 render都会用

export default {

downLoadUpdate: 'downLoadUpdate', // 手动下载更新

checkUpdate: 'checkUpdate', // 请求检查更新

startCheckUpdate: 'startCheckUpdate', // 开始检查更新

checkUpdateError: 'checkUpdateError', // 检查更新出错

checkingUpdate: 'checkingUpdate', // 正在检查更新

updateAvailable: 'updateAvailable', // 有新版本更新

updateNotAvailable: 'updateNotAvailable', // 没有新版本更新

updateDownloading: 'updateDownloading', // 正在下载中

updateDownloaded: 'updateDownloaded' // 下载完成

}

updater.js:

import { autoUpdater } from 'electron-updater'

// import { ipcMain } from 'electron'

// import logger from 'electron-log'

import Events from './events'

import { getLocalData, setLocalData, sleep } from './helper' // 是封装的持久化数据相关操作方法。

import { app, dialog } from 'electron'

// // 主进程接收渲染进程(页面)派发过来的 检测更新事件

// ipcMain.on(Events.checkUpdate, () => {

//  // 向服务端查询现在是否有可用的更新。在调用这个方法之前,必须要先调用 setFeedURL

//  autoUpdater.checkForUpdates()

// })

// // 主进程接收渲染进程(页面)派发过来的 下载更新事件

// ipcMain.on(Events.downLoadUpdate, () => {

//  autoUpdater.downloadUpdate()

// })

export async function listenUpdater(mainWindow, feedUrl) {

  autoUpdater.setFeedURL(feedUrl)

  await sleep(5000)

  //每次启动自动更新检查 更新版本 --可以根据自己方式更新、定时或者什么

  autoUpdater.checkForUpdates()

  autoUpdater.autoDownload = false

  // 当更新发生错误的时候触发

  autoUpdater.on('error', function(error) {

    mainWindow.webContents.send(Events.checkUpdateError, JSON.stringify(error))

  })

  // 当开始检查更新的时候触发

  autoUpdater.on('checking-for-update', function() {

    mainWindow.webContents.send(Events.checkingUpdate)

    // const { version } = info

    // askUpdate(version)

  })

  // 当发现一个可用更新的时候触发,更新包下载会自动开始

  autoUpdater.on('update-available', function(info) {

    mainWindow.webContents.send(Events.updateAvailable, info)

    // logger.info('检查到有更新,开始下载新版本')

    // logger.info(info)

    const { version } = info

    askUpdate(version)

  })

  // 当没有可用更新的时候触发

  autoUpdater.on('update-not-available', function(info) {

    mainWindow.webContents.send(Events.updateNotAvailable, info)

  })

  // 更新下载进度事件

  autoUpdater.on('download-progress', function(progressObj) {

    mainWindow.webContents.send(Events.updateDownloading, progressObj)

  })

  // 下载完成

  autoUpdater.on('update-downloaded', function() {

    // mainWindow.webContents.send(Events.updateDownloaded)

    // autoUpdater.quitAndInstall()

    // logger.info('下载完毕!提示安装更新')

    // logger.info(res)

    //dialog 想要使用,必须在BrowserWindow创建之后

    dialog

      .showMessageBox({

        title: '升级提示!',

        message: '已为您下载最新应用,点击确定马上替换为最新版本!',

      })

      .then(() => {

        // logger.info('退出应用,安装开始!')

        //重启应用并在下载后安装更新。 它只应在发出 update-downloaded 后方可被调用。

        mainWindow.webContents.send(Events.updateDownloaded)

        autoUpdater.quitAndInstall()

      })

  })

}

async function askUpdate(version) {

  // logger.info(`最新版本 ${version}`)

  let { updater } = getLocalData()

  let { auto, version: ver, skip } = updater || {}

  // logger.info(

  //  JSON.stringify({

  //    ...updater,

  //    ver: ver,

  //  })

  // )

  if (skip && version === ver) return

  if (auto) {

    // 不再询问 直接下载更新

    autoUpdater.downloadUpdate()

  } else {

    const { response, checkboxChecked } = await dialog.showMessageBox({

      type: 'info',

      buttons: ['关闭', '跳过这个版本', '安装更新'],

      title: '软件更新提醒',

      message: `最新版本是 ${version},您现在的版本是 ${app.getVersion()},现在要下载更新吗?`,

      defaultId: 2,

      cancelId: -1,

      checkboxLabel: '以后自动下载并安装更新',

      checkboxChecked: false,

      textWidth: 300,

    })

    if ([1, 2].includes(response)) {

      let updaterData = {

        version: version,

        skip: response === 1,

        auto: checkboxChecked,

      }

      setLocalData({

        updater: {

          ...updaterData,

        },

      })

      if (response === 2) autoUpdater.downloadUpdate()

      // logger.info(['更新操作', JSON.stringify(updaterData)])

    } else {

      // logger.info(['更新操作', '关闭更新提醒'])

    }

  }

}

在background.js 里执行监听

ackground.js 里执行监听


每次更新时只需把exe文件和 latest.yml 文件放到服务器上就行了!

为了打包时生成latest.yml文件,须要在 build 参数中添加 publish 配置

八:自定义electron窗口导航栏

方法一:自定义electron窗口

1、在background.js文件中找到BrowserWindow中设置frame为false,关闭原生导航栏

background.js文件中找到BrowserWindow中设置frame为false

2、在App.vue中引入窗口组件mainHeader.vue

App.vue中引入窗口组件mainHeader.vue

3、mainHeader.vue代码如下

<template>

  <div id="app">

    <div class="titleBar">

      <div class="title">

        <div class="logo">

          <img src="@/assets/logo.png">

        </div>

        <div class="txt">窗口标题</div>

      </div>

    <div class="windowTool">

      <div @click="minisize">

        <i class="iconfont iconminisize"></i>

      </div>

      <div v-if="!isMaxSize" @click="restore">

        <i class="iconfont iconrestore"></i>

      </div>

      <div v-else @click="maxsize">

        <i class="iconfont iconmaxsize"></i>

      </div>

      <div @click="close" class="close">

        <i class="iconfont iconclose"></i>

      </div>

    </div>

    </div>

    <div id="nav">

      <router-link to="/about">Home</router-link> |

      <router-link to="/about">About</router-link>

    </div>

    <router-view/>

  </div>

</template>

<script>

const {remote}=window.require('electron')

export default {

  data(){

    return{isMaxSize:true}

  },

  methods:{

//最小化

    minisize(){

      var win=remote.getCurrentWindow()

      win.minimize()

    },

//恢复

    restore(){

    remote.getCurrentWindow().restore()

    this.isMaxSize=!this.isMaxSize

    },

//最大化

    maxsize(){

      console.log(remote.getCurrentWindow())

      var win=remote.getCurrentWindow()

      win.maximize()

      this.isMaxSize=!this.isMaxSize

    },

//关闭

    close(){

    remote.getCurrentWindow().hide()

    }

  },

}

</script>

<style>

@import url(https://at.alicdn.com/t/font_1378132_s4e44adve5.css);

.titleBar{

  height:38px;

  line-height: 36px;

  background: #fff1f0;

  display: flex;

  border-bottom: 1px solid #f5222d;

}

.title{flex:1;

      display: flex;

      -webkit-app-region:drag;

}

.logo{padding-left:8px;

      padding-right:6px;

}

.logo img{

  width:20px;

  height:20px;

  margin-top:7px;

}

.txt{text-align: left;

    flex:1;

}

.close:hover{color: #fff;

            background-color:#ff4d4f;

}

.windowTool div{

  color:#888;

  height:100%;

  width:38px;

  display: inline-block;

  cursor:pointer;

}

i{font-size:12px;}

.windowTool div:hover{background: #ffccc7;}

body{margin:0;

    padding:0;

    overflow: hidden;

    height:100%;

    }

#app {

  font-family: Avenir, Helvetica, Arial, sans-serif;

  -webkit-font-smoothing: antialiased;

  -moz-osx-font-smoothing: grayscale;

  text-align: center;

  color: #2c3e50;

  display: flex;

  flex-direction: column;

}

#nav {

  padding: 30px;

}

#nav a {

  font-weight: bold;

  color: #2c3e50;

}

#nav a.router-link-exact-active {

  color: #42b983;

}

</style>

注意:使用上述代码方法时需要注意在background.js文件中找到BrowserWindow将enableRemoteModule设置为true

enableRemoteModule设置为true

方法二:Electron自定义导航栏

1、在主进程background.js文件中配置frame: false  关闭原生导航栏

配置frame: false  关闭原生导航栏

2、在App.vue中引入窗口组件mainHeader.vue

App.vue中引入窗口组件mainHeader.vue

3、mainHeader.vue代码如下

<template>

    <section class="MainHeader">

        <div style="display: flex;">

            <router-link :to="'/timetables'" class="logo noDrag" v-stat="{action:'logo'}"/>

            <div class="userInfo noDrag" v-if="userInfo">

                <div class="name " @click="path" v-stat="{action:'userInfo'}">

                    <img src="~assets/images/user.png"/>

                    <span>{{userInfo?userInfo.name:''}}</span>

                </div>

                <span class="logout" @click="logOut">退出</span>

            </div>

        </div>

        <div class="systemBtn">

            <i class="mini noDrag" title="最小化" @click="setWin('min')"></i>

            <i title="最大化" class="max noDrag" @click="setWin('max')"></i>

            <i title="关闭" class="close noDrag" @click="setWin('close')"></i>

        </div>

    </section>

</template>

<script>

/**渲染进程(也就是当前页面)通过ipcRenderer.send 将方法字段名传递给主进程。**/

const {ipcRenderer} = window.require("electron");

    export default {

        methods: {

            setWin(type){

                ipcRenderer.send(type);

            }

        }

</script>

<style>

/**自定义的导航栏鼠标点击是无法拖动的,需要在css中增加-webkit-app-region: drag; 让鼠标可以拖动,若可拖动的html结构中有点击事件,那//需要在点击元素的css中另加-webkit-app-region: no-drag; 让其可触发点击事件,若不加该css则点击事件无效。**/

.MainHeader {

        -webkit-app-region: drag;

        .noDrag{

            -webkit-app-region: no-drag;

       }

}

</style>

4、在主进程background.js中执行ipcMain方法。

import {BrowserWindow, ipcMain} from 'electron'

mainWindow = new BrowserWindow({

        height: 690,

        useContentSize: true,

        width: 1100,

        minWidth: 1100,

        minHeight: 690,

        frame: false

    })

ipcMain.on('min', () => mainWindow.minimize());

ipcMain.on('max', () => {

    if (mainWindow.isMaximized()) {

        mainWindow.restore();

    } else {

        mainWindow.maximize()

    }

});

ipcMain.on('close', () => mainWindow.close());

主进程background.js文件中设置

九:自定义桌面应用程序后,浏览器端无法识别window,客户端无法启动,因此可以检查代码是否正在Electron或浏览器中执行,来判断是否引用window.require('electron')

检查代码是否正在Electron或浏览器中执行

export function isElectron() {

    // Renderer process

    if (typeof window !== 'undefined' && typeof window.process === 'object' && window.process.type === 'renderer') {

        return true;

    }

    // Main process

    if (typeof process !== 'undefined' && typeof process.versions === 'object' && !!process.versions.electron) {

        return true;

    }

    // Detect the user agent when the `nodeIntegration` option is set to true

    if (typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('Electron') >= 0) {

        return true;

    }

    return false;

}

在mainHeader组件中引入

import {isElectron} from '@/utils/isElectron'

if(isElectron()){//检查代码是否正在Electron或浏览器中执行    var {remote}=window.require('electron')    console.log("Electron中执行 !");}else{    console.log("浏览器中执行");}

如图所示:

检查代码是否正在Electron或浏览器中执行

十:去掉导航栏菜单

需要去掉的导航栏菜单
主要代码

十一:electron修改vue项目打包后的exe图标

注意:以下生成图标的地址和打包牡蛎都在dist_electron文件中

1、准备一张icon.png图片保存在public目录下

2.安装 electron-icon-builder

npm i electron-icon-builder --D

3、在package.json的scripts文件中添加一条命令并保存:

"electron:generate-icons": "electron-icon-builder --input=./public/icon.png --output=dist_electron --flatten"

3、执行以下命令

npm run electron:generate-icons

就会在dist_electron文件夹中生成一系列打包所需的图标文件,如下:

4、在vue.config.json中pluginOptions对象下配置图标路径,其中directories 为打包地址,win为图标路径。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容