Omkar's face

Omkar Konaraddi

Building a Desktop App with Electron and Vue

Published on 2018-06-10.

Updated in May 2020.

In this post, we'll provide a brief overview on how to build desktop apps with Vue using Electron.

Electron enables developers to use web technologies to build cross platform desktop apps. It accomplishes this by "combining Chromium and Node into a single runtime and apps can be packaged for Mac, Windows, and Linux". This enables us to use Vue for building the UI of a desktop app while having access to Node and Electron's APIs within our apps.

We'll build a to-do list that saves our todos to a file on the users computer and sends a notification when all todos are completed. We'll explain things as we go.

This tutorial assumes the reader is familiar with Vue, has Node installed, and is familiar with using a terminal (also known as a command line).

Getting Started

First, let's set up a simple Electron app using Electron's quick start guide that we've copied below. Run the following commands in your terminal.

# Clone this repository
git clone https://github.com/electron/electron-quick-start

# Go into the repository
cd electron-quick-start

# Install dependencies
npm install

# Run the app
npm start

You should be greeted with the following.

Hello World Electron app

To stop running the app, enter ctrl + C in your terminal. You can use npm start to run and ctrl + C to stop our app as we develop it. Every time we make a change to our app, we'll need to rerun the app to see our changes. This is a bit inconvenient so we'll set up auto reloading using an npm package called electron-reloader. Run the following in your terminal.

$ npm install --save-dev electron-reloader

Then let's load the module at the top of main.js. Replace the contents of the main.js with the following:

try {
  require("electron-reloader")(module);
} catch (_) {}

const { app, BrowserWindow } = require("electron");

function createWindow() {
  const mainWindow = new BrowserWindow({
    width: 300,
    height: 400,
    webPreferences: {
      nodeIntegration: true,
    },
  });

  mainWindow.loadFile("index.html");
}

app.whenReady().then(createWindow);

app.on("window-all-closed", function () {
  if (process.platform !== "darwin") app.quit();
});

