Electron-vue + Element-UI 制作图片压缩工具实战
话不多说,先放源码地址
本篇文章的大纲
-
认识 Electron
-
Electron-Vue 项目目录介绍
-
初始化项目,实现第一个小目标——拖拽上传图片
-
实现图片压缩和展示列表
-
LowDB 实现本地持久化
-
利用主进程和渲染进程通信,完成托拽图片到图标上传图片
-
编译和打包
-
拓展与思考,分享 GitHub 源码地址
一、认识 Electron
我想愿意进来看教程的同学都是对 Electron 已经有一些认识和了解,这边就引用 Electron 官网给出的一段话来介绍 Electron —— 如果你可以建一个网站,你就可以建一个桌面应用程序。 Electron 是一个使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架,它负责比较难搞的部分,你只需把精力放在你的应用的核心上即可。
Electron 集成了 Node.js 和 Chromium,让网页开发有了 Node 的原生能力,大大的提高了网页开发的能力,让应用不再枯燥乏味。
二、Electron-vue 项目目录介绍
此项目是基于 Electron-vue
实现的,下面来看看它是怎么创建项目以及介绍它的目录结构。
# 全局安装 vue-cli
npm install -g vue-cli
# 这一步可能会比较慢,因为初始化项目的时候,会下载一些外服的安装包
vue init simulatedgreg/electron-vue image-compress
# 进入项目并且运行
cd image-compress
yarn # or npm install
yarn run dev # or npm run dev
注意️,运行上面脚本的时候可能会有些慢,请耐心等待片刻或者是使用淘宝镜像 cnpm 来安装;Windows用户安装过程可能会比较艰辛,这边亲测一个安装教程有效;安装过程中会提示你一些包的安装以及一些环境的配置,我的选择项如下图所示
配置项:

Windows 用户注意一下,若是需要打 Windows 安装包,这里选择打包插件的时候会有两个分别是
electron-builder
和electron-packager
,这里建议选择后者。
项目目录结构:

目录分析:
.electron-vue
:项目的一些打包和编译的脚本,这个无需深究,若是有兴趣可以单独另外研究。
build
:里面存放的是打包完后的安装包,可以打 dmg 文件和 exe 文件支持 mac 和 win 。
src
:内含 main
和 renderer
,字面量理解, main
是主进程,renderer
是渲染进程,index.ejs
是渲染入口页面,相当于vue开发单页应用的入口页。
static
:放一些静态资源的文件夹
三、初始化项目,实现第一个小目标——拖拽上传图片
⚠️ 运行
yarn run dev
或者npm run dev
的时候,项目报错提示ReferenceError: process is not defined
,解决方法也很简单,在webpack.renderer.config.js
和webpack.web.config
两个脚本里的HtmlWebpackPlugin
插件里加上如下配置

顺利启动项目之后,如下图所示:

顺便安装一下 element-ui
,然后在 src
目录下的 main.js
全局引入,这样组件内部便可通过按需引入的方式单独引入组件。
# main.js
import Vue from 'vue'
import ElementUI from 'element-ui'
import App from './App'
import router from './router'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
components: { App },
router,
template: '<App/>'
}).$mount('#app')
个人习惯容器组件是会新建一个 containers
文件夹来存放,这边我也延续我个人的习惯,当然同学们自己有什么开发习惯也不必拘泥于这些小节;在 src
目录下新建文件夹 containers——>Compress——index.vue
# index.vue
<template>
<div id="compress">
<el-upload
class="upload-demo"
action=""
accept='image/*'
:on-change="fileUpload"
:show-file-list="false"
:auto-upload="false"
:multiple="true"
:drag="true"
list-type="picture-card">
<el-button size="small" type="primary">点击或拖拽上传</el-button>
</el-upload>
</div>
</template>
<script>
export default {
data: () => {
return {
list: [],
}
},
methods: {
fileUpload: function(file, fileList) {
console.log('file', file)
}
}
}
</script>
<style lang="less">
#compress {
header h1 {
text-align: center;
margin: 10px 0;
}
.filter {
padding: 10px 10px;
}
.el-upload {
display: block;
margin: 0 auto;
.el-upload-dragger {
width: 100%;
margin: 0 auto;
background: transparent;
border: none;
}
}
.el-upload--picture-card {
margin-top: 10px;
width: 98%;
height: 170px!important;
}
.demo-image {
display: flex;
flex-wrap: wrap;
.block {
width: 100px;
display: flex;
flex-direction: column;
margin: 10px 10px;
.el-image {
margin: 10px 0;
}
.demonstration {
text-align: center;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
}
.operation {
display: flex;
justify-content: space-around;
i {
cursor: pointer;
}
}
}
</style>
拖拽上传的实现,借助于 Element-UI 的
el-upload
组件,要注意的是,这边只支持上传图片进行压缩accept='image/*'
且支持多张和拖拽:multiple="true" :drag="true"
,这边把css样式全部给出,大家可以直接复制粘贴进去,个人习惯用less
,所以在项目中要安装less
和less-loader
这两个包,本课程不拘泥样式。
再修改一下路由配置:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'home',
component: require('@/containers/Compress').default
},
{
path: '*',
redirect: '/'
}
]
})
基础的骨架就已经搭建完毕了,上传或者拖拽图片到虚线框区域,就能拿到图片资源。效果图如下:

