# my-umijs-web-electron **Repository Path**: sn_yang/my-umijs-web-electron ## Basic Information - **Project Name**: my-umijs-web-electron - **Description**: UMIJS + Web + Electron - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-02-22 - **Last Updated**: 2025-03-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 结合 umijs + electron 实现一个简单的 Web + Electron 双应用 这是一个非常简洁的实现,除了配置 `package.json` 文件外, 只要增加两个 js 文件,即可实现。 具有以下功能: - 支持构建成一个 web 应用。 - 支持构建成一个 Electron 应用。 - 支持热更新。 - 支持开发环境 以下方案,在 Linux 下测试通过。 源码:[gitee](https://gitee.com/sn_yang/my-umijs-web-electron) ## 准备环境 ```sh # 设置 npm 镜像 pnpm config set ELECTRON_MIRROR https://npmmirror.com/mirrors/electron/ pnpm config set registry https://registry.npmmirror.com ``` ### 创建 Umi 项目 ```sh # 创建 umi 项目 pnpm dlx create-umi@latest # 进入项目目录 cd # 启动 umi 项目 pnpm dev ``` ### 安装 electron ```sh # 增强命令行 pnpm install -D concurrently cross-env wait-on # 安装 electron pnpm install -D electron # 支持 electron 构建 pnpm install -D electron-builder # 支持 electron 热更新 pnpm install -D electron-reloader # 支持 electron 加载 web 内容 pnpm install @electron/remote # 修复 electron 安装问题 pushd node_modules/electron node install.js popd ``` ## 结合 electron 和 umi ### 技术说明 umijs 本身会生成一个 Web 服务。一般情况下,我们可以使用 HTTP URL 访问这个服务。 那么,在结合 electron 的时候,生产环境中需要: 1. 启动一个 HTTP 服务,加载 umi 项目的构建结果。 2. 在 electron 的 main.js 中, 调用这个服务即可。 在开发环境中: 1. 使用 `pnpm dev` 启动项目。 2. 然后启动 electron,在 main.js 中,调用项目的 HTTP 服务。 3. 注意,在开发环境中,electron 需要支持热更新。 ### 实现 #### 配置 `package.json` ```json "scripts": { { "name": "my-umijs-electron", "version": "0.0.1", "description": "my umijs electron", ..., "electron": "pnpm build && electron app/main.js", "electron-dev": "concurrently \"cross-env BROWSER=none pnpm dev\" \"wait-on http://localhost:8000 && cross-env NODE_ENV=development electron app/main.js\"", "electron-build": "pnpm build && electron-builder build && cp -r dist dist-app/linux/x64" }, "build": { "directories": { "output": "dist-app/${platform}/${arch}" } } } ``` 说明: - 命令 `electron`,是启动 electron 的应用(类似于生产环境模式)。 - 构建 umijs 产品到 `dist` 下; - 启动 electron 产品(electron 的 `app/www.js` 将启动 `$cwd/dist`)。 - 命令 `electron-dev`,是开发环境模式下启动 electron 的。 - 启动 umijs 应用 - 等待 umijs 应用启动完毕,再启动 electron 应用。 - 命令 `electron-build`: - 构建 umijs 产品到 `dist` 下; - 构建 electron 产品到 `dist-app/${platform}/${arch}` 下。 - 复制 `dist` 到 `dist-app/${platform}/${arch}`。 `build`的配置,是指定`electron-build`的输出目录位置到 `dist-app/${platform}/${arch}`。 - 开发模式下的 Electron 应用 ![开发模式下的 Electron 应用](screenshots/electron-dev.png) #### 实现 HTTP 服务 - `app/www.js`: 本地启动 umi 项目的 HTTP 服务 ```js const http = require('http'); const url = require('url'); const path = require('path'); const fs = require('fs'); let port = process.argv[2] || 3000; let server = http .createServer(function (request, response) { let uri = url.parse(request.url).pathname; let filename = path.join(process.cwd(), 'dist', uri); let contentTypesByExtension = { '.html': 'text/html', '.css': 'text/css', '.js': 'text/javascript', }; fs.exists(filename, function (exists) { if (!exists) { response.writeHead(404, { 'Content-Type': 'text/plain' }); response.write('404 Not Found\n'); response.end(); return; } if (fs.statSync(filename).isDirectory()) filename += '/index.html'; fs.readFile(filename, 'binary', function (err, file) { if (err) { response.writeHead(500, { 'Content-Type': 'text/plain' }); response.write(err + '\n'); response.end(); return; } let headers = {}; let contentType = contentTypesByExtension[path.extname(filename)]; if (contentType) headers['Content-Type'] = contentType; response.writeHead(200, headers); response.write(file, 'binary'); response.end(); }); }); }) .listen(parseInt(port, 10)); /** * Event listener for HTTP server "error" event. */ function onError(error) { if (error.syscall !== 'listen') { throw error; } let bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; // handle specific listen errors with friendly messages switch (error.code) { case 'EACCES': console.error(bind + ' requires elevated privileges'); process.exit(1); break; case 'EADDRINUSE': console.error(bind + ' is already in use'); process.exit(1); break; default: throw error; } } /** * Event listener for HTTP server "listening" event. */ function onListening() { let addr = server.address(); let bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; debug('Listening on ' + bind); } const startServer = () => { /** * Listen on provided port, on all network interfaces. */ // server.listen(port); server.on('error', onError); server.on('listening', onListening); }; // 暴露一个函数,用于启动服务 module.exports.startServer = startServer; ``` - `app/main.js` 创建一个 electron 窗口。 ```js // electron打包配置 const { app, BrowserWindow, globalShortcut, // dialog } = require('electron'); const path = require('path'); const { startServer } = require('./www'); const isPro = process.env.NODE_ENV !== 'development'; const remote = require('@electron/remote/main'); remote.initialize(); // window对象的全局引用 let mainWindow; function createWindow() { startServer(); mainWindow = new BrowserWindow({ minWidth: 800, // 最小宽度 minHeight: 600, // 最小高度 // width: 1000, // height: 800, title: '我的 Electron App', // autoHideMenuBar: true, // 关闭工具栏 // frame: false, // 设置为false后为无边框窗口,即无法拖拽,拉伸窗体大小,没有菜单项 icon: path.join(__dirname, './assets/logo.ico'), webPreferences: { // preload: path.join(__dirname, 'preload.js'), // 预加载文件 // 是否启用Node integration nodeIntegration: false, // Electron 5.0.0 版本之后它将被默认false // 是否在独立 JavaScript 环境中运行 Electron API和指定的preload 脚本.默认为 true contextIsolation: false, // Electron 12 版本之后它将被默认true }, }); remote.enable(mainWindow.webContents); // 注册快捷键 globalShortcut.register('CommandOrControl+M', () => { mainWindow.maximize(); }); globalShortcut.register('CommandOrControl+T', () => { mainWindow.unmaximize(); }); globalShortcut.register('CommandOrControl+H', () => { mainWindow.close(); }); if (isPro) { // 生产环境 mainWindow.loadURL('http://localhost:3000/'); } else { mainWindow.loadURL('http://localhost:8000/'); // 打开开发者工具 mainWindow.webContents.openDevTools(); } // 解决应用启动白屏问题 mainWindow.on('ready-to-show', () => { mainWindow.show(); mainWindow.focus(); }); // 关闭窗口弹框确认 mainWindow.on('close', (e) => {}); // 关闭时触发下列事件 mainWindow.on('closed', () => { mainWindow = null; }); } // 是否允许打开多个窗口 const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { // 检测到本次未取得锁,即有已存在的实例在运行,则本次启动立即退出,不重复启动。 app.quit(); } else { app.on('second-instance', (event, commandLine, workingDirectory) => { // 监听到第二个实例被启动时,检测当前实例的主窗口,并显示出来取得焦点 if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.focus(); } }); } app.on('ready', createWindow); // 热加载 try { require('electron-reloader')(module, {}); } catch (_) {} // 所有窗口关闭时退出应用 app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { if (mainWindow === null) { createWindow(); } }); app.on('before-quit', (event) => {}); ``` - 源码结构 ![源码结构](screenshots/src-structure.png) ## 命令 - 本地启动 umi Web 服务(开发环境) ```sh pnpm dev ``` - 启动 electron 应用(开发环境) ```sh pnpm electron-dev ``` - 启动 electron 应用(生产环境) ```sh pnpm electron ``` - 构建 electron 应用 运行 ```sh pnpm electron-build ``` - 构建结果: Electron 应用 ![构建结果: Electron 应用](screenshots/electron-app.png) ## 结合 umijs 和 Tauri - 安装 rust - 安装 tauri ```sh sudo apt update sudo apt install libwebkit2gtk-4.1-dev \ build-essential \ curl \ wget \ file \ libxdo-dev \ libssl-dev \ libayatana-appindicator3-dev \ librsvg2-dev # 使用 Tauri CLI 手动创建 # 安装 tauri 命令行 pnpm add -D @tauri-apps/cli@latest # 初始化 tauri 项目(创建一个名为 src-tauri 的子目录) pnpm tauri init ✔ What is your app name? · my-umijs-electron ✔ What should the window title be? · my-umijs-electron ✔ Where are your web assets (HTML/CSS/JS) located, relative to the "/src-tauri/tauri.conf.json" file that will be created? · ../dist ✔ What is the url of your dev server? · http://localhost:8000 ✔ What is your frontend dev command? · pnpm dev ✔ What is your frontend build command? · pnpm build # 运行 pnpm tauri dev ``` - Tauri 运行界面 ![Tauri 运行界面](screenshots/tauri-app.png) ## 结合 umijs 和 rust actixweb `actixweb` 是 rust 的一个 webframework。 - 创建项目 ```sh cargo init src-server cd src-server cargo add actix-web cargo add actix-cors ``` ```rust # src-server/src/main.rs use actix_cors::Cors; use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder}; #[get("/")] async fn hello() -> impl Responder { HttpResponse::Ok().body("Hello world!") } #[get("/admin")] async fn admin() -> impl Responder { HttpResponse::Ok().body("Shawn") } #[post("/echo")] async fn echo(req_body: String) -> impl Responder { HttpResponse::Ok().body(req_body) } async fn manual_hello() -> impl Responder { HttpResponse::Ok().body("Hey there!") } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { let cors = Cors::permissive(); App::new() .wrap(cors) .service(hello) .service(admin) .service(echo) .route("/hey", web::get().to(manual_hello)) }) .bind(("127.0.0.1", 8080))? .run() .await } ``` - 配置 umijs 的 proxy ```ts // config/proxy.ts const devUrl = 'http://localhost:8080'; export default { dev: { '/api/': { target: devUrl, changeOrigin: true, pathRewrite: { '^/api': '' }, }, }, }; ``` ```ts // .umirc.ts import proxy from './config/proxy'; export default defineConfig({ proxy: proxy['dev'], ... }); ``` - 运行 ```sh cd src-server cargo run # 另起一个终端 pnpm dev ``` ## 参照 - `@umijs/max` 模板项目,更多功能参考 [Umi Max 简介](https://umijs.org/docs/max/introduce) - [umi+electron 开始一个桌面应用](https://juejin.cn/post/7062181732442701861) - [Using Node.js as a simple web server](https://stackoverflow.com/questions/6084360/using-node-js-as-a-simple-web-server) - [Tauri 中文文档](https://v2.tauri.app/zh-cn/start/)