牢骚:C++需求还是很大,但是太需要积累了,初级程序员能干的越来越少;高money岗位 ZhaoP APP 搜一下基本都集中在游戏或服务器后台开发。
桌面应用程序,又称为 GUI 程序(Graphical User Interface),但是和 GUI 程序也有一些区别。桌面应用程序 将 GUI 程序从 GUI 具体为“桌面”,使冷冰冰的像块木头一样的电脑概念更具有 人性化,更生动和富有活力。
我们电脑上使用的各种客户端程序都属于桌面应用程序,近年来WEB和移动端的兴起让桌面程序渐渐暗淡,但是在某些日常功能或者行业应用中桌面应用程序仍然是必不可少的。
传统的桌面应用开发方式,一般是下面两种:
纯C++开发的客户端典型的就是基于QT开发,C++程序员一般会选择,而在工控嵌入式领域,基本就是唯一的选择。 由于QT已经发展了很多年(QT6已经出来了,但目前市场上主要还是使用QT5),内部各种基础库、UI控件都比较完善,也支持类似前端的CSS样式控制,相对MFC来说,总体开发算是比较方便的。当然,如果不需要跨平台,只希望在Windows上运行的话,那除了MFC,也可以选择WPF去开发,但其需要使用C#语言,适合熟悉.Net开发技术的程序员。
不过,上面两种对前端开发人员太不友好了,基本是前端人员不会设计的领域,但是在这个【大前端 】的时代,前端开发者正在想方设法涉足各个领域,使用WEB技术开发客户端的方式横空出世。
使用WEB技术进行开发,利用浏览器引擎完成UI渲染,利用Node.js实现服务器端JS编程并可以调用系统API,可以把它想像成一个套了一个客户端外壳的WEB应用。
在界面上,WEB的强大生态为UI带来了无限可能,并且开发、维护成本相对较低,有WEB开发经验的前端开发者很容易上手进行开发。
着重介绍使用WEB技术开发客户端程序的技术之一【electron】
前端Web和C++混合开发的客户端目前主流的是基于Electron和C++,开发UI界面和逻辑可以使用H5、NodeJS去实现,底层核心的模块可以通过C++来开发,封装为Node模块供上层调用。 当然,也可以选择使用多进程架构,UI部分使用Electron开发,而核心功能在后台进程中运行,两者之间通过WebSocket进行通信,实现整体应用功能。
近几年来,WebAssembly技术发展的也比较快,这也是支持通过C++/Rust开发核心模块,可以直接将编译生成的wasm模块集成到前端页面中调用的一种技术,虽然目前没有大规模应用,但也是一个值得关注的技术,主流的浏览器都已经支持这项技术。要说明的是,QT5也支持类似的混合开发技术,即QML技术,相对于传统基于Widget开发而言,对硬件配置要求会高些,大部分的嵌入式平台应该是不支持的。
对于第一种场景,团队中开发人员对于C++和C#并不熟悉,虽然可以现学,但是整个项目的技术管理和项目管理就会变得不可控。
对于第二种场景,对于应用的业务逻辑要求并不多,只是套一个具有浏览器的运行环境,单独为此配置一个C++、C#开发人员划不来。
对于这两种情况,如果现有的前端开发人员能直接搞定,那就非常完美了:Electron的诞生提供了这种可能性。它可以帮助前端开发者在不需要学习其他语言和技能的前提下,快速开发跨平台的桌面应用。
Electron
是由Github
开发,用HTML,CSS
和JavaScript
来构建跨平台桌面应用程序的一个开源库。 Electron
通过将Chromium
和Node.js
合并到同一个运行时环境中,并将其打包为Mac,Windows
和Linux
系统下的应用来实现这一目的。
https://electronjs.org/docs
https://juejin.im/post/5c67619351882562276c3162#heading-5
Web
技术进行开发,开发成本低,可扩展性强,更炫酷的UI
Windows、Linux、Mac
三套软件,且编译快速Web
应用上进行扩展,提供浏览器不具备的能力当然,我们也要认清它的缺点:性能比原生桌面应用要低,最终打包后的安装包和其他文件都比较大。
兼容性
虽然你还在用WEB
技术进行开发,但是你不用再考虑兼容性问题了,你只需要关心你当前使用Electron
的版本对应Chrome
的版本,一般情况下它已经足够新来让你使用最新的API
和语法了,你还可以手动升级Chrome
版本。同样的,你也不用考虑不同浏览器带了的样式和代码兼容问题。
Node 环境
这可能是很多前端开发者曾经梦想过的功能,在WEB
界面中使用Node.js
提供的强大API
,这意味着你在WEB
页面直接可以操作文件,调用系统API
,甚至操作数据库。当然,除了完整的Node API
,你还可以使用额外的几十万个npm
模块。
跨域
你可以直接使用Node
提供的request
模块进行网络请求,这意味着你无需再被跨域所困扰。
强大的扩展性
借助node-ffi
,为应用程序提供强大的扩展性(后面的章节会详细介绍)。
现在市面上已经有非常多的应用在使用electron
进行开发了,包括我们熟悉的VS Code
客户端、GitHub
客户端、Atom
客户端等等。印象很深的,去年迅雷在发布迅雷 X10.1
时的文案:
从迅雷 X 10.1 版本开始,我们采用 Electron 软件框架完全重写了迅雷主界面。使用新框架的迅雷 X 可以完美支持 2K、4K 等高清显示屏,界面中的文字渲染也更加清晰锐利。从技术层面来说,新框架的界面绘制、事件处理等方面比老框架更加灵活高效,因此界面的流畅度也显著优于老框架的迅雷。至于具体提升有多大?您一试便知。
你可以打开VS Code
,点击【帮助】【切换开发人员工具】来VS Code
客户端的界面。
Electron
结合了 Chromium
、Node.js
和用于调用操作系统本地功能的API
。
Chromium
是Google
为发展Chrome
浏览器而启动的开源项目,Chromium
相当于Chrome
的工程版或称实验版,新功能会率先在Chromium
上实现,待验证后才会应用在Chrome
上,故Chrome
的功能会相对落后但较稳定。
Chromium
为Electron
提供强大的UI
能力,可以在不考虑兼容性的情况下开发界面。
Node.js
是一个让JavaScript
运行在服务端的开发平台,Node
使用事件驱动,非阻塞I/O
模型而得以轻量和高效。
单单靠Chromium
是不能具备直接操作原生GUI
能力的,Electron
内集成了Nodejs
,这让其在开发界面的同时也有了操作系统底层API
的能力,Nodejs
中常用的 Path、fs、Crypto
等模块在 Electron
可以直接使用。
为了提供原生系统的GUI
支持,Electron
内置了原生应用程序接口,对调用一些系统功能,如调用系统通知、打开系统文件夹提供支持。
在开发模式上,Electron
在调用系统API
和绘制界面上是分离开发的,下面我们来看看Electron
关于进程如何划分。
Electron
区分了两种进程:主进程和渲染进程,两者各自负责自己的职能。
Electron
运行package.json
的 main
脚本的进程被称为主进程。一个 Electron
应用总是有且只有一个主进程。
职责:
APP
以及对APP
做一些事件监听)可调用的 API:
Node.js API
Electron
提供的主进程API
(包括一些系统功能和Electron
附加功能)由于 Electron
使用了 Chromium
来展示 web
页面,所以 Chromium
的多进程架构也被使用到。 每个Electron
中的 web
页面运行在它自己的渲染进程中。
主进程使用 BrowserWindow 实例创建页面。 每个 BrowserWindow 实例都在自己的渲染进程里运行页面。 当一个 BrowserWindow 实例被销毁后,相应的渲染进程也会被终止。
你可以把渲染进程想像成一个浏览器窗口,它能存在多个并且相互独立,不过和浏览器不同的是,它能调用Node API
。
职责:
HTML
和CSS
渲染界面JavaScript
做一些界面交互可调用的 API:
DOM API
Node.js API
Electron
提供的渲染进程API
在上面的章节我们提到,渲染进和主进程分别可调用的Electron API
。所有Electron
的API
都被指派给一种进程类型。 许多API
只能被用于主进程中,有些API
又只能被用于渲染进程,又有一些主进程和渲染进程中都可以使用。
你可以通过如下方式获取Electron API
const { BrowserWindow, ... } = require('electron')
下面是一些常用的Electron API
:
在后面的章节我们会选择其中常用的模块进行详细介绍。
你可以同时在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 一起使用。
主进程和渲染进程虽然拥有不同的职责,然是他们也需要相互协作,互相通讯。
例如:在
web
页面管理原生GUI
资源是很危险的,会很容易泄露资源。所以在web
页面,不允许直接调用原生GUI
相关的API
。渲染进程如果想要进行原生的GUI
操作,就必须和主进程通讯,请求主进程来完成这些操作。
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);
});
https://imweb.io/topic/5b13a663d4c96b9b1b4c4e9c
在主进程中可以通过BrowserWindow
的webContents
向渲染进程发送消息,所以,在发送消息前你必须先找到对应渲染进程的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);
});
ipcMain
和 ipcRenderer
都是 EventEmitter
类的一个实例。EventEmitter
类是 NodeJS
事件的基础,它由 NodeJS
中的 events
模块导出。
EventEmitter
的核心就是事件触发与事件监听器功能的封装。它实现了事件模型需要的接口, 包括 addListener,removeListener
, emit
及其它工具方法. 同原生 JavaScript
事件类似, 采用了发布/订阅(观察者)的方式, 使用内部 _events
列表来记录注册的事件处理器。
我们通过 ipcMain
和ipcRenderer
的 on、send
进行监听和发送消息都是 EventEmitter
定义的相关接口。
remote
模块为渲染进程(web 页面)和主进程通信(IPC
)提供了一种简单方法。 使用 remote
模块, 你可以调用 main
进程对象的方法, 而不必显式发送进程间消息, 类似于 Java
的 RMI
。
import { remote } from "electron";
remote.dialog.showErrorBox(“主进程才有的dialog模块”, “我是使用remote调用的”);
但实际上,我们在调用远程对象的方法、函数或者通过远程构造函数创建一个新的对象,实际上都是在发送一个同步的进程间消息。
在上面通过 remote
模块调用 dialog
的例子里。我们在渲染进程中创建的 dialog
对象其实并不在我们的渲染进程中,它只是让主进程创建了一个 dialog
对象,并返回了这个相对应的远程对象给了渲染进程。
Electron
并没有提供渲染进程之间相互通信的方式,我们可以在主进程中建立一个消息中转站。
渲染进程之间通信首先发送消息到主进程,主进程的中转站接受到消息后根据条件进行分发。
在两个渲染进程间共享数据最简单的方法是使用浏览器中已经实现的HTML5 API
。 其中比较好的方案是用Storage API
, localStorage,sessionStorage
或者 IndexedDB。
就像在浏览器中使用一样,这种存储相当于在应用程序中永久存储了一部分数据。有时你并不需要这样的存储,只需要在当前应用程序的生命周期内进行一些数据的共享。这时你可以用 Electron
内的 IPC
机制实现。
将数据存在主进程的某个全局变量中,然后在多个渲染进程中使用 remote
模块来访问它。
在主进程中初始化全局变量:
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秘密花园";
多个渲染进程共享同一个主进程的全局变量,这样即可达到渲染进程数据共享和传递的效果。