在Electron中,我们想要创建特定的BrowserWindow,想要使用react组件作为BrowserWindow的内容,请问如何实现呢?

在Electron中,我们想要创建特定的BrowserWindow,想要使用react组件作为BrowserWindow的内容,请问如何实现呢?

代码目录结构为如下:

main/
  index.ts

renderer/src/
  windows/
    win1.tsx
    win2.tsx
    win3.tsx

在renderer目录下定义了多个特定的window,那么在创建BrowserWindow的时候,都是加载的html文件:
比如:mainWindow

  const mainWindow = new BrowserWindow({
    width: 900,
    height: 670,
    show: false,
    autoHideMenuBar: true,
    ...(process.platform === 'linux' ? { icon } : {}),
    webPreferences: {
      preload: join(__dirname, '../preload/index.js'),
      sandbox: false
    }
  })

1、请问应该如何加载React的组件比如:win1.tsx 为内容呢?
2、请问比如我有20个这样特定的window:
1)那么是否是应该在按钮点开window的时候才创建,关闭窗口的时候关闭window(实际回收)?
2)还是指的在项目创建的时候就创建好这些window,然后点击开启window就show,点击关闭就hide(不实际回收)?

阅读 1.3k
avatarAI BotBETA

解答

1. 如何加载 React 组件作为 BrowserWindow 的内容?

要在 Electron 中加载 React 组件作为 BrowserWindow 的内容,你可以使用 Create React App(CRA)或者类似工具来管理你的 React 项目,并使用 Electron 的 loadFileloadURL 方法来加载生成的 HTML 文件。这里假设你已经使用 CRA 构建了 React 应用,并且每个窗口的 React 组件都已经渲染到一个独立的 HTML 文件中。

对于你的目录结构,假设 renderer/src/windows/win1.tsx 渲染到一个名为 win1.html 的文件中(这通常通过配置 CRA 的输出路径实现)。你可以这样创建 BrowserWindow 并加载 React 组件:

const win1 = new BrowserWindow({
  width: 800,
  height: 600,
  webPreferences: {
    preload: join(__dirname, '../preload/index.js'),
    contextIsolation: true,  // 启用上下文隔离以提高安全性
    enableRemoteModule: false,  // 禁用远程模块
    nodeIntegration: false  // 禁用 Node 集成
  }
});

win1.loadFile('path/to/your/build/win1.html');  // 加载生成的 HTML 文件

确保 path/to/your/build/win1.html 是你构建的 React 应用中 win1.tsx 组件对应的 HTML 文件的正确路径。

2. 管理多个 BrowserWindow 实例

对于你有 20 个特定窗口的情况,这里有两种常见的策略:

1)延迟加载(Lazy Loading)

  • 优点:节省资源,只有在需要时才创建和加载窗口。
  • 实现:在按钮点击事件中创建 BrowserWindow 实例,并在窗口关闭时销毁它。
const createWin1 = () => {
  const win1 = new BrowserWindow({ /* 配置 */ });
  win1.loadFile('path/to/your/build/win1.html');
  win1.on('closed', () => {
    win1 = null;  // 清理引用
  });
};

// 在某个按钮点击事件中调用 createWin1

2)预先加载(Preload)

  • 优点:快速响应,窗口可以立即显示而无需等待加载。
  • 实现:在项目启动时创建所有窗口,但隐藏它们直到需要显示。
let win1 = null;

const initializeWindows = () => {
  win1 = new BrowserWindow({ show: false, /* 其他配置 */ });
  win1.loadFile('path/to/your/build/win1.html');
  win1.on('closed', () => {
    win1 = null;
  });
};

app.on('ready', initializeWindows);

// 显示窗口的函数
const showWin1 = () => {
  if (win1) {
    win1.show();
  } else {
    createWin1();  // 如果窗口被关闭了,可以重新创建
    win1.show();
  }
};

// 隐藏窗口的函数
const hideWin1 = () => {
  if (win1) {
    win1.hide();
  }
};

选择哪种策略取决于你的应用需求。如果你的窗口内容较大或者数量很多,延迟加载可能更合适。如果你的应用需要快速响应并且窗口内容加载时间较短,预先加载可能更好。

3 个回答

问题1

目录结构

project-root/
├── main/
│   ├── index.ts
│   └── preload/
│       └── index.js
├── renderer/
│   ├── src/
│   │   ├── main.tsx
│   │   ├── windows/
│   │   │   ├── win1.tsx
│   │   │   ├── win2.tsx
│   │   │   └── win3.tsx
│   └── index.html
├── dist/
│   └── bundle.js
├── package.json
└── vite.config.ts

