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应用当本地版本和服务器地址的版本不一致自动会弹出更新提示

相关推荐
mldong3 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排3 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60614 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角5 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!5 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰5 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi7 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen7 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马8 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒8 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端