#前言
说明
1、使用UEditor实现富文本编辑,上传图片或视频并保存到OSS云存储。
2、使用vue-ueditor-wrap + SpringMVC + UEditor
3、OSS存储代码本文没有示例,需要自己网络搜索
概要流程
前端:
1、下载vue-ueditor-wrap,并将vue-ueditor-wrap.min.js引入前端html;
2、下载vue-ueditor-wrap/assets/downloads/utf8-jsp.zip,并解压放置项目的static目录下;
具体目录可以在JS中初始化UEditor时指定位置,配置项为UEDITOR_HOME_URL: ‘…/statics/UEditor/’,
3、配置文件上传地址,配置项为serverUrl: ‘http://localhost:8080/cell-school-framework/ueditor/exec’,(服务地址写全)
后端:
1、下载UEditor[1.4.3.3 完整源码],将压缩包内jsp\src\com\baidu目录下java文件导入到自己的项目中,并在webapp目录下新建conf文件夹将UEditor的config.json放进去
2、增加UeditorController,访问Mapping为/ueditor/exec
3、修改BinaryUploader.save()方法,加入上传对象服务器逻辑(增加了UploadOSSUtil工具类)
4、修改spring-mvc.xml配置文件,重写CommonsMultipartResolver
参考文档
1 前端部分
1.1 html示例
<!DOCTYPE html>
<html>
<head>
<title>iviewDemo</title>
#parse("sys/header.html")
<!-- 引入vue-ueditor-wrap.min.js -->
<script src="${rc.contextPath}/statics/libs/vue-ueditor-wrap.min.js"></script>
</head>
<body>
<div id="rrapp" v-cloak>
<!-- UEditor初始化 -->
<vue-ueditor-wrap v-model="richTextMSG" :config="ueConfig"></vue-ueditor-wrap>
</div>
<!-- 引入页面JS -->
<script src="${rc.contextPath}/js/test/richtext.js?_${date.systemTime}"></script>
</body>
</html>
1.2 JS示例
说明:
serverUrl: ‘http://localhost:8080/framework/ueditor/exec’,为服务器统一请求接口路径
UEDITOR_HOME_URL: ‘…/statics/UEditor/’,为前端UEditor的根目录
//JQuery
$(function () {
});
//VUE
var vm = new Vue({
el: '#rrapp',
//引入组件
components: {
VueUeditorWrap
},
data: {
//富文本内容
richTextMSG:'',
//UEditor配置
ueConfig: {
toolbars: [
[
'undo', //撤销
'bold', //加粗
'indent', //首行缩进
'italic', //斜体
'underline', //下划线
'strikethrough', //删除线
'subscript', //下标
'fontborder', //字符边框
'superscript', //上标
'formatmatch', //格式刷
'fontfamily', //字体
'fontsize', //字号
'justifyleft', //居左对齐
'justifycenter', //居中对齐
'justifyright', //居右对齐
'justifyjustify', //两端对齐
'insertorderedlist', //有序列表
'insertunorderedlist', //无序列表
'lineheight',//行间距
'insertvideo',
'insertimage',
]
],
'fontfamily':[
{ label:'',name:'songti',val:'宋体,SimSun'},
{ label:'仿宋',name:'fangsong',val:'仿宋,FangSong'},
{ label:'仿宋_GB2312',name:'fangsong',val:'仿宋_GB2312,FangSong'},
{ label:'',name:'kaiti',val:'楷体,楷体_GB2312, SimKai'},
{ label:'',name:'yahei',val:'微软雅黑,Microsoft YaHei'},
{ label:'',name:'heiti',val:'黑体, SimHei'},
{ label:'',name:'lishu',val:'隶书, SimLi'},
{ label:'',name:'andaleMono',val:'andale mono'},
{ label:'',name:'arial',val:'arial, helvetica,sans-serif'},
{ label:'',name:'arialBlack',val:'arial black,avant garde'},
{ label:'',name:'comicSansMs',val:'comic sans ms'},
{ label:'',name:'impact',val:'impact,chicago'},
{ label:'',name:'timesNewRoman',val:'times new roman'}
],
// 编辑器不自动被内容撑高
autoHeightEnabled: false,
// 初始容器高度
initialFrameHeight: 500,
// 初始容器宽度
initialFrameWidth: '100%',
// 上传文件接口(这个地址是我为了方便各位体验文件上传功能搭建的临时接口,请勿在生产环境使用!!!)
serverUrl: 'http://localhost:8080/framework/ueditor/exec',
//UEditor 资源文件的存放路径,如果你使用的是vue-cli生成的项目,通常不需要设置该选项,vue-ueditor-wrap会自动处理常见的情况
UEDITOR_HOME_URL: '../statics/UEditor/',
}
},
methods: {
}
});
2 后端JAVA代码部分
2.1 自定义config.json配置文件的路径,UE初始话读取配置文件会从该路径获取
代码路径:ConfigManager.getConfigPath();
private String getConfigPath () {
//return this.parentPath + File.separator + ConfigManager.configFileName;
return this.rootPath
+ File.separator + "conf"
+ File.separator + ConfigManager.configFileName;
}
2.2 编写UE入口Controller,以及自定义Action动作
说明:
这里的地址为JS中ueConfig的serverUrl
入口代码示例:
/**
* 用于处理关于ueditor插件相关的请求
*
*/
@RestController
@CrossOrigin
@RequestMapping("/ueditor")
public class UeditorController {
@RequestMapping(value = "/exec")
@ResponseBody
public String exec(HttpServletRequest request) throws UnsupportedEncodingException {
request.setCharacterEncoding("utf-8");
String rootPath = request.getRealPath("/");
return new ActionEnter(request, rootPath).exec();
}
}
2.3 根据前端actionCode自定义需要执行的动作,比如上传,参见ActionEnter.invoke();方法:
package com.platform.ueditor;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import com.platform.ueditor.define.ActionMap;
import com.platform.ueditor.define.State;
import com.platform.ueditor.hunter.FileManager;
import com.platform.ueditor.hunter.ImageHunter;
import com.platform.ueditor.upload.Uploader;
import com.platform.ueditor.define.AppInfo;
import com.platform.ueditor.define.BaseState;
public class ActionEnter {
private HttpServletRequest request = null;
private String rootPath = null;
private String contextPath = null;
private String actionType = null;
private ConfigManager configManager = null;
public ActionEnter ( HttpServletRequest request, String rootPath ) {
this.request = request;
this.rootPath = rootPath;
this.actionType = request.getParameter( "action" );
this.contextPath = request.getContextPath();
this.configManager = ConfigManager.getInstance( this.rootPath, this.contextPath, request.getRequestURI() );
}
public String exec () {
String callbackName = this.request.getParameter("callback");
if ( callbackName != null ) {
if ( !validCallbackName( callbackName ) ) {
return new BaseState( false, AppInfo.ILLEGAL ).toJSONString();
}
return callbackName+"("+this.invoke()+");";
} else {
return this.invoke();
}
}
public String invoke() {
if ( actionType == null || !ActionMap.mapping.containsKey( actionType ) ) {
return new BaseState( false, AppInfo.INVALID_ACTION ).toJSONString();
}
if ( this.configManager == null || !this.configManager.valid() ) {
return new BaseState( false, AppInfo.CONFIG_ERROR ).toJSONString();
}
State state = null;
int actionCode = ActionMap.getType( this.actionType );
Map<String, Object> conf = null;
switch ( actionCode ) {
//读取配置文件时的请求处理
case ActionMap.CONFIG:
return this.configManager.getAllConfig().toString();
//上传图片、视频、文件时的处理
case ActionMap.UPLOAD_IMAGE:
case ActionMap.UPLOAD_SCRAWL:
case ActionMap.UPLOAD_VIDEO:
case ActionMap.UPLOAD_FILE:
conf = this.configManager.getConfig( actionCode );
state = new Uploader( request, conf ).doExec();
break;
//抓取远程图片时的处理方式,此处也可以关闭
//当从网页上复制内容到编辑器中,如果图片与该域名不同源,则会自动抓到本地的服务器保存
case ActionMap.CATCH_IMAGE:
conf = configManager.getConfig( actionCode );
String[] list = this.request.getParameterValues( (String)conf.get( "fieldName" ) );
state = new ImageHunter( conf ).capture( list );
break;
//上传多文件时的文件在线管理
case ActionMap.LIST_IMAGE:
case ActionMap.LIST_FILE:
conf = configManager.getConfig( actionCode );
int start = this.getStartIndex();
state = new FileManager( conf ).listFile( start );
break;
}
return state.toJSONString();
}
public int getStartIndex () {
String start = this.request.getParameter( "start" );
try {
return Integer.parseInt( start );
} catch ( Exception e ) {
return 0;
}
}
/**
* callback参数验证
*/
public boolean validCallbackName ( String name ) {
if ( name.matches( "^[a-zA-Z_]+[\\w0-9_]*$" ) ) {
return true;
}
return false;
}
}
2.4 自定义文件上传,我这里使用的是阿里云上传
代码路径:BinaryUploader.save();或Base64Uploader.save();
BinaryUploader.save();完整代码示例如下:
public class BinaryUploader {
public static final State save(HttpServletRequest request,
Map<String, Object> conf) {
FileItemStream fileStream = null;
boolean isAjaxUpload = request.getHeader( "X_Requested_With" ) != null;
if (!ServletFileUpload.isMultipartContent(request)) {
return new BaseState(false, AppInfo.NOT_MULTIPART_CONTENT);
}
ServletFileUpload upload = new ServletFileUpload(
new DiskFileItemFactory());
if ( isAjaxUpload ) {
upload.setHeaderEncoding( "UTF-8" );
}
try {
FileItemIterator iterator = upload.getItemIterator(request);
while (iterator.hasNext()) {
fileStream = iterator.next();
if (!fileStream.isFormField())
break;
fileStream = null;
}
if (fileStream == null) {
return new BaseState(false, AppInfo.NOTFOUND_UPLOAD_DATA);
}
String originFileName = fileStream.getName();
String suffix = FileType.getSuffixByFilename(originFileName);
if (!validType(suffix, (String[]) conf.get("allowFiles"))) {
return new BaseState(false, AppInfo.NOT_ALLOW_FILE_TYPE);
}
InputStream is = fileStream.openStream();
/**
* 上传到阿里云
*/
//*******************开始***********************
String fileName = new StringBuffer().append(new Date().getTime()).append(fileStream.getName().substring(fileStream.getName().indexOf("."))).toString();
State storageState = new BaseState(true);
try {
//阿里OSS上传工具
new UploadOSSUtil();
String url = UploadOSSUtil.uploadImgAliyun(is,fileName);
storageState.putInfo("state", "SUCCESS");
//url:返回前端的访问路径,请根据自己实际情况填写
storageState.putInfo("url", url);
storageState.putInfo("title", fileName);
storageState.putInfo("original", fileName);
} catch (Exception e) {
// TODO: handle exception
System.out.println(e.getMessage());
storageState.putInfo("state", "文件上传失败!");
storageState.putInfo("url","");
storageState.putInfo("title", "");
storageState.putInfo("original", "");
//System.out.println("文件 "+fileName+" 上传失败!");
}
//********************结束**********************
is.close();
return storageState;
} catch (FileUploadException e) {
return new BaseState(false, AppInfo.PARSE_REQUEST_ERROR);
} catch (IOException e) {
}
return new BaseState(false, AppInfo.IO_ERROR);
}
private static boolean validType(String type, String[] allowTypes) {
List<String> list = Arrays.asList(allowTypes);
return list.contains(type);
}
}
3 Q&A
问题1:SpringMVC+UEditor上传无可用文件问题
措施:继承CommonsMultipartResolver,并在SpringMVC.xml中配置Bean
/**
*UEditor文件上传与SpringMVC上传冲突解决方案
*/
public class CommonsMultipartResolverOverride extends CommonsMultipartResolver
{
@Override
public boolean isMultipart(HttpServletRequest request)
{
String url = request.getRequestURI();
if (url != null && url.contains("/ueditor/exec"))
{
return false;
} else
{
return super.isMultipart(request);
}
}
}
<!-- UEditor文件上传与SpringMVC上传冲突解决方案 -->
<bean id="multipartResolver"
class="com.platform.webresolver.CommonsMultipartResolverOverride">
<property name="maxUploadSize" value="1000000000"/>
</bean>
参考:
问题2: UEditor获取config.json配置时,返回前端JSON字符串带\转义问题解决方案
措施:SpringMVC.xml配置文件中增加StringHttpMessageConverter
<!-- UEditor获取config.json配置时,返回前端JSON字符串带\转义问题解决方案 -->
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean id="stringHttpMessageConverter" class="org.springframework.http.converter.StringHttpMessageConverter">
<constructor-arg value="UTF-8" index="0"/>
<property name="supportedMediaTypes">
<list>
<value>text/plain;charset=UTF-8</value>
</list>
</property>
</bean>
<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=UTF-8</value>
<value>application/json;charset=UTF-8</value>
</list>
</property>
<property name="features">
<list>
<value>WriteNullListAsEmpty</value>
<value>WriteMapNullValue</value>
<value>QuoteFieldNames</value>
<value>WriteDateUseDateFormat</value>
<!-- 禁用fastjson循环引用检测 -->
<value>DisableCircularReferenceDetect</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
问题3: 保存富文本到数据库时没有HTML格式
措施:后台POST请求ContentType改为:contentType: “application/json;charset=UTF-8”,
saveOrUpdate: function (event) {
let url = vm.tHyPost.id == null ? "../thypost/save" : "../thypost/update";
Ajax.request({
url: url,
params: JSON.stringify(vm.tHyPost),
type: "POST",
contentType: "application/json;charset=UTF-8",//注意这里,否则入库无格式
successCallback: function (r) {
alert('操作成功', function (index) {
vm.reload();
});
}
});
},