[Vue3] Wangeditor富文本实现将粘贴中包含的图片自动上传后台,并替换src

前言

 因为老大突然说 网站后台负责维护的人员 上传到富文本(为了SEO就不用贴图)的内容, 
 一些图片显示失败,我一看还真是
之前好不容易弄好这个富文本的图片上传功能(就是点击图片, 选择上传)
还真没有想到他们直接扒过来别的网站内容(尤其里面包含图片)
这时候加入这个网站设置跨域, 图片就会因为跨域显示403失败,无法加载出来. 

吐槽: 还以为他们富文本把文字写好, 在一个个上传图片, 组成一片文章. 那就没有办法了, 只能修改下代码增加下自动上传图片功能. 后来写到一半才发现html 有个属性好像可以让跨域的图片 显示出来. <meta name="referrer" content="no-referrer" />

 暂时不太理解这个代码, 也不清楚有没有副作用, 希望有懂的大佬说下. 

以为这样就可以不用写了, 但是老大说 万一以后别人网站的图片不维护了, 那这个引用还是导致图片显示失败, 还是上传到后台保险. 嘚, 代码还是要写.


1. 具体思路

​ 因为自己代码写得很烂, 就把关键的代码贴出来供大家参考, 当然不止WangEditor富文本编辑器能用, 其他地方需要粘贴时候自动上传图片也能实现, 原理都是一样的

​ (无非其他地方需要 自己选择DOM节点, 触发粘贴事件, 然后具体完成后, 在这个DOM节点插入 处理好的内容)

1.1 介绍过程

概念会如下再介绍, 先说说具体过程, 就是

  • 首先通过粘贴事件触发, 停止默认粘贴事件, 获取其text/html的内容
  • 使用字符串正则 match匹配 内容中符合 <img … src= “…”>这样的内容, 获得匹配字符串数组
  • 对数组遍历, 传入url在图片onload加载好后触发回调函数, 会将图片转为base64
  • base64 转 Blob
  • Blob 转 file
  • 将file 传入请求上传后台函数
  • 当全部图片上传后, 我是创建一个Map类型, 通过replace去匹配替换html中的src的内容
  • 将其insert 插入

1.2 介绍概念

先跟大家介绍一些用到的概念, 方便后续理解

1.1.1 Paste粘贴事件

当用户在浏览器用户界面发起“粘贴”操作时,会触发 paste 事件。

触发大致代码如下:

const target = document.querySelector('div.target');

target.addEventListener('paste', (e) => {...
}); 

具体操作

1.获取事件对象

粘贴事件提供了一个clipboardData的属性,如果该属性有items属性,那么就可以查看items中是否有图片类型的数据了

clipboardData介绍

它实际上是一个DataTransfer类型的对象

clipboardData的属性介绍

属性 类型 说明
dropEffect String 默认是 none
effectAllowed String 默认是 uninitialized
files FileList 粘贴操作为空List
items DataTransferItemList 剪切板中的各项数据
types Array 剪切板中的数据类型 该属性在Safari下比较混乱

方法

1.getData()

事件处理程序可以通过调用事件的 clipboardData 属性上的 getData()访问剪贴板内容。

2.event.preventDefault()

要覆盖默认行为(例如,插入一些不同的数据或转换剪贴板的内容),事件处理程序必须使用 event.preventDefault(),取消默认操作,然后手动插入想要的数据。

items介绍

它是一个DataTransferItemList对象

items的属性介绍

属性 说明
kind 一般为string或者file
type 具体的数据类型,例如具体是哪种类型字符串或者哪种类型的文件,即MIME-Type

types属性介绍

我们中所需要的就是text/html 该值

比如只 复制一张图片 就是Files文件

如果复制网站一大串的html内容, 就是text/hmtl 属性

说明
text/plain 普通字符串
text/html 带有样式的html
Files 文件(例如剪切板中的数据)

Demo

本次粘贴内容以 电磁辐射的百度百科 部分html内容为例

