feat : Add electron auto update function

This commit is contained in:
任帅 2023-02-28 17:09:20 +08:00
parent 564c48861d
commit 9432cd793f
16 changed files with 170 additions and 209 deletions

View File

@ -37,6 +37,6 @@
publish:{ publish:{
provider: 'generic', provider: 'generic',
channel: 'latest', channel: 'latest',
url: 'https://github.com/RSS1101/electron-vite-react/releases/download/v9.9.9/', url: 'https://github.com/electron-vite/electron-vite-react/releases/download/v0.9.9/',
} }
} }

View File

@ -1,108 +1,90 @@
import { autoUpdater } from "electron-updater" import { autoUpdater } from "electron-updater"
import { app, ipcMain } from "electron"; import { app, ipcMain } from "electron";
export const update = (win: Electron.CrossProcessExports.BrowserWindow) => { export const update = (win: Electron.CrossProcessExports.BrowserWindow) => {
// 设置日志打印
// 是否自动下载更新,设置为 false 时将通过 API 触发更新下载 // When set to false, the update download will be triggered through the API
autoUpdater.autoDownload = false; autoUpdater.autoDownload = false;
autoUpdater.disableWebInstaller = false autoUpdater.disableWebInstaller = false
// 是否允许版本降级,也就是服务器版本低于本地版本时,依旧以服务器版本为主
autoUpdater.allowDowngrade = false; autoUpdater.allowDowngrade = false;
// 设置服务器版本最新版本查询接口配置 // Save the version status of whether the update needs to be installed,
autoUpdater.setFeedURL({ // Because the user needs to update immediately and later after the update is downloaded
provider: 'generic',
channel: 'latest',
url: 'https://github.com/RSS1101/electron-vite-react/releases/download/v9.9.9/',
});
// 保存是否需要安装更新的版本状态,因为需要提供用户在下载完成更新之后立即更新和稍后更新的操作
let NEED_INSTALL = false; let NEED_INSTALL = false;
Object.defineProperty(app, 'isPackaged', { // Check whether update is used
get() {
return true;
}
});
// 调用 API 检查是否用更新
ipcMain.on('check-update',()=>{ ipcMain.on('check-update',()=>{
autoUpdater.checkForUpdatesAndNotify() autoUpdater.checkForUpdatesAndNotify()
.then((res) => { .then((res) => {
win.webContents.send('check-update-type',{ checkUpdate: true}) win.webContents.send('check-update-type',{ checkUpdate: true})
}).catch(err => { }).catch(err => {
// 网络错误 network error // network error
win.webContents.send('check-update-type', { checkUpdate: false}) win.webContents.send('check-update-type', { checkUpdate: false})
}); });
}) })
// 检测开始 // start check
autoUpdater.on('checking-for-update', function () { autoUpdater.on('checking-for-update', function () {
console.log('checking-for-update') console.log('checking-for-update')
}) })
// 更新可用 // update available
autoUpdater.on('update-available', (arg) => { autoUpdater.on('update-available', (arg) => {
console.log('update-available') console.log('update-available')
win.webContents.send('is-update-available', { isUpdate: true, oldVersion: app.getVersion(), newVersion: arg?.version }) win.webContents.send('is-update-available', { isUpdate: true, oldVersion: app.getVersion(), newVersion: arg?.version })
}) })
// 更新不可用 // update not available
autoUpdater.on('update-not-available', (arg) => { autoUpdater.on('update-not-available', (arg) => {
console.log('update-not-available') console.log('update-not-available')
win.webContents.send('is-update-available', { isUpdate: false, oldVersion: app.getVersion(), newVersion: arg?.version }) win.webContents.send('is-update-available', { isUpdate: false, oldVersion: app.getVersion(), newVersion: arg?.version })
}) })
// API 触发更新下载
const startDownload = (callback: any, successCallback: any) => { const startDownload = (callback: any, successCallback: any) => {
// 监听下载进度并推送到更新窗口 // Monitor the download progress and push it to the update window
autoUpdater.on('download-progress', (data) => { autoUpdater.on('download-progress', (data) => {
console.log("progress", data) console.log("progress", data)
win.webContents.send('download-progress-data', data) win.webContents.send('download-progress-data', data)
callback && callback instanceof Function && callback(null, data); callback && callback instanceof Function && callback(null, data);
}); });
// 监听下载错误并推送到更新窗口 // Listen for download errors and push to the update window
autoUpdater.on('error', (err) => { autoUpdater.on('error', (err) => {
console.log("error")
callback && callback instanceof Function && callback(err); callback && callback instanceof Function && callback(err);
}); });
// 监听下载完成并推送到更新窗口 // Listen to the download completion and push it to the update window
autoUpdater.on('update-downloaded', () => { autoUpdater.on('update-downloaded', () => {
console.log("update-downloaded")
NEED_INSTALL = true; NEED_INSTALL = true;
successCallback && successCallback instanceof Function && successCallback(); successCallback && successCallback instanceof Function && successCallback();
}); });
// 下载更新
autoUpdater.downloadUpdate(); autoUpdater.downloadUpdate();
}; };
// 监听应用层发送来的进程消息,开始下载更新 // Listen to the process message sent by the application layer and start downloading updates
ipcMain.on('start-download', (event) => { ipcMain.on('start-download', (event) => {
console.log("start") console.log("start")
startDownload( startDownload(
(err: any, progressInfo: { percent: any; }) => { (err: any, progressInfo: { percent: any; }) => {
if (err) { if (err) {
//回推下载错误消息 // callback download error message
console.log("update-error")
event.sender.send('update-error', { updateError:true}); event.sender.send('update-error', { updateError:true});
} else { } else {
//回推下载进度消息 // callback update progress message
console.log("pdate-progress-percent")
event.sender.send('update-progress', { progressInfo: progressInfo.percent }); event.sender.send('update-progress', { progressInfo: progressInfo.percent });
} }
}, },
() => { () => {
//回推下载完成消息 // callback update downed message
console.log("update-downed")
event.sender.send('update-downed'); event.sender.send('update-downed');
} }
); );
}); });
// install now
ipcMain.on('quit-and-install', () => { ipcMain.on('quit-and-install', () => {
autoUpdater.quitAndInstall(false, true); autoUpdater.quitAndInstall(false, true);
}) })
// 用户点击稍后安装后程序退出时执行立即安装更新 // install later
app.on('will-quit', () => { app.on('will-quit', () => {
console.log("NEED_INSTALL=true") console.log("NEED_INSTALL=true")
if (NEED_INSTALL) { if (NEED_INSTALL) {

View File

@ -32,10 +32,6 @@
.card { .card {
padding: 2em; padding: 2em;
button{
margin: 0 20px;
background-color: #646cffaa;
}
} }
.read-the-docs { .read-the-docs {

View File

@ -1,75 +1,36 @@
import nodeLogo from "./assets/node.svg" import nodeLogo from './assets/node.svg'
import { useEffect, useRef, useState } from 'react' import { useState } from 'react'
import './App.scss' import './App.scss'
import Update from "./pages/update" import Update from '@/components/update'
import { ipcRenderer } from "electron"
console.log('[App.tsx]', `Hello world from Electron ${process.versions.electron}!`) console.log('[App.tsx]', `Hello world from Electron ${process.versions.electron}!`)
interface checkUpdateType {
checkUpdate: boolean
}
function App() { function App() {
const [count, setCount] = useState(0) const [count, setCount] = useState(0)
const ref =useRef<any>(null)
const [checkBtnText, setCheckBtnText] = useState('check update')
const [checkLoading, setCheckLoading] = useState(false)
const [checkType, setCheckType] = useState(false)
const [openModalType, setopenModalType] = useState(false)
/**
* @description Listen for new updates
*/
const checkUpdate = () => {
setCheckLoading(true)
setCheckBtnText('checking Update ...')
ipcRenderer.send('check-update')
}
/**
* @description Get the check results
*/
ipcRenderer.on('check-update-type', (_event, ...args: checkUpdateType[]) => {
setCheckLoading(false)
setCheckBtnText('check update')
setCheckType(args[0].checkUpdate)
setopenModalType(true)
})
/**
* @description ref.
*/
useEffect(()=>{
if (openModalType){
ref.current!.openModal()
setopenModalType(false)
}
}, [openModalType])
return ( return (
<div className="App"> <div className='App'>
<div> <div>
<a href="https://github.com/electron-vite/electron-vite-react" target="_blank"> <a href='https://github.com/electron-vite/electron-vite-react' target='_blank'>
<img src="./electron-vite.svg" className="logo" alt="Electron + Vite logo" /> <img src='./electron-vite.svg' className='logo' alt='Electron + Vite logo' />
</a> </a>
</div> </div>
<h1>Electron + Vite + React</h1> <h1>Electron + Vite + React</h1>
<div className="card"> <div className='card'>
<button onClick={() => setCount((count) => count + 1)}> <button onClick={() => setCount((count) => count + 1)}>
count is {count} count is {count}
</button> </button>
<button disabled={checkLoading} onClick={checkUpdate}>
{checkBtnText}
</button>
<p> <p>
Edit <code>src/App.tsx</code> and save to test HMR Edit <code>src/App.tsx</code> and save to test HMR
</p> </p>
</div> </div>
<p className="read-the-docs"> <p className='read-the-docs'>
Click on the Electron + Vite logo to learn more Click on the Electron + Vite logo to learn more
</p> </p>
<div className="flex-center"> <div className='flex-center'>
Place static files into the<code>/public</code> folder <img style={{ width: "5em" }} src={nodeLogo} alt="Node logo" /> Place static files into the<code>/public</code> folder <img style={{ width: '5em' }} src={nodeLogo} alt='Node logo' />
</div> </div>
<Update ref={ref} checkType={checkType}/> <Update />
</div> </div>
) )
} }

View File

@ -1,18 +0,0 @@
import { ReactNode } from 'react';
interface childrens {
titleText?: string;
isHeaderShow?: boolean;
isFooterShow?: boolean;
canCelText?: string;
submitText?: string;
onSubmit?: () => void;
onCanCel?: () => void;
}
export interface ModalChildType extends childrens {
body: ReactNode | null;
}
export interface ModalPropsType extends childrens {
isOpenModal: boolean;
children: ReactNode | null;
}

View File

@ -1,22 +0,0 @@
import { RsProgressType } from './type';
import './index.scss';
const RsProgress = (props: RsProgressType) => {
return (
<div className="rs-progress">
<div className="rs-progress-pr" style={{ width: props.rateWidth ?? 250 }}>
<div
className="rs-progress-rate"
style={{
width: (props.percent ?? 0) * ((props.rateWidth ?? 250) / 100),
backgroundColor: props.rateColor ?? 'blue',
}}
/>
</div>
<span className='rs-progress-num'>{props.percent > 100 ? 100 :(props.percent.toString().substring(0,2) ?? 0) }%</span>
</div>
);
};
export default RsProgress;

View File

@ -1,5 +0,0 @@
export interface RsProgressType {
rateColor?: string;
rateWidth?: number;
percent: number;
}

View File

@ -1,4 +1,4 @@
.rs-modal-bg{ .up-modal-bg{
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
position: fixed; position: fixed;
@ -7,30 +7,30 @@
z-index: 9999; z-index: 9999;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
} }
.rs-modal { .up-modal {
position: absolute; position: absolute;
top: 20vh; top: 20vh;
left: 30vw; left: 30vw;
z-index: 10000; z-index: 10000;
.rs-modal-panel { .up-modal-panel {
border: 1px solid #000000; border: 1px solid #000000;
border-radius: 5px; border-radius: 5px;
.rs-modal-header { .up-modal-header {
$titleheight: 38px; $titleheight: 38px;
width: 530px; width: 530px;
height: $titleheight; height: $titleheight;
line-height: $titleheight; line-height: $titleheight;
background-color: rgb(99, 153, 255); background-color: rgb(99, 153, 255);
display: flex; display: flex;
.rs-modal-header-text { .up-modal-header-text {
text-align: center; text-align: center;
width: 480px; width: 480px;
} }
} }
.rs-modal-body{ .up-modal-body{
background-color: #ffffff; background-color: #ffffff;
} }
.rs-modal-footer { .up-modal-footer {
background-color: #ffffff; background-color: #ffffff;
display: flex; display: flex;
justify-content: end; justify-content: end;

View File

@ -4,12 +4,12 @@ import './index.scss';
const ModalTemplate = (child: ModalChildType) => { const ModalTemplate = (child: ModalChildType) => {
return ( return (
<> <>
<div className="rs-modal-bg" onClick={child.onCanCel} /> <div className="up-modal-bg" onClick={child.onCanCel} />
<div className="rs-modal"> <div className="up-modal">
<div className="rs-modal-panel"> <div className="up-modal-panel">
{child.isHeaderShow ? ( {child.isHeaderShow ? (
<div className="rs-modal-header"> <div className="up-modal-header">
<div className="rs-modal-header-text">{child.titleText}</div> <div className="up-modal-header-text">{child.titleText}</div>
<svg <svg
onClick={child.onCanCel} onClick={child.onCanCel}
className="icon" className="icon"
@ -28,11 +28,11 @@ const ModalTemplate = (child: ModalChildType) => {
</div> </div>
) : null} ) : null}
<div className="rs-modal-body">{child.body}</div> <div className="up-modal-body">{child.body}</div>
{child.isFooterShow ? ( {child.isFooterShow ? (
<div className="rs-modal-footer"> <div className="up-modal-footer">
<button onClick={child.onSubmit}>{child.submitText ?? '确认'}</button> {(child.isSubmitShow ?? true) ?<button onClick={child.onSubmit}>{child.submitText ?? '确认'}</button> : null}
<button onClick={child.onCanCel}>{child.canCelText ?? '取消'}</button> {(child.isCanCelShow ?? true) ? <button onClick={child.onCanCel}>{child.canCelText ?? '取消'}</button> : null}
</div> </div>
) : null} ) : null}
</div> </div>
@ -41,13 +41,15 @@ const ModalTemplate = (child: ModalChildType) => {
); );
}; };
const RsModal = (props: ModalPropsType) => { const Modal = (props: ModalPropsType) => {
return createPortal( return createPortal(
props.isOpenModal? props.isOpenModal?
ModalTemplate({ ModalTemplate({
titleText: props.titleText, titleText: props.titleText,
isHeaderShow: props.isHeaderShow ?? true, isHeaderShow: props.isHeaderShow ?? true,
isFooterShow: props.isFooterShow ?? true, isFooterShow: props.isFooterShow ?? true,
isCanCelShow: props.isCanCelShow ?? true,
isSubmitShow: props.isSubmitShow ?? true,
body: props.children, body: props.children,
submitText: props.submitText, submitText: props.submitText,
canCelText: props.canCelText, canCelText: props.canCelText,
@ -57,4 +59,4 @@ const RsModal = (props: ModalPropsType) => {
document.body, document.body,
); );
}; };
export default RsModal; export default Modal;

20
src/components/update/Modal/type.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
import { ReactNode } from 'react'
interface childrens {
titleText?: string
isHeaderShow?: boolean
isFooterShow?: boolean
isCanCelShow?: boolean
isSubmitShow?: boolean
canCelText?: string
submitText?: string
onSubmit?: () => void
onCanCel?: () => void
}
export interface ModalChildType extends childrens {
body: ReactNode | null
}
export interface ModalPropsType extends childrens {
isOpenModal: boolean
children: ReactNode | null
}

View File

@ -1,16 +1,16 @@
.rs-progress{ .up-progress{
display: flex; display: flex;
align-items: center; align-items: center;
.rs-progress-pr{ .up-progress-pr{
border: 1px solid #000000; border: 1px solid #000000;
border-radius: 3px; border-radius: 3px;
height: 6px; height: 6px;
} }
.rs-progress-rate{ .up-progress-rate{
height: 6px; height: 6px;
border-radius: 3px; border-radius: 3px;
} }
.rs-progress-num{ .up-progress-num{
margin: 0 10px; margin: 0 10px;
} }
} }

View File

@ -0,0 +1,22 @@
import { RsProgressType } from './type'
import './index.scss'
const Progress = (props: RsProgressType) => {
return (
<div className="up-progress">
<div className="up-progress-pr" style={{ width: props.rateWidth ?? 250 }}>
<div
className="up-progress-rate"
style={{
width: (props.percent ?? 0) * ((props.rateWidth ?? 250) / 100),
backgroundColor: props.rateColor ?? 'blue',
}}
/>
</div>
<span className='up-progress-num'>{props.percent > 100 ? 100 :(props.percent.toString().substring(0,4) ?? 0) }%</span>
</div>
);
};
export default Progress;

View File

@ -0,0 +1,5 @@
export interface RsProgressType {
rateColor?: string
rateWidth?: number
percent: number
}

View File

@ -1,4 +1,4 @@
.modal-body{ .up-modal-body{
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@ -1,27 +1,18 @@
import RsModal from "@/components/RsModal" import Modal from '@/components/update/Modal'
import RsProgress from "@/components/RsProgress" import Progress from '@/components/update/Progress'
import { ipcRenderer } from "electron" import { ipcRenderer } from 'electron'
import { forwardRef, useEffect, useImperativeHandle, useState } from "react" import { useEffect, useState } from 'react'
import "./index.scss" import './index.scss'
import { checkUpdateType, isUpdateAvailable, ModalBtnText, VersionInfo } from './type'
interface VersionInfo {
oldVersion: string,
newVersion: string
}
interface isUpdateAvailable extends VersionInfo {
isUpdate: boolean,
}
interface ModalBtnText {
canCelText: string,
submitText: string
}
let onModalSubmit = () => { } let onModalSubmit = () => { }
let onModalCanCel = () => { } let onModalCanCel = () => { }
const Update = forwardRef((props: { checkType: boolean }, ref) => { const Update = () => {
const [checkBtnText, setCheckBtnText] = useState('check update')
const [checkType, setCheckType] = useState(false)
const [checkLoading, setCheckLoading] = useState(false)
const [isOpenModal, setIsOpenModal] = useState<boolean>(false) const [isOpenModal, setIsOpenModal] = useState<boolean>(false)
const [percentNum, setPercentNum] = useState<number>(0) const [percentNum, setPercentNum] = useState<number>(0)
const [isNeedUpdate, setIsNeedUpdate] = useState<boolean>(false) const [isNeedUpdate, setIsNeedUpdate] = useState<boolean>(false)
@ -35,23 +26,33 @@ const Update = forwardRef((props: { checkType: boolean }, ref) => {
submitText: '' submitText: ''
}) })
useImperativeHandle(ref, () => ({
openModal: () => setIsOpenModal(true)
}));
useEffect(() => { useEffect(() => {
onModalCanCel = () => setIsOpenModal(false) onModalCanCel = () => setIsOpenModal(false)
}, []) }, [])
/** // Check for updates
* @description Get version information and whether to update const checkUpdate = () => {
*/ setCheckLoading(true)
setCheckBtnText('checking Update ...')
ipcRenderer.send('check-update')
}
// Listen to get the check result
ipcRenderer.on('check-update-type', (_event, ...args: checkUpdateType[]) => {
setCheckLoading(false)
setCheckBtnText('check update')
setCheckType(args[0].checkUpdate)
setIsOpenModal(true)
})
// Get version information and whether to update
ipcRenderer.on('is-update-available', (_event, ...args: isUpdateAvailable[]) => { ipcRenderer.on('is-update-available', (_event, ...args: isUpdateAvailable[]) => {
setVersionInfo({ setVersionInfo({
oldVersion: args[0].oldVersion, oldVersion: args[0].oldVersion,
newVersion: args[0].newVersion, newVersion: args[0].newVersion,
}) })
setIsNeedUpdate(args[0].isUpdate) setIsNeedUpdate(args[0].isUpdate)
// Update required
if (args[0].isUpdate) { if (args[0].isUpdate) {
setModalBtnText({ setModalBtnText({
canCelText: 'cancel', canCelText: 'cancel',
@ -61,21 +62,19 @@ const Update = forwardRef((props: { checkType: boolean }, ref) => {
onModalCanCel = () => setIsOpenModal(false) onModalCanCel = () => setIsOpenModal(false)
} }
}) })
/**
* @description Throw the update failure message if the update fails // Throw the update failure message when the update fails
*/
ipcRenderer.on('update-error', (_event, ...args: { updateError: boolean }[]) => { ipcRenderer.on('update-error', (_event, ...args: { updateError: boolean }[]) => {
setUpdateError(args[0].updateError) setUpdateError(args[0].updateError)
setCheckType(false)
}) })
/**
* @description update progress // Get update progress
*/
ipcRenderer.on('update-progress', (_event, ...args: { progressInfo: number }[]) => { ipcRenderer.on('update-progress', (_event, ...args: { progressInfo: number }[]) => {
setPercentNum(args[0].progressInfo) setPercentNum(args[0].progressInfo)
}) })
/**
* @description is update been completed // is update been completed
*/
ipcRenderer.on('update-downed', (_event, ...args) => { ipcRenderer.on('update-downed', (_event, ...args) => {
setPercentNum(100) setPercentNum(100)
setModalBtnText({ setModalBtnText({
@ -90,34 +89,35 @@ const Update = forwardRef((props: { checkType: boolean }, ref) => {
}) })
return ( return (
<> <div>
<RsModal isOpenModal={isOpenModal} onCanCel={onModalCanCel} onSubmit={onModalSubmit} <Modal isOpenModal={isOpenModal} onCanCel={onModalCanCel} onSubmit={onModalSubmit}
canCelText={modalBtnText.canCelText} submitText={modalBtnText.submitText} canCelText={modalBtnText.canCelText} submitText={modalBtnText.submitText}
isFooterShow={props.checkType && isNeedUpdate}> isFooterShow={checkType && isNeedUpdate}>
<div className="modal-body"> <div className='up-modal-body'>
{updateError ? {updateError ?
<div className="update-error">Error downloading the latest version, please contact the developer</div> : <div className='update-error'>Error downloading the latest version, please contact the developer</div> :
props.checkType ? ( checkType ? (
isNeedUpdate ? ( isNeedUpdate ? (
<div> <div>
<div> <div>
<span> oldVersion : v.{versionInfo.oldVersion} </span> <span> oldVersion : v.{versionInfo.oldVersion} </span>
<span> newVersion : v.{versionInfo.newVersion} </span> <span> newVersion : v.{versionInfo.newVersion} </span>
</div> </div>
<div className="update-progress"> <div className='update-progress'>
<span className="progress-title"> update progress : </span> <span className='progress-title'> update progress : </span>
<RsProgress percent={percentNum} ></RsProgress> <Progress percent={percentNum} ></Progress>
</div> </div>
</div>) </div>)
: <span>This is last version : v.{versionInfo.oldVersion} !</span> : <span>This is last version : v.{versionInfo.oldVersion} !</span>
) : <span>Check update is Error,Please check your network!</span> ) : <span>Check update is Error,Please check your network!</span>
} }
</div> </div>
</RsModal> </Modal>
<button disabled={checkLoading} onClick={checkUpdate}>
</> {checkBtnText}
</button>
</div>
) )
}) }
export default Update export default Update

18
src/components/update/type.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
export interface checkUpdateType {
checkUpdate: boolean
}
export interface VersionInfo {
oldVersion: string
newVersion: string
}
export interface isUpdateAvailable extends VersionInfo {
isUpdate: boolean
}
export interface ModalBtnText {
canCelText: string
submitText: string
}