Skip to content

Demian1996/Sugar-Electron

 
 

Repository files navigation

Suger-Electron

NPM version NPM quality David deps Known Vulnerabilities Lincense

安装

npm i sugar-electron --save-dev

Sugar-Electron 是什么?

Sugar-Electron为Electron跨平台桌面应用而生,我们希望由Sugar-Electron衍生出更多的上层框架,甚至打造基于Sugar-Electron框架生态,帮助开发团队和开发人员降低开发和维护成本。

我们知道Electron应用程序有三大基础模块。

  • 主进程
  • 渲染进程
  • 进程间通信

在以往大部分情况下,应用会将大量的业务逻辑写在主进程中,在渲染进程中只处理UI相关的逻辑,这为整个程序的稳定性埋下了隐患。在Electron中,主进程控制了整个程序的 生命周期,同时也负责管理它创建出来的各个渲染进程。一旦主进程的代码出现问题,那么会导致以下情况发生

  • 主进程出现不可捕获的异常:如通过ffi调用dll,dll未捕获异常会直接导致Electorn主进程崩溃,进而整个程序崩溃退出
  • 主进程代码中,写了耗时较长的同步代码或同步死循环。这会导致主进程阻塞,主进程阻塞又会导致全部渲染进程卡主,程序处于假死状态

为了解决诸如此类的问题,提高Electorn应用的稳定性,我们实现了Sugar-Electron这个轻量级的框架。

设计原则

Sugar-Electron所有的模块都基于渲染进程设计,将原有在主进程的逻辑移植到渲染进程中,原有的主进程仅充当守护进程的角色。 这样的好处是,及时渲染进程崩溃,也不会影响到整个应用,同时主进程可以守护当前挂掉的渲染进程,可以重新创建该进程,达到复原的效果。

以这样的模式开发,会有很多的问题需要解决,如进程管理、进程间通信、数据共享等,Sugar-Electron框架对这些模块进行了高度的封装,开发时不需要重写这些模块。 Sugar-Electron也借鉴了Egg.js的设计模式,提供了一套完整的开发规范来约束团队成员开发,提高项目代码一致性及降低沟通成本。

Sugar-Electron具有较高的扩展性,通过插件机制,可以讲框架的部分能力与框架本身解耦,同时使用者也可以根据自己的业务场景定制插件组合到框架中,降低开发成本。

设计原则

Sugar-Electron基于类微内核架构设计,将内部分为以下七大核心模块:

  • 基础进程类
  • 服务进程类
  • 进程通信
  • 进程状态共享
  • 配置
  • 插件
  • 进程管理中心

设计原则

注:基础进程类与服务进程类同属于原渲染进程

开始

需求:

  1. 两个窗口winA、winB,服务进程service,程序启动是创建winA。
  2. 在winA中,点击按钮btn1创建winB。
  3. 在winB创建完成后,winA点击按钮btn2设置winB size[400, 400]。
  4. winA点击按钮btn3 向winB B1、B2发送“我是winA”的消息,winB收到消息后回复“我是winB”
  5. winA点击按钮btn4 向service service-1发送“我是winA”的消息,service收到消息后回复“service-1响应”
// 主进程
const { start, BaseWindow, Service } = require('Sugar-Electron');
// 启动sugar-electron,basePath设置框架根目录
start({ appName: 'sugar-electron', basePath: __dirname });
// 设置窗口默认设置,详情请参考Electron BrowserWindow文档
BaseWindow.setDefaultOptions({
  show: false
});

// winA
const winA = new BaseWindow('winA', {
   url: `file://${__dirname}/indexA.html`
});

// winB
const winB = new BaseWindow('winB', {
   url: `file://${__dirname}/indexB.html`
});

// service
const service = new Service('service', path.join(__dirname, 'app.js'));
service.on('success', function () {
   console.log('service进程启动成功');
});
service.on('fail', function () {
   console.log('service进程启动异常');
});

