一个小时学会 Electron

一、关于文章

本文主要是针对有 HTML,CSS 和 JavaScript 基础的同学,如果你对 HTML,CSS 和 JavaScript 不了解也没有关系,但是你得有其他编程语言的基础。其实所有的编程语言,套路都基本相似,只要学过一门编程语言,学其他的编程相对来说是很简单的。咱们这遍文章呢,当然也不只是针对 coder(编码的人)。如果你是一个架构师,也许这篇文章将会给带来一些桌面应用架构的灵感。如果你是一个产品经理,本文可以帮助寻求桌面应用产品设计上的解决方案。当然,如果你是 coder 的话,这篇文章将会提升你的编码境界,让你有一种豁然开朗的感觉。尤其是前端的开发人员。

二、Electron 简介

enter image description here

Electron 是用 HTML、CSS 和 JavaScript 来构建跨平台桌面应用程序的一个开源库。它将 Chromium 和 Node.js 合并到同一个运行环境中,也是就说 Electron 其实就是谷歌浏览器加 Node.js,但你使用 Electron 来装载界面的时候,你不用考虑浏览器的兼容性,只要你的前端项目能够在谷歌浏览器中正常运行,那么你的项目就可以正常地在 Electron 中运行。用 Electron 开发的桌面应用是可以跨平台的,在开发模式下或者是生成模式下同样的一套代码可以运行在不同平台,打包之后的应用可以运行在 Windows、Mac 和 Linux 等不同的平台上。Electron 中引入一个很重的机制,主进程和渲染进程,关于主进程和渲染进程,咱们在文章后面会有很详细的介绍。用Electron写项目的时候,你也不用担心代码的调试问题,Electron中引入来谷歌浏览器的开发者工具,只需要一段很少的代码就在项目中配置谷歌浏览器调试工具。Electron 给我们提供来很丰富的 API,这些 API 在你编码的过程中将会给你减去不少的代码量。咱们在这里可以一个简单的例子,假设现在你要开发一款区块链桌面应用钱包,备份私钥的时候,你需要去打开一个目录,然后生成一个私钥文件,将这个私钥文件存储到你打开的这个目录中,那么你只需要下面这段代码就能做到打开一个目录,并返回这个目录的路径。

var path = dialog.showOpenDialog({properties: ['openFile', 'openDirectory', 'multiSelections']});

path 变量就是你打开的那个目录的路径,给大家看一段我以前写钱包时候做的导出私钥的 Node.js 代码,看了之后大家也许就很明白这个代码的简洁了。下面这段代码你不用尝试去运行,因为这段代码是我从项目中摘取出来的,直接运行是运行不起来的,就是为了帮助大家了解 Electron 而已。

    const fs= require("fs");     const {dialog} = require('Electron')     const storePrivateKey = _storePrivateKeyIpc => ({       ['store-privateKey'](event, storeKey) {         const requestBack = data => {           _storePrivateKeyIpc.sendToClient('store-privateKey-back', data)         };         if(storeKey == null) {           console.log("Receive storeKey from front success and store key is null")         } else {           console.log("Receive storeKey private key from front success and private key is " + storeKey.privateKey);           var path = dialog.showOpenDialog({properties: ['openFile', 'openDirectory', 'multiSelections']});           fs.writeFile(path + '/privateKey.ert', storeKey.privateKey, {flag:'w',encoding:'utf-8',mode:'0666'}, function(err){             if(err){               console.log("write private key to file fail")               requestBack({                 success:false,                 writeMsg:"write private key to file fail",               })             }else{               console.log("write private key to file success");               requestBack({                 success:true,                 writeMsg:"success",               })             }           })         }       }     });     export default storePrivateKey

当然,Electron 的是代码变得简单而又简洁的优势并不只是这些,上面这些都是写废话,下面咱们正是进入 Electron 的学习。

三、Electron 的开发环境搭建

对于一个开发者来说,环境的搭建是第一步,也是最重要的一步,如果开发环境都搞不了,何谈开发呢。

3.1 Node 安装

