整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

canvas基础及图片缩放的实现

础部分

坐标系

画布坐标、屏幕坐标的概念

    • 屏幕坐标,绝对坐标,类似于css中的绝对定位
    • 画布坐标,类似于css中的相对定位,还要考虑缩放

几何变换:平移、缩放、旋转

    • 平移,位置移动,形状、相对位置不变
    • 缩放,位置(相对屏幕坐标)和大小都发生变化
    • 旋转,位置旋转,形状、相对位置不变

Canvas 中的所有几何变换针对的不是绘制的图形,而是针对画布本身,也就是说,当移动、缩放、旋转画布之后,新的坐标系只对新的操作生效

参考:

  • Canvas 几何变换 - Canvas 基础教程 - 简单教程,简单编程
    • Canvas 平移 translate() - Canvas 基础教程 - 简单教程,简单编程
    • Canvas 缩放 scale() - Canvas 基础教程 - 简单教程,简单编程
    • Canvas 旋转 rotate() - Canvas 基础教程 - 简单教程,简单编程

绘图步骤

基础绘图三步法

  1. 获取canvas对象
  2. 获取上下文环境对象context
  3. 开始绘制图形。

示例代码

const canvas = document.getElementById("canvas");
const context = convas.getContext("2d");
context.fillRect(100, 100, 50, 50);

通用绘图步骤

  1. 保存画布(状态),context.save();
  2. 画布操作,context.transform(叠加) 或者 context.setTransform(不叠加)
  3. 设置样式,绘制图形
  4. 恢复画布(状态),context.restore();

图形变换基本操作

  1. 清空画布,context.clearRect
  2. 保存状态,context.save
  3. 画布操作,doTransform
  4. getShapeList and forEach
  5. 恢复状态,context.restore


Canvas性能

Canvas的绘制和html的绘制是不一样的,html的绘制是增量的,当变化时,只会重新绘制变化的部分,没有变化的部分是不会重新绘制的,但是canvas不一样,每次都是全量绘制的,如果一个canvas里有很多图形,当改变一个图形时,需要重新绘制所有图形才可以(当然,可以用clearRect擦除部分区域,但一般很少这么用)。

了解canvas的绘制规则之后,就很容易发现性能问题,如果canvas上绘制了大量的图形(成千上万个),每次重绘就需要很长的时间,如果重绘的频率很高,那么就会有性能问题



那么如何解决这个问题呢,目前有以下几种方案

  1. 使用图层
  2. 使用临时图层
  3. 使用webworker或wasm
  4. 使用webgl

使用图层

图层的概念来自于PS,每一个图层都是一个canvas,既然在一个canvas上绘制太多图形会有性能问题,那么就分几个图层,每次仅重新绘制其中一个图层,每个图层的图形都不会很多,那么即使重绘的频率很高,也不会有性能问题。图层的概念图如下:

这里用背景颜色只是示意,实际上图层都是透明

代码实现

用一个父元素作为容器,把所有的元素设置成一样的宽高并放在里面重叠。

<div class="container">
    <canvas width="500" height="500"></canvas>
    <canvas width="500" height="500"></canvas>
    <canvas width="500" height="500"></canvas>
    <canvas width="500" height="500"></canvas>
    <canvas width="500" height="500"></canvas>
</div>


使用临时图层

绘制是很耗性能的,如果每次都清空画布然后重新画一次,那么性能会消耗很大(即使分了几个图层),我们应区分“变”与“不变”的部分,只对“变”的部分重新渲染,“不变”的部分不渲染,将经常变化的部分抽离到临时图层,这样仅需要渲染临时图层,临时图层有几种实现思路,一种是使用操作图层(俗称高性能图层),一种是使用隐藏图层(不绘制到界面上的)


高性能图层

一般高频(实时响应鼠标、键盘等事件)的操作会放在高性能图层,等操作完成之后,再将最终结果保存到其它图层,比如绘制、拖拽、缩放一个(或一批)shape

