使用 Editor.md 图片上传出现的问题及解决思路
Spring Boot 2.2.4.RELEASE
问题及解决思路
须知:项目是前后端分离,涉及跨域。
测试使用的是 Editor.md 提供的示例“image-upload.html”,该文件位于 examples 文件夹。
点击“本地上传”按钮,选择图片并上传,服务器端能接收到图片。
正常情况下,如果图片上传成功,且服务器端响应正确的数据,那么添加图片对话框中的图片地址应该回显服务器端返回的 URL。
但是,当我成功上传图片之后,客户端(浏览器)提示安全错误“SecurityError: Permission denied to access property “document” on cross-origin object”,如下:
根据提示,猜想“cross-origin object”是“跨源对象”,那么“SecurityError: Permission denied to access property “document” on cross-origin object”的大意应该是“无权访问跨源对象”。
打开 image-dialog.js 文件,定位到第 164 行,可以看到这里尝试:
var body = (uploadIframe.contentWindow ? uploadIframe.contentWindow : uploadIframe.contentDocument).document.body;
从代码字面意思,猜想应该是要取某个 document
的 body
。
有点模糊?嗯,还是先把 uploadIframe.contentWindow
和 uploadIframe.contentDocument
打印到控制台,看看它们是什么。修改 image-dialog.js 文件,如下:
if(log) { // 自定义的变量,var log = true;。如果后期处于调试阶段,又不想打印这些信息,将其设置 false 即可。
console.log("uploadIframe.contentWindow", uploadIframe.contentWindow);
console.log("uploadIframe.contentDocument", uploadIframe.contentDocument);
}
var body = (uploadIframe.contentWindow ? uploadIframe.contentWindow : uploadIframe.contentDocument).document.body;
修改完成并保存之后,回到浏览器,再次上传图片,故意触发该提示:
有点线索了,uploadIframe.contentDocument
为 null
,那么我们可以先不关注它。
再看看 uploadIframe.contentWindow
指向一个 Window
,且 URL 指向服务器端的上传接口,刚好也是 image-upload.html 文件中 editormd
配置的 imageUploadURL
参数值:
var testEditor = editormd("test-editormd", {
... // 其他配置
imageUpload : true,
imageFormats : ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
imageUploadURL : "http://localhost:8080/attachment/image/upload",
...
});
通过选取页面中的元素发现,以上提到 URL 刚好和图片上传的表单 action
一样:
由此猜想,上文提及的安全错误“SecurityError: Permission denied to access property “document” on cross-origin object”是由于 image-dialog.js 文件中的代码尝试访问图片上传的表单中的元素导致的:
通过查看 image-dialog.js 文件中的源码,可以发现 submitHandler
函数(第 1 ~ 18 行)绑定在 type="submit"
对象(暂时这么称呼,此处描述不准确)的点击事件(第 20 行):
var submitHandler = function() {
...
uploadIframe.onload = function() {
...
if(log) {
console.log("uploadIframe.contentWindow", uploadIframe.contentWindow);
console.log("uploadIframe.contentDocument", uploadIframe.contentDocument);
}
var body = (uploadIframe.contentWindow ? uploadIframe.contentWindow : uploadIframe.contentDocument).document.body;
...
return false;
};
};
dialog.find("[type=\"submit\"]").bind("click", submitHandler).trigger("click");
猜想 dialog.find("[type=\"submit\"]")
是一个提交按钮,可能是添加图片对话框中的本地上传按钮,因为当我们点击本地上传之后,就触发安全错误,而该错误又是执行 (uploadIframe.contentWindow ? uploadIframe.contentWindow : uploadIframe.contentDocument).document.body;
导致的,见以上 代码的第 12 行。
我们验证一下,修改 image-dialog.js 文件,看看 dialog.find("[type=\"submit\"]")
是什么:
if (log) {
console.log('dialog.find("[type=\"submit\"]")', dialog.find("[type=\"submit\"]"));
}
dialog.find("[type=\"submit\"]").bind("click", submitHandler).trigger("click");
再次上传图片,可以看到 dialog.find("[type=\"submit\"]")
返回的对象确实和本地上传按钮有关系!
根据 dialog.find("[type=\"submit\"]")
提供的线索,继续查看 image-dialog.js 文件,发现 type="submit"
的本地上传按钮,即有可能是在这块代码定义:
var dialogContent = ( (settings.imageUpload) ? "<form action=\"" + action +"\" target=\"" + iframeName + "\" method=\"post\" enctype=\"multipart/form-data\" class=\"" + classPrefix + "form\">" : "<div class=\"" + classPrefix + "form\">" ) +
( (settings.imageUpload) ? "<iframe name=\"" + iframeName + "\" id=\"" + iframeName + "\" guid=\"" + guid + "\"></iframe>" : "" ) +
"<label>" + imageLang.url + "</label>" +
"<input type=\"text\" data-url />" + (function(){
return (settings.imageUpload) ? "<div class=\"" + classPrefix + "file-input\">" +
"<input type=\"file\" name=\"" + classPrefix + "image-file\" accept=\"image/*\" />" +
"<input type=\"submit\" value=\"" + imageLang.uploadButton + "\" />" +
"</div>" : "";
})() +
"<br/>" +
"<label>" + imageLang.alt + "</label>" +
"<input type=\"text\" value=\"" + selection + "\" data-alt />" +
"<br/>" +
"<label>" + imageLang.link + "</label>" +
"<input type=\"text\" value=\"http://\" data-link />" +
"<br/>" +
( (settings.imageUpload) ? "</form>" : "</div>");
请耐心看一下代码,此处有两个关键点:
<form>
元素的action
属性,见第 1 行。- 第 7 行,
<input>
元素的类型(type)是submit
。现在它的value
是未知的。
我们可以看看这两者是什么,修改 image-dialog.js 文件,将它们打印到控制台(第 1 ~ 4 行):
if (log) {
console.log("action: ", action);
console.log("imageLang.uploadButton: ", imageLang.uploadButton);
}
var dialogContent = ( (settings.imageUpload) ? "<form action=\"" + action +"\" ... +
... // 其它,省略
从控制台的输出,我们可以看到,action
对应服务器的上传接口,而 imageLang.uploadButton
,即上文提到的 <input>
元素的 value
值,刚好对应添加图片对话框的本地上传按钮:
我们通过选取页面中的元素,可以看到图片上传表单的 action
和本地上传提交按钮与上面提到的信息刚好吻合:
由此,可以推测 var dialogContent = ( (settings.imageUpload) ? "<form action=\"" + action + ... );
主要功能是创建图片上传表单。
由于图片上传表单的 action
与 image-dialog.js 文件不同源,导致 image-dialog.js 文件中的代码不能访问图片上传表单的元素。
那么,该如何解决安全错误呢?
嗯,不妨使用 AJAX 代替表单提交功能。
如果使用 AJAX 上传图片,就不需要设置图片上传表单的 action
,因此,我们可以注释 editormd
配置中的 imageUploadURL
属性:
var testEditor = editormd("test-editormd", {
...
imageUpload : true,
imageFormats : ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
// imageUploadURL : "http://localhost:8080/attachment/image/upload",
...
});
这样,添加图片表单 action
就变成这样子:
上图中的添加图片表单 action
中的 guid
是一个时间戳,其在 image-dialog.js 文件中定义:
if (editor.find("." + dialogName).length < 1)
{
var guid = (new Date).getTime();
var action = settings.imageUploadURL + (settings.imageUploadURL.indexOf("?") >= 0 ? "&" : "?") + "guid=" + guid;
...
}
或者,我们可以直接修改 image-dialog.js 文件中 action
变量,将其置空(第 8 行):
if (editor.find("." + dialogName).length < 1)
{
var guid = (new Date).getTime();
var action = settings.imageUploadURL + (settings.imageUploadURL.indexOf("?") >= 0 ? "&" : "?") + "guid=" + guid;
...
action = "";
var dialogContent = ( (settings.imageUpload) ? "<form action=\"" + action + "\" ...
}
清除了表单的 action
属性之后,我们需要禁用本地上传的提交功能。修改 image-dialog.js 文件,在 submitHandler
函数中的末尾添加 return false;
,并关闭加载效果:
var submitHandler = function() {
var uploadIframe = document.getElementById(iframeName);
uploadIframe.onload = function() {
...
};
loading(false); // 关闭加载效果
return false;
};
现在,通过本地上传按钮选择一张图片并打开,是不会将图片上传至服务器。
接下来,让我们看看 uploadIframe.onload
干了什么。这里有三个关键信息:
- 第 8 行的
json.success
; - 第 9 行的
json.url
; - 第 12 行的
json.message
。
var submitHandler = function() {
...
uploadIframe.onload = function() {
...
if(!settings.crossDomainUpload) {
if (json.success === 1) { // 上传成功
dialog.find("[data-url]").val(json.url);
}
else {
alert(json.message);
}
}
return false;
};
loading(false); // 关闭加载效果
return false;
};
这三个关键信息,刚好对应 image-upload.html 示例中提到的 JSON 数据格式(第 8 ~ 13 行):
var testEditor = editormd("test-editormd", {
...
imageUpload : true,
imageFormats : ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
// imageUploadURL : "http://localhost:8080/attachment/image/upload",
/*
上传的后台只需要返回一个 JSON 数据,结构如下:
{
success : 0 | 1, // 0 表示上传失败,1 表示上传成功
message : "提示的信息,上传成功或上传失败及错误信息等。",
url : "图片地址" // 上传成功时才返回
}
*/
});
回到 image-dialog.js 文件中的 submitHandler
函数,让我们看看 dialog.find("[data-url]")
是什么,将其打印到控制台(第 3 行):
var submitHandler = function() {
if(log) {
console.log('dialog.find("[data-url]")', dialog.find("[data-url]"));
}
...
loading(false); // 关闭加载效果
return false;
};
可以看到,dialog.find("[data-url]")
对应图片地址:
那么,dialog.find("[data-url]").val(json.url);
的作用就是 – 当图片上传成功之后,将服务器端返回的 URL 填入添加图片对话框中的图片地址一项(第 9 行):
var submitHandler = function() {
...
uploadIframe.onload = function() {
...
if(!settings.crossDomainUpload) {
if (json.success === 1) { // 上传成功
dialog.find("[data-url]").val(json.url);
}
else {
alert(json.message);
}
}
return false;
};
loading(false); // 关闭加载效果
return false;
};
当目前为止,以上的分析大多都是猜测。接下来,让我们验证一下。修改 image-dialog.js 文件中的 submitHandler
函数,注释与 uploadIframe.onload
相关代码:
var submitHandler = function() {
// var uploadIframe = document.getElementById(iframeName);
// uploadIframe.onload = function() {
// loading(false);
// var body = (uploadIframe.contentWindow ? uploadIframe.contentWindow : uploadIframe.contentDocument).document.body;
// var json = (body.innerText) ? body.innerText : ( (body.textContent) ? body.textContent : null);
// json = (typeof JSON.parse !== "undefined") ? JSON.parse(json) : eval("(" + json + ")");
// if(!settings.crossDomainUpload)
// {
// if (json.success === 1)
// {
// dialog.find("[data-url]").val(json.url);
// }
// else
// {
// alert(json.message);
// }
// }
// return false;
// };
loading(false); // 关闭加载效果
return false;
};
使用 AJAX 上传图片:
var submitHandler = function() {
var form = dialog.find("[enctype=\"multipart/form-data\"]")[0];
var formData = new FormData(form);
$.ajax({
type: 'post',
// url: "http://localhost:8080/attachment/image/upload", // 你的服务器端的图片上传接口。如果你设置了 imageUploadURL,那么可以使用下面的方式
url: settings.imageUploadURL + (settings.imageUploadURL.indexOf("?") >= 0 ? "&" : "?") + "guid=" + guid,
data: formData,
cache: false,
processData: false,
contentType: false,
success: function(data, textStatus, jqXHR) {
// console.log(data);
// console.log(textStatus);
// console.log(jqXHR);
if (data.success === 1) { // 上传成功
dialog.find("[data-url]").val(data.url); // 设置图片地址
}
else {
alert(data.message); // 上传失败,弹出警告信息
}
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
// console.log(XMLHttpRequest);
// console.log(textStatus);
// console.log(errorThrown);
}
});
loading(false); // 关闭加载效果
return false;
};
现在,当我们成功上传一张图片之后,就能看到图片地址:
点击确定按钮,也能将图片地址插入到 Markdown 编辑器中: