
一、效果展示
1.图片压缩

2.转换成PNG

3.转换成JPG

二、Electron
electron可以使用 JavaScript,HTML 和 CSS 构建跨平台的桌面应用程序,我们熟悉的Visual Studio Code也是通常electron构建的
electron 核心分为 主进程和渲染进程:
对于图片压缩功能的实现,用户在页面批量选择图片,发送图片路径给主进程,主进程压缩图片并将图片保存在指定目录,将压缩成功或者失败的状态返回给渲染进程,页面提示成功或失败:

三、Vue3
渐进式 JavaScript 框架
四、实现教程
1.创建项目(已安装node.js与VueCLI)
vue create <项目名称>








2.安装依赖
- 安装 vue-cli-plugin-electron-builder
cd <项目目录>
vue add electron-builder


npm run electron:serve

3.主进程
在main.js 中引入element-plus
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';
createApp(App).use(router).use(ElementPlus).mount('#app')
编写路由:router/index.js
import { createRouter, createWebHashHistory } from 'vue-router'
import ImageCompress from '../views/ImageCompress.vue'
import transformPNG from '../views/transformPNG.vue'
import transformJPG from '../views/transformJPG.vue'
const routes = [
{
path: '/',
name: 'index',
redirect: '/first'
},
{
path: '/first',
name: 'ImageCompress',
component: ImageCompress
},
{
path: '/second',
name: 'transformPNG',
component: transformPNG
},
{
path: '/third',
name: 'transformJPG',
component: transformJPG
},
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
编写background.js,electron 中ipcmain(主进程)、ipcrenderer(渲染进程)负责主进程和渲染进程之间的通信
'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-image'
import transformPNG from './utils/tranform-png'
import transformJPG from './utils/tranform-jpg'
const isDevelopment = process.env.NODE_ENV !== 'production'
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } }
])
let BrowerWindow = null
async function createWindow() {
Menu.setApplicationMenu(null)
BrowerWindow = new BrowserWindow({
width: 800,
height: 660,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
webSecurity: false,
}
})
if (process.env.WEBPACK_DEV_SERVER_URL) {
await BrowerWindow.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
if (!process.env.IS_TEST) BrowerWindow.webContents.openDevTools()
} else {
createProtocol('app')
BrowerWindow.loadURL('app://./index.html')
}
}
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
app.on('ready', async () => {
if (isDevelopment && !process.env.IS_TEST) {
try {
await installExtension(VUEJS_DEVTOOLS)
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString())
}
}
createWindow()
})
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', (data) => {
if (data === 'graceful-exit') {
app.quit()
}
})
} else {
process.on('SIGTERM', () => {
app.quit()
})
}
}
ipcMain.on('compress-image', async (event, arg) => {
const status = await imageCompress(arg)
console.log('compress-status')
BrowerWindow.webContents.send('compress-status', status)
})
ipcMain.on('transform-png', async (event, arg) => {
const status = await transformPNG(arg)
console.log('png-status')
BrowerWindow.webContents.send('png-status', status)
})
ipcMain.on('transform-jpg', async (event, arg) => {
const status = await transformJPG(arg)
console.log('jpg-status')
BrowerWindow.webContents.send('jpg-status', status)
})
编写utils/utils.js,该方法用于创建文件存放目录
import fs from 'fs'
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,
}
编写utils/compress-image.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: 'best'
})
console.log(res)
const imageData = res.toJPEG(quality)
return imageData;
}
export default async (options) => {
const createDir = await mkdir(options.targetDir)
if (!createDir) return {
success: false,
msg: '创建图片保存目录失败!'
}
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: `图片压缩成功,保存在 ${options.targetDir} 目录中`
}
} catch (err) {
console.log(err, 'err')
return {
success: false,
msg: `图片压缩失败!`,
reason: err
}
}
}
编写utils/tranform-jpg.js,该方法用于将图片转换成jpg格式
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 imageData = image.toJPEG(quality)
return imageData;
}
export default async (options) => {
const createDir = await mkdir(options.targetDir)
if (!createDir) return {
success: false,
msg: '创建图片保存目录失败!'
}
try {
options.fileList.map((item) => {
const dirParse = path.parse(item)
const data = imageCompress(item, options.quality)
const targetDir = `${options.targetDir}${path.sep}${dirParse.name}.jpg`
fs.writeFileSync(targetDir,data)
})
return {
success: true,
msg: `图片转换成功,保存在 ${options.targetDir} 目录中`
}
} catch (err) {
console.log(err, 'err')
return {
success: false,
msg: `图片转换失败!`,
reason: err
}
}
}
编写utils/tranform-png.js,该方法用于将图片转换成png格式
import { nativeImage } from 'electron'
import path from 'path'
import fs from 'fs'
import { mkdir } from './utils'
const transformPNG = (input, quality) => {
quality = quality || 50
const image = nativeImage.createFromPath(input);
const imageData = image.toPNG()
return imageData;
}
export default async (options) => {
const createDir = await mkdir(options.targetDir)
if (!createDir) return {
success: false,
msg: '创建图片保存目录失败!'
}
try {
options.fileList.map((item) => {
const dirParse = path.parse(item)
const data = transformPNG(item, options.quality)
const targetDir = `${options.targetDir}${path.sep}${dirParse.name}.png`
fs.writeFileSync(targetDir,data)
})
return {
success: true,
msg: `图片转换成功,保存在 ${options.targetDir} 目录中`
}
} catch (err) {
console.log(err, 'err')
return {
success: false,
msg: `图片转换失败!`,
reason: err
}
}
}
4.渲染进程
现在我们来写应用界面和逻辑,app.vue文件如下
<template>
<el-tabs v-model="activeName" @tab-click="handleClick" class="tabs">
<el-tab-pane label="图片压缩" name="first"></el-tab-pane>
<el-tab-pane label="转换PNG" name="second"></el-tab-pane>
<el-tab-pane label="转换JPG" name="third"></el-tab-pane>
</el-tabs>
<router-view/>
</template>
<style lang="scss">
body{
margin: 0;
padding: 0;
background: linear-gradient(to right, #f1f1f1, #ffffff);
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
// text-align: center;
color: #868585;
.tabs{
width: 96%;
margin: 0 auto;
}
}
</style>
<script>
import { ref } from 'vue'
import { useRouter } from "vue-router";
export default {
setup(){
const activeName = ref(null);
const router = useRouter();
const handleClick = () =>{
let path = '/'+activeName.value
router.push(path)
}
return {
activeName,
handleClick
}
}
}
</script>
在imageCompress.vue中,界面组件使用element-plus: 压缩质量选择用滑块组件el-slider,图片选择用el-upload组件。
页面逻辑的编写,用户选择文件、压缩质量后,生成一个文件保存目录,并将文件的系统路径保存在数组中,通过ipcRenderer 传递给主进程,交由主进程中去进行图片处理,主进程处理完成(或失败)后,并且在页面响应由主进程返回的处理状态:
- ipcRenderer.send(): 向主进程(ipcMain)发送消息
- ipcRenderer.on(): 响应主进程(ipcMain)推送过来的消息
<template>
<div class="tinypng-wrapper">
<div class="tips">
<p>1. 只能压缩 <span class="highlight">jpg/png</span> 格式图片;</p>
<p>2. 一次最多压缩<span class="highlight">100张</span>;</p>
<p>
3. 压缩后的文件会保存在<span class="highlight"
>选择的路径下的image-compress文件夹</span
>中, 请留意成功后的提示;
</p>
<p>
4. image-compress文件夹中如果有同名文件,将被<span class="highlight"
>覆盖</span
>;
</p>
<p>5. 图片处理需要时间,点击压缩后请耐心等待片刻。</p>
</div>
<div class="header">
<el-button>压缩图片质量</el-button>
<el-slider
class="slider"
v-model="quality"
:step="10"
:min="10"
show-stops
>
</el-slider>
</div>
<div class="header">
<input type="file" id="file" @change="fileChange" hidden webkitdirectory>
<el-input placeholder="保存文件目录" v-model="targetDir" disabled>
<template #prepend>图片保存目录</template>
<template #append>
<el-button icon="el-icon-folder" @click="btnChange"></el-button>
</template>
</el-input>
<el-button style="margin-left: 24px" type="primary" @click="handleSubmit"
>开始压缩</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"
:on-exceed="handleExceed"
:on-preview="handlePictureCardPreview"
action=""
list-type="picture-card"
>
<i class="el-icon-plus"></i>
</el-upload>
<el-dialog v-model="dialogVisible">
<img width="100%" :src="dialogImageUrl" alt="">
</el-dialog>
</div>
</div>
</template>
<style lang="scss" scoped>
.tinypng-wrapper{
width: 96%;
margin: 0 auto;
.tips{
padding-left: 20px;
border: 1px dashed rgb(187, 187, 187);
.highlight{
color: rgb(84, 162, 252);
}
}
.header{
display: flex;
align-items:center;
justify-content: space-between;
margin-top: 25px;
.slider{
width: 78%;
margin-right: 20px;
}
}
.tinypng-content{
margin: 20px 0;
padding: 20px;
border: 1px dashed rgb(187, 187, 187);
border-radius: 5px;
}
}
</style>
<script>
const { ipcRenderer } = window.require("electron")
const PATH = window.require("path");
import { onMounted, ref, onBeforeUnmount } from "vue";
import { ElMessage, ElNotification, ElLoading } from "element-plus";
let loadingInstance = null;
export default {
setup() {
const fileList = ref([]);
const maxFileNum = ref(100);
const upload = ref(null);
const targetDir = ref(null);
const quality = ref(50);
const dialogVisible = ref(false);
const dialogImageUrl = ref(null);
const marks = ref({
10: "10",
20: "20",
30: "30",
40: "40",
50: "50",
60: "60",
70: "70",
80: "80",
90: "90",
100: "100"
});
const handleExceed = (files, fileList) => {
ElMessage.warning({
message: `最多只能选择 ${ maxFileNum.value }个文件哦,当前选择了 ${files.length + fileList.length} 个文件`,
type: "warning"
});
};
const handleSubmit = () => {
const uploadFiles = upload.value.uploadFiles;
if (!uploadFiles.length) {
ElNotification({
title: "警告",
message: "请先选择文件!",
type: "warning"
});
return false;
}
if (!targetDir.value) {
ElNotification({
title: "警告",
message: "请先选择文件存放路径!",
type: "warning"
});
return false;
}
const dir = PATH.normalize(targetDir.value);
const fileList = [];
uploadFiles.map(item => item?.raw?.path && fileList.push(item.raw.path));
const data = {
fileList,
quality: quality.value,
targetDir: dir
};
loadingInstance = ElLoading.service( {
background: "rgba(255,255,255,0.5)"
});
ipcRenderer.send("compress-image", data);
};
const btnChange = () => {
var file = document.getElementById('file')
file.click()
};
const fileChange = () => {
try {
const fu = document.getElementById('file')
if (fu == null) return
targetDir.value = PATH.parse(fu.files[0].path).dir+ `${PATH.sep}image-compress`
console.log(targetDir.value)
} catch (error) {
console.debug('choice file err:', error)
}
};
const handlePictureCardPreview = file => {
console.log('file.url',file.url)
ElNotification({
title: "通知",
message: "预览图片功能暂不支持"
});
}
onBeforeUnmount(() => {
loadingInstance = null;
});
onMounted(() => {
ipcRenderer.on("compress-status", (event, arg) => {
ElNotification({
title: arg.success ? "成功" : "失败",
message: arg.success ? arg.msg : arg.reason,
type: arg.success ? "success" : "error"
});
loadingInstance.close();
if (arg.success) {
upload.value.uploadFiles = [];
fileList.value = [];
quality.value = 50;
}
});
});
return {
targetDir,
upload,
quality,
marks,
fileList,
maxFileNum,
handleExceed,
handleSubmit,
btnChange,
fileChange,
handlePictureCardPreview,
dialogVisible,
dialogImageUrl
};
}
};
</script>
transfromJPG.vue:略
transfromPNG.vue:略
5.项目配置
应用打包后,安装程序和图标配置:在项目根目录下创建vue.config.js文件(目前为windows下的应用打包配置)
module.exports = {
pluginOptions: {
electronBuilder: {
builderOptions: {
"appId": "com.example.app",
"productName": "i-compress",
"copyright": "Copyright ? 2021",
"directories": {
"output": "build"
},
"win": {
"icon": "./src/assets/icon.ico",
"target": [
{
"target": "nsis",
"arch": [
"x64",
]
}
]
},
"nsis": {
"oneClick": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": true,
"installerIcon": "./src/assets/icon.ico",
"uninstallerIcon": "./src/assets/icon.ico",
"installerHeaderIcon": "./src/assets/icon.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "Icompress",
},
}
},
},
}
6.应用打包
npm run electron:build
运行上面命令行后,会在项目根目录下生成build文件夹,内容如下(贪图方便,应用图标笔者借用xmind的图标作为应用图标)

安装后,应用桌面效果

五、应用下载与源码链接
应用下载链接:链接:https://pan.baidu.com/s/1iWFFo0GSPPYpdR87DZdMtQ 提取码:kjhx
项目源码链接:https://github.com/innagine/i-compress (希望可以给个star)
参考链接:https://www.electronjs.org/
参考链接:https://vue3js.cn/docs/zh/
参考链接:https://juejin.cn/post/6924521091914776584#heading-9
 有兴趣的小伙伴可以关注公众号和博客,公众号工作日更新前端知识,并且可以根据小伙伴讨论的感兴趣内容进行内容更新,博客随时解答问题
|