Augular中接入tinymce富文本

1,350 阅读4分钟

Augular中接入tinymce富文本

本文主要介绍了如何在augular中接入tinymce富文本编辑器,并且实现图片和音频自定义上传的逻辑。

一、参考文档

二、富文本编辑器的接入

1、tiny插件的安装

执行命令:npm install tinymce@5.2.0 @tinymce/tinymce-angular@3.4.0 

注意:

  • 要根据augular的版本来选择第二个插件的版本,一般来说augular8以下的用@tinymce/tinymce-angular@3.0.0版本的,augular9以上用4.0版本的,augular4版本以下的不支持使用该插件,请注意升级
  • 如果是安装了3.0版本之后,在package.json中要把对应的插件版本固定下来,要不然的话在其他环境装包的时候可能会出现自动升级的情况,从而导致错误的出现

2、插件的引入

(1)、 在项目的components目录下添加tiny-editor文件夹

在下面创建tiny-editor.component.html、tiny-editor.component.scss、tiny-editor.component.ts文件

(2)、在tiny-editor.component.html文件中添加tiny-editor的引用,代码如下:

<div class="tiny-editor">
  <editor id="editorID" [init]="editorConfig" [(ngModel)]="editorContent"></editor>
</div>

其中,editorConfig中存储的是tiny的配置信息,也可以不传,不传的话tiny默认提供了几个功能可以使用,editContent中存储的是富文本编辑器的内容,是字符串的形式

(3)、完善tiny-editor.component.ts文件内容

根据上面,我们发现了两个参数,所以其实我们要做的很简单,就是把editorConfig的内容丰富好就行了,代码如下,以下会在代码中进行介绍:

// 编辑器配置
  editorConfig = {
    base_url: '/tinymce', // 设置编辑器的目录,指定一些自定义的文件的目录
    height: 500, // 编辑器的高度
    menubar: false, // 
    // 需要用到的插件,可以根据自己的需要进行选择,对应关系可以前往官网查看
    plugins: 'print preview searchreplace autolink fullscreen image media imagetools link code codesample table charmap hr pagebreak nonbreaking anchor advlist lists textpattern help autosave', // 当前版本这四个插件用不了bdmap indent2em formatpainter axupimgs,想要用需要单独去下载相关包
    // 菜单栏配置,这里设置决定了你需要向用户提供哪些功能的icon,具体功能的实现还是得plugins中实现
    toolbar: 'code undo redo restoredraft | cut copy | forecolor backcolor bold italic underline strikethrough link | alignleft aligncenter alignright alignjustify | bullist numlist blockquote subscript superscript removeformat | formatselect fontselect fontsizeselect | table image media charmap hr fullscreen',
    // 设置语言,另外默认的是英文包,需要自己卸载中文语言包放在项目目录下,后续会展开说明
    language: 'zh_CN',
    language_url: '/tinymce/lang/zh_CN.js',
    toolbar_mode : 'wrap', // 当菜单栏的总宽度超过编辑器的总宽度时,会折叠按钮,要想全部展示,设置为wrap换行展示,设置为scrolling在同一行滑动展示
    branding: false, // 是否展示右下角tiny支持字样
    autosave_ask_before_unload: false, // 不关闭的话,每次上传了图片之后再刷新都会提示是否重新加载页面
    // 自定义字体大小选择内容
    fontsize_formats: '12px 14px 16px 18px 24px 36px 48px 56px 72px',
    // 自定义字体的中英文难对照关系,默认英文
    font_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;',
    media_live_embeds: true, // 网上说开启该设置,视频上传后会自动播放,目测暂时不生效
    file_picker_types: 'media', // 指定文件上传类型 参数可为media(媒体)、image(图片)、file(文件)
    /*
    * @desc 自定义图片上传逻辑,否则的话就要服务端配合去tiny进行一些注册
    * @param blobInfo 文件信息,blobInfo.blob()文件流,blobInfo.filename()文件名称
    * @param succFun 成功回调,需要将成功的url放在这里,会回显到成功的框内
    * @param failFun 失败回调,上传失败后的提示
    */
    images_upload_handler: (blobInfo, succFun, failFun) => {
      // 这里去实现自己公司的图片上传的功能即可
      this.post('xxx', {chunk: blobInfo.blob()})
          .then(res => {
              succFun(res.url)
          })
          .catch(err => {
              failFun(err)
          })
    },
    //自定义文件选择器的回调内容
    file_picker_callback:  (callback, value, meta) => {
      if (meta.filetype === 'media') {
        let input = document.createElement('input');
        input.setAttribute('type', 'file');
        let _this = this;
        input.onchange = function () {
          // @ts-ignore
          let file = this.files[0];
          // 上传视频方法
          _this.uploadMv(file);
          // 因为只有这个callback才能够将他回显到页面上,所以要在这里callback
          if(_this.uploaded) {
            callback(_this.mvUrl);
          } else {
            setTimeout(() => {
              callback(_this.mvUrl);
            }, 1000)
          }
        }
        // 触发点击
        input.click()
        // callback('http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4');
      } else {
        this.commonMethodService.toast('请选择正确的音频文件');
      }
    },
    // tiny上传完音频返回的是一张封面图,而不是可播放的音频,要想可播放的话,需要加以下的方法,来渲染一个可以播放音频的video出来
    media_url_resolver: function (data, resolve) {
      try {
        console.log('data', data)
          let videoUri = encodeURI(data.url);
          let embedHtml = `<p>
              <span
                  class="mce-object-video"
                  data-mce-selected="1"
                  data-mce-object="video"
                  data-mce-p-width="100%"
                  data-mce-p-height="auto"
                  data-mce-p-controls="controls"
                  data-mce-p-controlslist="nodownload"
                  data-mce-p-allowfullscreen="true"
                  data-mce-p-src=${videoUri} >
                  <video src=${data.url} width="100%" height="auto" controls="controls" controlslist="nodownload">
                  </video>
              </span>
          </p>`
          resolve({ html: embedHtml });
      } catch (e) {
          resolve({ html: "" });
      }
    }
  };

