• 首页
  • vue
  • TypeScript
  • JavaScript
  • scss
  • css3
  • html5
  • php
  • MySQL
  • redis
  • jQuery
  • vite + vue3 + ts 安装 tinymce 富文本编辑器

    TinyMCE

    TinyMCE 是一个轻量级的基于浏览器的所见即所得编辑器,支持目前流行的各种浏览器,由JavaScript写成。功能配置灵活简单(两行代码就可以将编辑器嵌入网页中),支持AJAX。

    项目:https://github.com/tinymce/tinymce
    官网:https://www.tiny.cloud

    TinyMCE 针对不同的框架有三个版本:tinymce-vue、tinymce-react、tinymce-angular。

    TinyMCE 富文本经典样式

    TinyMCE-Vue

    tinymce-vue,是 TinyMCE 官方,把 TinyMCE 集成到 Vue 项目中。这个包是 TinyMCE 的一个应用安装包,使它更容易在 Vue 应用程序中使用。tinymce-vue 依赖 tinymce ,所以使用的时候,需要两个包:tinymce@tinymce/tinymce-vueTinyMCE 官网安装文档

    Using the Tiny Cloud,需要免费申请api-key,会得到一个api-key 密钥。申请的时候,需要自己有单独的域名,便于绑定使用。在使用的时候,会自动从官方项目中,加载最新版本的tinymce,所以在本地只下载@tinymce/tinymce-vue即可。
    Using a package manager,是自托管方式,需要把两个包:tinymce@tinymce/tinymce-vue,下载到本地。我们采用这种方式来安装制作 vue 组件。

    第一步:安装

    vue3相匹配的@tinymce/tinymce-vue@^4版本。

    #安装 tinymce
    npm install tinymce
    
    #安装 typscript版
    npm install @types/tinymce -D
    
    #安装 tinymce-vue
    npm install "@tinymce/tinymce-vue@^4"
    

    查看已经安装的全部包

    npm list
    

    查看已经安装的 tinymce 包

    npm list tinymce
    
    显示如下:
    ├─┬ @tinymce/tinymce-vue@4.0.7
    │ └── tinymce@5.10.5
    └── tinymce@6.1.2
    

    查看已经安装的 tinymce-vue 包

    npm list @tinymce/tinymce-vue
    
    显示如下:
    @tinymce/tinymce-vue@4.0.7
    

    在项目根目录下,查看安装目录

    tree -L 2 ./
    

    此应用包共有两个目录文件:tinymce@tinymce。以后若需要卸载的话,可以直接删除这两个目录文件。


    第二步:安装汉化语言包

    tinymce 默认是英文的,需要汉化。语言包下载列表,选择Chinese Simplified,解压得到zh-Hans.js文件。

    然后,在/node_modules/tinymce下,新建设目录langs,把zh-Hans.js复制过去。注意:langs目录必须与themes,处于同等目录下,否则不起作用。


    第三步:创建皮肤样式目录

    在项目根目录下的/pulic下,新建设tinymce目录,并且把node_modules/tinymce/skins目录下所有文件(是 tinymce 皮肤样式文件)复制过来。

    mkdir -p ./public/tinymce
    cp -rf ./node_modules/tinymce/skins/* ./public/tinymce
    

    tinymce 的主题(theme)跟皮肤(skin)是相互分离的,皮肤(skin)主要是字体、图标、css等一些内容。tinymce6 主题风格(theme)默认主题称为Silver,可以在./node_modules/tinymce/themes目录中。主题(theme)提供编辑器用户界面组件,例如按钮、对话框和菜单。要更改编辑器外观,请自定义皮肤、图标和其他用户界面元素,例如工具栏。

    安装了依赖为什么还要从 node_modules 里复制一份出来?因为复制出来的是 tinymce 的皮肤资源文件,放到 public 中,在项目打包时 public 文件夹的静态资源会直接复制,而不经过 webpack 处理,这样部署在线上才能保证 tinymce 不会缺失样式。


    第四步:创建组件

    components下,新建设TinymceEditor.vue文件。

    <script lang="ts" setup>
    import { computed, onMounted, onUnmounted } from 'vue';
    
    import tinymce from 'tinymce/tinymce'; //tinymce核心文件
    import Editor from '@tinymce/tinymce-vue';
    
    import 'tinymce/models/dom'; // 引入dom模块。从 Tinymce6,开始必须有此模块导入
    import 'tinymce/themes/silver'; //默认主题
    import 'tinymce/icons/default'; //引入编辑器图标icon,不引入则不显示对应图标
    import 'tinymce/langs/zh-Hans'; //引入编辑器语言包
    
    /* 引入编辑器插件
     * 位于 ./node_modules/tinymce/plugins 目录下,版本不同,插件会有所差异。根据自己版本来导入,若不存在的,不能导入,会报错。
     */
    import 'tinymce/plugins/advlist'; //高级列表
    import 'tinymce/plugins/anchor'; //锚点
    import 'tinymce/plugins/autolink'; //自动链接
    import 'tinymce/plugins/autoresize'; //编辑器高度自适应,注:plugins里引入此插件时,Init里设置的height将失效
    import 'tinymce/plugins/autosave'; //自动存稿
    import 'tinymce/plugins/charmap'; //特殊字符
    import 'tinymce/plugins/code'; //编辑源码
    import 'tinymce/plugins/codesample'; //代码示例
    import 'tinymce/plugins/directionality'; //文字方向
    import 'tinymce/plugins/emoticons'; //表情
    import 'tinymce/plugins/fullscreen'; //全屏
    import 'tinymce/plugins/help'; //帮助
    import 'tinymce/plugins/image'; //插入编辑图片
    import 'tinymce/plugins/importcss'; //引入css
    import 'tinymce/plugins/insertdatetime'; //插入日期时间
    import 'tinymce/plugins/link'; //超链接
    import 'tinymce/plugins/lists'; //列表插件
    import 'tinymce/plugins/media'; //插入编辑媒体
    import 'tinymce/plugins/nonbreaking'; //插入不间断空格
    import 'tinymce/plugins/pagebreak'; //插入分页符
    import 'tinymce/plugins/preview'; //预览
    import 'tinymce/plugins/quickbars'; //快速工具栏
    import 'tinymce/plugins/save'; //保存
    import 'tinymce/plugins/searchreplace'; //查找替换
    import 'tinymce/plugins/table'; //表格
    import 'tinymce/plugins/template'; //内容模板
    import 'tinymce/plugins/visualblocks'; //显示元素范围
    import 'tinymce/plugins/visualchars'; //显示不可见字符
    import 'tinymce/plugins/wordcount'; //字数统计
    
    const props = defineProps({
      modelValue: {
        type: String,
        required: true,
        default: '',
      },
      menubar: {
        type: [Boolean, String],
        default: 'file edit insert view format table tools help',
      },
    
      height: {
        type: Number,
        default: 600,
      },
      id: {
        type: [String, Number],
        default: 'myTinymce',
      },
    });
    
    const emit = defineEmits(['update:modelValue']);
    
    const contentValue = computed({
      get() {
        return resetContentSrc(props.modelValue);
      },
      set(value) {
        emit('update:modelValue', resetContentSrc(value));
      },
    });
    
    const resetContentSrc = (content: string) =>; {
      if (!content) return '';
      const reg = /<img.*?src="(.*?)".*?>;/g;
      if (content.match(reg)) {
        return content.replace(/src=(['"])\/upload/gi, 'src=$1' + import.meta.env.VITE_BASE_URL + '/upload');
      } else {
        return content;
      }
    };
    
    const handerImageUpload = (blobInfo: any, progress: (arg0: number) =>; number) =>;
      new Promise((resolve, reject) =>; {
        if (blobInfo.blob().size / 1024 / 1024 >; 10) {
          reject({ message: '图片大小不能超过 10M', remove: true });
        }
        const formData = new FormData();
        formData.append('file', blobInfo.blob());
        request({
          method: 'post',
          url: import.meta.env.VITE_UPLOAD_IMAGE,
          data: formData,
          headers: {
            'Content-Type': 'multipart/form-data;charset=UTF-8',
          },
          onUploadProgress: (progressEvent) =>; {
            // progressEvent.loaded:已上传文件大小
            // progressEvent.total:被上传文件的总大小
            progress(progressEvent.loaded / progressEvent.total) * 100;
          },
        })
          .then((res) =>; {
            if (res.data.picurl) {
              resolve(import.meta.env.VITE_BASE_URL + res.data.picurl);
            } else {
              reject({ message: '上传失败', remove: true });
            }
          })
          .catch((error) =>; {
            reject(error);
          });
      });
    
    const initOptions = {
      language: 'zh-Hans', //汉化
      skin_url: '/tinymce/ui/oxide-dark', //皮肤
      content_css: '/tinymce/content/default/content.css ',
      content_style: 'body{font-size:14px;font-family:Microsoft YaHei,微软雅黑,宋体,Arial,Helvetica,sans-serif;line-height:1.5}img {max-width:100%;}',
      height: props.height,
      menubar: 'file edit view insert format tools table help',
      menu: {
        file: { title: 'File', items: 'newdocument | preview | export | deleteallconversations' },
        edit: { title: 'Edit', items: 'undo redo restoredraft | cut copy | selectall | searchreplace' },
        view: { title: 'View', items: 'code | visualaid visualchars visualblocks | preview fullscreen | showcomments' },
        insert: {
          title: 'Insert',
          items: 'image link media addcomment pageembed template codesample inserttable | charmap emoticons | pagebreak nonbreaking anchor tableofcontents | insertdatetime',
        },
        format: {
          title: 'Format',
          items: 'bold italic underline strikethrough superscript subscript codeformat | styles blocks fontfamily fontsize align lineheight | forecolor backcolor | language | removeformat',
        },
        tools: { title: 'Tools', items: 'a11ycheck code wordcount' },
        table: { title: 'Table', items: 'inserttable | cell row column | advtablesort | tableprops deletetable' },
        help: { title: 'Help', items: 'help' },
      },
      toolbar:
        'fullscreen | code forecolor backcolor bold italic underline strikethrough link anchor | alignleft aligncenter alignright alignjustify outdent indent  lineheight | styleselect formatselect fontselect fontsizeselect | bullist numlist | blockquote subscript superscript removeformat | table image media | indent2em formatpainter axupimgs',
      plugins:
        'code codesample preview searchreplace autolink directionality visualblocks visualchars fullscreen image link media template table charmap pagebreak nonbreaking anchor insertdatetime advlist lists wordcount autosave ',
      line_height_formats: '1 1.2 1.4 1.6 2', //行高
      font_size_formats: '12px 14px 16px 18px 20px 22px 24px 28px 32px 36px 48px 56px 72px', //字体大小
      font_family_formats:'微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;',
      images_file_types: 'jpeg,jpg,png,gif,bmp,webp',
      images_upload_handler: handerImageUpload,
      placeholder: '在这里输入文字',
      branding: false, //tiny技术支持信息是否显示
      statusbar: false, //最下方的元素路径和字数统计那一栏是否显示
      elementpath: false, //元素路径是否显示
      custom_undo_redo_levels: 10, //撤销和重做的次数
      draggable_modal: true, //对话框允许拖拽
      element_format: 'xhtml',   //输出 xhtml
      entity_encoding: 'raw',   //不转化为实体字符
      br_in_pre: false,  //pre内不添加 br 标签
    };
    
    onMounted(async () => {
      tinymce.init({}); //初始化
    });
    onUnmounted(() => {
      tinymce.remove(); //销毁
    });
    </script>
    <template>
      <div class="tinymce-box">
        <Editor v-model="contentValue" :init="initOptions" :id="props.id" />
      </div>
    </template>
    <style lang="scss" scoped>
    .tinymce-box {
      width: 100%;
    }
    </style>
    


    第五步:使用组件

    TopicPost.vue中,使用此组件的方式:

    <script lang="ts" setup>
    ......
    import TinymceEditor from '@/components/TinymceEditor.vue';
    ......
    </script>
    <template>
      element-plus 表单
      <el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" label-width="120px" class="demo-ruleForm" :size="formSize">
        <el-form-item label="标题" prop="title">
          <el-input v-model="ruleForm.topic.title" />
        </el-form-item>
        ......
        <el-form-item label="内容" prop="content">
          <TinymceEditor v-model="ruleForm.topic.content" />
        </el-form-item>
        ......
        双向数据绑定了,element-plush 表单数据。
        <el-form-item>
          <el-button type="primary" @click="submitForm(ruleFormRef)">提交</el-button>
        </el-form-item>
      </el-form>
    </template>
    


    第六步:创建 typescript 类型声明文件

    使用 typescript 版本的时候,会出现提示:无法找到模块“tinymce/models/dom”的声明文件。xxxxxx。解决办法,在项目根目录下,新建设tinymce.d.ts文件:

    /// <reference types="vite/client" />
    
    declare module 'tinymce/models/dom';
    declare module 'tinymce/themes/silver';
    declare module 'tinymce/icons/default';
    declare module 'tinymce/langs/zh-Hans';
    declare module 'tinymce/plugins/advlist';
    declare module 'tinymce/plugins/anchor';
    declare module 'tinymce/plugins/autolink';
    declare module 'tinymce/plugins/autoresize';
    declare module 'tinymce/plugins/autosave';
    declare module 'tinymce/plugins/charmap';
    declare module 'tinymce/plugins/code';
    declare module 'tinymce/plugins/codesample';
    declare module 'tinymce/plugins/directionality';
    declare module 'tinymce/plugins/emoticons';
    declare module 'tinymce/plugins/fullscreen';
    declare module 'tinymce/plugins/help';
    declare module 'tinymce/plugins/image';
    declare module 'tinymce/plugins/importcss';
    declare module 'tinymce/plugins/insertdatetime';
    declare module 'tinymce/plugins/link';
    declare module 'tinymce/plugins/lists';
    declare module 'tinymce/plugins/media';
    declare module 'tinymce/plugins/nonbreaking';
    declare module 'tinymce/plugins/pagebreak';
    declare module 'tinymce/plugins/preview';
    declare module 'tinymce/plugins/quickbars';
    declare module 'tinymce/plugins/save';
    declare module 'tinymce/plugins/searchreplace';
    declare module 'tinymce/plugins/table';
    declare module 'tinymce/plugins/template';
    declare module 'tinymce/plugins/visualblocks';
    declare module 'tinymce/plugins/visualchars';
    declare module 'tinymce/plugins/wordcount';
    
    



    卸载 tinymce-vue

    出于某种原因(比如重新安装最新版)需要卸载 tinymce-vue 的时候,使用命令,包的名称与安装的时候相同:

    npm remove @tinymce/tinymce-vue
    npm remove @types/tinymce
    npm remove tinymce
    


    手工卸载 tinymce-vue

    上面的命令,一般是能卸载掉了。若卸载失败。那么使用rm命令,强制删除安装包目录文件,然后再从package.json文件中手工去掉加载项。

    cd ./node_modules
    
    rm -rf  @tinymce
    rm -rf  @types/tinymce
    rm -rf  tinymce
    

    查看缓存目录

    npm config get cache
    
    显示结果,用来清空其中缓存,不同设置上,会不同
    /root/.npm
    

    删除缓存

    rm -rf /root/.npm/*
    

    到此手工删除完毕,可以重新安装了。

    tiptap

    tiptap 编辑器基于 Prosemirror,完全可扩展且无渲染。可以轻松地将自定义节点添加为 Vue 组件。使用无渲染组件(函数式组件),你将(几乎)完全控制标记和样式。菜单的外观或在DOM中的显示位置。这完全取决于使用者。多人在线实时协同编辑。

    项目:https://github.com/ueberdosis/tiptap
    官网:https://tiptap.dev/

    tiptap 最初是为 Vue2 开发的,现在已发展成为独立框架工具,对 Vue3、React、Svelte 集成友好。它基于 Prosemirror 进行扩展开发,是一款无头(headerless)富文本编辑器,默认情况下,它没有任何 UI 样式,你完全可以自己来配置想要的 UI,不需要重写 class,也不需要 important 代码。

    tiptap 功能非常强大,功能插件丰富。但它比其他编辑器更棒的地方在于多人在线实时编辑。类似于谷歌文档那种效果,所有使用者在同一个页面彼此能看到对方的光标位置和名字,大家同时在线协同编辑。整个协同体验流畅,如果你的应用场景需要多人实时协同编辑,选 tiptap 就对了。