绝对原创!!粗浅的electron离线包的设计方案以及实现
前言
上一篇文章中我展示了electron应用程序更新的几种方案,但是阅读效果并不理想。 本来我是希望文章能为大家各种方案的优劣,这样大家实现业务需求的时候能够选择合适自己的方案。
但是现实往往不尽如人意。因此我将以一种松散的形式来叙述我的文章,希望能为大家提供其他价值。
文章结构
在这篇文章中,我将会提出一个在electron桌面技术基础上的离线包架构。小程序是类似于微信小程序的小应用。这个架构的内容包含三部分。
第一部分是实现一个应用桌面,用户可以通过应用桌面来添加,更新,删除小程序。
第二部分是阐述开发小程序时候使用的sdk的架构。
第三部分是对实现细节的描述,并且对如何提高应用性能,稳定性等问题提出普适性意见提出普适性的意见
解决的问题
- 为用户节约了安装应用的空间
- 实现在window端,mac端,linux端的跨端使用小程序
- 便于开发者灵活升级,添加应用
背景
什么是electron小程序
electon小程序是我自己创造的一个概念,本质上来说它是一个于electron桌面应用上运行的H5应用,这个应用能使用一般H5应用的操作系统的底层能力。
electon小程序架构的功能介绍
首先,从用户的角度来说。就如同像你手机上一个一个应用,在这套架构下用户用户可以打开,退出,在后台运行我们的应用,进一步地我们支持服务端和客户端主动安装,更新这些应用。下面的图片形象化地阐述了用户地这一需求。
其次,为了方便应用开发者开发,我们将会提供一套SDK以便应用的作者能使用一些底层能力,比如说,存储,日志,网络请求,获取系统信息等能力。下面的图片形象化地阐述了一个普通开发者这一需求。
实现
实现思路
为了分析这个问题,我们先了解一下electron的架构。
什么是electron ?
Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发 经验。
哈哈,不过假如你不知道什么是electron,估计你也不会看这篇文章了。简单得来说,eletron就是nodejs + chromuim,中间得➕号是进程通信。简单的解释,这是简化的说法。
实现思路
于是乎,大体的思路是这样的,就是让chromium来做渲染层,让nodejs运行时来提供底层能力,通过进程通信来将能力提供到chromium。
详细地讲:
其实在本架构中每个应用类似于浏览器的一个tab页。首先:我们可以理解应用的安装, 升级,后台运行,其实就是tab页的打开,刷新或者热更新,tab的切换。其次:因为每个tab页都是一个独立的进程,这样应用崩溃不会影响全局的崩溃。因此
对于nodejs运行时。我们可以将其类比为一个服务器来使用就行了,在应用上发起进程通信,在主进程上来处理这些计算请求,并异步返回就可以了。
下图从进程的角度来详细阐述这一架构: render proess 就是一个个应用, 而main process可以类比于服务器。
实现步骤
这样想好像能实现,但是我感觉还是太复杂了。回想起我的偶像波利亚曾经说过,
假如你不能解决这个问题,你能解决一部分问题吗?
架构第一部分:应用的安装,创建和更新
我们的问题最简单的版本就是如何加载一个html文件到一个electron窗口中。
第一个版本:小程序的本地加载
这是一个很基础的electron入门级别的操作,实际上官方文档就教会你如何去实现了。入门文档
第二个版本:实现动态更新小程序逻辑
主要思路
application类实现思路
我创建了一个application类,这个类只有两个方法create, destroy。
create方法创建根据一个html创建一个窗口,而这个html是从云端下载下来得。
destroy方法主要用于关闭窗口,并且清除一些副作用。
Container类实现思路
这个类有三个方法
createClient创建一个长连接的服务,能从云端接受一些命令,你可以使用你任何你想要得服务。例如: websocket,mqtt等。
createOrUpdateApplication 根据一个特殊的appName 判断是首次创建还是更新。
init方法里面主要是主要逻辑,监听服务端下发的命令
实现动态更新小程序逻辑的伪代码
//@ts-nocheck
const { app, BrowserWindow } = require('electron')
const path = require('path')
class application {
create(path) {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: sdkPath
}
})
win.loadFile(path)
return win
}
destroy() {
}
}
class Container {
applications = []
init() {
let client = createClient();
client.on('createOrUpdateApplication', (info) => {
createOrUpdateApplication()
})
}
createOrUpdateApplication ({ appName, url, other}) {
// 构建应用目录,用于存放相关数据文件
checkAppDirectoriesExistsOrMakeDir(pipelineName)
if(hasSameApplication(appName)) {
//根据独一无二的应用名来关闭应用,调用其destroy方法去除一些副作用
destroyApplicationByAppName(appName)
}
// 下载index.html, 相同目录下的indexhtml可以覆盖
let path = await downloadHtml(url)
let win = new application(path)
this.push({
appName,
win
})
}
createClient() {
// 这里我们可以初始化一些长连接的,websocket,mqtt等
// 返回一个事件监听器来接受云端的命令
// 这里我们监听一个事件 createOrUpdateApplication
}
}
app.whenReady().then(() => {
let container = new Container()
container.init()
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
架构第二部分:sdk实现
思路
sdk的安全加载的实现思路
sdk的安全加载一个十分简单的事情,下面我提供了文档地址并补充一些注意事项即可。
方法是如下面的文档解释的执行上下文隔离
sdk的进程通信的实现思路
上图主要介绍了是一个推和拉两种通信方式
应用拉
那么用户点击应用图标的时候,我们就可以使用sdk发起一个请求,附上我的appName,那么我们的container类可能就会去读取应用配置,下载或者加载html,并打开一个窗口。
这意味着我们必须有一个内置的应用,就是桌面了。这个应用可以随着electron程序下发时候一起下发下来。
容器推
比如说应用订阅了网络状态,可能是应用启动的时候订阅了网络状态。我们只需要应用在我们的sdk里面传入一个回调。当我们网络状态发生改变的时候就触发回调即可。
点对点和广播
- 广播
有时候有些服务是相同的,可以使用广播的方式推,就是向所有应用推。我们也可以创建一个没有appName作为前缀的信道来提供公共服务
- 点对点
我们可以用appName作为前缀的信道来提供针对应用的特殊服务
sdk伪代码实现
main process 实现
//@ts-nocheck
const { app, BrowserWindow } = require('electron')
const path = require('path')
class application {
appName: string
create(path, appName) {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: sdkPath
}
})
win.loadFile(path)
return win
}
listenSdk() {
ipc.answerRenderer(`${this.appName}.open`, async (appName: string) => {
openApplication(appName)
})
}
sendSomeMessage(eventName,data) {
ipc.sendToRenderers(`${this.appName}.${eventName}`, {
data,
})
}
destroy() {
// 销毁监听事件
}
}
class Container {
applications = []
init() {
let client = createClient();
listenOpenApplication();
this.createDesktop()
client.on('createOrUpdateApplication', (info) => {
createOrUpdateApplication()
})
}
// 监听多处
listenOpenApplication() {
ipc.answerRenderer(`open`, async (appName: string) => {
openApplication(appName)
})
}
// 广播消息
sendSomeMessage(eventName,data) {
ipc.sendToRenderers(eventName, {
data,
})
}
createOrUpdateApplication ({ appName, url, other}) {
// 构建应用目录,用于存放相关数据文件
checkAppDirectoriesExistsOrMakeDir(pipelineName)
if(hasSameApplication(appName)) {
//根据独一无二的应用名来关闭应用,调用其destroy方法去除一些副作用
destroyApplicationByAppName(appName)
}
// 下载index.html, 相同目录下的indexhtml可以覆盖
let path = await downloadHtml(url)
// 应用打开时候我们刷新一下,否则等待应用打开
if(hasSameApplication(appName)) {
let win = new application(path, appName)
this.push({
appName,
win
})
}
}
openApplication (appName) {
// 构建应用目录,用于存放相关数据文件
checkAppDirectoriesExistsOrMakeDir(pipelineName)
if(hasSameApplication(appName)) {
//根据独一无二的应用名来关闭应用,调用其destroy方法去除一些副作用
destroyApplicationByAppName(appName)
}
// 下载index.html, 相同目录下的indexhtml可以覆盖
let path
if(hasNoHtml(appName)) {
path = await downloadHtml(getUrlByAppname(appName))
} else {
path = getHtmlPath()
}
// 打开应用
let win = new application(path, appName)
this.push({
appName,
win
})
}
createClient() {
// 这里我们可以初始化一些长连接的,websocket,mqtt等
// 返回一个事件监听器来接受云端的命令
// 这里我们监听一个事件 createOrUpdateApplication
}
createDesktop() {
// 加载本地文件,创建一个桌面,一切梦开始的地方,
// 我们可以读取application这个属性,初始化应用列表
}
}
app.whenReady().then(() => {
let container = new Container()
container.init()
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
sdk的实现
// sdk
const appName = 'appName'
export const sdk = {
listen(fn) {
ipc.answerMain(`someEvent`, async info => {
return fn(info)
});
},
async open() {
return await ipc.callMain(`open`, appName)
}
}
// 挂在到window对象上
contextBridge.exposeInMainWorld(
'api',
sdk
)
架构第三部分:实现细节
sdk的安全注入
建议采用上下文隔离的方式注入sdk
成熟的进程通信库的好处
electron-better-ipc 有如下好处
- 使用这个库能实现通信的广播,一下通用的功能应该放在container类里面
- 可以使用appName作为区分信道的方式,这样的逻辑代码放在application里面
- 这个能自动序列化错误信息比较好用
应该将应用桌面视为一个内置应用
通过这个应用,用户对应用的所有操作都可以通过进程通信代理到主进程上来执行。用户在应用桌面上的操作实际上就是主进程对窗口的操作。例如:
那么应用的后台运行,其实就是最小化应用。但是chrome会性能优化一下,后台应用可能会卡死,需要简单配置一下,可以看看我这文章, 主要思路是关闭一个特性:backgroundthrottling。
性能优化
假如你的sdk进行计算量比较大得操作,不建议在主进程里面做。
原因是 渲染进程和主进程还做了很多私底下得通信,你的同步计算可能另这些通信延迟,导致ui进程崩溃。具体可以看这个。
假如你不的不进行计算。我有如下建议
- 使用另一个进程
- 可以是render进程,就是透明窗口
- 可以是nodejs进程
- 使用线程
- 使用webworker
- 或者nodejs得worker
- 或者采用远古手段: Synchronous code-splitting
如果你对这些手段得优缺点,如何实现,如何配合webpack打包感兴趣得话,可以给我留言我,我考虑写一写。
另一种更新应用的方式
我们可以使用类似热更新的方式更新你的小程序。这种方式不需要下载html,实现无刷新加载。如果你想了解可以评论区告诉我。
应用开发的其他方面
- 如果写出健壮性的代码
- 如何构建nodejs错误体系
- 如何实现一个定时,可取消的,能重试的,复杂的nodejs任务。
- 如何在编写nodejsc++扩展并加载dll,实现dll的更新
- 如何减压主进程
- 如何windows系统下如何保活你的应用
假如你对以上方面感兴趣的话,麻烦大家评论告诉我!
总结
这篇简短的文章中,我们讨论了如何实现一个electron小程序架构。主要分为三部分介绍了这个架构
第一部分我们讨论了:应用的安装,创建更新
- 安装
- 升级
- 后台运行
第二部分:我们讨论了sdk的实现
- sdk的注入
- sdk的通信方式
- 推服务
- 拉服务
- 点对点和广播
- 还要一些注意事项
第三部分:我们讨论实现的一些细节,包括如下
- 安全性
- 性能
- 另一种应用更新方式
- 其他优化
开一个冠希哥的玩笑
转载自:https://juejin.cn/post/7120949692086616071