I was recently asked to develop a desktop alarm to solve the problem of not playing the alarm sound when the browser page is closed.

Upon receiving this project, we naturally chose electron vUE for development (the VUE used by our company).

Now that I have time, make a summary of the problems encountered in the project.

I. Project construction & packaging

The project construction is relatively simple. You can directly generate the project using the official electron vue template. You need to install the vuE-CLI command line tool.

NPM install -g vue-cli // Need to install vue-CLI scaffolding vue init simulatedGreg /electron-vue project-name // Use the electron-vue official template to generate the project NPM install // Install depends on NPM run dev // to start the projectCopy the code

It was also easy to package the project, probably because my project itself was not complicated. NPM run build:dir, NPM run build:dir, NPM run build:dir

NPM run build // Packaged as an executable file NPM run build:dir // packaged as an installation-free fileCopy the code

Second, state management

Since electron runs each web page in its own renderer process, vuex cannot be used directly if you want to share state between multiple renderers.

The vuex-electron open source library provides a solution for sharing state across multiple processes (including the main process).

If you need to share state across multiple processes, use the createSharedMutations middleware.

/ / store. Js file
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() // Used for multiple processes to share state, including the main process].// ...
})
Copy the code

The store file is introduced in the main process. At the beginning, I didn’t know to introduce store file in main.js. As a result, the status couldn’t be updated and there was no error. I spent the whole afternoon debugging 😓

/ / the main js file
import './path/to/your/store' // The state cannot be updated unless store is introduced in the main process
Copy the code

Additionally, with the createSharedMutations middleware, you must use Dispatch or mapActions to update the status, not COMMIT.

Reading the vuex-electron source code, it is found that the renderer process rewrites the Dispatch. Dispatch only informs the main process without actually updating the store. The main process updates its store immediately after receiving the action. When the main store is successfully updated, all renderers are notified, at which point the renderer calls originalCommit to update its 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) = > {
        // Only notifies the main process without updating the store
        this.notifyMain({ type, payload })
    }

    // Subscribe on changes from main process and apply them
    this.onNotifyRenderers((event, { type, payload }) = > {
        // The renderer actually updates its store
        this.store.originalCommit(type, payload)
    })
}

/ /... Omit other code

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 }) = > {
        // The main process updates its store
        this.store.dispatch(type, payload)
    })

    // Subscribe on changes from Vuex store
    this.store.subscribe((mutation) = > {
        const { type, payload } = mutation

        // Notify all renderers of the success of the main process update
        this.notifyRenderers(connections, { type, payload })
    })
}
Copy the code

Note that the renderer actually updates the Store with the originalCommit method, not the originalDispatch method, which is just a proxy. Every mutations needs to write an actions method of the same name that accepts the same parameters, as shown in the official example below:

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 is not required
      // The key is the same name
      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"
})
Copy the code

In fact, if the application is very simple, for example, my project has only one window, there is no problem of shared state. Therefore, it is completely unnecessary to createSharedMutations middleware and introduce store files into main.js, and all applications of Store will be the same as vuex.

Three, logs,

I’m using the electron log, or log 4JS

Using the electron log in the main process is easy, just import it, call info, etc.

import log from 'electron-log';
 
log.info('Client started successfully');
Copy the code

Using electron log in the rendering process can override methods like console.log so that you don’t have to introduce electron log everywhere and use methods like console.log directly where you need to write logs.

import log from 'electron-log';
 
 // Override the log, error, and debug methods of console
console.log = log.log;
Object.assign(console, {
  error: log.error,
  debug: log.debug,
});

// After that, you can directly use console to collect logs
console.error('Client error')
Copy the code

By default, the electron log is printed to the console console and written to a local file. The local file path is as follows:

  • 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

If log4js is used, the configuration is a bit more complicated. Note that files cannot be written directly to the current directory. Instead, app.getPath(‘logs’) is used to obtain the path to the application’s log folder. Such as:

import log4js from 'log4js'
 
// Note: app.getPath('logs') must be used to obtain the log folder path
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 Comte.)
logger.warn('Cheese is quite smelly.')
logger.error('Cheese is too ripe! ')
logger.fatal('Cheese was breeding ground for listeria.')
Copy the code

Other issues

1. Modify the system tray icon. Please refer to the following code: juejin.cn/post/684490…

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: 'Show home screen'.click: () = > {
        if(mainWindow) { mainWindow.show(); }}}, {label: 'Exit program'.role: 'quit'},]);const appName = app.getName();
  tray.setToolTip(appName);
  tray.setContextMenu(contextMenu);
 
  let timer;
  let count = 0;
  ipcMain.on('newMessage'.() = > {
    // The icon blinks
    timer = setInterval(() = > {
      count += 1;
      if (count % 2= = =0) {
        tray.setImage(appIcon);
      } else {
        // Create an empty nativeImage instancetray.setImage(nativeImage.createEmpty()); }},500);
      tray.setToolTip('You have a new message');
  });
 
  tray.on('click'.() = > {
    if (mainWindow) {
      mainWindow.show();
      if (timer) {
        clearInterval(timer);
        tray.setImage(appIcon);
        tray.setToolTip(appName);
        timer = undefined;
        count = 0; }}}); }Copy the code

2. Play the sound

audio = new Audio('static/alarm.wav');
audio.play(); // Start playing
audio.pause(); / / pause
Copy the code

3. Displays notification messages

const notify = new Notification('title', {
   tag: 'Unique identifier'.// Only one notification is displayed for the same tag
   body: 'Description'.icon: 'Icon address'.requireInteraction: true.// The user is not allowed to interact with the user.
   data, // Other data
});
 
// Notify the event that the message was clicked
notify.onclick = () = > {
   console.log(notify.data)
};
Copy the code

4. Hide the top menu bar

import { Menu } from 'electron'
 
// Hide the top menu
 Menu.setApplicationMenu(null);
Copy the code

5. Reference materials

  • Electron official document: www.electronjs.org/docs
  • Electron – vue document: simulatedgreg. Gitbooks. IO/electron – vu…
  • The electron system tray and message flashing prompt: juejin.cn/post/684490…

(after)