react antdPro umi 项目 使用electron打包为桌面端 *.exe 文件

一、首先需要先有一个能正常运行的基于umi的项目

项目可以是antdPro的模板项目,也可以是通过umi创建的项目,创建过程不再赘述,可以参考官网创建

二、接入electron

首先根目录下创建app文件夹,用于管理electron文件


image.png

1.项目根目录下的配置

(1)安装electron依赖
yarn add electron -D
yarn add concurrently -D
yarn add cross-env -D
yarn add wait-on -D
(2)package.json下配置
  • scripts 下新增electron启动命令,wait-on http://localhost:8000这个端口要跟前端运行的端口一致才能监听
"electron": "electron app/main.js", 
"electron-dev": "concurrently \"cross-env BROWSER=none yarn start:dev\" \"wait-on http://localhost:8000 && cross-env NODE_ENV=development electron app/main.js\"", 
"electron-start": "yarn build && cross-env NODE_ENV=production electron app/main.js"
  • 配置主文件入口
"main": "app/main.js",
  • 我的package.json,可以参考位置
{
  "name": "project",
  "version": "1.0.0",
  "private": true,
  "description": "goinfo V3版本",
  "author": "jack",
  "main": "app/main.js",
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "analyze": "cross-env ANALYZE=1 max build",
    "build": "max build",
    "build:packages": "yarn workspace goinfo-core build && yarn workspace goinfo-components build && yarn workspace goinfo-common-view build",
    "deploy": "npm run build && npm run gh-pages",
    "dev": "npm run start:dev",
    "electron": "electron app/main.js",
    "electron-dev": "concurrently \"cross-env BROWSER=none yarn start:dev\" \"wait-on http://localhost:8000 && cross-env NODE_ENV=development electron app/main.js\"",
    "electron-start": "yarn build && cross-env NODE_ENV=production electron app/main.js",
    "gh-pages": "gh-pages -d dist",
    "i18n-remove": "pro i18n-remove --locale=zh-CN --write",
    "postinstall": "max setup",
    "jest": "jest",
    "lint": "npm run lint:js && npm run lint:prettier && npm run tsc",
    "lint-staged": "lint-staged",
    "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
    "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src ",
    "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
    "lint:prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\" --end-of-line auto",
    "openapi": "max openapi",
    "prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\"",
    "preview": "npm run build && max preview --port 8000",
    "record": "cross-env NODE_ENV=development REACT_APP_ENV=test max record --scene=login",
    "serve": "umi-serve",
    "start": "cross-env UMI_ENV=dev max dev",
    "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev",
    "start:no-mock": "cross-env MOCK=none UMI_ENV=dev max dev",
    "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev",
    "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
    "test": "jest",
    "test:coverage": "npm run jest -- --coverage",
    "test:update": "npm run jest -- -u",
    "tsc": "tsc --noEmit"
  },
  "lint-staged": {
    "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
    "**/*.{js,jsx,tsx,ts,less,md,json}": [
      "prettier --write"
    ]
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 10"
  ],
  "dependencies": {
    "@ant-design/icons": "^4.8.0",
    "@ant-design/pro-components": "^2.3.57",
    "@ant-design/use-emotion-css": "1.0.4",
    "@umijs/route-utils": "^2.2.2",
    "antd": "^5.13.0",
    "classnames": "^2.3.2",
    "fs-extra": "^11.1.1",
    "glob": "^7.1.4",
    "goinfo-common-view": "^1.0.0",
    "goinfo-components": "^1.0.0",
    "goinfo-core": "^1.0.0",
    "handsontable": "^13.1.0",
    "lodash": "^4.17.21",
    "moment": "^2.29.4",
    "omit.js": "^2.0.2",
    "rc-menu": "^9.8.2",
    "rc-util": "^5.27.2",
    "react": "^18.2.0",
    "react-dev-inspector": "^1.8.4",
    "react-dom": "^18.2.0",
    "react-helmet-async": "^1.3.0",
    "sortablejs": "^1.15.0",
    "umi-request": "^1.4.0"
  },
  "devDependencies": {
    "@ant-design/pro-cli": "^2.1.5",
    "@testing-library/react": "^13.4.0",
    "@types/classnames": "^2.3.1",
    "@types/express": "^4.17.17",
    "@types/history": "^4.7.11",
    "@types/jest": "^29.4.0",
    "@types/lodash": "^4.14.191",
    "@types/react": "^18.0.28",
    "@types/react-dom": "^18.0.11",
    "@types/react-helmet": "^6.1.6",
    "@umijs/fabric": "^2.14.1",
    "@umijs/lint": "^4.0.52",
    "@umijs/max": "^4.0.52",
    "concurrently": "^9.0.1",
    "cross-env": "^7.0.3",
    "electron": "^32.1.2",
    "eslint": "^8.34.0",
    "express": "^4.18.2",
    "gh-pages": "^3.2.3",
    "jest": "^29.4.3",
    "jest-environment-jsdom": "^29.4.3",
    "lint-staged": "^10.5.4",
    "mockjs": "^1.1.0",
    "prettier": "^2.8.4",
    "swagger-ui-dist": "^4.15.5",
    "ts-node": "^10.9.1",
    "typescript": "^4.9.5",
    "umi-presets-pro": "^2.0.2",
    "wait-on": "^8.0.1"
  },
  "engines": {
    "node": ">=12.0.0"
  }
}

