前端烂笔头专注前端开发
首页/Electron-vue + Element-UI 制作图片压缩工具实战/
Electron-vue + Element-UI 制作图片压缩工具实战
2019-11-29 1092

一、认识 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、能否做到在线更新项目,比如有新功能更新,客户端能做到静默更新。

image-compress源码地址

大胡子程序员,专注于WEB和移动前端开发

如果文章对你有帮助,可以给作者一点鼓励。这将是作者持续输出的动力.

系统由 Node + React + Ant Desgin 驱动
浙ICP备19047249号-1 邮箱地址:xianyou1993@qq.com