electron pc项目打包成桌面端

目录

0正常步骤

1如果下时间比较久卡着了-①换国内镜像②项目纯英文路径

1

2

[2问题英文路径打包不了,git bash复制换项目位置](#2问题英文路径打包不了,git bash复制换项目位置)

成功\release\文件里面找exe

​编辑3问题安装好了打开是白屏,打包路径问题

[解决electron/main.cjs 里加了前缀剥离](#解决electron/main.cjs 里加了前缀剥离)

0正常步骤
  1. 装依赖

    pnpm add -D electron electron-builder
    电子-建造者

  2. 本地调试(两个终端)

    pnpm dev # 终端1:vite dev server
    pnpm dev:electron # 终端2:electron 加载 http://localhost:5173/admin/

  3. 打包 exe

    pnpm build:win

1如果下时间比较久卡着了-①换国内镜像②项目纯英文路径
1
复制代码
"build:win": "pnpm build && cross-env ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ electron-builder --win --x64"
2
  1. 装 cross-env

    pnpm add -D cross-env

  2. 重新打包

    pnpm build:win

2问题英文路径打包不了,git bash复制换项目位置
复制代码
mkdir -p F:/work/ticket-admin
cp -r "F:/vue3/公司/排号系统/ticket-machine-admin/"{src,electron,public,package.json,pnpm-lock.yaml,tsconfig.json,tsconfig.app.json,tsconfig.node.json,vite.config.ts,index.html,.env.production,.env.development} F:/work/ticket-admin/
ls F:/work/ticket-admin
成功\release\文件里面找exe

rm -rf release

3问题安装好了打开是白屏,打包路径问题

vite 打包后 index.html 里引用的是 /admin/assets/index-xxx.js,但文件实际在 dist/assets/。我原来的服务逻辑直接 path.join(rootDir, '/admin/assets/xxx.js') → 找 dist/admin/assets/,不存在 → 回退返回 index.html。

解决electron/main.cjs 里加剥离前缀,把 URL 映射回真实的 dist 结构
复制代码
/**
 * 排号机 Electron 主进程
 *
 * 设计要点:
 * 1. 不改渲染层。现有 base='/admin/'、createBrowserRouter、相对 API('/basic-api') 全部保持原样。
 *    Electron 下用 http://168.3.0.1:<port>/admin/ 加载,避免 file:// 协议下路由 404 与 base 失效。
 * 2. 主进程内置静态服务托管 dist,并复刻 vite dev server 的 proxy 规则(/basic-api -> 后端)。
 *    这样 request.ts 和 .env.production 无需任何改动,也不会引入跨域。
 * 3. 设备参数(deviceCode / branchId)从 config.json 读,拼到 URL query 上,
 *    与浏览器里 ?deviceCode=xx 的入参方式保持一致。
 */
const { app, BrowserWindow, dialog } = require('electron')
const http = require('http')
const fs = require('fs')
const path = require('path')

const isDev = !app.isPackaged

const DEV_SERVER = 'http://localhost:5173' // vite dev server
const STATIC_PORT = 18231 // 生产内置静态服务端口
const WEB_PREFIX = '/admin'
const API_PREFIX = '/basic-api'
// 必须与 vite.config.ts 中的 proxy.target 保持一致
const API_TARGET = { host: '10.1.68.35', port: 5721 }

const MIME = {
  '.html': 'text/html; charset=utf-8',
  '.js': 'text/javascript; charset=utf-8',
  '.css': 'text/css; charset=utf-8',
  '.json': 'application/json; charset=utf-8',
  '.svg': 'image/svg+xml',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.gif': 'image/gif',
  '.ico': 'image/x-icon',
  '.woff2': 'font/woff2',
  '.ttf': 'font/ttf',
}

let mainWindow = null
let staticServer = null

/** 读取设备配置:优先 exe 同级 config.json(方便现场运维改),其次 userData */
function readDeviceConfig() {
  const candidates = isDev
    ? [path.join(app.getPath('userData'), 'config.json')]
    : [
        path.join(path.dirname(process.execPath), 'config.json'),
        path.join(app.getPath('userData'), 'config.json'),
      ]

  for (const file of candidates) {
    try {
      return JSON.parse(fs.readFileSync(file, 'utf-8'))
    } catch (_) {
      /* 继续尝试下一个候选路径 */
    }
  }
  return {}
}

/** 把 /basic-api/xxx 转发到后端,路径前缀剥离逻辑与 vite rewrite 一致 */
function proxyApi(req, res) {
  const upstreamPath = req.url.replace(/^\/basic-api/, '') || '/'
  const proxyReq = http.request(
    {
      host: API_TARGET.host,
      port: API_TARGET.port,
      path: upstreamPath,
      method: req.method,
      headers: { ...req.headers, host: `${API_TARGET.host}:${API_TARGET.port}` },
    },
    (proxyRes) => {
      res.writeHead(proxyRes.statusCode || 502, proxyRes.headers)
      proxyRes.pipe(res)
    }
  )

  proxyReq.on('error', (err) => {
    res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' })
    res.end(JSON.stringify({ code: 500, message: `后端服务不可达: ${err.message}` }))
  })

  req.pipe(proxyReq)
}

/** 托管 dist:/admin/* 映射到 dist/*,其余路径回退到 index.html(SPA 路由) */
function startStaticServer(rootDir) {
  staticServer = http.createServer((req, res) => {
    if (req.url.startsWith(API_PREFIX)) {
      proxyApi(req, res)
      return
    }

    const urlPath = decodeURIComponent(req.url.split('?')[0])

    // vite base='/admin/' 会让产物引用 /admin/assets/*,但文件实际落在 dist/assets/*
    // 这里剥离前缀,把 URL 映射回真实的 dist 结构
    let relativePath = urlPath
    if (relativePath === WEB_PREFIX || relativePath === `${WEB_PREFIX}/`) {
      relativePath = '/'
    } else if (relativePath.startsWith(`${WEB_PREFIX}/`)) {
      relativePath = relativePath.slice(WEB_PREFIX.length)
    }

    let filePath = path.join(rootDir, relativePath)

    // 目录穿越防护
    if (!filePath.startsWith(rootDir)) {
      res.writeHead(403)
      res.end('Forbidden')
      return
    }

    const isDir = fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()
    // 入口页与 SPA 子路由都回退到 index.html,保证前端路由能正常接管
    if (relativePath === '/' || isDir || !fs.existsSync(filePath)) {
      filePath = path.join(rootDir, 'index.html')
    }

    fs.readFile(filePath, (err, data) => {
      if (err) {
        res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' })
        res.end('Not Found')
        return
      }
      res.writeHead(200, { 'Content-Type': MIME[path.extname(filePath).toLowerCase()] || 'application/octet-stream' })
      res.end(data)
    })
  })

  staticServer.listen(STATIC_PORT, '127.0.0.1', () => {
    createWindow()
  })
}

function buildEntryUrl() {
  const cfg = readDeviceConfig()
  const query = new URLSearchParams({
    deviceCode: cfg.deviceCode || '',
    branchId: cfg.branchId || '',
  }).toString()

  const origin = isDev ? DEV_SERVER : `http://168.1.0.1:${STATIC_PORT}`
  return `${origin}${WEB_PREFIX}/?${query}`
}

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1280,
    height: 800,
    show: false,
    frame: false, // 自助机无边框
    fullscreen: true,
    autoHideMenuBar: true,
    backgroundColor: '#ffffff',
    webPreferences: {
      preload: path.join(__dirname, 'preload.cjs'),
      contextIsolation: true,
      nodeIntegration: false,
      webSecurity: true,
    },
  })

  mainWindow.loadURL(buildEntryUrl())

  mainWindow.once('ready-to-show', () => mainWindow.show())

  // 自助机场景:屏蔽快捷键退出,防止现场人员误操作
  mainWindow.webContents.on('before-input-event', (event, input) => {
    if (input.key === 'F12' || (input.control && input.key === 'r')) event.preventDefault()
  })
}

