在UEditor中集成秀米图文插件,并完成对秀米图片的转存。

3,081 阅读9分钟

秀米集成到UEditor的配置过程

虽然秀米官方有提供文档说明如何将秀米插件集成至UEditor里(秀米官方文档),但是该文档说明模糊不清,关键配置一句话带过,即使配置成功后还有坑留在里面。所以我重新整理了这个配置过程,回顾一下过程的同时也给以后需要用到的小伙伴提供便利。

1、将秀米图标集成至工具栏,并且成功弹出秀米弹框

1.1、生成秀米的html文件

首先我们需要一个秀米的html文件,这个html里主要就是弹框里的内容,里面有一个iframe指向秀米的网址。

取名xiumi-ue-dialog-v5.html,放入ueditor1_4_3_3文件夹(UE的资源文件夹)下或者ueditor1_4_3_3下的dialogs文件夹里,这个只是涉及到后面引入它的路径,不是很重要,我是放在ueditor1_4_3_3文件夹下的。

<!-- ueditor1\_4\_3\_3/xiumi-ue-dialog-v5.html -->
<!DOCTYPE html>
<!-- saved from url=(0049)http://hgs.xiumi.us/uedit/xiumi-ue-dialog-v5.html -->
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    
    <title>XIUMI connect</title>
    <style>
  html, body {
    padding: 0;
    margin: 0;
  }

  #xiumi {
    position: absolute;
    width: 100%;
    height: 100%;
    border: none;
    box-sizing: border-box;
  }
    </style>
</head>
<body style="">
<iframe id="xiumi" src="//xiumi.us/studio/v5#/paper">
</iframe>
<script type="text/javascript" src="./dialogs/internal.js"></script>
<script>
    var xiumi = document.getElementById('xiumi');
    var xiumi_url = window.location.protocol + "//xiumi.us";
    xiumi.onload = function () {
        xiumi.contentWindow.postMessage('ready', xiumi_url);
    };
    document.addEventListener("mousewheel", function (event) {
        event.preventDefault();
        event.stopPropagation();
    });
    window.addEventListener('message', function (event) {
        if (event.origin == xiumi_url) {
            editor.execCommand('insertHtml', event.data);
            // 这个方法用于触发UE的图片转存接口
            editor.fireEvent('afterpaste');
            dialog.close();
        }
    }, false);
</script>
</body></html>

1.2、增加一个秀米图标按钮

秀米的文档里使用的是UE.registerUI('dialog',fn)的方式动态在UE里添加一个秀米按钮并且在按钮里添加弹出弹框事件,但是这么做可能会影响到系统其他使用UE里的地方同样出现秀米图标,无法按需加载。所以我将此改进成在UE的前端配置中的toolbars数组里添加一个"xiumi"字符串即可展示出秀米的方式。步骤如下:

  1. 添加一个秀米的图标并添加样式。在ueditor1_4_3_3/themes/default/images文件夹里放入一个秀米图标;

  2. 在ueditor1_4_3_3/themes/default/css/ueditor.css 添加俩句样式

    	/* xiumi-dialog */
    	.edui-default .edui-for-xiumi .edui-dialog-content {
    	  width: calc(100vw - 60px) !important;
    	  height: 90vh !important;
    	  overflow: hidden;
    	}
    	
    	.edui-default .edui-for-xiumi .edui-icon {
    	  background-image: url("../images/xiumi-connect-icon.png") !important;
    	  background-size: contain;
    	}
    
    
  3. 在ueditor.all.js修改代码。在iframeUrlMap对象里添加键值对'xiumi': '~/xiumi-ue-dialog-v5.html',这个和刚刚那个html存放的地方有关。在btnCmds数组里添加一个'xiumi'字符串,在dialogBtns对象中的ok数组里添加'xiumi'字符串。

  4. 最后在UE的前端配置里toolbars数组里添加'xiumi',即可成功展示出秀米图标与秀米的弹框。

效果如下

1.3、设置xss过滤白名单

在ueditor.config.js的xss过滤白名单whitList配置里,修改section参数。同时将设置是否抓取远程图片catchRemoteImageEnable设置为true。

section:['class', 'style'],

步骤到这里,如果能成功弹出秀米的弹框,并且能将秀米里的模版成功勾选到本地的UE编辑器里,说明集成就成功了一半啦。

2、秀米域名的图片转存。

要完成秀米图片的转存,首先我们要先完成UE对复制过来的网络图片的转存,图片的转存分为img标签与背景图片的抓取与转存。

2.1、设置抓取白名单catcherLocalDomain。