隐藏图层

有些图层是不用给用户看的,这些canvas仅存在于内存中,不会插入html的dom中,用完就销毁,比如常见的canvas to image。

还有一种实现方式是离屏渲染(OffscreenCanvas),先在一个offCanvas操作,然后再将结果渲染到界面上(有点像虚拟dom操作),一般会结合webworker或webassembly


const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
// 绘制图片,或其它操作
context.drawImage();
// 转成base64图片
convas.toDataUrl();

使用webworker或wasm

影响canvas性能的除了绘制频率,还有一个重要的是像素点操作,一般图像处理会涉及到大量的像素点操作,如果放在主线程计算,那么会卡住其它操作,造成页面卡顿,特别影响用户体验,这些涉及大量计算的一般会单独开个线程来操作,而在浏览器中有这个能力的就只有webworker了。


有了webworker可能还不够,因为始终是在js上执行,js执行效率天生就比其它语言慢,所以一般的会使用webassembly,执行效率比js快很多,而且还能用到更丰富的图像处理库

使用webgl

如果还有更高的性能要求,那么普通的2d canvas可能就无法满足了,这个时候可以使用webgl,性能更高(当然学习成本也更高),再结合wasm,就可以有无限想象力了,鼎鼎大名的figma就是用webgl + wasm(rust)实现的,另外google doc在线文档也使用了webgl,飞书文档将来也会替换成wegbl,基于浏览器的渲染始终有诸多限制,一般有能力的都会实现自己的渲染引擎。

业务中图片缩放的实现设计

假设canvas大小为(867,350)

图片的大小为(768,576)

将上面这张图片放到canvas中,图片贴边处理,也即图片太大就缩小,图片太小就放大。那么我们如何实现这种效果呢?


总结一下,总共分为几步:

  1. 计算画布大小和图片大小
  2. 计算如果将图片以原图大小放入画布中心,左上角的坐标
  3. 将画布坐标移动到图片中心点
  4. 将画布放大或缩小(scale=Math.min(画布宽度/图片宽度,画布高度/图片高度))
  5. 重新移动画布坐标到左上角
  6. 绘制图片(或图形)

canvas的执行细节如下:

日分享最新,最流行的软件开发知识与最新行业趋势,希望大家能够一键三连,多多支持,跪求关注,点赞,留言。

了解如何为具有响应式尺寸和现代格式的用户更好地制作 HTML 图像,而不会使您作为开发人员的生活更加困难。

在本文中,了解如何使 HTML 图像更适合具有响应式尺寸和现代格式的用户,而不会使您作为开发人员的生活更加困难。


  • <img>只需src:易于实施;良好的开发经验;没有响应式图像;没有带有后备功能的现代格式;可能使用更多带宽;最差的用户体验
  • <img>具有srcset属性:中等工作;好的开发经验;支持响应式图像以减少带宽;没有带有后备功能的现代格式;留下潜在的节省;好的用户体验
  • <picture>with multiple <source>andsrcset:支持响应式图像和具有后备功能的现代格式;宁愿在我眼里喷墨西哥胡椒汁
  • <img>只需src使用图像管理器:易于实施并自动以最佳尺寸和格式发送图像;开发者和用户双赢!!!

假设我们只有一个从我的域加载图像的基本网站。代码可能如下所示:

<!DOCTYPE html><html><正文> <img src="https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png" alt="命令行终端和旧电脑,背景中有靛蓝色笔触。" ></正文></html>


具有单个图像标记且具有 asrc和alt属性的 HTML 文档在技术上可以正常工作。它按预期提供图像,但该图像存在一些问题。

在大屏幕上,这张图片会很好,但对于使用小屏幕设备的用户来说,1200 像素宽的图片意味着他们必须下载比他们需要的更大的图片。下载可能需要更长的时间,并且可能会花费他们的数据计划费用。

第 1 步:优化和调整图像大小

