今天給你們帶來一款基於Electron桌面開發平臺的自研應用框架Sugar-Electron,指望能改善Electron應用穩定性和幫助開發團隊下降開發和維護成本。html
筆者使用Electron作桌面應用,已經有3年的時間,期間也遇到不少大大小小的坑。但總結起來,最大的問題仍是應用穩定性和開發效率問題。咱們指望經過這個框架,能讓應用程序在這兩個方面有所優化。webpack
項目源碼地址: github.com/SugarTurboS…ios
若有任何疑問,能夠掃碼加入微信羣聊討論git
咱們知道Electron應用程序有三大基礎模塊。github
因爲咱們屬於多窗口(多渲染進程)的應用,因此咱們會把窗口公共的服務模塊都寫到主進程模塊,這爲整個程序的穩定性埋下了隱患。web
在Electron中,主進程控制了整個程序的生命週期,同時也負責管理它建立出來的各個渲染進程。一旦主進程的代碼出現問題,那麼會致使如下狀況發生。ajax
因此,在Sugar-Electron中,咱們引入了Service進程的概念,指望將業務原來寫在主進程的代碼,遷移到Service進程中(本質上是渲染進程),使得這些代碼致使的崩潰不會使得整個程序退出。而主進程的進程管理器能夠在Service崩潰時,重啓該進程並恢復崩潰前的狀態,從而提升整個程序的穩定性和可用性。npm
Electron屬於桌面開發平臺提供桌面應用開發的能力框架,上手簡單。但框架自己缺乏約定,所以使用Electron作應用開發,系統模塊會出現各類千奇百怪的劃分,代碼會出現多種多樣的寫法,這會顯著的增長學習成本,下降開發人員的效率。sugar-electron按照約定進行開發,下降團隊協做成本,以提高效率。json
1、sugar-electron一切圍繞渲染進程爲核心設計,主進程只是充當進程管理(建立、刪除、異常監控)和調度(進程通訊、狀態功能橋樑)的守護進程的角色。axios
主進程不處理業務邏輯,這麼設計的好處:
2、sugar-electron全部的業務模塊都是渲染進程。咱們知道進程之間是不能直接訪問的,爲了讓進程之間的調用就像同線程模塊之間直接調用同樣方便,sugar-electron提供瞭如下三個模塊:
3、爲了保證框架核心的足夠精簡、穩定、高效,所以框架的擴展能力相當重要,爲此sugar-electron提供自定義插件機制擴展框架能力,還能夠促進業務邏輯複用,甚至於生態圈的造成。
以下是框架邏輯視圖:
sugar-electron基於類微內核架構設計,以下圖所示:
其框架核心有七大模塊:
npm i sugar-electron
複製代碼
npm i sugar-electron-cli -g
sugar-electron-cli init
複製代碼
基礎進程類BaseWindow基於BrowserWindow二次封裝,sugar-electron以BaseWindow爲載體,聚合了框架全部核心模塊。
使用BrowserWindow建立渲染進程
// 在主進程中.
const { BrowserWindow } = require('electron')
let win = new BrowserWindow({ width: 800, height: 600, show: false });
win.on('ready-to-show', () => {})
win.loadURL('https://github.com');
複製代碼
使用BaseWindow建立渲染進程
// 在主進程中.
const { BaseWindow } = require('sugar-electron');
let win = new BaseWindow('winA', {
url: 'https://github.com' // BaseWindow 特有屬性,默認打開的頁面
width: 800, ght: 600, show: false
});
win.on('ready-to-show', () => {})
const browserWindowInstance = winA.open();
複製代碼
在實際業務開發中,咱們須要有一個進程去承載業務進程通用模塊的功能,Service爲此而生。Service進程實例實際上也是渲染進程,只是開發者只須要傳入啓動入口js文件,便可建立一個渲染進程,且BaseWindow同樣,聚合框架全部核心模塊。
// -----------------------主進程-----------------------
const service = new Service('service', path.join(__dirname, 'app.js'), true);
service.on('success', function () {
console.log('service進程啓動成功');
});
service.on('fail', function () {
console.log('service進程啓動異常');
});
service.on('crashed', function () {
console.log('service進程崩潰'); // 對應webContents.on('crashed')
});
service.on('closed', function () {
console.log('service進程關閉'); // 對應browserWindow.on('closed')
});
複製代碼
ipc做爲進程間通訊核心模塊,支持三種通訊方式:
邏輯視圖:
// 服務進程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 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響應
複製代碼
異常
狀態碼 1 | 說明 2 |
---|---|
1 | 找不到進程 |
2 | 找不到進程註冊服務 |
3 | 超時 |
邏輯視圖:
// 服務進程service
const { ipc } = require('sugar-electron');
setInterval(() => {
ipc.publisher('service-publisher', { name: '發佈消息' });
}, 1000);
// winA
const { ipc, windowCenter } = require('sugar-electron');
// 訂閱
const unsubscribe = windowCenter.service.subscribe('service-publisher', (json) => {
console.log(json); // { name: '發佈消息' }
});
// 等同
const unsubscribe = ipc.subscribe('service', service-publisher', (json) => {
console.log(json); // { name: '發佈消息' }
});
// 取消訂閱
unsubscribe();
// 等同
windowCenter.service.unsubscribe('service-publisher', cb);
複製代碼
sugar-electron框架設計理念全部業務模塊都有各個渲染進程完成,因此基本上不存在與主進程通訊的功能,但不排除有主進程與渲染進程通訊的場景。因此sugar-electron進程通訊模塊支持與主進程通訊接口。
// 主進程
const { ipc } = require('sugar-electron');
ipc.response('test', (data, cb) => {
console.log(data); // 我是渲染進程
cb('我是主進程')
});
// winA
const { windowCenter } = require('sugar-electron');
const res = windowCenter.main.request('test', '我是渲染進程');
console.log(res); // 我是主進程
// 或者
const res = ipc.request('main', 'test', '我是渲染進程');
console.log(res); // 我是主進程
複製代碼
sugar-electron全部的業務模塊都是渲染進程。咱們知道進程之間是不能直接訪問的,全部有了進程管理模塊。
全部的渲染進程都能在windowCenter中根據進程名對應的惟一key找到對應的渲染進程,讓進程之間的調用就像同線程模塊之間直接調用同樣方便。
需求:winA內打開winB,並在winB webContents初始化完成後,設置窗口B setSize(400, 400)
// 主進程
const { BaseWindow, Service, windowCenter } = require('sugar-electron');
// 設置窗口默認設置,詳情請參考Electron BrowserWindow文檔
BaseWindow.setDefaultOption({
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 winB = windowCenter.winB;
// 建立winB窗口實例
await winB.open();
// 訂閱窗口建立完成「ready-to-show」
const unsubscribe = winB.subscribe('ready-to-show', () => {
// 解綁訂閱
unsubscribe();
// 設置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也能夠獲取==
sugar-electron是多進程架構設計,在業務系統中,避免不了多個業務進程共享狀態。因爲進程間內存相互獨立,不互通,爲此sugar-electron框架集成了進程狀態共享模塊。
進程狀態共享模塊分紅兩個部分:
// 主進程——初始化申明state
const { store } = require('sugar-electron');
store.createStore({
state: {
name: '我是store'
},
modules: {
moduleA: {
state: {
name: '我是moduleA'
}
},
moduleB: {
state: {
name: '我是moduleB'
},
modules: {
moduleC: {
state: {
name: '我是moduleC'
}
}
}
}
}
});
複製代碼
// 渲染進程A,訂閱state變化
const { store } = require('sugar-electron');
console.log(store.state.name); // 我是store
// 訂閱更新消息
const unsubscribe = store.subscribe((data) => {
console.log(store.state.name); // 改變state
unsubscribe(); // 取消訂閱
});
// moduleA
const moduleA = store.getModule('moduleA');
console.log(moduleA.state.name); // 我是moduleA
const unsubscribeA = moduleA.subscribe((data) => {
console.log(moduleA.state.name); // 改變moduleA
unsubscribeA(); // 取消訂閱
});
複製代碼
// 渲染進程B,設置state
const { store } = require('sugar-electron');
await store.setState({
'name': '改變state'
});
// moduleA
const moduleA = store.getModule('moduleA');
await moduleA.setState({
'name': '改變moduleA'
});
複製代碼
sugar-electron提供了多環境配置,可根據環境變量切換配置,默認加載生成環境配置。
config
|- config.base.js // 基礎配置
|- config.js // 生產配置
|- config.test.js // 測試配置——環境變量env=test
|- config.dev.js // 開發配置——環境變量env=dev
複製代碼
流程圖:
// 主進程
const { config } = require('sugar-electron');
global.config = config.setOption({ appName: 'sugar-electron', configPath: path.join(__dirname, 'config') });
// 渲染進程
const { config } = require('sugar-electron');
console.log(config);
複製代碼
==備註:==
一個好用的框架離不開框架的可擴展性和業務複用。開發者經過plugins模塊自定義插件和配置安裝插件。
==使用一款插件,須要三個步驟:==
// 一、自定義封裝ajax插件adpter
const axios = require('axios');
const apis = {
FETCH_DATA_1: {
url: '/XXXXXXX1',
method: 'POST'
}
}
module.exports = {
/**
* 安裝插件,自定義插件必備
* @ctx [object] 框架上下文對象{ config, ipc, store, windowCenter, plugins }
* @params [object] 配置參數
*/
install(ctx, params = {}) {
// 經過配置文件讀取基礎服務配置
const baseServer = ctx.config.baseServer;
return {
async callAPI(action, option) {
const { method, url } = apis[action];
try {
// 經過進程狀態共享SDK獲取用戶ID
const token = ctx.store.state.token;
const res = await axios({
method,
url: `${baseServer}${url}`,
data: option,
timeout: params.timeout // 經過插件配置超時時間
});
if (action === 'LOGOUT') {
// 經過進程間通訊模塊,告知主進程退出登陸
ctx.ipc.sendToMain('LOGOUT');
}
return res;
} catch (error) {
throw error;
}
}
}
}
}
複製代碼
在配置中心目錄plugins.js配置插件安裝
config
|- config.base.js // 基礎配置
|- config.js // 生產配置
|- config.test.js // 測試配置——環境變量env=test
|- config.dev.js // 開發配置——環境變量env=dev
|- plugins.js // 插件配置文件
複製代碼
// 二、配置插件安裝
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 } // 傳入插件參數
};
複製代碼
// 三、使用插件——winA
const { plugins } = require('sugar-electron');
const res = await plugins.adpter.callAPI('FETCH_DATA_1', {});
複製代碼
使用過egg開發者應該知道,egg基礎功能模塊會根據對應的目錄自動初始化。sugar-electron也提供根據目錄自動初始化的能力。只須要使用框架啓動接口start傳入配置參數便可完成核心模塊自動初始化
const { start } = require('sugar-electron');
start({
appName: '應用名',
basePath: '啓動目錄',
configPath: '配置中心目錄', // 可選,默認basePath + './config'
storePath: '進程狀態共享目錄', // 可選,默認basePath + './store'
windowCenterPath: '配置中心目錄', // 可選,默認basePath + './windowCenter'
pluginsPath: '插件目錄', // 可選,默認basePath + './plugins'
})
複製代碼
一、因爲sugar-electron核心模塊會自動判斷主進程或者渲染進程環境,自動選擇加載不一樣環境的模塊,若是使用webpack打包會致使把兩個環境的代碼都打包進去,可能還會出現異常。
所以,若是使用webpack打包,引入sugar-electron採用以下方式:
// 主進程
const { ipc, store, ... } = require('sugar-electron/main')
// 渲染進程
const { ipc, store, ... } = require('sugar-electron/render')
複製代碼
框架啓動接口,自動掛載config、store、windowCenter、plugins模塊
主進程API
/**
* 啓動sugar
* @param {object} options 啓動參數
* @param {string} options.appName 應用名
* @param {string} options.basePath 啓動目錄
* @param {string} options.configPath 配置目錄,默認basePath + './config'
* @param {string} options.storePath 進程狀態共享目錄,默認basePath + './store'
* @param {string} options.windowCenterPath 窗口中心目錄,默認basePath + './windowCenter'
* @param {string} options.pluginsPath 插件目錄,默認basePath + './plugins'
*/
start(opions)
複製代碼
使用舉例
// -----------------------主進程-----------------------
start({
appName: string, 應用名,%appData%目錄
basePath: string, 啓動目錄
configPath: string, 配置目錄
storePath: string, 進程狀態共享目錄
windowCenterPath: string, 窗口中心目錄
pluginsPath: string 插件目錄
});
複製代碼
/**
* 主進程調用
* @param {string} name
* @param {object} option
*/
new BaseWindow(name, option);
複製代碼
主進程API
setDefaultOptions [類方法]設置窗口默認配置
/**
* @param {object} option 參考electron BrowserWindow
*/
setDefaultOptions(option)
複製代碼
open [實例方法]建立一個BrowserWindow實例
/**
* @param {object} option 參考electron BrowserWindow
* @return {browserWindow}
*/
open(option)
複製代碼
getInstance [實例方法]
/**
* @return {browserWindow}
*/
getInstance(option)
複製代碼
publisher [實例方法]向當前窗口發佈通知,可參考ipc模塊
/**
* @param {string} eventName 通知事件名
* @param {object} param 參數
* @return {browserWindow}
*/
publisher(eventName, param)
複製代碼
使用舉例
// -----------------------主進程-----------------------
const { BaseWindow } = require('sugar-electron');
BaseWindow.setDefaultOptions({
width: 600,
height: 800,
show: false,
...
});
const winA = new BaseWindow('winA', { url: 'https://github.com' });
const instance = winA.open({...}); // 建立窗口
instance === winA.getInstance(); // true
複製代碼
主進程API
/*
* 建立服務進程
* @param {string} name 服務進程名
* @param {string} path 啓動入口文件路徑(絕對路徑)
* @param {boolean} devTool 是否打開調試工具,默認false
*/
new Service(name = '', path = '', openDevTool = false);
複製代碼
使用舉例
// -----------------------主進程-----------------------
const service = new Service('service', path.join(__dirname, 'app.js'), true);
service.on('success', function () {
console.log('service進程啓動成功');
});
service.on('fail', function () {
console.log('service進程啓動異常');
});
service.on('crashed', function () {
console.log('service進程崩潰'); // 對應webContents.on('crashed')
});
service.on('closed', function () {
console.log('service進程關閉'); // 對應browserWindow.on('closed')
});
複製代碼
主進程、渲染進程API
windowCenter: object 進程集合key=進程名 value=進程實例,默認{}
複製代碼
使用舉例
// -----------------------主進程-----------------------
const service = new Service('service', path.join(__dirname, 'app.js'), true);
const winA = new BaseWindow('winA', {});
const winB = new BaseWindow('winB', {});
windowCenter['service'] === service; // true
windowCenter['winA'] === winA; // true
windowCenter['winB'] === winB; // true
windowCenter['winA'].open(); // 建立winA窗口實例,同步調用
windowCenter['winA'].on('ready-to-show', () => {
windowCenter['winA'].setFullscreen(true);
});
// -----------------------渲染進程-----------------------
// 渲染進程接口調用其實是經過ipc通道通知主進程進程接口調用,因此接口異步而非同步
(async () => {
await windowCenter['winA'].open(); // 建立winA窗口實例,異步Promise調用
windowCenter['winA'].subscribe('ready-to-show', async () => {
await windowCenter['winA'].setFullscreen(true);
});
})()
複製代碼
主進程API response 響應
/**
* 註冊響應服務
* @param {string} eventName 事件名
* @param {function} callback 回調
* 使用方式,渲染進程ipc.request('main', eventName, param)
*/
response(eventName, callback)
複製代碼
渲染進程API
setDefaultRequestTimeout 設置響應超時時間
setDefaultRequestTimeout(timeout = 0);
複製代碼
request 請求
/**
* @param {string} toId 進程ID(註冊通訊進程模塊名)
* @param {string} eventName 事件名
* @param {any} data 請求參數
* @param {number} timeout 超時時間,默認20s *
* @return 返回Promise對象
*/
request(toId, eventName, data, timeout)
複製代碼
response 響應
/**
* 註冊響應服務
* @param {string} eventName 事件名
* @param {function} callback 回調
*/
response(eventName, callback)
複製代碼
unresponse 註銷響應服務
/**
* @param {string} eventName 事件名
* @param {function} callback 回調
*/
unresponse(eventName, callback)
複製代碼
publisher 發佈
/**
* @param {string} eventName 事件名
* @param {any} param 參數
*/
publisher(eventName, param)
複製代碼
subscribe 訂閱
/**
* @param {string} toId 進程ID(註冊通訊進程模塊名)
* @param {string} eventName 事件名
* @param {function} callback 回調
*/
subscribe(toId, eventName, callback)
複製代碼
unsubscribe 取消訂閱
/**
* @param {string} toId 進程ID(註冊通訊進程模塊名)
* @param {string} eventName 事件名
* @param {function} callback 回調
*/
unsubscribe(toId, eventName, callback)
複製代碼
使用舉例
// ---------------------winA---------------------
const { ipc } = require('sugar-electron');
// 註冊響應服務A1
ipc.response('get-data', (json, cb) => {
console.log(json); // { name: 'winB' }
cb('winA響應');
});
// ---------------------winB---------------------
const { ipc, windowCenter } = require('sugar-electron');
const btn1 = document.querySelector('#btn1');
const { winA } = windowCenter;
btn1.onclick = () => {
const r1 = await winA.request('get-data', { name: 'winB' });
console.log(r1); // winA響應
// 等同
const r2 = await ipc.request('get-data', 'get-data', { name: 'winB' });
console.log(r2); // winA響應
}
複製代碼
主進程API
createStore 初始化state
/**
* @param {object} store
*/
createStore(store)
複製代碼
渲染進程API
setState 設置state
/**
* 單個值設置
* @param {string} key
* @param {any} value
* @return 返回Promise對象
*/
setState(key, value)
/**
* 批量設置
* @param {object} state
* @return 返回Promise對象
*/
setState(state)
複製代碼
subscribe 訂閱當前module的值變化通知
/**
* @param {function} cb 訂閱回調
* @return {function} 返回註銷訂閱function
*/
subscribe(cb)
複製代碼
unsubscribe 註銷訂閱
/**
* @param {funtion} cb 訂閱回調
*/
unsubscribe(cb)
複製代碼
getModule 獲取module
/**
* 獲取module
* @param {string} moduleName 模塊名
* @return {object} module
* 返回:setState: 設置當前模塊state;subscribe: 訂閱;unsubscribe: 註銷訂閱;getModule: 獲取當前模塊的子模塊;getModules
*/
getModule(moduleName)
複製代碼
getModules 獲取全部modules
/**
* @return {array} [module, module, module]
*/
getModules()
複製代碼
使用舉例
// 主進程——初始化申明state
const { store } = require('sugar-electron');
store.createStore({
state: {
name: '我是store'
},
modules: {
moduleA: {
state: {
name: '我是moduleA'
}
},
moduleB: {
state: {
name: '我是moduleB'
},
modules: {
moduleC: {
state: {
name: '我是moduleC'
}
}
}
}
}
});
// 渲染進程
const { store } = require('sugar-electron');
store.state.name; // 我是store
// 訂閱更新消息
const unsubscribe = store.subscribe((data) => {
console.log('更新:', data); // 更新:{ name: '我是store1' }
});
await store.setState({
'name': '我是store1'
});
unsubscribe(); // 取消訂閱
// moduleA
const moduleA = store.getModule('moduleA');
moduleA.state.name; // 我是moduleA
const unsubscribeA = moduleA.subscribe((data) => {
console.log('更新:', data); // 更新:{ name: '我是moduleA1' }
});
await moduleA.setState({
'name': '我是moduleA1'
});
moduleA.unsubscribe(cb); // 取消訂閱
複製代碼