Vue 集成 TinyMCE 富文本编辑器

发表于 2020-06-04 | 最后更新于 2021-12-20 | 开发

前言

几个月前就对许多比较有名的编辑器做过了解,并且使用过几个富文本编辑器。后来逐渐忘却了这件事,之前一段时间2.0版本的博客,发表文章的模块,所使用的的并不是现在芜桐3.0版本的markdown编辑器,而是这个tinymce富文本编辑器。

鉴于tinymce的官方文档是纯英文,中文的文档和博文数量稀少(曾经,现在看起来还蛮多的)的原因,所以简单说一下自己是如何在vue项目中整合的这个tiny富文本编辑器,还有自己踩过的各种坑。

安装

在npm上有一个vue版本的tiny,我们先安装这个包。

 npm i @tinymce/tinymce-vue -S

其实只用这一个包也是可以的。但是会出现一些小小的问题,由于需要我们提供api-key,官网是国外的网站,不考虑被墙的问题也要考虑英文阅读能力,所以我们最好是下载一个完整的编辑器的包,防止富文本编辑器加载缓慢和每次打开都会有一个烦人的弹窗的问题。

使用以下命令下载完整的包。

npm i tinymce -S

下载完成之后,需要把node_modules下的tinymce包下的资源拷贝到项目本地,以便本地访问这些资源。或者直接使用cdn(推荐)。查看文章底部的其他问题->cdn

使用

资源已经准备好了,那么就回到我们的vue文件里,看看怎么使用吧。

首先引入我们下载好的包

import Editor from "@tinymce/tinymce-vue";

在components下注册编辑器组件

components: {
  "tinymce-editor": Editor
},

在data里定义一个init变量来初始化我们的编辑器

init: {
        language_url: "/lib/js/zh_CN.js",
        language: "zh_CN",
        height: 430,
        plugins:"link lists image code table colorpicker textcolor wordcount contextmenu",
        toolbar:"bold italic underline strikethrough | fontsizeselect | forecolor backcolor | alignleft aligncenter alignright alignjustify|bullist numlist |outdent indent blockquote | undo redo | link unlink image code | removeformat",
        branding: false,
        images_upload_handler:(blobInfo, success,failure)=> {
          success('data:image/jpeg;base64,' + blobInfo.base64())
        }
     },

init里边的值在文档上都写得很清楚,常用的一些配置我也已经贴出来了,例子中的图片上传我直接使用了base64输出,大家可以自己写上传接口。

其中编辑器的汉化部分想必已经看到了,配置language和language_url即可。

来自博主芜桐提供的汉化包 https://cdn.jsdelivr.net/gh/wt-sml/wutong_cdn/js/tinymce-lang-zh@5.2.2.js

我贴一些常用的配置属性值。列表来自tinymce中文文档。

  • newdocument(新文档)
  • bold(加粗)
  • italic(斜体)
  • underline(下划线)
  • strikethrough(删除线)
  • alignleft(左对齐)
  • aligncenter(居中对齐)
  • alignright(右对齐)
  • alignjustify(两端对齐)
  • styleselect(格式设置)
  • formatselect(段落格式)
  • fontselect(字体选择)
  • fontsizeselect(字号选择)
  • cut(剪切)
  • copy(复制)
  • paste(粘贴)
  • bullist(项目列表UL)
  • numlist(编号列表OL)
  • outdent(减少缩进)
  • indent(增加缩进)
  • blockquote(引用)
  • undo(撤销)
  • redo(重做/重复)
  • removeformat(清除格式)
  • subscript(下角标)
  • superscript(上角标)

使用toolbar来配置工具栏上可用的按钮,多个控件使用空格分隔,使用“|”来创建分组。

放置组件。

<div>
    <tinymce-editor :init="init" v-model="form.content"></tinymce-editor>
</div>

可以看到,组件直接支持v-model指令,直接双向绑定数据。

其他问题

懒人使用cdn不想下载完整包,消除来自 api-key 的警告,在你的公共css或scss中,添加以下样式,如果是在组件内部,推荐开启scss预编译并添加样式穿透 /deep/

// 隐藏 tinymce alert
.tox .tox-notifications-container .tox-notification {
  display: none;
}

懒人想使用cdn但是又不想使用这个库自己写的cdn地址(有时会崩,不是自己的不放心),可以使用芜桐提供的以下cdn(也可以自己根据需求来搭建)