这第一步很关键,但我认为大多数人都熟悉,所以我不会太深入。

  1. 您网站上的图像应该只有它们需要的大小。如果你的图片只有 600px 宽 x 400px 高,你就不应该让人们下载 1200x800px 的图片。
  2. 大多数网站不需要图像是最高质量的。您可以通过压缩图像删除大量不必要的数据而不会降低质量。

Squoosh是一个非常棒的应用程序,可以手动执行此操作。

如果图像在您的 GitHub 存储库中,您也可以使用imgbot自动完成。

第 2 步:创建响应式图像

图像可以有一个srcset属性,允许我们根据某些设备特征(例如设备宽度)为图像定义多个来源。

我们可以srcset像这样向我们的图像标签添加一个属性:

<!DOCTYPE html><html><正文> <img srcset=" https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-1080x540.png 1080w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-480x240.png 480w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-200x100.png 200w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-768x384.png 768w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png 1200w" src="https://cdn.statically.io/img/austingil.com/f=auto%2Cq=70/wp-content/uploads/Command-Line-Blog-Cover.png" alt="命令行终端和旧电脑,背景中有靛蓝色笔触。" ></正文></html>


我链接到五个不同尺寸的五个不同图像并定义它们相应的视口宽度。如果我们在浏览器中打开我们的图像,视觉上并没有真正改变。但在引擎盖下,有一些改进。

当我们在不同的屏幕上重新加载时,打开开发工具网络选项卡。

在大屏幕上,我们将看到像以前一样下载了全尺寸图像(Command-Line-Blog-Cover.png)。没什么特别的。

但是,如果我们从 200 像素或更窄的屏幕开始,我们会加载 200x100 像素的图像(Command-Line-Blog-Cover-200x100.png)。

对于用户而言,该大小将变得更小更快。

仅发送较小图像的一个问题是:如果用户最初在小屏幕上加载页面,然后将浏览器调整为更大的尺寸会发生什么?在大屏幕上拉伸小图像会使它像素化。

属性不会发生这种情况srcset(假设我们提供了正确的图像),因为当从最小屏幕调整到最大屏幕时,会根据它们的断点下载新图像。

方便的是,从大屏幕到小屏幕的另一个方向不会发生相同的行为,因为毕竟,您可以在较小的屏幕上提供大图像,并且可以缩小而不损失质量。随着屏幕变小,您不想下载额外的图像,因为那样会使用更多数据而没有任何额外的好处。

这是一种非常方便的方法,可以通过节省带宽来改善用户体验,并通过以正确的尺寸提供正确的图像来提高性能,但仍有改进的空间。

第 3 步:使用现代图像格式

我们正在加载的图像是 PNG,但现代浏览器支持新的图像格式,例如WebP或AVIF。这些格式提供了更高的压缩率而没有明显的数据丢失,这意味着我们可以以更小的文件大小有效地提供相同的图像。

再一次,浏览器让我们覆盖了HTML<picture>元素。

类似于srcset属性,图片元素允许我们根据设备特性定义不同的图像源来服务。

所以我们可以做一些事情,比如根据设备宽度或不同的像素密度提供不同的图像。但是将图片元素与属性区分开来的一件事srcset是我们可以针对不同的 mime 类型。

如果浏览器支持,让我们在示例中添加一个包含 AVIF 和 WebP 格式的图片元素。当然,我们还希望继续提供响应式版本。

<!DOCTYPE html><html><正文> <图片> <来源 类型="图像/avif" srcset=" https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover.avif 1200w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-1080x540.avif 1080w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-768x384.avif 768w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-480x240.avif 480w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-200x100.avif 200w" > <来源 类型="图像/webp" srcset=" https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover.webp 1200w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-1080x540.webp 1080w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-768x384.webp 768w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-480x240.webp 480w, https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover-200x100.webp 200w" > <img src="https://cdn.statically.io/img/austingil.com/f=auto%2Cq=70/wp-content/uploads/Command-Line-Blog-Cover.png" alt="命令行终端和旧电脑,背景中有靛蓝色笔触。" > </图片></正文></html>