// 创建winA窗口实例
winA.open();
// winA
const { windowCenter } = require('Sugar-Electron');
const btn1 = document.querySelector('#btn1');
const btn2 = document.querySelector('#btn2');
const btn3 = document.querySelector('#btn3');
const btn4 = document.querySelector('#btn4');
// 获取winB句柄
const winB = windowCenter.winB;
// 获取service句柄
const service = windowCenter.service;
btn1.onclick = async function () {
   // 创建winB窗口实例
   await winB.open();
   
   // 订阅窗口创建完成“ready-to-show”
   const unsubscriber = winB.subscriber('ready-to-show', () => {
       // 解绑订阅
       unsubscriber();
       
       btn2.onclick = async function () {
           // 设置winB size[400, 400]
           const r1 = await winB.setSize(400, 400);
           // 获取winB size[400, 400]
           const r2 = await winB.getSize();
           console.log(r1, r2);
       }
   });
};

btn3.onclick = async function () {
   // 向winB请求B1
   const r1 = await winB.request('B1', '我是winA');
   console.log(r1); // B1,我是winB 
   // 向winB请求B2
   const r2 = await winB.request('B2', '我是winA');
   console.log(r2); // B2,我是winB
}

btn4.onclick = async function () {
   // 向winB请求B1
   const r1 = await service.request('service-1', '我是winA');
   console.log(r1); // service-1响应
}
// winB
const { ipc } = require('Sugar-Electron');
ipc.response('B1', (json, cb) => {
   console.log(json); // 我是winA
   cb('B1,我是winB');
});

ipc.response('B2', (json, cb) => {
   console.log(json); // 我是winA
   cb('B2,我是winB');
});
// service app.js
const { ipc } = require('Sugar-Electron');
ipc.response('service-1', (json, cb) => {
   console.log(json); // 我是winA
   cb('service-1响应');
});

基础进程类——BaseWindow

说明

Sugar-Electron框架核心基础进程类,以基础进程类为载体,聚合了框架所有核心模块。Sugar-Electron基础进程类BaseWindow基于BrowserWindow二次封装,新增方法:

  • open // 创建一个BrowserWindow示例,并返回BrowserWindow实例
  • getInstance // 获取BrowserWindow示例,示例未创建则返回null

一般情况下,基础进程类用于创建原有的渲染进程,处理窗口UI界面相关的逻辑。

示例

需求:

  1. 创建winA实例
  2. 创建winA窗口
// 主进程
const { BaseWindow } = require('Sugar-Electron');
// 设置窗口默认设置,详情请参考Electron BrowserWindow文档
BaseWindow.setDefaultOptions({
  show: false
});

// 创建winA实例
const winA = new BaseWindow('winA', {
   url: `file://${__dirname}/indexA.html`
});

// 创建winA窗口
winA.open();
// winA
const { windowCenter, ipc } = require('Sugar-Electron');
const winA = windowCenter.winA;
// 支持BrowserWindow实例所有生命周期对应事件
winA.subscriber('ready-to-show', () => { ... });
winA.subscriber('show', () => { ... });
winA.subscriber('hide', () => { ... });
winA.subscriber('focus', () => { ... });
...
// 支持BrowserWindow实例所有接口对应函数,但同步调用改成promise(原因是,sugar内部实现其实是通过ipc异步通信调用主进程窗口实例的函数)
await winA.setSize();
await winA.getSize();
await winA.show();
await winA.hide();
...

服务进程类——Service

说明

Sugar-Electron框架提供服务进程类,开发者只需要传入启动入口文件,即可创建一个服务进程。

所谓的服务进程,即承载了原来主进程应该执行的代码的渲染进程。上面有介绍到,为了保障整个应用的稳定性,我们将原来处于主进程中的业务逻辑,转移到了服务进程中。 它本质上就是一个渲染进程,拥有渲染进程的所有能力。只是在这基础之上,框架赋予了它特殊的能力,使得它能满足我们的业务场景,变为了”服务进程“。

服务进程与渲染进程区别:

  • 服务进程不显示界面,纯执行逻辑
  • 服务进程崩溃关闭后,可自动重启
  • 服务进程在崩溃重启后,可通过进程数据共享模块复原状态

示例

// 主进程
// 启动service
const service = new Service('service', path.join(__dirname, 'app.js'));
service.on('success', function () {
    console.log('service进程启动成功');
});
service.on('fail', function () {
    console.log('service进程启动异常');
});
// app.js
const { ipc } = require('Sugar-Electron');
ipc.response('service-1', (json, cb) => {
    cb('service-1响应');
});
ipc.response('service-2', (json, cb) => {
    cb('service-2响应');
});

进程通信——ipc

说明

