今天給你們帶來一款基於Electron桌面開發平臺的自研應用框架Sugar-Electron,指望能改善Electron應用穩定性和幫助開發團隊下降開發和維護成本。html
筆者使用Electron作桌面應用,已經有3年的時間,期間也遇到不少大大小小的坑。但總結起來,最大的問題仍是應用穩定性和開發效率問題。咱們指望經過這個框架,能讓應用程序在這兩個方面有所優化。webpack
項目源碼地址:
https://github.com/SugarTurboS/Sugar-Electronios
若有任何疑問,能夠掃碼加入微信羣聊討論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); // 取消訂閱