2025-04-08【Electron】Electron+Vite+Vue3 路由子窗口

使用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更新
①②——>③的变换导致闪烁

  • 有两种解决办法

  1. 添加一个超长的延迟
setTimeout(() => {
  subWindow.show();  // 显示窗口
  }, xxxx ms);

但不稳定,比如100ms的延迟可以在高性能电脑完成①②,但性能差的电脑仍会看到闪烁

  1. 把路由完成消息作为事件传递到主进程,再监听事件,显示窗口
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();  // 显示窗口
    });  // 窗口准备好后再显示

也没有闪烁了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容