用JS开辟跨平台桌面运用,从原理到实践

导读

运用Electron开辟客户端顺序已有一段时间了,团体以为照样异常不错的,个中也遇到了一些坑点,本文是从【运转道理】到【现实运用】对Electron举行一次体系性的总结。【多图,长文预警~】

本文一切实例代码均在我的github electron-react上,连系代码浏览文章结果更佳。别的electron-react还可作为运用Electron + React + Mobx + Webpack 手艺栈的脚手架工程。

一、桌面运用顺序

《用JS开辟跨平台桌面运用,从原理到实践》

桌面运用顺序,又称为 GUI 顺序(Graphical User Interface),然则和 GUI 顺序也有一些辨别。桌面运用顺序 将 GUI 顺序从GUI 细致为“桌面”,使冷冰冰的像块木头一样的电脑观点更具有 人性化,更活泼和富有生机。

我们电脑上运用的种种客户端顺序都属于桌面运用顺序,近年来WEB和挪动端的鼓起让桌面顺序逐渐昏暗,然则在某些一样平常功用或许行业运用中桌面运用顺序依旧是必不可少的。

传统的桌面运用开辟体式格局,平常是下面两种:

1.1 原生开辟

直接将言语编译成可实行文件,直接挪用体系API,完成UI绘制等。这类开辟手艺,有着较高的运转效力,但平常来讲,开辟速率较慢,手艺要求较高,比方:

  • 运用C++ / MFC开辟Windows运用
  • 运用Objective-C开辟MAC运用

1.2 托管平台

一最先就有当地开辟和UI开辟。一次编译后,获得中心文件,经由历程平台或虚机完成二次加载编译或诠释运转。运转效力低于原生编译,但平台优化后,其效力也是比较可观的。就开辟速率方面,比原生编译手艺要快一些。比方:

  • 运用C# / .NET Framework(只能开辟Windows运用)
  • Java / Swing

不过,上面两种对前端开辟职员太不友爱了,基础是前端职员不会触及的范畴,然则在这个【大前端😅】的时期,前端开辟者正在千方百计涉足各个范畴,运用WEB手艺开辟客户端的体式格局横空出世。

1.3 WEB开辟

运用WEB手艺举行开辟,应用浏览器引擎完成UI衬着,应用Node.js完成服务器端JS编程并能够挪用体系API,能够把它想像成一个套了一个客户端外壳的WEB运用。

在界面上,WEB的壮大生态为UI带来了无穷能够,而且开辟、庇护本钱相对较低,有WEB开辟履历的前端开辟者很轻易上手举行开辟。

本文就来偏重引见运用WEB手艺开辟客户端顺序的手艺之一【electron

二、Electron

《用JS开辟跨平台桌面运用,从原理到实践》

Electron是由Github开辟,用HTML,CSSJavaScript来构建跨平台桌面运用顺序的一个开源库。 Electron经由历程将ChromiumNode.js合并到同一个运转时环境中,并将其打包为Mac,WindowsLinux体系下的运用来完成这一目标。

2.1 运用Electron开辟的来由:

  • 1.运用具有壮大生态的Web手艺举行开辟,开辟本钱低,可扩大性强,更炫酷的UI
  • 2.跨平台,一套代码可打包为Windows、Linux、Mac三套软件,且编译疾速
  • 3.可直接在现有Web运用上举行扩大,供应浏览器不具有的才
  • 4.你是一个前端👨‍💻~

固然,我们也要认清它的瑕玷:机能比原生桌面运用要低,终究打包后的运用比原生运用大很多。

2.2 开辟体验

兼容性

虽然你还在用WEB手艺举行开辟,然则你没必要再斟酌兼容性题目了,你只须要体贴你当前运用Electron的版本对应Chrome的版本,平常情况下它已充足新来让你运用最新的API和语法了,你还能够手动晋级Chrome版本。一样的,你也没必要斟酌差异浏览器带的款式和代码兼容题目。

Node环境

这多是很多前端开辟者曾梦想过的功用,在WEB界面中运用Node.js供应的壮大API,这意味着你在WEB页面直接能够操纵文件,挪用体系API,以至操纵数据库。固然,除了完整的 Node API,你还能够运用分外的几十万个npm模块。

跨域

你能够直接运用Node供应的request模块举行收集要求,这意味着你无需再被跨域所搅扰。

壮大的扩大性

借助node-ffi,为运用顺序供应壮大的扩大性(背面的章节会细致引见)。

2.3 谁在用Electron

《用JS开辟跨平台桌面运用,从原理到实践》

如今市面上已有异常多的运用在运用Electron举行开辟了,包括我们熟习的VS Code客户端、GitHub客户端、Atom客户端等等。印象很深的,客岁迅雷在宣布迅雷X10.1时的案牍:

从迅雷X 10.1版本最先,我们采纳Electron软件框架完整重写了迅雷主界面。运用新框架的迅雷X能够圆满支撑2K、4K等高清显现屏,界面中的笔墨衬着也越发清楚锋利。从手艺层面来讲,新框架的界面绘制、事宜处置惩罚等方面比老框架越发天真高效,因而界面的流通度也明显优于老框架的迅雷。至于细致提拔有多大?您一试便知。

你能够翻开VS Code,点击【协助】【切换开辟职员东西】来调试VS Code客户端的界面。

《用JS开辟跨平台桌面运用,从原理到实践》

三、Electron运转道理

《用JS开辟跨平台桌面运用,从原理到实践》

Electron 连系了 ChromiumNode.js 和用于挪用操纵体系当地功用的API