安装依赖

npm install electron vite @vitejs/plugin-react react react-dom

Vite配置

vite.config.ts:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import * as path from 'path';

export default defineConfig({
  root: path.join(__dirname, 'renderer/src'), // 设置项目根目录
  build: {
    outDir: path.join(__dirname, 'dist'), // 输出目录
    rollupOptions: {
      input: path.join(__dirname, 'renderer/src/index.html') // 入口文件
    }
  },
  plugins: [react()] // 使用React插件
});

React组件

renderer/src/windows/win1.tsx:

import React from 'react';

const Win1: React.FC = () => {
  return <div>Hello from Win1</div>; // 定义一个简单的React组件
};

export default Win1;

renderer/src/windows/win2.tsx:

import React from 'react';

const Win2: React.FC = () => {
  return <div>Hello from Win2</div>; // 定义一个简单的React组件
};

export default Win2;

renderer/src/windows/win3.tsx:

import React from 'react';

const Win3: React.FC = () => {
  return <div>Hello from Win3</div>; // 定义一个简单的React组件
};

export default Win3;

渲染React组件

renderer/src/main.tsx:

import React, { lazy, Suspense } from 'react';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Route } from 'react-router-dom';

// 动态加载组件
const loadComponent = (componentName: string) => {
  return lazy(() => import(`./windows/${componentName}`));
};

const routes = [
  { path: '/win1', component: loadComponent('win1') },
  { path: '/win2', component: loadComponent('win2') },
  { path: '/win3', component: loadComponent('win3') },
  // 添加更多路由
];

const App: React.FC = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      {routes.map(route => (
        <Route key={route.path} path={route.path} component={route.component} />
      ))}
    </Suspense>
  </Router>
);

ReactDOM.render(<App />, document.getElementById('root')); // 渲染App组件到root元素

HTML文件

renderer/src/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Electron with React</title>
</head>
<body>
  <div id="root"></div> <!-- React组件将会挂载到这个div -->
  <script type="module" src="./main.tsx"></script> <!-- 引入打包后的JavaScript文件 -->
</body>
</html>

主进程代码

main/index.ts:

import { app, BrowserWindow } from 'electron';
import * as path from 'path';

const windows: { [key: string]: BrowserWindow | null } = {};

// 配置窗口信息
const windowConfigs = [
  { name: 'win1', route: '/win1' },
  { name: 'win2', route: '/win2' },
  { name: 'win3', route: '/win3' },
  // 添加更多窗口配置
];

function createWindow(name: string, route: string) {
  const win = new BrowserWindow({
    width: 900,
    height: 670,
    webPreferences: {
      preload: path.join(__dirname, 'preload/index.js'), // 使用path.join构建预加载脚本的路径
      contextIsolation: true,
      enableRemoteModule: false
    }
  });

  win.loadURL(`file://${path.join(__dirname, '../renderer/src/index.html')}#${route}`); // 加载带有路由的URL
  windows[name] = win;

  win.on('closed', () => {
    windows[name] = null;
  });
}

app.on('ready', () => {
  // 动态创建所有窗口
  windowConfigs.forEach(config => createWindow(config.name, config.route));
});

问题2

多窗口管理

通过预先创建窗口并隐藏(窗口池),提高响应速度和用户体验。

main/index.ts(修改后)

 import { app, BrowserWindow } from 'electron';
import * as path from 'path';

const windowPool: BrowserWindow[] = [];
const maxPoolSize = 5; // 设置窗口池的最大大小

function createWindowPool() {
  for (let i = 0; i < maxPoolSize; i++) {
    const win = new BrowserWindow({
      width: 900,
      height: 670,
      webPreferences: {
        preload: path.join(__dirname, 'preload/index.js'),
        contextIsolation: true,
        enableRemoteModule: false
      }
    });
    win.loadFile(path.join(__dirname, '../renderer/src/index.html'));
    win.hide();
    windowPool.push(win);
  }
}

function getWindowFromPool(route: string): BrowserWindow {
  const win = windowPool.find(w => !w.isVisible());
  if (win) {
    win.loadURL(`file://${path.join(__dirname, '../renderer/src/index.html')}#${route}`);
    win.show();
    return win;
  } else {
    const newWin = new BrowserWindow({
      width: 900,
      height: 670,
      webPreferences: {
        preload: path.join(__dirname, 'preload/index.js'),
        contextIsolation: true,
        enableRemoteModule: false
      }
    });
    newWin.loadURL(`file://${path.join(__dirname, '../renderer/src/index.html')}#${route}`);
    return newWin;
  }
}

