Electron的踩坑之旅-基础篇

1,595 阅读9分钟

1.在成都

在成都用Electron的公司很少,都是些写小程序或者H5,移动端的很多,然后加上后台。一般vue3和TS就足够用了,万万没想到我去了一家用Electron的公司。如果有说的不对的地方希望大佬多多指正

2.electron用来干啥的

Electron其实就是一个跨平台解决方案,和Flutter,react native一样,当然这两个我还不会哈。我希望有机会能学习下,还有three.主要使用来开发桌面程序的。可以结合vue和react来使用它,vue的组件页面就相当于是渲染进程。他的Demo学完很快,网上的教程很多,如果和我一样的朋友们,我建议对许多的功能自己多敲,多实现,用Demo先测试实现一次,再运用再稍微复杂的项目环境中,用了几次你会发现,你越用越快,越用越快。我还记得一位朋友的一句话,你的开发效率就是你摸鱼的时间,哈哈真的是你做的越快摸鱼的事件越多。

3.我遇到那些问题

讲我遇到的一些初级的问题和知识点,可能对大佬们来说都太简单了。可能我都懂得东西本身也就是皮毛。我觉得常用的就是嵌入网页,插件;还有electron如何结合vue;以及主进程和渲染进程之间的通信。当然还有很多小功能,托盘,创建新页面,拖拽,很多不一一赘述哈。

3-1网页和插件

我遇到了需要嵌入插件和网页的需求 采用的解决方案:

1. iframe

这个是很老的一个解决方案,不过也有很多边缘问题需要解决

2. webview插入网页

        <webview
          src="http://www.baidu.com"
          rel="external nofollow"
          plugins
          style="
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: flex !important;
          "
        ></webview>

plugin设置可以引入插件样,不过我自己还没实现

在background.js文件中打开

webPreferences: {
      nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
      webviewTag: true,
      webSecurity: false,
      plugins: true,//这里
    },
const { session } = require('electron');
const path = require('path');

const pluginPath = path.join(__dirname, '<插件扩展程序目录>');
session.defaultSession.loadExtension(pluginPath);
其中,<插件扩展程序目录> 是插件的扩展程序目录路径,通常是插件的根目录。
注意,由于 Chrome 插件和 Electron 应用程序使用的是不同的上下文环境,因此在插件和应用程序之间通信时需要使用 Electron 提供的 IPC(进程间通信)机制,例如使用 ipcRenderer 和 ipcMain 对象。

3 BrowserView

这一种给我自己还没实现,写上来供大家参考哈

const { BrowserView } = require('electron');
const view = new BrowserView();
mainWindow.setBrowserView(view);
view.setBounds({ x: 0, y: 50, width: 800, height: 600 });
view.setAutoResize({ width: true, height: true });
view.webContents.loadURL('chrome-extension://<插件 ID>/<插件内容页路径>');
其中,mainWindow 是 Electron 应用程序的主窗口对象;<插件 ID> 和 <插件内容页路径> 分别是要加载的 Chrome 插件的唯一标识符和内容页面路径。

这里推荐哈官网和W3c的属性和事件,标签讲解挺详细的:www.w3cschool.cn/electronman… www.electronjs.org/zh/docs/lat…

3-2electron结合vue

为了效率,我是直接拿模板过来改的,模板加UI框架,开发效率必备。

1 使用 vue cli 创建vue项目

npm install -g @vue/cli //全局安装 
vue create electron-test //创建项目

2. 安装插件 vue-cli-plugin-electron-builder

vue add electron-builder

会多出来几个东西,非常重要,bacjground.js

我当时参考这个大佬的juejin.cn/post/703853…

多了个background.js文件。 package.json新增了几个scripts。
{
    "electron:build": "vue-cli-service electron:build platform=win32",
    "electron:serve": "vue-cli-service electron:serve",
    "postinstall": "electron-builder install-app-deps",
    "postuninstall": "electron-builder install-app-deps",
    "b32": "vue-cli-service electron:build platform=win32",
    "b": "vue-cli-service electron:build"
  },
  "main": "background.js",

3-3主进程和渲染进程之间的通信

这是今天没时间我还得忙着学哈浏览器插件,就简单记录一下我觉得重要的点

3-3-1需要声明ipcRener全局使用

e5d04b4af312dd56cd9fe45a55b5616.png

就是在Vue原型上面挂载ipcRenderer

注意:他是主进程操作node,渲染进程操作DOM元素

3-3-2渲染进程的操作