3.1 Chromium

Chromium Google 为生长 Chrome 浏览器而启动的开源项目,Chromium 相当于 Chrome 的工程版或称试验版,新功用会率先在 Chromium 上完成,待考证后才会运用在Chrome 上,故 Chrome 的功用会相对落伍但较稳固。

ChromiumElectron供应壮大的UI才,能够在不斟酌兼容性的情况下开辟界面。

3.2 Node.js

Node.js是一个让 JavaScript 运转在服务端的开辟平台,Node 运用事宜驱动,非壅塞I/O 模子而得以轻量和高效。

单单靠Chromium是不能具有直接操纵原生GUI才的,Electron内集成了Nodejs,这让其在开辟界面的同时也有了操纵体系底层 API 的才,Nodejs 中经常使用的 Path、fs、Crypto 等模块在 Electron 能够直接运用。

3.3 体系API

为了供应原生体系的GUI支撑,Electron内置了原生运用顺序接口,对挪用一些体系功用,如挪用体系关照、翻开体系文件夹供应支撑。

在开辟情势上,Electron在挪用体系API和绘制界面上是星散开辟的,下面我们来看看Electron关于历程怎样离别。

3.4 主历程

Electron辨别了两种历程:主历程和衬着历程,二者各自担任本身的职能。

《用JS开辟跨平台桌面运用,从原理到实践》

Electron 运转 package.json main 剧本的历程被称为主历程。一个 Electron 运用总是有且只要一个主历程。

职责:

  • 竖立衬着历程(可多个)
  • 控制了运用生命周期(启动、退出APP以及对APP做一些事宜监听)
  • 挪用体系底层功用、挪用原生资本

可挪用的API:

  • Node.js API
  • Electron供应的主历程API(包括一些体系功用和Electron附加功用)

3.5 衬着历程

由于 Electron 运用了 Chromium 来展现 web 页面,所以 Chromium 的多历程架构也被运用到。 每一个 Electron 中的 web页面运转在它本身的衬着历程中。

主历程运用 BrowserWindow 实例竖立页面。 每一个 BrowserWindow 实例都在本身的衬着历程里运转页面。 当一个 BrowserWindow 实例被烧毁后,相应的衬着历程也会被停止。

你能够把衬着历程想像成一个浏览器窗口,它能存在多个而且相互自力,不过和浏览器差异的是,它能挪用Node API

职责:

  • HTMLCSS衬着界面
  • JavaScript做一些界面交互

可挪用的API:

  • DOM API
  • Node.js API
  • Electron供应的衬着历程API

四、Electron基础

4.1 Electron API

在上面的章节我们提到,衬着进和主历程离别可挪用的Electron API。一切ElectronAPI都被指派给一种历程范例。 很多API只能被用于主历程中,有些API又只能被用于衬着历程,又有一些主历程和衬着历程中都能够运用。

你能够经由历程以下体式格局猎取Electron API

const { BrowserWindow, ... } = require('electron')

下面是一些经常使用的Electron API

《用JS开辟跨平台桌面运用,从原理到实践》

在背面的章节我们会挑选个中经常使用的模块举行细致引见。

4.2 运用 Node.js 的 API

《用JS开辟跨平台桌面运用,从原理到实践》

你能够同时在Electron的主历程和衬着历程运用Node.js API,)一切在Node.js能够运用的API,在Electron中一样能够运用。

import {shell} from 'electron';
import os from 'os';

document.getElementById('btn').addEventListener('click', () => { 
  shell.showItemInFolder(os.homedir());
})

有一个异常主要的提醒: 原生Node.js模块 (即指,须要编译源码事后才被运用的模块) 须要在编译后才和Electron一同运用。

4.3 历程通信

主历程和衬着历程虽然具有差异的职责,然是他们也须要相互协作,相互通信。

比方:在
web页面治理原生
GUI资本是很风险的,会很轻易泄漏资本。所以在
web页面,不允许直接挪用原生
GUI相干的
API。衬着历程假如想要举行原生的
GUI操纵,就必须和主历程通信,要求主历程来完成这些操纵。

《用JS开辟跨平台桌面运用,从原理到实践》

4.4 衬着历程向主历程通信

ipcRenderer 是一个 EventEmitter 的实例。 你能够运用它供应的一些要领,从衬着历程发送同步或异步的音讯到主历程。 也能够吸收主历程复兴的音讯。

在衬着历程引入ipcRenderer

import { ipcRenderer } from 'electron';

异步发送:

经由历程 channel 发送同步音讯到主历程,能够照顾恣意参数。

在内部,参数会被序列化为
JSON,因而参数对象上的函数和原型链不会被发送。

ipcRenderer.send('sync-render', '我是来自衬着历程的异步音讯');

同步发送:

 const msg = ipcRenderer.sendSync('async-render', '我是来自衬着历程的同步音讯');

注重: 发送同步音讯将会壅塞悉数衬着历程,直到收到主历程的相应。

主历程监听音讯:

ipcMain模块是EventEmitter类的一个实例。 当在主历程中运用时,它处置惩罚从衬着器历程(网页)发送出来的异步和同步信息。 从衬着器历程发送的音讯将被发送到该模块。

ipcMain.on:监听 channel,当吸收到新的音讯时 listener 会以 listener(event, args...) 的情势被挪用。

  ipcMain.on('sync-render', (event, data) => {
    console.log(data);
  });

4.5 主历程向衬着历程通信

在主历程中能够经由历程BrowserWindowwebContents向衬着历程发送音讯,所以,在发送音讯前你必须先找到对应衬着历程的BrowserWindow对象。:

const mainWindow = BrowserWindow.fromId(global.mainId);
 mainWindow.webContents.send('main-msg', `ConardLi]`)

依据音讯来源发送:

ipcMain接收音讯的回调函数中,经由历程第一个参数event的属性sender能够拿到音讯来源衬着历程的webContents对象,我们能够直接用此对象回应音讯。

  ipcMain.on('sync-render', (event, data) => {
    console.log(data);
    event.sender.send('main-msg', '主历程收到了衬着历程的【异步】音讯!')
  });

衬着历程监听:

ipcRenderer.on:监听 channel, 当新音讯抵达,将经由历程 listener(event, args...) 挪用 listener

ipcRenderer.on('main-msg', (event, msg) => {
    console.log(msg);
})

4.6 通信道理

ipcMainipcRenderer 都是 EventEmitter 类的一个实例。EventEmitter 类是 NodeJS 事宜的基础,它由 NodeJS 中的 events 模块导出。

EventEmitter 的中心就是事宜触发与事宜监听器功用的封装。它完成了事宜模子须要的接口, 包括 addListener,removeListener, emit 及别的东西要领. 同原生 JavaScript 事宜相似, 采纳了宣布/定阅(观察者)的体式格局, 运用内部 _events 列表来纪录注册的事宜处置惩罚器。

我们经由历程 ipcMainipcRendereron、send 举行监听和发送音讯都是 EventEmitter 定义的相干接口。

4.7 remote

remote 模块为衬着历程(web页面)和主历程通信(IPC)供应了一种简朴要领。 运用 remote 模块, 你能够挪用 main 历程对象的要领, 而没必要显式发送历程间音讯, 相似于 JavaRMI

import { remote } from 'electron';

remote.dialog.showErrorBox('主历程才有的dialog模块', '我是运用remote挪用的')

《用JS开辟跨平台桌面运用,从原理到实践》

但现实上,我们在挪用长途对象的要领、函数或许经由历程长途组织函数竖立一个新的对象,现实上都是在发送一个同步的历程间音讯。

在上面经由历程 remote 模块挪用 dialog 的例子里。我们在衬着历程中竖立的 dialog 对象实在并不在我们的衬着历程中,它只是让主历程竖立了一个 dialog 对象,并返回了这个相对应的长途对象给了衬着历程。

4.8 衬着历程间通信

Electron并没有供应衬着历程之间相互通信的体式格局,我们能够在主历程中竖立一个音讯中转站。

衬着历程之间通信起首发送音讯到主历程,主历程的中转站吸收到音讯后依据前提举行分发。

4.9 衬着历程数据同享

在两个衬着历程间同享数据最简朴的要领是运用浏览器中已完成的 HTML5 API。 个中比较好的计划是用 Storage APIlocalStorage,sessionStorage 或许 IndexedDB。

就像在浏览器中运用一样,这类存储相当于在运用顺序中永远存储了一部份数据。偶然你并不须要如许的存储,只须要在当前运用顺序的生命周期内举行一些数据的同享。这时刻你能够用 Electron 内的 IPC 机制完成。

将数据存在主历程的某个全局变量中,然后在多个衬着历程中运用 remote 模块来访问它。

《用JS开辟跨平台桌面运用,从原理到实践》

在主历程中初始化全局变量:

global.mainId = ...;
global.device = {...};
global.__dirname = __dirname;
global.myField = { name: 'ConardLi' };

在衬着历程中读取:

import { ipcRenderer, remote } from 'electron';

const { getGlobal } = remote;

const mainId = getGlobal('mainId')
const dirname = getGlobal('__dirname')
const deviecMac = getGlobal('device').mac;

在衬着历程中转变:

getGlobal('myField').name = 'code隐秘花圃';

多个衬着历程同享同一个主历程的全局变量,如许即可到达衬着历程数据同享和通报的结果。

五、窗口

5.1 BrowserWindow

主历程模块BrowserWindow用于竖立和控制浏览器窗口。

  mainWindow = new BrowserWindow({
    width: 1000,
    height: 800,
    // ...
  });
  mainWindow.loadURL('http://www.conardli.top/');

你能够在这里检察它一切的组织参数。

《用JS开辟跨平台桌面运用,从原理到实践》

5.2 无框窗口

无框窗口是没有镶边的窗口,窗口的部份(如东西栏)不属于网页的一部份。

BrowserWindow的组织参数中,将frame设置为false能够指定窗口为无边框窗口,将东西栏隐蔽后,就会发作两个题目:

  • 1.窗口控制按钮(最小化、全屏、封闭按钮)会被隐蔽
  • 2.没法拖拽挪动窗口

能够经由历程指定titleBarStyle选项来再将东西栏按钮显现出来,将其设置为hidden示意返回一个隐蔽题目栏的全尺寸内容窗口,在左上角依旧有规范的窗口控制按钮。

new BrowserWindow({
    width: 200,
    height: 200,
    titleBarStyle: 'hidden',
    frame: false
  });

5.3 窗口拖拽

默许情况下, 无边框窗口是不可拖拽的。我们能够在界面中经由历程CSS属性-webkit-app-region: drag手动制订拖拽地区。

在无框窗口中, 拖动行动能够与挑选文本争执,能够经由历程设定-webkit-user-select: none;禁用文本挑选:

.header {
  -webkit-user-select: none;
  -webkit-app-region: drag;
}

相反的,在可拖拽地区内部设置
-webkit-app-region: no-drag 则能够指定特定不可拖拽地区。

5.4 通明窗口