// 单实例:设备重复启动会导致本地静态服务端口冲突
if (!app.requestSingleInstanceLock()) {
  app.quit()
} else {
  app.on('second-instance', () => {
    if (mainWindow) {
      if (mainWindow.isMinimized()) mainWindow.restore()
      mainWindow.focus()
    }
  })

  app.whenReady().then(() => {
    if (isDev) {
      createWindow() // 开发态直接连 vite dev server
    } else {
      const rootDir = path.join(__dirname, '..', 'dist')
      if (!fs.existsSync(path.join(rootDir, 'index.html'))) {
        dialog.showErrorBox('启动失败', `未找到前端构建产物: ${rootDir}`)
        app.quit()
        return
      }
      startStaticServer(rootDir)
    }

    app.on('activate', () => {
      if (BrowserWindow.getAllWindows().length === 0) createWindow()
    })
  })
}

app.on('window-all-closed', () => app.quit())

相关推荐
前端大斗师1 小时前
「大于 1000」把 1000 元那单也算进去了:我在 Vue3 订单页对了 10 句话
前端·人工智能·typescript·大模型·原力计划
Jmyd01232 小时前
用 Web VR 引擎搭建 3D 虚拟展厅:从素材上云到多端实时渲染的完整数据链路
前端·3d·vr·三维数字化
刘天远2 小时前
Agent成本核算实现:事件表、状态分布与Python归集
前端·数据库·人工智能·python
欣欣之王来了3 小时前
开发环境准备:Node.js、npm、VS Code安装配置
前端·学习·架构·项目·vue3教程
xiaolinudao1233 小时前
将多个 Excel 表格中的数据合并到单个表中|6 种实现方案全解析
java·前端·excel
LRL_3 小时前
跨平台 Node 模块安装指南:如何利用 pnpm/npm 配置`supportedArchitectures` 锁定平台依赖
前端·npm·node.js
EatFan4 小时前
React 项目踩坑实录:useEffect 闭包陷阱、Context 性能陷阱与 React 18 升级避坑排查手册
前端·javascript·react.js·react·react hooks·useeffect·闭包陷阱
风骏时光牛马4 小时前
企业业务运营信息数据库
前端
IT_陈寒5 小时前
Vue的数组更新把我坑惨了
前端·人工智能·后端