Sugar-electron是多进程架构设计,进程间通信必不可少。

ipc作为Sugar-electron进程间通信核心模块,支持两种通信方式:

  • 请求响应(渲染进程间)
  • 发布订阅(渲染进程间)
  • 主进程与渲染进程通信

请求响应

进程间通信

进程间通信

示例

// 服务进程service
const { ipc } = require('Sugar-electron');  
// 注册响应服务A1
ipc.response('service-1', (json, cb) => {
    console.log(json); // { name: 'winA' }
    cb('service-1响应');
});
// winA
const { ipc, windowCenter } = require('Sugar-electron');  
const btn1 = document.querySelector('#btn1');
btn1.onclick = () => {
    const r1 = await windowCenter.service.request('service-1', { name: 'winA' });
    console.log(r1); // service-1响应
    // 等同
    const r2 = await ipc.request('service', 'service-1', { name: 'winA' });
    console.log(r2); // service-1响应
};

异常

Sugar-electron对响应异常做处理。

状态码(code) 说明
1 找不到进程
2 找不到进程注册服务
3 超时

发布订阅

进程间通信

示例

// 服务进程service
const { ipc } = require('Sugar-electron');
setInterval(() => {
    ipc.publisher('service-publisher', { name: '发布消息' });
}, 1000);
// winA
const { ipc, windowCenter } = require('Sugar-electron');  
const btn1 = document.querySelector('#btn1');
// 订阅
const unsubscriber1 = windowCenter.service.subscriber('service-publisher', (json) => {
    console.log(json); // { name: '发布消息' }
});
// 订阅
const unsubscriber2 = ipc.subscriber('service', 'service-publisher', (json) => {
    console.log(json); // { name: '发布消息' }
});

btn1.onclick = () => {
    // 取消订阅
    unsubscriber1();
    unsubscriber2();
    // 或windowCenter.service.unsubscriber('service-publisher', cb);
    // 或ipc.unsubscriber('service', 'service-publisher', cb);
};

注:渲染进程订阅消息队列在主进程内缓存,所以发布服务进程重启不需要重新订阅,且通过监听渲染进程关闭事件,可自动释放对应的渲染进程缓存消息队列。

主进程与渲染进程间通信

Sugar-electron框架设计理念所有业务模块都有各个渲染进程完成,所以基本上不存在与主进程通信的功能,但也非绝无仅有。所以Sugar-electron进程通信模块支持与主进程通信接口。

示例

// 主进程
const { ipc } = require('Sugar-electron');
ipc.response('test', (data, cb) => {
    console.log(data); // 我是渲染进程
    cb('我是主进程')
});

// winA
const { ipc } = require('Sugar-electron');
// 请求
ipc.request('main', 'test', '我是渲染进程' , (data) => {
    console.log(data); // 我是主进程
});

进程管理中心——windowCenter

说明

Sugar-electron是是多进程架构设计,在业务系统中,避免不了进程间相互调用,由于进程间彼此独立且进程可能并没有创建,并不能通过ipc消息调用。

sugar-electron核心模块windowCenter就是为了解决此类问题存在,保证所有的业务逻辑在独立在各个进程内完成,屏蔽中间进程通信流程。

windowCenter默认根据根目录windowCenter自动挂载基础进程

示例

例如:winA内打开winB,并在winB webContents初始化完成后,设置窗口B setSize(400, 400)。

// 主进程
const { BaseWindow, Service, windowCenter } = require('Sugar-Electron');
// 设置窗口默认设置,详情请参考Electron BrowserWindow文档
BaseWindow.setDefaultOptions({
  show: false
});

// winA
const winA = new BaseWindow('winA', {
   url: `file://${__dirname}/indexA.html`
});

// winB
const winB = new BaseWindow('winB', {
   url: `file://${__dirname}/indexB.html`
});

// 创建winA窗口实例
windowCenter.winA.open(); // 等同于winA.open();
// winA
const { windowCenter } = require('Sugar-Electron');
const btn1 = document.querySelector('#btn1');
const btn2 = document.querySelector('#btn2');
// 获取winB句柄
const winB = windowCenter.winB;
btn1.onclick = async function () {
   // 创建winB窗口实例
   await winB.open();
   
   // 订阅窗口创建完成“ready-to-show”
   const unsubscriber = winB.subscriber('ready-to-show', () => {
       // 解绑订阅
       unsubscriber();
       
       btn2.onclick = async function () {
           // 设置winB size[400, 400]
           const r1 = await winB.setSize(400, 400);
           // 获取winB size[400, 400]
           const r2 = await winB.getSize();
           console.log(r1, r2);
       }
   });
};

备注:服务进程句柄通过windowCenter也可以获取

进程间状态共享——store

说明

Sugar-electron是多进程架构设计,在业务系统中,避免不了多个业务进程共享状态。由于进程间内存相互独立,不互通,为此Sugar-electron框架集成了进程状态共享模块。

进程状态共享模块分成两个部分:

  • 主进程申明共享状态数据
  • 渲染进程设置、获取共享状态数据

注:sugar-electron默认根据根目录store自动初始化

示例

// 主进程——初始化申明state
const { store } = require('Sugar-electron');
store.createStore({
    state: {
        name: '我是store'
    },
    modules: {
        moduleA: {
            state: {
                name: '我是moduleA'
            }
        },
        moduleB: {
            state: {
                name: '我是moduleB'
            }
        }
    }
});
 
// 渲染进程
const { store } = require('Sugar-electron');
const r1 = await store.getState('name');
console.log(r1.value); // 我是store
// 订阅r1更新消息
const unsubscriber1 = r1.subscriber((value) => {
    console.log('更新:', value); // 更新:我是store1
});
await store.setState('name', '我是store1');
unsubscriber1(); // 取消订阅,或r1.unsubscriber(cb);
 
const moduleA = await store.getModule('moduleA');
const r2 = await moduleA.getState('name'); // 我是moduleA
console.log(r2.value); // 我是moduleA

// Error: 找不到store state key => .none,请在主进程初始化store中声明
await store.setState('none', '没有声明的state'); 

配置——config

说明

Sugar-electron提供了多环境配置,可根据环境变量切换配置,默认加载生成环境配置。

config
|- config.base.js     // 基础配置
|- config.js          // 生产配置
|- config.test.js     // 测试配置——环境变量env=test
|- config.dev.js      // 开发配置——环境变量env=dev

配置

注:

  • AppData/appName 配置文件config.json { "env": "环境变量", "config": "配置" }
  • sugar-electron默认根据根目录config自动初始化

示例

// 主进程
const { config } = require('Sugar-electron');
const { start, config } = require('Sugar-Electron');
// 启动sugar-electron,basePath设置框架根目录
start({ appName: 'sugar-electron', basePath: __dirname });
 
// 渲染进程
const { config } = require('Sugar-electron');
console.log(config);

插件——plugins

说明

一个好用的框架离不开框架的可扩展性,Sugar-electron插件模块提供开发者扩展Sugar-electron功能的能力。 Sugar-electron通过框架聚合这些插件,开发者可根据自己的业务场景定制配置,开发应用成本变得更低。

示例

使用一款插件,需要三个步骤:

  • 自定义封装
  • config目录配置问题plugins.js配置插件安装
  • 使用插件

插件封装

// 1、自定义封装ajax插件adpter
const axios = require('axios');
const apis = {
    FETCH_DATA_1: {
        url: '/XXXXXXX1',
        method: 'POST'
    },
    FETCH_DATA_2: {
        url: '/XXXXXXX2',
        method: 'GET'
    },
    FETCH_DATA_3: {
        url: '/XXXXXXX3',
        method: 'PUT'
    },
    FETCH_DATA_4: {
        url: '/XXXXXXX4',
        method: 'POST'
    }
}

module.exports = {
    /**
     * 安装插件,自定义插件必备
     * @ctx [object] 框架上下文对象{ config, ipc, store, windowCenter }
     * @params [object] 配置参数
    */
    install(ctx, params = {}) {
        // 通过配置文件读取基础服务配置
        const baseServer = ctx.config.baseServer;
        return {
            async callAPI(action, options) {
                const { method, url } = apis[action];
                try {
                    // 通过进程状态共享SDK获取用户ID
                    const token = await ctx.store.getState('token');
                    const res = await axios({
                        method,
                        url: `${baseServer}${url}`,
                        data: options,
                        timeout: params.timeout // 通过插件配置超时时间
                    });
                    if (action === 'LOGOUT') {
                        // 通过进程间通信模块,告知主进程退出登录
                        ctx.ipc.sendToMain('LOGOUT');
                    }

                    return res;
                } catch (error) {
                    throw error;
                }
            }
        }
    }
}