四、实现图片压缩和展示列表
市面上当然也是有很多图片压缩工具,最有名的莫过于 tinypng 熊猫压图,但是他给的接口是一个月 500 张,想多就要收钱,于是我找到了一款压缩效果还不错的 npm
包 lxz
,GitHub
有2.7k的 star,虽然说是目前不再维护,但是拿来用用还是可以的。下面贴出拿到上传的图片资源后,如何使用 lrz
包:
# fileUpload方法里增加lrz如下代码
fileUpload: function(file, fileList) {
const self = this;
console.log('file', file)
lrz(file.raw)
.then((rst) => {
// 处理成功会执行
console.log('rst', rst)
})
}
执行结果打印出来的 rst
如下:

- base64:处理完后图片的 base64
- file:处理后的 blob 对象
- origin:处理前的图片资源信息
通过这么我们就能知道压缩的比例,这张图片原来的大小是 132216 字节,压缩后的大小为 35926 字节,压缩率为 73% 左右,可以说还是挺高的。
其次大家可以查阅 lrz
文档,可以自定义压缩后的宽高以及压缩的质量系数,这里就不细说。
展示压缩后图片的列表以及下载图篇:
# 样式在上面的代码中已经全部放出
<div class="demo-image">
<div class="block" v-for="(img, index) in list" :key="index">
<span class="demonstration">{{ img.name }}</span>
<el-image
style="width: 100px; height: 100px; border: 1px solid #e9e9e9;"
:src="img.url"
alt="非图片资源"
fit="cover"
>
<div slot="error" class="image-slot"><span>非图片资源</span></div>
</el-image>
<div class="operation">
<el-tooltip class="item" effect="dark" content="下载图片" placement="top">
<i class="el-icon-upload2" @click="download(img.name, img.url)">
<a :href="img.file" :download="img.name">下载</a>
</i>
</el-tooltip>
<el-badge :value="img.proportion" class="badge"></el-badge>
</div>
</div>
</div>
<script>
import lrz from 'lrz'
export default {
data: () => {
return {
list: [],
visible: false
}
}
methods: {
fileUpload: function(file, fileList) {
const self = this;
console.log('file', file)
lrz(file.raw)
.then((rst) => {
// 处理成功会执行
console.log('rst', rst)
const { origin, fileLen, base64, file } = rst
const proportion = `${((fileLen / origin.size) * 100).toFixed(0)}%` // 压缩比例
self.list.push({
proportion: proportion,
name: origin.name,
url: base64,
file: URL.createObjectURL(file)
})
})
}
}
}
</script>
压缩完图片之后,通过 URL.createObjectURL
方法将 blob
文件转为链接, URL.createObjectURL
方法会根据传入的参数创建一个指向该参数对象的 URL
。这个 URL
的生命仅存在于它被创建的这个文档里. 新的对象 URL
指向执行的 File
对象或者是 Blob
对象。再绑定到 a
标签上,加上 download
属性,让其可以被下载到本地。然后再加上压缩比例显示到列表中,最后效果如下图所示:

