react+hook+vite项目使用eletron打包成桌面应用+可以热更新

使用Hooks-Admin的架构

Hooks-Admin: 🚀🚀🚀 Hooks Admin,基于 React18、React-Router V6、React-Hooks、Redux、TypeScript、Vite2、Ant-Design 开源的一套后台管理框架。https://gitee.com/HalseySpicy/Hooks-Adminexe桌面应用效果图

一.安装依赖包

npm i electron-updater element-plus is-electron
npm i electron@26.1.0 electron-builder electron-log vite-plugin-electron vite-plugin-electron-renderer --save-dev

安装后的package.json文件

二.配置package.json

1.添加 "main": "dist-electron/main.js",
2.在scripts下的build:test属性 先删除--mode test 然后添加 && electron-builder

3.在底部添加build打包配置

"build": {
		"appId": "com.electron.desktop",
		"productName": "qjyiot",
		"asar": true,
		"copyright": "Copyright © 2022 electron",
		"directories": {
			"output": "release/${version}"
		},
		"files": [
			"dist",
			"dist-electron"
		],
		"mac": {
			"artifactName": "${productName}_${version}.${ext}",
			"target": [
				"dmg"
			]
		},
		"win": {
			"target": [
				{
					"target": "nsis",
					"arch": [
						"x64"
					]
				}
			],
			"artifactName": "${productName}_${version}.${ext}",
			"icon": "electron/icon/logo.ico"
		},
		"nsis": {
			"oneClick": false,
			"perMachine": false,
			"allowToChangeInstallationDirectory": true,
			"deleteAppDataOnUninstall": false
		},
		"publish": [
			{
				"provider": "generic",
				"url": "exe应用服务器地址"
			}
		],
		"releaseInfo": {
			"releaseNotes": "版本更新的具体内容"
		}
	}

三.配置vite.config.ts

给plugins属性添加两个插件,屏蔽eslint插件。

import electron from "vite-plugin-electron";
import renderer from "vite-plugin-electron-renderer";
 
electron([{ entry: "electron/main.ts" }]),
renderer()

四.配置electron工具

helper.ts文件

import { join } from 'path'
import fs from 'fs'
import { app } from 'electron'
const dataPath = join(app.getPath('userData'), 'data.json')

export function getLocalData(key?:any) {
  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?:any, value?:any) {
  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)
  })
}

updater.ts文件

import { autoUpdater } from "electron-updater";
import { BrowserWindow, app, ipcMain, dialog } from "electron";
import { getLocalData, setLocalData, sleep } from "./helper";
import logger from "electron-log";
import pkg from "../package.json";

export default function updater(mainWin: BrowserWindow | null) {
  autoUpdater.autoDownload = false; // 是否自动更新
  autoUpdater.autoInstallOnAppQuit = false; // APP退出的时候自动安装
  // autoUpdater.allowDowngrade = true // 是否可以回退的属性

  /*
   * 在开启更新监听事件之前设置
   * 一定要保证该地址下面包含lasted.yml文件和需要更新的exe文件
   */

  // 发送消息给渲染线程
  function sendStatusToWindow(status?: any, params?: any) {
    mainWin && mainWin.webContents.send(status, params);
  }

  // 检查更新
  autoUpdater.on("checking-for-update", () => {
    sendStatusToWindow("checking-for-update");
  });

  // 可以更新版本
  autoUpdater.on("update-available", (info: any) => {
    // sendStatusToWindow("autoUpdater-canUpdate", info);

    const { version } = info;
    askUpdate(version);
  });

  // 更新错误
  autoUpdater.on("error", (err: any) => {
    sendStatusToWindow("autoUpdater-error", err);
  });
  // 发起更新程序
  ipcMain.on("autoUpdater-toDownload", () => {
    autoUpdater.downloadUpdate();
  });
  // 正在下载的下载进度
  autoUpdater.on("download-progress", (progressObj: any) => {
    sendStatusToWindow("autoUpdater-progress", progressObj);
  });
  // 下载完成
  autoUpdater.on("update-downloaded", (res) => {
    sendStatusToWindow("autoUpdater-downloaded");
  });

  //  没有可用的更新,也就是当前是最新版本
  autoUpdater.on("update-not-available", function (info: any) {
    sendStatusToWindow("autoUpdater-available", info);
  });

  // 退出程序
  ipcMain.on("exit-app", () => {
    autoUpdater.quitAndInstall();
  });

  // 重新检查是否有新版本更新
  ipcMain.on("monitor-update-system", () => {
    autoUpdater.checkForUpdates();
  });

  // 检测是否有更新
  setTimeout(() => {
    autoUpdater.checkForUpdates();
  }, 2000);
}

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: `${
        pkg.build.productName
      } 最新版本是 ${version},您现在的版本是 ${app.getVersion()},现在要下载更新吗?`,
      defaultId: 2,
      // checkboxLabel: "以后自动下载并安装更新",
      // checkboxChecked: false,
      textWidth: 300,
    });
    if ([1, 2].includes(response)) {
      let updaterData = {
        version: version,
        skip: false,
        // auto: checkboxChecked,
      };
      setLocalData({
        updater: {
          ...updaterData,
        },
      });
      if (response === 2) autoUpdater.downloadUpdate();
      logger.info(["更新操作", JSON.stringify(updaterData)]);
    } else {
      logger.info(["更新操作", "关闭更新提醒"]);
    }
  }
}

