Vue Router 和 Electron 集成要做哪些准备?
做桌面应用的时候,不少同学会选 Electron 搭框架,再结合 Vue 生态提升开发效率,但把 Vue Router 放到 Electron 里用,和纯网页端开发差别还挺大,路由模式咋选?多窗口咋处理?刚上手很容易踩坑,今天就顺着“Vue Router 在 Electron 里咋用?”这个问题,把集成步骤、关键配置、场景处理这些事儿聊透。
先得把项目架子搭起来,要是从零开始,用 Vue CLI 初始化项目最方便,打开终端敲 vue create my-electron-app
,选好 Vue 版本(Vue 3),创建完后,得把 Electron 整合进来,常用的是 electron-builder
或者 electron-vite
,以 electron-builder
为例,先装依赖:npm install electron electron-builder --save-dev
。
接下来要配置 Electron 的主进程(main process),在项目根目录新建 electron/main.js
(名字随你定,后续要在 package.json
里指定),主进程负责创建 BrowserWindow(渲染进程的容器),代码大概长这样:
const { app, BrowserWindow } = require('electron') const path = require('path') function createWindow() { const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, 'preload.js'), // 预加载脚本,可选 nodeIntegration: true, // 允许渲染进程用Node API(谨慎开,安全考虑) contextIsolation: false // 配合nodeIntegration,看需求 } }) // 加载Vue项目的入口,开发时是本地服务,打包后是文件路径 if (process.env.NODE_ENV === 'development') { win.loadURL('http://localhost:8080') // 假设Vue开发服务跑在8080 } else { win.loadFile(path.join(__dirname, '../dist/index.html')) // 打包后的静态文件 } } app.whenReady().then(createWindow) app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit() }) app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow() })
然后在 package.json
里加 Electron 相关配置:
{ "main": "electron/main.js", "scripts": { "electron:serve": "vue-cli-service build --watch & electron .", "electron:build": "vue-cli-service build && electron-builder" }, "build": { "appId": "com.yourname.app", "files": ["dist/**/*", "electron/**/*"] } }
现在项目结构里,Vue Router 该咋配?和网页端一样,在 src/router/index.js
里定义路由:
import { createRouter, createWebHashHistory } from 'vue-router' import Home from '../views/Home.vue' const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/about', name: 'About', component: () => import('../views/About.vue') } ] const router = createRouter({ history: createWebHashHistory(), // 先记住这里选hash模式,后面讲原因 routes }) export default router
最后在 src/main.js
里把路由挂到 Vue 实例上:
import { createApp } from 'vue' import App from './App.vue' import router from './router' createApp(App).use(router).mount('#app')
到这儿,基本的集成架子就搭好了,开发时跑 npm run electron:serve
,Electron 窗口会加载 Vue 开发服务的页面,路由也能初步工作,但这只是开始,路由模式、多窗口这些细节才是难点。
Electron 里 Vue Router 选啥路由模式?
Vue Router 有两种常用模式:createWebHashHistory
(hash 模式,URL 带 #)和 createWebHistory
(history 模式,URL 干净),在纯网页应用里,history 模式需要后端配置路由重定向,但 Electron 是桌面应用,用的是 file://
协议或者本地服务(开发时),情况更特殊。
先看 hash 模式:URL 长这样 http://localhost:8080/#/about
,路由由 # 后面的部分控制,这种模式下,不管是开发时的本地服务(http 协议)还是打包后的文件协议(file://),路由都能正常工作,因为 # 后面的变化不会触发浏览器的页面刷新,Electron 的 BrowserWindow 也能正确识别路由变化,所以新手第一次集成,优先选 hash 模式,不容易踩坑。
再看 history 模式:如果强行用 createWebHistory
,开发时(http 协议)其实能跑,因为 Vue 开发服务器会处理路由,但打包后,Electron 用 file://
协议加载 index.html
时,就会出问题——比如访问 file:///path/to/dist/index.html/about
,Electron 会认为这是一个不存在的文件,直接白屏。
那 history 模式就不能用了?也不是,如果非要 URL 干净,可以在 Electron 主进程里拦截请求,把所有路由请求指向 index.html
,修改主进程的 win.loadFile
不太够,得用 webRequest
拦截,举个例子,在主进程里加:
const { session } = require('electron') session.defaultSession.webRequest.onBeforeRequest((details, callback) => { const { url } = details // 判断是否是文件协议,且不是index.html本身 if (url.startsWith('file://') && !url.endsWith('index.html')) { callback({ path: path.join(__dirname, '../dist/index.html') }) } else { callback({}) } })
但这种方法有点 hack,还要考虑多窗口、缓存等问题,复杂度高,所以除非产品对 URL 美观度要求极高,否则优先用 hash 模式更稳妥。
咋在 Electron 里实现页面跳转?
和网页端一样,Vue Router 提供声明式(
)和编程式(this.$router.push
)两种跳转方式,但 Electron 里要注意单窗口内跳转和多窗口跳转的区别。
单窗口内的路由跳转
如果整个 Electron 应用只有一个 BrowserWindow(大部分工具类应用是这样),那和网页端几乎没区别,比如在组件里用声明式导航:
<template> <div> <router-link to="/">首页</router-link> <router-link to="/about">lt;/router-link> <router-view></router-view> </div> </template>
编程式导航也一样:
<script setup> import { useRouter } from 'vue-router' const router = useRouter() const goAbout = () => { router.push('/about') } </script>
这时路由变化只会更新当前 BrowserWindow 里的页面内容,不会新建窗口,体验和网页一致。
多窗口场景的路由处理
如果应用需要多窗口(比如点击按钮弹出新窗口显示详情页),就得考虑新窗口的路由加载,假设要在点击时新建窗口并打开 /detail
路由,步骤如下:
- 主进程创建新窗口:在主进程里写一个创建窗口的函数,接收路由路径参数:
function createDetailWindow(routePath) { const detailWin = new BrowserWindow({ width: 400, height: 300, webPreferences: { ... } }) // 加载页面时,把路由参数拼到URL里 if (process.env.NODE_ENV === 'development') { detailWin.loadURL(`http://localhost:8080/#${routePath}`) // hash模式下要带# } else { detailWin.loadFile(path.join(__dirname, '../dist/index.html'), { hash: routePath // 给file协议的URL加hash参数 }) } }
- 渲染进程触发新窗口:在触发新建窗口的组件里,通过 Electron 的
ipcRenderer
给主进程发消息,传递要打开的路由:
<script setup> import { ipcRenderer } from 'electron' const openDetail = () => { ipcRenderer.send('open-detail-window', '/detail') // 发消息给主进程 } </script>
- 主进程监听消息并创建窗口:在主进程的
main.js
里监听open-detail-window
事件:
const { ipcMain } = require('electron') ipcMain.on('open-detail-window', (event, routePath) => { createDetailWindow(routePath) })
这样新窗口打开时,会加载指定的路由页面,要注意的是,每个 BrowserWindow 都是独立的渲染进程,它们的 Vue Router 实例也是独立的,所以多窗口之间路由状态不会互相影响,这和网页端单页应用完全不同,得自己处理窗口间的数据通信(比如用 ipcRenderer/ipcMain
或者 localStorage/sessionStorage,但 Electron 里更推荐 ipc 通信)。
多窗口场景下 Vue Router 咋处理?
刚才讲了多窗口的路由跳转,但实际开发中还有更细的问题:比如新窗口的路由守卫、窗口关闭时的路由状态清理、多窗口共享数据时的路由同步。
新窗口的路由守卫
每个窗口的路由实例独立,所以在新窗口的路由里用 beforeEnter
或者组件内的 onBeforeRouteEnter
是有效的,比如新窗口的详情页需要校验权限:
// src/router/index.js 里给/detail路由加守卫 { path: '/detail', name: 'Detail', component: () => import('../views/Detail.vue'), beforeEnter: (to, from, next) => { // 这里可以通过ipcRenderer请求主进程获取权限信息 const hasPermission = window.electron.ipcRenderer.invoke('check-permission') if (hasPermission) { next() } else { next('/forbidden') } } }
窗口关闭时的路由状态
如果多个窗口共享一些临时数据,窗口关闭时可能需要通知其他窗口更新路由状态,比如主窗口是列表页,详情窗口关闭后,主窗口要跳转到列表的刷新页,这时候可以在详情窗口的 beforeUnmount
钩子发 ipc 消息:
// Detail.vue <script setup> import { onBeforeUnmount } from 'vue' import { ipcRenderer } from 'electron' onBeforeUnmount(() => { ipcRenderer.send('detail-window-closed') }) </script>
主窗口的渲染进程监听这个消息,触发路由跳转:
// Home.vue <script setup> import { onMounted } from 'vue' import { ipcRenderer } from 'electron' import { useRouter } from 'vue-router' const router = useRouter() onMounted(() => { ipcRenderer.on('detail-window-closed', () => { router.push('/refresh') // 假设/refresh是列表刷新后的路由 }) }) </script>
这些常见问题你大概率会碰到
问题1:路由切换后页面白屏,控制台报文件找不到
这大概率是路由模式选了 history,打包后用 file 协议加载导致的,解决方法:要么换成 hash 模式,要么按前面说的在主进程拦截请求,把所有路由指向 index.html。
问题2:开发时路由正常,打包后路由失效
原因和上面类似,打包后是 file 协议,history 模式不兼容,另外要检查 electron-builder
的打包配置,确保 dist
目录下的静态文件被正确打包,并且主进程里 loadFile
的路径没错。
问题3:多窗口打开同一路由,数据不同步
因为每个窗口的路由实例独立,数据也是隔离的,解决方法是通过主进程做“中间人”,用 ipc 通信同步数据,比如主窗口更新了用户信息,发消息给主进程,主进程再广播给所有打开的窗口,每个窗口的路由守卫或组件内再更新数据。
问题4:路由跳转时 Electron 窗口大小变化异常
这可能是因为路由切换时组件里的 DOM 操作影响了窗口大小(比如某些组件渲染后高度突变),可以在路由切换前固定窗口大小,或者在组件 onMounted
后再调整窗口大小:
// 路由守卫里控制窗口大小 router.beforeEach((to, from, next) => { const win = require('electron').remote.getCurrentWindow() win.setSize(800, 600) // 切换前重置大小 next() })
把 Vue Router 放进 Electron 里,核心是理解“桌面应用的多进程架构”和“前端路由的单页逻辑”之间的差异,路由模式要适配 Electron 的文件协议,多窗口要处理进程间的路由通信,这些细节得结合场景慢慢调,要是你刚上手,从 hash 模式开始,先把单窗口应用跑通,再逐步扩展多窗口、路由守卫这些功能,踩坑时回到 Electron 和 Vue Router 的官方文档找答案,基本都能解决,毕竟工具的设计都是为了让开发更顺,摸清它们的脾气,桌面应用开发也能像写网页一样丝滑~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。