1、建议使用electron-builder进行打包
electron-builder与electron-forge相比,参考的资料更多一些,比较适合新手;
使用electron-builder打包,需要设置package.json里的main修改为"./public/electron.js",同时将原来的main.js文件名改为electron.js,放到public里;如果不修改,编译的时候将会提示找不到electron.js;
如果一些文件夹不想打包进app.asar,可以在package.json里进行配置,下面提供一个配置
***package.json里添加***斜体样式
"build": {
"productName": "系统名称",
"appId": "zhongke.suyuan",
"copyright":"@2023 我的",
"directories": {//配置打包后文件放置的地方
"output": "out"
},
"win":{
"target": [{
"target":"nsis"
}],
"icon":"./build/favicon.ico" //系统的图标
},
//配置不想打包进app.asar的文件夹,文件夹里必须起码要有一个文件,不然打包后不会有这个文件夹
"extraResources": [
{
"from": "./report",
"to": "report"
}
],
//配置打包后的exe的名称等相关配置
"nsis": {
"shortcutName": "系统名称",
"oneClick": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": true,
"perMachine": true,
"installerIcon": "./build/favicon.ico",
"uninstallerIcon": "./build/favicon.ico",
"installerHeaderIcon": "./build/favicon.ico"
}
},
2、如果想存储缓存数据,使用electron-store
//安装
yarn add electron-store
使用widow.localStorage存储数据只能在软件未关闭前能拿到。一旦软件关闭再重新启动,那么缓存的数据将会被清除掉。使用上面的插件,能有效解决。
//在main.js里添加如下代码
const Store = require('electron-store');
const store = new Store();
// 定义ipcRenderer监听事件
ipcMain.on('setStore', (_, key, value) => {
store.set(key, value)
})
ipcMain.on('getStore', (_, key) => {
let value = store.get(key)
_.returnValue = value || ""
})
//在preload.js里添加代码,此文件是专门暴露electron的api给react使用的,直接使用是拿不到的
const { contextBridge, ipcRenderer ,shell } = require('electron')
const fs = require("fs")
const Store = require('electron-store');
const store = new Store();
contextBridge.exposeInMainWorld(
'electron',
{
readFile: (filePath)=>{///读取本地文件内容
let data = fs.readFileSync(filePath,'utf-8')
return data
},
saveFile:(filePath,name,type)=>{//保存文件或图片到本地
///理论上来讲应该是第一个,但是不知道为啥生成的pdf多了filename=generated.pdf这个,导致失效,只能写死
// let base64 = filePath.replace(/^data:application\/\w+;base64,/, '');
默认获取报告的文件夹位置
let reportPath = store.get(type || 'reportPath') //文件存储的地址,例如C:/users 存储在c盘的用户文件夹里
let base64 = filePath.replace('data:application/pdf;filename=generated.pdf;base64,', '').replace('data:image/png;base64,', '');
let dataBuffer = Buffer.from(base64,'base64')
try {
fs.writeFileSync(reportPath + '/'+name, dataBuffer)
return true
}catch (err){
return false
}
},
ipcRenderer:{//缓存数据或或获取数
setStoreValue:(key,value)=>{
ipcRenderer.send("setStore", key, value)
},
getStoreValue:(key)=>{
const resp = ipcRenderer.sendSync("getStore", key)
return resp
},
},
getBuffer:(filePath)=>{//获取本地文件的buffer
try {
let data = fs.readFileSync(filePath)
return data
}catch {
return false
}
}
)
//在react代码里使用方式
//key是存储的字段名,value是存储的值,使用方式跟window.localStorage类似
window.electron.ipcRenderer.setStoreValue(key,value)
window.electron.ipcRenderer.getStoreValue(key)
//其他方法是使用方式跟上面的类似
3、main.js文件的基础配置
const { app, BrowserWindow, ipcMain} = require('electron')
process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true' ///去除一些警告
// 引入node的 path 和url模块
const path = require('path')
const url = require('url');
// 获取在 package.json 中的命令脚本传入的参数,来判断是开发还是生产环境
const mode = process.argv[2];
const Store = require('electron-store');
const store = new Store();
// 保持window对象的全局引用,避免JavaScript对象被垃圾回收时,窗口被自动关闭.
let mainWindow
//ipcMain在preload.js里暴露给window,返回的是undefind,只能在主程序里使用;其他一些api也有类似的特性
// 定义ipcRenderer监听事件
ipcMain.on('setStore', (_, key, value) => {
store.set(key, value)
})
ipcMain.on('getStore', (_, key) => {
let value = store.get(key)
_.returnValue = value || ""
})
const createWindow = () => {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
webSecurity: false, // 禁用同源策略,能够允许跨域请求
preload: path.join(__dirname, 'preload.js'),
nodeIntegration:true
}
})
//判断是否是开发模式
if(mode === 'dev') {
mainWindow.loadURL("http://localhost:3000/")
} else {
mainWindow.loadURL(url.format({
pathname:path.join(__dirname, './build/index.html'),
protocol:'file:',
slashes:true
}))
}
// 打开开发者工具,默认不打开
// mainWindow.webContents.openDevTools()
// 关闭window时触发下列事件.
mainWindow.on('closed', ()=> {
mainWindow = null
})
// 解决应用启动白屏问题
mainWindow.on('ready-to-show', () => {
mainWindow.show();
mainWindow.focus();
});
}
app.whenReady().then(() => {
createWindow()
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
// macOS中点击Dock图标时没有已打开的其余应用窗口时,则通常在应用中重建一个窗口
if (mainWindow === null) {
createWindow()
}
})
4、react使用路由时,切记不要用BrowserRouter,要用HashRouter,不然开发的时候是好的,打包后启动软件,页面是空白的
//package.json里添加"homepage": "./",
{
"name": "suyuan",
"version": "0.1.0",
"private": true,
"main": "main.js",
"homepage": "./",
...
}
//app.js
import './App.css';
import Routers from './router/router'
import {HashRouter} from 'react-router-dom';
const App = () => {
return (
<HashRouter>
<div className="App">
<Routers />
{/*<Footer />*/}
</div>
</HashRouter>
)
}
export default App;
//router.js
//引入路由
import { Routes, Route, useNavigate} from 'react-router-dom'
import Index from '../pages/index'
import Login from '../pages/login'
import {useEffect} from "react";
import {getStorage} from "../components/baseFun";
const Routers = () => {
const navigate = useNavigate()
useEffect(()=>{
//如果没有token,那么将跳转到登录页面
let token = getStorage('token')
if(!token){
navigate('/login')
}
},[navigate])
return (
<Routes>
<Route path='/login' element={<Login />}/>
<Route path='/index' element={<Index />}/>
<Route path='/' element={<Index />}/>
</Routes>
);
};
export default Routers
有这么一种情况,比如说token过期了,我想在全局请求拦截里判断进行跳转,但是useNavigate只能在hooks组件里使用,在其他地方使用会直接报错;为了解决这个问题,我们使用如下方式
//history.js 因为路由是用的HashRouter,所有用的createHashHistory,而非createBrowserHistory,不要用错了
import { createHashHistory } from 'history';
export default createHashHistory ();
//在要使用的地方导入上面的文件
history.push('/login',{isLogin:true}) ///只是修改了值,并不会触发页面的重新渲染;嗐,我也暂时没解决,使用了下面一行解决问题
window.location.reload();//刷新页面,手动加载当前路由实现跳转
5、神奇的react-jsx-parser,专门用来解析react的jsx字符串。
此插件一般用在请求数据渲染不同内容的情况,比如说新闻,报告模板等,它比dangerouslySetInnerHTML
好的地方在于支持react组件的导入,动态数据的渲染,下图例子展示
import {Checkbox, Col, Row,Image,Radio,QRCode} from "antd";
import dayjs from 'dayjs'
import JsxParser from 'react-jsx-parser'
import './reportTemplate.css'
import Barcode from "../../../components/Barcode";
import checkedImg from '../../../images/checked.png'
import nocheckedImg from '../../../images/nochecked.png'
const ReportTemplate = (props) => {
//jsx内容例子
//<Row>
// <Col>
// {baseInfo.name}
// </Col>
// <Image src={checkedImg} />
//</Row>
let {formValue,baseInfo,jsx} = props
return <JsxParser
components={{ Row, Col,Checkbox,Image,Radio,Barcode,QRCode}} ///用来传jsx使用到的组件
bindings={{baseInfo,formValue,dayjs,getUrlId,checkedImg,nocheckedImg}} ///用来传jsx使用到的数据
jsx={`${jsx}`} //用来传jsx字符串
/>
}
export default ReportTemplate
需要注意的一点是,如果jsx的写法不规范,那么此插件渲染出的内容为空,要检测下
更多使用方式点击这里
6、 数据持久缓存库 lndb
npm install lndb 或者 yarn add lndb
//electron.js文件
const appPath = app.isPackaged ? path.dirname(app.getPath('exe')) : app.getAppPath();
//配置数据存放的文件夹
store.set('dataPath', mode === 'dev'?appPath+'/data':appPath+'/resources/data')
//preload.js文件
const LNDB = require('lndb')
//在contextBridge.exposeInMainWorld里添加api
//存储数据 建议数据加个id,用于标记唯一性
saveData:({folder,key,value})=>{
//获取数据存放的文件夹
let dataPath = store.get('dataPath')
const db = new LNDB(dataPath)
// 初始类型
const pg = db.init(folder)
let datas = pg.get(key)
//已经存储过数据了
if(datas.data){
//我这边是默认存储数组对象的,所以这么写,如果有其他数据格式,自己在这边调整
//检查是否已存在此数据,防止重复添加
let hasData = datas.data.find(item=>item.code === value.code && value.code)
if(hasData){
ipcRenderer.send("showLog", {title:'提示信息',message:'项目代码已存在,请检查!'})
return false
}
datas.data.unshift(value)
pg.setAsync(key, datas.data).then()
}else {
pg.setAsync(key,[value]).then()
}
//此处返回所有数据是为了前端页面展示最新数据,可去除或修改
return pg.get(key).data
}
//这个是在jsx文件里的调用方式
// let data = window.electron.saveData({
// folder:'sampletype', //自定义
// key:'list', //自定义
// value:values //要保存的数据
// })
//获取数据
getData:({folder,key})=>{
//获取数据存放的文件夹
let dataPath = store.get('dataPath')
const db = new LNDB(dataPath)
// 初始类型
const pg = db.init(folder)
let datas = pg.get(key)
return datas?.data || []
}
// datas = window.electron.getData({folder:'tester',key:'list'}) 这个是在jsx文件里的调用方式
//编辑数据
editData:({folder,key,value})=>{
let dataPath = store.get('dataPath')
const db = new LNDB(dataPath)
// 初始类型
const pg = db.init(folder)
let datas = pg.get(key).data
const foundIndex = datas.findIndex(obj => obj.id === value.id);
if (foundIndex !== -1) {
datas[foundIndex] = value;
}
pg.setAsync(key,datas).then()
return pg.get(key).data
}
//删除数据
deleteData:({folder,key,id})=>{
let dataPath = store.get('dataPath')
const db = new LNDB(dataPath)
// 初始类型
const pg = db.init(folder)
let datas = pg.get(key).data
const foundIndex = datas.findIndex(obj => obj.id === id);
if (foundIndex !== -1) {
datas.splice(foundIndex,1)
}
pg.set(key,datas)
return datas
}
//此插件保存数据后的文件层级关系,如下图展示
// let data = window.electron.saveData({
// folder:'sampletype', //自定义
// key:'list', //自定义
// value:values //要保存的数据
// })