Tinymce富文本编辑器 在 vue 项目中的封装与使用 解决上传图片与文件

写在前面


作者简书地址
在我们前端开发中有时候需要对文章或者商品详情之类的进行编辑,需要图文并茂,文字排版.......普通的form表单已经不能担此重任。
本文呢?讲的如何在vue项目中如何封装富文本编辑器 Tinymce 成组件 如何上传图片 文件
公司采用的是阿里云oss上传 所以以这个为例
简书首发网址

tinymce.gif

第三步将整个tinymce封装的代码贴出
第四步对代码进行解析,描述一下为什么这么做
第五步将贴出再其它组件调用tinymce的注意事项与使用方法

具体步骤

一 、下载tinymce 原数据包 并拷贝到 static文件夹下
版本锁定4.8.2

npm i tinymce -S

1、从node_modules中找到tinymce数据包
2、拷贝tinymce数据到static文件夹下


image.png

二、在componts文件夹下建个Tinymce组件


image.png

三、编写tinymce组件
<template>
  <div>
    <input type="file" id="photoFileUpload" style="display: none" />
    <textarea :id="Id"></textarea>
  </div>
</template>
<script>
import { ossUpload, uploadImg } from '@/api/public'
import '../../../static/tinymce/tinymce'
export default {
  name: 'mceeditor',
  props: {
    value: {
      default: '',
      type: String
    },
    config: {
      type: Object,
      default: () => {
        return {
          theme: 'modern',
          height: 600
        }
      }
    },
    url: {
      default: '',
      type: String
    },
    accept: {
      default: 'image/jpeg, image/png',
      type: String
    },
    maxSize: {
      default: 2097152,
      type: Number
    }
  },
  data() {
    const Id = Date.now()
    return {
      Id: Id,
      myEditor: null,
      DefaultConfig: {
        // GLOBAL
        language: 'zh_CN', //汉化
        height: 500, //默认高度
        theme: 'modern', //默认主题
        menubar: true,
        toolbar: `styleselect | fontselect | formatselect | fontsizeselect | forecolor backcolor | bold italic underline strikethrough | insertfile link image | table | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | preview removeformat  hr | paste code | undo redo | fullscreen `,//需要的工具栏
        plugins: `
            paste
            importcss
            image
            code
            table
            advlist
            fullscreen
            link
            lists
            textcolor
            colorpicker
            hr
            preview
          `,
        // CONFIG
        forced_root_block: 'p',
        force_p_newlines: true,
        importcss_append: true,
        // CONFIG: ContentStyle 这块很重要, 在最后呈现的页面也要写入这个基本样式保证前后一致, `table`和`img`的问题基本就靠这个来填坑了
        content_style: `
            *                         { padding:0; margin:0; }
            html, body                { height:100%; }
            img                       { max-width:100%; display:block;height:auto; }
            a                         { text-decoration: none; }
            iframe                    { width: 100%; }
            p                         { line-height:1.6; margin: 0px; }
            table                     { word-wrap:break-word; word-break:break-all; max-width:100%; border:none; border-color:#999; }
            .mce-object-iframe        { width:100%; box-sizing:border-box; margin:0; padding:0; }
            ul,ol                     { list-style-position:inside; }
          `,
        insert_button_items: 'image link | inserttable',
        // CONFIG: Paste
        paste_retain_style_properties: 'all',
        paste_word_valid_elements: '*[*]',        // word需要它
        paste_data_images: true,                  // 粘贴的同时能把内容里的图片自动上传,非常强力的功能
        paste_convert_word_fake_lists: false,     // 插入word文档需要该属性
        paste_webkit_styles: 'all',
        paste_merge_formats: true,
        nonbreaking_force_tab: false,
        paste_auto_cleanup_on_paste: false,
        // CONFIG: Font
        fontsize_formats: '10px 11px 12px 14px 16px 18px 20px 24px',
        // CONFIG: StyleSelect
        style_formats: [
          {
            title: '首行缩进',
            block: 'p',
            styles: { 'text-indent': '2em' }
          },
          {
            title: '行高',
            items: [
              { title: '1', styles: { 'line-height': '1' }, inline: 'span' },
              { title: '1.5', styles: { 'line-height': '1.5' }, inline: 'span' },
              { title: '2', styles: { 'line-height': '2' }, inline: 'span' },
              { title: '2.5', styles: { 'line-height': '2.5' }, inline: 'span' },
              { title: '3', styles: { 'line-height': '3' }, inline: 'span' }
            ]
          }
        ],
        // FontSelect
        font_formats: `
            微软雅黑=微软雅黑;
            宋体=宋体;
            黑体=黑体;
            仿宋=仿宋;
            楷体=楷体;
            隶书=隶书;
            幼圆=幼圆;
            Andale Mono=andale mono,times;
            Arial=arial, helvetica,
            sans-serif;
            Arial Black=arial black, avant garde;
            Book Antiqua=book antiqua,palatino;
            Comic Sans MS=comic sans ms,sans-serif;
            Courier New=courier new,courier;
            Georgia=georgia,palatino;
            Helvetica=helvetica;
            Impact=impact,chicago;
            Symbol=symbol;
            Tahoma=tahoma,arial,helvetica,sans-serif;
            Terminal=terminal,monaco;
            Times New Roman=times new roman,times;
            Trebuchet MS=trebuchet ms,geneva;
            Verdana=verdana,geneva;
            Webdings=webdings;
            Wingdings=wingdings,zapf dingbats`,
        // Tab
        tabfocus_elements: ':prev,:next',
        object_resizing: true,
        // Image
        imagetools_toolbar: 'rotateleft rotateright | flipv fliph | editimage imageoptions'
      }
    }
  },
  methods: {
    setContent(content) {
      this.myEditor.setContent(content)
    },
    getContent() {
      return this.myEditor.getContent()
    },
    init() {
      const self = this
      window.tinymce.init({
        // 默认配置
        ...this.DefaultConfig,
        // 挂载的DOM对象
        selector: `#${this.Id}`,
        file_picker_types: 'file',
        //上传文件
        file_picker_callback: function (callback, value, meta) {
          let fileUploadControl = document.getElementById("photoFileUpload")
          fileUploadControl.click()
          fileUploadControl.onchange = function () {
            if (fileUploadControl.files.length > 0) {
              let localFile = fileUploadControl.files[0]
              ossUpload({ type: localFile.type }).then(res => {
                uploadImg(res.data, localFile).then(res => {
                  if (res.code == 0) {
                    callback(res.data.name, { text: localFile.name, })
                    self.$emit('on-upload-complete', res)  // 抛出 'on-upload-complete' 钩子
                  } else {
                    callback()
                    self.$emit('on-upload-complete', res)  // 抛出 'on-upload-complete' 钩子
                  }
                })
              })
            } else {
              alert('请选择文件上传')
            }
          }
        },
        // 图片上传
        images_upload_handler: function (blobInfo, success, failure) {
          if (blobInfo.blob().size > self.maxSize) {
            failure('文件体积过大')
          }
          if (self.accept.indexOf(blobInfo.blob().type) >= 0) {
            uploadPic()
          } else {
            failure('图片格式错误')
          }

          function uploadPic() {
            ossUpload({ type: "image/png" }).then(res => {
              uploadImg(res.data, blobInfo.blob()).then(res => {
                if (res.code == 0) {
                  success(res.data.name)
                  self.$emit('on-upload-complete', res)  // 抛出 'on-upload-complete' 钩子
                } else {
                  failure('上传失败: ')
                  self.$emit('on-upload-complete', res)  // 抛出 'on-upload-complete' 钩子
                }
              })
            })
          }
        },
        // prop内传入的的config
        ...this.config,
        setup: (editor) => {
          self.myEditor = editor
          editor.on(
            'init', () => {
              self.loading = true
              self.$emit('on-ready')   // 抛出 'on-ready' 事件钩子
              editor.setContent(self.value)
              self.loading = false
            }
          )
          // 抛出 'input' 事件钩子,同步value数据
          editor.on(
            'input change undo redo', () => {
              self.$emit('input', editor.getContent())
            }
          )
        }
      })
    }
  },
  mounted() {
    this.init()
  },
  beforeDestroy() {
    // 销毁tinymce
    this.$emit('on-destroy')
    window.tinymce.remove(`#${this.Id}`)
  },
}
</script>