(我们不需要标签srcset,<img>因为它可以作为不支持的旧浏览器的后备srcset。)

好的,我们有一个图像,它只会以最小的尺寸为用户提供最现代的图像格式。

上面的代码是不是……很漂亮?

真的让您想向您的网站添加新图像,对吗?

正确的!?!?

不!我们做了什么!?!?

第 4 步:开始质疑生活选择

(你好黑暗,我的老朋友……)

第 5 步:简化现代图像格式

好的,既然我们已经解决了这个问题,很明显,上面的解决方案并不是很好。

对于我们想要定位的每种格式和设备宽度,我们需要不同版本的图像。在上面的示例中,这是同一图像的11 个版本(5 个 AVIF、5 个 WebP、1 个 PNG)。

尽管浏览器为我们提供了提供更好的图像和提供更好的用户体验的功能,但这是以开发人员体验为代价的。

(谁愿意付钱?不是我!)

我们可以在上传图像以生成不同的格式和大小时自动执行该过程,然后将这些详细信息存储在数据库中。有一些工具可以提供帮助,例如sharp,但这仍然需要大量工作。我很少看到人们这样做,因为要么工作量太大,要么他们根本不考虑。

我的建议是外包。

今天我将使用 Akamai 图像和视频管理器,因为它是我最熟悉的服务,尽管还有其他人做同样的工作。主要目标是在不让开发人员发疯的情况下为用户提供最佳图像。

我已经设置了一个图像管理器的实例。

要从我的域中添加图像,我只需将“图像”前缀添加到常规图像 URL。所以“austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png”变成了“images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png”。

没什么太有趣的。

但是让我们回到我们的代码,去掉<picture>元素,替换旧的 URL,看看我们能做什么:

<!DOCTYPE html><html><正文> <img srcset=" https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover-1080x540.png 1080w, https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover-480x240.png 480w, https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover-200x100.png 200w, https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover-768x384.png 768w, https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png 1200w" data-fr-src="https://cdn.statically.io/img/austingil.com/f=auto%2Cq=70/wp-content/uploads/Command-Line-Blog-Cover.png" alt="命令行终端和旧电脑,背景中有靛蓝色笔触。" ></正文></html>


<picture>与将元素与 AVIF 和 WebP一起使用相比,我认为没有人会抱怨这一点。这肯定是少了一些工作,但是结果呢?

如果我重新加载页面,我们可以看到相同的图像出现(“Command-Line-Blog-Cover.png”),但如果我们查看响应标题,事情会变得有点有趣。

尽管请求是针对 PNG 图像的,但响应实际上是发送 WebP 数据。这导致图像尺寸更小,这意味着只需打开图像管理器,我就已经为我的用户节省了带宽,而无需做任何事情。

第 6 步:简化响应式图像

我们已经改进了格式,但是响应式图像大小呢?

我仍然将srcset属性设置为在小屏幕上提供小图像,在大屏幕上提供大图像。但是这个系统仍然依赖于提供五种不同分辨率的五种不同的图像。

还是很痛的。

仅提供上传功能就可以成为一项重要功能,更不用说调整大小和存储不同的图像了。对我们来说幸运的是,因为我们正在使用图像管理器,我们实际上可以使这个过程变得更简单。

我实际上将多次引用同一个图像,而不是使用五个不同大小的图像。但我将附加一个查询字符串参数,该参数明确定义我想要定位的大小。

<!DOCTYPE html><html><正文> <img srcset=" https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png?imwidth=1080 1080w, https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png?imwidth=480 480w, https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png?imwidth=200 200w, https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png?imwidth=768 768w, https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png 1200w" src="https://cdn.statically.io/img/austingil.com/f=auto%2Cq=70/wp-content/uploads/Command-Line-Blog-Cover.png" alt="命令行终端和旧电脑,背景中有靛蓝色笔触。" ></正文></html>