//监听主进程发送过来的消息
ipcRenderer.on('gsh-active',(evevt,arg)=>{
  console.log(event)
  console.log(event)
}
//渲染进程发送消息给主进程
var btn =document.queryselector('#btn')
btn.onclick=function(){
  ipcRenderer.send('openNewWindow','这里还可以携带数据')
}

//在渲染进程操作主进程使用node方法
const dialog = require("@electron/remote").dialog;
// Electron的内置方法,选择多个文件
const res = dialog.showOpenDialogSync({
    title: "读取文件",
    buttonLabel: '读取',
    filters: [{
        name: 'Custom File Type',
        extensions: ['js']
    }],
})
remote在主进程background.js中设置
// 解决remote使用它主进程问题
  require("@electron/remote/main").initialize();
  require("@electron/remote/main").enable(mainWindow.webContents);

3-3-3主进程的操作

//监听渲染进程发送过来的消息
ipcMain.on('openNewWindow',(evevt,arg)=>{
  event.reply('gsh-active','这里是主进程的答复')
  //还可以写业务逻辑,比如打开新窗口,还可以设置网页
}
//主进程可以主动发送消息,不过在创建过程最好用定时器延迟2秒

setTimeout(()=>{
  mainWindow.webContents.send('gsh-active','创建窗口之后,延时三秒主进程主动发数据给渲染进程')
},3000)

未完待续。。。。。

4.我开发过程中遇到的问题

1.报错require未定义的错误

然后我把electron的9版本升到13,出现了报错require未定义的错误。

本来排查以为是node和electron版本不兼容的问题,因为electron13需要node16才支持。

结果不是,配置了一下主进程就可以了,但是治标不治本,遇到必须开contextIsolation的时候还是会报错, 希望谁解决了可以私信或者评论告诉我一下

webPreferences: {
nodeIntegration: true,
// 官网似乎说是默认false,但是这里必须设置contextIsolation
contextIsolation: false
}

2.需要chrome的版本60+

这里说一下需要注意node,electron和chrome内核版本需要对应

举个栗子:Chrome 112 对应的 Electron 版本是 18.0.0,对应的 Nodejs 版本是 16.1.0

这里有个可以查chrome对应版本的链接:juejin.cn/post/702637…

最后解决方案:我是electron的版本对了的,但是webview的chrome和electron的版本不一样,所以需要专门设置下 webview的useragent属性

        <webview
          src="https://web.whatsapp.com/"

          useragent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
         
          style="width: 100%; height: 100%; display: flex !important"
        ></webview>

wenview提供很多属性和方法,还有事件

useragent  修改请求头的UA,设置用户代理
preload    规定预加载脚本的url须如 `file:` 或者 `asar:`,因为它在是 guest page 中通过通过 `require` 命令加载的。
nodeintegration  并且拥有可以使用系统底层的资源,例如 `require` 和 `process`
src        填写链接地址

更多的话可以查询链接:www.w3cschool.cn/electronman…

3.我遇到个不断重启webview网页的问题

在 WebView 初始化时设置 CacheMode、JavaScriptEnabled、MixedContentMode 等选项(缓存模式,加载混合内容)。 示例代码:

    skype.addEventListener("did-stop-loading", () => {

      //注入js脚本
const skypes=skype.getWebContents()
  // 设置 WebView 的缓存模式
  skypes.session.setCacheMode('prefer-cache')

  // 允许 WebView 加载混合内容
  skypes.session.setCertificateVerifyProc((request, callback) => {
    callback(0) // 返回 0 表示信任所有证书
  })
  })

5.常用命令行开关

5-1.chromium命令行开关

Electron 中的 chromium 内核支撑了窗口的创建和内容的渲染,而 chromium 内核对网页的渲染运行有很多默认设置,比如处于网页内容调用摄像头需要用户授权、音视频自动播放。

那么怎么知道自己什么时候需要去找这些命令行参数来辅助开发呢?

只要是窗口页面的功能不符合浏览器的设置或标准,都可以尝试去找相对应的 chromium 命令开关来解决实际问题。

这里引用大佬:链接:juejin.cn/post/685457…

详细齐全的命令行链接:shaozhuqing.com/?p=5326

//触摸屏上会出现点击范围扩大的问题,这个可以解决 
app.commandLine.appendSwitch('disable-touch-adjustment', true); //chrome 66以上版本屏蔽自动播放限制设置,音视频随心放
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required'); //触摸屏上禁用双指缩放 
app.commandLine.appendSwitch('disable-pinch', true); //关闭网络代理 
app.commandLine.appendSwitch('no-proxy-server', true); //关闭浏览器的证书拦截 
app.commandLine.appendSwitch('ignore-certificate-errors', true);

5-2. webPreferences命令的配置

      nodeIntegration: true,//是否开启node
      webviewTag: true,  //允许webview
      webSecurity: false,//允许跨域,上线时删除此配置
      plugins: true,  //允许插件
      contextIsolation: false,//解决require报错问题,我也不知道为啥

5-3. 基本的一些小操作

1.设置快捷键

const {
  app,
  BrowserWindow,
  globalShortcut,
  ipcMain
} = require('electron')

globalShortcut.register('CommandOrControl+M',()=>{
  mainWindow.maximize()
})
globalShortcut.register('CommandOrControl+T',()=>{
  mainWindow.unmaximize()
})
globalShortcut.register('CommandOrControl+H',()=>{
  mainWindow.close()
})

2. 设置热加载

// 热加载
const reloader=require('electron-reloader')
reloader(module)
//在on(ready)生命周期内部
// 自动打开开发者调试工具
  mainWindow.webContents.openDevTools()

3.两种加载页面html区别

//用于加载文件
  mainWindow.loadFile('./src/index.html')
//用于加载html,也可以加载外部网页  
  mainWindow.loadURL(startPage);

4.preload预加载脚本,沙盒化进程

沙盒模式就是渲染进程没有办法操作node,就像普通浏览器一样,相当于把每个渲染进程当成一个独立的空间

5.remote渲染进程和主进程可以相互访问对象和方法

Remote 模块允许主进程和渲染进程之间相互访问对象和方法。


//主进程中初始化
import * as remoteMain from '@electron/remote/main';
remoteMain.initialize();



//渲染进程中
const BrowserWindow = require("@electron/remote").BrowserWindow;
//通过remote拿到
const dialog = require("@electron/remote").dialog;
const globalShortcut = require("@electron/remote").globalShortcut;
// Electron的内置方法,选择多个文件
//使用主进程dialog方法
const res = dialog.showOpenDialogSync({
    title: "读取文件",
    buttonLabel: '读取',
    filters: [{
        name: 'Custom File Type',
        extensions: ['js']
    }],
})
 //使用主进程globalShortcut方法
 globalShortcut.register('Ctrl+A', () => {
    console.log('ctrl+o')
})

6.操作webview里面嵌入的网页

有两种方法:webview有注入js和css的方法,还可以使用预加载

//开始加载事件监听
    webview.addEventListener("did-start-loading", ()=> {
    console.log("did-start-loading...");
   
})
//停止加载事件监听
webview.addEventListener("did-stop-loading", ()=> {
    console.log("did-stop-loading...");
  
   注入css
    webview.insertCSS(`
    .title-blog {
        background: red !important;
    }
    `)
   注入js脚本
    webview.executeJavaScript(`
        setTimeout(()=>{
            alert("内容是:"+document.querySelector('._21AquerySelectorhp').innerHTML);
        }, 2000);
    `)
   /这里app的id能拿到标签,说明是能够操作嵌入的
   打开调试工具
    webview.openDevTools();
})

7.设置桌面置顶的功能

// 开启桌面文件置顶功能
   win.setAlwaysOnTop(true);

8.使桌面可以自定义拖拽大小

// 创建初始窗口
  win = new BrowserWindow({
    width: 500,
    height: 600,
    webPreferences: {
      nodeIntegration: true,//是否开启node
      webviewTag: true,  //允许webview
      webSecurity: false,//允许跨域,上线时删除此配置
      plugins: true,  //允许插件
      resizable: true,//可以拖拽窗口大小
    },
  });
  win.setResizable(true);//设置可以拖拽窗口大小

9.可以打开嵌入webviewd的对应网页的控制台,可以看到网页结构,控制台

还是位置和数据模式

生命周期mounted中

    const skype = this.$refs.skype;//拿到对应网页的webview,通过ref
    skype.addEventListener("dom-ready", () => {
      skype.openDevTools({
        mode: "detach",
      });
    });
    //监听页面加载完毕,打开配置可以打开嵌入的网页控制台,默认关闭了

10.监听webview嵌入网页的变化,mutationObserver监听页面变化

1.这个是js的原生方法,使用就是监听谁,开启什么监听,把这个两个参数传过去就行了

    const chatList = document.querySelector(".css-1dbjc4n .r-13awgt0"); // 获取聊天消息列表
      
      const observer = new MutationObserver(mutations => {
          for (const mutation of mutations) {
              if (mutation.type === "childList") {
                  console.log(123,'内容变化来了');
                  //alert('内容是'+chatList.innerHTML)
                  alert(mutation.target.textContent)
                  //mutation.target可以拿到变动的节点,textContent可以拿到变动节点的内容
              }
          }
      });
      observer.observe(chatList, { childList: true ,subtree:true});//这里开启所有的后代DOM节点监听subtree,childList会自动变成监听子孙节点

未完待续,欢迎私信我交流,共同进步