设置抓取白名单catcherLocalDomain,在白名单里的地址,UE不会对其发起转存请求。可以使用两种方式设置:

  1. 使用UE.utils.extend方法强制在UE实例化后设置

    UE.utils.extend(editor.options, {
        catcherLocalDomain: ['127.0.0.1', 'localhost', "static-alpha-engage.gridsumdissector.com", "static-beta-engage.gridsumdissector.com", "static-uat-engage.gridsumdissector.com", "static.engage-all.com"],
    });
    
  2. 修改ueditor.all.js里的初始配置代码,在这个js文件大概第8100行的地方,有一个loadServerConfig的函数,可以直接在这里将catcherLocalDomain的初始值设置为我们想要过滤的白名单域名。

2.2、修改抓取图片的后端请求地址。

修改抓取图片的后端请求地址。UE发起所有接口请求都依赖于一个名为serverUrl的前端配置,然后通过使用serverUrl这个唯一的请求地址,通过GET参数action指定不同请求类型,比如uploadimage(执行上传图片或截图的action名称), uploadvideo(执行上传视频的action名称), catchimage(执行抓取远程图片的action名称)等等,但是后端为了方便接口的管理,一般会将接口拆分出来,当然UE也支持自定义请求地址。UE推荐的方法如下:

UE.Editor.prototype._bkGetActionUrl = UE.Editor.prototype.getActionUrl;
UE.Editor.prototype.getActionUrl = function(action) {
    if (action == 'uploadimage' || action == 'uploadscrawl' || action == 'uploadimage') {
        return 'http://a.b.com/upload.php';
    } else if (action == 'uploadvideo') {
        return 'http://a.b.com/video.php';
    } else {
        return this._bkGetActionUrl.call(this, action);
    }
}

原理就是使用一个名为_bkGetActionUrl的临时变量承载原来的getActionUrl方法,然后重写getActionUrl方法,判断action的类型,来返回不同的接口,同时也还可以执行原来的getActionUrl方法。 但是我在使用这种方法的时候,会偶尔碰到内存溢出,导致上传图片功能不可用的原因,可能是getActionUrl在其他地方也被重新赋值了,产生循环引用该函数的问题。所以我依旧在ueditor.all.js里修改了getActionUrl原始函数,大概在第8040行代码里,判断getActionUrl函数内部一个actionName的参数,来返回你想要的接口名。

2.3、抓取请求注意点

到这里差不多能够抓取到img标签的src并且可以将其替换了,但需要注意俩点地方:

  1. 注意抓取后,后端返回的数据格式时候和UE里源码设置的一样,在ueditor.all.js找到UE.plugins['catchremoteimage']这个函数,在其内部执行catchremoteimage的success回掉函数的地方注意获取源路径和新路径的数据路径的地方。

  2. UE内部有一个判断是否跨域的方法,如果跨域会使用jsonp的方式请求接口,如果你是本地调试,并且后端已经对跨域crose处理,不需要使用jsonp的方式请求,你可以把它关了。同样在UE.plugins['catchremoteimage']这个函数里,找到定义catchremoteimage函数的地方,将其内部ajax请求的option里,将dataType固定设置为空字符串即可。

2.4、背景图片的抓取与替换

