关于前端:扔掉-Electron-后我用-Tauri-Rust-Wasm-开发了一个图片压缩应用

4次阅读

共计 7443 个字符,预计需要花费 19 分钟才能阅读完成。

前言

作为前端开发人员,你是否受够了每次 UI 给到的切图都大到让你想提桶跑路的懊恼,在你打算提桶之前,请先留步,看完这篇文章再跑不迟~

先露个脸

我管它叫 Image Tiny

大抵看一下压缩率,png 和 jpg 格局根本都能达到 80% 左右,gif 能达到 11% 左右。

心动么💓,是不是还不错。

性能介绍

  • 反对的图片格式包含 png、jpg、gif
  • 反对 5M 以上图片压缩
  • 不依赖网络,不依赖服务器,基于客户端本地压缩
  • 反对拖拽图片文件进行压缩
  • 反对压缩品质参数调整
  • 反对窗口置顶
  • 反对单张图片保留
  • 反对一键打包,把列表中所有图片打一个 zip 包保留到本地

压缩率比照 TinyPNG

TinyPNG 这个在线图片压缩网站想必作为前端开发人员肯定很相熟,它反对 png,jpg,webp 格局的图片压缩,而且压缩率也很不错,用过它的小伙伴肯定不在少数。因而 Image Tiny 就抉择和 TinyPNG 进行比照。

以下是 4 张 png、2 张 jpg、2 张 gif 图片的压缩数据比照:

对于 png 格局的图片,Image Tiny 的压缩率根本能达到 TinyPNG 的程度;

对于 jpg 格局的图片,经测试把 Image Tiny 压缩品质调整到 80% 以下,是能够超过 TinyPNG 的;

对于 gif 格局的图片,不好意思,TinyPNG 不反对;

对于 5 MB 及以上的图片,不好意思,TinyPNG 也不反对;

综合来看,咱们的 Image Tiny 还是很不错的么,而且还完全免费、不依赖网络、不依赖服务器。

技术实现

压缩外围

借助 libimagequant、libpng、libjpeg、gifsicle 这几个 C 语言的库实现图片压缩,
应用 Emscripten SDK (emsdk) 将 C 代码编译为 wasm 文件,供浏览器端调用。

具体压缩的代码此处就不展现了,本我的项目有开源的打算,到时候大家天然能够看到。

利用框架

Tauri + Rust + Vue3.0 + Vite

如果对 Tauri 还不相熟,能够翻看之前发表的一篇文章:

掘金链接🔗:扔掉 Electron,拥抱基于 Rust 开发的 Tauri

微信公众号链接🔗:扔掉 Electron,拥抱基于 Rust 开发的 Tauri

代码小窥

1. 窗口置顶性能

import {window} from '@tauri-apps/api';

// 窗口置顶
function handleWindowTop() {let curWin = window.getCurrent();
  if (datas.winTop === '窗口置顶') {curWin.setAlwaysOnTop(true);
    datas.winTop = '勾销置顶';
  } else {curWin.setAlwaysOnTop(false);
    datas.winTop = '窗口置顶';
  }
}

@tauri-apps/api 引入 window api,通过 window.getCurrent 办法获取到以后窗口实例,实例上有一个 setAlwaysOnTop 办法,通过参数 true\false 能够管制窗口置顶或者勾销置顶。

至于为什么要给利用增加窗口置顶🔝性能,这里先挖个坑,前面会填上。

2. 利用菜单项及快捷键增加

main.rs

use tauri::{Menu, MenuItem, Submenu};