3、其他配置信息修改

(1)、修改augular.json

主要是在assets下指定将node_modules里面插件的内容部署到我们指定的文件目录下,从而让timy编辑器能够找到,完整代码如下
{
    "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
    "version": 1,
    "newProjectRoot": "projects",
    "projects": {
        "web-template": {
            "root": "",
            "sourceRoot": "src",
            "projectType": "application",
            "architect": {
                "build": {
                    "builder": "@angular-devkit/build-angular:browser",
                    "options": {
                        "outputPath": "dist",
                        "index": "src/index.html",
                        "main": "src/main.ts",
                        "polyfills": "src/polyfills.ts",
                        "tsConfig": "src/tsconfig.app.json",
                        "assets": [
                            "src/favicon.ico",
                            "src/assets",
                            {
                              "glob": "**/*",
                              "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
                              "output": "/assets/"
                            },
                            {
                              "glob": "**/*",
                              "input": "node_modules/tinymce/skins",
                              "output": "/tinymce/skins/"
                            },
                            {
                              "glob": "**/*",
                              "input": "node_modules/tinymce/themes",
                              "output": "/tinymce/themes/"
                            },
                            {
                              "glob": "**/*",
                              "input": "node_modules/tinymce/plugins",
                              "output": "/tinymce/plugins/"
                            },
                            {
                              "glob": "**/*",
                              "input": "src/tinymce/lang",
                              "output": "/tinymce/lang/"
                            },
                            {
                              "glob": "**/*",
                              "input": "node_modules/tinymce/icons",
                              "output": "/tinymce/icons/"
                            },
                            { "glob": "**/*", "input": "node_modules/tinymce", "output": "/tinymce/" }
                        ],
                        "styles": [
                            "node_modules/ng-zorro-antd/src/ng-zorro-antd.min.css",
                            "node_modules/@jdb-ui/core/publish/src/jdb-plg-ui.min.css",
                            "src/styles.scss",
                            "src/jdb-ui.scss"
                        ],
                        "scripts": [
                            "src/assets/laydate.js",
                            "node_modules/tinymce/tinymce.min.js"
                        ]
                    },
                    "configurations": {
                        "serve": {
                            "fileReplacements": [{
                                "replace": "src/environments/environment.ts",
                                "with": "src/environments/environment.serve.ts"
                            }]
                        },
                        "debug": {
                            "fileReplacements": [{
                                "replace": "src/environments/environment.ts",
                                "with": "src/environments/environment.test.ts"
                            }]
                        },
                        "test": {
                            "optimization": true,
                            "outputHashing": "all",
                            "sourceMap": false,
                            "extractCss": true,
                            "namedChunks": false,
                            "aot": true,
                            "extractLicenses": true,
                            "vendorChunk": false,
                            "buildOptimizer": true,
                            "fileReplacements": [{
                                "replace": "src/environments/environment.ts",
                                "with": "src/environments/environment.test.ts"
                            }]
                        },
                        "huidu": {
                            "optimization": true,
                            "outputHashing": "all",
                            "sourceMap": false,
                            "extractCss": true,
                            "namedChunks": false,
                            "aot": true,
                            "extractLicenses": true,
                            "vendorChunk": false,
                            "buildOptimizer": true,
                            "fileReplacements": [{
                                "replace": "src/environments/environment.ts",
                                "with": "src/environments/environment.huidu.ts"
                            }]
                        },
                        "betaProd": {
                            "fileReplacements": [{
                                "replace": "src/environments/environment.ts",
                                "with": "src/environments/environment.prod.ts"
                            }],
                            "optimization": true,
                            "outputHashing": "all",
                            "sourceMap": true,
                            "extractCss": true,
                            "namedChunks": false,
                            "aot": true,
                            "extractLicenses": true,
                            "vendorChunk": false,
                            "buildOptimizer": true
                        },
                        "production": {
                            "fileReplacements": [{
                                "replace": "src/environments/environment.ts",
                                "with": "src/environments/environment.prod.ts"
                            }],
                            "optimization": true,
                            "outputHashing": "all",
                            "sourceMap": false,
                            "extractCss": true,
                            "namedChunks": false,
                            "aot": true,
                            "extractLicenses": true,
                            "vendorChunk": false,
                            "buildOptimizer": true
                        }
                    }
                },
                "serve": {
                    "builder": "@angular-devkit/build-angular:dev-server",
                    "options": {
                        "browserTarget": "web-template:build"
                    },
                    "configurations": {
                        "serve": {
                            "browserTarget": "web-template:build:serve",
                            "proxyConfig": "proxy.serve.js"
                        },
                        "debug": {
                            "browserTarget": "web-template:build:debug",
                            "proxyConfig": "proxy.debug.json"
                        }
                    }
                },
                "extract-i18n": {
                    "builder": "@angular-devkit/build-angular:extract-i18n",
                    "options": {
                        "browserTarget": "web-template:build"
                    }
                },
                "test": {
                    "builder": "@angular-devkit/build-angular:karma",
                    "options": {
                        "main": "src/test.ts",
                        "polyfills": "src/polyfills.ts",
                        "tsConfig": "src/tsconfig.spec.json",
                        "karmaConfig": "src/karma.conf.js",
                        "styles": [
                            "node_modules/ng-zorro-antd/src/ng-zorro-antd.min.css",
                            "node_modules/@jdb-ui/core/publish/src/jdb-plg-ui.min.css",
                            "src/styles.scss"
                        ],
                        "scripts": [
                            "src/assets/laydate.js"
                        ],
                        "assets": [
                            "src/favicon.ico",
                            "src/assets",
                            {
                                "glob": "**/*",
                                "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
                                "output": "/assets/"
                              }
                        ]
                    }
                },
                "lint": {
                    "builder": "@angular-devkit/build-angular:tslint",
                    "options": {
                        "tsConfig": [
                            "src/tsconfig.app.json",
                            "src/tsconfig.spec.json"
                        ],
                        "exclude": [
                            "**/node_modules/**"
                        ]
                    }
                }
            }
        },
        "web-template-e2e": {
            "root": "e2e/",
            "projectType": "application",
            "architect": {
                "e2e": {
                    "builder": "@angular-devkit/build-angular:protractor",
                    "options": {
                        "protractorConfig": "e2e/protractor.conf.js",
                        "devServerTarget": "web-template:serve"
                    },
                    "configurations": {
                        "production": {
                            "devServerTarget": "web-template:serve:production"
                        }
                    }
                },
                "lint": {
                    "builder": "@angular-devkit/build-angular:tslint",
                    "options": {
                        "tsConfig": "e2e/tsconfig.e2e.json",
                        "exclude": [
                            "**/node_modules/**"
                        ]
                    }
                }
            }
        }
    },
    "defaultProject": "web-template",
    "schematics": {
        "@schematics/angular:component": {
            "styleext": "scss",
            "spec": false
        },
        "@schematics/angular:directive": {
            "spec": false
        },
        "@schematics/angular:service": {
            "spec": false
        },
        "@schematics/angular:pipe": {
            "spec": false
        },
        "@schematics/angular:module": {
            "spec": false
        }
    }
}

