使用 Editor.md 图片上传出现的问题及解决思路

使用 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;

从代码字面意思,猜想应该是要取某个 documentbody

有点模糊?嗯,还是先把 uploadIframe.contentWindowuploadIframe.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.contentDocumentnull,那么我们可以先不关注它。

再看看 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>");

请耐心看一下代码,此处有两个关键点:

  1. <form> 元素的 action 属性,见第 1 行。
  2. 第 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 干了什么。这里有三个关键信息:

  1. 第 8 行的 json.success
  2. 第 9 行的 json.url
  3. 第 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 编辑器中:
在这里插入图片描述

参考

Editor.md 开源在线 Markdown 编辑器

Spring Boot - AJAX 跨域图片上传

发布了55 篇原创文章 · 获赞 0 · 访问量 3178

猜你喜欢

转载自blog.csdn.net/qq_29761395/article/details/104194166