请注意,现在每个图像 URL 都转到相同的“Command-Line-Blog-Cover.png”文件,但其中一些imwidth附加了一个额外的查询字符串。

如果我像以前一样在这个小屏幕上重新加载并调整到更大的屏幕,我们会看到类似的行为。不同尺寸的不同图像被加载到它们各自的屏幕尺寸中。

但是这里有一个很大的区别:我们不必创建、上传和管理五张不同大小的不同图像,我们只需要担心一张图像。通过 queer 字符串参数生成不同的大小。

与其他情况相比,它的工作量要少得多,尤其是在您使用基于组件的框架时。

但我们可以做的工作更少!

我们可以回到只使用图像 URL,并让图像管理器为我们选择合适的尺寸。

为了展示它,我喜欢在新标签而不是网站中打开图像。将鼠标悬停在浏览器选项卡上会显示图像的尺寸,1200x600px。

如果我打开我的开发工具,我可以转到Elements 选项卡并模拟不同的设备,例如 iPhone SE:

现在,当我重新加载时会发生一些有趣的事情。

尽管 URL 完全相同,但我可以看到图像大约是一半大小。将鼠标悬停在标签上可确认尺寸为 640x320 像素。

最好的部分是什么?确定要提供哪个图像的所有工作都是在我(开发人员)无需做任何事情的情况下进行的。每次请求都会自动发送设备特征,并且 Image Manager 会以更小的图像进行响应。

老实说,我实际上无法解释它是如何工作的。这很神奇,而且超级酷,让我的生活更轻松,让我的用户生活得更好。

(如果你真的很好奇它是如何工作的,请在 Twitter 上联系我,我会为你找出答案。)

如果您是 Akamai 客户并且您没有使用此功能,您应该立即使用它!

可选的“我是个坏蛋”步骤:自己动手!

我还想为非 Akamai 客户的人们分享一些替代方案。有两个开源项目看起来提供了类似的功能,尽管我没有亲自使用过它们。

其中之一称为imgproxy。另一个称为Thumbor。只要您习惯使用 Docker,它们看起来都很容易安装。

我真的很喜欢Linode的廉价、强大的 VP。

我是 DIY 方法的忠实拥护者,但我认为它并非没有缺点(我的意思是,除了设置和维护之外)。我不确定这些服务在哪里托管图像。如果它们与安装托管在同一台服务器上,则可能存在两个问题。

  1. 如果您要生成同一图像的多个版本,则它们必须存在于某个地方。如果他们住在同一个 VPS 上,那可能会变得更贵。尝试对图像使用对象存储之类的东西,因为它的存储费用更便宜。
  2. 从同一位置加载图像可能会导致延迟问题。我非常喜欢 Image Manager 如何使用 Akamai CDN 网络,因此图像总是来自离用户最近的位置。这可以大大加快下载时间。

也就是说,我认为它仍然值得一试。

如果你确实让它工作并且你喜欢它,请回来告诉我它是怎么回事。什么进展顺利,什么没用,什么很棒,和/或缺少什么?

结束的想法

好吧,这就是我今天为你准备的全部内容。我们介绍了处理图像的不同选项及其优缺点。我们从<img>一个src属性开始,添加一个srcset属性,然后<picture>使用不同的<source>标签,最终把它全部烧掉,回到只是属性,但合并了图像管理器<img>。src

换句话说,这是一个非常迂回的旅程:

<! – 坏 – ><img src="https://austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png" alt="命令行终端和旧电脑,背景中有靛蓝色笔触。">


对此:

<! – 好 – ><img data-fr-src="https://images.austingil.com/wp-content/uploads/Command-Line-Blog-Cover.png" alt="命令行终端和旧电脑,背景中有靛蓝色笔触。">


多么虎头蛇尾。

可能本来可以节省时间,然后说“改善图像的第一步:使用图像管理器”,但其中的乐趣在哪里?我也喜欢挖掘原因。

