Detailed explanation of desktop application using Vue3 and Electron

Detailed explanation of desktop application using Vue3 and Electron

In order to record some personal essays, I recently built a blog system with Laravel and Vue 3.0 , which used a markdown editor Vue component v-md-editor based on markdown-it. I think it is very convenient to use it to write markdown . Later, I had an idea to use Electron to implement a markdown desktop application based on this component, which is also a good choice for my daily use.

Off topic: VS Code is a desktop application developed with Electron . Except for mobile development, I now use VS Code for all other developments. It is really convenient to develop various plug-ins.

Next, I will take you step by step to implement this function.

Vue CLI builds a Vue project

Execute vue create electron-vue3-mark-down in the selected directory

Select a custom template (you can choose the default Vue 3 template)

Select Vue3 and TypeScript , and choose other options based on your own project

vue3 + TypeScript

Execute npm run serve to see the effect

Effect

Vue project transformed into markdown editor

Run npm i @kangc/v-md-editor@next -S to install v-md-editor

Add TypeScript type definition file

Since the v-md-editor library does not have a TypeScript type definition file, I added it directly after the shims-vue.d.ts file. Of course, you can also create a new file to add the declaration (as long as tsconfig.json can find this file).

declare module "*.vue" {
  import type { DefineComponent } from "vue";
  const component: DefineComponent<{}, {}, any>;
  export default component;
}

<!-- Added content -->
declare module "@kangc/v-md-editor/lib/theme/vuepress.js";
declare module "@kangc/v-md-editor/lib/plugins/copy-code/index";
declare module "@kangc/v-md-editor/lib/plugins/line-number/index";
declare module "@kangc/v-md-editor";
declare module "prismjs";

Transform App.vue

<template>
  <div>
    <v-md-editor v-model="content" height="100vh"></v-md-editor>
  </div>
</template>

<script lang="ts">
// Editor import VMdEditor from "@kangc/v-md-editor";
import "@kangc/v-md-editor/lib/style/base-editor.css";
import vuepress from "@kangc/v-md-editor/lib/theme/vuepress.js";
import "@kangc/v-md-editor/lib/theme/style/vuepress.css";
// Highlight import Prism from "prismjs";
import "prismjs/components/prism-json";
import "prismjs/components/prism-dart";
import "prismjs/components/prism-c";
import "prismjs/components/prism-swift";
import "prismjs/components/prism-kotlin";
import "prismjs/components/prism-java";

// Quickly copy code import createCopyCodePlugin from "@kangc/v-md-editor/lib/plugins/copy-code/index";
import "@kangc/v-md-editor/lib/plugins/copy-code/copy-code.css";
// Line number import createLineNumbertPlugin from "@kangc/v-md-editor/lib/plugins/line-number/index";
VMdEditor.use(vuepress, {
  Prism,
})
  .use(createCopyCodePlugin())
  .use(createLineNumbertPlugin());

import { defineComponent, ref } from "vue";
export default defineComponent({
  name: "App",
  components: { VMdEditor },
  setup() {
    const content = ref("");

    return { content };
  },
});
</script>

<style>
/* Remove some buttons */
.v-md-icon-save,
.v-md-icon-fullscreen {
  display: none;
}
</style>

This file is also very simple. The whole page is an editor <v-md-editor v-model="content" height="100vh"></v-md-editor> . This markdown editor has plug-ins such as highlighting, code line number display, and copy code button. Of course, it is more convenient to add other plug-ins to enrich the functions of this markdown editor .

The effect is as follows

Editor Effects

Vue CLI Plugin Electron Builder

I tried to use Vite 2.0 to build an Electron project, but I didn’t find a similar tool that combines Vite and Electron well, so I gave up the temptation of Vite 2.0 . If anyone has any recommendations, please share them.

Use vue add electron-builder to install. I chose the latest version of Electron , 13.0.0 .

I usually choose the highest version. In fact, this version has a pitfall. I will think about introducing this pitfall later, haha.

Effect