经由历程将transparent选项设置为true, 还能够使无框窗口通明:

new BrowserWindow({
    transparent: true,
    frame: false
  });

5.5 Webview

运用 webview 标签在Electron 运用中嵌入 “外来” 内容。外来内容包括在 webview 容器中。 运用中的嵌入页面能够控制外来内容的规划和重绘。

iframe 差异, webview 在与运用顺序差异的历程中运转。它与您的网页没有雷同的权限, 运用顺序和嵌入内容之间的一切交互都将是异步的。

六、对话框

dialog 模块供应了api来展现原生的体系对话框,比方翻开文件框,alert框,所以web运用能够给用户带来跟体系运用雷同的体验。

注重:dialog是主历程模块,想要在衬着历程挪用能够运用remote

《用JS开辟跨平台桌面运用,从原理到实践》

6.1 毛病提醒

dialog.showErrorBox用于显现一个显现毛病音讯的模态对话框。

 remote.dialog.showErrorBox('毛病', '这是一个毛病弹框!')

6.2 对话框

dialog.showErrorBox用于挪用体系对话框,能够为指定几种差异的范例: “none“, “info“, “error“, “question” 或许 “warning“。

在 Windows 上, “question” 与”info”显现雷同的图标, 除非你运用了 “icon” 选项设置图标。 在 macOS 上, “warning” 和 “error” 显现雷同的正告图标

remote.dialog.showMessageBox({
  type: 'info',
  title: '提醒信息',
  message: '这是一个对话弹框!',
  buttons: ['肯定', '作废']
}, (index) => {
  this.setState({ dialogMessage: `【你点击了${index ? '作废' : '肯定'}!!】` })
})

6.3 文件框

dialog.showOpenDialog用于翻开或挑选体系目次。

remote.dialog.showOpenDialog({
  properties: ['openDirectory', 'openFile']
}, (data) => {
  this.setState({ filePath: `【挑选途径:${data[0]}】 ` })
})

6.4 信息框

这里引荐直接运用HTML5 API,它只能在衬着器历程中运用。

let options = {
  title: '信息框题目',
  body: '我是一条信息~~~',
}
let myNotification = new window.Notification(options.title, options)
myNotification.onclick = () => {
  this.setState({ message: '【你点击了信息框!!】' })
}

七、体系

7.1 猎取体系信息

《用JS开辟跨平台桌面运用,从原理到实践》

经由历程remote猎取到主历程的process对象,能够猎取到当前运用的各个版本信息:

  • process.versions.electronelectron版本信息
  • process.versions.chromechrome版本信息
  • process.versions.nodenode版本信息
  • process.versions.v8v8版本信息

猎取当前运用根目次:

remote.app.getAppPath()

运用nodeos模块猎取当前体系根目次:

os.homedir();

7.2 复制粘贴

《用JS开辟跨平台桌面运用,从原理到实践》

Electron供应的clipboard在衬着历程和主历程都可运用,用于在体系剪贴板上实行复制和粘贴操纵。

以纯文本的情势写入剪贴板:

clipboard.writeText(text[, type])

以纯文本的情势猎取剪贴板的内容:

clipboard.readText([type])

7.3 截图

desktopCapturer用于从桌面捕捉音频和视频的媒体源的信息。它只能在衬着历程中被挪用。

《用JS开辟跨平台桌面运用,从原理到实践》

下面的代码是一个猎取屏幕截图并保存的实例:

  getImg = () => {
    this.setState({ imgMsg: '正在截取屏幕...' })
    const thumbSize = this.determineScreenShotSize()
    let options = { types: ['screen'], thumbnailSize: thumbSize }
    desktopCapturer.getSources(options, (error, sources) => {
      if (error) return console.log(error)
      sources.forEach((source) => {
        if (source.name === 'Entire screen' || source.name === 'Screen 1') {
          const screenshotPath = path.join(os.tmpdir(), 'screenshot.png')
          fs.writeFile(screenshotPath, source.thumbnail.toPNG(), (error) => {
            if (error) return console.log(error)
            shell.openExternal(`file://${screenshotPath}`)
            this.setState({ imgMsg: `截图保存到: ${screenshotPath}` })
          })
        }
      })
    })
  }

  determineScreenShotSize = () => {
    const screenSize = screen.getPrimaryDisplay().workAreaSize
    const maxDimension = Math.max(screenSize.width, screenSize.height)
    return {
      width: maxDimension * window.devicePixelRatio,
      height: maxDimension * window.devicePixelRatio
    }
  }

八、菜单

运用顺序的菜单能够协助我们快速的抵达某一功用,而不借助客户端的界面资本,平常菜单分为两种:

  • 运用顺序菜单:位于运用顺序顶部,在全局范围内都能运用
  • 上下文菜单:可自定义恣意页面显现,自定义挪用,如右键菜单

Electron为我们供应了Menu模块用于竖立本机运用顺序菜单和上下文菜单,它是一个主历程模块。

你能够经由历程Menu的静态要领buildFromTemplate(template),运用自定义菜单模版来组织一个菜单对象。

template是一个MenuItem的数组,我们来看看MenuItem的几个主要参数:

  • label:菜单显现的笔墨
  • click:点击菜单后的事宜处置惩罚函数
  • role:体系预定义的菜单,比方copy(复制)、paste(粘贴)、minimize(最小化)…
  • enabled:指导是不是启用该项目,此属机能够动态变动
  • submenu:子菜单,也是一个MenuItem的数组

引荐:最好指定role与规范角色相匹配的任何菜单项,而不是尝试手动完成click函数中的行动。内置role行动将供应最好的当地体验。

