作者:李晓健。苏宁视频云前端部门经理。拥有7年前端业务研发和架构经验,目前负责苏宁云视频前端研发和架构工作。

Electron简介

Electron 是一个使用 JavaScript,HTML 和 CSS 等 Web 技术创建原生桌面程序的框架。官网是https://electronjs.org/

Electron就是使用Web技术来开发,然后打包成桌面客户端的一个框架。它渲染Web内容使用的是Chromium,Chromium基本上和大家常用的chrome浏览器差不多,并且Electron在打包时是将Chromium嵌入到了应用里,所以它并不依赖用户机器上安装的浏览器,大家都知道做Web开发,最大的一个问题就是兼容性的问题,经常相同的代码在不同操作系统中不同的浏览器里,甚至不同版本的浏览器里都可能会出现不同的效果。Electron相当于是把一个固定版本的浏览器嵌入到应用中,所以他就不存在兼容性问题,只要我们开发时写的代码可以正常运行,就不存在打包后在用户的机器上因为兼容性问题无法使用的情况。

Electron支持Node.js的原生模块,也就是说可以在Electron应用中直接使用Node.js的模块。Web应用为了安全等一系列的问题考虑,限制了很多的功能,比如本地文件的读写、操作系统中硬件设备的调用等等,但是Node.js并没有这些限制。也就是说在Electron应用中,我们不光可以使用Web技术的特点,还能够突破Web技术的限制,使用Node.js的模块功能,使应用功能更加丰富。

Electron开发出来的应用是跨平台的,当应用开发完成后,我们可以将同一套代码打包成Linux、macOS、Windows平台上的可执行程序或安装包。

Electron中比较重要的概念

学习任何一门框架,里面或多或少都会有一些新的概念,只有把这些新的概念都弄懂了,才能更好的掌握和运用这门框架,Electron中新的概念并不是很多,接下来我就就来说说最重要的3个概念。

1. 主进程:Electron应用中运行package.json的main脚本的进程被称为主进程,在主进程中运行的脚本通过创建web页面来展示用户界面。一个 Electron 应用总是有且只有一个主进程。

2. 渲染进程:Electron应用中是通过在主进程里创建web页面来展示用户界面的,在主进程中可以通过BrowserWindow 实例创建页面,这里的一个web页面就是运行在一个渲染进程里的,一个Electron应用可以包含多个web页面,所以一个Electron 应用中是可以有多个渲染进程的,当一个 BrowserWindow 实例被销毁后,相应的渲染进程也会被终止。

3. IPC:既然有主进程和渲染进程,而主进程管控的是一个应用全局的设置和方法,而渲染进程只在一个web页面中存在,那他们肯定不会始终完全孤立的存在,有时也是需要有通信的,比如我在页面做了一个操作,整个应用需要去做一个响应,就是就需要用到主进程和渲染进程的通信。就里就有一个概念IPC(Inter-Proess Communication),进程中通信。IPC就提供了相应的方法来供主进程和渲染进程间进行通信。

所以他们大概的关系就是一个应用中有一个主进程,然后主进程可以创建多个渲染进程,主进程和渲染进程间通过IPC来通信。如下图:

Electron的安装

