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-Vue
tinymce-vue,是 TinyMCE 官方,把 TinyMCE 集成到 Vue 项目中。这个包是 TinyMCE 的一个应用安装包,使它更容易在 Vue 应用程序中使用。tinymce-vue 依赖 tinymce ,所以使用的时候,需要两个包:tinymce和@tinymce/tinymce-vue。TinyMCE 官网安装文档。
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版本。
#安装 tinymcenpm install tinymce #安装 typscript版npm install @types/tinymce -D #安装 tinymce-vuenpm 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/tinymcecp -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', //输出 xhtmlentity_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-vuenpm remove @types/tinymcenpm remove tinymce
手工卸载 tinymce-vue
上面的命令,一般是能卸载掉了。若卸载失败。那么使用rm命令,强制删除安装包目录文件,然后再从package.json文件中手工去掉加载项。
cd ./node_modulesrm -rf @tinymcerm -rf @types/tinymcerm -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 就对了。