前言
因为老大突然说 网站后台负责维护的人员 上传到富文本(为了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();
});
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/
- 字符| 表达式 | 描述 || — | — ||
[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
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…地址
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, 需要一一匹配才能替换
暂时想到只有这样, 大佬们如果有更好想法也可以告诉.
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的提问。
有需要的小伙伴,可以点击下方卡片领取,无偿分享
版权归原作者 前端开发小陈 所有, 如有侵权,请联系我们删除。