Electron + Vite 调用网页记录
Electron Vite 调用网页记录近期要开发桌面应用程序实现调用网页功能记录一下组件内使用iframe加载外部网页。使用 JavaScript。一、为什么选择iframe官方推荐Electron 团队目前不推荐使用webview未来可能移除。iframe是标准 HTML 元素兼容性好长期稳定。轻量简单无需额外配置直接在组件中使用适合大多数“内嵌网页”的场景。安全性可控通过sandbox属性可以精细控制嵌入页面的权限脚本、弹窗、表单提交等。如果需求是独立的 session 隔离、preload 注入或细粒度网络事件拦截请考虑WebContentsView但代码复杂度较高。对于普通的内嵌展示iframe足够。二、项目初始化使用electron-vite脚手架推荐npm create quick-start/electron-vite my-app cd my-app npm install然后按下面的结构创建文件和目录vue创建项目时会自动生成目录结构。三、项目结构以 Vue 为例my-app/ ├── src/ │ ├── main/ │ │ └── index.js # 主进程入口 │ ├── preload/ │ │ └── index.js # 预加载脚本 │ └── renderer/ │ ├── index.html # 渲染进程入口 HTML │ └── src/ │ ├── main.js # Vue 应用入口 │ ├── App.vue # 根组件 │ └── components/ │ └── IframeView.vue # iframe 组件 ├── electron.vite.config.mjs # 配置文件ESM ├── package.json └── ...四、核心配置与代码1.electron.vite.config.mjs会自动生成无需手动修改import{resolve}frompathimport{defineConfig,externalizeDepsPlugin}fromelectron-viteimportvuefromvitejs/plugin-vueexportdefaultdefineConfig({main:{plugins:[externalizeDepsPlugin()],build:{rollupOptions:{input:{index:resolve(__dirname,src/main/index.js)}}}},preload:{plugins:[externalizeDepsPlugin()],build:{rollupOptions:{input:{index:resolve(__dirname,src/preload/index.js)}}}},renderer:{root:resolve(__dirname,src/renderer),plugins:[vue()],build:{rollupOptions:{input:{index:resolve(__dirname,src/renderer/index.html)}}}}})2.package.json关键字段会自动生成无需手动修改{ name: my-app, version: 1.0.0, main: ./out/main/index.js, scripts: { dev: electron-vite dev, build: electron-vite build, preview: electron-vite preview }, dependencies: { electron: ^30.0.0, vite: ^5.4.0, electron-vite: ^2.3.0, vitejs/plugin-vue: ^5.0.0, vue: ^3.4.0 }, devDependencies: { electron-builder: ^24.13.0 } }3. 主进程 —src/main/index.js主进程创建窗口不需要设置webviewTag: trueiframe 是标准 HTML默认可用。import{app,BrowserWindow,shell}fromelectronimport{join}frompathimport{is}fromelectron-toolkit/utilsfunctioncreateWindow(){constmainWindownewBrowserWindow({width:1100,height:750,show:false,webPreferences:{preload:join(__dirname,../preload/index.js),sandbox:false,// 关闭沙箱以便 iframe 可正常加载contextIsolation:true,nodeIntegration:false}})// 开发模式加载 Vite 开发服务器if(is.devprocess.env[ELECTRON_RENDERER_URL]){mainWindow.loadURL(process.env[ELECTRON_RENDERER_URL])}else{// 生产模式加载打包后的文件mainWindow.loadFile(join(__dirname,../renderer/index.html))}mainWindow.once(ready-to-show,(){mainWindow.show()})// 外部链接用默认浏览器打开防止新窗口弹出mainWindow.webContents.setWindowOpenHandler(({url}){shell.openExternal(url)return{action:deny}})}app.whenReady().then((){createWindow()app.on(activate,(){if(BrowserWindow.getAllWindows().length0)createWindow()})})app.on(window-all-closed,(){if(process.platform!darwin)app.quit()})4. 预加载脚本 —src/preload/index.js可选如果需要向渲染进程暴露一些 API例如通知、文件操作可以写在这里。本例中仅作示范实际可按需增减。import{contextBridge,ipcRenderer}fromelectroncontextBridge.exposeInMainWorld(electronAPI,{platform:process.platform,sendMessage:(channel,data)ipcRenderer.send(channel,data),onMessage:(channel,callback){ipcRenderer.on(channel,(_event,...args)callback(...args))}})5. 渲染进程 — Vue 组件方式推荐src/renderer/index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleElectron Iframe/title /head body div idapp/div script typemodule src./src/main.js/script /body /htmlsrc/renderer/src/main.jsimport{createApp}fromvueimportAppfrom./App.vuecreateApp(App).mount(#app)src/renderer/src/App.vuetemplate div idapp-root h2Electron 内嵌网页 (iframe)/h2 IframeView / /div /template script setup import IframeView from ./components/IframeView.vue /scriptsrc/renderer/src/components/IframeView.vue这是一个带地址栏、前进后退的 iframe 组件。template div classiframe-wrapper !-- 工具栏 -- div classtoolbar input v-modelurl typetext placeholder输入网址按回车访问 keyup.enternavigate / button clickgoBack :disabled!canGoBack←/button button clickgoForward :disabled!canGoForward→/button button clickreload⟳/button /div !-- iframe 区域 -- iframe refiframeRef :srccurrentUrl sandboxallow-same-origin allow-scripts allow-popups allow-forms frameborder0 stylewidth:100%; height:600px; loadonIframeLoad /iframe !-- 状态提示 -- div classstatus{{ statusText }}/div /div /template script setup import { ref, computed } from vue const url ref(https://www.baidu.com) const currentUrl ref(https://www.baidu.com) const iframeRef ref(null) const canGoBack ref(false) const canGoForward ref(false) const statusText ref() // 由于 iframe 没有内置的 history API我们手动维护历史记录 const historyStack ref([https://www.baidu.com]) const historyIndex ref(0) function navigate() { let targetUrl url.value.trim() if (!targetUrl) return if (!targetUrl.startsWith(http://) !targetUrl.startsWith(https://)) { targetUrl https:// targetUrl } // 更新历史 historyStack.value historyStack.value.slice(0, historyIndex.value 1) historyStack.value.push(targetUrl) historyIndex.value historyStack.value.length - 1 currentUrl.value targetUrl updateNavButtons() } function goBack() { if (historyIndex.value 0) { historyIndex.value-- currentUrl.value historyStack.value[historyIndex.value] url.value currentUrl.value updateNavButtons() } } function goForward() { if (historyIndex.value historyStack.value.length - 1) { historyIndex.value currentUrl.value historyStack.value[historyIndex.value] url.value currentUrl.value updateNavButtons() } } function reload() { // 强制刷新 iframe重新设置 src const tempSrc currentUrl.value currentUrl.value setTimeout(() { currentUrl.value tempSrc }, 50) } function updateNavButtons() { canGoBack.value historyIndex.value 0 canGoForward.value historyIndex.value historyStack.value.length - 1 } function onIframeLoad() { statusText.value 页面加载完成 } /script style scoped .iframe-wrapper { display: flex; flex-direction: column; height: 100%; font-family: sans-serif; } .toolbar { display: flex; gap: 6px; padding: 8px; background: #f5f5f5; border-bottom: 1px solid #ddd; } .toolbar input { flex: 1; padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; } .toolbar button { padding: 6px 14px; cursor: pointer; border: 1px solid #aaa; background: white; border-radius: 4px; } .toolbar button:disabled { opacity: 0.5; cursor: not-allowed; } .status { padding: 4px 8px; color: #666; font-size: 12px; } /style五、开发与生产环境开发模式运行npm run devVite 开发服务器会在http://localhost:5173启动主进程自动加载该 URL。iframe 中的网页如百度不受影响。生产模式运行npm run build所有文件打包到out/目录。主进程加载本地index.htmliframe 依然正常工作。六、打包发布npm run build:win也可以打包成Linux下的安装包七、重要注意事项1.sandbox属性sandbox表示启用所有限制脚本、弹窗、表单提交等全部禁止。常用组合sandboxallow-same-origin allow-scripts allow-popups allow-forms如果不加sandboxiframe 内的页面拥有和宿主相同的权限不安全。建议始终显式设置sandbox只开放必要的权限。2. 跨域与通信iframe 默认受同源策略限制无法直接操作宿主 DOM。如需通信可使用postMessage双方都需要配合。如果加载的是自己控制的页面可以在 iframe 页面内通过parent.postMessage()发送消息。3. 外部链接在新窗口打开主进程中的setWindowOpenHandler会拦截target_blank的链接并用默认浏览器打开。如果在 iframe 内导航而不是弹出新窗口可以在 iframe 的load事件中检测并处理但更简单的方法是在 iframe 页面内统一使用_self或_top目标。4. 关于webSecurity生产环境下不要关闭webSecurity否则存在安全风险。如果 iframe 加载本地file://文件遇到跨域问题请改用自定义协议protocol.registerFileProtocol或使用loadURL加载远程页面。5. 性能iframe 与宿主共享同一个渲染进程大量嵌套 iframe 可能导致卡顿。如果只是嵌入一两个页面完全没问题。