target.addEventListener('paste', (event) => {// 获得 事件(text/html 富文本)的 内容let paste = (event.clipboardData || window.clipboardData).getData("text/html");//具体操作console.log(paste)
	...//阻止默认粘贴事件, 之后在将处理的内容insert插入event.preventDefault();
}); 

image-20230226173014461

1.1.2 image事件

因为涉及到后面图片 转 base64

image对象是JS中内置的对象, 当我们创建一个Image对象, 其实就是给浏览器缓存一张图片,

在创建image对象后, width height默认0, 需要赋值, 同时还有src

这里重点就是 onload事件

当image的src发生改变,浏览器就会跑去加载这个src里的资源。这个操作是异步的.

就是说,js不会傻傻地在原地等待图片的加载,而是继续读代码,直到图片加载完成,触发onload事件,js才会回来执行onload里面的内容。

1.1.3 base64 & Blob & File

因为上传到后台的请求时, 需要传入File类型, 而我们一开始只有url

BASE64

图片的 base64 编码就是可以将一副图片数据编码成一串字符串,使用该字符串代替图像地址。

场景中,图片的下载始终都要向服务器发出请求,要是图片的下载不用向服务器发出请求,而可以随着 HTML 的下载同时下载到本地那就太好了,而 base64 正好能解决这个问题。

一般如下

<!-- 在html代码img标签里的写法 -->
<img src="data:image/gif;base64,R0lGODlhHAAmAKIHAKqqqsvLy0hISOffffm5vf394uLiwAAAP///yH5B…EoqQqJKAIBaQOVKHAXrgBjboSvB8EpLoFZywOAo3LFE5lYs/QW9LT1TRk1V7S2xYJADs="> 

Blob

一个 Blob对象表示一个不可变的, 原始数据的类似文件对象。Blob表示的数据不一定是一个JavaScript原生格式 blob对象本质上是js中的一个对象,里面可以储存大量的二进制编码格式的数据。

创建blob对象

创建blob对象本质上和创建一个其他对象的方式是一样的,都是使用Blob() 的构造函数来进行创建。 构造函数接受两个参数:

第一个参数为一个数据序列,可以是任意格式的值。

第二个参数是一个包含两个属性的对象{ type: MIME的类型, endings: 决定第一个参数的数据格式,可以取值为 “transparent” 或者 “native”(transparent的话不变,是默认值,native 的话按操作系统转换) 。 }

File

一个FileList 对象通常来自于一个 HTML input 元素的 files 属性,你可以通过这个对象访问到用户所选择的文件,或者拖拽文件

File 的构造函数很简单,使用 new File() 即返回一个新创建的文件对象

1.1.4 字符串操作

1.replace> replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。replace(" "," ") // 替换字符串中的字符(区分大小写)" "会自动转化成Regexp(正则表达式 / /)var a = "Visit Microsoft!";var b = a.replace("Microsoft","W3School");console.log(b); // Visit W3School! 2.match> match() 方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。

var str="The rain in SPAIN stays mainly in the plain"; 
var n=str.match(/ain/g);
// 结果ain,ain,ain 

1.1.5 正则

基本概念大致如下, 其他具体可以自己在查

有一个网站能够检验自己的公式regex101.com/

image-20230227095042618

  • 字符| 表达式 | 描述 || — | — || [abc] | 字符集。匹配集合中所含的任一字符。 || [^abc] | 否定字符集。匹配任何不在集合中的字符。 |* 分组和引用| 表达式 | 描述 || — | — || (expression) | 分组。匹配括号里的整个表达式。 |* 锚点和边界* 数量表示| 表达式 | 描述 || — | — || ? | 匹配前面的表达式0个或1个。即表示可选项。 || + | 匹配前面的表达式至少1个。 || * | 匹配前面的表达式0个或多个。 |* 预查断言* 特殊标志/…/g 全局匹配2. 过程思路
    ========

2.1 引入 & 富文本API

需要引入的

<script lang="ts" setup>
import md5 from "blueimp-md5"; // md5加密,后续会为了方便匹配(可以github搜这个blueimp-md5)
import { ElLoading } from "element-plus"; // loading优化体验
import { baseRequest } from "/@/api/invoke"; 

首先因为用到了wangEditor, 会有一些API