三、常见问题解决

1、富文本编辑器报icons文件找不到了或者说warning提示你说要先注册

此时你F12打开你的netWork查看的话,你会发现你的浏览器加载的tiny.min.js文件的话,是走的cdn加载,也就是从网络上获取的文件。

从网络上获取的文件,他本身是不会有icons相关内容的,所以他才会给你报错说,有个icons.js文件找不到了,然后那个注册的提示也是同理,因为你走的是cdn在线的一个文件,而且此时其实文件加载也是比较慢的

解决方法:

在augular.json中增加scripts配置,配置内容为,即为指定从node_modules中加载tiny.min.js文件,就可以了,详细配置位置等参考上满augular.json中配置
"scripts": [
    "node_modules/tinymce/tinymce.min.js"
]

注意: 1、关于提示没有在tiny注册这个东西还有一个解决方法就是去tiny那边注册一下,然后把你的域名添加到白名单就好了

2、为什么要配置本地加载也有一个好处,虽然说cdn是就近加速,但是其实还是会受到网络状态的影响,会导致富文本的加载速度和体验都不会很好,所以换成本地加载的话也能有效的解决这一个问题

2、关于图片上传

tiny提供了自定义图片上传的参数images_upload_handler,在这里可以拦截自定义图片上传的逻辑

