DEV Community

Cover image for Building a Desktop App with Vue: Electron
Natalia Tepluhina
Natalia Tepluhina

Posted on • Edited on

Building a Desktop App with Vue: Electron

In my previous article I described building a desktop application with Vue framework using Vuido. It's a great library to create a fast and small-sized desktop app with native platform components but it has its own downsides like no CSS styling or images support. Now it's time to try Electron to build a desktop app.

Electron is an open source library developed by GitHub for building cross-platform desktop applications with HTML, CSS, and JavaScript. Electron accomplishes this by combining Chromium and Node.js into a single runtime and apps can be packaged for Mac, Windows, and Linux

To have a quick start I've used an electron-vue boilerplate by Greg Holguin. It provides a developer with vue-cli scaffolding, common Vue plugins, packager, testing, devtools, and other features.

💻 What are we going to build

We will build the same application as in the previous article: an app to check the weather in the city of user's choice built on top of OpenWeatherMap API.

If you want just to check the final Electron-powered app code, it's here.

🛠️ Installation

An electron-vue boilerplate is built as a template for VueCLI 2.x and includes options to customize an application. So you need to install it globally:

npm install -g vue-cli
Enter fullscreen mode Exit fullscreen mode

If you prefer to use the latest version of VueCLI, you need to install a global bridge too:

npm install -g @vue/cli @vue/cli-init
Enter fullscreen mode Exit fullscreen mode

and then initialize your project:

vue init simulatedgreg/electron-vue weather-app
Enter fullscreen mode Exit fullscreen mode

This will start an installation project with a couple of choices you need to make during it.

Installation

The cool thing is if you need some commonly used plugins and libraries such as axios, you can pick them up during the installation process.

Almost all choices were clear but there was one concern 🤔:

Electron packager choice

I decided to google it and found this useful thread on StackOverflow. According to it, electron-builder seemed to be a better choice for me, so I went with it.

After the project is set up, you need to go to the application folder and run npm install or yarn install and we are ready to go.

🔦 Understanding an application structure

Right after installation is finished you can see two folders inside the src one: main and renderer. First one is required for Electon main process

According to electron-vue docs, in Electron the process that runs package.json’s main script is called the main process. The script that runs in the main process can display a GUI by creating web pages.

There are two files in the main folder: index.js and index.dev.js. First one is your application's main file, the file in which electron boots with. It is also used as webpack's entry file for production. All main process work should start here.

index.dev.jsis used specifically and only for development as it installs electron-debug & vue-devtools. You don't need to touch it while developing an application.

renderer folder is required for renderer process.

Since Electron uses Chromium for displaying web pages, Chromium’s multi-process architecture is also used. Each web page in Electron runs in its own process, which is called the renderer process.

As you may notice, it's a 'normal' Vue application structure with assets and components folders, main.js and App.vue files. Here is the structure of the latter:

<template>
  <div id="app">
    <landing-page></landing-page>
  </div>
</template>

<script>
  import LandingPage from '@/components/LandingPage'

  export default {
    name: 'weather-app',
    components: {
      LandingPage
    }
  }
</script>

<style>
  /* CSS */
</style>
Enter fullscreen mode Exit fullscreen mode

And if you try to run dev task you will have this result:

Start app template

So there is a landing-page component and also devtools opened. Now we can start to change it!

🕹️ Scaffolding an app

Unlike Vuido, an Electron-powered app is built with HTML-tags, not native components. So we will make a structure similar to the usual web app and style it with CSS.

NOTE: I didn't install any CSS framework or component library on purpose: I wanted to compare package size without adding any different dependencies. The only library used in both projects is axios.

The first step was to get rid of landing-page component. Then I added a simple input field and a button:

<div id="app">
    <p>Enter the city name to check current weather in it</p>
    <section class="weather-input">
      <input type="text" v-model="query">
      <button :disabled="!query.length">Check</button>
    </section>
</div>
Enter fullscreen mode Exit fullscreen mode

Now our application looks this way:

App with input only

We have a query property in data to handle the user input and we will make an API call with this query as a parameter.

🔗 Making an API call

I used the OpenWeatherMap current weather API. It gives you a lot of different information, you can check an example of JSON response here.

We already included axios to our application during the installation process. Let's have a look at src/renderer/main.js:

import Vue from 'vue';
import axios from 'axios';
import App from './App';

if (!process.env.IS_WEB) Vue.use(require('vue-electron'));
Vue.http = Vue.prototype.$http = axios;
Vue.config.productionTip = false;
Enter fullscreen mode Exit fullscreen mode

So we can use axios methods as this.$http in the component instance! The only thing we will add here is a base URL for our API calls:

axios.defaults.baseURL = 'http://api.openweathermap.org/data/2.5';
Enter fullscreen mode Exit fullscreen mode

Now in the App.vue we will create a bunch of data properties to display different weather data:

data() {
    return {
      query: '',
      error: false,
      city: '',
      country: '',
      weatherDescription: '',
      temp: null,
      tempMin: null,
      tempMax: null,
      humidity: null,
      icon: '',
    };
},
Enter fullscreen mode Exit fullscreen mode

