前端处理复杂图片样式的兜底方案
正如文章题目所示,本文的目的是为了记录工作中遇到的,在页面中处理复杂图片样式的解决方案。使用的技术栈有:
- web Worker
- fetch
- createImageBitmap
- OffscreenCanvas
- Blob
之所以称之为“兜底”方案,是因为下文中提到的方法能够以像素级别操作图片。而,如果只是这样,也没有什么值得记录的。但是,本文为了防止这种像素级别的操作对页面渲染性能造成大的冲击,结合 web worker 异步处理,解决了这个问题。
从本文的思路出发,至少可以收获两点技术:
- web worker 的实际使用案例
- 前端 PS 的基础知识
下面让我们开始吧~
本文分为三个小节,第一小节简单的介绍上面提到的技术栈;第二小节在非工程化 Demo 中演示这个解决方案的流程;第三小节将此方法分装成一个 React 组件,以便后面复用和维护。
1. 技术栈介绍
- Web Worker: Web Worker是浏览器提供的一种能在后台线程中运行JavaScript的技术,它不会阻塞或影响页面的性能。通过创建一个新的Worker对象,可以将耗时的计算或数据处理任务放在后台执行,从而避免UI线程被长时间占用,提高页面的响应性和用户体验。Web Worker通过postMessage和onmessage进行主线程和工作线程之间的通信。
- Fetch: Fetch是一个现代、强大且灵活的网络请求API,它提供了一个全局fetch()方法,用于异步获取网络资源。与XMLHttpRequest相比,Fetch更加简洁、高效,且支持Promise模式,使得异步处理更加直观和方便。Fetch还支持跨域请求、请求和响应的拦截、处理HTTP管道等高级功能,是现代Web开发中网络请求的首选方式。
- createImageBitmap: createImageBitmap是一个用于创建位图图像的API,它可以直接从各种图像源(如Blob、ImageData、ImageBitmap、HTMLCanvasElement等)生成一个高效的位图。这个API是异步的,不会阻塞主线程,因此非常适合用于处理大量图像数据。生成的ImageBitmap对象可以直接用于Canvas的drawImage方法,极大地提高了图像渲染的性能。
- OffscreenCanvas: OffscreenCanvas是HTML5 Canvas API的一个扩展,它允许在Web Worker中使用Canvas功能。这意味着图像处理、渲染等计算密集型任务可以在不阻塞主线程的情况下进行。OffscreenCanvas通过transferControlToOffscreen或getContext('2d', {offscreen: true})等方式创建,并可以在主线程和工作线程之间传递,从而实现了真正的后台渲染。
- Blob: Blob(Binary Large Object)是一个用于处理二进制数据的JavaScript对象。它可以存储大量的二进制数据,并允许通过URL.createObjectURL()方法创建一个指向该数据的URL,这个URL可以直接用于img、audio、video等元素的src属性。Blob常用于处理文件上传、下载以及图像、音频、视频的动态生成等场景。此外,Blob还支持切片操作,便于大数据的处理和传输。
这些知识不能说不常见吧,反正是有点高级的。所以这个解决方案还是有点东西的,你可以用来在面试中吹牛逼🤥
2. 兜底方案流程图
本文介绍的解决方案可以用下面的流程图表示:
在一个空白的文件夹下面创建以下文件
- index.html
- worker.js
使用到的代码如下:
<!DOCTYPE html>
<html>
<head>
<title>Web Worker Image Processing</title>
</head>
<body>
<img id="originalImage" src="https://images.pexels.com/photos/12196392/pexels-photo-12196392.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load" alt="Original Image" />
<img id="processedImage" alt="Processed Image" />
<script>
// 创建一个Web Worker实例
const worker = new Worker('worker.js');
// 监听Web Worker的消息
worker.onmessage = function (e) {
const processedImageUrl = e.data;
const _ = document.getElementById('processedImage');
_.onload = () => {
worker.terminate();
}
_.src = processedImageUrl;
};
// 图片地址,你可以根据需要修改
const imageUrl = 'https://images.pexels.com/photos/12196392/pexels-photo-12196392.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load';
// 当原始图片加载完成后,向Web Worker发送消息
document.getElementById('originalImage').onload = function () {
worker.postMessage(imageUrl);
};
</script>
</body>
</html>
// worker.js
// 当此Web Worker接收到消息时,会调用此函数
self.onmessage = async function (e) {
// 从接收到的消息中提取出图片的URL
const imageUrl = e.data;
try {
// 使用fetch API从给定的URL异步获取图片资源
const response = await fetch(imageUrl);
// 检查HTTP响应状态,如果不是200-299之间,则抛出错误
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 将响应体转换为Blob对象,这通常用于处理二进制数据
const blob = await response.blob();
// 使用Blob对象创建一个ImageBitmap,这是一个可以高效绘制到Canvas上的位图图像
const imgBitmap = createImageBitmap(blob);
// ImageBitmap对象创建是异步的,所以使用.then()来处理创建成功后的操作
imgBitmap.then(function (bitmap) {
// 创建一个离屏Canvas,其尺寸与位图相同
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
// 获取Canvas的2D渲染上下文
const ctx = canvas.getContext('2d');
// 在Canvas上绘制位图
ctx.drawImage(bitmap, 0, 0);
// 从Canvas上获取图像数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// 获取图像数据的像素数组
const data = imageData.data;
// 遍历每个像素,将其转换为灰度(通过计算RGB通道的平均值,并将其设置为每个通道的值)
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // R
data[i + 1] = avg; // G
data[i + 2] = avg; // B
}
// 将处理后的图像数据放回Canvas
ctx.putImageData(imageData, 0, 0);
// 将Canvas转换为Blob对象
canvas.convertToBlob().then(blob => {
// 创建一个表示该Blob对象的URL
const newUrl = URL.createObjectURL(blob);
// 将新的图像URL发送回主线程
self.postMessage(newUrl);
});
}).catch(e => {
// 如果在处理过程中发生错误,则抛出一个新的错误(这里可以添加更详细的错误处理)
throw new Error();
});
} catch (e) {
// 如果在尝试获取或处理图像时发生错误,则将原始图像URL发送回主线程
self.postMessage(imageUrl);
}
};
理解上面代码的逻辑可以结合代码注释和流程图,在此就不过多赘述了。
完成之后使用 live server 启动 index.html
可以看到如下效果:
左边是原图,而右边是经过 web worker 处理之后的图像。
在这个示例中,你可以简单的将 web worker 理解成为 PS。它接受原始图片的地址,返回处理之后的图片的地址。
需要注意的是,图片是二进制数据,所以我们用到了 Blob 将数据转成 URL。
3. 封装成 React 组件
在你的前端工程目录中创建 src/component/ImageProcessor
目录,然后创建下面两个文件:
文件中的代码如下所示:
// index.js
// 引入React及其相关hooks
import React, { useEffect, useRef, useState } from 'react';
// 引入Web Worker的脚本
import workerScript from './worker';
// ImageProcessor组件,它接受原始图片的URL、宽度和高度作为属性
const ImageProcessor = ({ originSrc, width, height }) => {
// 使用useState hook来存储处理后的图片URL,初始值为原始图片的URL
const [src, setSrc] = useState(originSrc);
// 使用useRef hook来存储Web Worker的实例
const workerInstance = useRef(new Worker(workerScript));
// 使用useEffect hook来处理图片的URL变化
useEffect(() => {
// 当原始图片URL与处理后的图片URL不相同时,才进行处理
if (originSrc === src) {
// 设置Web Worker的onmessage事件处理函数
workerInstance.current.onmessage = function (e) {
// 接收处理后的图片URL
const processedImageUrl = e.data;
// 更新处理后的图片URL状态
setSrc(processedImageUrl);
};
// 向Web Worker发送原始图片的完整URL,以便进行处理
workerInstance.current.postMessage(window.location.origin + originSrc);
}
// 清除函数,在组件卸载或状态变化时终止Web Worker
return () => {
workerInstance.current.terminate();
}
// 当原始图片URL或处理后的图片URL发生变化时,触发此hook
}, [src, originSrc])
// 图片加载完成后的事件处理函数
const imageLoaded = (event) => {
// 终止当前的Web Worker
workerInstance.current.terminate();
};
// 返回JSX,表示组件的UI
return (
<div>
{/* 当原始图片的URL与处理后的图片URL不相同时,显示处理后的图片 */}
{originSrc !== src && <img
style={{
position: 'absolute',
left: 0,
top: 0,
zIndex: 0,
width: width ?? '100%',// 使用nullish coalescing操作符来提供默认值
height: height ?? '100%',
objectFit: 'cover',// 图片填充方式
}}
src={src}// 设置图片的URL
alt="Original Image"// 图片的替代文本
onLoad={imageLoaded}// 图片加载完成后的事件处理函数
/>}
</div>
);
};
// 导出ImageProcessor组件
export default ImageProcessor;
// worker.js
// 定义一个workerCode函数,这个函数将作为Web Worker的代码
const workerCode = () => {
// 使用_self变量来引用全局的self对象,以便在Web Worker内部使用
// 是用来告诉eslint忽略下一行的代码检查,因为这里我们对self进行了重新赋值
// eslint-disable-next-line
const _self = self;
// 当Web Worker接收到消息时,会调用此函数
_self.onmessage = async function (e) {
// 从接收到的消息中提取出图片的URL
const imageUrl = e.data;
try {
// 使用fetch API异步地从给定的URL获取图片资源
const response = await fetch(imageUrl);
// 检查HTTP响应状态,如果不是200-299之间,则抛出错误
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 将HTTP响应的内容转换为Blob对象,这通常用于处理二进制数据
const blob = await response.blob();
// 使用Blob对象创建一个ImageBitmap,这是一个可以高效绘制到Canvas上的位图图像
// 注意:createImageBitmap是异步的
const imgBitmap = createImageBitmap(blob);
// ImageBitmap对象创建成功后,会执行以下操作
imgBitmap.then(function (bitmap) {
// 创建一个与位图尺寸相同的离屏Canvas
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
// 获取这个离屏Canvas的2D渲染上下文
const ctx = canvas.getContext('2d');
// 在Canvas上绘制之前创建的位图
ctx.drawImage(bitmap, 0, 0);
// 从Canvas上获取整个图像的图像数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// 获取图像数据的像素数组
const data = imageData.data;
// 遍历每个像素,将彩色图像转换为灰度图像
// 灰度是通过计算RGB三个通道的平均值,并将其设置为每个通道的值来得到的
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // R
data[i + 1] = avg; // G
data[i + 2] = avg; // B
}
// 将处理后的灰度图像数据重新放回Canvas
ctx.putImageData(imageData, 0, 0);
// 将Canvas内容转换为Blob对象
canvas.convertToBlob().then(blob => {
// 创建一个表示该Blob对象的URL
const newUrl = URL.createObjectURL(blob);
// 将处理后的灰度图像的URL发送回主线程
_self.postMessage(newUrl);
});
}).catch(e => {
// 如果在处理ImageBitmap或Canvas时出现错误,抛出一个新的错误
// 这里可以添加更详细的错误处理逻辑
throw new Error();
});
} catch (e) {
// 如果在尝试获取或处理图像时发生任何错误(如网络错误、fetch失败等)
// 则将原始的图像URL发送回主线程,表示处理失败
_self.postMessage(imageUrl);
}
};
};
// 将workerCode函数转换为字符串,并截取大括号内的内容作为Web Worker的代码
let code = workerCode.toString();
code = code.substring(code.indexOf('{') + 1, code.lastIndexOf('}'));
// 创建一个包含处理过的代码的Blob对象
const blob = new Blob([code], { type: 'application/javascript' });
// 为这个Blob对象创建一个URL,这个URL可以被用作Web Worker的脚本源
const workerScriptURL = URL.createObjectURL(blob);
// 导出这个URL,以便其他模块可以使用它来创建一个新的Web Worker
export default workerScriptURL;
这样组件就封装完成了,在你需要的地方使用下面的代码来调用上面的组件:
import ImageProcessor from "@/component/ImageProcessor";
<ImageProcessor originSrc = {CompressImageUrl} />
你可以将上述代码的这部分抽取成一个函数,放到公用工具库中:
// 遍历每个像素,将彩色图像转换为灰度图像
// 灰度是通过计算RGB三个通道的平均值,并将其设置为每个通道的值来得到的
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // R
data[i + 1] = avg; // G
data[i + 2] = avg; // B
}
这样一来,通过替换像素算法就可以实现更加有趣且复杂的图像处理了。
最后总结一下上面代码中值得注意的细节:
- React Hooks的使用:
useState
,useEffect
,useRef
这三个React Hooks在代码中都被使用了。useState
用于存储处理后的图片URL;useEffect
用于在组件挂载、更新时执行某些操作(如启动和终止Web Worker);useRef
用于存储Web Worker的实例,并确保其在整个组件生命周期内保持不变。 - Web Worker的使用:为了提高性能,代码使用了Web Worker来在后台处理图像转换任务,从而避免阻塞主线程。Web Worker通过
postMessage
和onmessage
进行通信。 - Blob和URL.createObjectURL:在处理图像后,Web Worker使用
Blob
和URL.createObjectURL
创建了一个新的URL,这个URL指向包含处理后图像数据的Blob对象。这样可以在不将图像数据实际写入磁盘的情况下,将其作为一个可访问的资源。 - createImageBitmap和OffscreenCanvas:为了提高图像处理性能,代码中使用了
createImageBitmap
来高效地将Blob转换为位图,并使用OffscreenCanvas
进行离屏渲染。这两个API都允许在不影响页面渲染的情况下进行高性能的图像处理。 - 灰度图像处理:在Web Worker内部,代码遍历了图像的每个像素,并将其转换为灰度。这是通过计算RGB通道的平均值,并将这个平均值设置为新的RGB值来实现的。
- 动态创建Web Worker脚本:
worker.js
文件最后将自身代码转换为字符串,并通过Blob和URL.createObjectURL
创建了一个URL,这个URL被用作Web Worker的脚本源。这是一种动态创建和执行JavaScript代码的技术,允许在不依赖外部文件的情况下运行Web Worker。 - 错误处理和异常捕获:在Web Worker内部和外部都有错误处理和异常捕获的逻辑。例如,在尝试获取或处理图像时发生错误,会将原始的图像URL发送回主线程表示处理失败。
- 使用nullish coalescing操作符:在JSX中,
width ?? '100%'
和height ?? '100%'
使用了nullish coalescing操作符(??
),这是ES2020引入的新特性。它允许在左侧操作数为null
或undefined
时返回右侧的值,否则返回左侧的值。这提供了一种简洁的方式来为变量提供默认值。 - 图片加载完成后的处理:在图片加载完成后,会终止当前的Web Worker。这有助于释放资源并避免不必要的后台处理。
- 代码组织和模块导出:
ImageProcessor
组件和workerScriptURL
都被导出,以便在其他模块中使用。这体现了良好的模块化和代码复用实践。
如果您觉得这篇文章对您有所帮助的话,劳驾您发财的小手,点个赞吧,谢谢!
转载自:https://juejin.cn/post/7361353743912009780