版本说明
- node:18.17.1
- Electron:26.2.1
- Electron Forge:6.4.2
- react:18.2.0
- react-router-dom:6.17.0
- antd:5.10.1
- ahooks:3.7.8
- dexie:3.2.4
- dexie-react-hooks:1.1.6
- zustand:4.4.3
- react-refresh:0.14.0
- @pmmmwh/react-refresh-webpack-plugin:0.5.11
- react-activation:0.12.4
使用 Electron Forge 工具安装 Electron 工程模板
详情请见:Webpack + Typescript - Electron Forge
// yarn
yarn create electron-app my-new-app --template=webpack-typescript
// npm
npm init electron-app@latest my-new-app -- --template=webpack-typescript
项目目录预览
项目启动预览
主要目录文件说明
-
/src
源码目录,包含主进程、渲染进程的源代码
-
/src/index.ts
项目入口文件,也是主进程文件
-
/src/renderer.ts
渲染进程入口文件
-
/src/preload.ts
预加载脚本,在这里可以在页面渲染之前做一些事
-
/forge.config.ts
Electron Forge 配置文件,具体配置信息查看官方文档
-
/webpack.main.config.ts
webpack 主入口文件
-
/webpack.plugins.ts
webpack 插件
-
/webpack.renderer.config.ts
webpack 配置渲染进程相关的配置项,如:配置 sass-loader...
-
/webpack.rules.ts
webpack 规则文件
-
/tsconfig.json
typescript 配置文件
主进程
- /src/index.ts
1、通过 BrowserWindow
创建窗口
const mainWindow = new BrowserWindow({
height: 600,
width: 800,
webPreferences: {
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
},
});
mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
内容安全策略 CSP
内容安全策略(CSP) 是应对跨站脚本攻击和数据注入攻击的又一层保护措施。 官方建议任何载入到 Electron 的站点都要开启
详情请见:[Content Security Policy(内容安全策略)](安全 | Electron (electronjs.org))
// csp
app.whenReady().then(async () => {
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [`connect-src 'self' http://localhost:4000`],
},
});
});
});
进程通信
通信方式主要有下面三种:
详情请见:进程间通信 | Electron (electronjs.org)
-
(单向)渲染进程 ---> 主进程
import { ipcMain, ipcRenderer } from 'electron'; // 主进程,使用ipcMain.on监听事件 ipcMain.on(事件名, 事件处理函数); // 渲染进程,使用ipcRenderer.send发送数据 ipcRenderer.send(事件名, 数据);
-
(双向)渲染进程 ---> 主进程
import { ipcMain, ipcRenderer } from 'electron'; // 主进程,使用ipcMain.handle监听事件 ipcMain.handle(事件名, 事件处理函数); // 渲染进程,使用ipcRenderer.invoke调用并发送数据 ipcRenderer.invoke(事件名, 数据); // ipcRenderer.invoke返回一个promise,接收主进程的回复,例: ipcMain.handle('event-name', () => { // 回复渲染进程(调用者) return 123; }); const data = await ipcRenderer.invoke('event-name', 数据); console.log(data); // 123
-
主进程 ---> 渲染进程
// 主进程使用webContents.send发送数据 mainWindow.webContents.send(事件名, 数据); // 渲染进程使用ipcRenderer.on监听事件 ipcRenderer.on(事件名, 事件处理函数);
主进程和渲染进程之间的通信需要通过预渲染进程实现
// /src/preload.ts
import { contextBridge } from 'electron';
// ELECTRON属性会添加到window对象上
contextBridge.exposeInMainWorld('ELECTRON', {
getName: 'hp'
// 获取窗口是否处于最大化
getMaximized: async () => await ipcRenderer.invoke('method:maximized'),
});
// 主进程
// 将最大化状态传递到渲染进程,mainWindow是BrowserWindow的实例对象
ipcMain.handle('method:maximized', () => mainWindow.isMaximized());
// 渲染进程
const name = window.ELECTRON.getName() // hp
const isMaximized = await window.ELECTRON.getMaximized() // true or false
使用 node 模块
electron
默认关闭 node
集成,如需使用需要手动开启,修改文件 /src/index.ts
const createWindow = (): void => {
// Create the browser window.
const mainWindow = new BrowserWindow({
...
webPreferences: {
+ nodeIntegration: true,
},
});
};
然后可以在预渲染进程中使用 node模块
,出于安全考虑,渲染进程使用 node模块
需要通过预渲染进程配合 进程通信
实现
// /src/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('ELECTRON', {
// 暴露整个fs
fs: window.require('fs')
// 暴露整个fs(Promises版)
fsPromises: window.require('fs/promises')
// 只暴露fs.readFile
readFile: window.require('fs').readFile
});
// 渲染进程
window.ELECTRON.readFile(path,options,callback)
渲染进程
ReactDOM.createRoot(document.getElementById('app')).render(
<ConfigProvider
locale={zhCN}
theme={{
token: {
borderRadius: 2,
},
components: {
Tabs: {
inkBarColor: 'var(--music-color)',
itemSelectedColor: 'var(--music-color)',
itemHoverColor: 'rgba(0, 0, 0, 0.88)',
},
},
}}
componentSize="small"
>
<RouterProvider router={router} future={{ v7_startTransition: true }} />
</ConfigProvider>,
);
渲染进程默认是 .ts
文件,为了方便使用,可以修改为 .tsx
文件,同时需要修改 /forge.config.ts
配置文件
const config: ForgeConfig = {
...
plugins: [
new WebpackPlugin({
renderer: {
entryPoints: [
{
- js: './src/renderer.ts',
+ js: './src/renderer.tsx',
}
]
}
})
]
}
配置路由
import { RouterProvider } from 'react-router-dom';
import ReactDOM from 'react-dom/client';
import router from 'src/router';
ReactDOM.createRoot(document.getElementById('app')).render(
<RouterProvider router={router} future={{ v7_startTransition: true }} />,
);
注意:
-
路由模式如果使用的是
history
,需要添加路由前缀import { routes } from './routes'; const router = createBrowserRouter(routes, { basename: '/main_window' });
-
这个路由前缀是在
/forge.config.ts
配置文件中配置的,默认是 main_windowconst config: ForgeConfig = { ... plugins: [ new WebpackPlugin({ renderer: { entryPoints: [ { name: 'main_window', } ] } }) ] }
解决文件修改后,刷新整个项目的问题
-
安装依赖
yarn add -D @pmmmwh/react-refresh-webpack-plugin react-refresh
-
修改配置文件
/forge.config.ts
const config: ForgeConfig = { ... plugins: [ new WebpackPlugin({ ... + devServer: { + liveReload: false, + } }) ] }
-
修改配置文件 webpack.plugins.ts
+ import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; export const plugins = [ ... + new ReactRefreshWebpackPlugin(), ];
-
然后重启项目即可
添加 KeepAlive 缓存
-
安装依赖
yarn add react-activation
-
使用
import KeepAlive, { AliveScope } from 'react-activation'; import { Outlet, useLocation } from 'react-router-dom'; export default function LayoutBasic() { ... const location = useLocation(); return ( <AliveScope> ... <KeepAlive id={location.pathname + location.search + location.hash} style={{ height: '100%' }}> <Outlet /> </KeepAlive> </AliveScope> ) }
使用数据仓库
-
详情请见:zustand
-
安装
zustand
yarn add zustand
-
使用非常简单
import { create, StateCreator } from 'zustand'; import { persist } from 'zustand/middleware'; type State = { music: any; updateMusic: (value: any) => void; }; const stateCreator: StateCreator<State> = (set, get) => ({ music: null, updateMusic: (value) => set({ music: value }), }); // 持久化 const persistStore = persist(stateCreator, { name: 'playingStore' }); export default create(persistStore);
组件中使用
import store from './store' export default function MyComponent() { const [music] = store(state => [state.music]) ... }
使用 IndexedDB
-
原生的
IndexedDB
操作比较繁琐,这里借助一个插件:dexie
yarn add dexie dexie-react-hooks
-
初始化
-
方式 1
import Dexie from 'dexie'; export const db = new Dexie('myDatabase'); db.version(1).stores({ friends: '++id, name, age', // Primary key and indexed props });
-
方式 2
import Dexie, { Table } from 'dexie'; export interface Friend { id?: number; name: string; age: number; } export class MySubClassedDexie extends Dexie { // 'friends' is added by dexie when declaring the stores() // We just tell the typing system this is the case friends!: Table<Friend>; constructor() { super('myDatabase'); this.version(1).stores({ friends: '++id, name, age', // Primary key and indexed props }); } } export const db = new MySubClassedDexie();
-
-
使用
liveQuery
订阅数据变化import { liveQuery } from 'dexie' export class MySubClassedDexie extends Dexie { ... constructor() { ... } private subscribeLikeMusics() { // 监听 喜欢的音乐 数据变化,更新 likeStore、playStore 中的数据 const likeMusicsObservable = liveQuery(() => this.likeMusics.toArray()); likeMusicsObservable.subscribe({ next: (value) => { const ids = value.map((item) => item.id); myFavoriteStore.setState({ musicIds: ids }); }, }); } }
-
组件内使用
useLiveQuery
监听数据变化,比liveQuery
方便一些import { useLiveQuery } from 'dexie-react-hooks'; import dexieIndexedDB from 'src/store/dexieIndexedDB'; export default function MyComponent() { const musics = useLiveQuery(() => dexieIndexedDB.likeMusics.toArray()); ... }
安装开发者工具
-
**React Developer Tools,Redux DevTools **
// /src/index.ts import { app, session } from 'electron'; import path from 'path'; import os from 'os'; app.whenReady().then(async () => { const reactDevToolsPath = path.join( os.homedir(), `/AppData/Local/Google/Chrome/User Data/Default/Extensions/fmkadmapgofadopljbjfkapdkoienihi/4.28.5_0`, ); const reduxDevToolsPath = path.join( os.homedir(), `/AppData/Local/Google/Chrome/User Data/Default/Extensions/lmhkpmbekcpmknklioeibfkpmmfibljd/3.1.3_0`, ); await session.defaultSession.loadExtension(reactDevToolsPath); await session.defaultSession.loadExtension(reduxDevToolsPath); });
-
注意:对于不同的 electron 版本,开发者工具的 manifest_version 版本会影响扩展的正常加载(当前文档中的版本可以正常加载)
manifest_version:表示谷歌浏览器插件接口版本,最新版本是 3
具体参考:Electron(v26.2.1)无法加载 React Developer Tools(v4.28.0) - 掘金 (juejin.cn)