Vue3 + Electron develops image compression desktop application

Electron is not complicated to use for understanding the children's shoes of NodeJS. It needs to focus on understanding the difference and communication between the rendering process and the main process. This article is an online practice, and the process drawing is very clear.
The simple understanding is that electron provides system related API s for NodeJS, and also provides the communication encapsulation of rendering process (can be thought of as browser) and main process (NodeJS). Then the written page is responsible for UI rendering and interaction, and NodeJS is used to realize the ability of native applications.
Original text: juejin.cn/post/6924521091914776584
Author: Iris_Mei

Vue3 + Electron develops image compression desktop application

preface

Image compression is a very common operation in front-end development. There are three common methods:

  • The UI is compressed and provided to the front end (where can I find such a considerate UI sister ~)
  • PS and other image processing software compression
  • Online website image compression (tinypng, etc.)

However, today we are going to write a desktop application with js to compress pictures. The screenshot of the software is as follows:

Application features:

  • Batch compression: you can configure the number of pieces for batch compression by yourself. We set 100 pieces this time (tinypng can compress up to 20 pieces at a time online)
  • Fast compression speed
  • The compression quality is similar to tinypng

Talk about technology selection: Electron + Vue3 + Element plus

Electron:

It is a popular framework for js to build cross platform applications. I have also developed small applications using electron before. I personally like electron for three reasons:

  • Cross platform, one-time development, multi platform application;
  • Low learning cost and development time cost, especially for front-end developers;
  • Built in common function modules. For example, our image compression this time uses the nativeImage module built in electron

There are many advantages of electron. You can go to the official website of electron to understand that many of our commonly used software are developed using electron, such as one of our front-end engineers' job software: vs code

Of course, there are some disadvantages. The application is slightly larger after packaging. For example, our image compression application is more than 50 M after packaging. Even if the packaging optimization is done, the package volume will not be very small. This is determined by the bottom implementation of electron itself. I hope the official can optimize this point~

Vue3:

I prefer the Composition API of version 3.0, but the company is now vue2 Version x, I'm going to practice with this application~

Element Plus:

This is mainly lazy (covering the face). The ready-made components are really fragrant to use~

Functional thinking

The electron core is divided into main process and rendering process:

  • The rendering process is our front-end environment. This project is a single page application built by vue;
  • The main process manages the rendering process and is responsible for the interaction with the system. It is the bridge between the rendering process and the system;

For the implementation of the image compression function, the user selects images in batches on the page and sends the image path to the main process. The main process compresses the images and saves them in the specified directory, and returns the status of successful or failed compression to the rendering process. The page prompts success or failure:

Project construction

First you need to have installed:

  • node
  • npm
  • vue-cli

Let's create a project:

vue create <entry name>

Then enter the information of the project:

? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, CSS Pre-processors
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use history mode for router? (Requires proper server setup for index fallback in production) No
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)
? Where do you prefer placing config for Babel, ESLint, etc.? In package.json
? Save this as a preset for future projects? No

Install Vue cli plugin electron builder. Select ^ 9.0.0 for electron version:

cd <Project directory>
vue add electron-builder

Start project:

 npm run electron:serve

Project directory

There are already some pages in the initialized project, but we don't need them. Let's simplify the project directory below:

  • dist_electron
  • node_modules
  • public
    • index.html
  • src
    • router
      • index.js
    • styles
      • base.scss
    • utils
      • utils.js
      • compress-electron.js
    • views
      • ImageCompress.vue
    • App.vue
    • background.js
    • main.js
  • babel.config.js
  • package.json
  • README.md

Start coding

First, we install element plus

npm install element-plus --save

In main JS to introduce element plus and base SCSS (base.scss are some basic styles and common styles)

// main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus';
import 'element-plus/lib/theme-chalk/index.css';
import './styles/base.scss'

createApp(App).use(router).use(ElementPlus).mount('#app')

Write route: router / index js

// router/index.js 
import { createRouter, createWebHashHistory } from 'vue-router'
import ImageCompress from '../views/ImageCompress.vue'
const routes = [{
    path: '/',
    name: 'ImageCompress',
    component: ImageCompress,
  }]
const router = createRouter({
  history: createWebHashHistory(),
  routes
})
export default router
 Copy code

In electron, ipcmian (main process) and ipcrender (rendering process) are responsible for the communication between the main process and the rendering process, which requires the introduction of electron Before the introduction of ipclender, we need to check the background of electron JS, which allows the page to integrate the node module

// background.js

const win = new BrowserWindow({
    width: 800, // Application interface width
    height: 600, // Application interface height
    webPreferences: {
      nodeIntegration: true, //Allow page integration node module
      webSecurity: false,// Remove cross domain restrictions
    }
})

Now we can introduce node modules such as electron and path into the page and use window Require ("electron") is introduced.

