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"><</div><div :class="['forward', { active: canGoForward }]" @click="goForward">></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 为
的 a 标签点击都没反应:_blank
这是因为 webview 默认不允许打开新窗口,需要设置 allowpopups 属性才行:
<webview ref="webview" class="webview" src="about:blank" allowpopups></webview>
效果如下:
webview 的功能非常强大,建议大家先阅读一遍官方文档,初步了解 webview 可以提供哪些能力,具体 API 的使用细节可以等到后面用到的时候再研究。
最后
整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。
有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享
部分文档展示:
文章篇幅有限,后面的内容就不一一展示了
有需要的小伙伴,可以点下方卡片免费领取
版权归原作者 前端开发小司机 所有, 如有侵权,请联系我们删除。