likes
comments
collection
share

绝对原创!!粗浅的electron离线包的设计方案以及实现

作者站长头像
站长
· 阅读数 48

前言

上一篇文章中我展示了electron应用程序更新的几种方案,但是阅读效果并不理想。 本来我是希望文章能为大家各种方案的优劣,这样大家实现业务需求的时候能够选择合适自己的方案。

但是现实往往不尽如人意。因此我将以一种松散的形式来叙述我的文章,希望能为大家提供其他价值。

文章结构

在这篇文章中,我将会提出一个在electron桌面技术基础上的离线包架构。小程序是类似于微信小程序的小应用。这个架构的内容包含三部分。

第一部分是实现一个应用桌面,用户可以通过应用桌面来添加,更新,删除小程序。

第二部分是阐述开发小程序时候使用的sdk的架构。

第三部分是对实现细节的描述,并且对如何提高应用性能,稳定性等问题提出普适性意见提出普适性的意见

解决的问题

  1. 为用户节约了安装应用的空间
  2. 实现在window端,mac端,linux端的跨端使用小程序
  3. 便于开发者灵活升级,添加应用

背景

什么是electron小程序

electon小程序是我自己创造的一个概念,本质上来说它是一个于electron桌面应用上运行的H5应用,这个应用能使用一般H5应用的操作系统的底层能力。

electon小程序架构的功能介绍

首先,从用户的角度来说。就如同像你手机上一个一个应用,在这套架构下用户用户可以打开,退出,在后台运行我们的应用,进一步地我们支持服务端和客户端主动安装,更新这些应用。下面的图片形象化地阐述了用户地这一需求。

绝对原创!!粗浅的electron离线包的设计方案以及实现

其次,为了方便应用开发者开发,我们将会提供一套SDK以便应用的作者能使用一些底层能力,比如说,存储,日志,网络请求,获取系统信息等能力。下面的图片形象化地阐述了一个普通开发者这一需求。

绝对原创!!粗浅的electron离线包的设计方案以及实现

实现

实现思路

为了分析这个问题,我们先了解一下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可以类比于服务器。

绝对原创!!粗浅的electron离线包的设计方案以及实现

实现步骤

这样想好像能实现,但是我感觉还是太复杂了。回想起我的偶像波利亚曾经说过,

假如你不能解决这个问题,你能解决一部分问题吗?

绝对原创!!粗浅的electron离线包的设计方案以及实现

架构第一部分:应用的安装,创建和更新

我们的问题最简单的版本就是如何加载一个html文件到一个electron窗口中。

第一个版本:小程序的本地加载

这是一个很基础的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的进程通信的实现思路

绝对原创!!粗浅的electron离线包的设计方案以及实现

上图主要介绍了是一个推和拉两种通信方式

应用拉

那么用户点击应用图标的时候,我们就可以使用sdk发起一个请求,附上我的appName,那么我们的container类可能就会去读取应用配置,下载或者加载html,并打开一个窗口。

这意味着我们必须有一个内置的应用,就是桌面了。这个应用可以随着electron程序下发时候一起下发下来。

绝对原创!!粗浅的electron离线包的设计方案以及实现

容器推

比如说应用订阅了网络状态,可能是应用启动的时候订阅了网络状态。我们只需要应用在我们的sdk里面传入一个回调。当我们网络状态发生改变的时候就触发回调即可。

点对点和广播
  1. 广播

有时候有些服务是相同的,可以使用广播的方式推,就是向所有应用推。我们也可以创建一个没有appName作为前缀的信道来提供公共服务

  1. 点对点

我们可以用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 有如下好处

  1. 使用这个库能实现通信的广播,一下通用的功能应该放在container类里面
  2. 可以使用appName作为区分信道的方式,这样的逻辑代码放在application里面
  3. 这个能自动序列化错误信息比较好用

应该将应用桌面视为一个内置应用

通过这个应用,用户对应用的所有操作都可以通过进程通信代理到主进程上来执行。用户在应用桌面上的操作实际上就是主进程对窗口的操作。例如:

那么应用的后台运行,其实就是最小化应用。但是chrome会性能优化一下,后台应用可能会卡死,需要简单配置一下,可以看看我这文章, 主要思路是关闭一个特性:backgroundthrottling。

性能优化

假如你的sdk进行计算量比较大得操作,不建议在主进程里面做。

原因是 渲染进程和主进程还做了很多私底下得通信,你的同步计算可能另这些通信延迟,导致ui进程崩溃。具体可以看这个。

假如你不的不进行计算。我有如下建议

  1. 使用另一个进程
    1. 可以是render进程,就是透明窗口
    2. 可以是nodejs进程
  2. 使用线程
    1. 使用webworker
    2. 或者nodejs得worker
  3. 或者采用远古手段: Synchronous code-splitting

如果你对这些手段得优缺点,如何实现,如何配合webpack打包感兴趣得话,可以给我留言我,我考虑写一写。

另一种更新应用的方式

我们可以使用类似热更新的方式更新你的小程序。这种方式不需要下载html,实现无刷新加载。如果你想了解可以评论区告诉我。

应用开发的其他方面

  1. 如果写出健壮性的代码
  2. 如何构建nodejs错误体系
  3. 如何实现一个定时,可取消的,能重试的,复杂的nodejs任务。
  4. 如何在编写nodejsc++扩展并加载dll,实现dll的更新
  5. 如何减压主进程
  6. 如何windows系统下如何保活你的应用

假如你对以上方面感兴趣的话,麻烦大家评论告诉我!

总结

这篇简短的文章中,我们讨论了如何实现一个electron小程序架构。主要分为三部分介绍了这个架构

第一部分我们讨论了:应用的安装,创建更新

  1. 安装
  2. 升级
  3. 后台运行

第二部分:我们讨论了sdk的实现

  1. sdk的注入
  2. sdk的通信方式
    1. 推服务
    2. 拉服务
    3. 点对点和广播
    4. 还要一些注意事项

第三部分:我们讨论实现的一些细节,包括如下

  1. 安全性
  2. 性能
  3. 另一种应用更新方式
  4. 其他优化

开一个冠希哥的玩笑

绝对原创!!粗浅的electron离线包的设计方案以及实现

转载自:https://juejin.cn/post/7120949692086616071
评论
请登录