We can see that many new dependent libraries have been added, and a background.ts file has been added. To briefly introduce, this file is executed in the main thread, and other pages are in the rendering thread. The rendering thread has many limitations, and some functions can only be executed in the main thread, which will not be elaborated here.

Execute npm run electron:serve to see the effect

Effect

At this point, you can see the effect of the desktop application, and while modifying the Vue code, the desktop application can also see the modified effect in real time.

Optimization

Start full screen display

Introducing screen

import { screen } from "electron";

Set to screen size when creating a window

<!-- background.ts -->
async function createWindow() {
  const { width, height } = screen.getPrimaryDisplay().workAreaSize;
  const win = new BrowserWindow({
    width,
    height,
    // Omitted...
  });
    // Omitted...
}

This way the application will be displayed in full screen when it starts.

Modify the menu bar

Define the menu bar

<!-- background.ts -->

const template: Array<MenuItemConstructorOptions> = [
  {
    label: "MarkDown",
    submenu: [
      {
        label: "About",
        accelerator: "CmdOrCtrl+W",
        role: "about",
      },
      {
        label: "Exit program",
        accelerator: "CmdOrCtrl+Q",
        role: "quit",
      },
    ],
  },
  {
    label: "file",
    submenu: [
      {
        label: "Open file",
        accelerator: "CmdOrCtrl+O",
        click: (
          item: MenuItem,
          focusedWindow: BrowserWindow | undefined,
          _event: KeyboardEvent
        ) => {
            // TODO: open the file},
      },
      {
        label: "Storage",
        accelerator: "CmdOrCtrl+S",
        click: (
          item: MenuItem,
          focusedWindow: BrowserWindow | undefined,
          _event: KeyboardEvent
        ) => {
          //TODO: store content},
      },
    ],
  },
  {
    label: "Edit",
    submenu: [
      {
        label: "Revoke",
        accelerator: "CmdOrCtrl+Z",
        role: "undo",
      },
      {
        label: "redo",
        accelerator: "Shift+CmdOrCtrl+Z",
        role: "redo",
      },
      {
        type: "separator",
      },
      {
        label: "Cut",
        accelerator: "CmdOrCtrl+X",
        role: "cut",
      },
      {
        label: "Copy",
        accelerator: "CmdOrCtrl+C",
        role: "copy",
      },
      {
        label: "Paste",
        accelerator: "CmdOrCtrl+V",
        role: "paste",
      },
    ],
  },
  {
    label: "window",
    role: "window",
    submenu: [
      {
        label: "Minimize",
        accelerator: "CmdOrCtrl+M",
        role: "minimize",
      },
      {
        label: "maximize",
        accelerator: "CmdOrCtrl+M",
        click: (
          item: MenuItem,
          focusedWindow: BrowserWindow | undefined,
          _event: KeyboardEvent
        ) => {
          if (focusedWindow) {
            focusedWindow.maximize();
          }
        },
      },
      {
        type: "separator",
      },
      {
        label: "Switch full screen",
        accelerator: (function () {
          if (process.platform === "darwin") {
            return "Ctrl+Command+F";
          } else {
            return "F11";
          }
        })(),
        click: (
          item: MenuItem,
          focusedWindow: BrowserWindow | undefined,
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          _event: KeyboardEvent
        ) => {
          if (focusedWindow) {
            focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
          }
        },
      },
    ],
  },
  {
    label: "Help",
    role: "help",
    submenu: [
      {
        label: "Learn more",
        click: function () {
          shell.openExternal("http://electron.atom.io");
        },
      },
    ],
  },
];

For details on how to define it, see Electron Menu.

Opening files and storing them is not yet implemented, but will be implemented later.

Set up the menu bar

import { Menu } from "electron";
app.on("ready", async () => {
  // Omitted...
  // Create menuMenu.setApplicationMenu(Menu.buildFromTemplate(template));
});

Set the Menu in the ready hook function.

Effect

Menu Effects

The editor opens the contents of the markdonw file

The main thread selects the file and passes the file path to the rendering thread

<!-- background.ts -->
dialog
  .showOpenDialog({
    properties: ["openFile"],
    filters: [{ name: "Custom File Type", extensions: ["md"] }],
  })
  .then((res) => {
    if (res && res["filePaths"].length > 0) {
      const filePath = res["filePaths"][0];
      // Pass the file to the rendering thread if (focusedWindow) {
        focusedWindow.webContents.send("open-file-path", filePath);
      }
    }
  })
  .catch((err) => {
    console.log(err);
  });

showOpenDialog is the method to open a file. Here we specify to open only the md file.

After obtaining the file path, pass the file path to the rendering thread through focusedWindow.webContents.send("open-file-path", filePath); method.

The rendering thread obtains the file path, reads the file content, and assigns it to the markdown editor

<!-- App.vue -->
import { ipcRenderer } from "electron";
import { readFileSync } from "fs";

export default defineComponent({
  // Omitted...
  setup() {
    const content = ref("");
    
    onMounted(() => {
      // 1.
      ipcRenderer.on("open-file-path", (e, filePath: string) => {
        if (filePath && filePath.length > 0) {
          // 2.
          content.value = readFileSync(filePath).toString();
        }
      });
    });

    return { content };
  },
});

Vue adds node support

<!-- vue.config.js -->
module.exports = {
  pluginOptions: {
    electronBuilder: {
      nodeIntegration: true,
    },
  },
};

Effect

Rendering

Save the contents of markdonw to the file

The main thread initiates a request to the rendering thread to obtain the editor content

<!-- background.js -->
if (focusedWindow) {
    focusedWindow.webContents.send("get-content", "");
}

The rendering thread returns the editor content to the main thread

<!-- App.vue -->
onMounted(() => {
    ipcRenderer.on("get-content", () => {
        ipcRenderer.send("save-content", content.value);
    });
});

The main thread receives the content and saves it to the file

<!-- background.ts -->
// Store file ipcMain.on("save-content", (event: unknown, content: string) => {
  if (openedFile.length > 0) {
    // Store directly into the file try {
      writeFileSync(openedFile, content);
      console.log("Save successfully");
    } catch (error) {
      console.log("Save failed");
    }
  } else {
    const options = {
      title: "Save File",
      defaultPath: "new.md",
      filters: [{ name: "Custom File Type", extensions: ["md"] }],
    };
    const focusedWindow = BrowserWindow.getFocusedWindow();
    if (focusedWindow) {
      dialog
        .showSaveDialog(focusedWindow, options)
        .then((result: Electron.SaveDialogReturnValue) => {
          if (result.filePath) {
            try {
              writeFileSync(result.filePath, content);
              console.log("Save successfully");
              openedFile = result.filePath;
            } catch (error) {
              console.log("Save failed");
            }
          }
        })
        .catch((error) => {
          console.log(error);
        });
    }
  }
});