五、lowdb实现本地持久化
每次重启客户端的时候,之前上传的图片就会丢失,这是因为只是把图片存到了内存里,而没有把图片存储到计算机本地,那么接下来就为大家安利一款比较好用的静态数据库 lowdb
。文档可以点进去自行学习,操作简单易学。下面我们对 lowdb
做一个二次封装,让使用更加方便,src
目录下新建文件夹 datastore
,新建文件 index.js
,代码如下:
# index.js
import Datastore from 'lowdb'
import FileSync from 'lowdb/adapters/FileSync'
import path from 'path'
import fs from 'fs-extra'
import { app, remote } from 'electron'
import LodashId from 'lodash-id'
// const APP = process.type === 'renderer' ? remote.app : app // 根据process.type判断是main还是renderer调用了该文件
const home = process.env.HOME || (process.env.HOMEDRIVE + process.env.HOMEPATH);
const STORE_PATH = `${home}/.upload_data` // 存到用户目录
// 开发环境下路径已经存在,而在生产环境下这个路径是没有的,所以会报错,这边要创建一个
if (!fs.pathExistsSync(STORE_PATH)) {
fs.mkdirpSync(STORE_PATH)
}
const adapter = new FileSync(path.join(STORE_PATH, '/data.json')) // 初始化lowdb读写的json文件名以及存储路径
const db = Datastore(adapter) // lowdb接管该文件
db._.mixin(LodashId) // 通过._mixin()引入
// 初始化数据
if (!db.has('imgList').value()) {
db.set('imgList', []).write()
}
const insert = (filename, data) => {
db.read().get(filename).insert(data).write()
}
const remove = (filename, by) => {
db.read().get(filename).remove(by).write()
}
const update = (filename, by, data) => {
db.read().get(filename).find(by).assign(data).write()
}
const find = (filename, data) => {
if (data) {
return db.read().get(filename).find(data).value()
} else {
return db.read().get(filename).value()
}
}
const removeAll = (filename) => {
db.read().get(filename).remove().write()
}
export default {
insert,
remove,
removeAll,
update,
find
} //暴露出去db
注意,️要安装
lowdb
、fs-extra
、lodash-id
,封装好之后,将增删改查方法抛出去供引入对象使用。
下面将封装好的方法挂载到Vue的原型链上,全局便可使用,代码如下:
import Vue from 'vue'
import axios from 'axios'
import ElementUI from 'element-ui'
import App from './App'
import router from './router'
import 'element-ui/lib/theme-chalk/index.css'
import db from '../datastore'
Vue.use(ElementUI)
if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
Vue.prototype.$db = db
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
components: { App },
router,
template: '<App/>'
}).$mount('#app')
使用方法
this.$db.xxx
通过静态数据库获取数据:
# Compress/index.vue
mounted() {
this.getImgList()
},
methods: {
getImgList() {
this.list = [].concat(this.$db.find('imgList')) || [] // 数组单纯的替换是无法触发视图的更新的
console.log(this.list);
},
handleDeleteAll() {
this.$db.removeAll('imgList')
this.$message({
message: '删除成功',
type: 'success',
center: true
})
this.visible = false
this.getImgList()
},
handleDelete(id) {
this.$db.remove('imgList', { id: id })
this.$message({
message: '删除成功',
type: 'success',
center: true
})
this.getImgList()
},
fileUpload: function(file, fileList) {
const self = this;
console.log('file', file)
lrz(file.raw)
.then((rst) => {
// 处理成功会执行
console.log('rst', rst)
const { origin, fileLen, base64 } = rst
const proportion = `${((fileLen / origin.size) * 100).toFixed(0)}%` // 压缩比例
self.$db.insert('imgList',{
proportion: proportion,
name: origin.name,
url: base64,
file: URL.createObjectURL(file)
})
self.getImgList()
})
},
}
模板上也添加相应的方法,代码如下:
<template>
<div id="compress">
<div class="filter">
<el-button type="primary" @click="handleDeleteAll" icon="el-icon-delete">一键全删</el-button>
</div>
<el-upload
class="upload-demo"
action=""
accept='image/*'
:on-change="fileUpload"
:show-file-list="false"
:auto-upload="false"
:multiple="true"
:drag="true"
list-type="picture-card">
<el-button size="small" type="primary">点击或拖拽上传</el-button>
</el-upload>
<div class="demo-image">
<div class="block" v-for="(img, index) in list" :key="index">
<span class="demonstration">{{ img.name }}</span>
<el-image
style="width: 100px; height: 100px; border: 1px solid #e9e9e9;"
:src="img.url"
alt="非图片资源"
fit="cover"
>
<div slot="error" class="image-slot"><span>非图片资源</span></div>
</el-image>
<div class="operation">
<a class="download-a" :href="img.file" :download="img.name"><i class="el-icon-upload2"> </i></a>
<el-badge :value="img.proportion" class="badge"></el-badge>
<el-tooltip class="item" effect="dark" content="删除图片" placement="top">
<i v-on:click="handleDelete(img.id)" class="el-icon-delete"></i>
</el-tooltip>
</div>
</div>
</div>
</div>
</template>
本地数据库(json文件)存储在指定的路径下,卸载软件重新安装,数据不会丢失
最后的效果图如下所示:

六、利用主进程和渲染进程通信,完成托住到图标上传图片
首先需要修改主进程的脚本,代码如下:
import { app, BrowserWindow, Menu, Tray } from 'electron'
import db from '../datastore'
/**
* Set `__static` path to static files in production
* https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html
*/
if (process.env.NODE_ENV !== 'development') {
global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
}
let mainWindow
const winURL = process.env.NODE_ENV === 'development'
? `http://localhost:9080`
: `file://${__dirname}/index.html`
let tray = null
function createWindow () {
/**
* Initial window options
*/
mainWindow = new BrowserWindow({
height: 563,
useContentSize: true,
width: 1000
})
mainWindow.loadURL(winURL)
mainWindow.on('closed', (event) => {
mainWindow = null
})
mainWindow.on('close', (event) => {
app.quit()
})
const menubarPic = process.platform === 'darwin' ? `${__static}/upload.png` : `${__static}/upload.png`
tray = new Tray(menubarPic)
const contextMenu = Menu.buildFromTemplate([
{ label: '退出', click: () => {
mainWindow.destroy()
tray.destroy()
}},
])
tray.setToolTip('one piece')
tray.setContextMenu(contextMenu)
tray.on('click',function(){
mainWindow.show();
})
mainWindow.on('close',(e) => {
app.quit()
})
tray.on('drop-files', async (event, files) => {
console.warn('files', files);
mainWindow.webContents.send('insert-success', files);
// 成功获取资源后通知渲染进程,并且带上图片资源
})
}
app.on('ready', createWindow)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (mainWindow === null) {
createWindow()
}
})
此时拿到的 files
是一个本地路径的数组,当渲染进程内接收到 insert-success
通知之后,回调函数内能拿到 files
,然后将 files
里的内容循环遍历,将本地路径转换为 base64
格式,再将 base64
转化为 File
格式,代码如下:
# Compress/index.vue
mounted() {
const self = this
self.getImgList()
ipcRenderer.on('insert-success', (event, files) => {
for (let item in files) {
const name = files[item].split('/')[(files[item].split('/').length - 1)]
const result = base64Img.base64Sync(files[item]);
const file = this.dataURLtoFile(result, name)
lrz(file)
.then((rst) => {
// 处理成功会执行
console.log('rst', rst)
const { origin, fileLen, base64, file } = rst
const proportion = `${((fileLen / origin.size) * 100).toFixed(0)}%` // 压缩比例
self.$db.insert('imgList',{
proportion: proportion,
name: origin.name,
url: base64,
file: URL.createObjectURL(file)
})
self.getImgList()
})
}
});
},
methods: {
dataURLtoFile(dataurl, filename) {
var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
while(n--){
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, {type:mime});
},
}
注意,新增
base64-img
包,安装之后引入import base64Img from 'base64-img'
效果如下:

截屏无法截取电脑的导航栏,所以这边就是把图片拖拽到最上面的
logo
图标,实现上传。
七、编译和打包
在根目录执行 yarn run build
或者 npm run dev
命令,项目将会自动打包到 build
目录下

可以自己设计
icons
图标,制作一个属于自己的独一无二的小项目
八、拓展与思考
1、上传图片之前可以通过一个 Slider 滑块组件去控制压缩质量系数的大小,从而控制图片压缩后的质量情况。
2、是否能做到压缩完图片之后上传到 CDN 功能。
3、能否做到在线更新项目,比如有新功能更新,客户端能做到静默更新。