/*
* @param blobInfo -- 图片的文件流,提供了很多方法可以获得文件的名字、内容等信息
* @param succFun 成功的回调,将上传成功的图片url放在回调中即可
* @param failFun 失败回调
*/
images_upload_handler: (blobInfo, succFun, failFun) => {
  this.post('xxxx', params)
    .then((res) => {
      succFun(res.data && res.data.url);
    })
    .catch((err) => {
      failFunn(err);
    })
}

3、关于音频上传

tiny提供了自定义文件上传的参数file_picker_callback,在这里可以拦截自定义文件上传的逻辑,这里面包括了media(媒体),image(图片),file(文件)

file_picker_callback:  (callback, value, meta) => {
  if (meta.filetype === 'media') {
    // 创建一个input框,来触发文件选择的逻辑
    let input = document.createElement('input');
    input.setAttribute('type', 'file');
    let _this = this;
    input.onchange = function () {
      // @ts-ignore
      let file = this.files[0];
      // 自定义音频上传逻辑,业务自己实现
      _this.uploadMv(file);
      if(_this.uploaded) {
        // 上传成功之后,将url回调回去
        callback(_this.mvUrl);
      } else {
        setTimeout(() => {
          callback(_this.mvUrl);
        }, 1000)
      }
    }
    // 触发点击
    input.click()
    // callback('http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4');
  } else {
    this.commonMethodService.toast('请选择正确的音频文件');
  }
},

4、音频上传之后,只能展示封面,无法播放视频

核心思路就是我们构建一个可以播放音频的html的片段,插入到富文本中,利用tiny提供的media_url_resolver方法

// tiny上传完音频返回的是一张封面图,而不是可播放的音频,要想可播放的话,需要加以下的方法,来渲染一个可以播放音频的video出来
media_url_resolver: function (data, resolve) {
  try {
    console.log('data', data)
      let videoUri = encodeURI(data.url);
      let embedHtml = `<p>
          <span
              class="mce-object-video"
              data-mce-selected="1"
              data-mce-object="video"
              data-mce-p-width="100%"
              data-mce-p-height="auto"
              data-mce-p-controls="controls"
              data-mce-p-controlslist="nodownload"
              data-mce-p-allowfullscreen="true"
              data-mce-p-src=${videoUri} >
              <video src=${data.url} width="100%" height="auto" controls="controls" controlslist="nodownload">
              </video>
          </span>
      </p>`
      resolve({ html: embedHtml });
  } catch (e) {
      resolve({ html: "" });
  }
}