0


Electron 中的 webview 实战 —— 手写简易浏览器

webview 标签的使用

webview 标签是 Electron 提供的一个类似于 web 中 iframe 的容器,可以嵌入另外的页面:

<body><p>下面使用 webview 标签嵌入了百度网站</p><webview src="https://www.baidu.com"></webview>
</body> 

那么展示效果如下:

默认情况下,Electron 是不启用 webview 标签的,需要在创建 Window 的时候在 webPreferences 里面设置 webviewTag 为 true 才行:

win = new BrowserWindow({width: 800,height: 600,webPreferences: {webviewTag: true, // 需要添加此行},
}) 

webview 与 iframe 的区别

webview 是 chromium 浏览器中的概念,它跟 iframe 是非常类似的,但又不一样,绝大部分开发者搞不懂它们之间的区别,这里为大家详细介绍。首先官方对 webview 标签的解释为:

For the most part, Blink code will be able to treat a similar to an . However, there is one important difference: the parent frame of an is the document that contains the element, while the root frame of a has no parent and is itself a main frame. It will likely live in a separate frame tree.

其实已经说得很明白了,webview 和 iframe 的不同点在于:

  • iframe 的父 frame 是包含 iframe 标签的页面
  • webview 是没有父 frame 的,自己本身就是一个 mainFrame

这是什么意思呢?接下来通过两个案例来进一步说明:

简单案例

我们写个简单的案例来验证一下,首先在主进程里面写:

let win
app.whenReady().then(() => {win = new BrowserWindow({width: 800,height: 600,webPreferences: { webviewTag: true },})win.loadFile(path.join(__dirname, '../renderer/index.html'))setTimeout(printFrames, 2000)
}) 

应用启动后,延迟两秒打印当前页面的所有 frames 信息(用 framesInSubtree 方法):

function printFrames() {const frames = win.webContents.mainFrame.framesInSubtreeconst print = (frame) => frame && frame.url && path.basename(frame.url)frames.forEach((it) => {console.log(`current frame: ${print(it)}`)console.log(` children: ${JSON.stringify(it.frames.map((it) => print(it)))}`)console.log(` parent`, print(it.parent), '\n')})
} 

使用 iframe 标签

如果

index.html

页面用的是 iframe 标签:

<body><iframe src="./embed.html"></iframe>
</body> 

那么打印出来的结果是:

current frame: index.html children: ["embed.html"] parent null

current frame: embed.html children: [] parent index.html 

可以看到

embed.html

index.html

的子 Frame,

index.html

embed.html

的父 Frame。

使用 webview 标签

但是如果把 iframe 换成 webview 标签:

<body><webview src="./embed.html"></webview>
</body> 

那么打印出来的结果是:

current frame: index.html children: [] parent null 

current frame: embed.html children: [] parent null 

也就是说,embed.html 和 index.html 不存在父子关系,这两个 Frame 是彼此独立的。

嵌套案例

为了更清晰的演示,构造下面的嵌套案例:

  • index.html 里面通过 iframe 嵌入了 webview.html
  • webview.html 里面通过 iframe 嵌入了 iframe.html
  • iframe.html 里面通过 iframe 嵌入了 iframe-inside.html

打开控制台 Application 面板,可以看到这种层次结构:

如果把 iframe 都换成 webview 标签,即:

  • index.html 里面通过 webview 嵌入了 webview.html
  • webview.html 里面通过 webview 嵌入了 iframe.html
  • iframe.html 里面通过 webview 嵌入了 iframe-inside.html

打开控制台 Application 面板,层次结构就消失了:

这就验证了官方文档中的那句话:

has no parent and is itself a main frame. It will likely live in a separate frame tree.

webview 标签没有父 Frame,它会创建独立的 frame 树(并且有自己的 webContents 对象,这个概念后续会专门介绍)。

实现简易浏览器

webview 标签可创建一个浏览器沙箱环境来加载第三方网站,Electron 提供了丰富的 API 能够拦截各种事件,因此非常适合今天开发简易浏览器的场景。

首先新建

browser-simple/main

目录用于存放主进程文件,这里使用 pnpm + vite + vue 进行前端页面的开发,可以进入 browser-simple 路径下执行下面的命令:

$ pnpm create vite 

在交互式命令行环境中选择 Vue 框架和 JavaScript 语言,项目名称叫 renderer,那么最终会自动生成项目文件:

browser-simple
├── main
│ └── index.js
└── renderer├── README.md├── index.html├── package.json├── pnpm-lock.yaml├── src│ ├── App.vue│ ├── main.js│ └── style.css└── vite.config.js 

进入 renderer 目录下启动前端项目:

$ pnpm run dev

VITE v4.0.4ready in 741 ms

➜Local: http://127.0.0.1:5173/
➜Network: use --host to expose
➜press h to show help 

编写

main/index.js

主进程文件,加载 Vue 项目页面:

mainWindow = new BrowserWindow({width: 1200,height: 1000,webPreferences: {webviewTag: true,},
})
mainWindow.loadURL('http://127.0.0.1:5173/') 

可以发现顺利启动起来了:

改造 App.vue ,编写简易浏览器的页面,用的是传统的 Vue 语法和 CSS 样式,这里不做过多赘述:

<template><div><div class="toolbar"><div :class="['back', { active: canGoBack }]" @click="goBack">&lt;</div><div :class="['forward', { active: canGoForward }]" @click="goForward">&gt;</div><input v-model="url" placeholder="Please enter the url" @keydown.enter="go" /><div class="go" @click="go">Go</div></div><webview ref="webview" class="webview" src="about:blank"></webview></div>
</template> 

可以看到,DOM 结构是非常简单的,顶部工具条放前进/后退按钮,网址输入框和前往按钮,下面就是在 webview 标签。

但是当启动项目之后,控制台发现 webview 标签竟然变成了注释:

非常奇怪,怀疑是 Electron 的 webview 标签被 Vue 编译时做了特殊处理了,于是搜索了一下 Vue 的源码,在

packages/runtime-dom/types/jsx.d.ts

中找到了 webview 标签,跟 div、span 这种标签放在了一起:

于是在 Vue 文档的 web-components 章节中找到了 isCustomElement 选项,可以通过该选项设置自定义元素,不让 Vue 进行编译处理:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({plugins: [vue({template: {compilerOptions: {isCustomElement: (tag) => tag === 'webview',},},}),],
}) 

重启之后,发现 webview 标签可以顺利在 DOM 中显示了,接下来就是具体的逻辑实现了,最关键的就是:点击 Go 按钮之后,让 webview 加载 input 输入框中的网站,这里用到了 webview 的 loadURL 方法:

<script setup>
import { ref } from 'vue'
const url = ref('')
const webview = ref(null)
function go() {webview.value.loadURL(url.value)
}
</script> 

此时在浏览器中输入网址,然后点击 Go 按钮(或者键盘回车),可以发现 webview 中加载的网站可以顺利展示出来了:

不过这里有个细节,如果在模板里面 webview 不加 src 属性的话,会出问题的,调用 loadURL 的时候报错:

node:electron/js2c/isolated_bundle:17 Uncaught Error: The WebView must be attached to the DOM and the dom-ready event emitted before this method can be called.at WebViewElement.getWebContentsId (node:electron/js2c/isolated_bundle:17:695)at e.<computed> [as loadURL] (node:electron/js2c/isolated_bundle:21:3433) 

所以如果不想让 webview 默认加载某个网站,可以初始化为

about:blank

或者

data:text/plain

那如何实现前进和后退功能呢?这就需要用到 webview 标签的事件能力了,Electron 提供了非常多的事件,例如:

  • dom-ready
  • page-title-updated
  • page-favicon-updated
  • did-start-loading
  • did-stop-loading
  • did-start-navigation
  • did-navigate

具体 API 的含义和使用方法可以参考官方文档,在此结合前进后退功能,展示部分 API 的使用:

<script setup>
import { ref, onMounted } from 'vue'
const url = ref('')
const webview = ref(null)
const webviewDomReady = ref(false)
const canGoBack = ref(false)
const canGoForward = ref(false)

onMounted(() => {const el = webview.valueif (<img src="http')) {url.value = event.url}}" style="margin: auto" />
})

const updateNavigationState = () => {if (!webview.value) returnif (!webviewDomReady.value) returncanGoBack.value = webview.value.canGoBack()canGoForward.value = webview.value.canGoForward()
}

const goBack = () => {const el = webview.valueif (el.canGoBack()) el.goBack()
}

const goForward = () => {const el = webview.valueif (el.canGoForward()) el.goForward()
}
</script> 

上面的代码并不复杂,主要是监听了几个事件,然后绑定相关变量,从而更新按钮状态,里面有几个关键点:

  • 大部分的 webview 方法需要在 dom-ready 之后才能调用
  • did-start-navigation 事件中可以拿到跳转的 URL

    到这里,一个简单的浏览器的雏形就有了,不过目前有个比较严重的问题,所有 target 为
    _blank
    
    的 a 标签点击都没反应:

    这是因为 webview 默认不允许打开新窗口,需要设置 allowpopups 属性才行:
<webview ref="webview" class="webview" src="about:blank" allowpopups></webview> 

效果如下:

webview 的功能非常强大,建议大家先阅读一遍官方文档,初步了解 webview 可以提供哪些能力,具体 API 的使用细节可以等到后面用到的时候再研究。

最后

整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享

部分文档展示:



文章篇幅有限,后面的内容就不一一展示了

有需要的小伙伴,可以点下方卡片免费领取


本文转载自: https://blog.csdn.net/web22050702/article/details/129585003
版权归原作者 前端开发小司机 所有, 如有侵权,请联系我们删除。

“Electron 中的 webview 实战 —— 手写简易浏览器”的评论:

还没有评论