2.创建的app文件夹下的配置

(1)创建main.js主文件(electron主配置文件)
// electron打包配置
const {
  app,
  BrowserWindow,
  globalShortcut,
  // dialog
} = require('electron');
const path = require('path');

const isPro = process.env.NODE_ENV !== 'development';
const remote = require('@electron/remote/main');
remote.initialize();

// window对象的全局引用
let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({
    minWidth: 800, // 最小宽度
    minHeight: 600, // 最小高度
    width: 1000,
    heigth: 800,
    title: '标题',
    // autoHideMenuBar: true, // 关闭工具栏
    // frame: false, // 设置为false后为无边框窗口,即无法拖拽,拉伸窗体大小,没有菜单项
    icon: path.join(__dirname, './assets/logo.ico'),
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'), // 预加载文件
      // 是否启用Node integration
      nodeIntegration: true, // 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();
  });
  // 引入目录
  require(path.join(__dirname, 'menu.js'));

  if (isPro) {
    // 生产环境
    mainWindow.loadFile(`${__dirname}/build/index.html`);
    // mainWindow.loadURL(`http://192.168.10.15:30102`);
  } else {
    // 这边要对应前端dev运行起来的端口
    mainWindow.loadURL('http://localhost:8000/');
    // 打开开发者工具
    mainWindow.webContents.openDevTools();
  }

  // 解决应用启动白屏问题
  mainWindow.on('ready-to-show', () => {
    mainWindow.show();
    mainWindow.focus();
  });

  // // 关闭窗口弹框确认
  // mainWindow.on("close", (e) => {
  //   e.preventDefault();
  //   dialog.showMessageBox(mainWindow, {
  //     type: "warning",
  //     title: "关闭",
  //     message: "是否要关闭窗口",
  //     buttons: ["取消", "确定"],
  //   }).then((index) => {
  //     if (index.response === 1) {
  //       app.exit();
  //     }
  //   })
  // })

  // 关闭时触发下列事件
  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) => {
//   dialog.showOpenDialog({

//   })
//     // 询问用户是否退出
//   event.preventDefault() // 阻止本次退出
// })
(2)创建目录文件menu.js(桌面客户端运行时顶部的菜单配置)
const { app, Menu } = require('electron');

const isMac = process.platform === 'darwin';

const template = [
  // { role: 'appMenu' }  // 如果是mac系统才有
  ...(isMac
    ? [
        {
          label: app.name,
          submenu: [
            {
              role: 'about',
            },
            {
              type: 'separator',
            },
            {
              role: 'services',
            },
            {
              type: 'separator',
            },
            {
              role: 'hide',
            },
            {
              role: 'hideOthers',
            },
            {
              role: 'unhide',
            },
            {
              type: 'separator',
            },
            {
              role: 'quit',
            },
          ],
        },
      ]
    : []),
  // { role: 'fileMenu' }
  {
    label: '文件',
    submenu: [
      {
        label: '新建',
        accelerator: 'CmdOrCtrl+N',
        click: () => {},
      },
      {
        label: '打开',
        accelerator: 'CmdOrCtrl+O',
        click: () => {},
      },
      {
        label: '保存',
        accelerator: 'CmdOrCtrl+S',
        click: () => {},
      },
      {
        type: 'separator',
      }, // 分割线
      ,
      isMac
        ? {
            role: 'close',
          }
        : {
            role: 'quit',
            label: '退出',
          },
    ],
  },
  // { role: 'editMenu' }
  {
    label: '编辑',
    submenu: [
      {
        role: 'undo',
        label: '撤消',
      },
      {
        role: 'redo',
        label: '恢复',
      },
      {
        type: 'separator',
      },
      {
        role: 'cut',
        label: '剪切',
      },
      {
        role: 'copy',
        label: '复制',
      },
      {
        role: 'paste',
        label: '粘贴',
      },
      ...(isMac
        ? [
            {
              role: 'pasteAndMatchStyle',
            },
            {
              role: 'delete',
            },
            {
              role: 'selectAll',
            },
            {
              type: 'separator',
            },
            {
              label: 'Speech',
              submenu: [
                {
                  role: 'startSpeaking',
                },
                {
                  role: 'stopSpeaking',
                },
              ],
            },
          ]
        : [
            {
              role: 'delete',
              label: '删除',
            },
            {
              type: 'separator',
            },
            {
              role: 'selectAll',
              label: '全选',
            },
          ]),
    ],
  },
  // { role: 'viewMenu' }
  {
    label: '查看',
    submenu: [
      {
        role: 'reload',
        label: '重新加载',
      },
      {
        role: 'forceReload',
        label: '强制重新加载',
      },
      {
        role: 'toggleDevTools',
        label: '切换开发工具栏',
      },
      {
        type: 'separator',
      },
      {
        role: 'resetZoom',
        label: '原始开发工具栏窗口大小',
      },
      {
        role: 'zoomIn',
        label: '放大开发工具栏窗口',
      },
      {
        role: 'zoomOut',
        label: '缩小开发工具栏窗口',
      },
      {
        type: 'separator',
      },
      {
        role: 'togglefullscreen',
        label: '切换开发工具栏全屏',
      },
    ],
  },
  // { role: 'windowMenu' }
  {
    label: '窗口',
    submenu: [
      {
        role: 'minimize',
        label: '最小化',
      },
      ...(isMac
        ? [
            {
              type: 'separator',
            },
            {
              role: 'front',
            },
            {
              type: 'separator',
            },
            {
              role: 'window',
            },
          ]
        : [
            {
              role: 'close',
              label: '关闭',
            },
          ]),
    ],
  },
];

const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
(3)创建preload.js文件(监听浏览器事件)
window.addEventListener('DOMContentLoaded', () => {
  // const replaceText = (selector, text) => {
  //   const element = document.getElementById(selector)
  //   if (element) element.innerText = text
  // }
  // for (const dependency of ['chrome', 'node', 'electron']) {
  //   replaceText(`${dependency}-version`, process.versions[dependency])
  // }
});
(4)app文件夹下创建package.json(electron项目打包所需相关依赖)
  • 需要安装3个依赖
electron
electron-builder  // 用于打包electron项目
electron-reloader  // 用于编写electron时,代码保存可以热更新
  • scripts下配置打包命令 dist-win32 dist-win64 dist-mac
  • app文件夹下的package.json,直接复制到你的项目中,然后在app文件夹下执行yarn安装依赖
// package.json
{
  "name": "electron_project",
  "version": "0.0.1",
  "description": "description",
  "author": "jack",
  "main": "main.js",
  "scripts": {
    "dist-win32": "electron-builder --win --ia32", 
    "dist-win64": "electron-builder --win --x64",
    "dist-mac": " electron-builder --mac"
  },
  "dependencies": {
    "@electron/remote": "^2.0.10"
  },
  "devDependencies": {
    "electron": "^25.1.0",
    "electron-builder": "^23.6.0", // 用于打包
    "electron-reloader": "^1.2.3" // 用于编写electron时,代码保存可以热更新
  },
  "build": { // electron打包配置
    "productName": "electron_project",
    "appId": "com.example.app",
    "directories": {
      "output": "dist"
    },
    "files": [ // 打包时需要保存的文件
      "build/**/*",
      "main.js",
      "menu.js",
      "preload.js",
    ],
    "extraMetadata": {
      "main": "./main.js"
    },
    "win": {
      "icon": "./assets/logo.ico"
    },
    "mac": {
      "icon": "./assets/logo.ico"
    },
    "linux":{
      "icon": "./assets/logo.ico"
    }
  }
}

三、运行&打包

  • 打包正常的HTML静态页面还是在根目录下打包,这个不变
  • 打包exe安装包需在app文件夹下打包,并且静态的HTML资源需要放在build文件夹中,才能打进去

1.运行

  • 根目录下执行
yarn electron-dev

即可运行web端,web端启动后,会自动运行electron桌面端应用,并且支持热更新

2.打包流程

打包需要分两步:
(1)打包web端文件
在根目录执行

yarn build

(2)打包electron容器
把根目录打包后的web端文件放入app下的build文件夹中
再在app文件夹下运行

dist-win32/dist-win64 

打包后的exe最终会输出到app/dist文件夹下

四、打包后使用的问题

  1. baseURL接口地址的配置
    生产环境baseURL一般都是空,跟后端打包在一起使用的。但是打包成exe后,无法放在一起了,baseURL需要写成固定的
  2. publicPath静态资源地址的问题
    正常是publicPath: '/' 也就是默认在根目录访问
    如果项目部署在某个文件夹下,publicPath: '/project/' 代表在project文件夹中部署,那静态资源会去project文件夹中找
    但是打包成exe后,前端的位置就变成了安装路径,所以要用相对路径。 publicPath: './'

五、修改图标

1.修改窗口图标


image.png

2.更换Electron应用程序的桌面图标
可以是PNG格式或者ICO格式,ICO格式直接改后缀是不行的,打包会报错,必须通过转换工具转一下,并且大小不能超过256*256


image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容