likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 15

[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默认是 none
effectAllowedString默认是 uninitialized
filesFileList粘贴操作为空List
itemsDataTransferItemList剪切板中的各项数据
typesArray剪切板中的数据类型 该属性在Safari下比较混乱

方法

  1. getData()

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

  1. 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();
});

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

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/

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

  • 字符

    表达式描述
    [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

这是wangeditoe的网站www.wangeditor.com/v5/editor-c…

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

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

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; //获取后台设定传回的url
    urlArray.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); // 匹配获取src
    
    if (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, 相等时表示图片全部上传完成并获取到url
      if (tempArray.value) {
        tempArray.value.forEach((item, index) => {
          //html.replace(item.match(/src=['"]([^'"]+)/)[1], urlArray[index]);
          const match1 = item.match(/src=['"]([^'"]+)/)[1]; //与之前相同匹配src
          tempHtml.value = tempHtml.value.replace(
            match1, // src
            urlArray.get(md5(match1))  //之前Map set存的url 
          );
          /*tempHtml.value = newhtml;*/
          if (tempArray.value.length == index + 1) { // 不知道怎么结束暂时瞎写的
            closeLoading(); //关闭loading
            editorRef.value.dangerouslyInsertHtml(tempHtml.value); //插入
            tempHtml.value = null; //重置
            tempArray.value = [];
            urlArray.clear(); // 清空Map
          }
        });
      }
    }
  },
  {
    deep: true,
    immediate: true
  }
);

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

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

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, 需要一一匹配才能替换

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

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

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

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

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

3.3 总结