I've added one additional property comparing to Vuido version and it's an icon. API provides a weather icon but we couldn't use it in Vuido app because currently there is no support for displaying images.

Let's also create a method to fetch our data:

methods: {
  showWeather() {
    this.$http
      .get(`/weather?q=${this.query}&units=metric&&appid=${API_KEY}`)
      .then(response => {
        this.city = response.data.name;
        this.country = response.data.sys.country;
        this.weatherDescription = response.data.weather[0].description;
        this.temp = response.data.main.temp;
        this.tempMin = response.data.main.temp_min;
        this.tempMax = response.data.main.temp_max;
        this.humidity = response.data.main.humidity;
        this.icon = `http://openweathermap.org/img/w/${
          response.data.weather[0].icon
        }.png`;
        this.error = false;
      })
      .catch(() => {
        this.error = true;
        this.city = '';
      });
  },
},
Enter fullscreen mode Exit fullscreen mode

and add it to the click callback of our button:

<button :disabled="!query.length" @click="showWeather">Check</button>
Enter fullscreen mode Exit fullscreen mode

Now if you enter the text into an input field and click the button, you can observe the API call in the Network tab:

API call response

💅 Displaying weather data

Let's add this data to the template:

<template>
  <main id="app">
    <p>Enter the city name to check current weather in it</p>
    <section class="weather-input">
      <input type="text" v-model="query">
      <button :disabled="!query.length" @click="showWeather">Check</button>
    </section>
    <section v-if="error" class="weather-error">
      There is no such city in the database
    </section>
    <section v-if="city.length" class="weather-result">
      <h1>{{city}}, {{country}}</h1>
      <p><em>{{weatherDescription}}</em></p>
      <div class="weather-result__main">
        <img :src="icon" alt="Weather icon">
        <div class="weather-result__temp">
          {{temp}}&deg;C
        </div>
      </div>
      <div class="weather-result__details">
        <p>Min: {{tempMin}}&deg;C</p>
        <p>Max: {{tempMax}}&deg;C</p>
        <p>Humidity: {{humidity}}%</p>
      </div>
    </section>
  </main>
</template>
Enter fullscreen mode Exit fullscreen mode

Our application view:

App without styling

Awesome, we can see an actual weather! But it looks like it's 1999... Let's add some CSS magic to it (actually, a lot of CSS magic)!

<style lang="scss">
* {
  margin: 0;
  padding: 0;
}
html,
body,
#app {
  height: 100%;
}

#app {
  font-family: Arial, Helvetica, sans-serif;
  font-size: 16px;
  padding: 10px;
  background: rgb(212, 228, 239);
  background: -moz-radial-gradient(
    center,
    ellipse cover,
    rgba(212, 228, 239, 1) 0%,
    rgba(134, 174, 204, 1) 100%
  );
  background: -webkit-radial-gradient(
    center,
    ellipse cover,
    rgba(212, 228, 239, 1) 0%,
    rgba(134, 174, 204, 1) 100%
  );
  background: radial-gradient(
    ellipse at center,
    rgba(212, 228, 239, 1) 0%,
    rgba(134, 174, 204, 1) 100%
  );
  filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#d4e4ef', endColorstr='#86aecc',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */
}

.weather-input {
  display: flex;
  align-items: center;
  padding: 20px 0;
}

.weather-result {
  text-align: center;
  &__main {
    display: flex;
    align-items: center;
    justify-content: center;
    padding-top: 5px;
    font-size: 1.3rem;
    font-weight: bold;
  }
  &__details {
    display: flex;
    align-items: center;
    justify-content: space-around;
    color: dimgray;
  }
}

.weather-error {
  color: red;
  font-weight: bold;
}

input {
  width: 75%;
  outline: none;
  height: 20px;
  font-size: 0.8rem;
}

button {
  display: block;
  width: 25%;
  height: 25px;
  outline: none;
  border-radius: 5px;
  white-space: nowrap;
  margin: 0 10px;
  font-size: 0.8rem;
}
</style>
Enter fullscreen mode Exit fullscreen mode

And finally we have nice fully-functional app:

App ready

The last thing to do before packaging it is to reduce a window size. If we check a src/main/index.js file, we can find settings for it:

mainWindow = new BrowserWindow({
    height: 563,
    useContentSize: true,
    width: 1000
})
Enter fullscreen mode Exit fullscreen mode

Let's change width to be 450 and height to be 250

📦 Packaging

Great news: you can build your app as a web application! If you run a build:web task, you will find a build web app in a dist folder.

But let's go back to our desktop app and run build task. As a result, you will have a folder named after your platform inside the build folder (for me it's mac) and an application file inside of it. And its size it... wow, 133 Mb!

App size

It's a lot for such a little application! Also, if you try to run it, you can notice it's starting a bit slower than a Vuido-powered app.

Final look:

Final look

🌟 Conclusions

Pros:

  • easy to start
  • good docs
  • provides web app build
  • can be customized via CSS styling