下面的实例是一个简朴的额菜单template

const template = [
  {
    label: '文件',
    submenu: [
      {
        label: '新建文件',
        click: function () {
          dialog.showMessageBox({
            type: 'info',
            message: '嘿!',
            detail: '你点击了新建文件!',
          })
        }
      }
    ]
  },
  {
    label: '编辑',
    submenu: [{
      label: '剪切',
      role: 'cut'
    }, {
      label: '复制',
      role: 'copy'
    }, {
      label: '粘贴',
      role: 'paste'
    }]
  },
  {
    label: '最小化',
    role: 'minimize'
  }
]

8.1 运用顺序菜单

运用Menu的静态要领setApplicationMenu,可竖立一个运用顺序菜单,在 WindowsLinux 上,menu将被设置为每一个窗口的顶层菜单。

注重:必须在模块ready事宜后挪用此 API app。

我们能够依据运用顺序差异的的生命周期,差异的体系对菜单做差异的处置惩罚。

《用JS开辟跨平台桌面运用,从原理到实践》

app.on('ready', function () {
  const menu = Menu.buildFromTemplate(template)
  Menu.setApplicationMenu(menu)
})

app.on('browser-window-created', function () {
  let reopenMenuItem = findReopenMenuItem()
  if (reopenMenuItem) reopenMenuItem.enabled = false
})

app.on('window-all-closed', function () {
  let reopenMenuItem = findReopenMenuItem()
  if (reopenMenuItem) reopenMenuItem.enabled = true
})

if (process.platform === 'win32') {
  const helpMenu = template[template.length - 1].submenu
  addUpdateMenuItems(helpMenu, 0)
}

8.2 上下文菜单

运用Menu的实例要领menu.popup可自定义弹出上下文菜单。

《用JS开辟跨平台桌面运用,从原理到实践》

    let m = Menu.buildFromTemplate(template)
    document.getElementById('menuDemoContainer').addEventListener('contextmenu', (e) => {
      e.preventDefault()
      m.popup({ window: remote.getCurrentWindow() })
    })

8.3 快速键

在菜单选项中,我们能够指定一个accelerator属性来指定操纵的快速键:

  {
    label: '最小化',
    accelerator: 'CmdOrCtrl+M',
    role: 'minimize'
  }

别的,我们还能够运用globalShortcut来注册全局快速键。

    globalShortcut.register('CommandOrControl+N', () => {
      dialog.showMessageBox({
        type: 'info',
        message: '嘿!',
        detail: '你触发了手动注册的快速键.',
      })
    })

CommandOrControl代表在macOS上为Command键,以及在Linux和Windows上为Control键。

九、打印

很多情况下顺序中运用的打印都是用户无感知的。而且想要天真的控制打印内容,每每须要借助打印机给我们供应的api再举行开辟,这类开辟体式格局异常烦琐,而且开辟难度较大。第一次在营业中用到Electron实在就是用到它的打印功用,这里就多引见一些。

Electron供应的打印api能够异常天真的控制打印设置的显现,而且能够经由历程html来誊写打印内容。Electron供应了两种体式格局举行打印,一种是直接挪用打印机打印,一种是打印到pdf

而且有两种对象能够挪用打印:

  • 经由历程windowwebcontent对象,运用此种体式格局须要零丁开出一个打印的窗口,能够将该窗口隐蔽,然则通信挪用相对庞杂。
  • 运用页面的webview元素挪用打印,能够将webview隐蔽在挪用的页面中,通信体式格局比较简朴。

上面两种体式格局同时具有printprintToPdf要领。

《用JS开辟跨平台桌面运用,从原理到实践》

9.1 挪用体系打印

contents.print([options], [callback]);

打印设置(options)中只要简朴的三个设置:

  • silent:打印时是不是不展现打印设置(是不是寂静打印)
  • printBackground:是不是打印背景
  • deviceName:打印机装备称号

起首要将我们运用的打印机称号设置好,而且要在挪用打印前起首要推断打印机是不是可用。

运用webContentsgetPrinters要领可猎取当前装备已设置的打印机列表,注重设置过不是可用,只是在此装备上安装过驱动。

经由历程getPrinters猎取到的打印机对象:https://electronjs.org/docs/a…

我们这里尽管体贴两个,namestatusstatus0时示意打印机可用。

print的第二个参数callback是用于推断打印使命是不是发出的回调,而不是打印使命完成后的回调。所以平常打印使命发出,回调函数即会挪用并返回参数true。这个回调并不能推断打印是不是真的胜利了。

    if (this.state.curretnPrinter) {
      mainWindow.webContents.print({
        silent: silent, printBackground: true, deviceName: this.state.curretnPrinter
      }, () => { })
    } else {
      remote.dialog.showErrorBox('毛病', '请先挑选一个打印机!')
    }

9.2 打印到PDF

printToPdf的用法基础和print雷同,然则print的设置项异常少,而printToPdf则扩大了很多属性。这里翻了一下源码发明另有很多没有被贴进api的,大概有三十几个包括能够对打印的margin,打印页眉页脚等举行设置。

contents.printToPDF(options, callback)

callback函数在打印失利或打印胜利后挪用,可猎取打印失利信息或包括PDF数据的缓冲区。

    const pdfPath = path.join(os.tmpdir(), 'webviewPrint.pdf');
    const webview = document.getElementById('printWebview');
    const renderHtml = '我是被暂时插进去webview的内容...';
    webview.executeJavaScript('document.documentElement.innerHTML =`' + renderHtml + '`;');
    webview.printToPDF({}, (err, data) => {
      console.log(err, data);
      fs.writeFile(pdfPath, data, (error) => {
        if (error) throw error
        shell.openExternal(`file://${pdfPath}`)
        this.setState({ webviewPdfPath: pdfPath })
      });
    });

