React處理復(fù)雜圖片樣式的方法詳解
正如文章題目所示,本文的目的是為了記錄工作中遇到的,在頁(yè)面中處理復(fù)雜圖片樣式的解決方案。使用的技術(shù)棧有:
- web Worker
- fetch
- createImageBitmap
- OffscreenCanvas
- Blob
之所以稱(chēng)之為“兜底”方案,是因?yàn)橄挛闹刑岬降姆椒軌蛞?strong>像素級(jí)別操作圖片。而,如果只是這樣,也沒(méi)有什么值得記錄的。但是,本文為了防止這種像素級(jí)別的操作對(duì)頁(yè)面渲染性能造成大的沖擊,結(jié)合 web worker 異步處理,解決了這個(gè)問(wèn)題。
從本文的思路出發(fā),至少可以收獲兩點(diǎn)技術(shù):
- web worker 的實(shí)際使用案例
- 前端 PS 的基礎(chǔ)知識(shí)
下面讓我們開(kāi)始吧~
本文分為三個(gè)小節(jié),第一小節(jié)簡(jiǎn)單的介紹上面提到的技術(shù)棧;第二小節(jié)在非工程化 Demo 中演示這個(gè)解決方案的流程;第三小節(jié)將此方法分裝成一個(gè) React 組件,以便后面復(fù)用和維護(hù)。
1. 技術(shù)棧介紹
- Web Worker: Web Worker是瀏覽器提供的一種能在后臺(tái)線(xiàn)程中運(yùn)行JavaScript的技術(shù),它不會(huì)阻塞或影響頁(yè)面的性能。通過(guò)創(chuàng)建一個(gè)新的Worker對(duì)象,可以將耗時(shí)的計(jì)算或數(shù)據(jù)處理任務(wù)放在后臺(tái)執(zhí)行,從而避免UI線(xiàn)程被長(zhǎng)時(shí)間占用,提高頁(yè)面的響應(yīng)性和用戶(hù)體驗(yàn)。Web Worker通過(guò)postMessage和onmessage進(jìn)行主線(xiàn)程和工作線(xiàn)程之間的通信。
- Fetch: Fetch是一個(gè)現(xiàn)代、強(qiáng)大且靈活的網(wǎng)絡(luò)請(qǐng)求API,它提供了一個(gè)全局fetch()方法,用于異步獲取網(wǎng)絡(luò)資源。與XMLHttpRequest相比,F(xiàn)etch更加簡(jiǎn)潔、高效,且支持Promise模式,使得異步處理更加直觀(guān)和方便。Fetch還支持跨域請(qǐng)求、請(qǐng)求和響應(yīng)的攔截、處理HTTP管道等高級(jí)功能,是現(xiàn)代Web開(kāi)發(fā)中網(wǎng)絡(luò)請(qǐng)求的首選方式。
- createImageBitmap: createImageBitmap是一個(gè)用于創(chuàng)建位圖圖像的API,它可以直接從各種圖像源(如Blob、ImageData、ImageBitmap、HTMLCanvasElement等)生成一個(gè)高效的位圖。這個(gè)API是異步的,不會(huì)阻塞主線(xiàn)程,因此非常適合用于處理大量圖像數(shù)據(jù)。生成的ImageBitmap對(duì)象可以直接用于Canvas的drawImage方法,極大地提高了圖像渲染的性能。
- OffscreenCanvas: OffscreenCanvas是HTML5 Canvas API的一個(gè)擴(kuò)展,它允許在Web Worker中使用Canvas功能。這意味著圖像處理、渲染等計(jì)算密集型任務(wù)可以在不阻塞主線(xiàn)程的情況下進(jìn)行。OffscreenCanvas通過(guò)transferControlToOffscreen或getContext('2d', {offscreen: true})等方式創(chuàng)建,并可以在主線(xiàn)程和工作線(xiàn)程之間傳遞,從而實(shí)現(xiàn)了真正的后臺(tái)渲染。
- Blob: Blob(Binary Large Object)是一個(gè)用于處理二進(jìn)制數(shù)據(jù)的JavaScript對(duì)象。它可以存儲(chǔ)大量的二進(jìn)制數(shù)據(jù),并允許通過(guò)URL.createObjectURL()方法創(chuàng)建一個(gè)指向該數(shù)據(jù)的URL,這個(gè)URL可以直接用于img、audio、video等元素的src屬性。Blob常用于處理文件上傳、下載以及圖像、音頻、視頻的動(dòng)態(tài)生成等場(chǎng)景。此外,Blob還支持切片操作,便于大數(shù)據(jù)的處理和傳輸。
這些知識(shí)不能說(shuō)不常見(jiàn)吧,反正是有點(diǎn)高級(jí)的。所以這個(gè)解決方案還是有點(diǎn)東西的,你可以用來(lái)在面試中吹牛
2. 兜底方案流程圖
本文介紹的解決方案可以用下面的流程圖表示:
在一個(gè)空白的文件夾下面創(chuàng)建以下文件
- 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> // 創(chuàng)建一個(gè)Web Worker實(shí)例 const worker = new Worker('worker.js'); // 監(jiān)聽(tīng)Web Worker的消息 worker.onmessage = function (e) { const processedImageUrl = e.data; const _ = document.getElementById('processedImage'); _.onload = () => { worker.terminate(); } _.src = processedImageUrl; }; // 圖片地址,你可以根據(jù)需要修改 const imageUrl = 'https://images.pexels.com/photos/12196392/pexels-photo-12196392.jpeg?auto=compress&cs=tinysrgb&w=600&lazy=load'; // 當(dāng)原始圖片加載完成后,向Web Worker發(fā)送消息 document.getElementById('originalImage').onload = function () { worker.postMessage(imageUrl); }; </script> </body> </html>
// worker.js // 當(dāng)此Web Worker接收到消息時(shí),會(huì)調(diào)用此函數(shù) self.onmessage = async function (e) { // 從接收到的消息中提取出圖片的URL const imageUrl = e.data; try { // 使用fetch API從給定的URL異步獲取圖片資源 const response = await fetch(imageUrl); // 檢查HTTP響應(yīng)狀態(tài),如果不是200-299之間,則拋出錯(cuò)誤 if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // 將響應(yīng)體轉(zhuǎn)換為Blob對(duì)象,這通常用于處理二進(jìn)制數(shù)據(jù) const blob = await response.blob(); // 使用Blob對(duì)象創(chuàng)建一個(gè)ImageBitmap,這是一個(gè)可以高效繪制到Canvas上的位圖圖像 const imgBitmap = createImageBitmap(blob); // ImageBitmap對(duì)象創(chuàng)建是異步的,所以使用.then()來(lái)處理創(chuàng)建成功后的操作 imgBitmap.then(function (bitmap) { // 創(chuàng)建一個(gè)離屏Canvas,其尺寸與位圖相同 const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); // 獲取Canvas的2D渲染上下文 const ctx = canvas.getContext('2d'); // 在Canvas上繪制位圖 ctx.drawImage(bitmap, 0, 0); // 從Canvas上獲取圖像數(shù)據(jù) const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); // 獲取圖像數(shù)據(jù)的像素?cái)?shù)組 const data = imageData.data; // 遍歷每個(gè)像素,將其轉(zhuǎn)換為灰度(通過(guò)計(jì)算RGB通道的平均值,并將其設(shè)置為每個(gè)通道的值) 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 } // 將處理后的圖像數(shù)據(jù)放回Canvas ctx.putImageData(imageData, 0, 0); // 將Canvas轉(zhuǎn)換為Blob對(duì)象 canvas.convertToBlob().then(blob => { // 創(chuàng)建一個(gè)表示該Blob對(duì)象的URL const newUrl = URL.createObjectURL(blob); // 將新的圖像URL發(fā)送回主線(xiàn)程 self.postMessage(newUrl); }); }).catch(e => { // 如果在處理過(guò)程中發(fā)生錯(cuò)誤,則拋出一個(gè)新的錯(cuò)誤(這里可以添加更詳細(xì)的錯(cuò)誤處理) throw new Error(); }); } catch (e) { // 如果在嘗試獲取或處理圖像時(shí)發(fā)生錯(cuò)誤,則將原始圖像URL發(fā)送回主線(xiàn)程 self.postMessage(imageUrl); } };
理解上面代碼的邏輯可以結(jié)合代碼注釋和流程圖,在此就不過(guò)多贅述了。
完成之后使用 live server 啟動(dòng) index.html
可以看到如下效果:
左邊是原圖,而右邊是經(jīng)過(guò) web worker 處理之后的圖像。
在這個(gè)示例中,你可以簡(jiǎn)單的將 web worker 理解成為 PS。它接受原始圖片的地址,返回處理之后的圖片的地址。
需要注意的是,圖片是二進(jìn)制數(shù)據(jù),所以我們用到了 Blob 將數(shù)據(jù)轉(zhuǎn)成 URL。
3. 封裝成 React 組件
在 React 中使用 web worker 需要一點(diǎn)技巧,請(qǐng)參考我之前的文章: 在react項(xiàng)目中使用web worker的方法 - 掘金 (juejin.cn)
在你的前端工程目錄中創(chuàng)建 src/component/ImageProcessor
目錄,然后創(chuàng)建下面兩個(gè)文件:
文件中的代碼如下所示:
// index.js // 引入React及其相關(guān)hooks import React, { useEffect, useRef, useState } from 'react'; // 引入Web Worker的腳本 import workerScript from './worker'; // ImageProcessor組件,它接受原始圖片的URL、寬度和高度作為屬性 const ImageProcessor = ({ originSrc, width, height }) => { // 使用useState hook來(lái)存儲(chǔ)處理后的圖片URL,初始值為原始圖片的URL const [src, setSrc] = useState(originSrc); // 使用useRef hook來(lái)存儲(chǔ)Web Worker的實(shí)例 const workerInstance = useRef(new Worker(workerScript)); // 使用useEffect hook來(lái)處理圖片的URL變化 useEffect(() => { // 當(dāng)原始圖片URL與處理后的圖片URL不相同時(shí),才進(jìn)行處理 if (originSrc === src) { // 設(shè)置Web Worker的onmessage事件處理函數(shù) workerInstance.current.onmessage = function (e) { // 接收處理后的圖片URL const processedImageUrl = e.data; // 更新處理后的圖片URL狀態(tài) setSrc(processedImageUrl); }; // 向Web Worker發(fā)送原始圖片的完整URL,以便進(jìn)行處理 workerInstance.current.postMessage(window.location.origin + originSrc); } // 清除函數(shù),在組件卸載或狀態(tài)變化時(shí)終止Web Worker return () => { workerInstance.current.terminate(); } // 當(dāng)原始圖片URL或處理后的圖片URL發(fā)生變化時(shí),觸發(fā)此hook }, [src, originSrc]) // 圖片加載完成后的事件處理函數(shù) const imageLoaded = (event) => { // 終止當(dāng)前的Web Worker workerInstance.current.terminate(); }; // 返回JSX,表示組件的UI return ( <div> {/* 當(dāng)原始圖片的URL與處理后的圖片URL不相同時(shí),顯示處理后的圖片 */} {originSrc !== src && <img style={{ position: 'absolute', left: 0, top: 0, zIndex: 0, width: width ?? '100%',// 使用nullish coalescing操作符來(lái)提供默認(rèn)值 height: height ?? '100%', objectFit: 'cover',// 圖片填充方式 }} src={src}// 設(shè)置圖片的URL alt="Original Image"http:// 圖片的替代文本 onLoad={imageLoaded}// 圖片加載完成后的事件處理函數(shù) />} </div> ); }; // 導(dǎo)出ImageProcessor組件 export default ImageProcessor;
// worker.js // 定義一個(gè)workerCode函數(shù),這個(gè)函數(shù)將作為Web Worker的代碼 const workerCode = () => { // 使用_self變量來(lái)引用全局的self對(duì)象,以便在Web Worker內(nèi)部使用 // 是用來(lái)告訴eslint忽略下一行的代碼檢查,因?yàn)檫@里我們對(duì)self進(jìn)行了重新賦值 // eslint-disable-next-line const _self = self; // 當(dāng)Web Worker接收到消息時(shí),會(huì)調(diào)用此函數(shù) _self.onmessage = async function (e) { // 從接收到的消息中提取出圖片的URL const imageUrl = e.data; try { // 使用fetch API異步地從給定的URL獲取圖片資源 const response = await fetch(imageUrl); // 檢查HTTP響應(yīng)狀態(tài),如果不是200-299之間,則拋出錯(cuò)誤 if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // 將HTTP響應(yīng)的內(nèi)容轉(zhuǎn)換為Blob對(duì)象,這通常用于處理二進(jìn)制數(shù)據(jù) const blob = await response.blob(); // 使用Blob對(duì)象創(chuàng)建一個(gè)ImageBitmap,這是一個(gè)可以高效繪制到Canvas上的位圖圖像 // 注意:createImageBitmap是異步的 const imgBitmap = createImageBitmap(blob); // ImageBitmap對(duì)象創(chuàng)建成功后,會(huì)執(zhí)行以下操作 imgBitmap.then(function (bitmap) { // 創(chuàng)建一個(gè)與位圖尺寸相同的離屏Canvas const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); // 獲取這個(gè)離屏Canvas的2D渲染上下文 const ctx = canvas.getContext('2d'); // 在Canvas上繪制之前創(chuàng)建的位圖 ctx.drawImage(bitmap, 0, 0); // 從Canvas上獲取整個(gè)圖像的圖像數(shù)據(jù) const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); // 獲取圖像數(shù)據(jù)的像素?cái)?shù)組 const data = imageData.data; // 遍歷每個(gè)像素,將彩色圖像轉(zhuǎn)換為灰度圖像 // 灰度是通過(guò)計(jì)算RGB三個(gè)通道的平均值,并將其設(shè)置為每個(gè)通道的值來(lái)得到的 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 } // 將處理后的灰度圖像數(shù)據(jù)重新放回Canvas ctx.putImageData(imageData, 0, 0); // 將Canvas內(nèi)容轉(zhuǎn)換為Blob對(duì)象 canvas.convertToBlob().then(blob => { // 創(chuàng)建一個(gè)表示該Blob對(duì)象的URL const newUrl = URL.createObjectURL(blob); // 將處理后的灰度圖像的URL發(fā)送回主線(xiàn)程 _self.postMessage(newUrl); }); }).catch(e => { // 如果在處理ImageBitmap或Canvas時(shí)出現(xiàn)錯(cuò)誤,拋出一個(gè)新的錯(cuò)誤 // 這里可以添加更詳細(xì)的錯(cuò)誤處理邏輯 throw new Error(); }); } catch (e) { // 如果在嘗試獲取或處理圖像時(shí)發(fā)生任何錯(cuò)誤(如網(wǎng)絡(luò)錯(cuò)誤、fetch失敗等) // 則將原始的圖像URL發(fā)送回主線(xiàn)程,表示處理失敗 _self.postMessage(imageUrl); } }; }; // 將workerCode函數(shù)轉(zhuǎn)換為字符串,并截取大括號(hào)內(nèi)的內(nèi)容作為Web Worker的代碼 let code = workerCode.toString(); code = code.substring(code.indexOf('{') + 1, code.lastIndexOf('}')); // 創(chuàng)建一個(gè)包含處理過(guò)的代碼的Blob對(duì)象 const blob = new Blob([code], { type: 'application/javascript' }); // 為這個(gè)Blob對(duì)象創(chuàng)建一個(gè)URL,這個(gè)URL可以被用作Web Worker的腳本源 const workerScriptURL = URL.createObjectURL(blob); // 導(dǎo)出這個(gè)URL,以便其他模塊可以使用它來(lái)創(chuàng)建一個(gè)新的Web Worker export default workerScriptURL;
這樣組件就封裝完成了,在你需要的地方使用下面的代碼來(lái)調(diào)用上面的組件
import ImageProcessor from "@/component/ImageProcessor"; <ImageProcessor originSrc = {CompressImageUrl} />
你可以將上述代碼的這部分抽取成一個(gè)函數(shù),放到公用工具庫(kù)中:
// 遍歷每個(gè)像素,將彩色圖像轉(zhuǎn)換為灰度圖像 // 灰度是通過(guò)計(jì)算RGB三個(gè)通道的平均值,并將其設(shè)置為每個(gè)通道的值來(lái)得到的 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 }
這樣一來(lái),通過(guò)替換像素算法就可以實(shí)現(xiàn)更加有趣且復(fù)雜的圖像處理了。
最后總結(jié)一下上面代碼中值得注意的細(xì)節(jié):
- React Hooks的使用:
useState
,useEffect
,useRef
這三個(gè)React Hooks在代碼中都被使用了。useState
用于存儲(chǔ)處理后的圖片URL;useEffect
用于在組件掛載、更新時(shí)執(zhí)行某些操作(如啟動(dòng)和終止Web Worker);useRef
用于存儲(chǔ)Web Worker的實(shí)例,并確保其在整個(gè)組件生命周期內(nèi)保持不變。 - Web Worker的使用:為了提高性能,代碼使用了Web Worker來(lái)在后臺(tái)處理圖像轉(zhuǎn)換任務(wù),從而避免阻塞主線(xiàn)程。Web Worker通過(guò)
postMessage
和onmessage
進(jìn)行通信。 - Blob和URL.createObjectURL:在處理圖像后,Web Worker使用
Blob
和URL.createObjectURL
創(chuàng)建了一個(gè)新的URL,這個(gè)URL指向包含處理后圖像數(shù)據(jù)的Blob對(duì)象。這樣可以在不將圖像數(shù)據(jù)實(shí)際寫(xiě)入磁盤(pán)的情況下,將其作為一個(gè)可訪(fǎng)問(wèn)的資源。 - createImageBitmap和OffscreenCanvas:為了提高圖像處理性能,代碼中使用了
createImageBitmap
來(lái)高效地將Blob轉(zhuǎn)換為位圖,并使用OffscreenCanvas
進(jìn)行離屏渲染。這兩個(gè)API都允許在不影響頁(yè)面渲染的情況下進(jìn)行高性能的圖像處理。 - 灰度圖像處理:在Web Worker內(nèi)部,代碼遍歷了圖像的每個(gè)像素,并將其轉(zhuǎn)換為灰度。這是通過(guò)計(jì)算RGB通道的平均值,并將這個(gè)平均值設(shè)置為新的RGB值來(lái)實(shí)現(xiàn)的。
- 動(dòng)態(tài)創(chuàng)建Web Worker腳本:
worker.js
文件最后將自身代碼轉(zhuǎn)換為字符串,并通過(guò)Blob和URL.createObjectURL
創(chuàng)建了一個(gè)URL,這個(gè)URL被用作Web Worker的腳本源。這是一種動(dòng)態(tài)創(chuàng)建和執(zhí)行JavaScript代碼的技術(shù),允許在不依賴(lài)外部文件的情況下運(yùn)行Web Worker。 - 錯(cuò)誤處理和異常捕獲:在Web Worker內(nèi)部和外部都有錯(cuò)誤處理和異常捕獲的邏輯。例如,在嘗試獲取或處理圖像時(shí)發(fā)生錯(cuò)誤,會(huì)將原始的圖像URL發(fā)送回主線(xiàn)程表示處理失敗。
- 使用nullish coalescing操作符:在JSX中,
width ?? '100%'
和height ?? '100%'
使用了nullish coalescing操作符(??
),這是ES2020引入的新特性。它允許在左側(cè)操作數(shù)為null
或undefined
時(shí)返回右側(cè)的值,否則返回左側(cè)的值。這提供了一種簡(jiǎn)潔的方式來(lái)為變量提供默認(rèn)值。 - 圖片加載完成后的處理:在圖片加載完成后,會(huì)終止當(dāng)前的Web Worker。這有助于釋放資源并避免不必要的后臺(tái)處理。
- 代碼組織和模塊導(dǎo)出:
ImageProcessor
組件和workerScriptURL
都被導(dǎo)出,以便在其他模塊中使用。這體現(xiàn)了良好的模塊化和代碼復(fù)用實(shí)踐。
以上就是React處理復(fù)雜圖片樣式的方法詳解的詳細(xì)內(nèi)容,更多關(guān)于React處理圖片樣式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react-router-dom?v6?使用詳細(xì)示例
這篇文章主要介紹了react-router-dom?v6使用詳細(xì)示例,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,感興趣的小伙伴可以參考一下,希望對(duì)你的學(xué)習(xí)有所幫助2022-09-09ReactNative 之FlatList使用及踩坑封裝總結(jié)
本篇文章主要介紹了ReactNative 之FlatList使用及踩坑封裝總結(jié),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11基于webpack開(kāi)發(fā)react-cli的詳細(xì)步驟
這篇文章主要介紹了基于webpack開(kāi)發(fā)react-cli的詳細(xì)步驟,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-06-06React啟動(dòng)時(shí)webpack版本沖突報(bào)錯(cuò)的解決辦法
在啟動(dòng)React應(yīng)用時(shí),遇到Webpack版本不匹配導(dǎo)致的運(yùn)行錯(cuò)誤,解決方法包括刪除全局及局部的webpack和webpack-cli,然后根據(jù)項(xiàng)目需求安裝特定版本的webpack,本文通過(guò)代碼示例給大家介紹的非常詳細(xì),需要的朋友可以參考下2024-09-09詳解React中的setState執(zhí)行機(jī)制
setState是React組件中用于更新?tīng)顟B(tài)的方法,是類(lèi)組件中的方法,用于更新組件的狀態(tài)并重新渲染組件,本文給大家詳細(xì)介紹了React中的setState執(zhí)行機(jī)制,文中通過(guò)代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2023-12-12手挽手帶你學(xué)React之React-router4.x的使用
這篇文章主要介紹了手挽手帶你學(xué)React之React-router4.x的使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02React+hook實(shí)現(xiàn)聯(lián)動(dòng)模糊搜索
這篇文章主要為大家詳細(xì)介紹了如何利用React+hook+antd實(shí)現(xiàn)聯(lián)動(dòng)模糊搜索功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-02-02