Effect

Rendering

Pack

Set the name and image of the application

<!-- vue.config.js -->
module.exports = {
  pluginOptions: {
    electronBuilder: {
      nodeIntegration: true,
      // Added settings builderOptions: {
        appId: "com.johnny.markdown", 
        productName: "JJMarkDown", // Application name copyright: "Copyright © 2021", // Copyright statement mac: {
          icon: "./public/icon.icns", // icon
        },
      },
    },
  },
};

Prepare a 1024*1024 image for icon.icns generation and create a folder named icons.iconset in the same directory;

Create image files of various sizes

sips -z 16 16 icon.png -o icons.iconset/icon_16x16.png
sips -z 32 32 icon.png -o icons.iconset/[email protected]
sips -z 32 32 icon.png -o icons.iconset/icon_32x32.png
sips -z 64 64 icon.png -o icons.iconset/[email protected]
sips -z 128 128 icon.png -o icons.iconset/icon_128x128.png
sips -z 256 256 icon.png -o icons.iconset/[email protected]
sips -z 256 256 icon.png -o icons.iconset/icon_256x256.png
sips -z 512 512 icon.png -o icons.iconset/[email protected]
sips -z 512 512 icon.png -o icons.iconset/icon_512x512.png
sips -z 1024 1024 icon.png -o icons.iconset/[email protected]

