使用Electron+Vite实现调用网页记录功能

 更新时间:2026年07月12日 09:12:31   作者:栀椩  
想要在桌面应用里流畅嵌套网页,这篇实战教程教你用 Electron + Vue 轻松实现,通过iframe加载外部网页,无需繁杂配置,一步到位,手把手拆解核心代码,涵盖权限控制与跨域通信,需要的朋友可以参考下

近期要开发桌面应用程序,实现调用网页功能,记录一下

组件内使用 <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调用网页记录的资料请关注脚本之家其它相关文章!

相关文章

  • 前端实现记住密码功能的完整步骤

    前端实现记住密码功能的完整步骤

    记住密码功能是Web前端开发中一个经典且高频的用户交互需求,下面这篇文章主要介绍了前端实现记住密码功能的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2026-02-02
  • 详解ES6 CLASS在微信小程序中的应用实例

    详解ES6 CLASS在微信小程序中的应用实例

    这篇文章主要介绍了详解ES6 CLASS在微信小程序中的应用实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-04-04
  • uni-app中px与rpx的转换方法

    uni-app中px与rpx的转换方法

    这篇文章主要给大家介绍了关于uni-app中px与rpx的转换方法,rpx单位最初由微信推出,它与我们常用px一个很大的区别就是具有响应式,需要的朋友可以参考下
    2023-10-10
  • 详解如何在JavaScript中创建线性仪表图

    详解如何在JavaScript中创建线性仪表图

    线性仪表图表示显示所需值的垂直或水平线性刻度,带有颜色刻度以及单个或多个指针。本文将详细讲解如何利用JavaScript创建线性仪表图,需要的可以参考一下
    2022-03-03
  • JavaScript数组降维之将二维数组转为一维数组的五种方案

    JavaScript数组降维之将二维数组转为一维数组的五种方案

    本文介绍了五种数组降维方法,从最简单的Array.prototype.flat()方法,到兼容旧版本的concat;spread方法,再到通用的reduce方法,以及处理任意深度嵌套的递归方法, 兌时建议总结不同方法的特点、适用场景和性能差异,需要的朋友可以参考下
    2026-05-05
  • js简易版购物车功能

    js简易版购物车功能

    这篇文章主要为大家分享了js简易版的购物车,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-06-06
  • JavaScript中常见的高阶函数总结

    JavaScript中常见的高阶函数总结

    JavaScript的函数其实都指向某个变量,既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数,这篇文章主要给大家介绍了关于JavaScript中常见的高阶函数,需要的朋友可以参考下
    2022-02-02
  • layui上传图片到服务器的非项目目录下的方法

    layui上传图片到服务器的非项目目录下的方法

    今天小编就为大家分享一篇layui上传图片到服务器的非项目目录下的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-09-09
  • Taro小程序自定义顶部导航栏功能的实现

    Taro小程序自定义顶部导航栏功能的实现

    这篇文章主要介绍了Taro小程序自定义顶部导航栏功能的实现,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-12-12
  • Javascript直接定义对象实例[

    Javascript直接定义对象实例[

    Javascript直接定义对象实例[...
    2006-12-12

最新评论