0


[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

的属性介绍
属性类型说明dropEffectString默认是 noneeffectAllowedString默认是 uninitializedfilesFileList粘贴操作为空ListitemsDataTransferItemList剪切板中的各项数据typesArray剪切板中的数据类型 该属性在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带有样式的htmlFiles文件(例如剪切板中的数据)
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的提问。



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

标签: javascript html 前端

本文转载自: https://blog.csdn.net/weixin_53312997/article/details/129364324
版权归原作者 前端开发小陈 所有, 如有侵权,请联系我们删除。

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

还没有评论