Electron是以Node.js中的模块的方式进行发布的,所以他的安装也非常简单,通过npm来进行安装就可以了。既然Electron是通过Node.js的模块起先发布的,那它的安装就需要依赖Node.js的环境,也就是需要在我们的电脑上安装Node.js,安装Node.js直接去其官网( https://nodejs.org)下载相应的安装包,然后按照默认的设置一路安装就行了,在安装Node.js的同时也会安装上npm,安装完成后检测Node.js和npm是否安装成功,只需要在命令行工具拦执行相应的版本查看命令就行了,如果能看到有版本号输出,则说明安装成功。如下图:

接下来我们就可以直接使用npm来安装我们所需要的模块了。首先进入到我们的项目开发目录,然后初始化好项目(创建好package.json文件),然后在该目录执行

npm install electron –save

然后等待安装完成就行了。

在这个安装过程中,可能由于网络问题会安装失败,我们可以修改npm的镜像源,将npm的镜像源修改成国内的镜像源:在命令行执行 npm config set registry http://registry.npm.taobao.org/ 或者安装cnpm,然后通过cnpm来安装Node.js的模块。

安装完成后,在命令行执行

electron -v

可以看到electron的版本号,就说明Electron安装成功了。

Electron项目的搭建

Electron安装好就,就可以搭建项目了。官网给我们提供了一个空的种子项目,这个项目里面非常的干净,所以可以直接使用,首先我们通过命令行进入我们工作目录,克隆下这个项目(前提是我们的电脑需要安装git),执行

git clonehttps://github.com/electron/electron-quick-start.git

克隆完成后,我们的工作目录中就多了一个 electron-quick-start 目录,这个目录就是

我们的项目目录,这里面有几个文件是我们项目需要用到的文件

1. package.json:项目的一些依赖和配置文件,里面有一些我们项目的基本信息,我们可以根据自己项目的情况来修改这里的值 bash

{
  "name": "electron-quick-start", //项目的名称
  "version": "1.0.0", //项目的版本号
  "description": "A minimal Electron application", //项目的描述
  "main": "main.js", //主入口文件
  "scripts": { //快捷脚本
    "start": "electron ."
  },
  "repository": "https://github.com/electron/electron-quick-start", //代码仓库
  "keywords": [ //项目的关键词
    "Electron",
    "quick",
    "start",
    "tutorial",
    "demo"
  ],
  "author": "GitHub", //项目的作者
  "license": "CC0-1.0", //项目的许可协议
  "devDependencies": { //项目的依赖
    "electron": "^2.0.0"
  }
}

2. main.js:这个文件是项目的启动文件,也是项目的入口文件,该文件就是运行在主进程中的。

// 引入electron中的API

const {app, BrowserWindow} = require(‘electron’)

//定义一个web窗口 这个窗口就运行在一个一个渲染进程中

let mainWindow

//创建web窗口的方法

function createWindow () {

  // 通过BrowserWindow来创建一个窗口

  mainWindow = new BrowserWindow({width: 800, height: 600})

  // 在web窗口中加载html页面

  mainWindow.loadFile(‘index.html’)

  // 监听窗口关闭的事件

  mainWindow.on(‘closed’, function () {

    mainWindow = null

  })

}

// 当应用准备好了  就调用创建web窗口的方法

app.on(‘ready’, createWindow)

// 当所有的窗口都关闭后,就退出应用

app.on(‘window-all-closed’, function () {

    //在非MAC平台下退出应用

    //在MAC平台下所有窗口关闭 应用并不需要退出,它还可以通过dock来重新激活应用

  if (process.platform !== ‘darwin’) {

    app.quit()

  }

})

//当应用被激活事件 

app.on(‘activate’, function () {

  //如果窗口不存在 就创建一个窗口

  if (mainWindow === null) {

    createWindow()

  }

})

3. index.html:这是web窗口里展示需要展示的内容,当然这里也可以是一个远程html的url

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
    <!-- All of the Node.js APIs are available in this renderer process. -->
    We are using Node.js <script>document.write(process.versions.node)</script>,
    Chromium <script>document.write(process.versions.chrome)</script>,
    and Electron <script>document.write(process.versions.electron)</script>.

    <script>
      // You can also require other files to run in this process
      require('./renderer.js')
    </script>
  </body>
</html>

这里就是一个普通的html,但是这里面也有和普通html不同之处,我们可以看到html的script标签里有用到process.xxx.xxx;并且前面没没有定义process,这里确可以直接访问,html里引入script用了require(‘./renderer.js’)。这些写法在我们平常的script里都会报错的,但是这里确可以正常运行的,原因就是我们前面说过的Electron应用中可以直接使用Node.js的Api,这里的process就是Node.js里的变量,require就是Node.js中引入外部文件的关键字。

renderer.js:这个文件是被引入到index.html中的,所以它就是用来写html中逻辑的文件,所以这个文件也是运行在渲染进程中的。

我们上面说到的4个文件的文件名和路径并不是固定的,也可以根据项目需要进行修改,他们的关系是main.js 就是 package.json中main属性对应的值,如果需要修改main.js的名称,这两处需要同时修改。index.html 就是在main.js中 mainWindow.loadFile(‘index.html’) 引入的文件,如果需要修改这两处也需要同时修改。renderer.js就是在index.html中require(‘./renderer.js’) 引入的js文件,当然这里也可以引入多个js谁的,和我们普通的html一样。

文件作用和关系都理清楚了,我们就可以安装依赖了。在命令行执行

npm install

等待安装完成之后,就可以运行项目了,在命令行执行

npm run start

这里的start 就是我们在package.json中scripts属性中配置的值 “start”: “electron .”,我们在命令行运行 npmrun start 就相当于我们运行 npm run electron . ;因为有时有些命令比较长或带了比较多的参数,所以我们在package.json中配置一些别名会大大减少我们在命令行的输入。等命令运行完成我们就可以看到我们的桌面上打开了一个客户端,如下图:

这样我们的一个初始项目就算搭建完成了。

Electron中常用的功能模块

electron不光是可以将我们的页面打包成客户端应用,它也为我们提供了大量的功能模块,方便我们的开发,下面我们就简单介绍一下用的比较多的功能模块。

1. app: 控制应用程序的事件生命周期,用于监听应用的的事件,该模块在主进程中。

2. BrowserWindow:创建和控制浏览器窗口,在创建窗口时可以传入很多的参数,来控制窗口的样式和行为该模块在主进程中。

3. ipcMain:从主进程到渲染进程的异步通信,该模块在主进程中。

4. ipcRenderer:从渲染进程到主进程的异步通信,该模块在渲染进程中。

5. remote:在渲染进程中引入主进程模块,该模块在渲染进程。

6. dialog:显示用于打开和保存文件、警报等的本机系统对话框,该模块在主进程中。

7. crashReporter:将崩溃日志提交给远程服务器

8. globalShortcut:当应用程序没有键盘焦点时监听全局键盘事件,该模块在主进程中。

9. Menu:创建原生应用菜单和上下文菜单,该模块在主进程中。

10. desktopCapturer:可以访问那些用于从桌面上捕获音频和视频的媒体源信息,该模块在渲染进程中。

11. net:使用Chromium的原生网络库发出HTTP/ HTTPS请求,该模块在主进程中。

12. autoUpdater:使应用程序能够自动更新,该模块在主进程中。

以上的这些模块都是比较常用的模块,所有的功能模块并不是在任何地方都可以调用,有些模块在主进程中可以直接引入,有些模块只能在渲染进程中引入,当然也有些模块可以直接主进程和渲染进程中引入使用。这些在主进程中的模块也是可以在渲染进程中引入的,不过不能直接引入,需要通过remote模块来间接引入,所以在主进程中的模块也是可以在渲染进程中使用,例如我们想在渲染进程中使用BrowserWindow:

//我们需要先引入remote模块
const {remote} = require('electron');
//再从remote模块中引入BrowserWindow
const {BrowserWindow} = remote
let win = new BrowserWindow({width: 800, height: 600})
win.loadURL('http://www,pptvyun.com')

更多的功能模块和使用方法可以参考Electron的文档:https://electronjs.org/docs

Electron的Demo

Electron的大概内容已经介绍的差不多了,接下来我们就使用Electron来做一个拍照保存的小demo,能正常运行需要您的电脑上有摄像头。我们的这个demo就基于前面搭建好的空项目来做。前面我们说过,项目的文件名和路径并不是固定的,接下来我们就来调整一下前面那个项目的目录结构,我们将我们需要写代码都放到src目录下,调整个目录如下:

我们将html代码放到view目录下,将样式文件放到style目录下,将图片放入到images目录下,将我们的js代码放到scripts目录下,将main.js改名为index.js(这里改文件只是为了说明这个文件名是可以修改的,并不是固定的),然后直接放到src目录下。

package.json 修改如下

{
  "name": "take-photo",
  "version": "0.0.1",
  "description": "A take photo Electron application",
  "main": "src/index.js",
  "scripts": {
    "start": "electron ."
  },
  "repository": "",
  "keywords": ["Electron","demo"],
  "author": "pptvyun",
  "license": "MIT",
  "devDependencies": {
    "electron": "^2.0.0"
  }
}

index.js 修改如下

// 引入electron中的API

const {app, BrowserWindow} = require(‘electron’)

//定义一个web窗口 这个窗口就运行在一个一个渲染进程中

let mainWindow

//创建web窗口的方法

function createWindow () {

    // 通过BrowserWindow来创建一个窗口

    mainWindow = new BrowserWindow({width: 800, height: 600})

    // 在web窗口中加载html页面

    mainWindow.loadFile(`${__dirname}/view/index.html`)

    // 监听窗口关闭的事件

    mainWindow.on(‘closed’, function () {

        mainWindow = null

    })

}

// 当应用准备好了  就调用创建web窗口的方法

app.on(‘ready’, createWindow)

// 当所有的窗口都关闭后,就退出应用

app.on(‘window-all-closed’, function () {

    //在非MAC平台下退出应用

    //在MAC平台下所有窗口关闭 应用并不需要退出,它还可以通过dock来重新激活应用

    if (process.platform !== ‘darwin’) {

        app.quit()

    }

})

//当应用被激活事件

app.on(‘activate’, function () {

    //如果窗口不存在 就创建一个窗口

    if (mainWindow === null) {

        createWindow()

    }

})

index.html 修改如下

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
    <!-- All of the Node.js APIs are available in this renderer process. -->
    We are using Node.js <script>document.write(process.versions.node)</script>,
    Chromium <script>document.write(process.versions.chrome)</script>,
    and Electron <script>document.write(process.versions.electron)</script>.

    <script>
       //这里修改了renderer.js的引入路径
      require('../scripts/renderer.js')
    </script>
  </body>
</html>

接下来我们就根据我们的业务来写具体的代码,首先我们的代码肯定是需要调试的,Electron使用的是Chromium来做渲染的,所以它也给我们提供了chrome的开发者工具供我们使用。开发完成后的代码如下

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>take photo</title>
    <link rel="stylesheet" href="../style/index.css">
</head>
<body>
<header>拍照保存</header>
<div class="content">
    <video src="" id="video"></video>
    <canvas width="250" height="187" id="canvas"></canvas>
    <div class="button-box">
        <a href="javascript:;" id="openCamera" class="button">打开摄像头</a>
        <a href="javascript:;" id="takePhoto" class="button">拍摄照片</a>
        <a href="javascript:;" id="savePhoto" class="button">保存照片</a>
    </div>
</div>

<script>
    require('../scripts/renderer.js')
</script>
</body>
</html>

src/style/index.css

src/index.js

// 引入electron中的API
const {app, BrowserWindow,globalShortcut} = require('electron')

//定义一个web窗口 这个窗口就运行在一个一个渲染进程中
let mainWindow

//创建web窗口的方法
function createWindow () {
    // 通过BrowserWindow来创建一个窗口
    mainWindow = new BrowserWindow({width: 800, height: 600})

    // 在web窗口中加载html页面
    mainWindow.loadFile(`${__dirname}/view/index.html`)

    // 监听窗口关闭的事件
    mainWindow.on('closed', function () {
        mainWindow = null
    })
    //这里我们注册一个打开 开发者工具有快捷键 win下是Ctrl+F12 macOS下是command+f12
    globalShortcut.register('CommandOrControl+F12', () => {
        mainWindow.webContents.openDevTools()
    })
}

// 当应用准备好了  就调用创建web窗口的方法
app.on('ready', createWindow)

// 当所有的窗口都关闭后,就退出应用
app.on('window-all-closed', function () {
    //在非MAC平台下退出应用
    //在MAC平台下所有窗口关闭 应用并不需要退出,它还可以通过dock来重新激活应用
    if (process.platform !== 'darwin') {
        app.quit()
    }
})

//当应用被激活事件
app.on('activate', function () {
    //如果窗口不存在 就创建一个窗口
    if (mainWindow === null) {
        createWindow()
    }
})

src/sctipts/renderer.js

//引入Electron中的dialog 用来显示消息提示
const {dialog} = require('electron').remote
//引入Node.js的fs模块  用来写文件
const fs = require('fs');
//页面打开摄像头的按钮
const button = document.querySelector('#openCamera');
//页面拍摄照片的按钮
const takephoto = document.querySelector('#takePhoto');
//页面保存照片的按钮
const saveImage = document.querySelector('#savePhoto');
//页面的video标签,用来展示摄像头拍摄的画面
const video = document.querySelector('#video');
//页面的canvas标签,用来展示拍摄的照片
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');

//获取摄像头的参数
const mediaConfig = {
    video: true
}
//点击打开摄像头的按钮
button.addEventListener('click',e=>{
    //调用电脑的摄像头
    navigator.mediaDevices.getUserMedia(mediaConfig)
        .then(function(stream) {
            //将摄像头拍摄的内容给到video标签,并从video标签上播放
            video.srcObject = stream;
            video.onloadedmetadata = function(e) {
                video.play();
            };
        })
        .catch(function(err) {
            alert('打开挺像头失败')
        });
})

//点击拍照按钮
takephoto.addEventListener('click',e=>{
    //将video上的画面画到canvas上
    ctx.drawImage(video,0,0,250,187);
})
//点击保存图片按钮
saveImage.addEventListener('click',e=>{
    //选择图片的保存路径
    dialog.showOpenDialog({properties:['openDirectory']},filePaths=>{
        if(filePaths && filePaths[0]){
            //保存图片的目录
            const floder = filePaths[0]+'/';
            //奖canvas上的图片转成base64编码
            let dataURL = canvas.toDataURL('image/png');
            var regex = /^data:.+\/(.+);base64,(.*)$/;
            if(dataURL){
                var matches = dataURL.match(regex);
                var buffer = new Buffer(matches[2], 'base64');
                //将文件流写入选择的目录里
                const file = floder+Date.now()+'.' + matches[1];
                fs.writeFile(file, buffer,err=>{
                    if(err){
                        dialog.showErrorBox("错误提示", '图片保存失败')
                    }else{
                        //图片保存成功的提示
                        dialog.showMessageBox({
                            type:'info',
                            title:'提示信息',
                            message:'图片保存成功,文件保存在'+file,
                        })
                    }
                });
            }
        }
    })
})

代码开发完成后,运行项目,在命令行输入

npm run start

就可以看到项目结果了,页面会出现3个按钮,点击打开摄头,客户端上就会出现摄像头拍摄的内容,然后点击保存照片按钮,客户端就会出现一张静态的图片,然后点击保存按钮,这张图片就会保存在我们选择的目录下。效果如下图:

Electron项目的构建

这个demo开发到这里就算完成了,但是我们的客户端还是需要给别人来使用,总不能让别人把我们的代码拷过去,然后安装依赖,在命令行运行npm run start吧,我们常见的客户端都是有一个安装程序,安装之后就可以直接双击运行的,或者是一个绿色版,有一个可执行程序,我们双击就可以运行的。接下来我们就也把我们的demo打包成一个安装程序,可供用户安装使用。

将Electron打包成安装程序有多种方式,今天我们说的是通过 electron-builder 模块来打包,electron-builder也是一个Node.js的模块,所以我们可以直接通过npm来安装,在命令行运行

npm install electron-builder –save-dev

等待安装完成就可以了,它的文档在https://github.com/electron-userland/electron-builder ,它可以打包出window、macOS、Linux上的可执行程序。然后我们参照文档修改配置文件,这里只需要修改package.json就好。修改后如下

{
  "name": "take-photo",
  "version": "0.0.1",
  "description": "A take photo Electron application",
  "main": "src/index.js",
  "scripts": {
    "start": "electron .",
    "build-all": "electron-builder -mwl",
    "build-win": "electron-builder --win --x64",
    "build-mac": "electron-builder --mac --x64",
    "build-linux": "electron-builder --linux --x64"
  },
  "build": {
    "appId": "com.pptvyun.test",
    "copyright": "pptvyun",
    "productName": "demo",
    "mac": {
      "target": "dmg"
    },
    "dmg": {
      "window": {
        "x": 200,
        "y": 200,
        "width": 800,
        "height": 600
      }
    },
    "win": {
      "icon": "logo.png",
      "target": "nsis"
    },
    "nsis": {
      "oneClick": false,
      "allowToChangeInstallationDirectory": true,
      "artifactName": "Setup-${productName}-${version}-${arch}.${ext}"
    },
    "snap": {}
  },
  "repository": "",
  "keywords": [
    "Electron",
    "demo"
  ],
  "author": "pptvyun",
  "license": "MIT",
  "devDependencies": {
    "electron": "^2.0.0",
    "electron-builder": "^20.19.2"
  }
}

在scripts中添加了在各个平台上的打包脚本,添加了build属性,用来配置在各个平台上的打包参数。我们用打包出window平台的安装程序为例,在命令行运行

npm run build-win

等待打包完成,第一次可能会有点慢,因为它需要下载一些依赖,打包完成后我们的项目里就会多出来一个build的目录,当然这个目录是我们在package.json中配置的,这个目录里就是打包之后的文件,里面有一个win-unpacked的目录,这个目录里有一个demo.exe的文件,这个文件就是可执行程序,这个win-unpacked目录里的文件就相当于是我们平常见的绿色免安装版,直接双击demo.exe就可以运行这个demo。还有一个Setup-demo-0.0.1.exe文件,这个文件就是我们需要的安装程序,双击它就可以运行安装,安装完成后也会在我们的桌面创建快捷方式,不同平台的打包命令可能需要到相应的平台上去执行,比如在window上执行macOS的打包命令可能会失败。

至此,我们的demo就算开发完成了,当然这里还有一些细节上的东西需要注意,比如我们的应用需要在各大平台上发布,供其他用户来下载安装,就需要对代码进行签名,代码签名具体可参考文档https://electronjs.org/docs/tutorial/code-signing ,这里的客户端界面我们用的是默认的,当然我们也可以做一个无边框的应用,然后自定义客户端的界面,还有自动升级等等功能,Electron都是可以支持的,可以查看它的官方文档做出功能丰富的客户端面应用。

Comments are closed.