preface
Image compression is a very common operation in front-end development, generally there are three common ways:
- UI small sister compression good to provide the front end (so intimate UI small sister where to find ~)
- PS and other image processing software compression
- Online website image compression (Tinypng, etc.)
However, today we are going to use JS to write a desktop application, used to compress pictures, software screenshots are as follows:
Application features:
- Batch compression: you can configure the number of pieces of batch compression, we set 100 pieces this time (tinypng online at most 20 pieces)
- Fast compression speed
- The compression quality is similar to tinypng
Let’s talk about technology selection: Electron + Vue3 + Element Plus
Electron:
It is a popular JS framework for building cross-platform applications. I have also used electron to develop small applications before. I personally like electron for three reasons:
- Cross-platform, one-time development, multi-platform application;
- Low cost of learning and development time, especially for front-end developers;
- Commonly used function modules are built in. For example, we use the built-in nativeImage module of Electron for image compression this time
There are many other advantages of Electron. You can go to the official website of Electron to learn about them. Many of our commonly used software are developed by electron, such as VS Code, one of our front-end engineer’s bowl software
Of course, there are also some disadvantages, the application is slightly larger after packaging, for example, our picture compression application this time is more than 50 meters after packaging optimization, the package volume will not be very small, this is determined by the low-level implementation of Electron itself, we expect the official can optimize this point ~
Vue3:
Personally, I prefer the 3.0 version of the Composition API, but the company is currently running ve2. X, so I want to get started with this app
Element Plus:
This is mainly lazy (cover face), ready-made components to use really sweet ~
Function of thinking
The Electron core is divided into the main process and the render process:
- The renderer process is our front-end environment, in this case a single page application built by VUE;
- The main process manages the renderer process and is responsible for the interaction with the system. It is the bridge between the renderer process and the system.
For the realization of 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 images and saves them in the specified directory. The state of compression success or failure is returned to the rendering process, and the page prompts success or failure:
The project build
First you need to have installed:
- node
- npm
- vue-cli
Let’s create the project:
Vue create < project name >Copy the code
Then enter the project information:
? 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
Copy the code
Install vUE – CLI-plugin-electron -builder, electron version select ^9.0.0:
CD < project directory > vue add electron- BuilderCopy the code
Launch project:
npm run electron:serve
Copy the code
Project directory
There are already some pages in the initialized project, but we don’t need them. Let’s simplify the project directory:
- 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
- router
- babel.config.js
- package.json
- README.md
Start coding
First we install Element-Plus
npm install element-plus --save
Copy the code
Introduce element-plus and base.scss in main.js (Base.scss are some base 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')
Copy the code
Write a 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 the code
In electron, IPcmian (the main process) and ipcrenderer (the rendering process) are responsible for the communication between the main process and the rendering process. This requires the introduction of electron. Ipcrenderer in the Vue page. We need to configure this in electron’s background.js to allow the page to integrate the Node module
// background.js
const win = new BrowserWindow({
width: 800.// Width of the application interface
height: 600.// Height of the application interface
webPreferences: {
nodeIntegration: true.// Allow pages to integrate node modules
webSecurity: false.// Remove cross-domain restrictions}})Copy the code
Now we can introduce node modules such as electron and path into the page using window.require(“electron”).
Now let’s write the application interface and logic. The interface component uses Elder-Plus: the compression quality component uses el-Slider, and the image component uses el-Upload. The page structure is as follows:
<! -- Page structure -->
<template>
<div class="tinypng-wrapper">
<div class="tips">
<p>1. Only compress<span class="highlight">jpg/png</span>Format picture;</p>
<p>2. Maximum compression at one time<span class="highlight">100</span>;</p>
<p>3. The compressed file is saved in the<span class="highlight"
>Note that the image-compress folder </ SPAN > is displayed in the last image path.</p>
<p>4. If a file with the same name exists in the image-compress folder, the image compress folder will be deleted<span class="highlight"
></span >;</p>
<p>5. Image processing takes time, please wait for a moment after clicking compress.</p>
</div>
<div class="header">
<span class="label">The 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 save directory</template>
</el-input>
<el-button style="margin-left: 24px" type="success" @click="handleSubmit">To compress</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>
Copy the code
Write here is page logic, the user select files, compressed quality, generates a file directory, and file system path stored in arrays, by ipcRenderer passed to the main process, to the main process of image processing, the main process processing is complete (or failure), and the page response returned by the main process of processing condition:
- Ipcrenderer.send (): sends a message to the main process (ipcMain)
- Ipcrenderer.on (): responds to a message pushed by the main process (ipcMain)
// Page logic
<script>
// Electron ipcRenderer -- communicates with the electron main process
const { ipcRenderer } = window.require("electron")
// the path module handles file paths
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() {
// List of files
const fileList = ref([]);
// Limit the number of files processed in batches
const maxFileNum = ref(100);
// Image select component
const upload = ref(null);
// The target directory to save the image
const targetDir = ref(null);
// Image compression quality
const quality = ref(50);
// Image 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"
});
// If the number of files selected exceeds the set value, a warning box will be displayed
const handleExceed = (files, fileList) = > {
ElMessage.warning({
message: 'At most, you can only choose${ maxFileNum.value }Three files, currently selected${files.length + fileList.length}A file `.type: "warning"
});
};
// File change event, set the file save directory to the image-compress folder in the current directory. No files with the same name will be created, and files with the same name 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 that the image is selected, no pop-up warning is selected
if(! uploadFiles.length) { ElNotification({title: "Warning".message: "Please select the file first!".type: "warning"
});
return false;
}
const dir = PATH.normalize(targetDir.value);
// Walk through the path of the image file
const fileList = [];
uploadFiles.map(item= >item? .raw? .path && fileList.push(item.raw.path));// Message parameters
const data = {
fileList,
quality: quality.value,
targetDir: dir
};
/ / display loading
loadingInstance = ElLoading.service({
background: "Rgba (255255255,0.5)"
});
// Send a message to the main process containing: compression quality, compressed save directory, compressed file address (array)
ipcRenderer.send("compress-image", data);
};
onBeforeUnmount(() = > {
loadingInstance = null;
});
// Mounted Life cycle
onMounted(() = > {
// The compression status of the picture pushed by the main process is displayed in a popup box
ipcRenderer.on("compress-status".(event, arg) = > {
ElNotification({
title: arg.success ? "Success" : "Failure".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>Copy the code
Style slightly…
Now you need to respond to the message sent by the page in the main process, which communicates using ipcMain: ipcmain.on (): accepts the message sent by the page
// background.js
// Image compression: receives the message from the page. Arg is the message parameter
ipcMain.on('compress-image'.async (event, arg) => {
// Image compression
const status = await imageCompress(arg)
// Send the result to the page
BrowerWindow.webContents.send('compress-status', status)
})
Copy the code
Utils /utils. Js is the encapsulation of some common methods, utils/ compressed-electron.js is the compression logic of the image: utils/utils/compressed-electron.js
// utils.js
import fs from 'fs'
// Create directory, return the result of creating 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,
}
Copy the code
// 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({
/ / quality of image compression, the optional value: better | | good | | best
quality: 'best'
})
console.log(res)
// const imageData = res.toPNG()
// JPG compressed image quality Settings
const imageData = res.toJPEG(quality)
return imageData;
}
export default async (options) => {
// Create a directory to save images
const createDir = await mkdir(options.targetDir)
if(! createDir)return {
success: false.msg: 'Failed to create picture save 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: 'The image is compressed successfully and saved in${options.targetDir}Directory `}}catch (err) {
console.log(err, 'err')
return {
success: false.msg: 'Picture compression failed! `.reason: err
}
}
}
Copy the code
Finally, import the compresse-electron. Js file in the electron entry file background-js and hide the menu at the top. The full code for background-electron:
'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'
constisDevelopment = 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: receives 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)
})
Copy the code
Effect:
packaging
Execute the command
npm run electron:build
Copy the code
After a few minutes, you’ll be able to view the.exe file in the dist_electron folder in your project directory. Here’s the package (the same is true for MAC systems, but the suffix is different). Double-click to run the.exe file. Here’s a demo:
More functions
The electron nativeImage can also transform the image format (PNG to JPG, JPG to PNG). The image can be scaled and clipped in theThe official documentationLook at the API and encapsulate the common method. The effect diagram I wrote is as follows:
The last
If there are any mistakes in this article, please correct them
Finally, thanks to the help and guidance of the whole big guy ~~