app.on('ready', createWindowPool);

// 示例:在按钮点击事件中调用 getWindowFromPool
// const win = getWindowFromPool('/win1');

通过以上步骤,可以实现在Electron中加载React组件作为BrowserWindow的内容,并使用窗口池来优化多窗口管理,特别是当用户频繁切换窗口时。

补充

Webpack和Vite的对比

1. 构建速度

Webpack

  • 冷启动:Webpack的冷启动时间较长,尤其在大型项目中,需要解析和打包所有模块。
  • 热更新:热更新速度相对较慢,特别是模块依赖复杂时。

Vite

  • 冷启动:Vite的冷启动速度非常快,利用了原生的ES模块(ESM)和浏览器的能力,只在需要时进行打包。
  • 热更新:热更新速度极快,只重新编译和加载实际修改的模块。

2. 构建方式

Webpack

  • 打包一切:Webpack将所有资源视为模块并打包成一个或多个文件。
  • 配置复杂:Webpack的配置文件较复杂,需要手动配置各种加载器和插件。

Vite

  • 按需打包:Vite在开发模式下使用原生ES模块进行按需加载,生产模式下才进行打包。
  • 配置简单:Vite的配置文件相对简单,内置许多常用功能,减少了手动配置需求。

3. 开发体验

Webpack

  • 复杂性:Webpack的配置和插件系统功能强大,但也增加了学习曲线和配置复杂度。
  • 生态系统:Webpack有庞大的生态系统,几乎可以找到适用于任何需求的插件。

Vite

  • 简洁性:Vite的配置和使用体验更加简洁,适合快速开发和原型设计。
  • 现代化:Vite专注现代前端开发,默认支持TypeScript、JSX等现代特性。

4. 适用场景

Webpack

  • 适用于大型项目和需要复杂构建配置的场景。
  • 适合需要高度自定义和优化的项目。

Vite

  • 适用于中小型项目和需要快速开发的场景。
  • 适合现代前端开发,特别是使用Vue、React等框架的项目。

示例对比

Webpack配置示例

const path = require('path');

module.exports = {
  entry: './renderer/src/index.tsx',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  },
  target: 'electron-renderer'
};

Vite配置示例

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import * as path from 'path';

export default defineConfig({
  root: path.join(__dirname, 'renderer/src'),
  build: {
    outDir: path.join(__dirname, 'dist'),
    rollupOptions: {
      input: path.join(__dirname, 'renderer/src/index.html')
    }
  },
  plugins: [react()]
});

总结

Webpack

  • 适合需要复杂配置和高度自定义的项目,特别是大型项目。

Vite

  • 适合快速开发和现代前端开发,特别是中小型项目。

不是,难道就没有路由的概念吗?React-router是摆设不用?

这个问题,可以从两部分去理解思考解决办法。

第一:

Electron 的browserwindow 加载的是什么?
根据官方 API 主要分为2类,
第一类: loadURl,这个可以是远程也可以是本地。为了区分简单,我们可以从http 协议的远程地址来理解。 比如, https://sf.gg 这种。
第二类: loadFile, 通过api 名称我们不难理解,这个是本地文件。 比如,你的静态html .
也就是说, browserwindow, 只具备 加载url 或者加载 html File的能力。

第二:
我们的React 组件, 是能直接在浏览器运行的吗? 不能,所以我们需要经过, webpack 或者vite , 进行打包编译成本地,SPA 的bundle.js 和我们的index.html 入口文件。

第三步:
当我们拿到了,打包后的 SPA 产物。 我们有2种方式和我们的Electron 进行集成结合。 第一种, 我们直接部署拿到URL 通过,具体的页面地址 ,looadUrl即可。
第二种, 使用loadFile 和 hash 路由。

   const window = new BrowserWindow({
    width: 600,
    height: 500,
    frame: false,
    webPreferences: {
      webviewTag: true,
      webSecurity: false,
      contextIsolation: false,
      enableRemoteModule: true,
      nodeIntegration: true,
      preload: path.join(__dirname, "preload.js"),
    },
  });
  Window.loadFile("dist/index.html", {
    hash: "#/login",
  });

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