fn main() {
  let submenu_main = Submenu::new("ImageTiny".to_string(),
    Menu::new()
      .add_native_item(MenuItem::Minimize)
      .add_native_item(MenuItem::Hide)
      .add_native_item(MenuItem::Separator)
      .add_native_item(MenuItem::CloseWindow)
      .add_native_item(MenuItem::Quit),
  );

  let menu = Menu::new().add_submenu(submenu_main);

  tauri::Builder::default()
    .menu(menu)
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

引入 Menu, MenuItem, Submenu,通过 Submenu::new() 办法新建一个菜单项,调用 add_native_item 办法增加原生的 MenuItem 项在其中,而后新建一个 Menu,通过 Menu::new().add_submenu() 办法将 Submenu 增加到菜单中,最初通过 tauri::Builder::default().menu() 将菜单注册到利用。

3. 拖拽图片进行压缩性能

<div
  class="middle-con"
  @dragenter="dragenterEvent"
  @dragover="dragoverEvent"
  @dragleave="dragleaveEvent"
  @drop="dropEvent"
>
  ...
</div>
function dragenterEvent(event) {event.stopPropagation();
  event.preventDefault();}
function dragoverEvent(event) {event.stopPropagation();
  event.preventDefault();}
function dragleaveEvent(event) {event.stopPropagation();
  event.preventDefault();}
function dropEvent(event) {event.stopPropagation();
  event.preventDefault();
  const files = event.dataTransfer.files;
  displayChsFile(files);
}

应用的是浏览器的 drag 事件来监听拖拽,在 drop 事件中拿到文件信息。

drop 事件返回的参数是一个 DragEvent 对象,该对象上有一个 dataTransfer 字段,该字段上面有一个 files 字段 存储的就是咱们须要的 FileList,外面就是咱们拖拽的 File Object
获取到 FileList 后,传递给 displayChsFile 办法进行遍历压缩图片文件。

tips:须要禁用掉 tauri 提供的文件拖拽事件,代码如下

tauri.conf.json

{
  ...
  "tauri": {
    "windows": [
      {
        ...
        "fileDropEnabled": false,
        ...
      }
    ],
  }
  ...
}

4. 图片文件压缩性能

为了不便公司其余我的项目接入图片压缩性能,咱们将这块外围代码封装了一个公有 npm 插件,不便各个我的项目的接入,上面大抵看一下代码:

import pngtiny from '../plugins/pngtiny'

/**
 * @description: 图像压缩
 * @param {File} file 原始 File 文件对象
 * @param {Number} quality 压缩品质,10-90,倡议 80
 * @return {Promise<File>} 压缩过的 File 文件对象
 */
const imageTiny = (file, quality = 80) => {pngtiny.run()
  return new Promise((resolve, reject) => {
    try {const reader = new FileReader()
      reader.readAsArrayBuffer(file)
      reader.onload = function(e) {const fcont = new Uint8Array(e.target.result)
        const fsize = fcont.byteLength
        const dataptr = pngtiny._malloc(fsize)
        const retdata = pngtiny._malloc(4)
        pngtiny.HEAPU8.set(fcont, dataptr)
        pngtiny._tiny(dataptr, fsize, retdata, quality)
        let rdata = new Int32Array(pngtiny.HEAPU8.buffer, retdata, 1)
        const size = rdata[0]
        rdata = new Uint8Array(pngtiny.HEAPU8.buffer, dataptr, size)
        const blob = new Blob([rdata], {type: file.type})
        let outFile = new File([blob], file.name, {type: file.type})
        if (outFile.size === 0) {outFile = file}
        resolve(outFile)
        pngtiny._free(dataptr)
        pngtiny._free(retdata)
      }
    } catch (error) {reject(error)
    }
  })
}
export default imageTiny

通过 emsdk 将 C 代码编译为 WebAssembly 时,会生成一个 .wasm 文件和一个 .js 的胶水代码,这个 js 胶水代码会解决 wasm 文件,咱们只须要应用导出的 pngtiny 对象即可,下面蕴含了咱们须要应用的办法。

imageTiny 办法输出参数为:

  • file:File 文件对象
  • quality:压缩品质

输入为:

  • 压缩过的 File 文件对象

5. 保留单个图片性能

import {writeBinaryFile} from '@tauri-apps/api/fs';
import {path, dialog} from '@tauri-apps/api';
// 保留单个图片
async function handleSaveFile(file) {
  datas.tip = '图片保留中...';
  const basePath = await path.downloadDir();
  let selPath = await dialog.save({defaultPath: basePath,});
  selPath = selPath.replace(/Untitled$/, '');
  const reader = new FileReader();
  reader.readAsArrayBuffer(file.data);
  reader.onload = function (e) {let fileU8A = new Uint8Array(e.target.result);
    writeBinaryFile({contents: fileU8A, path: `${selPath}${file.data.name}` });
    datas.tip = '图片保留胜利';
  };
}

引入 tauri 的 api:writeBinaryFile、path、dialog

应用 dialog.save() 办法关上一个文件保留弹框,供用户抉择保留门路,
该办法有一个 defaultPath 参数来设置默认的保留门路,办法返回值就是最终要保留的文件门路。咱们抉择零碎的默认下载门路作为默认保留门路,能够通过 path.downloadDir() 办法来获取零碎默认的下载门路。

须要特地留神,因为文件保留对话框中默认提供的文件名是 Untitled,如果开发者没有批改或删除,那么 dialog.save() 办法返回的门路中就会蕴含一级 Untitled 目录,咱们能够手动截取掉。

handleSaveFile 办法承受到的参数是一个 File Object,外面蕴含了以后图片文件的根本信息以及咱们自定义的一些信息,如下图:

通过 FileReader api 读取到图片文件的 Uint8Array 数据,

通过 tauri 提供的 writeBinaryFile api,将文件写入本地。writeBinaryFile 承受一个对象参数,蕴含 contentspath 字段,
contents 就是文件的 Uint8Array 数据,path 就是要保留的门路。

为了使用者的不便,咱们将文件名拼接到了门路上,这样用户就不须要在保留文件对话框中手动填写文件名了,间接保留即可。

6. 一键保留性能

import JSZip from 'jszip';

// 一键打包保留
async function handleDownloadAll() {
  const len = datas.imgList.length;
  if (len === 0) {return;}
  datas.tip = 'zip 保留中...';
  const zip = new JSZip();
  for (let i = 0; i < len; i++) {zip.file(datas.imgList[i].name, datas.imgList[i].data);
  }
  const date = new Date();
  const mon = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '_';
  const day = date.getDate() + '_';
  const hour = date.getHours() + '_';
  const min = date.getMinutes();

  const basePath = await path.downloadDir();
  let selPath = await dialog.save({defaultPath: basePath,});
  selPath = selPath.replace(/Untitled$/, '');

  zip.generateAsync({type: 'blob'}).then((content) => {let file = new FileReader();
    file.readAsArrayBuffer(content);
    file.onload = function (e) {let fileU8A = new Uint8Array(e.target.result);
      writeBinaryFile({contents: fileU8A, path: `${selPath}IMG_${mon + day + hour + min}.zip` });
      datas.tip = 'zip 保留胜利';
    };
  });
}

咱们借助 jszip 插件,来将文件打为 zip 包。

首先咱们应用 new JSZip() 来新建一下 zip 实例,遍历压缩过的文件列表,调用 zip.file() 办法将文件增加进去,file() 办法接管两个参数,一个是文件名,一个是文件的内容数据。

接着通过 dialog.save() api 调用文件保留弹框,拿到须要保留到的门路,调用 zip.generateAsync 办法生成 zip 包的 ArrayBuffer 数据,通过 writeBinaryFile api,将文件写入本地。

tips:zip 包的命名形式抉择了获取年月日信息来命名。

踩过的坑

1.tauri 版本的抉择

这能够说是最大的一个坑,何出此言,咱们接着往下看。

起初在我的项目搭建初期,我满心期待的抉择了最新版本的 tauri,可是当性能开发到文件系统相干的 api 时,遇到了一个重大的问题:Unhandled Promise Rejection: cannot traverse directory。tauri 的 fs 相干的 api 不能读取任意门路下的文件。什么?这还怎么欢快的游玩?明明之前有用过还是能够的,难道是因为版本问题?我带着疑难和 tauri 社区进行了沟通。最终失去的答案是:处于平安思考,在新版本中 fs 相干的 api 做了平安限度,只能拜访 tauri 提供进去的几个零碎门路下的文件。

这必定行不通,咱们不可能让用户压缩个图片还得当时把图片放到指定目录下能力拜访吧。

于是便向 tauri 社区反馈了这个问题,他们示意后续版本会打算将用户抉择的门路增加到白名单里,来绕过这个限度。

失去了答案后,解决方案就是回退 tauri 版本,于是只能退回到了旧版,具体版本信息如下:

package.json

"@tauri-apps/api": "=1.0.0-beta.8",
"@tauri-apps/cli": "=1.0.0-beta.10",

Cargo.toml

[build-dependencies]
tauri-build = {version = "=1.0.0-beta.4"}

[dependencies]
serde = {version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauri = {version = "=1.0.0-beta.8", features = ["api-all"] }

如果有其余小伙伴在应用 tauri 时也遇到了文件拜访限度问题,能够参照回退到以上版本。

2. 文件上传形式的抉择

总的来说有三种形式,咱们一一来看。

一、input 上传形式,被 tauri 给禁止掉了,基本打不开文件抉择对话框

二、tauri 提供的全局拖拽事件

import {listen} from '@tauri-apps/api/event';
listen('tauri://file-drop', async (event) => {console.log(event);
});

通过监听 tauri 提供的 tauri://file-drop 事件,能够拿到事件的 event 对象,外面会返回文件门路。没错,仅仅返回了文件门路列表,咱们还须要遍历文件门路列表应用 fs 相干 api 再来读取每个门路对应的文件。此流程长且耗时,体验及其不佳。

三、drap & drop 事件

通过监听 drop 事件,可间接获取到上传的 FileList 对象,外面蕴含有文件的具体信息,堪称方便快捷,所以这个计划也是本文采取的计划。

填个坑:

为什么增加窗口置顶性能?

因为 Image Tiny 的图片上传形式是拖拽上传,如果没有窗口置顶性能的话,很容易被其余利用遮挡,势必极大升高应用体验。有了置顶性能,使用者就无需放心遮挡的问题了。

安装包奉献

🔗 代码仓库

🔗 安装包地址

欢送大家下载安装应用,好用的话别忘了给文章点个赞👍🏻,如果能 转发 就更好了,以便更多的小伙伴能够看到。

总结

整个利用差不多消耗了我近一周工夫才开发实现,期间也是踩坑有数,一直的摸爬滚打,寻找问题解决方案。然而当开发实现时,心田还是十分喜悦的,也心愿后续能应用 Tauri 开发进去更多的小工具给大家带来一点便当~

更多精彩请关注咱们的公众号「百瓶技术」,有不定期福利呦!

正文完
 0