app.on("activate", function () {
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

Now every time you make a change, the app will reload. Make sure this works for you by editing the index.html and checking whether the change appears. You may neeed to restart the app for hot reloading to take effect.

Next, delete preload.js and replace the contents of index.html with the following:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>todo-app</title>
  </head>
  <body>
    <h1>Hello World!</h1>
    <script src="./renderer.js"></script>
  </body>
</html>

Let's talk about our main.js and renderer.js files for a moment.

Electron apps run two types of processes: main and renderer. The renderer processes are responsible for rendering the webpages (the user interface) and the main process is responsible for everything else such as managing the renderer processes, setting the window size, and setting the window's menu options. In our case, the main process will execute main.js which will set up the app's window and webpage. The app's window's webpage, our index.html and the renderer.js file it includes, runs in a renderer process.

Node and Electron APIs are accessible from the renderer processes. Imagine a browser with access to operating system APIs and you've got an Electron application.

💡 You can read more about the main and renderer processes at Electron's documentation.

Next, we'll make a todo list.

Base Example

Let's start off with a basic todo list in Vue. We'll use this CodeSandbox as the basis for our todo list. Replace the contents of index.html with the following.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>todo-app</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
  </head>
  <body>
    <div id="app">
      <h1>Todo list</h1>
      <form @submit.prevent="addTodo">
        <input type="text" v-model="newTodo" placeholder="Enter a todo" />
        <button type="submit">ADD todo</button>
      </form>
      <br />
      <div v-if="list.length > 0">
        <div v-for="(todo,i) in list" :key="todo">
          <label style="cursor: pointer;">
            <input type="checkbox" v-model="todo.done" />
            
          </label>
          <button type="button" class="btn_del" @click="deleteTodo(i)">
            &times;
          </button>
        </div>
      </div>
      <div v-else>
        <p>Add a todo!</p>
      </div>
    </div>
    <script src="./renderer.js"></script>
  </body>
  <style>
    #app {
      margin: 32px;
    }
  </style>
</html>

Then replace renderer.js with the following.

let app = new Vue({
  el: "#app",
  data: {
    newTodo: "",
    list: [
      {
        name: "todo 1",
        done: true,
      },
      {
        name: "todo 2",
        done: false,
      },
    ],
  },
  methods: {
    deleteTodo: function(index) {
      this.list.splice(index,1)
    },
    addTodo: function () {
      if (this.newTodo) {
        this.list.push({
          name: this.newTodo,
          done: false,
        });
        this.newTodo = "";
      }
    },
  },
});

Now our app looks like the following.

Minimal todo

You can add todos, remove todos, and mark todos as done.

We've built a desktop app for a todo list with Electron and Vue. But it doesn't use desktop-only features like reading/writing to files or sending desktop notifications. We also haven't made an executable for our desktop app; it only runs when we run npm start from a terminal. In the next section we'll incorporate desktop-only features. Later, we'll build an executable for our app.

Real World Example

In this section, we'll use APIs from Node and Electron to implement features we would expect to see in a desktop app like notifications and reading/writing to files.

Currently, our app doesn't save our todos so our app's data resets every time we restart the app. It'd be more useful if our app remembered the todos and their current states.

Saving todos

We can save todos to a file when we quit the app and then, the next time we open the app, we can read the todos from the same file.

To read and write to files, we'll use Node's os and fs modules. Let's import those modules in renderer.js . We'll also add some print statements so we can determine whether they loaded. Add the first four lines to renderer.js.

const os = require("os")
const fs = require("fs")
console.log(os)
console.log(fs)

let app = new Vue({
// [...]

Where do we see the console.log statements? Electron uses Chromium to render the UI so we have access to Chromium's devtools. To open the devtools, put the todo app in focus and press Command+Option+I (Mac) or Control+Shift+I. You should see something similar to the below.

Viewing devtools in our app

If you see the Object and Object in the console (like in the picture above) then you've successfully loaded the os and fs modules.

We'll save the user's todos in a file called todo.txt and we'll create the file in the user's home directory. The user's home directory path is different for Linux, MacOS, and Windows so we'll use the homedir() from Node's os module to get the home directory regardless of the user's operating system.

On Windows, the home directory is usually C:\Users\your_username. On MacOS, the home directory is usually /Users/your_username. On Linux, the home directory is usually ~ or /home/your_username.

Let's save our todos whenever the user quits the app. We can save todos to a file by using the writeFile function from Node's fs module. Add the following lines to renderer.js.

const os = require("os");
const fs = require("fs");

let app = new Vue({
  // [...]
});

// Add the following lines

let nodeConsole = require('console').Console(process.stdout, process.stderr);

window.onbeforeunload = function () {
  const path_to_todos = `${os.homedir}/todo.txt`;
  const data = JSON.stringify(app.$data);

  fs.writeFile(path_to_todos, data, (err) => {
    if (err) {
      return nodeConsole.log("Failed to save default values");
    }
    
    nodeConsole.log(`Saved ${data} to ${path_to_todos}`);
  });
};

We're using nodeConsole to print to our terminal instead of the browser's console because when we quit the app, we won't be able to see devtools anymore. The window's onbeforeunload will save our todos to a file.

Try adding some todos and quitting the app. Use the terminal or your computer's file manager to view the todo.txt file in your home directory.

$ cat ~/todo.txt 
# {"newTodo":"","list":[{"name":"todo 1","done":true},{"name":"todo 2","done":false}]}

Now we need to read from that file to load our todo list when we open/run the app again. We'll do that inside our Vue instance's created lifecycle hook so when the Vue instance is created, we can update our Vue instance's data.

Checkout the Vue Lifecycle Diagram to see when a Vue instance is created.

We can use the existsSync function from Node's fs module to check if a file exists. If it exists, then we'll load the data from the file by using readFileSync. If the file doesn't exist, then the user is probably opening our app for the first time so nothing needs to be done (the todo app's data will be based on the initial values in the Vue instance's data() function).

Add the following created() { [...] } to the app's Vue instance.

// [...]

let app = new Vue({
  // [...]
  // Add the following
  created() {
    const path_to_todos = `${os.homedir}/todo.txt`;
    if (fs.existsSync(path_to_todos)) {
      let data = fs.readFileSync(path_to_todos).toString();
      let dataObject = JSON.parse(data)
      this.newTodo = dataObject.newTodo
      this.list = dataObject.list
      console.log(`Loaded ${data} from ${path_to_todos}`);
    } else {
      console.log("No todo.txt found")
    }
  }
})

// [...]

Now when we add todos, quit the app, and rerun the app, we can see our previously added todos. Next, we'll use an API from Electron to send a notification when all todos have been completed.

Sending desktop notifications

We'll use HTML5 notifications from Electron to send desktop notifications whenever all todos are marked as complete.

HTML5 notifications come from Chromium but Electron tweaks the HTML5 notification API to send desktop notifications instead of in-browser notifications. Learn more about Electron's notifications here.

Here's how we create a HTML5 notification:

new Notification("Title of notification", {
  body: "And a longer subtitle for the notification"
});

When all tasks are marked as complete, we'll send a notification. In index.html, add v-on:change="onCheckboxChange" to the existing input element so we can listen for changes on the checkboxes.

<input type="checkbox" v-model="todo.done" v-on:change="onCheckboxChange"/>

Next, add the following onCheckboxChange function to methods on our app's Vue instance.

// [...]
methods: {
  deleteTodo: // [...]
  addTodo:    // [...]

  // Add the below
  onCheckboxChange: function() {
    let areAllTodosDone = this.list.every(todo => todo.done == true)
    if (areAllTodosDone) {
      new Notification("🎉 Woo hoo!", {
        body: "We're all done!"
      });
    }
  }

},
// [...]

Add some tasks, check all of them, and you should see a notification like the below.

Example notification on MacOS

We've built a todo app that saves our tasks and sends a celebratory notification when we're done with our tasks.

Building the Desktop Executable

So far we've been running our desktop app using npm start which starts a local server and rerenders our application on changes. Let's build an executable so we can share our app with others and actually install it on to our system like we do with desktop apps.

We'll use electron-builder package to build our application into executables for Windows, Mac, and Linux. Run npx electron-builder from the project folder to temporary install and execute electron-builder.

$ npx electron-builder

Note that we're using npx instead of npm (and npx comes with npm as of v5.2.0). It can take a while to build the executables for Windows/MacOS/Linux.

A new folder called dist will be created and inside of electron-quick-start/dist/ folder you should see your platform's, and possibly another platform's, executable. For Windows this is a .exe file and for MacOS it is a .app file. For Linux, the executable's name is the project name with no extension.

Additional Context

We finished building a todo list in Electron and Vue. Writing a cross platform desktop app with Electron and Vue app is similar to writing a regular Vue application but with access to additional APIs from Node and Electron. You can also build Electron + Vue apps using Vue's single file components and the electron-vue package makes it easy to get started.

You can explore Electron's documentation and electron-vue's documentation to build more complex Electron and Vue applications.

Writing a desktop app with Electron and Vue can be convenient for web developers. It enables web developers to leverage their prior knowledge and experience.

When to Avoid

Electron applications tend to have larger build sizes and require more resources when compared to their native counterparts written in their respective operating systems' preferred language(s) and framework(s). This is because Electron bundles it's own version of Chromium, a browser, with your application. It doesn't matter if your system already has Chromium or if it already has Electron apps installed. Each Electron application comes with it's own browser.

If the performance and size of an application is more valuable than the convenience of using web technologies, then avoid building desktop apps with Electron.

Alternatives

There's an alternative to building cross platform desktop apps with Vue: vuido. As of this post, it's not ready for production and it was last updated in early 2019. You can also build cross platform desktop apps, without Vue, using either Qt or Proton Native. Vuido, Qt, and Proton Native offer better performance and smaller app sizes than apps built with Electron.

You can build desktop apps in each operating systems preferred language and framework too. This usually translates to maintaining separate codebases with similar function and form but usually reaps the best performance and minimizes the application's size.