用JS开发跨平台桌面应用,从原理到实践
使用Electron开发客户端程序已经有一段时间了,整体感觉还是非常不错的,其中也遇到了一些坑点,本文是从【运行原理】到【实际应用】对Electron进行一次系统性的总结。【多图,长文预警~】
本文所有实例代码均在我的github electron-react上,结合代码阅读文章效果更佳。另外electron-react还可作为使用Electron + React + Mobx + Webpack技术栈的脚手架工程。
github:https://github.com/ConardLi/electron-react
桌面应用程序,又称为 GUI 程序(Graphical User Interface),但是和 GUI 程序也有一些区别。桌面应用程序 将 GUI 程序从GUI 具体为“桌面”,使冷冰冰的像块木头一样的电脑概念更具有 人性化,更生动和富有活力。
我们电脑上使用的各种客户端程序都属于桌面应用程序,近年来WEB和移动端的兴起让桌面程序渐渐暗淡,但是在某些日常功能或者行业应用中桌面应用程序仍然是必不可少的。
传统的桌面应用开发方式,一般是下面两种:
直接将语言编译成可执行文件,直接调用系统API,完成UI绘制等。这类开发技术,有着较高的运行效率,但一般来说,开发速度较慢,技术要求较高,例如:
- 使用C++ / MFC开发Windows应用
- 使用Objective-C开发MAC应用
一开始就有本地开发和UI开发。一次编译后,得到中间文件,通过平台或虚机完成二次加载编译或解释运行。运行效率低于原生编译,但平台优化后,其效率也是比较可观的。就开发速度方面,比原生编译技术要快一些。例如:
- 使用C# / .NET Framework(只能开发Windows应用)
- Java / Swing
不过,上面两种对前端开发人员太不友好了,基本是前端人员不会涉及的领域,但是在这个【大前端】的时代,前端开发者正在想方设法涉足各个领域,使用WEB技术开发客户端的方式横空出世。
使用WEB技术进行开发,利用浏览器引擎完成UI渲染,利用Node.js实现服务器端JS编程并可以调用系统API,可以把它想像成一个套了一个客户端外壳的WEB应用。
在界面上,WEB的强大生态为UI带来了无限可能,并且开发、维护成本相对较低,有WEB开发经验的前端开发者很容易上手进行开发。
本文就来着重介绍使用WEB技术开发客户端程序的技术之一【electron】
Electron是由Github开发,用HTML,CSS和JavaScript来构建跨平台桌面应用程序的一个开源库。 Electron通过将Chromium和Node.js合并到同一个运行时环境中,并将其打包为Mac,Windows和Linux系统下的应用来实现这一目的。
- 使用具有强大生态的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
下面是一些常用的Electron API:
在后面的章节我们会选择其中常用的模块进行详细介绍。
你可以同时在Electron的主进程和渲染进程使用Node.js API,)所有在Node.js可以使用的API,在Electron中同样可以使用。
有一个非常重要的提示: 原生Node.js模块 (即指,需要编译源码过后才能被使用的模块) 需要在编译后才能和Electron一起使用。
主进程和渲染进程虽然拥有不同的职责,然是他们也需要相互协作,互相通讯。
例如:在web页面管理原生GUI资源是很危险的,会很容易泄露资源。所以在web页面,不允许直接调用原生GUI相关的API。渲染进程如果想要进行原生的GUI操作,就必须和主进程通讯,请求主进程来完成这些操作。
ipcRenderer 是一个 EventEmitter 的实例。 你可以使用它提供的一些方法,从渲染进程发送同步或异步的消息到主进程。 也可以接收主进程回复的消息。
在渲染进程引入ipcRenderer:
异步发送:
通过 channel 发送同步消息到主进程,可以携带任意参数。
在内部,参数会被序列化为 JSON,因此参数对象上的函数和原型链不会被发送。
同步发送:
注意: 发送同步消息将会阻塞整个渲染进程,直到收到主进程的响应。
主进程监听消息:
ipcMain模块是EventEmitter类的一个实例。 当在主进程中使用时,它处理从渲染器进程(网页)发送出来的异步和同步信息。 从渲染器进程发送的消息将被发送到该模块。
ipcMain.on:监听 channel,当接收到新的消息时 listener 会以 listener(event, args…) 的形式被调用。
在主进程中可以通过BrowserWindow的webContents向渲染进程发送消息,所以,在发送消息前你必须先找到对应渲染进程的BrowserWindow对象。:
根据消息来源发送:
在ipcMain接受消息的回调函数中,通过第一个参数event的属性sender可以拿到消息来源渲染进程的webContents对象,我们可以直接用此对象回应消息。
渲染进程监听:
ipcRenderer.on:监听 channel, 当新消息到达,将通过listener(event, args…)调用 listener。
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 。
但实际上,我们在调用远程对象的方法、函数或者通过远程构造函数创建一个新的对象,实际上都是在发送一个同步的进程间消息。
在上面通过 remote 模块调用 dialog 的例子里。我们在渲染进程中创建的 dialog 对象其实并不在我们的渲染进程中,它只是让主进程创建了一个 dialog 对象,并返回了这个相对应的远程对象给了渲染进程。
Electron并没有提供渲染进程之间相互通信的方式,我们可以在主进程中建立一个消息中转站。
渲染进程之间通信首先发送消息到主进程,主进程的中转站接收到消息后根据条件进行分发。
在两个渲染进程间共享数据最简单的方法是使用浏览器中已经实现的HTML5 API。 其中比较好的方案是用Storage API, localStorage,sessionStorage 或者 IndexedDB。
就像在浏览器中使用一样,这种存储相当于在应用程序中永久存储了一部分数据。有时你并不需要这样的存储,只需要在当前应用程序的生命周期内进行一些数据的共享。这时你可以用 Electron 内的 IPC 机制实现。
将数据存在主进程的某个全局变量中,然后在多个渲染进程中使用 remote 模块来访问它。
在主进程中初始化全局变量:
在渲染进程中读取:
在渲染进程中改变:
多个渲染进程共享同一个主进程的全局变量,这样即可达到渲染进程数据共享和传递的效果。
主进程模块BrowserWindow用于创建和控制浏览器窗口。
你可以在这里查看它所有的构造参数。
无框窗口是没有镶边的窗口,窗口的部分(如工具栏)不属于网页的一部分。
在BrowserWindow的构造参数中,将frame设置为false可以指定窗口为无边框窗口,将工具栏隐藏后,就会产生两个问题:
- 1.窗口控制按钮(最小化、全屏、关闭按钮)会被隐藏
- 2.无法拖拽移动窗口
可以通过指定titleBarStyle选项来再将工具栏按钮显示出来,将其设置为hidden表示返回一个隐藏标题栏的全尺寸内容窗口,在左上角仍然有标准的窗口控制按钮。
默认情况下, 无边框窗口是不可拖拽的。我们可以在界面中通过CSS属性-webkit-app-region: drag手动制定拖拽区域。
在无框窗口中, 拖动行为可能与选择文本冲突,可以通过设定-webkit-user-select: none;禁用文本选择:
相反的,在可拖拽区域内部设置 -webkit-app-region: no-drag则可以指定特定不可拖拽区域。
通过将transparent选项设置为true, 还可以使无框窗口透明:
使用 webview 标签在Electron 应用中嵌入 \”外来\” 内容。外来内容包含在 webview 容器中。 应用中的嵌入页面可以控制外来内容的布局和重绘。
与 iframe 不同, webview 在与应用程序不同的进程中运行。它与您的网页没有相同的权限, 应用程序和嵌入内容之间的所有交互都将是异步的。
dialog 模块提供了api来展示原生的系统对话框,例如打开文件框,alert框,所以web应用可以给用户带来跟系统应用相同的体验。
注意:dialog是主进程模块,想要在渲染进程调用可以使用remote
dialog.showErrorBox用于显示一个显示错误消息的模态对话框。
dialog.showErrorBox用于调用系统对话框,可以为指定几种不同的类型: \”none\”, \”info\”, \”error\”, \”question\” 或者 \”warning\”。
在 Windows 上, \”question\” 与\”info\”显示相同的图标, 除非你使用了 \”icon\” 选项设置图标。 在 macOS 上, \”warning\” 和 \”error\” 显示相同的警告图标
dialog.showOpenDialog用于打开或选择系统目录。
这里推荐直接使用HTML5 API,它只能在渲染器进程中使用。
通过remote获取到主进程的process对象,可以获取到当前应用的各个版本信息:
- process.versions.electron:electron版本信息
- process.versions.chrome:chrome版本信息
- process.versions.node:node版本信息
- process.versions.v8:v8版本信息
获取当前应用根目录:
使用node的os模块获取当前系统根目录:
Electron提供的clipboard在渲染进程和主进程都可使用,用于在系统剪贴板上执行复制和粘贴操作。
以纯文本的形式写入剪贴板:
以纯文本的形式获取剪贴板的内容:
desktopCapturer用于从桌面捕获音频和视频的媒体源的信息。它只能在渲染进程中被调用。
下面的代码是一个获取屏幕截图并保存的实例:
应用程序的菜单可以帮助我们快捷的到达某一功能,而不借助客户端的界面资源,一般菜单分为两种:
- 应用程序菜单:位于应用程序顶部,在全局范围内都能使用
- 上下文菜单:可自定义任意页面显示,自定义调用,如右键菜单
Electron为我们提供了Menu模块用于创建本机应用程序菜单和上下文菜单,它是一个主进程模块。
你可以通过Menu的静态方法buildFromTemplate(template),使用自定义菜单模版来构造一个菜单对象。
template是一个MenuItem的数组,我们来看看MenuItem的几个重要参数:
- label:菜单显示的文字
- click:点击菜单后的事件处理函数
- role:系统预定义的菜单,例如copy(复制)、paste(粘贴)、minimize(最小化)…
- enabled:指示是否启用该项目,此属性可以动态更改
- submenu:子菜单,也是一个MenuItem的数组
推荐:最好指定role与标准角色相匹配的任何菜单项,而不是尝试手动实现click函数中的行为。内置role行为将提供最佳的本地体验。
下面的实例是一个简单的额菜单template。
使用Menu的静态方法setApplicationMenu,可创建一个应用程序菜单,在 Windows 和 Linux 上,menu将被设置为每个窗口的顶层菜单。
注意:必须在模块ready事件后调用此 API app。
我们可以根据应用程序不同的的生命周期,不同的系统对菜单做不同的处理。
使用Menu的实例方法menu.popup可自定义弹出上下文菜单。
在菜单选项中,我们可以指定一个accelerator属性来指定操作的快捷键:
另外,我们还可以使用globalShortcut来注册全局快捷键。
CommandOrControl代表在macOS上为Command键,以及在Linux和Windows上为Control键。
很多情况下程序中使用的打印都是用户无感知的。并且想要灵活的控制打印内容,往往需要借助打印机给我们提供的api再进行开发,这种开发方式非常繁琐,并且开发难度较大。第一次在业务中用到Electron其实就是用到它的打印功能,这里就多介绍一些。
Electron提供的打印api可以非常灵活的控制打印设置的显示,并且可以通过html来书写打印内容。Electron提供了两种方式进行打印,一种是直接调用打印机打印,一种是打印到pdf。
并且有两种对象可以调用打印:
- 通过window的webcontent对象,使用此种方式需要单独开出一个打印的窗口,可以将该窗口隐藏,但是通信调用相对复杂。
- 使用页面的webview元素调用打印,可以将webview隐藏在调用的页面中,通信方式比较简单。
上面两种方式同时拥有print和printToPdf方法。
打印配置(options)中只有简单的三个配置:
- silent:打印时是否不展示打印配置(是否静默打印)
- printBackground:是否打印背景
- deviceName:打印机设备名称
首先要将我们使用的打印机名称配置好,并且要在调用打印前首先要判断打印机是否可用。
使用webContents的getPrinters方法可获取当前设备已经配置的打印机列表,注意配置过不是可用,只是在此设备上安装过驱动。
通过getPrinters获取到的打印机对象:electronjs.org/docs/api/st…
我们这里只管关心两个,name和status,status为0时表示打印机可用。
print的第二个参数callback是用于判断打印任务是否发出的回调,而不是打印任务完成后的回调。所以一般打印任务发出,回调函数即会调用并返回参数true。这个回调并不能判断打印是否真的成功了。
printToPdf的用法基本和print相同,但是print的配置项非常少,而printToPdf则扩展了很多属性。这里翻了一下源码发现还有很多没有被贴进文档的,大概有三十几个,包括可以对打印的margin,打印页眉页脚等进行配置。
callback函数在打印失败或打印成功后调用,可获取打印失败信息或包含PDF数据的缓冲区。
这个例子中的打印是使用webview完成的,通过调用executeJavaScript方法可动态向webview插入打印内容。
上面提到,使用webview和webcontent都可以调用打印功能,使用webcontent打印,首先要有一个打印窗口,这个窗口不能随时打印随时创建,比较耗费性能。可以将它在程序运行时启动好,并做好事件监听。
此过程需和调用打印的进行做好通信,大致过程如下:
可见通信非常繁琐,使用webview进行打印可实现同样的效果但是通信方式会变得简单,因为渲染进程和webview通信不需要经过主进程,通过如下方式即可:
之前专门为ELectron打印写过一个DEMO:electron-print-demo有兴趣可以clone下来看一下。
下面是几个针对常用打印功能的工具函数封装。
崩溃监控是每个客户端程序必备的保护功能,当程序崩溃时我们一般期望做到两件事:
- 1.上传崩溃日志,及时报警
- 2.监控程序崩溃,提示用户重启程序
electron为我们提供给了crashReporter来帮助我们记录崩溃日志,我们可以通过crashReporter.start来创建一个崩溃报告器:
当程序发生崩溃时,崩溃报日志将被储存在临时文件夹中名为YourName Crashes的文件文件夹中。submitURL用于指定你的崩溃日志上传服务器。 在启动崩溃报告器之前,您可以通过调用app.setPath(\’temp\’, \’my/custom/temp\’)API来自定义这些临时文件的保存路径。你还可以通过crashReporter.getLastCrashReport()来获取上次崩溃报告的日期和ID。
我们可以通过webContents的crashed来监听渲染进程的崩溃,另外经测试有些主进程的崩溃也会触发该事件。所以我们可以根据主window是否被销毁来判断进行不同的重启逻辑,下面是整个崩溃监控的逻辑:
有的时候我们并不想让用户通过点关闭按钮的时候就关闭程序,而是把程序最小化到托盘,在托盘上做真正的退出操作。
首先要监听窗口的关闭事件,阻止用户关闭操作的默认行为,将窗口隐藏。
这时程序就再也找不到了,任务托盘中也没有我们的程序,所以我们要先创建好任务托盘,并做好事件监听。
windows平台使用ico文件可以达到更好的效果
在很多情况下,你的应用程序要和外部设备进行交互,一般情况下厂商会为你提供硬件设备的开发包,这些开发包基本上都是通过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。
下面是一个简单的实例:
上面的代码中,我们用ffi包装C++接口生成的动态链接库test.dll,并使用ref进行一些类型映射。
使用JavaScript调用这些映射方法时,推荐使用TypeScript来约定参数类型,因为弱类型的JavaScript在调用强类型语言的接口时可能会带来意想不到的风险。
借助这一能力,前端开发工程师也可以在IOT领域一展身手了~
一般情况下,我们的应用程序可能运行在多套环境下(production、beta、uat、moke、development…),不同的开发环境可能对应不同的后端接口或者其他配置,我们可以在客户端程序中内置一个简单的环境选择功能来帮助我们更高效的开发。
具体策略如下:
- 在开发环境中,我们直接进入环境选择页面,读取到选择的环境后进行响应的重定向操作
- 在菜单保留环境选择入口,以便在开发过程中切换
最后也是最重要的一步,将写好的代码打包成可运行的.app或.exe可执行文件。
这里我把打包氛围两部分来做,渲染进程打包和主进程打包。
一般情况下,我们的大部分业务逻辑代码是在渲染进程完成的,在大部分情况下我们仅仅需要对渲染进程进行更新和升级而不需要改动主进程代码,我们渲染进程的打包实际上和一般的web项目打包没有太大差别,使用webpack打包即可。
这里我说说渲染进程单独打包的好处:
打包完成的html和js文件,我们一般要上传到我们的前端静态资源服务器下,然后告知服务端我们的渲染进程有代码更新,这里可以说成渲染进程单独的升级。
注意,和壳的升级不同,渲染进程的升级仅仅是静态资源服务器上html和js文件的更新,而不需要重新下载更新客户端,这样我们每次启动程序的时候检测到离线包有更新,即可直接刷新读取最新版本的静态资源文件,即使在程序运行过程中要强制更新,我们的程序只需要强制刷新页面读取最新的静态资源即可,这样的升级对用户是非常友好的。
这里注意,一旦我们这样配置,就意味着渲染进程和主进程打包升级的完全分离,我们在启动主窗口时读取的文件就不应该再是本地文件,而是打包完成后放在静态资源服务器的文件。
为了方便开发,这里我们可以区分本地和线上加载不同的文件:
具体的webpack配置这里就不再贴出,可以到我的github electron-react的/scripts目录下查看。
这里需要注意,在开发环境下我们可以结合webpack的devServer和electron命令来启动app:
主进程,即将整个程序打包成可运行的客户端程序,常用的打包方案一般有两种,electron-packager和electron-builder。
electron-packager在打包配置上我觉得有些繁琐,而且它只能将应用直接打包为可执行程序。
这里我推荐使用electron-builder,它不仅拥有方便的配置 protocol 的功能、内置的 Auto Update、简单的配置 package.json 便能完成整个打包工作,用户体验非常不错。而且electron-builder不仅能直接将应用打包成exe app等可执行程序,还能打包成msi dmg等安装包格式。
你可以在package.json方便的进行各种配置:
执行electron-builder打包命令时,可指定参数进行打包。
关于主进程的更新你可以使用electron-builder自带的Auto Update模块,在electron-react也实现了手动更新的模块,由于篇幅原因这里就不再赘述,如果有兴趣可以到我的github查看main下的update模块。
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/electron-react
希望你阅读本篇文章后可以达到以下几点:
- 了解Electron的基本运行原理
- 掌握Electron开发的核心基础知识
- 了解Electron关于弹框、打印、保护、打包等功能的基本使用
文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。
想阅读更多优质文章、可关注我的github博客,你的star✨、点赞和关注是我持续创作的动力!
github:https://github.com/ConardLi/ConardLi.github.io
原链接:https://juejin.im/post/5cfd2ec7e51d45554877a59f
HTML5 崛起之时,Java 桌面时代就已经终结了
2004 年 Google Maps 的面世标志着 Java 桌面时代的终结,也改变了桌面环境下“跨平台”的基本定义。
本文作者以个人视角对 Java 桌面发展历程做了回顾,内容来自他在上世纪九十年代后期担任 Java 开发者时的所见所感,主要讲述曾经的“杀手级”桌面语言 Java 是为何从 21 世纪开始颓势尽显、步入衰落的。值得一提的是,作者如今在做一款开发者友好型 Java 桌面部署工具(jDeploy),其实他还是希望 Java 可以重拾风采,再度变得对桌面开发具有吸引力。
本文是该回顾系列文章中的第二篇,在上期文章中,作者回顾了 Java 制霸桌面的鸿图如何在 1999 至 2005 的短短几年间烟消云散。当初的 Java 可谓志得意满、凭 Applet 小程序技惊四座,下决心要在互联网时代下重新定义“桌面”。互联网的未来在于“跨平台”,而 Java 的血管中涌动的正是“跨平台”的血液,优势在握!可遗憾的是,事后来看,此跨平台似乎并非彼跨平台。接下来,让我们继续跟着作者的脚步去看看,具体在 2004 至 2007 年间,Java 桌面又经历了什么。
2002 年左右,我在客服中心为客户提供计算机与打印机技术支持。我和小伙伴们挤在小小的隔间里,面对着一款桌面程序。通过这款软件,我们可以快速查询客户和产品信息,并把通话中的重要信息记录进去。
在典型的客服来电中,我们会询问客户的产品序列号,再把结果输入系统。如果他们之前就打过电话,系统就会输出窗口,里面包含产品的完整历史记录和之前的求助细节。在参考其他同事留下的事由记录后,我还能操作界面中的选项卡和功能按钮,例如帮客户更换新机。
我不记得这款软件叫什么名字了,可能是为公司或者客服中心专门定制的吧。印象里这应该是 PeopleSoft(仁科公司,2005 年已被甲骨文收购)的产品,但我也不太确定。总之,这款桌面软件运行在 Windows 2000 系统上,肯定不是 Web 应用程序。它其实挺复杂,里面包含不少菜单和表单;不过一旦上手,整个使用体验相当棒——速度快、反应灵敏,几乎没有任何延迟。以输入电话号码查询客户记录为例,我们只需要在“电话”字段里输入号码,其余空白表格就会立刻被填充完整。
据我所知,这款程序肯定不是用 Swing 编写的。但如今全球各地无数公司都在使用由 Swing 编写的企业级桌面软件,它们在使用体验上跟我当初接触的这款程序非常相似。换句话说,Swing 已经满足了我们在 2001、2002 年那会对于桌面业务软件的全部期望和想象。
在工作半年之后,上边来了新指示,要求我们用 Web 应用程序替换掉之前的桌面软件。据说新系统会让我们的工作更轻松,但在第一节培训课刚刚过去十分钟后,我们就意识到这根本就是胡说八道:新系统简直烂透了!
我不太记得当时使用的是 IE 5.5 还是 IE 6 了,总之就是前 AJAX 时代的 Web 环境。现在在产品字段中输入序号后,系统会弹出一个窗口,上面写着“正在加载……请勿关闭此窗口”。几秒后,窗口自行消失,客户详细信息出现在表单当中。反正每当需要从服务器获取内容时,这个倒霉窗口就会跳出来。领导还提醒我们别随便在浏览器里点“刷新”,说是这样会破坏系统状态。于是每每出现问题,我就只能先登出、再重新登录。
我不太理解公司为什么要用这款“傻了吧唧”的 Web 应用程序替代之前的桌面软件。可能是出于成本考虑吧,毕竟跟桌面软件相比,Web 应用程序的开发和维护成本都更低。或者是软件供应商强行施压,比如“Web 才是未来,每个人都必须接受!”但,真有这么强势的乙方吗?
无论如何,这里透露出一个重要的信息:Web 应用程序还没等发展完善,就已经开始蚕食桌面软件的生存空间。唯一的问题就是 Web 应用需要多久才能追平桌面软件的使用体验。而事实证明,用不了多久。
再回到 Java 这边。热情的支持者们正不断扩大 Java 帝国的桌面版图,对 WORA(一次编写、随处运行)的热情也引导他们最终迈向跨平台小程序与“本机”应用程序之间的秘密山谷。那时候的 Java IDE 主要面向三大构建目标:
- 小程序
- Java Web 开发
- 可执行 Jar 文件是的,没有直接开发本机应用程序的选项。虽然有第三方工具可以把 Jar 文件转换为本机应用程序,但这类工具相当复杂而且操作流程极为繁琐。只有对自己最“狠”的人才能坚持用得下去。而 Java 之所以有勇气忽视这一点,靠的就是对未来的判断——本机桌面应用程序终于被淘汰。其实这个预言是正确的,只是在时间上有所偏差。
从 2022 年的角度回顾,Java 身上其实有很多显而易见的问题。应用程序可以作为 Web 部署、也可以按本机部署,但这两种形式都没有一丁点“原生”感。Web 部署的小程序运行在自己的“沙箱”内并被集成到网页当中,整个运行过程又慢又迟钝。
虽然 Java 总想在 Web 和桌面之间建起一道桥梁,但它自身却被 Web 所裹挟。到 2002 年,很多企业开始把原本的桌面软件功能迁移到 Web 端。这些 Web 应用程序的构建、维护和部署成本确实比桌面软件低得多,代价就是在用户体验上做出妥协。
大约也是在这个时候,Java 开始推崇“富互联网应用”的概念,希望把好 Web 应用跟差 Web 应用区分开来。但到 2004 年 Google Maps 正式亮相时,Java 的小把戏彻底宣告破产。Google Maps 以令人震惊的效果为富 Web 应用程序树立了标杆,而人家用的是 HTML5。
我最近又看了一次 Bill Atkinson 第一次向苹果爱好者们展示 MacPaint 的旧视频。在他第一次通过鼠标用画笔工具绘出图案时,现场一片“哇哦”和掌声。这就叫开创性。我第一次看到 Google Maps 也是类似的感觉,地图可以无缝缩放、万向平移,压根看不出来任何拼接的痕迹。这里使用的全新技术被称为 AJAX(异步 JavaScript 与 XML),这也是人们第一次能够在 Web 应用程序中向服务器后台无缝发出请求。现在这一切当然被视为理所当然,可 2004 年那会,开发者需要绞尽脑汁才能把那些让人想吐的框架或者弹窗隐藏起来,确保不用刷新整个页面就能从服务器处加载新数据。
身为 Web 开发者,我当然对其中的无穷可能性心生向往。但从桌面开发的角度看,这场历史性的变革似乎没有给桌面、特别是 Java 带来任何影响。
在 HTML5 之前,“跨平台”的意思是“跨 Windows、Mac 和 Linux”,所以跨的范围还是在桌面范畴之内。当时我并没意识到,但现在来看 HTML5 的亮相代表着新平台时代的降临——它将成为客户端应用程序的客观标准;更重要的是,Java 支持不了这个平台。突然之间,WORA 理念就出现空白了——Swing 应用程序适用于一切平台,除了最重要的那个:网络浏览器。
那 Java 桌面开发者们都跑哪去了?方向主要有三:
- 服务器
- 浏览器(HTML5)
- 桌面应用如果大家对自己的基本定位首先是“Java 开发者”、其次是“客户端开发者”,那最终应该会选择 Java 在当下仍然占据主动的平台——服务器。如果你对面向用户开发(客户端)更感兴趣,而且主要看中 Java 的跨平台价值主张,那接下来的目标很可能是 HTML5 (Javascript/HTML/CSS)开发。如果你是铁杆“保皇党”(比如说我),那就继续坚守 Java 桌面开发,同时满腹狐疑地看着自己这个圈子越来越小。
2000 年初,JavaScript 开发工具尚处于起步阶段。大多数 Web 开发者只能使用文本编辑器来编写.js 文件。简单的验证脚本和交互设计倒是没问题,但这种粗糙的方法肯定不能扩展并支持大型企业应用程序项目。另外,当时的 JavaScript 语言还不具备开发者在重构等重要操作时所需要的功能,例如静态类型。
相比之下,Java 已经拥有一套全面的开发工具,能够轻松扩展至任何规模的项目。到 2004 年,领先且成熟的 Java IDE 已经成为开发环境中的标杆,其中的静态类型更是大大简化了大型项目的维护难度。到这时,唯一的遗憾就是 Java 应用程序无法在网络浏览器中运行(只有小程序可以)。
为了解决这个难题,Google 打造出 GWT(Google Web Toolkit)。这是一套 Java 到 JavaScript 的编译器加运行时库,允许开发者借助 Java 那一整套领先的开发工具编写应用程序,再把成果部署成 JavaScript 应用的形式在浏览器内原生运行。这套运行时库包含诸多核心 Java API(例如 java.lang、java.util 等)的实现,确保业务逻辑能够在 GWT 应用程序与服务器应用程序间顺畅共享。
在用户界面方面,GWT 也提供自己的功能部件,其实质就是以 Java 的形式将各部件与浏览器中的本机 HTML 部件相绑定。虽然我们还是没法直接使用 Swing 代码、大部分第三方库也不在支持之列,但我们至少可以用到自己最熟悉的 Java 开发环境和核心 API。
所以这不能算是让 Java 真正走进了浏览器——标准 JavaSE 库仍然大部分不受支持,线程等核心功能也无法起效。但至少对多数用例来说,这已经够了。
Google 用 GWT 开发出很多流行一时的 HTML5 应用程序,其中最著名的就是 Gmail,这个项目还催生出一个规模不大、但却相当活跃的开源社区。虽然影响力已经今非昔比,但这个社区直到现在也仍然存在。与此同时,JavaScript 工具的逐步改进也在挤占 GWT 的生存空间,过去十年来诞生的一系列更为现代的解决方案也允许我们在浏览器中更“无脑”地使用 Java。
HTML5 的出现颠覆了 Java 制霸桌面的野心,但这里也有好消息。由于不必分神于桌面端,Java 在服务器端迎来了全面发展。Java 做好了战斗准备、努力满足开发者对后端服务的种种新需求——毕竟没有后端,再好的 Web 应用也出不来。
Java 在服务器端的受欢迎程度在接下来几年中持续增长,也吸引到整个生态系统的高度关注。第三方库不断涌现,而 2005 年 Maven 的诞生也让第三方库的使用不再复杂繁琐。无需额外下载、不必寻找依赖项,直接把片段粘贴到 pom 文件中,它就能自动下载一切相应依赖项。
Java 的开发工具也在不断改进,这在很大程度上要归功于 Java 在服务器端的优势地位。这些改进也对桌面开发者产生了积极影响,让我们用上了跟服务器端相同的 IDE、编译器、虚拟机和库。然而,代表 Java 世界“最后的坚持”的这帮桌面开发者眼界还是没能打开,仍在围着 UI 库的改进和部署打转。
遇到问题时,我的习惯是上 Google 搜一搜,看看有没有其他人遇到或者已经解决过相同的问题。但在 Swing 开发上,我发现最新的搜索结果也基本是 2005 年左右的内容了,之后基本再无新增。在找不到答案时,我偶尔会写一篇问题分析博文。而在两年后再次遇到类似问题时,我在 Google 上找到的就是自己两年前那篇博文……说真的,现在还有喘气的 Swing 开发者吗?感觉真的说不好。
从各个方面来看,Web 的兴起让“桌面应用”的概念清晰了起来。Java 最初的跨平台客户端开发愿景并没有把瘦客户端(主要与远程服务器交互)跟本机完整桌面应用程序区分开来。这不仅提高了理解难度,更让安全模型的设计有些无所适从。Java 理解中的“平台”就是计算机本身,所以会使用笨拙的沙箱来限制可能引发安全威胁的 API 访问,例如访问文件系统。这是 Java 一切安全漏洞的根源,也是导致 Java 被逐出浏览器世界的原因。
这种基于“沙箱”的开发体验相当糟糕,因为我们很容易意外“越界”并触发安全异常。最终结果是,几乎所有客户端都会请求对系统进行“可信”访问,这样也就完全绕过了沙箱的限制。
相比之下,HTML5 在 Web 和桌面之间设立了明确的边界。Web 应用程序默认无权访问客户端计算机,而浏览器才是那个“平台”,这就让客户端应用程序的安全保障变得更轻松、更易行。
经过此番变革,“桌面”的范畴变得更小,以往很多被视为“桌面应用程序”的软件现在被划入“客户端应用程序”类别。具体来讲,如果应用程序只负责在用户与服务器交互时提供 UI,那它就属于客户端应用程序。“桌面”这个概念现在指的就是那些以某种方式与本机设备相集成的应用程序,包括访问文件系统(开发工具、文件转换工具等)、调用浏览器中不存在的某些平台本机 API、以及执行算力密集型任务的软件。
这倒不是说“客户端”应用程序跟“桌面”应用程序间就毫无交集——当然有,这两者都涉及 GUI,而且不少现代桌面应用程序也都需要接入服务器。所以无论是桌面还是客户端应用程序,都能享受到 GUI 工具包改进、媒体(音频/视频)及网络等技术层面的改进成果。
2004 年,我曾在 Mac 和 Windows 上都开发出一些商用级别的 Java 桌面应用程序。HTML5 对这类应用程序基本没有任何直接影响。结合自身需求,Swing 还是完全够用,我用来构建本机捆绑包的各种桌面部署工具也都能正常起效。
但很遗憾,科技行业就是个不进则退的世界。在接下来的几年中,Web 平台一路突飞猛进、而 Swing 却始终停滞不前。到 2007 年,Swing 已经到了不变革、就消亡的危难关头。它需要响应 HTML5 这波历史性潮流,而最终答案就是 JavaFX。这是一种新奇的 Java UI 工具包,能够把 Java 带入 GPU 加速、场景图、3D 图形、Web 视图的现代新世界,同时支持 MP3 和 MP4 等现代音视频编解码器。
在下一篇文章中,我们将回顾 JavaFX 的火爆人气、深远影响,以及 2011 年 Mac 应用商店出现前 Java 领域的其他发展趋势。别小瞧 Mac 应用商店,它的出现堪称对 Java for Mac 桌面开发生态的“斩首行动”。
原文链接:
https://jdeploy.substack.com/p/the-decline-and-fall-of-java-on-the-970
了解更多软件开发与相关领域知识,点击访问 InfoQ 官网:https://www.infoq.cn/,获取更多精彩内容!
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。