我们甚至没有涉及诸如延迟加载、解码或获取优先级之类的内容,但这些内容更具上下文和细微差别,而上面的建议几乎是全面的。

无论如何,我希望您喜欢它,并且希望您实施其中一些解决方案,因为发送具有现代格式的较小图像是使互联网成为更快、更环保的地方的好方法。

下截图:

点击文件选择框,我们不妨选一张尺寸比较大的图片,例如下面这种2M多的钓鱼收获照:

于是图片歘歘歘地传上去了:

此时我们点击最终上传完毕的图片地址,会发现原来2M多3000多像素宽的图片被限制为400像素宽了:

保存到本地会发现图片尺寸已经变成只有70K了:

以上就是图片前端压缩并上传demo的完整演示。

二、实现原理

要想使用JS实现图片的压缩效果,原理其实很简单,核心API就是使用canvas的drawImage()方法。

Canvas本质上就是一张位图,而drawImage()方法可以把一张大大的图片绘制在小小的Canvas画布上,不久等同于图片尺寸压缩了?

对于本案例的压缩,使用的5个参数的API方法:

context.drawImage(img, dx, dy, dWidth, dHeight);复制代码

各参数具体含义可以参见“Canvas API中文文档-drawImage”,这里不展开。

举例:

一张图片(假设图片对象是img)的原始尺寸是4000*3000,现在需要把尺寸限制为400*300大小,很简单,原理如下代码示意:

var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
canvas.width = 400;
canvas.height = 300;
// 核心JS就这个
context.drawImage(img,0,0,400,300);复制代码

把大图片画在一张小画布上,压缩就这么实现了,是不是简单的有点超乎想象。

三、如果想要上传或下载?

如果想要上传图片或者下载图片,可以使用canvas.toDataURL()或者canvas.toBlob()方法先进行转换。

1. canvas.toDataURL()

语法如下:canvas.toDataURL(mimeType, qualityArgument)复制代码

可以把画布转换成base64格式信息图像信息,纯字符的图片表示法。

其中:

mimeType表示canvas导出来的base64图片的类型,默认是png格式,也即是默认值是'image/png',我们也可以指定为jpg格式'image/jpeg'或者webp等格式。file对象中的file.type就是文件的mimeType类型,在转换时候正好可以直接拿来用(如果有file对象)。

qualityArgument表示导出的图片质量,只要导出为jpg和webp格式的时候此参数才有效果,默认值是0.92,是一个比较合理的图片质量输出参数,通常情况下,我们无需再设定。

更多关于toDataURL()方法的信息可以参见“Canvas API中文文档-toDataURL()”。

2. canvas.toBlob()方法

语法如下:canvas.toBlob(callback, mimeType, qualityArgument)复制代码

可以把画布转换成Blob文件,通常用在文件上传中,因为是二进制的,对后端更加友好。

和toDataURL()方法相比,toBlob()方法是异步的,因此多了个callback参数,这个callback回调方法默认的第一个参数就是转换好的blob文件信息,本文一开始的demo案例中的文件上传就是将canvas图片转换成二进制的blob文件,然后再ajax上传的,代码如下:

// canvas转为blob并上传
canvas.toBlob(function (blob) {
 // 图片ajax上传
 var xhr = new XMLHttpRequest();
 // 开始上传
 xhr.open("POST", 'upload.php', true);
 xhr.send(blob); 
});复制代码

更多关于toBlob()方法的信息可以参见“Canvas API中文文档-toBlob()”。

一旦有了可传输的图像数据,上传下载就好实现了。例如下载前端压缩好的图片,可以参考我上一篇在掘金发布的文章:“纯JS生成并下载各种文本文件或图片”。

四、总结

经过“图片→canvas压缩→图片”三步曲,我们完成了图片前端压缩功能。

作者:张鑫旭

链接:https://juejin.im/post/5bec3c6cf265da614312a0fa

来源:掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。