main.ts文件

import { BrowserWindow, app, ipcMain } from "electron";
import path from "path";
let win: BrowserWindow | null;
import updater from "./updater";
import pkg from "../package.json";

const createWindow = () => {
  win = new BrowserWindow({
    width: 1250,
    height: 700,
    minWidth: 1250,
    minHeight: 700,
    title: pkg.build.productName,
    icon: path.join(__dirname, "..", pkg.build.win.icon),
    webPreferences: {
      webviewTag: true,
      nodeIntegration: true,
      contextIsolation: false,
    },
  });

  if (win) {
    // win.setMenu(null); // 隐藏左上角菜单
  }

  if (process.env.NODE_ENV === "development") {
    process.env.VITE_DEV_SERVER_URL &&
      win.loadURL(process.env.VITE_DEV_SERVER_URL); // 使用vite开发服务的url路径访问应用
    
  } else {
    win.loadFile(path.join(__dirname, "..", "dist/index.html"));
  }

  updater(win);
};

// 定义关闭事件
ipcMain.handle("quit", () => {
  app.quit();
});

// 打开开发者工具
ipcMain.handle("openDevTools", () => {
  win && win.webContents.openDevTools();
});

// electron阻止应用多开
const additionalData = { myKey: "myValue" };
const gotTheLock = app.requestSingleInstanceLock(additionalData);
if (!gotTheLock) {
  app.quit();
} else {
  app.on(
    "second-instance",
    (event, commandLine, workingDirectory, additionalData) => {
      //输入从第二个实例中接收到的数据
      //有人试图运行第二个实例,我们应该关注我们的窗口
      if (win) {
        if (win.isMinimized()) win.restore();
        win.focus();
      }
    }
  );
  // if (process.env.NODE_ENV !== "development") { 
  app.whenReady().then(createWindow);
  // }
}

五.定义更新应用版本组件

在src目录下定义layouts\Updater\index.tsx

import { useState, useEffect } from "react";
import { Modal, message, Progress } from "antd";