插件安装

// 2、配置插件安装
const path = require('path');
exports.adpter = {
    // 如果根路径plugins目录有对应的插件名,则不需要配置path或package
    path: path.join(__dirname, '../plugins/adpter'),  // 插件绝对路径
    package: 'adpter',  // 插件包名,如果package与path同时存在,则package优先级更高
    enable: true, // 是否启动插件
    include: ['winA'], // 插件使用范围,如果为空,则所有渲染进程安装
    params: { timeout: 20000 } // 传入插件参数
};

插件使用

// 3、使用插件——winA
const { plugins } = require('Sugar-electron');
const res = await plugins.adpter.callAPI('FETCH_DATA_1', {});

API文档

/**
 * @appName [string] 可选应用name
 * @basePath [string] 可选sugar-electron自动初始化模块(config、store、windowCenter)根目录
 */
start(appName, basePath)

基础进程类BaseWindow

/**
 * @name [string] 必选 渲染进程名,唯一标识
 * @options [object] 可选 窗口配置,具体可参考electron BrowserWindow
 */
class BaseWindow(name, options)
实例
- open(options) // 创建一个BrowserWindow示例,并返回BrowserWindow实例;option可选参数,覆盖BaseWindow实例窗口配置
- getInstance() // 获取BrowserWindow示例,示例未创建则返回null

服务进程类Service

/**
 * @name [string] 必选 服务进程名,唯一标识
 * @path [string] 必选 服务进程启动文件,绝对路径
 */
class Service(name, path)
实例
- start(isDebug) // 开始服务进程,isDebug是否打开调试工具
- stop() // 结束服务进程

配置config

/**
 * @option [object] 可选
 * appName [string] 应用名,默认''
 * configPath [string] 配置目录路径,默认根目录config
 */
setOption({ appName, configPath })

进程间通信ipc

// 主进程

/**
 * 响应,主进程名`main`,渲染进程通过ipc.request('main', eventName)请求主进程服务
 * @eventName [string] 事件名  
 * @callback [function] 回调
 */
response(eventName, callback)

// 渲染进程
/**
 * 设置响应超时时间
 * @timeout [number] 时间毫秒
 */
setDefaultRequestTimeout(timeout)

/**
 * 请求
 * @toId [string] 进程ID(注册通信进程模块名) 
 * @eventName [string] 事件名 
 * @data 请求参数 
 * @timeout [number] 超时时间,默认20s * 
 * @return 返回Promise对象
 */
request(toId, eventName, data, timeout)

/**
 * 响应
 * @eventName [string] 事件名  
 * @callback [function] 回调
 */
response(eventName, callback)

/**
 * 发布
 * @eventName [string] 事件名  
 * @params 参数
 */
publisher(eventName, params)

/**
 * 订阅
 * @toId [string] 进程ID(注册通信进程模块名) 
 * @eventName [string] 事件名 
 * @callback [function] 回调
 */
subscriber(toId, eventName, callback)

/**
 * 取消订阅
 * @toId [string] 进程ID(注册通信进程模块名) 
 * @eventName [string] 事件名 
 * @callback [function] 回调
 */
unsubscribe(toId, eventName, callback)

进程管理中心windowCenter

在主进程、渲染进程获取进程句柄,例如窗口A windowCenter['winA'];

进程间状态共享store

// 主进程
/**
 * 初始化state
 * @createStore [object] 初始化state
 */
createStore({ appName, configPath })

// 渲染进程
/**
 * 设置state
 * @key [string]
 * @value state
 * @return 返回Promise对象
 */
setState(key, value)

/**
 * 获取state
 * @key [string] 事件名
 * @return 返回Promise对象 { value: '值', subscriber: '订阅更新', unsubscriber: '取消订阅'}
 */
getState(key)

/**
 * 获取module
 * @moduleName [string] 模块名
 * @return 返回Promise对象
 * 返回:setState: 设置当前模块state getState: 获取当前模块state getModule: 获取当前模块的子模块
 */
getModule(moduleName)

插件plugins

请参考插件说明模块

About

基于Electron的轻型开发框架

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • JavaScript 96.6%
  • HTML 3.4%