electron + react 开发软件遇到的问题与解决方法(更新于2024.03.01)

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  //要保存的数据
//    })

在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值