Get the icon file named icon.icns

iconutil -c icns icons.iconset -o icon.icns

Pack

npm run electron:build

result

dmg

The obtained dmg file can be installed and used directly.

Code

<!-- background.ts -->
"use strict";

import {
  app,
  protocol,
  BrowserWindow,
  screen,
  Menu,
  MenuItem,
  shell,
  dialog,
  ipcMain,
} from "electron";
import { KeyboardEvent, MenuItemConstructorOptions } from "electron/main";
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
import installExtension, { VUEJS3_DEVTOOLS } from "electron-devtools-installer";
const isDevelopment = process.env.NODE_ENV !== "production";
import { writeFileSync } from "fs";

let openedFile = "";
// Store file ipcMain.on("save-content", (event: unknown, content: string) => {
  if (openedFile.length > 0) {
    // Store directly into the file try {
      writeFileSync(openedFile, content);
      console.log("Save successfully");
    } catch (error) {
      console.log("Save failed");
    }
  } else {
    const options = {
      title: "Save File",
      defaultPath: "new.md",
      filters: [{ name: "Custom File Type", extensions: ["md"] }],
    };
    const focusedWindow = BrowserWindow.getFocusedWindow();
    if (focusedWindow) {
      dialog
        .showSaveDialog(focusedWindow, options)
        .then((result: Electron.SaveDialogReturnValue) => {
          if (result.filePath) {
            try {
              writeFileSync(result.filePath, content);
              console.log("Save successfully");
              openedFile = result.filePath;
            } catch (error) {
              console.log("Save failed");
            }
          }
        })
        .catch((error) => {
          console.log(error);
        });
    }
  }
});

const template: Array<MenuItemConstructorOptions> = [
  {
    label: "MarkDown",
    submenu: [
      {
        label: "About",
        accelerator: "CmdOrCtrl+W",
        role: "about",
      },
      {
        label: "Exit program",
        accelerator: "CmdOrCtrl+Q",
        role: "quit",
      },
    ],
  },
  {
    label: "file",
    submenu: [
      {
        label: "Open file",
        accelerator: "CmdOrCtrl+O",
        click: (
          item: MenuItem,
          focusedWindow: BrowserWindow | undefined,
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          _event: KeyboardEvent
        ) => {
          dialog
            .showOpenDialog({
              properties: ["openFile"],
              filters: [{ name: "Custom File Type", extensions: ["md"] }],
            })
            .then((res) => {
              if (res && res["filePaths"].length > 0) {
                const filePath = res["filePaths"][0];
                // Pass the file to the rendering thread if (focusedWindow) {
                  focusedWindow.webContents.send("open-file-path", filePath);
                  openedFile = filePath;
                }
              }
            })
            .catch((err) => {
              console.log(err);
            });
        },
      },
      {
        label: "Storage",
        accelerator: "CmdOrCtrl+S",
        click: (
          item: MenuItem,
          focusedWindow: BrowserWindow | undefined,
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          _event: KeyboardEvent
        ) => {
          if (focusedWindow) {
            focusedWindow.webContents.send("get-content", "");
          }
        },
      },
    ],
  },
  {
    label: "Edit",
    submenu: [
      {
        label: "Revoke",
        accelerator: "CmdOrCtrl+Z",
        role: "undo",
      },
      {
        label: "redo",
        accelerator: "Shift+CmdOrCtrl+Z",
        role: "redo",
      },
      {
        type: "separator",
      },
      {
        label: "Cut",
        accelerator: "CmdOrCtrl+X",
        role: "cut",
      },
      {
        label: "Copy",
        accelerator: "CmdOrCtrl+C",
        role: "copy",
      },
      {
        label: "Paste",
        accelerator: "CmdOrCtrl+V",
        role: "paste",
      },
    ],
  },
  {
    label: "window",
    role: "window",
    submenu: [
      {
        label: "Minimize",
        accelerator: "CmdOrCtrl+M",
        role: "minimize",
      },
      {
        label: "maximize",
        accelerator: "CmdOrCtrl+M",
        click: (
          item: MenuItem,
          focusedWindow: BrowserWindow | undefined,
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          _event: KeyboardEvent
        ) => {
          if (focusedWindow) {
            focusedWindow.maximize();
          }
        },
      },
      {
        type: "separator",
      },
      {
        label: "Switch full screen",
        accelerator: (function () {
          if (process.platform === "darwin") {
            return "Ctrl+Command+F";
          } else {
            return "F11";
          }
        })(),
        click: (
          item: MenuItem,
          focusedWindow: BrowserWindow | undefined,
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          _event: KeyboardEvent
        ) => {
          if (focusedWindow) {
            focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
          }
        },
      },
    ],
  },
  {
    label: "Help",
    role: "help",
    submenu: [
      {
        label: "Learn more",
        click: function () {
          shell.openExternal("http://electron.atom.io");
        },
      },
    ],
  },
];