关于 Node.js 的安装,网上的教程很多,这里我就不多说了,但我建议安装 node 的时候,最好安装 v8.11.2 版本的,当然你也可以安装其他版本的。当你安装了 Node.js 之后,node 有一个自带的 npm 工具,我们需要用它来安装 Electron。npm 是安装国外镜像的工具,如果你不能翻墙的话,建议你使用 cnpm 工具,这个工具安装的都是淘宝的镜像。在这里我顺便说一嘴,node 的版本管理,如果你想在你的机器上安装很多版本的 Node.js,那么我建议你安装一个 nvm 工具,这个工具可以很好地帮助你管理你的 node 的版本。

3.2 Electron 的安装

3.2.1 安装
    npm install Electron --save-dev
3.2.2 全局安装
    npm install Electron -g
2.3.自定义

如果需要安装某一位数的版本(例如,在 x64 位的系统中安装 ia32 位版本),则可以使用 npm 中的 arch 命令,或可以通过设置 npm_config_arch 的环境变量来进行安装:

    npm install --arch=ia32 Electron

此外, 您还可以使用 --platform 来指定开发平台(例如 win32、linux 等):

    npm install --platform=win32 Electron

四、使用 Electron 来做一个小案例

搭建完环境之后,咱们就要进入实战了,通过这个小案例,我将带领大家了解 Electron 框架,了解 Electron 的一些特性,如果你可以完整的了解这个案例,那么我相信你就达到可以开发 Electron 桌面应用的水平了。

4.1 代码目录截图

enter image description here

4.2 package.json 源码解析

    {       "name": "StadyCase",       "version": "1.0.0",       "description": "a case for study",       "main": "main.js",       "scripts": {         "start": "Electron ."       },       "keywords": [         "case"       ],       "author": "guoshijiang",       "license": "ISC",       "dependencies": {         "Electron": "^2.0.2"       }     }

name 是这个应用的名字,version 该应用的版本号,description 项目的描述信息,main 主进程的入口程序,scripts 项目的启动脚本明亮,keywords 项目的关键字子,author 项目的坐着,license 项目的 licenses,dependencies 项目的第三方依赖包。当你启一个项目的时候,通 npm init 命令去生成和填写这个文件。

4.3 主进程的代码解析

4.3.1 完整版代码
    const Electron = require('Electron');     const url = require('url');     const path = require('path');     const {app, BrowserWindow, Menu, ipcMain} = Electron;     process.env.NODE_ENV = 'production';     let mainWindow;     let addWindow;     app.on('ready', function () {         mainWindow = new BrowserWindow({});         mainWindow.loadURL(url.format({             pathname:path.join(__dirname, "mainWindow.html"),             protocol:'file',             slashes:true         }));         mainWindow.on('close', function () {             app.quit();         });         const mainMenu = Menu.buildFromTemplate(mainMenuTemplate);         Menu.setApplicationMenu(mainMenu);     });     function createAddWindow() {         addWindow = new BrowserWindow({             width:300,             height:200,             title:'Add Shopping List Item'         });         addWindow.loadURL(url.format({             pathname:path.join(__dirname, "addWindow.html"),             protocol:'file',             slashes:true         }));         addWindow.on('close', function () {             addWindow = null;         });     }     ipcMain.on('item:add', function (e, item) {         mainWindow.webContents.send('item:add', item);         addWindow.close();     });     const mainMenuTemplate = [         {             label:'File',             submenu:[                 {                     label: 'Add Item',                     click(){                         createAddWindow();                     }                 },                 {                     label: 'Clear Items',                     click()                     {                         mainWindow.webContents.send('item:clear');                     }                 },                 {                     label: 'Quit',                     accelerator:process.platform == 'darwin' ? 'Command + Q' :                         'Ctrl + Q',                     click(){                         app.quit();                     }                 }             ]         }     ];     if(process.platform == 'darwin') {         mainMenuTemplate.unshift({});     }     if(process.env.NODE_ENV !== 'production'){         mainMenuTemplate.push({            label:'Developer Tools',            submenu:[                {                    label:'Toggle DevTools',                    accelerator:process.platform == 'darwin' ? 'Command + I' :                        'Ctrl + I',                    click(item, focuseWindow){                        focuseWindow.toggleDevTools();                    }                },                {                    role:'reload'                }            ]         });     }
4.3.2 代码解析

Node.js 引入依赖的代码,这个项目依赖的第三方包有 Electron、url、path:

    const Electron = require('Electron');     const url = require('url');     const path = require('path');     const {app, BrowserWindow, Menu, ipcMain} = Electron;

