最近公司讓我開發一個桌面報警器,以解決瀏覽器頁面關閉無法播放報警聲音的問題。
接到這個項目,自然的選擇了 electron-vue 進行開發(我們公司使用的 vue)
現在有時間了,對項目中遇到的問題進行一個總結。
一、項目搭建 & 打包
項目搭建比較簡單,直接使用 electron-vue 的官方模板就可以生成項目,需要安裝 vue-cli 命令行工具。
npm install -g vue-cli // 需要安裝 vue-cli 腳手架
vue init simulatedgreg/electron-vue project-name // 使用 electron-vue 官方模板生成項目
npm install // 安裝依賴
npm run dev // 啓動項目
項目打包也比較簡單,可能也是因為我的項目本身不復雜吧。普通打包執行 npm run build 即可,如果要打包成免安裝文件,執行 npm run build:dir,非常方便!
npm run build // 打包成可執行文件
npm run build:dir // 打包成免安裝文件
二、狀態管理
因為 electron 每個網頁都在自己的渲染進程(renderer process)中運行,所以如果要在多個渲染進程間共享狀態,就不能直接使用 vuex 了。
vuex-electron 這個開源庫為我們提供了,在多個進程間共享狀態的方案(包括主進程)。
如果需要在多個進程間共享狀態,需要使用 createSharedMutations 中間件。
// store.js 文件
import Vue from "vue"
import Vuex from "vuex"
import { createPersistedState, createSharedMutations } from "vuex-electron"
Vue.use(Vuex)
export default new Vuex.Store({
// ...
plugins: [
createPersistedState(),
createSharedMutations() // 用於多個進程共享狀態,包括主進程
],
// ...
})
並在主進程中引入 store 文件。這裏有點坑,最開始的時候我不知道要在 main.js 中引入 store 文件,結果狀態一直無法更新,又沒有任何報錯,調試了一下午😓
// main.js 文件
import './path/to/your/store' // 需要在主進程引入 store ,否則狀態無法更新
另外,使用 createSharedMutations 中間件,必須使用 dispatch 或 mapActions 更新狀態,不能使用 commit 。
閲讀 vuex-electron 的源代碼,發現渲染進程對 dispatch 進行了重寫,dispatch 只是通知主進程,而不實際更新 store,主進程收到 action 之後,立即更新自己的 store,主進程 store 更新成功之後,會通知所有的渲染進程,這個時候渲染進程才調用 originalCommit 更新自己的 store。
rendererProcessLogic() {
// Connect renderer to main process
this.connect()
// Save original Vuex methods
this.store.originalCommit = this.store.commit
this.store.originalDispatch = this.store.dispatch
// Don't use commit in renderer outside of actions
this.store.commit = () => {
throw new Error(`[Vuex Electron] Please, don't use direct commit's, use dispatch instead of this.`)
}
// Forward dispatch to main process
this.store.dispatch = (type, payload) => {
// 只是通知主進程,沒有更新 store
this.notifyMain({ type, payload })
}
// Subscribe on changes from main process and apply them
this.onNotifyRenderers((event, { type, payload }) => {
// 渲染進程真正更新自己的 store
this.store.originalCommit(type, payload)
})
}
// ... 省略其他代碼
mainProcessLogic() {
const connections = {}
// Save new connection
this.onConnect((event) => {
const win = event.sender
const winId = win.id
connections[winId] = win
// Remove connection when window is closed
win.on("destroyed", () => {
delete connections[winId]
})
})
// Subscribe on changes from renderer processes
this.onNotifyMain((event, { type, payload }) => {
// 主進程更新了自己的 store
this.store.dispatch(type, payload)
})
// Subscribe on changes from Vuex store
this.store.subscribe((mutation) => {
const { type, payload } = mutation
// 主進程更新成功之後,通知所有渲染進程
this.notifyRenderers(connections, { type, payload })
})
}
注意,渲染進程真正更新 store 用的 originalCommit 方法,而不是 originalDispatch 方法,其實 originalDispatch 只是個代理,每一個 mutations 都需要寫一個同名的 actions 方法,接收相同的參數,如下面的官方樣例:
import Vue from "vue"
import Vuex from "vuex"
import { createPersistedState, createSharedMutations } from "vuex-electron"
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
},
actions: {
increment(store) {
// 按照推理,這裏的 commit 其實不起作用,不是必須
// 關鍵是名稱相同
store.commit("increment")
},
decrement(store) {
store.commit("decrement")
}
},
mutations: {
increment(state) {
state.count++
},
decrement(state) {
state.count--
}
},
plugins: [createPersistedState(), createSharedMutations()],
strict: process.env.NODE_ENV !== "production"
})
事實上,如果應用很簡單,比如我的項目只有一個窗口,就不存在共享狀態的問題,所以完全可以不用 createSharedMutations 中間件,也不用在 main.js 中引入 store 文件,store 所有用法就跟 vuex 一樣了。
三、日誌
日誌我採用的是 electron-log,也可以用 log4js
在主進程中使用 electron-log 很簡單,直接引入,調用 info 等方法即可。
electron-log 提供了 error, warn, info, verbose, debug, silly 六種級別的日誌,默認都是開啓。
import log from 'electron-log';
log.info('client 啓動成功');
log.error('主進程出錯');
在渲染進程使用 electron-log,可以覆蓋 console.log 等方法,這樣就不用到處引入 electron-log 了,需要寫日誌的地方直接使用 console.log 等方法即可。
import log from 'electron-log';
// 覆蓋 console 的 log、error、debug 三個方法
console.log = log.log;
Object.assign(console, {
error: log.error,
debug: log.debug,
});
// 之後,就可以直接使用 console 收集日誌
console.error('渲染進程出錯')
electron-log 默認會打印到 console 控制枱,並寫入到本地文件,本地文件路徑如下:
- on Linux: ~/.config/{app name}/logs/{process type}.log
- on macOS: ~/Library/Logs/{app name}/{process type}.log
- on Windows: %USERPROFILE%\AppData\Roaming{app name}\logs{process type}.log
如果使用 log4js 的話,配置相對複雜一點,需要注意的是文件不能直接寫到當前目錄,而是要使用 app.getPath('logs') 獲取應用程序日誌文件夾路徑,否則打包之後無法生成日誌文件。例如:
import log4js from 'log4js'
// 注意:這裏必須使用 app.getPath('logs') 獲取日誌文件夾路徑
log4js.configure({
appenders: { cheese: { type: 'file', filename: app.getPath('logs') + '/cheese.log' } },
categories: { default: { appenders: ['cheese'], level: 'error' } }
})
const logger = log4js.getLogger('cheese')
logger.trace('Entering cheese testing')
logger.debug('Got cheese.')
logger.info('Cheese is Comté.')
logger.warn('Cheese is quite smelly.')
logger.error('Cheese is too ripe!')
logger.fatal('Cheese was breeding ground for listeria.')
四、其他問題
1.修改系統托盤圖標,下面代碼參考了:https://juejin.im/post/6844903872905871373
let tray;
function createTray() {
const iconUrl = path.join(__static, '/app-icon.png');
const appIcon = nativeImage.createFromPath(iconUrl);
tray = new Tray(appIcon);
const contextMenu = Menu.buildFromTemplate([
{
label: '顯示主界面',
click: () => {
if (mainWindow) {
mainWindow.show();
}
},
},
{ label: '退出程序', role: 'quit' },
]);
const appName = app.getName();
tray.setToolTip(appName);
tray.setContextMenu(contextMenu);
let timer;
let count = 0;
ipcMain.on('newMessage', () => {
// 圖標閃爍
timer = setInterval(() => {
count += 1;
if (count % 2 === 0) {
tray.setImage(appIcon);
} else {
// 創建一個空的 nativeImage 實例
tray.setImage(nativeImage.createEmpty());
}
}, 500);
tray.setToolTip('您有一條新消息');
});
tray.on('click', () => {
if (mainWindow) {
mainWindow.show();
if (timer) {
clearInterval(timer);
tray.setImage(appIcon);
tray.setToolTip(appName);
timer = undefined;
count = 0;
}
}
});
}
2.播放聲音
audio = new Audio('static/alarm.wav');
audio.play(); // 開始播放
audio.pause(); // 暫停
3.顯示通知消息
const notify = new Notification('標題', {
tag: '唯一標識', // 相同 tag 只會顯示一個通知
body: '描述信息',
icon: '圖標地址',
requireInteraction: true, // 要求用户有交互才關閉(實測無效)
data, // 其他數據
});
// 通知消息被點擊事件
notify.onclick = () => {
console.log(notify.data)
};
4.隱藏頂部菜單欄
import { Menu } from 'electron'
// 隱藏頂部菜單
Menu.setApplicationMenu(null);
五、參考資料
- electron 官方文檔:https://www.electronjs.org/docs
- electron-vue 文檔:https://simulatedgreg.gitbooks.io/electron-vue/content/cn/
- electron系統托盤及消息閃動提示:https://juejin.im/post/6844903872905871373