Now let's write the application interface and logic. The interface components use element plus: the slider component El slider for compression quality selection and the El upload component for image selection. The page structure is as follows:

<!-- Page structure-->
<template>
  <div class="tinypng-wrapper">
    <div class="tips">
      <p>1\. Compression only <span class="highlight">jpg/png</span> Format picture;</p>
      <p>2\. Maximum compression at one time<span class="highlight">100 Zhang</span>;</p>
      <p>
        3\. The compressed file is saved in the<span class="highlight"
          >Under the path of the last picture image-compress folder</span
        >in, Please pay attention to the prompt after success;
      </p>
      <p>
        4\. image-compress If there is a file with the same name in the folder, it will be deleted<span class="highlight"
          >cover</span
        >;
      </p>
      <p>5\. Image processing takes time. Please wait patiently after clicking compression.</p>
    </div>
    <div class="header">
      <span class="label">compression quality</span>
      <el-slider
        class="slider"
        v-model="quality"
        :step="10"
        :min="10"
        :marks="marks"
        show-stops
      >
      </el-slider>
    </div>
    <div class="header">
      <el-input placeholder="Save file directory" v-model="targetDir" disabled>
        <template #Prepend > picture saving directory < / template >
      </el-input>
      <el-button style="margin-left: 24px" type="success" @click="handleSubmit">Start compression</el-button>
    </div>

    <div class="tinypng-content">
      <el-upload
        class="upload-demo"
        ref="upload"
        accept=".jpg,.png"
        multiple
        :auto-upload="false"
        :limit="maxFileNum"
        :file-list="fileList"
        :before-upload="beforeUpload"
        :on-exceed="handleExceed"
        :on-change="handleChangeFile"
        action=""
        list-type="picture-card"
      >
        <i class="el-icon-plus"></i>
      </el-upload>
    </div>
  </div>
</template>

The following is the compilation of page logic. After selecting the file and compression quality, the user generates a file saving directory, saves the system path of the file in the array, passes it to the main process through ipcRenderer, and sends it to the main process for image processing. After the processing of the main process is completed (or failed), and the page responds to the processing status returned by the main process:

  • ipcRenderer.send(): Send a message to the main process (ipcMain)
  • ipcRenderer.on(): respond to messages pushed by the main process (ipcMain)
// Page logic
<script>
// electron ipcRenderer -- communicate with the electron main process
const { ipcRenderer } = window.require("electron")
// Path module, processing file path
const PATH = window.require("path");

import { onMounted, ref, onBeforeUnmount } from "vue";
import { ElMessage, ElNotification, ElLoading } from "element-plus";
// loading instance
 let loadingInstance = null;

export default {
  setup() {
    // File list
    const fileList = ref([]);
    // Limit on the number of batch processing files
    const maxFileNum = ref(100);
    // Picture selection component
    const upload = ref(null);
    // Destination directory for image saving
    const targetDir = ref(null);
    // Picture compression quality
    const quality = ref(50);
    // Picture compression quality options
    const marks = ref({
      10: "10",
      20: "20",
      30: "30",
      40: "40",
      50: "50",
      60: "60",
      70: "70",
      80: "80",
      90: "90",
      100: "100"
    });

    // When the number of files selected exceeds the set value, a warning box will pop up
    const handleExceed = (files, fileList) => {
        ElMessage.warning({
            message: `At most ${ maxFileNum.value }File Oh, currently selected ${files.length + fileList.length} Files`,
            type: "warning"
        });

    };

    // File change event: set the file saving directory to the image compress folder under the current directory. If there is no file, it will be created, and if there is a file with the same name, it will be overwritten
    const handleChangeFile = file => {
        const parseUrl = PATH.parse(file.raw.path);
        targetDir.value = parseUrl.dir + `${PATH.sep}image-compress`;
    };

    // Confirm button to start compression
    const handleSubmit = () => {
      const uploadFiles = upload.value.uploadFiles;
      // Verify whether a picture is selected and no warning message pops up
       if (!uploadFiles.length) {
        ElNotification({
          title: "warning",
          message: "Please select a file first!", 
          type: "warning"
        });
        return false;
      }

      const dir = PATH.normalize(targetDir.value);

      // Traverse the path of the picture file
      const fileList = [];
      uploadFiles.map(item => item?.raw?.path && fileList.push(item.raw.path));

        // Message parameters
      const data = {
        fileList,
        quality: quality.value,
        targetDir: dir
      };

      // Show loading
       loadingInstance = ElLoading.service({
            background: "rgba(255,255,255,0.5)"
          });

     // Send a message to the main process. The message includes: compression quality, compressed storage directory and compressed file address (array)
     ipcRenderer.send("compress-image", data);
    };

    onBeforeUnmount(() => {
      loadingInstance = null;
    });

    // mounted lifecycle
    onMounted(() => {
    // Respond to the picture compression status pushed by the main process and display it in a pop-up box
          ipcRenderer.on("compress-status", (event, arg) => {
        ElNotification({
            title: arg.success ? "success" : "fail",
            message: arg.success ? arg.msg : arg.reason,
            type: arg.success ? "success" : "error"
        });
        loadingInstance.close();
        if (arg.success) {
          fileList.value = [];
          quality.value = 50;
          targetDir.value = null;
        }

    });
    });

    return {
      targetDir,
      upload,
      quality,
      marks,
      fileList,
      maxFileNum,
      handleExceed,
      handleChangeFile,
      handleSubmit
    };
  }
};
</script>