image-20230227103340857

image-20230227105634295

2.2 转换函数

一些转换函数

// 画布图片转base64
function imageToBase64(img) {let canvas = document.createElement("canvas"); // 创建一个canvas对象// 初始化canvas.width = img.naturalWidth;canvas.height = img.naturalHeight;// 也是初始化, getContext("2d")这个方法表示创建一个2d的画布, 详情可以看文档let ctx = canvas.getContext("2d");// 把我们创建的图片传入, 画布创建ctx.drawImage(img, 0, 0, img.width, img.height);let ext = img.src.substring(img.src.lastIndexOf(".") + 1).toLowerCase(); // 拿后缀png这些// 我们要的base64就拿到了let dataURL = canvas.toDataURL("image/" + ext);return dataURL;
} 
// 创建image对象回调
export const getImage = (url, callback) => {let image = new Image();image.setAttribute("crossOrigin", "*"); // 跨域image.src = url;//image.src = url + "?v=" + Math.random(); // 处理缓存,fix缓存bug,有缓存,浏览器会报错;// onload事件, image一旦加载玩就会触发image.onload = () => {let base64 = imageToBase64(image); // 这里就将我们的图片传入canvas了// 因为实在onload事件内, 所以结束要以回调的形式返回callback && typeof callback == "function" && callback(base64, url);};
}; 

src=“data:image/gif;base64,R0lGODlhHAAmAKIHAKqqqsvLy0hISOffffm5vf394uLiwAAAP///yH5B…EoqQqJKAIBaQOVKHAXrgBjboSvB8EpLoFZywOAo3LFE5lYs/QW9LT1TRk1V7S2xYJADs=”

split通过 , 分割获取上面后面内容

// base64转 Blob
export const dataURLtoBlob = dataurl => {var arr = dataurl.split(","),mime = arr[0].match(/:(.*?);/)[1], //获取前面的类型bstr = atob(arr[1]), // 获取后面内容n = bstr.length,u8arr = new Uint8Array(n); // 这里好像都是Uint8Array这个类型, 但不是太懂希望大佬告知while (n--) {u8arr[n] = bstr.charCodeAt(n);}return new Blob([u8arr], { type: mime });
};

//2,再将blob转换为file
export const blobToFile = (theBlob, fileName) => {theBlob.lastModifiedDate = new Date(); // 文件最后的修改日期theBlob.name = fileName; // 文件名return new File([theBlob], fileName, {type: theBlob.type,lastModified: Date.now()});
}; 

2.3 请求上传函数

请求上传函数, 这是我们那里的逻辑

const urlArray = reactive(new Map()); //存储 url