<!-- tinymce -->
  <script src="https://cdn.jsdelivr.net/gh/wt-sml/wutong_cdn/js/tinymce@5.2.2.min.js"></script>
  <script src="https://cdn.jsdelivr.net/gh/wt-sml/wutong_cdn/js/tinymce-silver-theme.min@5.2.2.js"></script>
  <script src="https://cdn.jsdelivr.net/gh/wt-sml/wutong_cdn/js/tinymce-table-plugin@5.2.2.min.js"></script>
  <script src="https://cdn.jsdelivr.net/gh/wt-sml/wutong_cdn/js/tinymce-image-plugin@5.2.2.min.js"></script>
  <script src="https://cdn.jsdelivr.net/gh/wt-sml/wutong_cdn/js/tinymce-paste-plugin@5.2.2.min.js"></script>
  <script src="https://cdn.jsdelivr.net/gh/wt-sml/wutong_cdn/js/tinymce-lang-zh@5.2.2.js"></script>
  <link rel="stylesheet"
    href="https://cdn.jsdelivr.net/gh/wt-sml/wutong_cdn/css/tinymce-skins-ui-oxide-skin@5.2.2.min.css">
  <link rel="stylesheet"
    href="https://cdn.jsdelivr.net/gh/wt-sml/wutong_cdn/css/tinymce-skins-content-default-content@5.2.2.min.css">
  <link rel="stylesheet"
    href="https://cdn.jsdelivr.net/gh/wt-sml/wutong_cdn/css/tinymce-skins-ui-oxide-content@5.2.2.min.css">

想预防xss攻击,同时不希望用户直接粘贴进来一些富文本,在你的 init 里面,添加以下属性

  paste_preprocess: (pl, o) => {
        o.content = $stripTags(o.content, "sup,sub");
  },

其中 $stripTags是去除HTML标签的方法,第二个参数是你想保留的不被去除的标签 ,$stripTags 方法的实现如下。有点长

const $stripTags = (str, allowed_tags) => {
    let key = '', allowed = false
    let matches = []
    let allowed_array = []
    let allowed_tag = ''
    let i = 0
    let k = ''
    let html = ''
    let replacer = function (search, replace, str) {
        return str.split(search).join(replace)
    }
    // Build allowes tags associative array
    if (allowed_tags) {
        allowed_array = allowed_tags.match(/([a-zA-Z0-9]+)/gi)
    }
    str += ''

    // Match tags
    matches = str.match(/(<\/?[\S][^>]*>)/gi)
    // Go through all HTML tags
    for (key in matches) {
        if (isNaN(key)) {
            // IE7 Hack
            continue
        }

        // Save HTML tag
        html = matches[key].toString()
        // Is tag not in allowed list? Remove from str!
        allowed = false

        // Go through all allowed tags
        for (k in allowed_array) {            // Init
            allowed_tag = allowed_array[k]
            i = -1

            if (i != 0) {
                i = html.toLowerCase().indexOf('<' + allowed_tag + '>')
            }
            if (i != 0) {
                i = html.toLowerCase().indexOf('<' + allowed_tag + ' ')
            }
            if (i != 0) {
                i = html.toLowerCase().indexOf('</' + allowed_tag)
            }

            // Determine
            if (i == 0) {
                allowed = true
                break
            }
        }
        if (!allowed) {
            str = replacer(html, "", str) // Custom replace. No regexing
        }
    }
    return str
}

关于图片上传。上面的例子只是简单的base64,可是如果图片资源很大,base64是很不明智的,我们还是需要把图片传到服务器上。在此我贴一下自己实际开发中的图片上传到阿里云cdn的 前端代码

首先是 init 里的 images_upload_handler 函数

images_upload_handler: async (blobInfo, success, failure) => {
    const file = blobInfo.blob();
    if (file.size > 5242880) {
      failure("图片请不要大于 5MB");
    } else {
      try {
        success(await $postImg(file));
        $msg("图片上传成功");
      } catch {
        failure("上传图片失败");
      }
    }
 }

其中 $msg 是一个类似于alert的提示消息的方法,代码就不贴了。 $postImg 是跟后端对接的接口,大概分两步,先请求上传到阿里云cdn所需要的的参数,然后再拿着这些参数去请求阿里云cdn服务器实现上传。代码中的 $http 是对 axios 的二次封装,每个人都有自己的习惯,就不贴出来了。代码仅供参考,代码中的一些常量也是因人而异。

const $postImg = (file, type = $UPLOAD_IMAGE_TYPE_CARD_DESCRIPTION) => new Promise((resolve, reject) => {
  $http(
    {
      url: "image",
      method: "post",
      data: {
        type,
        suffix: "jpeg"
      }
    },
    res => {
      const data = {
        ...res,
        file,
        success_action_status: 200
      };
      $http(
        {
          setToken: false,
          baseURL: `http://${res.bucket}.${res.endPoint}`,
          url: "",
          method: "post",
          data: $formDataFormat(data)
        },
        () => {
          resolve(
            $HOST_REPO +
            (res.key[0] === "/"
              ? res.key.slice(1, res.key.length)
              : res.key)
          );
        },
        () => {
          reject(null)
        }
      );
    },
    () => {
      reject(null)
    }
  );
})

好了就分享到这里吧,还有什么问题在下边留言吧。