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

How to use squid to build a proxy server for http and https

When we introduced nginx, we also used nginx to s...

Solutions to MySQL batch insert and unique index problems

MySQL batch insert problem When developing a proj...

Advantages and disadvantages of common MySQL storage engines

Table of contents View all storage engines InnoDB...

How to set up vscode remote connection to server docker container

Table of contents Pull the image Run the image (g...

Detailed description of the function of new in JS

Table of contents 1. Example 2. Create 100 soldie...

HTML table markup tutorial (5): light border color attribute BORDERCOLORLIGHT

In a table, you can define the color of the upper...

JavaScript Basics Series: Functions and Methods

Table of contents 1. The difference between funct...

How to resize partitions in CentOS7

Yesterday, I helped someone install a system and ...

VSCode+CMake+Clang+GCC environment construction tutorial under win10

I plan to use C/C++ to implement basic data struc...

Tutorial on installing VMWare15.5 under Linux

To install VMWare under Linux, you need to downlo...

Detailed examples of Zabbix remote command execution

Table of contents one. environment two. Precautio...