使用Electron+Vite+Vue3 实现桌面应用的子窗口
记录遇到的一些小坑
创建子窗口的流程为:
渲染进程触发——>主进程渲染
主进程内定义创建子窗口的函数和接收渲染进程触发消息的监听器
只写主要代码
main.js
// 创建子窗口的函数
function createSubWindow (options) {
const defaultOptions = {
width: 400,
height: 300,
parent: BrowserWindow.getFocusedWindow(),
modal: true,
minimizable: false,
maximizable: false,
resizable: false,
autoHideMenuBar: true,
show: false, // 先隐藏窗口,on('ready-to-show')事件后再显示
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
}
options = {...defaultOptions, ...options};
let subWindow = new BrowserWindow(options);
if(process.env['VITE_DEV_SERVER_URL']){
subWindow.loadURL(process.env['VITE_DEV_SERVER_URL'] + '#' + options.routerPath) // 开发环境下指定路径
}else{
subWindow.loadFile(path.resolve(__dirname,"../dist/index.html"), {
hash: '#' + options.routerPath,
}) // 打包才有用 因为开始时没有dist文件夹
}
subWindow.once('ready-to-show', () => subWindow.show()); // 窗口准备好后再显示
subWindow.on('closed', () => {
subWindow = null; // 销毁引用
});
}
// 监听器
ipcMain.on(subWindowEvents.MAKE_ROLE, (event, {options}) => {
createSubWindow(options);
console.log('创建子窗口成功', options.routerPath)
});
subWindowEvents.MAKE_ROLE是我自定义的事件名称,只不过用变量封装起来了
省略接口文件preload.js内的内容
渲染进程触发代码
test.vue
这里使用一个按钮触发,点击就创建子窗口
<template>
<h1>这是一个测试页面</h1>
<button @click="makeSubWindow">生成子窗口</button>
</template>
<script setup>
import { subWindowEvents } from '../../shared/ipcEvents'
const makeSubWindow = () => {
window.ipcRenderer.send(subWindowEvents.MAKE_ROLE, {
options: {
resizable: true,
routerPath: '/chat'
},
})
}
</script>
<style scoped>
</style>
路由配置
router/index.js
// router/index.js
import { createRouter, createWebHashHistory } from 'vue-router' // 必须使用 hash 模式
const router = createRouter({
history: createWebHashHistory(), // Electron 本地文件协议需要 hash 模式 兼容file://协议
routes: [
{ path: '/', redirect: '/home' }, // 默认将根路径跳转到 home
{ path: '/home', component: () => import('../components/views/home.vue') },
{ path: '/chat', component: () => import('../components/views/chat.vue') },
{ path: '/settings', component: () => import('../components/views/test.vue') }
]
})
export default router // 导出 router
下面介绍踩过的坑
- 使用
hash模式路由
要保证开发环境和生产环境都能正确创建子窗口并加载对应组件,务必使用vue路由的hash模式。
写法参照
if(process.env['VITE_DEV_SERVER_URL']){
subWindow.loadURL(process.env['VITE_DEV_SERVER_URL'] + '#' + options.routerPath) // 开发环境下指定路径
}else{
subWindow.loadFile(path.resolve(__dirname,"../dist/index.html"), {
hash: '#' + options.routerPath,
}) // 打包才有用 因为开始时没有dist文件夹
}
- 子窗口总是加载根组件,而不是路由指定的组件
因为我在根组件App.vue中,写了挂载时默认加载/home路由的代码
onMounted(() => {
onClickMenuItem('/home') // 页面加载完成后,默认跳转到主页
})
删除即可恢复子窗口的正常路由
但这样以后,首次打开应用时,首先显示的就是App.vue内不加载任何子组件的空白画面,如何恢复首次打开显示/home呢?
只需要在router/index.js中指定根路径/的跳转去向即可,因为首次加载app时,本质就是加载的/,即写{ path: '/', redirect: '/home' },完整代码如下:
import { createRouter, createWebHashHistory } from 'vue-router' // 必须使用 hash 模式
const router = createRouter({
history: createWebHashHistory(), // Electron 本地文件协议需要 hash 模式 兼容file://协议
routes: [
{ path: '/', redirect: '/home' }, // 默认将根路径跳转到 home
{ path: '/home', component: () => import('../components/views/home.vue') },
{ path: '/chat', component: () => import('../components/views/chat.vue') },
{ path: '/settings', component: () => import('../components/views/test.vue') }
]
})
export default router // 导出 router
加载子窗口时,发生主界面到子界面的跳转闪烁
源代码:
<template>
<div class="app">
<template v-if="!isSubWindow">
主窗口
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
</template>
<template v-else>
<!-- 子窗口专用简约布局 -->
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
</template>
</div>
</template>
const isSubWindow = computed(() => route.meta.isSubWindow)
闪烁原因
创建子窗口时
①先渲染整体布局App.vue——>②子路由生效,计算属性更新——>③DOM更新
①②——>③的变换导致闪烁
-
有两种解决办法
- 添加一个超长的延迟
setTimeout(() => {
subWindow.show(); // 显示窗口
}, xxxx ms);
但不稳定,比如100ms的延迟可以在高性能电脑完成①②,但性能差的电脑仍会看到闪烁
- 把路由完成消息作为事件传递到主进程,再监听事件,显示窗口
subWindow.once('ready-to-show', () => {
ipcMain.once(subWindowEvents.ROUTE_READY, (event) => {
setTimeout(() => {
subWindow.show(); // 显示窗口
}, 80);
});
}); // 窗口准备好后再显示
这样把很多①②的工作隐藏在了后台,80ms的设置即可满足高低性能机器避免闪烁(低于80仍会看到闪烁,因为②——>③也需要时间,但特别短)
测试发现方法2仍有弊端
首次启动时,ipcMain.once可能接收不到子路由完成的消息事件,导致show()不触发,无法创建窗口,更改一下顺序就好,还可以不用加延迟80ms
ipcMain.once(subWindowEvents.ROUTE_READY, (event) => {
console.log('子窗口路由准备就绪');
subWindow.once('ready-to-show', () => {
subWindow.show(); // 显示窗口
}); // 窗口准备好后再显示
});
再记录一下,发现方法2的弊端是未使用命名路由导致的
之前直接使用push('/chat')切换,后来换用push(name:'chat')这样的命名路由。
奇怪的是采用命名路由,即便单纯使用
subWindow.once('ready-to-show', () => {
subWindow.show(); // 显示窗口
}); // 窗口准备好后再显示
也没有闪烁了