关于前端:Chrome插件切图压缩工具

34次阅读

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

作者:lkl

前言

在前端我的项目开发中,尤其是流动我的项目,大量应用未压缩的图片必将会影响页面关上速度,升高用户体验。因而,咱们须要对下载的切图进行压缩解决。常见的图片压缩工具有 TinyPNGPP 鸭,但这两款软件是免费的,并且不反对定制化。应用这些软件压缩图片的过程更是简单繁琐,如果有一款工具能够在下载切图时就帮忙咱们压缩图片,或间接提供压缩后的图片地址,那将会大大提高以后的工作效率。本文将介绍实现这样一个切图压缩工具的关键技术点。

获取原图片

罕用的设计稿软件有两个,蓝湖和 Figma。这里用蓝湖作例简述如何获取原图。首先蓝湖是一个网页,须要用 Chrome 关上。这时候就必须祭出 F12 这个大杀器了,间接调试源码来定位下载操作的走向。会发现在最终下载图片的代码块中有以下一段。

这里的 t 变量就是一个 a 标签,通过调用 dispatchEvent 办法来触发 click 事件进行文件下载。晓得了下载方式,下一步就是如何去拦挡它。间接上原型大法,把 dispatchEvent 办法给重写以便拿到 a 标签实例,来获取要下载的文件信息。

const originDispatchEvent = EventTarget.prototype.dispatchEvent;
Object.defineProperty(HTMLAnchorElement.prototype, 'dispatchEvent', {
    writable: true,
    configurable: true,
    enumerable: true,
    value: function (event) {
        const nodeName = this.nodeName;
        const href = this.href;
        const filename = this.download;
        if (nodeName === 'A' && filename && /^blob:/.test(href)) {console.warn(filename, href);
            return false;
        }
        return originDispatchEvent.apply(this, [event]);
    }
});

把以上代码输出到控制台,点击下载图片就会看到这样的日志输入。至此便能拿到下载的切图数据了。

插件注入脚本

有了能够拦挡下载数据的脚本,那如何把它利用起来,实现主动注入呢?这就必须应用到 Chrome 插件了。能够应用其提供的 scripting_api 实现。

function inject(eventName) {
    const originDispatchEvent: Function = EventTarget.prototype.dispatchEvent;
    Object.defineProperty(HTMLAnchorElement.prototype, 'dispatchEvent', {
        writable: true,
        configurable: true,
        enumerable: true,
        value: function (event) {
            const nodeName = this.nodeName;
            const href = this.href;
            const filename = this.download;
            if (nodeName === 'A' && filename && /^blob:/.test(href)) {
                // ...
                return false;
            }
            return originDispatchEvent.apply(this, [event]);
        }
    });
}

// 在网页刷新后,注入拦挡脚本
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {if (tab.status === 'complete' && /^https?/.test(tab.url || '')) {
          // 在指定的 tab 页下执行函数
        chrome.scripting.executeScript({
            func: inject,
            target: {tabId},
            world: 'MAIN',
            args: [SITE_DOWN_IMAGE]
        }).catch((err) => {console.error(err);
        });
    }
});

须要留神的是注入时配置 world: 'MAIN' 是必须的,否则注入的脚本将在隔离环境中运行,就无法访问页面上的 JS 环境了。

拦挡到下载的切图数据后,能够通过 postMessage 发送给插件的内容脚本。上面就是在内容脚本中来实现图片压缩的能力。

压缩能力实现

那要如何实现一个能够在网页中应用的压缩工具呢?先看下现有的压缩工具 TinyPNG 和 PP 鸭,一个是网页一个是本地 App。本地 App 必定是不行了,TinyPNG 实践上是没问题的,其有提供压缩接口。但因为它是免费的且须要上传,也没法间接应用。通过查问材料,理解到业界开源的 PNG 图片压缩工具有 pngquantadvpngoxipng 等,通过尝试,选取 pngquantadvpng 来进行 PNG 图片的压缩。

  • pngquant

这是一个命令行工具,用于 PNG 图片的有损压缩。其转换后的图片能够升高高达 70% 的大小,并且保留了残缺的 alpha 透明度,生成的图像与所有 Web 浏览器和操作系统兼容。具备以下特点:

  1. 应用矢量量化算法的组合,生成高质量的调色板
  2. 与规范的 Floyd-Steinberg 相比,独特的自适应抖动算法为图像减少了更少的乐音

援用自 pngquant 官网简介

  • advpng

这是一个 PNG 图片的无损压缩库,通过移除 PNG 图片中的辅助块,整合 IDAT 数据块和应用 7zip Deflate 进行更高比例的压缩实现。

那如何把他们用到 Web 上呢?那必须要是 WebAssembly 呀。首先把这两个库整合到一起,应用 Emscripten 编译成 Wasm,提供接口以便前端调用。这个过程中你可能会遇到这些问题:

  1. 内存文件转换

因为 pngquant 是一个命令行工具,其压缩操作都是基于磁盘文件读写的,然而在 WebAssembly 传参时须要的是字节数组,都是在内存中的。就须要对源码进行肯定的革新。次要是将 fopen 替换为 fmemopenopen_memstream 实现在内存数据中进行 FILE 的操作。如读取文件批改。

FILE *infile;
if ((infile = fopen(filename, "rb")) == NULL) {fprintf(stderr, "error: cannot open %s for reading\n", filename);
  return READ_ERROR;
}
// 批改为如下
if ((infile = fmemopen(file_buffer, file_size, "rb")) == NULL) {return READ_ERROR;}
  1. 内置 libpng 和 zlib 包