这个例子中的打印是运用
webview完成的,经由历程挪用
executeJavaScript要领可动态向
webview插进去打印内容。

9.3 两种打印计划的挑选

上面提到,运用webviewwebcontent都能够挪用打印功用,运用webcontent打印,起首要有一个打印窗口,这个窗口不能随时打印随时竖立,比较消耗机能。能够将它在顺序运转时启动好,并做好事宜监听。

此历程需和挪用打印的举行做好通信,大抵历程以下:

《用JS开辟跨平台桌面运用,从原理到实践》

可见通信异常烦琐,运用webview举行打印可完成一样的结果然则通信体式格局会变得简朴,由于衬着历程和webview通信不须要经由主历程,经由历程以下体式格局即可:

  const webview = document.querySelector('webview')
  webview.addEventListener('ipc-message', (event) => {
    console.log(event.channel)
  })
  webview.send('ping');

  const {ipcRenderer} = require('electron')
  ipcRenderer.on('ping', () => {
    ipcRenderer.sendToHost('pong')
  })

之前特地为ELectron打印写过一个DEMOelectron-print-demo有兴致能够clone下来看一下。

9.4 打印功用封装

下面是几个针对经常使用打印功用的东西函数封装。

/**
 * 猎取体系打印机列表
 */
export function getPrinters() {
  let printers = [];
  try {
    const contents = remote.getCurrentWindow().webContents;
    printers = contents.getPrinters();
  } catch (e) {
    console.error('getPrintersError', e);
  }
  return printers;
}
/**
 * 猎取体系默许打印机
 */
export function getDefaultPrinter() {
  return getPrinters().find(element => element.isDefault);
}
/**
 * 检测是不是安装了某个打印驱动
 */
export function checkDriver(driverMame) {
  return getPrinters().find(element => (element.options["printer-make-and-model"] || '').includes(driverMame));
}
/**
 * 依据打印机称号猎取打印机对象
 */
export function getPrinterByName(name) {
  return getPrinters().find(element => element.name === name);
}

十、顺序庇护

《用JS开辟跨平台桌面运用,从原理到实践》

10.1 崩溃

崩溃监控是每一个客户端顺序必备的庇护功用,当顺序崩溃时我们平常希冀做到两件事:

  • 1.上传崩溃日记,实时报警
  • 2.监控顺序崩溃,提醒用户重启顺序

electron为我们供应给了crashReporter来协助我们纪录崩溃日记,我们能够经由历程crashReporter.start来竖立一个崩溃报告器:

const { crashReporter } = require('electron')
crashReporter.start({
  productName: 'YourName',
  companyName: 'YourCompany',
  submitURL: 'https://your-domain.com/url-to-submit',
  uploadToServer: true
})

当顺序发作崩溃时,崩溃报日记将被储存在暂时文件夹中名为YourName Crashes的文件文件夹中。submitURL用于指定你的崩溃日记上传服务器。 在启动崩溃报告器之前,您能够经由历程挪用app.setPath('temp', 'my/custom/temp') API来自定义这些暂时文件的保存途径。你还能够经由历程crashReporter.getLastCrashReport()来猎取上次崩溃报告的日期和ID

我们能够经由历程webContentscrashed来监听衬着历程的崩溃,别的经测试有些主历程的崩溃也会触发该事宜。所以我们能够依据主window是不是被烧毁来推断举行差异的重启逻辑,下面使悉数崩溃监控的逻辑:

import { BrowserWindow, crashReporter, dialog } from 'electron';
// 开启历程崩溃纪录
crashReporter.start({
  productName: 'electron-react',
  companyName: 'ConardLi',
  submitURL: 'http://xxx.com',  // 上传崩溃日记的接口
  uploadToServer: false
});
function reloadWindow(mainWin) {
  if (mainWin.isDestroyed()) {
    app.relaunch();
    app.exit(0);
  } else {
    // 烧毁其他窗口
    BrowserWindow.getAllWindows().forEach((w) => {
      if (w.id !== mainWin.id) w.destroy();
    });
    const options = {
      type: 'info',
      title: '衬着器历程崩溃',
      message: '这个历程已崩溃.',
      buttons: ['重载', '封闭']
    }
    dialog.showMessageBox(options, (index) => {
      if (index === 0) mainWin.reload();
      else mainWin.close();
    })
  }
}
export default function () {
  const mainWindow = BrowserWindow.fromId(global.mainId);
  mainWindow.webContents.on('crashed', () => {
    const errorMessage = crashReporter.getLastCrashReport();
    console.error('顺序崩溃了!', errorMessage); // 可零丁上传日记
    reloadWindow(mainWindow);
  });
}

10.2 最小化到托盘

有的时刻我们并不想让用户经由历程点封闭按钮的时刻就封闭顺序,而是把顺序最小化到托盘,在托盘上做真正的退出操纵。

起首要监听窗口的封闭事宜,阻挠用户封闭操纵的默许行动,将窗口隐蔽。