Style slightly

Now you need to respond to the messages sent by the page in the main process. The communication of the main process uses ipcMain:ipcmain On (): accept the message sent by the page

// background.js

// Image compression: receive the message from the page, arg is the message parameter
ipcMain.on('compress-image', async (event, arg) => { 
    // Picture compression
  const status = await imageCompress(arg)
  // Send results to page
  BrowerWindow.webContents.send('compress-status', status)
})

Next, start the logic of compressing the picture, utils / utils JS is the encapsulation of some common methods, utils / compress electron JS is the logic of image compression:

// utils.js

import fs from 'fs'

// Create a directory and return the result of creating a directory
const mkdir = (path) => {
    return new Promise((resolve, reject) => {
        if (fs.existsSync( path )) { 
            resolve(true)
            return
        }
        fs.mkdir(path, (error) => {
            if (error) {
                reject(false)
            } else {
                resolve(true)
            }
        })
    })
}
export {
    mkdir,
}
// compress-electron.js 
import { nativeImage } from 'electron'
import path from 'path'
import fs from 'fs'
import { mkdir } from './utils'

const imageCompress = (input, quality) => {
    quality = quality || 50
    const image = nativeImage.createFromPath(input);
    const res = image.resize({
        // Picture compression quality, optional value: better | good | best
        quality: 'best'  
    })
    console.log(res)
    // const imageData = res.toPNG()
    // jpg compressed picture quality settings
    const imageData = res.toJPEG(quality)
    return imageData;
}

export default async (options) => {
    // Create and save the picture directory. If it fails, exit
    const createDir = await mkdir(options.targetDir)
    if (!createDir) return {
        success: false,
        msg: 'Failed to create picture saving directory!'
    } 

    try {
        options.fileList.map((item) => {
            const dirParse = path.parse(item)
            const data = imageCompress(item, options.quality)
            const targetDir = `${options.targetDir}${path.sep}${dirParse.name}${dirParse.ext}`
            fs.writeFileSync(targetDir,data)
        })
        return {
            success: true,
            msg: `Picture compressed successfully, saved in ${options.targetDir} In the directory`
        }
    } catch (err) {
        console.log(err, 'err')
        return {
            success: false,
            msg: `Picture compression failed!`,
            reason: err
        }
    }
}

Finally, in the electron entry file background JS to introduce compress electron JS, and hide the menu at the top, background JS complete code:

'use strict'

import { app, protocol, BrowserWindow, ipcMain, Menu } from 'electron'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
import imageCompress from './utils/compress-electron.js'
const isDevelopment = process.env.NODE_ENV !== 'production'

// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
  { scheme: 'app', privileges: { secure: true, standard: true } }
])

let BrowerWindow = null

async function createWindow() {
  // Create the browser window.
  BrowerWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
      webSecurity: false,// Remove cross domain restrictions
    }
  })

  if (process.env.WEBPACK_DEV_SERVER_URL) {
    // Load the url of the dev server if in development mode
    await BrowerWindow.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
    if (!process.env.IS_TEST) BrowerWindow.webContents.openDevTools()
  } else {
    createProtocol('app')
    // Load the index.html when not in development
    BrowerWindow.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(VUEJS_DEVTOOLS)
    } catch (e) {
      console.error('Vue Devtools failed to install:', e.toString())
    }
  }
  createWindow()
})

// 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()
    })
  }
}

// Image compression: receive the message from the page, arg is the message parameter
ipcMain.on('compress-image', async (event, arg) => { 
  const status = await imageCompress(arg)
  console.log('compress-status')
  BrowerWindow.webContents.send('compress-status', status)
})

design sketch:

pack

Execute command

npm run electron:build

Different computers have different packing speed and time. After a few minutes, you can use dist in the project directory_ See in the electron folder exe file, which is the package we packaged (the mac system is packaged in this way, but the generated file suffix is different). Double click to run exe file is enough. The following is the effect demonstration:

More features

The nativeImage of electron can also realize the conversion of pictures (png to jpg, jpg to png). Pictures can be scaled and cropped in Official documents Check the api and encapsulate the common methods. The effect diagram I wrote is as follows:

last

If there are any mistakes in the text, please correct them~~

Finally, thank you for your help and guidance~~

Tags: Vue Electron

Posted by koolaid on Fri, 15 Apr 2022 04:05:46 +0930