#前言

说明

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();
                    });
                }
			});
		},