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())

相关推荐
葡萄城技术团队7 小时前
1000 人协同压测的环境瓶颈排查:CPU 争用与 Wi\-Fi
前端
excel8 小时前
Marked 与 Markdown 有什么区别?该怎么选?
前端
Da Da 泓9 小时前
五子棋 Web 端测试用例报告
前端·网络·websocket·网络协议·测试用例·项目
zxcvb15310 小时前
远程查看本地文档:一条轻量 Web 文件服务的工程化链路
前端·远程查看文档
niucloud-admin10 小时前
NIUCLOUD PHP SAAS上门家政使用文档:城市管理
java·前端·php
程序员大阳10 小时前
副队长JavaScript教程(50)-DOM:网页开发的起点
javascript·dom·es·节点·概念·document·关系
北岛贰10 小时前
啵啵音乐迎来全新升级!
前端
甘晴void12 小时前
我给 Magpie 做了个 macOS 桌宠:106 段动画,请求一多就亮灯
macos·electron·开源·deepseek·桌面宠物·magpie
hkNaruto12 小时前
【AI】基于大模型的AI游戏地图生成、控制方案:AI Native Game Harness 迁移操作手册
前端·人工智能·游戏
IT_陈寒12 小时前
Java序列化踩坑记:对象循环引用让我加班两小时
前端·人工智能·后端