配置成生产环境

    process.env.NODE_ENV = 'production';

加载主窗口,监听事件,加载菜单项

    app.on('ready', function () {             mainWindow = new BrowserWindow({});             mainWindow.loadURL(url.format({                 pathname:path.join(__dirname, "mainWindow.html"),                 protocol:'file',                 slashes:true             }));             mainWindow.on('close', function () {                 app.quit();             });             const mainMenu = Menu.buildFromTemplate(mainMenuTemplate);             Menu.setApplicationMenu(mainMenu);         });

加载子窗口

    function createAddWindow() {             addWindow = new BrowserWindow({                 width:300,                 height:200,                 title:'Add Shopping List Item'             });             addWindow.loadURL(url.format({                 pathname:path.join(__dirname, "addWindow.html"),                 protocol:'file',                 slashes:true             }));             addWindow.on('close', function () {                 addWindow = null;             });         }

主进程,接受渲染进程传过的值:

    ipcMain.on('item:add', function (e, item) {             mainWindow.webContents.send('item:add', item);             addWindow.close();         });

主菜单配置模版

    const mainMenuTemplate = [             {                 label:'File',                 submenu:[                     {                         label: 'Add Item',                         click(){                             createAddWindow();                         }                     },                     {                         label: 'Clear Items',                         click()                         {                             mainWindow.webContents.send('item:clear');                         }                     },                     {                         label: 'Quit',                         accelerator:process.platform == 'darwin' ? 'Command + Q' :                             'Ctrl + Q',                         click(){                             app.quit();                         }                     }                 ]             }         ];

配置相应的运行平台

    if(process.platform == 'darwin') {             mainMenuTemplate.unshift({});         }

配置谷歌的调试工具

    if(process.env.NODE_ENV !== 'production'){         mainMenuTemplate.push({            label:'Developer Tools',            submenu:[                {                    label:'Toggle DevTools',                    accelerator:process.platform == 'darwin' ? 'Command + I' :                        'Ctrl + I',                    click(item, focuseWindow){                        focuseWindow.toggleDevTools();                    }                },                {                    role:'reload'                }            ]         });     }

4.4 渲染进程的代码解析

4.4.1 mainWindow.html 完整代码
    <!DOCTYPE html>     <html>     <head>         <title>ShoppingList</title>     </head>     <body>         <nav>             <div>                 <a class="brand-log, center" href="#">ShoppingList</a>             </div>         </nav>         <ul id="ul"></ul>         <script>             const Electron = require('Electron');             const { ipcRenderer } = Electron;             const ul = document.getElementById('ul');             //add item             ipcRenderer.on('item:add', function (e, item){                 const li = document.createElement('li');                 li.innerHTML = item;                 ul.appendChild(li);             });             //clear item             ipcRenderer.on('item:clear', function (){                 ul.innerText = '';             });         </script>     </body>     </html>
4.4.2 addWindow.html 完整代码
    <!DOCTYPE html>     <html>     <head>         <title>Add Shopping List Item</title>     </head>     <body>         <div>             <form>                 <div>                     <label>Enter Item</label>                     <input type="text" id="item" autofocus>                 </div>                 <button type="submit">add Item</button>             </form>         </div>     </body>     <script>         const Electron = require('Electron');         const { ipcRenderer } = Electron;         const form = document.querySelector('form');         form.addEventListener('submit', submitForm);          function submitForm(e) {             e.preventDefault();             const item = document.querySelector('#item').value;             ipcRenderer.send('item:add', item);         }     </script>     </html>
4.4.3 渲染进程的源码解析

渲染进程发送一个消息到主进程

    const Electron = require('Electron');     const { ipcRenderer } = Electron;     const form = document.querySelector('form');     form.addEventListener('submit', submitForm);      function submitForm(e) {         e.preventDefault();         const item = document.querySelector('#item').value;         ipcRenderer.send('item:add', item);     }

渲染进程接收主进程消息

     const Electron = require('Electron');      const { ipcRenderer } = Electron;      const ul = document.getElementById('ul');      //add item      ipcRenderer.on('item:add', function (e, item){           const li = document.createElement('li');           li.innerHTML = item;           ul.appendChild(li);      });      //clear item      ipcRenderer.on('item:clear', function (){           ul.innerText = '';       });

