引言
如下图所示,这是19
年高考试卷,题干格式复杂,并非只有简单的文字描述,公式、图示都需要使用图片存储。
欲存储试题内容,须使用富文本编辑器。
想去维基百科上找找富文本编辑器的官方解释,发现并没有该词条。
也不知道怎么解释这个词,反正就像segmentfault
编辑器这样式的,不止是简简单单的textarea
,功能更丰富,可以承载样式与媒体内容。
对比多家富文本编辑器后,决定选用TinyMCE
,该编辑器号称世界上最好的JavaScript
富文本编辑库。
打开TinyMCE官网,发现这个第一名副其实。
本文带大家一起在SpringBoot
+ Angular
的环境下,领悟TinyMCE
的魅力。
实战
安装
直接找tinymce-angular - github,阅读README
。
不想因为安装步骤的变更而失去本文的参考价值!
基础使用
导入EditorModule
模块。
@NgModule({
declarations: [
// ...
],
exports: [
// ...
],
imports: [
// ...
EditorModule
]
})
export class FuncModule {
}
使用库中已有的editor
组件。
editor
组件初始化配置。
/** 设置通用配置 */
this.options = {
base_url: '/tinymce',
suffix: '.min',
plugins: 'lists link image paste',
menubar: false,
height: 300,
toolbar: 'undo | redo | bold | italic | bullist | numlist'
};
一个简单的富文本编辑器就出来了。
图片上传
欲于富文本编辑器中实现图片上传功能,需配置图片上传拦截器。
如下代码,在options
配置对象中添加配置项images_upload_handler
,该handler
为自定义回调函数。
/** 设置通用配置 */
this.options = {
// ...
// 文件上传拦截器
images_upload_handler: (blobInfo, success, failure): void => {
this.attachmentService.upload(blobInfo.blob())
.subscribe((attachment: Attachment) => {
success('/api/attachment/' + attachment.id);
}, (response: HttpErrorResponse) => {
failure('上传图片异常: ' + response.error);
});
},
// 允许拖拽图片
paste_data_images: true
};
因为官方的TypeScript
类型未维护,编写时没有提示,全参考官方文档编写。
- 参数
blobInfo
,一个持有blob
文件对象的对象。 - 参数
success
,库提供的回调函数,通过参数的形式将当前上传成功的图片的url
回传。 - 参数
failure
,库提供的回调函数,通过参数的形式将当前上传的错误信息回传。
附件上传
模版代码如下:
/**
* 上传附件
* @param blob 文件对象
*/
upload(blob: Blob): Observable {
// 创建 FormData
const data: FormData = new FormData();
data.append('attachment', blob);
// 发起请求
return this.httpClient.post(`${this.baseUrl}`, data);
}
什么是Blob?
Blob
:binary large object
,二进制大型对象,通常是影像、声音或多媒体文件。
Blob
对象表示一个不可变、原始数据的类文件对象。
在前台,常用的图片、文件等都属于Blob
。
附件接口
附件上传控制器,接收文件,并转发给service
。
@PostMapping
public Attachment upload(@RequestParam("attachment") MultipartFile multipartFile) {
return attachmentService.upload(multipartFile);
}
附件上传service
,直接从计量项目中移植过来的,根据SHA-1
与MD5
对文件进行Hash
,如果两者一致,认为是同一个文件,跳过上传,直接使用原文件。
因为位数固定,所以肯定是会有冲突的一天。但是SHA-1
和MD5
同时冲突的可能性非常小,要是1/2^67
都能中奖,那就不用写代码了,去买彩票吧!
参考:How hard is it to generate a simultaneous MD5 and SHA1 collision?
@Override
public Attachment upload(MultipartFile multipartFile) {
logger.debug("检验附件信息");
if (multipartFile.isEmpty()) {
throw new RuntimeException("上传的附件不能为空");
}
logger.debug("新建附件对象");
Attachment attachment = new Attachment();
try {
logger.debug("获取文件名");
String fileName = multipartFile.getOriginalFilename();
logger.debug("从文件名中截取拓展名");
// 从"."最后一次出现的位置的下一位开始截取,获取扩展名
assert fileName != null;
String ext = fileName.substring(fileName.lastIndexOf(".") + 1);
logger.debug("计算文件SHA-1 MD5值");
String sha1 = CommonService.encrypt(multipartFile, "SHA-1");
String md5 = CommonService.encrypt(multipartFile, "MD5");
logger.debug("设置附件信息");
attachment.setSha1(sha1);
attachment.setMd5(md5);
attachment.setOriginName(fileName);
attachment.setExt(ext);
logger.debug("查询持久化附件");
Attachment persistAttachment = attachmentRepository.findTopOneBySha1AndMd5(sha1, md5);
if (persistAttachment == null) {
logger.debug("计算存储文件名称");
String name = CommonService.md5(sha1 + System.currentTimeMillis()) + "." + ext;
logger.debug("获取文件存储路径");
Path path = this.getPath();
logger.debug("如果目录不存在,则创建目录");
if (Files.notExists(path)) {
Files.createDirectories(path);
}
logger.debug("文件上传至目录");
Files.copy(multipartFile.getInputStream(), path.resolve(name), StandardCopyOption.REPLACE_EXISTING);
logger.debug("设置附件存储信息");
attachment.setPath(path.toString());
attachment.setName(name);
} else {
logger.debug("从原附件实体中复制信息");
attachment.setPath(persistAttachment.getPath());
attachment.setName(persistAttachment.getName());
}
logger.debug("存储附件");
attachmentRepository.save(attachment);
} catch (Exception e) {
logger.error("上传附件出现异常");
throw new RuntimeException("附件上传错误");
}
return attachment;
}
附件查询控制器,这个也是考虑过性能的,在从Resource
的InputStream
写入数据到Response
的OutputStream
时,考虑了StackOverflow
中的建议。
I suggest a buffer of at least 10KB to 100KB. That's not much and can speed up copying large amounts of data tremendously.
@GetMapping("{id}")
public void getResource(@PathVariable Long id, HttpServletResponse response) throws IOException {
logger.debug("获取资源");
Resource resource = attachmentService.getResourceByAttachmentId(id);
logger.debug("获取输入输出流");
InputStream in = resource.getInputStream();
OutputStream out = response.getOutputStream();
/*
* 参考问题: https://stackoverflow.com/questions/43157/easy-way-to-write-contents-of-a-java-inputstream-to-an-outputstream
* 缓冲区设置为 10K
*/
logger.debug("缓冲读取内容写入输出流");
byte[] buffer = new byte[10240];
int len = in.read(buffer);
while (len != -1) {
out.write(buffer, 0, len);
len = in.read(buffer);
}
logger.debug("关闭输出流");
out.flush();
out.close();
}
最终效果
最终效果如下图所示:
生成的代码如下:
测试题干测试题干
总结
程序员的日常:上Google
,找Github
,抄代码。
虽然Google
是恶魔,但没Google
代码还真就写不出来。