添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

我正在参加「掘金·启航计划」

本节主要知识点是 Electron 中的 webview 标签,学完之后,会带领大家用 Vue + Electron 实现一个简单浏览器,效果如下:

webview 标签的使用

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

< 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.framesInSubtree
      const 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 标签:

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

    那么打印出来的结果是:

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

    可以看到 embed.htmlindex.html 的子 Frame,index.htmlembed.html 的父 Frame。

    使用 webview 标签

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

    <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.4  ready 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 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.value
      if (!el) return
      el.addEventListener('dom-ready', () => {
        webviewDomReady.value = true
        updateNavigationState()
      el.addEventListener('did-start-loading', (event) => {
        updateNavigationState()
      el.addEventListener('did-stop-loading', (event) => {
        updateNavigationState()
      el.addEventListener('did-start-navigation', (event) => {
        updateNavigationState()
        if (event.url.startsWith('http')) {
          url.value = event.url
    const updateNavigationState = () => {
      if (!webview.value) return
      if (!webviewDomReady.value) return
      canGoBack.value = webview.value.canGoBack()
      canGoForward.value = webview.value.canGoForward()
    const goBack = () => {
      const el = webview.value
      if (el.canGoBack()) el.goBack()
    const goForward = () => {
      const el = webview.value
      if (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 的使用细节可以等到后面用到的时候再研究。