4.5 本案列的代码流程图

enter image description here

五、Electron 原理分析

enter image description here

从上面这幅图中可以看出,Electron 是一个 Chromium + Node.js + NativeApis 的项目,这个项目集成的这些组件是 Electron 的核心Chromium 是 Google 为发展 Chrome 浏览器而启动的开源项目,Chromium 相当于 Chrome 的工程版或称实验版(尽管 Chrome 自身也有 β 版阶段),新功能会率先在 Chromium 上实现,待验证后才会应用在Chrome 上,故 Chrome 的功能会相对落后但较稳定。Electron API就像Node一样,被设计成支持用户开发模块和应用程序。

六、Electron 主进程和渲染进程

6.1 主进程

ipcMain 是 Electron 的主进程的 EventEmitter 的实例,当在主进程中使用时,它处理从渲染器进程(网页)发送出来的异步和同步信息。 从渲染器进程发送的消息将被发送到该模块。

  • 发送消息时,事件名称为 channel。

  • 回复同步信息时,需要设置 event.returnValue。

  • 将异步消息发送回发件人,需要使用 event.sender.send(...)。

下面是在渲染和主进程之间发送和处理消息的一个例子:

主进程

      const {ipcMain} = require('Electron')       ipcMain.on('asynchronous-message', (event, arg) => {         console.log(arg) // prints "ping"         event.sender.send('asynchronous-reply', 'pong')       })       ipcMain.on('synchronous-message', (event, arg) => {         console.log(arg) // prints "ping"         event.returnValue = 'pong'       })

在渲染器进程(网页)中

      const {ipcRenderer} = require('Electron')       console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"       ipcRenderer.on('asynchronous-reply', (event, arg) => {         console.log(arg) // prints "pong"       })       ipcRenderer.send('asynchronous-message', 'ping')

6.1.1 方法

IpcMain 模块有以下方法来侦听事件:

监听 channel,当接收到新的消息时 listener 会以 listener(event, args...) 的形式被调用。

    ipcMain.on(channel, listener)     channel String     listener Function

添加一次性的 listener。当且仅当下一个消息发送到 channel 时 listener 才会被调用,随后 <0>listener</0> 会被移除。

    ipcMain.once(channel, listener)     channel String     listener Function

从监听器数组中移除监听 channel 的指定 listener。

    ipcMain.removeListener(channel, listener)     channel String     listener Function

删除所有监听者,或特指的 channel 的所有监听者.

    ipcMain.removeAllListeners([channel])     channel String
6.1.2 事件对象

传递给 callback 的 event 对象有如下方法: 将此设置为在一个同步消息中返回的值.

    event.returnValue

返回发送消息的 webContents ,你可以调用 event.sender.send 来回复异步消息,

    event.sender

6.2 渲染进程

ipcRenderer 是渲染进程的 EventEmitter 的实例。 你可以使用它提供的一些方法从渲染进程(web 页面)发送同步或异步的消息到主进程。也可以接收主进程回复的消息。

案例请看上面的主进程。

6.2.1 方法

监听 channel,当新消息到达,将通过 listener(event, args...) 调用 listener。

    ipcRenderer.on(channel, listener)     channel String     listener Function

为事件添加一个一次性用的 listener 函数。这个 listener 只有在下次的消息到达 channel 时被请求调用,之后就被删除了.

    ipcRenderer.once(channel, listener)     channel String     listener Function

为特定的 channel 从监听队列中删除特定的 listener 监听者。

    ipcRenderer.removeListener(channel, listener)     channel String     listener Function

移除所有的监听器,当指定 channel 时只移除与其相关的所有监听器。

    ipcRenderer.removeAllListeners(channel)     channel String

通过 channel 发送异步消息到主进程,可以携带任意参数。 在内部,参数会被序列化为 JSON,因此参数对象上的函数和原型链不会被发送。

    ipcRenderer.send(channel[, arg1][, arg2][, ...])     channel String     ...args any[]

主进程可以使用 ipcMain 监听channel 来接收这些消息。

返回 any - 由 ipcMain 处理程序发送过来的值。

    ipcRenderer.sendSync(channel[, arg1][, arg2][, ...])     channel String     ...args any[]

通过 channel 发送同步消息到主进程,可以携带任意参数。 在内部,参数会被序列化为 JSON,因此参数对象上的函数和原型链不会被发送。

主进程可以使用 ipcMain 监听 channel 来接收这些消息,并通过 event.returnValue 设置回复消息。

通过 channel 发送消息到带有 windowid 的窗口。

    ipcRenderer.sendTo(windowId, channel, [, arg1][, arg2][, ...])     windowId Number     channel String     ...args any[]

就像 ipcRenderer.send,不同的是消息会被发送到 host 页面上的 <webview> 元素,而不是主进程。

    ipcRenderer.sendToHost(channel[, arg1][, arg2][, ...])     channel String     ...args any[]

七、Electron重要的组件和事件介绍

  • app:主要用于控制整个应用程序的生命周期

        const {app} = require('Electron')           app.on('window-all-closed', () => {             app.quit()           })

window-all-closed 是一个事件,当所有窗口关闭时,app 组件自动退出。

  • autoUpdater:使应用程序能够自动更新,目前只支持 Windows 和 Mac

        import { autoUpdater } from 'Electron-updater'         autoUpdater.on('update-downloaded', () => {           autoUpdater.quitAndInstall()         })         app.on('ready', () => {           if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates()         })
 

update-downloaded:是一个自动更新下载事件

  • BrowserWindow:创建和控制浏览器窗口

          const {BrowserWindow} = require('Electron')           let win = new BrowserWindow({width: 800, height: 600})           win.on('closed', () => {             win = null           })           win.loadURL('https://github.com')           win.loadURL(`file://${__dirname}/app/index.html`)

closed 关闭事件:这里假设 window 即为 BrowserWindow 创建的对象。

    windo.on(channel,callback)

这个 on 方法其实不只是 BrowserWindow 独有的,很多其他地方都有,之前也提到过。event 指的是事件名称,而 callback 指的是接收到这个事件以后的响应。windo.on 可以响应很多事件,我用到的有两种,分别是 ’closed’ 和 ’window-all-closed’,分别表示当前窗口关闭和所有窗口关闭时的行为。其他的还有 responsive, focus 等很多其他事件。

    windo.once(channel,callback)

这个我看到的只能响应 ’ready-to-show’ 信道。

  • static 方法

这个静态方法的我的理解就是跟 java 中的 static 方法类似,是属于类本身的方法而非类对象的方法。BrowserWindow 类的方法有以下这些:

  • BrowserWindow.getAllWindows() 返回 BrowserWindow[],返回所有窗口

  • BrowserWindow.getFocusedWindow() 返回当前锁定的窗口

  • BrowserWindow.fromWebContents(webContents) 看样子是从网页获得一个窗口

其他还有一些用法,也不全列了,感兴趣的可以去上面列出的网址看

    windo.show()

之前已经出现过了,如果在创建窗口的时候 show=false, 则调用 win.show() 能够让窗口显示。

  • windo 的其他方法

BrowserWindow 的对象还有很多其他方法,例如 destory(),close(),focus(),isFocused(),hide() 等方法。

  • webContents渲染以及控制 web 页面

         const {BrowserWindow} = require('Electron')           let win = new BrowserWindow({width: 800, height: 1500})           win.loadURL('http://github.com')           let contents = win.webContents           console.log(contents)
   
  • <webview> 标签

在一个独立的 frame 和进程里显示外部 web 内容。

  • BrowserWindowProxy

操纵子浏览器窗口,使用 window.open 创建一个新窗口时会返回一个 BrowserWindowProxy 对象,并提供一个有限功能的子窗口。

  • shell

使用默认应用程序管理文件和 url,shell 模块提供与桌面集成相关的功能,在用户的默认浏览器中打开 URL 的示例:

const {shell} = require('Electron') shell.openExternal('https://github.com')

咱们上面就列出这些,这些组件和事件在咱们的 Electron + Vue 的项目中会用到,如果大家对 Electron 的其他组件和事件感兴趣,希望大家可去看官方文档。咱们的这篇文章是交大家怎么去使用 Electron,而不是让大加去背 Electron 的组件、事件或者是方法。

八、Electron + Vue 项目代码分析

此处请大家看本人的 github,在 github 上会有详细的代码分析,代码分析在本月 12 号会上传,github 地址为 https://github.com/guoshijiang/go-ethereum-code-analysis/tree/master/wallet/linkeye-wallet


0
702