function checkQuit(mainWindow, event) {
  const options = {
    type: 'info',
    title: '封闭确认',
    message: '确认要最小化顺序到托盘吗?',
    buttons: ['确认', '封闭顺序']
  };
  dialog.showMessageBox(options, index => {
    if (index === 0) {
      event.preventDefault();
      mainWindow.hide();
    } else {
      mainWindow = null;
      app.exit(0);
    }
  });
}
function handleQuit() {
  const mainWindow = BrowserWindow.fromId(global.mainId);
  mainWindow.on('close', event => {
    event.preventDefault();
    checkQuit(mainWindow, event);
  });
}

这时刻顺序就再也找不到了,使命托盘中也没有我们的顺序,所以我们要先竖立好使命托盘,并做好事宜监听。

windows平台运用
ico文件能够到达更好的结果

export default function createTray() {
  const mainWindow = BrowserWindow.fromId(global.mainId);
  const iconName = process.platform === 'win32' ? 'icon.ico' : 'icon.png'
  tray = new Tray(path.join(global.__dirname, iconName));
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '显现主界面', click: () => {
        mainWindow.show();
        mainWindow.setSkipTaskbar(false);
      }
    },
    {
      label: '退出', click: () => {
        mainWindow.destroy();
        app.quit();
      }
    },
  ])
  tray.setToolTip('electron-react');
  tray.setContextMenu(contextMenu);
}

十一、扩大才

《用JS开辟跨平台桌面运用,从原理到实践》

在很多情况下,你的运用顺序要和外部装备举行交互,平常情况下厂商会为你供应硬件装备的开辟包,这些开辟包基础上都是经由历程C++ 编写,在运用electron开辟的情况下,我们并不具有直接挪用C++代码的才,我们能够应用node-ffi来完成这一功用。

node-ffi供应了一组壮大的东西,用于在Node.js环境中运用纯JavaScript挪用动态链接库接口。它能够用来为库构建接口绑定,而不须要运用任何C++代码。

注重
node-ffi并不能直接挪用
C++代码,你须要将
C++代码编译为动态链接库:在
Windows下是
Dll ,在
Mac OS 下是
dylib
,Linux
so

node-ffi 加载 Library是有限定的,只能处置惩罚 C作风的 Library

下面是一个简朴的实例:

const ffi = require('ffi');
const ref = require('ref');
const SHORT_CODE = ref.refType('short');


const DLL = new ffi.Library('test.dll', {
    Test_CPP_Method: ['int', ['string',SHORT_CODE]], 
  })

testCppMethod(str: String, num: number): void {
  try {
    const result: any = DLL.Test_CPP_Method(str, num);
    return result;
  } catch (error) {
    console.log('挪用失利~',error);
  }
}

this.testCppMethod('ConardLi',123);

上面的代码中,我们用ffi包装C++接口天生的动态链接库test.dll,并运用ref举行一些范例映照。

运用JavaScript挪用这些映照要领时,引荐运用TypeScript来商定参数范例,由于弱范例的JavaScript在挪用强范例言语的接口时能够会带来意想不到的风险。

借助这一才,前端开辟工程师也能够在IOT范畴一展技艺了😎~

十二、环境挑选

平常情况下,我们的运用顺序能够运转在多套环境下(productionbetauatmokedevelopment…),差异的开辟环境能够对应差异的后端接口或许其他设置,我们能够在客户端顺序中内置一个简朴的环境挑选功用来协助我们更高效的开辟。

《用JS开辟跨平台桌面运用,从原理到实践》

细致战略以下:

《用JS开辟跨平台桌面运用,从原理到实践》

  • 在开辟环境中,我们直接进入环境挑选页面,读取到挑选的环境后举行相应的重定向操纵
  • 在菜单保存环境挑选进口,以便在开辟历程当中切换
const envList = ["moke", "beta", "development", "production"];
exports.envList = envList;
const urlBeta = 'https://wwww.xxx-beta.com';
const urlDev = 'https://wwww.xxx-dev.com';
const urlProp = 'https://wwww.xxx-prop.com';
const urlMoke = 'https://wwww.xxx-moke.com';
const path = require('path');
const pkg = require(path.resolve(global.__dirname, 'package.json'));
const build = pkg['build-config'];
exports.handleEnv = {
  build,
  currentEnv: 'moke',
  setEnv: function (env) {
    this.currentEnv = env
  },
  getUrl: function () {
    console.log('env:', build.env);
    if (build.env === 'production' || this.currentEnv === 'production') {
      return urlProp;
    } else if (this.currentEnv === 'moke') {
      return urlMoke;
    } else if (this.currentEnv === 'development') {
      return urlDev;
    } else if (this.currentEnv === "beta") {
      return urlBeta;
    }
  },
  isDebugger: function () {
    return build.env === 'development'
  }
}

十三、打包

末了也是最主要的一步,将写好的代码打包成可运转的.app.exe可实行文件。

这里我把打包气氛两部份来做,衬着历程打包和主历程打包。

13.1 衬着历程打包和晋级

平常情况下,我们的大部份营业逻辑代码是在衬着历程完成的,在大部份情况下我们仅仅须要对衬着历程举行更新和晋级而不须要修改主历程代码,我们衬着历程的打包现实上和平常的web项目打包没有太大差异,运用webpack打包即可。

这里我说说衬着历程零丁打包的优点:

打包完成的htmljs文件,我们平常要上传到我们的前端静态资本服务器下,然后示知服务端我们的衬着历程有代码更新,这里能够说成衬着历程零丁的晋级。

注重,和壳的晋级差异,衬着历程的晋级仅仅是静态资本服务器上htmljs文件的更新,而不须要从新下载更新客户端,如许我们每次启动顺序的时刻检测到离线包有更新,即可直接革新读取最新版本的静态资本文件,纵然在顺序运转历程当中要强迫更新,我们的顺序只须要强迫革新页面读取最新的静态资本即可,如许的晋级对用户是异常友爱的。

