Code前端首页关于Code前端联系我们

Vue Router 和 Electron 集成要做哪些准备?

terry 8小时前 阅读数 11 #Vue

做桌面应用的时候,不少同学会选 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 路由,步骤如下:

  1. 主进程创建新窗口:在主进程里写一个创建窗口的函数,接收路由路径参数:
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参数
    })
  }
}
  1. 渲染进程触发新窗口:在触发新建窗口的组件里,通过 Electron 的 ipcRenderer 给主进程发消息,传递要打开的路由:
<script setup>
import { ipcRenderer } from 'electron'
const openDetail = () => {
  ipcRenderer.send('open-detail-window', '/detail') // 发消息给主进程
}
</script>
  1. 主进程监听消息并创建窗口:在主进程的 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前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门