Electron-Vue 实战图片压缩小工具🔧

2,256 阅读6分钟

Electron-vue + Element-UI 制作图片压缩工具实战

话不多说,先放源码地址

源码地址

本篇文章的大纲

  1. 认识 Electron

  2. Electron-Vue 项目目录介绍

  3. 初始化项目,实现第一个小目标——拖拽上传图片

  4. 实现图片压缩和展示列表

  5. LowDB 实现本地持久化

  6. 利用主进程和渲染进程通信,完成托拽图片到图标上传图片

  7. 编译和打包

  8. 拓展与思考,分享 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-builderelectron-packager,这里建议选择后者。

项目目录结构:

目录分析:

.electron-vue:项目的一些打包和编译的脚本,这个无需深究,若是有兴趣可以单独另外研究。

build:里面存放的是打包完后的安装包,可以打 dmg 文件和 exe 文件支持 mac 和 win 。

src:内含 mainrenderer,字面量理解, main 是主进程,renderer 是渲染进程,index.ejs 是渲染入口页面,相当于vue开发单页应用的入口页。

static:放一些静态资源的文件夹

三、初始化项目,实现第一个小目标——拖拽上传图片

⚠️ 运行 yarn run dev 或者 npm run dev 的时候,项目报错提示 ReferenceError: process is not defined,解决方法也很简单,在 webpack.renderer.config.jswebpack.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,所以在项目中要安装 lessless-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 张,想多就要收钱,于是我找到了一款压缩效果还不错的 npmlxzGitHub 有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

注意,️要安装 lowdbfs-extralodash-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、能否做到在线更新项目,比如有新功能更新,客户端能做到静默更新。