这里注重,一旦我们如许设置,就意味着衬着历程和主历程打包晋级的完整星散,我们在启动主窗口时读取的文件就不应该再是当地文件,而是打包完成后放在静态资本服务器的文件。

为了轻易开辟,这里我们能够辨别当地和线上加载差异的文件:

function getVersion (mac,current){
  // 依据装备mac和当前版本猎取最新版本
}
export default function () {
  if (build.env === 'production') {
    const version = getVersion (mac,current);
    return 'https://www.xxxserver.html/electron-react/index_'+version+'.html';
  }
  return url.format({
    protocol: 'file:',
    pathname: path.join(__dirname, 'env/environment.html'),
    slashes: true,
    query: { debugger: build.env === "development" }
  });
}

细致的webpack设置这里就不再贴出,能够到我的github electron-react/scripts目次下检察。

这里须要注重,在开辟环境下我们能够连系webpackdevServerelectron敕令来启动app

  devServer: {
    contentBase: './assets/',
    historyApiFallback: true,
    hot: true,
    port: PORT,
    noInfo: false,
    stats: {
      colors: true,
    },
    setup() {
      spawn(
        'electron',
        ['.'],
        {
          shell: true,
          stdio: 'inherit',
        }
      )
        .on('close', () => process.exit(0))
        .on('error', e => console.error(e));
    },
  },//...

13.2 主历程打包

主历程,行将悉数顺序打包成可运转的客户端顺序,经常使用的打包计划平常有两种,electron-packagerelectron-builder

electron-packager在打包设置上我以为有些烦琐,而且它只能将运用直接打包为可实行顺序。

这里我引荐运用electron-builder,它不仅具有轻易的设置 protocol 的功用、内置的 Auto Update、简朴的设置 package.json 便能完成悉数打包事情,用户体验异常不错。而且electron-builder不仅能直接将运用打包成exe app等可实行顺序,还能打包成msi dmg等安装包花样。

你能够在package.json轻易的举行种种设置:

 "build": {
   "productName": "electron-react", // app中文称号
   "appId": "electron-react",// app标识
   "directories": { // 打包后输出的文件夹
     "buildResources": "resources",
     "output": "dist/"
   }
   "files": [ // 打包后依旧保存的源文件
     "main_process/",
     "render_process/",
   ],
   "mac": { // mac打包设置
     "target": "dmg",
     "icon": "icon.ico"
   },
   "win": { // windows打包设置
     "target": "nsis",
     "icon": "icon.ico"
   },
   "dmg": { // dmg文件打包设置
     "artifactName": "electron_react.dmg",
     "contents": [
       {
         "type": "link",
         "path": "/Applications",
         "x": 410,
         "y": 150
       },
       {
         "type": "file",
         "x": 130,
         "y": 150
       }
     ]
   },
   "nsis": { // nsis文件打包设置
     "oneClick": false,
     "allowToChangeInstallationDirectory": true,
     "shortcutName": "electron-react"
   },
 }

实行electron-builder打包敕令时,可指定参数举行打包。

 --mac, -m, -o, --macos   macOS打包
 --linux, -l              Linux打包
 --win, -w, --windows     Windows打包
 --mwl                    同时为macOS,Windows和Linux打包
 --x64                    x64 (64位安装包)
 --ia32                   ia32(32位安装包) 

关于主历程的更新你能够运用electron-builder自带的Auto Update模块,在electron-react也完成了手动更新的模块,由于篇幅缘由这里就不再赘述,假若有兴致能够到我的github检察main下的update模块。

13.3 打包优化

electron-builder打包出来的App要比雷同功用的原生客户端运用体积大很多,纵然是空的运用,体积也要在100mb以上。缘由有很多:

第一点;为了到达跨平台的结果,每一个Electron运用都包括了悉数V8引擎和Chromium内核。

第二点:打包时会将悉数node_modules打包进去,人人都晓得一个运用的node_module体积是异常巨大的,这也是使得Electron运用打包后的体积较大的缘由。

第一点我们没法转变,我们能够从第二点对运用体积举行优化:Electron在打包时只会将denpendencies的依靠打包进去,而不会将 devDependencies 中的依靠举行打包。所以我们应尽能够的削减denpendencies中的依靠。在上面的历程中,我们运用webpack对衬着历程举行打包,所以衬着历程的依靠悉数都能够移入devDependencies

别的,我们还能够运用双packajson.json的体式格局来举行优化,把只在开辟环境中运用到的依靠放在悉数项目标根目次的package.json下,将与平台相干的或许运转时须要的依靠装在app目次下。细致详见two-package-structure

参考

本项目源码地点:
https://github.com/ConardLi/e…

小结

愿望你浏览本篇文章后能够到达以下几点:

  • 相识Electron的基础运转道理
  • 控制Electron开辟的中心基础知识
  • 相识Electron关于弹框、打印、庇护、打包等功用的基础运用

文中若有毛病,迎接在批评区斧正,假如这篇文章协助到了你,迎接点赞和关注。

想浏览更多优良文章、可关注我的github博客,你的star✨、点赞和关注是我延续创作的动力!

引荐关注我的微信民众号【code隐秘花圃】,天天推送高质量文章,我们一同交换生长。

《用JS开辟跨平台桌面运用,从原理到实践》

关注民众号后复兴【加群】拉你进入优良前端交换群。

    原文作者:ConardLi
    原文地址: https://segmentfault.com/a/1190000019426512
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