四、解析代码 择重点讲
1.dom结构 主要有两个标签,input textrea
input: 用来做模拟文件上传的,上传后文件的地址 以a标签存在。
官网的demo中文件(附件)上传需要有php 和 .net环境支持 需要后台配合,效果会更好,貌似还需要收费。自己可以招后端小伙伴研究
tinymce 官网附件地址:https://www.moxiemanager.com/demos/tinymce.php
使用input标签的方法是我绞尽脑汁想出来的唯一替代方法,也是我封装出来与其他人不同的地方,这里所需要的参数和配置在以下两个参数中:
file_picker_types: 'file', 参数
file_picker_callback ,上传文件的钩子函数 在里面配置的上传文件函数即可
textarea:用来初始化tinymce的容器

2.import 引入 static文件下的 tinymce.js

3.prop: 组件调用时可传入的配置参数。

  • url:是留出来给上传函数封装普通ajax的路径,因为我是oss上传 所以直接改造了images_upload_handler钩子
  • accept:是点击上传图片 可选择图片类型

4.data部分

* DefaultConfig 注意配置tinymce的默认项 可以根据自己的需求增减
* myEditor 用来获取tinymce的setContent 和 getContent 两个实用api,观看代码它在 tinymce的setup阶段出现

5.methods
注意暴露出 两个实用api 再将tinymce初始化
需要自己动手的地方 ⚠️