function uploadImage(file, filename) {// 上传函数VITE是自己的后台地址
let data = new FormData();
data.append("file", file);
const config = {method: "post",url: VITE... + "/admin/upload", // 上传图片地址 // 大致像这样 url: "http://192.168../.../admin/upload", //上传图片地址headers: { "Content-Type": "multipart/form-data", token: getToken() //这是我们的 需要token, 没有不写 }, data: data
};axios(config).then(res => {const fileUrl = res.data.data.fileUrl; //获取后台设定传回的urlurlArray.set(filename, fileUrl); //添加到一个Map中});//return urlArray;
} 

2.4 自定义粘贴

下面继续, 通过这个方法会触发自定义粘贴

const customPaste = (editor, event, callback) => {openLoading();let html = event.clipboardData.getData("text/html"); // 获取粘贴的 html tempHtml.value = html; // 中间变量let srcArray = html.match(/<img [^>]*src=['"]([^'"]+)[^>]*>/g); // 匹配获取srcif (srcArray) { //增加的中间变量	tempArray.value = srcArray;	}// onlaod 回调函数	function fn(dataURL, url) {	const filename = md5(url); // md5 保证该url唯一	let file1 = blobToFile(dataURLtoBlob(dataURL), filename); // blob 转 file	//输入标志 	 	uploadImage(file1, filename); // 请求上传函数	}//如果含有图片的html,执行getImage函数(会获得base64) if (srcArray) {	srcArray.forEach((item, index) => {	getImage(item.match(/src=['"]([^'"]+)/)[1], fn);	});
 	 } else { closeLoading(); //关闭loading	editor.dangerouslyInsertHtml(html); // 正常插入 }// 返回 false ,阻止默认粘贴行为	event.preventDefault();	callback(false); // 返回值(注意,vue 事件的返回值,不能用 return)
} 

2.4 watch监听

最后watch, 发现图片全部上传完成, 大致思路是, 新建中间变量

获取中间图片总数组长度, 监听watch的size, 相等时表示图片全部上传完成并获取到url

这时候开始替换

let tempArray = ref([]); // 获取url的数组
let tempHtml = ref(null); // 获取截取html内容
watch(() => urlArray,(value, oldValue) => {if (urlArray.size == tempArray.value.length) { //监听watch的size, 相等时表示图片全部上传完成并获取到urlif (tempArray.value) {tempArray.value.forEach((item, index) => {//html.replace(item.match(/src=['"]([^'"]+)/)[1], urlArray[index]);const match1 = item.match(/src=['"]([^'"]+)/)[1]; //与之前相同匹配srctempHtml.value = tempHtml.value.replace(match1, // srcurlArray.get(md5(match1))//之前Map set存的url );/*tempHtml.value = newhtml;*/if (tempArray.value.length == index + 1) { // 不知道怎么结束暂时瞎写的closeLoading(); //关闭loadingeditorRef.value.dangerouslyInsertHtml(tempHtml.value); //插入tempHtml.value = null; //重置tempArray.value = [];urlArray.clear(); // 清空Map}});}}},{deep: true,immediate: true}
); 

最终效果如下, 百度百科的图片地址 被 替换我们 后台的 121…地址

image-20230227130405446

3 错误和总结

萌新, 方法和监听watch写的有点混乱, 想抽离出去, 但是很多内容要监听事件才能获取, 暂时没有弄得优雅,只想着实现就行

3.1 如何替换

​ 另外就是 在写代码的 过程中, 遇到一个bug, 一直弄不好, 后来发现是由于没有彻底理解 image.onload()这个方法, 在很多图 同时for循环时

  • onlaod 表示触发图片加载完成, 但其实他们因为各种原因, 加载速度导致顺序不一定, 但是我以为遍历按顺序(例如复制的html包含图片A,B,C, D但是其实onlaod加载顺序可能是D, B, C, A) 如果把后台返回的url 放到一个数组里, 在依次取出数组里的url 去替换, 结果发现每次图片顺序不同导致 图片替换错误,(导致展示图片D, B C, A) 一直找了很久.> 如这个从浏览器缓存看到 每次顺序不一样, 所以特地用来Map, 需要一一匹配才能替换

暂时想到只有这样, 大佬们如果有更好想法也可以告诉.

image-20230227125137551

3.2 如何验证图片全部上传完毕

关于这个我是监听 Map的size 和 粘贴事件中 匹配图片数组的 长度相等时

验证, 但不是很喜欢watch, 喜欢大佬有更好办法提出

3.3 总结

在依次取出数组里的url 去替换, 结果发现每次图片顺序不同导致 图片替换错误,(导致展示图片D, B C, A) 一直找了很久.> 如这个从浏览器缓存看到 每次顺序不一样, 所以特地用来Map, 需要一一匹配才能替换

暂时想到只有这样, 大佬们如果有更好想法也可以告诉.

[外链图片转存中…(img-AIXykEdz-1678088945405)]

3.2 如何验证图片全部上传完毕

关于这个我是监听 Map的size 和 粘贴事件中 匹配图片数组的 长度相等时

验证, 但不是很喜欢watch, 喜欢大佬有更好办法提出

3.3 总结

这个自动上传功能说句实话很难, 主要一步步了解这些概念, 理解后才能继续写下去,总之花了很多时间, 另外第一次掘金写文章, 很多排版和设计不太美观, 希望大家给出建议, 或者哪里有学习参考文章 也可以推荐给我!

最后

整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

猜你喜欢

转载自blog.csdn.net/weixin_53312997/article/details/129364324