写在前面的废话
首先,这是一篇缝合文,我的目的就是想用vite、vue结合electron打包一个windows应用;其次,项目只是这三个工具的简单应用,目前还没有往里面添加其他内容。再次,项目过程中参考了google的多篇文字内容,所以如果看到有和别人一样的代码内容,不需要奇怪我就是抄的。最后,旨在记录自己项目过程中遇到的一些bug以及如果需要创建类似项目的基本流程,所以内容并不晦涩难懂,也可能会有疏漏错误,如有错误,还望指正。
PS:请勿转载也没有多大的转载价值了,您看看就好。
工具版本
- node v18.16.0
- npm v9.6.4
- vite v4.2.0
- vue v3.2.47
- electron v24.1.2
正文
创建vite+vue项目
npm create vite 【项目名】 -- --template vue
安装 electron相关依赖
npm i -D electron electron-builder vite-plugin-electron vite-plugin-electron-renderer
修改vite.config.js
import { rmSync } from 'node:fs'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path';
import { builtinModules } from 'module';
import electron from 'vite-plugin-electron'
import renderer from 'vite-plugin-electron-renderer'
import pkg from './package.json'
const port = process.env.port || process.env.npm_config_port || 8081 // 端口
// https://vitejs.dev/config/
export default defineConfig(({ command }) => {
//同步删除给定路径上的文件
rmSync('dist-electron', { recursive: true, force: true })
const isServe = command === 'serve'
const isBuild = command === 'build'
const sourcemap = isServe || !!process.env.VSCODE_DEBUG
return {
//base:'',//有点重要,新项目打包时没留意项目设置的是绝对路径,导致项目打包后白屏在这里浪费了很长时间。
resolve: {
alias: {
'@': resolve('src'),
},
},
server: {
host: 'localhost',
port: port,
open: true, //先注释,不然启动不起来怪尴尬的
strictPort: false,
https: false,
// proxy: {//跨域设置
// },
},
plugins: [
vue(),
electron([
{
// Main-Process entry file of the Electron App.
entry: 'electron/main.js',
onstart(options) {
if (process.env.VSCODE_DEBUG) {
console.log(/* For `.vscode/.debug.script.mjs` */'[startup] Electron App')
} else {
options.startup()
}
},
vite: {
build: {
sourcemap,
minify: isBuild,
outDir: 'dist-electron/main',
rollupOptions: {
external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}),
},
},
},
},
{
entry: 'electron/preload.js',
onstart(options) {
// Notify the Renderer-Process to reload the page when the Preload-Scripts build is complete,
// instead of restarting the entire Electron App.
options.reload()
},
vite: {
build: {
sourcemap: sourcemap ? 'inline' : undefined, // #332
minify: isBuild,
outDir: 'dist-electron/preload',
rollupOptions: {
external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}),
},
},
},
}
]),
// Use Node.js API in the Renderer-process
renderer(),
],
build: {
assetsDir: 'static', // 静态资源的存放目录
assetsPublicPath: './',
assetsInlineLimit: 4096, // 图片转 base64 编码的阈值
chunkSizeWarningLimit: 1000,
rollupOptions: {
external: [ // 告诉 Rollup 不要打包内建 API
'electron',
...builtinModules,
],
},
optimizeDeps: {
exclude: ['electron'], // 告诉 Vite 排除预构建 electron,不然会出现 __diranme is not defined
},
},
clearScreen: false,
}
})
创建electron文件夹,并且新建main.js和preload.js
main.js内容如下:
// 控制应用生命周期和创建原生浏览器窗口的模组
const { app, BrowserWindow, shell, ipcMain, protocol } = require('electron')
const { release } = require('node:os')
const path = require('path')
/*
* process.env.NODE_ENV用在正式项目中,此值有可能是undefined
* 可用electron的app.isPackaged代替判断是否开发环境
* app.isPackaged返回一个boolean值,如果应用已经打包,返回true ,否则返回false 。 对于大多数应用程序,此属性可用于区分开发和生产环境。
*/
const isDev = process.env.NODE_ENV === 'development' ? true : false
/*
* 如果获取不到值,可以先写死接口
*/
const port = process.env.port || process.env.npm_config_port || 8081 // 端口
protocol.registerSchemesAsPrivileged([{ scheme: 'app', privileges: { secure: true, standard: true, stream: true } }]);
// 禁用 Windows 7 的 GPU 加速
if (release().startsWith('6.1')) app.disableHardwareAcceleration()
// 为 Windows 10+ 通知设置应用程序名称
if (process.platform === 'win32') app.setAppUserModelId(app.getName())
//
if (!app.requestSingleInstanceLock()) {
app.quit()
process.exit(0)
}
let mainWindow = null;
function createWindow() {
// 创建浏览器窗口
mainWindow = new BrowserWindow({
width: 1800,
height: 1600,
minWidth: 1000,
minHeight: 800,
webPreferences: {
nodeIntegration: true, //在渲染进程启用Node.js
contextIsolation: false,
preload: path.join(__dirname, 'preload.js')
}
})
// 加载 index.html
mainWindow.loadURL(
isDev
? `http://localhost:${port}`
: `file://${path.join(__dirname, '../dist/index.html')}`
);
if (isDev) {
// 打开开发工具
mainWindow.webContents.openDevTools()
}
// Test actively push message to the Electron-Renderer
mainWindow.webContents.on('did-finish-load', () => {
mainWindow?.webContents.send('main-process-message', new Date().toLocaleString())
})
// Make all links open with the browser, not with the application
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('https:')) shell.openExternal(url)
return { action: 'deny' }
})
}
// 这段程序将会在 Electron 结束初始化
// 和创建浏览器窗口的时候调用
// 部分 API 在 ready 事件触发后才能使用。
app.whenReady().then(createWindow)
app.on('activate', function () {
// 通常在 macOS 上,当点击 dock 中的应用程序图标时,如果没有其他
// 打开的窗口,那么程序会重新创建一个窗口。
const allWindows = BrowserWindow.getAllWindows()
if (allWindows.length) {
allWindows[0].focus()
} else {
createWindow()
}
})
app.on('second-instance', () => {
if (mainWindow) {
// Focus on the main window if the user tried to open another
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.focus()
}
})
// 除了 macOS 外,当所有窗口都被关闭的时候退出程序。 因此,通常对程序和它们在
// 任务栏上的图标来说,应当保持活跃状态,直到用户使用 Cmd + Q 退出。
app.on('window-all-closed', function () {
mainWindow = null
if (process.platform !== 'darwin') app.quit()
})
// New window example arg: new windows url
ipcMain.handle('open-win', (_, arg) => {
const childWindow = new BrowserWindow({
webPreferences: {
preload,
nodeIntegration: true,
contextIsolation: false,
},
})
if (process.env.VITE_DEV_SERVER_URL) {
childWindow.loadURL(`http://localhost:${port}#${arg}`)
} else {
childWindow.loadFile(path.join(__dirname, '../dist/index.html'), { hash: arg })
}
})
preload.js内容如下:
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector);
if (element) element.innerText = text;
};
for (const type of ['chrome', 'node', 'electron']) {
replaceText(`${type}-version`, process.versions[type]);
}
});
function domReady(condition = ['complete', 'interactive']) {
return new Promise((resolve) => {
if (condition.includes(document.readyState)) {
resolve(true)
} else {
document.addEventListener('readystatechange', () => {
if (condition.includes(document.readyState)) {
resolve(true)
}
})
}
})
}
const safeDOM = {
append(parent, child) {
if (!Array.from(parent.children).find(e => e === child)) {
return parent.appendChild(child)
}
},
remove(parent, child) {
if (Array.from(parent.children).find(e => e === child)) {
return parent.removeChild(child)
}
},
}
/**
* https://tobiasahlin.com/spinkit
* https://connoratherton.com/loaders
* https://projects.lukehaas.me/css-loaders
* https://matejkustec.github.io/SpinThatShit
*/
function useLoading() {
const className = `loaders-css__square-spin`
const styleContent = `
@keyframes square-spin {
25% { transform: perspective(100px) rotateX(180deg) rotateY(0); }
50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); }
75% { transform: perspective(100px) rotateX(0) rotateY(180deg); }
100% { transform: perspective(100px) rotateX(0) rotateY(0); }
}
.${className} > div {
animation-fill-mode: both;
width: 50px;
height: 50px;
background: #fff;
animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite;
}
.app-loading-wrap {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #282c34;
z-index: 9;
}
`
const oStyle = document.createElement('style')
const oDiv = document.createElement('div')
oStyle.id = 'app-loading-style'
oStyle.innerHTML = styleContent
oDiv.className = 'app-loading-wrap'
oDiv.innerHTML = `<div class="${className}"><div></div></div>`
return {
appendLoading() {
safeDOM.append(document.head, oStyle)
safeDOM.append(document.body, oDiv)
},
removeLoading() {
safeDOM.remove(document.head, oStyle)
safeDOM.remove(document.body, oDiv)
},
}
}
// ----------------------------------------------------------------------
const { appendLoading, removeLoading } = useLoading()
domReady().then(appendLoading)
window.onmessage = (ev) => {
ev.data.payload === 'removeLoading' && removeLoading()
}
setTimeout(removeLoading, 4999)
修改package.json文件
"main": "electron/main.js",//增加,重要。
//去掉"type": "module",
"scripts": {
//修改
"build": "vite build && electron-builder",
//增加
"electron:serve": "electron ."
}
//增加build项
"build": {
"appId": "electronApp",
"productName": "某应用",
"copyright": "Copyright © 2023",
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
},
"asar": true,
"asarUnpack":[
"./dist/electron",
"./package.json"
],
"win":{
"target": [{
"target": "nsis",
"arch": [
"x64",
"ia32"
]
}]
},
"extraResources": [
{
"from": "public/",
"to": "static/"
}
],
"files": [
"dist/**/*",
"electron/**/*"
],
"directories": {
"output": "release"
}
},
运行npm run dev命令,项目正常启动。
项目启动完成后发现控制台有个警告,警告如图
解决方法:在index.html文件head标签内增加如下元数据标签
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
打包后效果:
npm run build
打包后bug总结:
直接整个开发的项目,问题还是很多啊。
1、关注路径问题,绝对路径和相对路径真的得注意
2、加入vue-router,将router的mode: 'history'先注释掉或改成'hash'
3、axios http请求不到,暂时方案是baseURL进行判断,如果是development走代理,否则,就是直接请求服务器地址
//import.meta.env.VITE_BASE_PATH=http://xxx.xxx.xx.xx:xxxx
const services = axios.create({
baseURL:
process.env.NODE_ENV === 'development'
? '/api'
: import.meta.env.VITE_BASE_PATH,
})