使用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 } from 'path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
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: true,iframe 是标准 HTML,默认可用。
import { app, BrowserWindow, shell } from 'electron'
import { join } from 'path'
import { is } from '@electron-toolkit/utils'
function createWindow() {
const mainWindow = new BrowserWindow({
width: 1100,
height: 750,
show: false,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false, // 关闭沙箱以便 iframe 可正常加载
contextIsolation: true,
nodeIntegration: false
}
})
// 开发模式:加载 Vite 开发服务器
if (is.dev && process.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().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
4. 预加载脚本 —src/preload/index.js(可选)
如果需要向渲染进程暴露一些 API(例如通知、文件操作),可以写在这里。本例中仅作示范,实际可按需增减。
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.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 lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Electron + Iframe</title> </head> <body> <div id="app"></div> <script type="module" src="./src/main.js"></script> </body> </html>
src/renderer/src/main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
src/renderer/src/App.vue
<template>
<div id="app-root">
<h2>Electron 内嵌网页 (iframe)</h2>
<IframeView />
</div>
</template>
<script setup>
import IframeView from './components/IframeView.vue'
</script>
src/renderer/src/components/IframeView.vue
这是一个带地址栏、前进后退的 iframe 组件。
<template>
<div class="iframe-wrapper">
<!-- 工具栏 -->
<div class="toolbar">
<input
v-model="url"
type="text"
placeholder="输入网址,按回车访问"
@keyup.enter="navigate"
/>
<button @click="goBack" :disabled="!canGoBack">←</button>
<button @click="goForward" :disabled="!canGoForward">→</button>
<button @click="reload">⟳</button>
</div>
<!-- iframe 区域 -->
<iframe
ref="iframeRef"
:src="currentUrl"
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
frameborder="0"
style="width:100%; height:600px;"
@load="onIframeLoad"
></iframe>
<!-- 状态提示 -->
<div class="status">{{ 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 dev,Vite 开发服务器会在http://localhost:5173启动,主进程自动加载该 URL。iframe 中的网页(如百度)不受影响。 - 生产模式:运行
npm run build,所有文件打包到out/目录。主进程加载本地index.html,iframe 依然正常工作。
六、打包发布
npm run build:win
也可以打包成Linux下的安装包
七、重要注意事项
1.sandbox属性
sandbox=""表示启用所有限制(脚本、弹窗、表单提交等全部禁止)。- 常用组合:
sandbox="allow-same-origin allow-scripts allow-popups allow-forms" - 如果不加
sandbox,iframe 内的页面拥有和宿主相同的权限(不安全)。建议始终显式设置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 可能导致卡顿。如果只是嵌入一两个页面,完全没问题。
以上就是使用Electron+Vite实现调用网页记录功能的详细内容,更多关于Electron Vite调用网页记录的资料请关注脚本之家其它相关文章!
相关文章
JavaScript数组降维之将二维数组转为一维数组的五种方案
本文介绍了五种数组降维方法,从最简单的Array.prototype.flat()方法,到兼容旧版本的concat;spread方法,再到通用的reduce方法,以及处理任意深度嵌套的递归方法, 兌时建议总结不同方法的特点、适用场景和性能差异,需要的朋友可以参考下2026-05-05


最新评论