TinyMCE 富文本编辑器实战

引言

如下图所示,这是19年高考试卷,题干格式复杂,并非只有简单的文字描述,公式、图示都需要使用图片存储。

image.png

欲存储试题内容,须使用富文本编辑器。

想去维基百科上找找富文本编辑器的官方解释,发现并没有该词条。

也不知道怎么解释这个词,反正就像segmentfault编辑器这样式的,不止是简简单单的textarea,功能更丰富,可以承载样式与媒体内容。

image.png

对比多家富文本编辑器后,决定选用TinyMCE,该编辑器号称世界上最好的JavaScript富文本编辑库。

打开TinyMCE官网,发现这个第一名副其实。

image.png

本文带大家一起在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'
};

一个简单的富文本编辑器就出来了。

image.png

图片上传

欲于富文本编辑器中实现图片上传功能,需配置图片上传拦截器。

如下代码,在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类型未维护,编写时没有提示,全参考官方文档编写。

  1. 参数blobInfo,一个持有blob文件对象的对象。
  2. 参数success,库提供的回调函数,通过参数的形式将当前上传成功的图片的url回传。
  3. 参数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?

Blobbinary large object,二进制大型对象,通常是影像、声音或多媒体文件。

Blob对象表示一个不可变、原始数据的类文件对象。

在前台,常用的图片、文件等都属于Blob

附件接口

附件上传控制器,接收文件,并转发给service

@PostMapping
public Attachment upload(@RequestParam("attachment") MultipartFile multipartFile) {
    return attachmentService.upload(multipartFile);
}

附件上传service,直接从计量项目中移植过来的,根据SHA-1MD5对文件进行Hash,如果两者一致,认为是同一个文件,跳过上传,直接使用原文件。

因为位数固定,所以肯定是会有冲突的一天。但是SHA-1MD5同时冲突的可能性非常小,要是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;
}

附件查询控制器,这个也是考虑过性能的,在从ResourceInputStream写入数据到ResponseOutputStream时,考虑了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();
}

最终效果

最终效果如下图所示:

result.gif

生成的代码如下:

测试题干测试题干

总结

程序员的日常:上Google,找Github,抄代码。

虽然Google是恶魔,但没Google代码还真就写不出来。

你可能感兴趣的:(tinymce,富文本编辑器,实战,springboot,angular)