* file_picker_types
* file_picker_callback
* images_upload_handler

这三个地方就是最重要的 文件上传以及图片上传,根据自己的业务需求改造上传函数即可

6.抛出了三个钩子函数给tinymce的父组件调用

  • on-ready
  • on-upload-complete
  • on-destroy

五、组件调用

<template>
  <div id="home" class="pd20">
    tinyMce的使用
    <tinymce ref="richText" v-model="content" @on-upload-complete="onEditorUploadComplete"></tinymce>
    <div class="mt20">
      <el-button type="primary" class="w100" @click="get">保存</el-button>
      <el-button type="primary" class="w100" @click="set">设置</el-button>
    </div>
  </div>
</template>

<script>
import tinymce from '@/components/Tinymce'
window.tinymce.baseURL = '/static/tinymce' //需要调用tinymce的组件中得加入这,不然会报错
//this.$refs.richText.setContent//getContent 两个方法 获取与设置
export default {
  name: 'Dashboard',
  components: {
    tinymce
  },
  data() {
    return {
      content: '欢迎来到首页'
    }
  },
  watch: {

  },
  computed: {
  },
  methods: {
    onEditorUploadComplete(res) {
      if (res.code == 0) {
        this.$message({
          type: 'success',
          message: '上传成功'
        })
      } else {
        this.$message({
          type: 'error',
          message: res.msg
        })
      }
    },
    set() {
      this.$refs.richText.setContent('设置内容')
    },
    get() {
      console.log(this.$refs.richText.getContent())
    },
  },
  created() {

  },
  mounted() {

  }
}
</script>

<style lang="less" scoped>
</style>

补个知识点:
vue 中的ref 相当于 dom节点中id的作用 显然是更强大,this.$refs.ref,是可以拿到这个这个名为ref的实例,可以调用这个ref组件的methods方法(俗称 :父组件调用子组件的方法)

写在后面

总结:本文也是参考了许多其他优秀的前端开发写的博客,路数也差不多,都是写到图片上传就点到为止了,所以相似度很。但是我极少看见其他人会把tinymce文件上传的这部分写出来,我也算是另辟蹊径(歪门邪道),把这个共享给大家。希望大家会喜欢。觉得有用的小伙伴可以给个心,给个关注,有不懂的地方可以评论我和私信我,有空会一一解答。也可以加扣(214395856)
目前本人因要踏入node开发,学习egg.js 所以前端的文章会更新的较少了。如果有想到好的题材也会分享出来。
简书首发网址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270