因为老项目还在用Ueditor,现在又需要集成秀米,期间踩了很多坑,在这里做个笔记。
1.官方文档失效
我们之前用的是haochuan9421大佬的vue-ueditor-wrap版本,github上可以看到文档。
点击这里:vue-ueditor-wrap源码-github
关于集成秀米,实际上官方文档已经写了:
但是这个链接已经打不开了,在网上搜也几乎没有vue-ueditor-wrap版本的资料。没关系,进入源码里的docs文件夹,可以找到xiumi.md,网上其余资料较为复杂,使用官方提供的资料则几行代码就可解决。里面清晰的讲述了集成秀米的两种方式。
我这里选择的是使用vue-dependencies方式集成:
第一步:下载所需文件,这里秀米官方提供了文件
秀米官方集成文档
可以看到里面需要你下载四个文件,按要求下载就好。
第二步:打开你已经做好的vue-ueditor-wrap文件。应该封装了一个vue文件作为组件使用,具体怎么封装同样参考:
vue-ueditor-wrap源码-github
封装好后先看.vue文件,使用子组件提供的editor-dependencies即可,这里editorDependencies里放了四个文件,如果突然报了一些语法方面的错,如缺少“<”、“UE is udefined”之类的奇怪错误,就去仔细检查一下路径,路径建议省略“/public”。
第三步:在config中将远程图片抓取打开:catchRemoteImageEnable: true,
(记得在serverUrl
配好后端服务器地址)。其余都是常规配置,不再赘述。完整源码如下:
<template>
<vue-ueditor-wrap
v-model="msg"
editor-id="editor-with-xiumi"
:config="editorConfig"
:editor-dependencies="editorDependencies"
></vue-ueditor-wrap>
</template>
<script>
export default {
data() {
return {
msg: '',
};
},
created() {
this.editorConfig = {
serverUrl: '//ueditor.zhenghaochuan.com/cos',
UEDITOR_HOME_URL: '/UEditor/',
catchRemoteImageEnable: true, // 抓取远程图片
// whiteList 已经在 ueditor.config.js 里改过了,此处略
};
// 指定依赖的资源列表,下面的 JS 会依次加载,注意顺序。实际请求资源时会拼接上 UEDITOR_HOME_URL,当然你也可以填完整的 URL
this.editorDependencies = [
'ueditor.config.js',
'ueditor.all.min.js',
// 添加秀米相关的资源
'xiumi/xiumi-ue-dialog-v5.js',
'xiumi/xiumi-ue-v5.css',
];
},
};
</script>
到这里启动项目后应该就能看到秀米图标了,类似这样:
但是有些时候可能看不到图标,但是鼠标移上去会发现是已经可以点击的,只是没有图标。这时候去 点击这里 另存为或下载图标到你的项目文件里,然后将 xiumi-ue-v5.css文件里的背景路径替换掉就行了,类似这样:
.edui-button.edui-for-xiumi-connect .edui-button-wrap .edui-button-body .edui-icon {
background-image: url("/xiumi-connect-icon.png") !important;
background-size: contain;
}
进行到这里都不算困难。如果你发现都配置好以后各项功能都可以如期运行,那么就没有必要再看下去了。
==============================================================================
图片转存
点击秀米图标,就可以出现iframe弹框,然后进入秀米网页,点击上面打开你做好的图文然后点击导出就行了。
关键在导出以后发布。我需要做的是在小程序上显示这个图文,发布到小程序上以后发现图片无法显示。然后我便去排查原因,点击后发现是因为图片转存没有如期实现。查看是否如期实现图片转存功能只需要先随便点击一张已经导出到ueditor的图片,然后点右下角“修改”就可以查看图片所在地址:
可以清楚的看到图片地址,目前还是显示https://img.xiumi.us,这就是有问题的。到时候图片调用时就会因为自身请求被秀米服务器拒绝而导致图片无法加载。正常情况下我们在开启catchRemoteImageEnable: true,
后,导出的图片地址会自然变成我们自己服务器的地址。这个地址理论上就是你之前在serverUrl: '//ueditor.zhenghaochuan.com/cos',
中配好的地址。
目前发现了是发送请求的问题。那么就去寻找ueditor源码里发送图片转存请求的地方。
这里就要观察ueditor的js脚本文件。先找到/public/UEditor/ueditor.all.js
然后找到里面最关键的插件:catchremoteimage
开头大约长这样:
// plugins/catchremoteimage.js
///import core
///commands 远程图片抓取
///commandsName catchRemoteImage,catchremoteimageenable
///commandsTitle 远程图片抓取
/**
* 远程图片抓取,当开启本插件时所有不符合本地域名的图片都将被抓取成为本地服务器上的图片
*/
UE.plugins['catchremoteimage'] = function () {
找到了就去分析整段代码逻辑。
先是定义了一些数据,然后该插件监听了一个afterpaste
事件,监听到事件后立即执行函数catchRemoteImage
。
afterpaste就不必多看,听名字就知道是粘贴事件。关键的发送请求显然在catchRemoteImage
函数体中,往下翻找到catchRemoteImage
函数,源代码长这样:
function catchremoteimage(imgs, callbacks) {
var params = utils.serializeParam(me.queryCommandValue('serverparam')) || '',
url = utils.formatUrl(catcherActionUrl + (catcherActionUrl.indexOf('?') == -1 ? '?':'&') + params),
isJsonp = utils.isCrossDomainUrl(url),
opt = {
'method': 'POST',
'dataType': isJsonp ? 'jsonp':'',
'timeout': 60000, //单位:毫秒,回调请求超时设置。目标用户如果网速不是很快的话此处建议设置一个较大的数值
'onsuccess': callbacks["success"],
'onerror': callbacks["error"]
};
opt[catcherFieldName] = imgs;
ajax.request(url, opt);
}
可以清晰看出这就是个ajax请求,其中ajax已经被封装好。前面定义的数据都是请求设置。关键在于这句:opt[catcherFieldName] = imgs;
请求携带的参数是imgs,显然这就是我想要找的发送失败的图片。继续查找imgs是什么。可以看到它是这么定义imgs的:
me.addListener("catchRemoteImage", function () {
var remoteImages = [],
imgs = domUtils.getElementsByTagName(me.document, "img"),
test = function (src, urls) {
if (src.indexOf(location.host) != -1 || /(^\.)|(^\/)/.test(src)) {
return true;
}
if (urls) {
for (var j = 0, url; url = urls[j++];) {
if (src.indexOf(url) !== -1) {
return true;
}
}
}
return false;
};
for (var i = 0, ci; ci = imgs[i++];) {
if (ci.getAttribute("word_img")) {
continue;
}
var src = ci.getAttribute("_src") || ci.src || "";
if (/^(https?|ftp):/i.test(src) && !test(src, catcherLocalDomain)) {
remoteImages.push(src);
}
}
对catchRemoteImage
也添加了一个监听事件,里面定义了三个变量remoteImages ,imgs 和test。可以看到imgs就是从刚刚的粘贴事件获取的所有img元素的数组。然后再对imgs数组每个元素进行处理,将其添加到remoteImages中。
接下来是
if (remoteImages.length) {
部分。主要是处理对抓取图片事件成功与失败的响应,成功了就将服务器响应的新图片url替换原url。
到这里基本分析完了。简而言之:
这个插件先是监听用户的粘贴事件,发现有项目粘贴进来时就立即搜索该项目中的内容,如果有图片,就将图片添加到imgs数组,简单处理后得到图片的url再放入remoteImages数组。然后再通过ajax请求将其发送到已经设置好地址的服务器中。然后获取从服务器传回的值,再将这些值作为newSrc替换原本图片的地址。这样所谓的图片转存就完成了。如果原本的地址没有被替换,那么大概率就是请求发送出现了问题。
到这里基本后端处理请求的逻辑也应该出现了:接受到前端发送的url地址后,去将文件都下载下来转存到自己服务器的minio上,然后将minio返回的地址再返回给前端即可。但是我们的后端接口只能处理一个图片,而且不接受url,只接受formdata中的二进制文件形式。因此到这里矛盾出现了,自然转存也就失败了。但是由于我不方便更改后端,于是我选择了在前端解决这个问题。
整体思路也和后端类似,接收到了imgs数组之后,将图片都转存下来,然后以文件形式一个个发送到后端服务器,然后再将服务器传回的url作为newSrc,使用domUtils.setAttributes
方法更改图片地址,这样也同样可以解决图片转存问题。
整体代码与源码大同小异,主要更改的点在于:
1.获取图片url后将图片转为二进制文件形式urlToFile:
async function urlToFile(url, filename, mimeType) {
const res = await fetch(url);
const blob = await res.blob();
return new File([blob], filename, { type: mimeType });
}
2.不再使用他提供的ajax,改用fetch:
fetch(url, {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json'
}
})
.then(response => {
return response.json()
})
.then(info => {
var cj = info;
if (cj.state == "SUCCESS") {
var newSrc = catcherUrlPrefix + cj.url;
domUtils.setAttributes(img.element, {
"src": newSrc,
"_src": newSrc
});
}
me.fireEvent('catchremotesuccess');
})
.catch(error => {
console.error("Request failed or timed out for image:", img.src);
me.fireEvent("catchremoteerror");
});
3.不再统一发送图片,使用foreach逐个发送图片:
if (remoteImages.length) {
remoteImages.forEach(function (img) {
catchremoteimage(img);
});
}
完成!
Ueditor已经停止维护,vue-ueditor-wrap也是如此,因此对于富文本编辑器,不再推荐使用ueditor。
目前我们可以选择的有很多,包括TinyMCE 、wangeditor 、Quill 等等。如果是新项目或者大规模重构,可以考虑这些还在维护,功能更加丰富的编辑器。
结尾附上完整代码,万一以后什么时候又用到了呢:
UE.plugins['catchremoteimage'] = function () {
var me = this;
if (me.options.catchRemoteImageEnable === false) return;
me.setOpt({
catchRemoteImageEnable: false
});
me.addListener("afterpaste", function () {
me.fireEvent("catchRemoteImage");
});
me.addListener("catchRemoteImage", function () {
var catcherLocalDomain = me.getOpt('catcherLocalDomain'),
catcherActionUrl = me.getActionUrl(me.getOpt('catcherActionName')),
catcherUrlPrefix = me.getOpt('catcherUrlPrefix'),
catcherFieldName = me.getOpt('catcherFieldName');
var remoteImages = [],
imgs = domUtils.getElementsByTagName(me.document, "img"),
test = function (src, urls) {
if (src.indexOf(location.host) != -1 || /(^\.)|(^\/)/.test(src)) {
return true;
}
if (urls) {
for (var j = 0, url; url = urls[j++];) {
if (src.indexOf(url) !== -1) {
return true;
}
}
}
return false;
};
for (var i = 0, ci; ci = imgs[i++];) {
if (ci.getAttribute("word_img")) {
continue;
}
var src = ci.getAttribute("_src") || ci.src || "";
if (/^(https?|ftp):/i.test(src) && !test(src, catcherLocalDomain)) {
remoteImages.push({ element: ci, src: src });
}
}
if (remoteImages.length) {
remoteImages.forEach(function (img) {
catchremoteimage(img);
});
}
function catchremoteimage(img) {
urlToFile(img.src, 'image.jpg', 'image/jpeg').then(file => {
if (file) {
var formData = new FormData();
formData.append("file", file);
var params = utils.serializeParam(me.queryCommandValue('serverparam')) || '';
var url = utils.formatUrl(catcherActionUrl + (params ? '&' + params : ''));
fetch(url, {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json'
}
})
.then(response => {
return response.json()
})
.then(info => {
var cj = info;
if (cj.state == "SUCCESS") {
var newSrc = catcherUrlPrefix + cj.url;
domUtils.setAttributes(img.element, {
"src": newSrc,
"_src": newSrc
});
}
me.fireEvent('catchremotesuccess');
})
.catch(error => {
console.error("Request failed or timed out for image:", img.src);
me.fireEvent("catchremoteerror");
});
}
}).catch(err => {
console.error("Failed to convert URL to file:", err);
me.fireEvent("catchremoteerror");
});
}
});
async function urlToFile(url, filename, mimeType) {
const res = await fetch(url);
const blob = await res.blob();
return new File([blob], filename, { type: mimeType });
}
};