在通过 emcmake 构建时,会发现无奈应用零碎装置的 libpngzlib 动静库。须要将这两个库的源码下载到我的项目中,一起进行编译。

...
file(GLOB PNG_SOURCE libpng/*.c)
file(GLOB ZLIB_SOURCE zlib/*.c)
...
add_library(${PROJECT_NAME} STATIC pngquant.c rwpng.c ${PNG_SOURCE} ${ZLIB_SOURCE} ${QUA_SOURCE} ${ADVPNG_SOURCE})

在内容脚本中,就能间接通过这个 Wasm 模块来实现图片的压缩了。在蓝湖中,内容脚本间接应用 Wasm 时并不会有任何阻力,然而放到 figma 中就会被内容安全策略所禁止。因为 figma 在响应时就间接设置了内容安全策略,这时候就要借助 Chrome 插件提供的 sandbox 能力,通过在页面中新增 iFrame 页面来实现。

到此该工具的根本能力就都曾经实现了,能够把要下载的切图数据拿到,通过内容脚本压缩后再下载。为了更方便使用切图,下一步就是要把压缩后的图片上传到 CDN 并提供 URL 来进行复制。

切图上传

到这里就简略起来了,以上曾经能够在内容脚本中获取到压缩后的切图数据,上面就是把它上传到一个适合的图床平台了。如应用七牛云,惟一可能会遇到的问题是上传接口不反对跨域。这是就须要利用 Chrome 插件的主机权限,把用的的接口配置上。

// manifest.json
{
  ...
  "host_permissions": [
    "https://rsf-z0.qiniuapi.com/*",
    "https://*.qiniu.com/*"
  ]
  ...
}

对于如何间接在前端上传文件到七牛,能够参考七牛开发者文档。这里惟一麻烦的可能是上传凭证的生成,七牛官网举荐的是在服务端生成凭证,前端 SDK 就没间接提供生成办法。能够参考生成算法在前端实现。

import {urlSafeBase64Encode} from 'qiniu-js';
import HmacSHA1 from 'crypto-js/hmac-sha1';
import encBase64 from 'crypto-js/enc-base64';

function getUploadToken(bucket, secretKey) {
  const returnBody = {key: '$(key)',
    hash: '$(etag)',
    name: '$(fname)',
    size: '$(fsize)',
    width: '$(imageInfo.width)',
    height: '$(imageInfo.height)'
  };
  const putPolicy = JSON.stringify({
    scope: bucket,
    deadline,
    returnBody: JSON.stringify(returnBody)
  });

  const encodedPolicy = urlSafeBase64Encode(putPolicy);

  const hash = HmacSHA1(encodedPolicy, secretKey);
  const encodedSigned = hash.toString(encBase64);

  return this.accessKey + ':' + safe64(encodedSigned) + ':' + encodedPolicy;
}

切图治理

如果你应用的 CDN 有现成的列表接口,那间接调用就行。但如七牛并没有提供好用的列表接口,为了治理上传的图片列表,就须要在前端保留切图列表,这时你就要抉择一个适合的存储。你能想到的可能会有 localStorageCookieIndexedDB,可能还会有 chrome.stroage。思考到切图列表的性质,其须要较大的存储空间并且要能不便的进行分页查问,那这样应用 IndexedDB 作为存储将会是一个更好的抉择。

上面就来用 IndexedDB 实现切图治理的性能,不便查看已上传的切图列表。首先明确两个接口定义:

  • 新增已上传的切图
  • 分页查问已上传的切图
// 存储的数据结构
interface ImageEntry {
  name: string
  width: number
  height: number
  size: number
  cdnUrl: string
  uploadTime: number
}

// 接口定义
interface IImageDB {add(...images: ImageEntry[]): Promise<void>
  findPage(page: number, limit: number): Promise<ImageEntry>
}

借助开源库 dexie 实现起来也很简略

import Dexie from 'dexie';

export default class ImageDB {constructor(name = 'zimagedb') {let db = new Dexie(name);
        db.version(1).stores({images: '++_id,name,cdnUrl'});
        this.db = db;
    }

    async add(...datas) {for (const item of datas) {await this.db.images.add(item);
        }
    }

    async findPage(page = 0, limit = 10) {
        const offset = page * limit;
        return this.db.images.limit(limit).offset(offset).toArray();}
}

为了能让 IndexedDB 贮存惟一,你应该把它放在 Chrome 扩大的背景页内,再通过音讯通信在内容脚本中应用。

// 如 background.js 监听音讯
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {if (message.type === 'getImages') {db.findPage(message.page).then(images => {sendResponse({ success: true, images});
    }).catch(err => {...});
    return true;
  }
});

// 在 content.js 中查问切图列表
function getImages(page = 0) {return new Promise((resolve, reject) => {chrome.runtime.sendMessage({type: 'getImages', page}, response => {if (response?.success) {resolve(response.images);
      } else {reject(...);
      }
    });
  });
}

总结

以上简述了该 切图压缩工具 的一些关键技术点。波及到有 Chrome 扩大开发,JavaScript 原型的利用,WebAssembly 开发,IndexedDB 存储等。尤其是 WebAssembly 和 IndexedDB 这让本需依赖服务端能力实现的一些性能在前端也能很好的实现,给前端带来了更多的可能性。

参考资料

  • https://developer.chrome.com/docs/extensions/mv3/
  • https://github.com/kornelski/pngquant
  • https://www.advancemame.it/doc-advpng.html
  • https://webassembly.org/
  • https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
  • https://developer.qiniu.com/kodo/1283/javascript
  • https://dexie.org/

本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

正文完
 0