protocol.registerSchemesAsPrivileged([
  { scheme: "app", privileges: { secure: true, standard: true } },
]);

async function createWindow() {
  const { width, height } = screen.getPrimaryDisplay().workAreaSize;
  const win = new BrowserWindow({
    width,
    height,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
    },
  });

  if (process.env.WEBPACK_DEV_SERVER_URL) {
    // Load the url of the dev server if in development mode
    await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string);
    if (!process.env.IS_TEST) win.webContents.openDevTools();
  } else {
    createProtocol("app");
    // Load the index.html when not in development
    win.loadURL("app://./index.html");
  }
}

// Quit when all windows are closed.
app.on("window-all-closed", () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== "darwin") {
    app.quit();
  }
});

app.on("activate", () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on("ready", async () => {
  if (isDevelopment && !process.env.IS_TEST) {
    // Install Vue Devtools
    try {
      await installExtension(VUEJS3_DEVTOOLS);
    } catch (e) {
      console.error("Vue Devtools failed to install:", e.toString());
    }
  }
  createWindow();
  // Create menuMenu.setApplicationMenu(Menu.buildFromTemplate(template));
});

// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
  if (process.platform === "win32") {
    process.on("message", (data) => {
      if (data === "graceful-exit") {
        app.quit();
      }
    });
  } else {
    process.on("SIGTERM", () => {
      app.quit();
    });
  }
}

This is the end of this article about the detailed explanation of Vue3 and Electron to implement desktop applications. For more relevant Vue3 Electron desktop application content, please search 123WORDPRESS.COM's previous articles or continue to browse the following related articles. I hope everyone will support 123WORDPRESS.COM in the future!

You may also be interested in:
  • Vite+Electron to quickly build VUE3 desktop applications
  • Detailed explanation of the operation process of packaging desktop with Electron + Vue
  • Sample code for making desktop applications with vue + Electron

<<:  A brief discussion on the performance issues of MySQL paging limit

>>:  Summary of 6 Linux log viewing methods

Recommend

Overview of the basic components of HTML web pages

<br />The information on web pages is mainly...

Steps to create a WEBSERVER using NODE.JS

Table of contents What is nodejs Install NodeJS H...

JavaScript data transmission between different pages (URL parameter acquisition)

On web pages, we often encounter this situation: ...

MySQL backup table operation based on Java

The core is mysqldump and Runtime The operation i...

Summary of various methods for JS data type detection

Table of contents background What are the methods...

Summary of common docker commands

Docker installation 1. Requirements: Linux kernel...

Install Docker environment in Linux environment (no pitfalls)

Table of contents Installation Prerequisites Step...

A detailed tutorial on using Docker to build a complete development environment

Introduction to DNMP DNMP (Docker + Nginx + MySQL...

4 principles for clean and beautiful web design

This article will discuss these 4 principles as t...

Implementation of CSS3 button border animation

First look at the effect: html <a href="#...

Detailed explanation of the relationship between React and Redux

Table of contents 1. The relationship between red...

About React Native unable to link to the simulator

React Native can develop iOS and Android native a...