const Updater = (props: any) => {
  const { ipcRenderer } = window.require("electron");
  const [showUpdater, setShowUpdater] = useState(false);
  const [downloadProcess, setDownloadProcess] = useState({
    percent: 10,
    speed: 0,
    transferred: "1kb",
    total: "2M",
  });

  const handleKeyDown = () => {
    document.onkeydown = (e) => {
      // 点击键盘F12键打开控制台
      if (e.key === "F12") {
        ipcRenderer.invoke("openDevTools");
      }
    };
  };

  const setIpcRenderer = () => {
    // 最新版本
    ipcRenderer.on("autoUpdater-available", (event: any, info: any) => {
      message.success(`【v${info.version}】当前是最新版本啦`);
    });
    // 发现新版本 once
    ipcRenderer.on("autoUpdater-canUpdate", (event: any, info: any) => {
      /*
       * 这儿会监听,如果info.version比现在版本小;就会触发;反之,不会触发
       */
      Modal.confirm({
        title: "提示",
        content: `发现有新版本【v${info.version}】,是否更新?`,
        maskClosable: false,
        onOk() {
          ipcRenderer.send("autoUpdater-toDownload");
        },
        onCancel() {},
      });
    });
    // 下载进度
    ipcRenderer.on("autoUpdater-progress", (event: any, process: any) => {
      if (process.transferred >= 1024 * 1024) {
        process.transferred =
          (process.transferred / 1024 / 1024).toFixed(2) + "M";
      } else {
        process.transferred = (process.transferred / 1024).toFixed(2) + "K";
      }
      if (process.total >= 1024 * 1024) {
        process.total = (process.total / 1024 / 1024).toFixed(2) + "M";
      } else {
        process.total = (process.total / 1024).toFixed(2) + "K";
      }
      if (process.bytesPerSecond >= 1024 * 1024) {
        process.speed =
          (process.bytesPerSecond / 1024 / 1024).toFixed(2) + "M/s";
      } else if (process.bytesPerSecond >= 1024) {
        process.speed = (process.bytesPerSecond / 1024).toFixed(2) + "K/s";
      } else {
        process.speed = process.bytesPerSecond + "B/s";
      }
      process.percent = process.percent.toFixed(2);
      setDownloadProcess({ ...downloadProcess, ...process });
      setShowUpdater(true);
    });
    // 下载更新失败
    ipcRenderer.once("autoUpdater-error", () => {
      setShowUpdater(false);
      message.error("更新失败");
    });
    // 下载完成
    ipcRenderer.once("autoUpdater-downloaded", () => {
      setShowUpdater(false);
      Modal.confirm({
        title: "提示",
        content: "更新完成,是否关闭应用程序安装新版本?",
        maskClosable: false,
        onOk() {
          ipcRenderer.send("exit-app");
        },
        onCancel() {},
      });
    });
  };

  useEffect(() => {
    window.addEventListener("keydown", handleKeyDown);
    setIpcRenderer();
    return () => {
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, []);

  return (
    <>
      <Modal
        title="更新中......"
        visible={showUpdater}
        footer={[]}
        maskClosable={false}
        closable={false}
      >
        <p>
          当前:【{downloadProcess.transferred}】 / 共【
          {downloadProcess.total}】
        </p>
        <Progress percent={downloadProcess.percent} strokeWidth={18} />
        <p>正在下载({downloadProcess.speed})......</p>
      </Modal>
    </>
  );
};

export default Updater;

五.App.vue根组件引用

六.打包

npm run build:test

生成exe应用,点击可安装

安装完后,打开exe应用当本地版本和服务器地址的版本不一致自动会弹出更新提示

相关推荐
小白学前端66611 分钟前
React Router 深入指南:从入门到进阶
前端·react.js·react
web1309332039831 分钟前
前端下载后端文件流,文件可以下载,但是打不开,显示“文件已损坏”的问题分析与解决方案
前端
outstanding木槿43 分钟前
react+antd的Table组件编辑单元格
前端·javascript·react.js·前端框架
好名字08211 小时前
前端取Content-Disposition中的filename字段与解码(vue)
前端·javascript·vue.js·前端框架
隐形喷火龙2 小时前
element ui--下拉根据拼音首字母过滤
前端·vue.js·ui
m0_748241122 小时前
Selenium之Web元素定位
前端·selenium·测试工具
等一场春雨2 小时前
springboot 3 websocket react 系统提示,选手实时数据更新监控
spring boot·websocket·react.js
风无雨2 小时前
react杂乱笔记(一)
前端·笔记·react.js
前端小魔女2 小时前
2024-我赚到自媒体第一桶金
前端·rust
鑫~阳2 小时前
快速建站(网站如何在自己的电脑里跑起来) 详细步骤 一
前端·内容管理系统cms