Cons

  • really big package size
  • slower than an application built with native GUI components

Electron-vue is a good option if your application needs a unique appearance and you care less about package size and performance.

Update

If your web application is built with Vue CLI 3, you can simply make it a desktop app with Vue CLI Plugin Electron Builder. You just need to run the following command in your project root folder:

vue add electron-builder
Enter fullscreen mode Exit fullscreen mode

After it's done you will have two additional npm tasks: serve:electron and build:electron to work with a desktop app.

Top comments (30)

Collapse
 
ben profile image
Ben Halpern

I feel like Vue + Electron may become a really popular approach for developer experience. Hard to beat this productivity.

I hope Electron has a plan for being kind to WebAssembly to improve performance and resource management in the future.

Collapse
 
n_tepluhina profile image
Natalia Tepluhina

I agree. Development experience is great, but the resulting application is resource-expensive indeed.

Collapse
 
eryouhao profile image
Eryou Hao

So cool~, I recently developed a static blog writing client based on Vue & Electron, welcome to use! github.com/hve-notes/hve-notes

Collapse
 
n_tepluhina profile image
Natalia Tepluhina

Wow, it looks great! Thank you for sharing!

Collapse
 
dbelyaeff profile image
Dmitriy Belyaev

Thanks, Natasha! Very useful article.

Have you tried UPX to compress final binary?

upx.github.io

It can compress final binary up to 2x.

The main disadvantage of Electron is output file size.

Collapse
 
n_tepluhina profile image
Natalia Tepluhina

Thank you! Will look into it

Collapse
 
dbelyaeff profile image
Dmitriy Belyaev

You're welcome.

 
ben profile image
Ben Halpern

Thanks, means a lot!

Collapse
 
itsnwa profile image
Nichlas Wærnes Andersen

Very cool, I built a side-project with this exact setup + Firebase Auth, Storage and Firestore, such good dev experience and love love loved the flexibility of styling!

getbrandy.io

Collapse
 
cynthiablue profile image
cynthiablue

I am super new to Electron and Vue. I am trying to step through this tutorial but I cannot get it to run.

You say: "And if you try to run dev task you will have this result:"
But I don't know what that means? I have tried 'npm dev' and that doesn't work. looking through the code that was created the package.json file says:
"main": "./dist/electron/main.js",

but under the dist/electron directory, there is no main.js file.
I would love clarification, thanks.

Collapse
 
superdiana profile image
Super Diana

Thank you!! This is awesome

Collapse
 
n_tepluhina profile image
Natalia Tepluhina

Thank you for reading it ;)

Collapse
 
davefollett profile image
Dave Follett

Have you (or anyone) tried this Vue CLI v3 plugin for electron? I haven't had a chance to yet:

Greg is suppose to be doing a re-write for Vue CLI v3 but I haven't seen any updates from him lately.

Collapse
 
n_tepluhina profile image
Natalia Tepluhina

No, but thanks for the idea! I'm going to check it :)

Collapse
 
sh3fas profile image
sh3fas

Good morning, I'm getting an error when editing App.vue file. Error is:

ERROR in ./src/renderer/App.vue?vue&type=script&lang=js& (./node_modules/babel-loader/lib!./node_modules/vue-loader/lib??vue-loader-options!./src/renderer/App.vue?vue&type=script&lang=js&)
Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError: C:/Users/Kiseliovai/Desktop/weather-app/src/renderer/App.vue: Unexpected token, expected ; (17:9)

15 | }
16 |

17 | data() {
| ^
18 | return {
19 | query: '',
20 | error: false,

Any suggestions?

Collapse
 
n_tepluhina profile image
Natalia Tepluhina

Could you please post the whole script block here? It seems an error is before the data

Collapse
 
bromoapp profile image
B Kunto Adji • Edited

Hi Natalia,

I'm pretty new to Vue js but not new to programming. I have been using Vue tutorials I found here and youtube to nicely study Vue.

Currently, I have an incoming project to build an electron app using Vue js. And by using a plugin from nklayman.github.io/vue-cli-plugin-... most of my Vue apps produced from learning the above tutorials can be nicely turned into Electron app with simple commands.

This coming electron project requires me to implement multiple windows layout and docking mechanism, and after I did some research, I came to golden-layout.com/ library... The integration of this lib for Vue also available at npmjs.com/package/vue-golden-layout

Maybe because I'm very new to Vue, but the description on how to use Vue-golden-layout lib in both NPM site and it's Github repo really confused me. I don't mean to be lazy here. But since I feel that I'm still a newbie in Vue while my Boss is on my tail ... is it possible that you make a very basic article on how to use Golden Layout with Vue?

I will be very very grateful if you can spend a little bit of your time to make one. Since desktop app without multiple windows layout and docking would not 100% complete :D

Or if any Vue experts here already have done such thing, it would be great for me to jump start with your samples...

Regards,
Bromo

Collapse
 
n_tepluhina profile image
Natalia Tepluhina

Thank you for this comment! I will look into it :)