什么是 Electron?

介绍

  • 使用 JavaScript, HTML 和 CSS 构建跨平台(Windows、MacOs、Linux)的桌面应用——这是Electron官网的简介
  • 最初被GitHub开发,2013年4月11日以Atom Shell为名起步,2014年5月16日开源,2015年4月17日改名为Electron。

electron nsis 镜像 electron source_html

组成

electron nsis 镜像 electron source_chrome_02

  • Chromium : 为Electron提供了强大的UI能力,可以不考虑兼容性的情况下,利用强大的Web生态来开发界面。(本质上就是chromium(chrome开源版本)浏览器,有最新的东西都会在chromium测试,所以electron可以体验最新的api,这也是好处之一)

Chromium 多进程架构图

electron nsis 镜像 electron source_electron nsis 镜像_03

简单描述下。

主进程中的RenderProcessHost和 render 进程中的RenderProcess是用来处理进程间通信的(IPC(Inter-Process Communication,进程间通信)。

Render 进程中的 RenderView 内容基于 WebKit(浏览器引擎) 排版展示出来的

Render 进程中的ResourceDispatcher是用来处理资源请求的。Render 进程中如果有请求则创建一个请 求 ID,转发到 IPC,由 Browser 进程中处理后返回    

Chromium 是多进程架构,包括一个主进程,多个渲染进程

  • Node.js :让Electron有了底层的操作能力,比如文件的读写,甚至是集成C++等等操作,并可以使用大量开源的npm包来完成开发需求。
  • Native API : Native API让Electron有了跨平台和桌面端的原生能力,比如说它有统一的原生界面,窗口、托盘、消息通知这些。

通过三者的巧妙组合,我们开发应用变的十分高效。

Electron 架构

Electron 架构和 Chromium 架构类似,也是具有1个主进程和多个渲染进程。但是也有区别

  • 在各个进行中暴露了 Native API ,提供了 Native 能力。
  • 引入了 Node.js,所以可以使用 Node 的能力
  • 但是渲染进程使用node 需要配置,下文会有所提到

可以简单的理解为Electron为web项目套上了Node.js环境的壳,使得我们可以调用Node.js的丰富的API。这样我们可以用JavaScript来写桌面应用,拓展很多我们在web端不能做的事情。

示意图

electron nsis 镜像 electron source_html_04

Electron 进程

electron核心我们可以分成2个部分,主进程和渲染进程。

主进程

electron nsis 镜像 electron source_json_05

Electron 运行 package.json 的 main 脚本的进程被称为主进程 (只有一个

  • 主进程特点:
  • 主进程连接着操作系统和渲染进程,可以把她看做页面和计算机沟通的桥梁。
  • 进程间通信、窗口管理
  • 全局通用服务。
  • 一些只能或适合在主进程做的事情。例如浏览器下载、全局快捷键处理、托盘、session。
  • 维护一些必要的全局状态

渲染进程

electron nsis 镜像 electron source_前端_06

渲染进程就是我们所熟悉前端环境了。只是载体改变了,从浏览器变成了window.

注:出于安全考虑,渲染进程是不能直接访问本地资源的),因此都需要在主进程完成。

  • 渲染进程特点
  • Electron 使用了 Chromium 来展示 web 页面,所以 Chromium 的多进程架构也被使用到。
  • 每个web页面运行在它自己的渲染进程中。每个渲染进程都是相互独立的,并且只关心他们自己的网页。
  • 使用BrowserWindow类开启一个渲染进程并将这个实例运行在该进程中,当一个BrowserWindow实例被销毁后,相应的渲染进程也会被终止。
  • 渲染进程中不能调用原生资源,但是渲染进程中同样包含Node.js环境,所以可以引入Node.js(下文会提到:nodeIntegration )

主进程与渲染进程的区别

  • 主进程使用 BrowserWindow 实例创建网页。
  • 每个 BrowserWindow 实例都在自己的渲染进程里运行着一个网页。当一个 BrowserWindow 实例被销毁后,相应的渲染进程也会被终止。
  • 主进程管理所有页面和与之对应的渲染进程。
  • 由于在网页里管理原生 GUI 资源是非常危险而且容易造成资源泄露,所以在网页面调用 GUI 相关的 APIs 是不被允许的。如果你想在网页里使用 GUI 操作,其对应的渲染进程必须与主进程进行通讯,请求主进程进行相关的 GUI 操作。

electron nsis 镜像 electron source_chrome_07

把它们想象成这样

Chrome(或其他浏览器)的每个标签页(tab)及其页面,就好比 Electron 中的一个单独渲染进程。即使关闭所有标签页,Chrome 依然存在。这好比 Electron 的主进程,能打开新的窗口或关闭这个应用。

electron nsis 镜像 electron source_前端_08

Electron 优缺点

  • 优点

  • 上手简单
  • HTML、CSS、JS、Node 。npm包、UI框架 ,方便高效,能很轻松的实现很好看的UI
  • 多端运行 
  • 快速构建“跨平台”(Windows、MacOs、Linux)的桌面级应用 
  • 开发时间短
  •  相对其他跨平台方案(如 QT GTK+ 等),更稳定,bug少, 毕竟只要浏览器外壳跑起 来了就可以了,当然坑是少不了的 
  • 再也不用兼容多浏览器
  • 只针对谷歌 但要兼容mac、Linux
  • 缺点
  • 安装包体积略大(打包了Chromium) 至少包含了一个浏览器的体积 ,每装一个 app 就相当于装一个 chrome
  • 性能不如原生应用,mac下丝滑一些,window就有点丢帧 
  • 卡、启动慢、新开一个进程,起步价就是一个nodejs的内存开销 
  • loadURL加载远程页面白屏事件长,优化可采用 vscode 骨架屏

有哪些著名应用是用Electron开发

electron nsis 镜像 electron source_html_09

Postman、uTools、Typora、Atom、Brave浏览器 ......... 成千上万

Hello world

创建项目

最简单的目录

electron nsis 镜像 electron source_chrome_10

生成package.json文件并修改

npm init

// 修改package.json
{
  "name": "electron-app",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "devDependencies": {
    "electron": "^10.1.5"
  }
}

注意:控制台中文乱码可以使用

"start": "chcp 65001 && electron ."
// utf8的值是65001

安装 Electron

npm install electron --save-dev
// npm 安装会十分的慢,甚至是失败,可以先使用cnpm 淘宝镜像去下载,速度快
创建main.js文件
const { app, BrowserWindow } = require('electron') 

let mainWindow = null ;
app.on('ready',()=>{
    mainWindow = new BrowserWindow({ 
        width:500,
        height:500,
        webPreferences:{ 
          nodeIntegration:true //设置为true就可以在这个渲染进程中调用Node.js
        }
    });

    mainWindow.loadFile('index.html'); // 加载本地文件
    // mainWindow.loadURL('https://zhuiyi.ai/'); // 加载远程文件

    mainWindow.webContents.openDevTools({ mode: 'bottom' }); // 控制台开关
  
    mainWindow.on('close',(e)=>{ 
        // 在窗口要关闭的时候触发
                e.preventDefault(); // 避免进程意外关闭导致进程销毁
    });

    mainWindow.on('closed',()=>{ 
       // 当窗口已经关闭的时候触发
    });

});

app 模块是为了控制整个应用的生命周期设计的。

BrowserWindow参数: BrowserWindow | Electron 中文文档

创建html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World!</title>
</head>
<body>
    <h1>Hello World!</h1>
</body>
</html>

启动项目

electron .

electron nsis 镜像 electron source_json_11

运行过程

electron nsis 镜像 electron source_chrome_12

electron nsis 镜像 electron source_前端_13

也许你现在还不能理解这个流程,但是你需要记住这个流程,只有我们记住这个流程后,在以后程序出现问题时,才可以很快的定位问题.

自定义菜单

使用到的模块

  • menu 模块
  • menu类可以用来创建原生菜单,它可用作应用菜单和 context 菜单,这个模块是一个主进程的模块,并且可以通过 remote 模块给渲染进程调用,每个菜单有一个或几个菜单项 menu items,并且每个菜单项可以有子菜单.
  • Tray 模块
  • 用一个 Tray 来表示一个图标,这个图标处于正在运行的系统的通知区 ,通常被添加到一个 context menu 上.
  • remote 模块
  • 提供了一种在渲染进程(网页)和主进程之间进行进程间通讯(IPC)的简便途径。

窗口菜单

// 创建菜单模板
const template = [{
      label: '凤来怡洗浴会所',
      submenu: [
        {
          label: '精品SPA',
          click:()=>{
            console.log('SPA');
          }
        },
        {label: '泰式按摩'}
      ]
    },
    {
      label: '大浪淘沙洗浴中心',
      submenu: [
        {label: '来杯奶茶'},
        {label: '来个拔罐'}
      ]
    }
  ];
// 一定是在ready生命周期中 创建进程时设置菜单
app.on('ready', () => {
  const m = Menu.buildFromTemplate(template); // 创建菜单模板
  Menu.setApplicationMenu(m);
});

需要注意的是,Menu属于是主线程下的模块,要在渲染进程中使用的话需要借助 remote模块

自定义右键菜单

const {remote} = require('electron')
 const rigthTemplate = [{
          label: '粘贴',
                click: ()=>{
            // 事件
          }
        },
        {
          label: '复制'
        }
      ]
const m = remote.Menu.buildFromTemplate(rigthTemplate);
window.addEventListener('contextmenu', function (e) {
    //阻止当前窗口默认事件
  e.preventDefault();
  //把菜单模板添加到右键菜单
  m.popup({
      window: remote.getCurrentWindow()
  });
});

渲染进程使用remote模块的前提

webPreferences: {
    enableRemoteModule: true // 在初始化窗口的时候 允许渲染进程使用Remote模块 否则报错
 }

托盘图标菜单

let appIcon = null;
app.on('ready', function(){
  appIcon = new Tray(require('path').join(__dirname,'./assets/ban.png')); // 最好用path.join绝对位置方式应用图片
    cosnt contextMenu = Menu.buildFromTemplate([ //菜单配置
        {
            label: '退出',
            click: function () {
                app.quit();
            }
        }
    ]);
    // 设置托盘悬浮提示
    appIcon.setToolTip('never forget');
    // 设置托盘菜单
    appIcon.setContextMenu(contextMenu);
    // 单击托盘小图标显示应用
    appIcon.on('click', function () {
        // 显示主程序
      if(currentWin.isVisible()){
         currentWin.hide();
      }else{
         currentWin.show();
      }
    });
});

注意:appIcon 必须声明在app.on('ready)之外,防止被垃圾回收机制回收,导致托盘图标消失let appIcon = null; 

进程之间通信

主进程和渲染进程之间可以通过ipcRenderer 模块和 ipcMain 模块通信。

通信使用到的模块

  • ipcMain  模块
  • 是类 EventEmitter 的实例.当在主进程中使用它的时候,它控制着由渲染进程(web page)发送过来的异步或同步消息.从渲染进程发送过来的消息将触发事件.
  • ipcRenderer  模块
  • 是一个 EventEmitter 类的实例. 它提供了有限的方法,你可以从渲染进程向主进程发送同步或异步消息. 也可以收到主进程的相应.

主进程 —> 渲染进程

// 入口文件 main.js中 例如菜单中触发
 {
     label: '泰式按摩',
     click:()=>{
         console.log('泰式按摩 ');
         currentWin.webContents.send('message','我是主进程发送的参数');
    }
}

// 渲染进程 / web页面中 html (第一种方式)
 <script>
        const {ipcRenderer} = require('electron')
        ipcRenderer.on('message', (event, data) => {
           console.log('>>>>>>>>params',data) //我是主进程发送的参数
        });
</script>

//在创建窗口时  (第二种方式)  现阶段已经不推荐这样使用 容易报错坑多
currentWin.webContents.executeJavaScript(`
    const {ipcRenderer} = require('electron')
    ipcRenderer.on('message', (evt, data) => {
       console.log('>>>>>>>>params',data) //我是主进程发送的参数
        });
`)

渲染进程 —> 主进程

// web 页面中
<body>
    <h1>渲染进程 —> 主进程</h1>
    <button id="btn">发送参数</button>
    <script>
        const {ipcRenderer} = require('electron');
        let btn = document.getElementById('btn');
        btn.onclick = function () {
            ipcRenderer.send('params','我是渲染进程发送的参数');
        }
    </script>
</body>

//main.js文件中
ipcMain.on('params',(event,data)=>{
    console.log('>>>>>>>>params',data) // 我是渲染进程发送的参数
})
渲染
进程1 -> 渲染进程2
// 渲染进程1   web 页面中
<script>
  const {ipcRenderer} = require('electron')
   ipcRenderer.send('params','9453913')
</script>

// 渲染进程2   web 页面中
<script>
    const {ipcRenderer} = require('electron')
    ipcRenderer.on('params','9453913')
</script>
// 主进程作为中转
// 渲染进程1 -> 主进程 -> 渲染进程2
// 渲染进程2 -> 主进程 -> 渲染进程1

项目打包

通过electron-packager及electron-builder两种方式,将已有的electron应用打包成msi格式和exe可执行文件

electron-builder就是有比electron-packager有更丰富的的功能,支持更多的平台,同时也支持了自动更新。除了这几点之外,由electron-builder打出的包更为轻量,并且可以打包出不暴露源码的setup安装程序

npm install electron-builder --save-dev

package.json中做如下配置

"scripts": {
      "start": "electron .",
      "pack": "electron-builder --win --x64"
},
"build": {
    "appId": "test.app",
    "productName": "测试项目",
    "mac": {
      "target": ["dmg","zip"]
    },
    "win": {
      "target": ["nsis","zip"]
    }
}

注意:mac的dmg包 需要macos签名,所以最好mac打dmg的包

常见的报错

require is not defined

electron nsis 镜像 electron source_electron nsis 镜像_14

远程渲染页面(loadURL),页面使用require('electron'),容易报require 错误 undefined等,以下方法可解决

// 配置窗口时 添加 preload
webPreferences: {
    nodeIntegration: true, //渲染进程允许使用node
    preload: path.join(__dirname, '../static/preload.js'), // electron 与 独立vue项目使用
}

preload.js

/**
 * 挂在electron 在全局 在远程页面中可以读取
 */
window.electron = require('electron');

currentWin.webContents.executeJavaScript(不推荐使用)

electron nsis 镜像 electron source_chrome_15

electron nsis 镜像 electron source_html_16