如何使用原生JavaScript、HTML和CSS在Electron中实现单页应用(SPA)及路由问题咨询
嘿,聊聊 Electron 里用原生 JS/CSS 实现 SPA 的那些事
我之前也折腾过类似的场景,刚好能给你一些实际的建议和代码示例:
先回答你的第一个疑问:控制区块显隐算不算 SPA?
这种通过切换DOM元素显示/隐藏来切换页面内容的方式,属于SPA的简易实现——毕竟SPA的核心就是单HTML文件内切换内容,不刷新页面。不过它是无路由的SPA,适合功能简单的小应用;如果需要支持浏览器前进后退、书签保存,或者页面结构复杂,带路由的方案会更实用。
解决 location.pathname 返回文件路径的坑
在Electron里直接加载本地HTML时,location.pathname会返回文件系统的绝对路径(比如/Users/xxx/app/index.html),这是因为默认用file://协议加载本地资源,不是HTTP协议的路径规则。这里有两种靠谱的解决思路:
方案1:Hash模式路由(零配置,最省心)
Hash模式用URL中#后面的部分做路由(比如#/posts),这种方式不会触发页面刷新,Electron也能完美识别。你只需要把路由逻辑里的location.pathname换成location.hash.slice(1)(去掉开头的#),再监听hashchange事件就行。
方案2:History模式路由(更美观的无# URL)
如果想要干净的URL(不带#),就得在Electron主进程里做些配置:拦截页面跳转,让Electron始终加载主HTML文件,再交给前端路由处理。具体步骤我后面会给代码示例。
完整可运行的Hash模式示例
主进程代码(main.js)
const { app, BrowserWindow } = require('electron'); const path = require('path'); function createWindow() { const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, contextIsolation: false, // 如果你需要在渲染进程直接用Node API,否则可以设为true并写preload脚本 } }); mainWindow.loadFile(path.join(__dirname, 'index.html')); } app.whenReady().then(() => { createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
渲染进程代码(index.html)
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Electron SPA Demo</title> <style> .nav-btn { margin: 0 10px; padding: 8px 16px; cursor: pointer; border: none; border-radius: 4px; background: #2196F3; color: white; } .nav-btn:hover { background: #1976D2; } .page { padding: 20px; } </style> </head> <body> <nav> <button class="nav-btn" data-path="#/">Dashboard</button> <button class="nav-btn" data-path="#/posts">Posts</button> <button class="nav-btn" data-path="#/settings">Settings</button> </nav> <div id="app"></div> <script> // 页面组件类 class Dashboard { async getHtml() { return `<div class="page"><h1>Dashboard</h1><p>欢迎来到你的控制台!这里是默认首页~</p></div>`; } } class Posts { async getHtml() { return `<div class="page"><h1>文章列表</h1><ul><li>第一篇技术博客</li><li>Electron路由实践</li><li>原生JS SPA技巧</li></ul></div>`; } } class Settings { async getHtml() { return `<div class="page"><h1>设置页面</h1><p>在这里调整应用的各项参数:</p><label><input type="checkbox" /> 开启夜间模式</label></div>`; } } // 路由工具函数(和你视频里的逻辑一致,适配Hash路径) function getParams(match) { const values = match.result.slice(1); const keys = Array.from(match.route.path.matchAll(/:(\w+)/g)).map(result => result[1]); return Object.fromEntries(keys.map((key, i) => [key, values[i]])); } function pathToRegex(path) { return new RegExp(`^${path.replace(/:\w+/g, '([^/]+)')}$`); } // 核心路由逻辑 const router = async () => { const routes = [ { path: "/", view: Dashboard }, { path: "/posts", view: Posts }, { path: "/settings", view: Settings } ]; // 获取当前Hash路由(去掉#) const currentPath = location.hash.slice(1) || '/'; // 匹配路由 const potentialMatches = routes.map(route => ({ route: route, result: currentPath.match(pathToRegex(route.path)) })); let match = potentialMatches.find(p => p.result !== null); if (!match) match = { route: routes[0], result: [currentPath] }; // 渲染对应页面 const view = new match.route.view(getParams(match)); document.querySelector("#app").innerHTML = await view.getHtml(); }; // 初始化路由 + 监听Hash变化 window.addEventListener('hashchange', router); window.addEventListener('load', router); // 导航按钮点击事件 document.querySelectorAll('.nav-btn').forEach(btn => { btn.addEventListener('click', e => { location.hash = e.target.dataset.path; }); }); </script> </body> </html>
如果你想用视频里的History模式
只需要在主进程中添加页面跳转拦截逻辑,同时修改渲染进程的路由监听:
主进程新增代码(main.js的createWindow函数里)
mainWindow.webContents.on('will-navigate', (event, url) => { const parsedUrl = new URL(url); // 只允许加载主HTML文件,其他路由都交给前端处理 if (parsedUrl.pathname !== path.join(__dirname, 'index.html')) { event.preventDefault(); // 提取路由路径,通知前端更新 const routePath = parsedUrl.pathname.replace(path.dirname(path.join(__dirname, 'index.html')), ''); mainWindow.webContents.send('update-route', routePath); } });
渲染进程修改路由逻辑
把Hash相关的部分换成History模式:
// 核心路由逻辑修改 const router = async (currentPath = location.pathname) => { const routes = [ { path: "/", view: Dashboard }, { path: "/posts", view: Posts }, { path: "/settings", view: Settings } ]; // 匹配路由(逻辑和之前一致) const potentialMatches = routes.map(route => ({ route: route, result: currentPath.match(pathToRegex(route.path)) })); let match = potentialMatches.find(p => p.result !== null); if (!match) match = { route: routes[0], result: [currentPath] }; const view = new match.route.view(getParams(match)); document.querySelector("#app").innerHTML = await view.getHtml(); }; // 监听主进程的路由更新事件 const { ipcRenderer } = require('electron'); ipcRenderer.on('update-route', (event, path) => { history.pushState({}, '', path); router(path); }); // 监听浏览器前进后退 window.addEventListener('popstate', () => router(location.pathname)); // 导航按钮修改 document.querySelectorAll('.nav-btn').forEach(btn => { btn.addEventListener('click', e => { const path = e.target.dataset.path.slice(1); // 去掉# history.pushState({}, '', path); router(path); }); });
这样就能实现不带#的干净URL路由了。
内容的提问来源于stack exchange,提问作者L. de Troz
相关产品推荐
相关产品推荐