除了img标签的src替换,背景图片也是需要进行图片抓取和替换的。其实就是依葫芦画瓢,看明白了UE是如何对img标签进行src替换的,也就明白该如何对背景图片进行替换了。

  1. 首先你需要在ueditor.all.js文件的domUtils参数里新添一个方法getElementsByTagNameStyle。通过元素的style来获取元素节点数组。

      /**
         * 方法getElementsByTagNameStyle的封装
         * @method getElementsByTagNameStyle
         * @param { e } node 目标节点对象
         * @param { t } tagName 需要查找的节点的tagName, 多个tagName以空格分割
         * @param { i } style 节点对象的筛选条件
         * @return { Array } 符合条件的节点集合
       */
    getElementsByTagNameStyle: function (e, t, i) {
        if (i && utils.isString(i)) {
            var n = i;
            i = function (e) {
                for (var t, i = n.split(","), o = !0, r = e.getAttribute("style"), a = 0; t = i[a++];) if (!r || r.indexOf(t) < 0) {
                    o = !1;
                    break
                }
                return o
            }
        }
        t = utils.trim(t).replace(/[ ]{2,}/g, " ").split(" ");
        for (var o, r = [], a = 0; o = t[a++];) for (var s, l = e.getElementsByTagName(o), d = 0; s = l[d++];) i && !i(s) || r.push(s);
        return r
    },
    
  2. 在UE.plugins['catchremoteimage']下的catchRemoteImage监听函数里添加代码。首先你得的到有背景图片的的元素节点数组。使用刚刚新添的方法

    backgroundimagestags = domUtils.getElementsByTagNameStyle(me.document, "section div p", "background,url")//抓取背景图片所在的标签
    

    然后将该元素节点数组里的图片地址都抽取出来,放在一个存放图片地址的数组里

    var backgroundimages = [];
    for (var i = 0, backci; backci = backgroundimagestags[i++];) {
    
        var bstyle = backci.style;
        var backgroundimgurltag = bstyle['background-image'] || bstyle['background'] || "";
        if (backgroundimgurltag != null && backgroundimgurltag != "") {
            var backsrc = backgroundimgurltag.split("(")[1].split(")")[0].replace(/\"/g, "")
                          || backgroundimgurltag.split("(")[1].split(")")[0].replace(/\"/g, "")
                          || "";
            if (backsrc != null && backsrc != "") {
                if (/^(https?|ftp):/i.test(backsrc) && !test(backsrc, catcherLocalDomain)) {
                    backgroundimages.push(encodeURI(backsrc));
                }
            }
        }
    }
    
  3. 最后依葫芦画瓢,对该图片地址数组循环,依次发起转存请求,并且将对应的节点内的背景图片url进行替换。

if(backgroundimages.length) {
    catchremoteimage(backgroundimages, {
        //成功抓取
        success: function (r) {
            try {
                var info = r.state !== undefined ? r:eval("(" + r.responseText + ")");
            } catch (e) {
                return;
            }

            /* 获取源路径和新路径 */
            var i, j, ci, cj, oldSrc, newSrc, styleText ,list = info.data.list;

            for (i = 0; ci = backgroundimagestags[i++];) {
                styleText = ci.getAttribute("style");
                oldSrc = styleText;
                if (oldSrc.indexOf('url("') > 0) {
                    oldSrc = oldSrc.split('url("')[1].split('")')[0];
                } else if (oldSrc.indexOf("url('") > 0) {
                    oldSrc = oldSrc.split("url('")[1].split("')")[0];
                } else {
                    if (!(oldSrc.indexOf("url(") > 0)) continue;
                    oldSrc = oldSrc.split("url(")[1].split(")")[0];
                }
                if (oldSrc.indexOf("?") >= 0) {
                    oldSrc = oldSrc.split("?")[0];
                }
                for (j = 0; cj = list[j++];) {
                    if (oldSrc == cj.source && "SUCCESS" == cj.state) {
                        newSrc = catcherUrlPrefix + cj.url, styleText = styleText.replace(oldSrc, newSrc), domUtils.setAttributes(ci, { style: styleText }), domUtils.setAttributes(ci, { is_updata: "true" });
                        break
                    }
                }
            }
            me.fireEvent('catchremotesuccess')
        },
        //回调失败,本次请求超时
        error: function () {
            me.fireEvent("catchremoteerror");
        }
    });
}

2.5、抓取图片注意点

到这里,UE对普通的img标签及背景图片的抓取与转存已经成功了,如果想要对秀米过来的图片进行转存需要注意以下俩点:

  1. 对某些秀米图片地址的后缀进行处理,去掉?号之后类似?x-oss-process=所有的部分。这个在catchremoteimage函数里对imgs进行一步map循环,将?后面的东西去掉即可,同时注意在图片地址数组循环的过程中oldSrc?后面的内容也要去掉,否则oldSrc == cj.source && "SUCCESS" == cj.state这一步判断不会通过,就无法进行图片地址的替换了。

  2. 从秀米回到本地UE编辑器的时候不会触发图片转存请求,这也是秀米文档埋下的一个坑!解决这个问题,你需要在xiumi-ue-dialog-v5.html文件里,在dialog.close();代码前添加一句editor.fireEvent('afterpaste');来主动触发afterpaste这个监听函数。

3、实现前端对图片资源的下载

  1. 下载目标图片资源,我们通过js内置的方法fetch访问图片地址,并且通过blob函数将挂起一个流操作并且在完成时读取其值,得到 一个blob类型的数据。
async function getImageBlob(url) {
    let blob;
    await fetch(url).then(r => {
        return r.blob()
    }).then(imgBlob => {
        blob = imgBlob;
    });
    return blob;
}
  1. 调用后端接口上传至服务器,得到对应的在线地址。
await fetch(`https://${host}/platform/api/v1/upload`, {
    body: fd,
    headers: {},
    method: 'POST',
    timeout: 20000,
    credentials: 'include',
  })
  .then(response => response.json())
  .then(data => {
      if (data.errorCode === 0) {
        callback(data.data.file);
      } else {
        callback(null);
      }
  });

接下来的操作流程就是将图片地址替换,因为上传的接口目前只支持单张图片的上传,所以如果进来的图片较多,上传的接口也会比较多,会造成图文内容中的图片发生短暂的空白情况。

4、成功效果预览