转载:https://segmentfault.com/a/1190000012791569?utm_source=tag-newest

2019-2-18

貌似这篇文章帮了大家一些小忙
最近tinymce出5.0版本了,下面的api还是4.x的,新版本可能会有些不适用了,最近业务繁忙,等哪天周末有时间的话我再做点更新 :)

前言

最近因业务需求在项目中嵌入了tinymce这个编辑器,用于满足平台给用户编辑各类新闻内容什么的业务需求,前后也花了不少时间体验和对比了市面上各类开源编辑器。

各大WYSIWYG编辑器的简单比较

UEditor: 因为已经不再维护了,需要大量修改源码,很多都是专门为jsp等服务器渲染项目写的代码需要删除, 然后越删越害怕越删越不敢用,依赖jquery,需要专门用js去parse编辑完成的内容,parse完的内容还可能污染全局css,兼容老浏览器还不错, 但是,我们不怎么考虑兼容IE。所以,告辞。

wangEditor: 中文文档,上手快,依赖jquery,功能少点要花时间去写插件,需要单独为图片上传功能写个接口,老项目忙着上线临时用过,感觉并不适合当前业务这么重的编辑功能于是放弃了。

Quill:api友好, 功能少,需要特定的css去解析文本(这点我不大喜欢),ui好看,适合作为论坛回帖功能使用。

CKEditor: CKEditor目前主流的还是4.x的版本,但是文档看着很瞎眼实在是提不起兴致去配置,草草用了下就放弃了,5.x版本刚从beta结束,需要指定专门的node以及npm版本,虽然功能强大配置灵活ui漂亮不过目前糟糕的兼容性基本是不可能出现在大众视野了。

KingEditor: 丑,不喜欢,不爱用

Draft-js: 知乎最近刚改的文本编辑器就是在draft的基础上开发的,依赖react, 弃。

Medium-editor: 虽然看着感觉很酷炫,但是,不适合我们的业务场景啊, api也简陋可怕。

trix: 嗯,又一个小而美,放弃

Slate: react,放弃

Bootstrap-wysiwyg: bootstrap, jquery, 放弃

tinymce: 文档好,功能强,bug少,无外部依赖,大家用了都说好,嗯,没错就是它了。

编辑器配置方面只要能看得懂英文耍起来还是比较简单的,适配中碰到的大部分问题都可以通过看文档解决,即便看文档解决不了网上也有大量的文章能告诉你怎么配置能解决。

当然了,主要是我这里需要解决一些别人觉得超简单自己一想都很烦人的需求,比如:

  1. word文档粘贴进来要带格式
  2. 兼容移动端
  3. word文档粘贴进来要正常显示并且还要兼容移动端
  4. 电脑网页里粘贴进来内容要正常显示并且排版还不能乱
  5. 电脑网页拷过来的内容还要兼容到移动端

初始化

因为tinymce的Plugins是按需加载的
为了能先快速上手这个编辑器
就先在vue-cli的index.html中默认塞入一条在线cdn地址

<script src="https://cdn.bootcss.com/tinymce/4.7.4/tinymce.min.js"></script>

记得去下载语言包到本地,
然后就在文件内引入

import './zh_CN.js'

后面有机会再写下单独打包的事项,毕竟这货体积还不小。

插入vue组件模板

<template>
<div>
<textarea :id= "Id"></textarea>
</div>
</template>

记得一定要在textarea外面包一层div,不然...你自己试试看就知道了。

组件基础配置

将tinymce通过指定的selector挂载到组件中

<template>
<div>
<textarea :id= "Id"></textarea>
</div>
</template>
<script>
import './zh_CN.js'
export default {
data () {
const Id = Date.now()
return {
Id: Id,
Editor: null,
DefaultConfig: {}
}
},
props: {
value: {
default: '',
type: String
},
config: {
type: Object,
default: () => {
return {
theme: 'modern',
height: 300
}
}
}
},
mounted () {
this.init()
},
beforeDestroy () {
// 销毁tinymce
this.$emit('on-destroy')
window.tinymce.remove(`#${this.Id}`)
},
methods: {
init () {
const self = this
this.Editor = window.tinymce.init({
// 默认配置
...this.DefaultConfig, // prop内传入的的config
...this.config, // 挂载的DOM对象
selector: `#${this.Id}`, setup: (editor) => {
// 抛出 'on-ready' 事件钩子
editor.on(
'init', () => {
self.loading = false
self.$emit('on-ready')
editor.setContent(self.value)
}
)
// 抛出 'input' 事件钩子,同步value数据
editor.on(
'input change undo redo', () => {
self.$emit('input', editor.getContent())
}
)
}
})
}
}
}
</script>

好了,组件基本的初始化完成,后面正式开始踩坑之旅

API

具体内容看官网的API就行,英语不好的用chrome翻译下对照着demo也能看个七七八八,当然主要原因还是我比较懒。

我这边根据自身业务需求在组件的data内写了个默认配置

DefaultConfig: {
// GLOBAL
height: 500,
theme: 'modern',
menubar: false,
toolbar: `styleselect | fontselect | formatselect | fontsizeselect | forecolor backcolor | bold italic underline strikethrough | image media | table | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | preview removeformat hr | paste code link | undo redo | fullscreen `,
plugins: `
paste
importcss
image
code
table
advlist
fullscreen
link
media
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'
}

因为本人比较懒,以上配置导出的代码可能会有代码注入的风险,建议保存的时候再前后端都做下注入过滤,不过一般数据安全问题主要还是服务器那边的事情?。

后面的图片上传可以单独拆出来做个小配置,直接写到props里好了。

  url: {
default: '',
type: String
},
accept: {
default: 'image/jpeg, image/png',
type: String
},
maxSize: {
default: 2097152,
type: Number
},
withCredentials: {
default: false,
type: Boolean
}

然后把这套东西塞到init配置里

  // 图片上传
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 () {
const xhr = new XMLHttpRequest()
const formData = new FormData()
xhr.withCredentials = self.withCredentials
xhr.open('POST', self.url)
xhr.onload = function () { if (xhr.status !== 200) {
// 抛出 'on-upload-fail' 钩子
self.$emit('on-upload-fail')
failure('上传失败: ' + xhr.status)
return
} const json = JSON.parse(xhr.responseText)
// 抛出 'on-upload-success' 钩子
self.$emit('on-upload-complete' , [
json, success, failure
])
}
formData.append('file', blobInfo.blob())
xhr.send(formData)
}
}

至此, 一个组件的封装基本算是完成了

看下初阶成果

<template>
<div>
<textarea :id= "Id"></textarea>
</div>
</template>
<script>
import './zh_CN.js'
export default {
data () {
const Id = Date.now()
return {
Id: Id,
Editor: null,
DefaultConfig: {
// GLOBAL
height: 500,
theme: 'modern',
menubar: false,
toolbar: `styleselect | fontselect | formatselect | fontsizeselect | forecolor backcolor | bold italic underline strikethrough | image media | table | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | preview removeformat hr | paste code link | undo redo | fullscreen `,
plugins: `
paste
importcss
image
code
table
advlist
fullscreen
link
media
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'
}
}
},
props: {
value: {
default: '',
type: String
},
config: {
type: Object,
default: () => {
return {
theme: 'modern',
height: 300
}
}
},
url: {
default: '',
type: String
},
accept: {
default: 'image/jpeg, image/png',
type: String
},
maxSize: {
default: 2097152,
type: Number
},
withCredentials: {
default: false,
type: Boolean
}
},
mounted () {
this.init()
},
beforeDestroy () {
// 销毁tinymce
this.$emit('on-destroy')
window.tinymce.remove(`$#{this.Id}`)
},
methods: {
init () {
const self = this this.Editor = window.tinymce.init({
// 默认配置
...this.DefaultConfig, // 图片上传
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 () {
const xhr = new XMLHttpRequest()
const formData = new FormData()
xhr.withCredentials = self.withCredentials
xhr.open('POST', self.url)
xhr.onload = function () { if (xhr.status !== 200) {
// 抛出 'on-upload-fail' 钩子
self.$emit('on-upload-fail')
failure('上传失败: ' + xhr.status)
return
} const json = JSON.parse(xhr.responseText)
// 抛出 'on-upload-complete' 钩子
self.$emit('on-upload-complete' , [
json, success, failure
])
}
formData.append('file', blobInfo.blob())
xhr.send(formData)
}
}, // prop内传入的的config
...this.config, // 挂载的DOM对象
selector: `#${this.Id}`,
setup: (editor) => {
// 抛出 'on-ready' 事件钩子
editor.on(
'init', () => {
self.loading = false
self.$emit('on-ready')
editor.setContent(self.value)
}
)
// 抛出 'input' 事件钩子,同步value数据
editor.on(
'input change undo redo', () => {
self.$emit('input', editor.getContent())
}
)
}
})
}
}
}
</script>

直接引入组件调用就行了

<template>
<mce-editor
:config = "Config"
v-model = "Value"
:url = "Url"
:max-size = "MaxSize"
:accept = "Accept"
:with-credentials = false
@on-ready = "onEditorReady"
@on-destroy = "onEditorDestroy"
@on-upload-success= "onEditorUploadComplete"
@on-upload-fail = "onEditorUploadFail"
></mce-editor>
</template>

但是作为一名优秀的程序员,这怎么可能够嘛。 
下面说下打包的事情

塞入webpack

为了加快页面载入速度就要首先解决载入文件过多的问题,而大部分时间用户并不需要每次打开页面都先加载一遍editor的核心文件,而editor本身也要按需加载内容,一开始想把每个plugin都搞成独立组件模块按需载入,但是这就要涉及到修改编辑器本身源码,或者说对window.tinymce删掉点特性,这些都太麻烦也都有风险,对后面的代码维护影响也大,索性就都先留着。
后面边做边改吧

还是以vue-cli为例
把官网下载的包塞到stataic文件夹中
然后删掉index.html模版中的cdn代码吧不需要了
当然这里有俩选择
要么做成一个异步组件,单独打包,按需载入
要么直接引入到main.js中将包打成为一个巨无霸
所以我选择前者,

首先老规矩 引入编辑器主体

import '../../static/tinymce/tinymce.min.js'

然后刷新下页面,不出意外应该是报这么个错Uncaught SyntaxError: Unexpected token <
眼尖的朋友应该知道是怎么回事了theme.js:1
在默认配置下, tinymce载入的theme的路径居然是这个
Request URL:http://localhost:8080/themes/modern/theme.js
然后我跑去官网搜了下api 只搜到一个叫document_base_url的api,但是根据多年程序员的直觉经验告诉我 不是这货(嗯,我在这里卡住了),网上翻了下各地文献,都没有啊,
那怎么办呢
于是我就跑去看源码...但是4万行...算了...
然后我就在控台打印了下tinymce对象,然后发现了一个叫baseURLstring对象,嗯,有希望了。
在源码里搜了下baseURL
蹦出来这段代码 .... 算了有很多段...
大致思想就是通过当前URI拆出来个baseURL,改掉就行了

window.tinymce.baseURL = '/static/tinymce'

如果需要载入的地址是另一个比如自己公司的cdn的路径,那改成全路径就行了

window.tinymce.baseURL = 'http://cdn.xxx.com/static/tinymce'

貌似路径的问题解决了

但是新的问题又出现了,
插件下过来都是带min的,但默认载入的插件都是不带min的,一定是我源码没看仔细,
然后我又搜了一下代码

if (!baseURL && document.currentScript) {
src = document.currentScript.src;
if (src.indexOf('.min') != -1) {
suffix = '.min';
} baseURL = src.substring(0, src.lastIndexOf('/'));
}

希望就在眼前,貌似是业务我载入的方式是直接导入到模块的,于是一个叫suffix的默认值为空了,于是我去又加了行代码:

window.tinymce.suffix = '.min'

成功!
你看嘛,超级简单的是不是,根本不用改源码,网上说的动不动就去改源码什么的不要信啊不要信,大部分面向对象的事情改个默认值就行了。

对了,还记得前面的语言包嘛,
下过来塞到/static/tinymce/langs文件夹里
然后删掉

import './zh_CN.js'

这行代码
DefaultConfig中放入一个新配置项

language: 'zh_CN'

好了,后面就是模块打包的事情了,

打包

前面打的包有一个问题是默认配置是载入tinyMce本体,那么就会造成这个包大概有500k的体积,如果这个组件不做异步载入的处理,那么对于某些业务来说就是灾难。虽然这么做打开只用载入一个文件,业务比较稳定。
但我觉得这样不优雅所以最后还是把它单独拎出来了。
同理,根据这个库本身的特性,我们完全可以把这么多个必须的plugin按需要直接统一打成一个包,直接载入。这样,我们就又多了一个几百k的plugins包。
然后把plugins包和tinyMce主体包在不阻塞页面加载的情况下,做个懒加载提前缓存好文件方便后面使用,而组件本身在挂载前做个监听window.tinymce全局变量的方法,然后cdn控制下文件的过期时间即可。
这样,在保证了灵活度的前提下也保证了业务载入的速度。

完,感谢阅读。

附:两个demo

demo1:https://github.com/Ta0hua/tinymce-study (项目结构是vue-cli3.x)

demo2: https://github.com/2601940602/tinymcedemo (项目结构是vue-cli2.x)

vue项目移植tinymce踩坑的更多相关文章

  1. vue中的小踩坑(01)

    前言: 昨天算是使用vue2.0+element-ui做了一点小小的页面,可是源于其中遇到的问题,特地整理一下,以防自己还有其他的小伙伴们继续踩坑. 过程:         1.不知道大家有没有注意到 ...

  2. Vue.js provide / inject 踩坑

    最近学习JavaScript,并且使用vuejs,第一次使用依赖注入,结果踩坑,差点把屏幕摔了..始终获取不到如组件的属性,provide中的this对象始终是子组件的this对象 慢慢也摸索到了些v ...

  3. vue项目开发中踩过的坑

    一.路由 这两天移动端的同事在研究vue,跟我说看着我的项目做的,子路由访问的时候是空白的,我第一反应是,不会模块没加载进来吧,还是....此处省略一千字... 废话不多说上代码 路由代码 { pat ...

  4. vue.js使用typescript踩坑记

    最近在把https://github.com/renrenio/renren-fast-vue这个项目转为typescript,在此记录一下遇到的小坑 name坑:属性该怎么给? 声明文件坑:如何解决 ...

  5. Vue路由history模式踩坑记录:nginx配置解决404问题

    问题背景: vue-router 默认是hash模式,使用url的hash来模拟一个完整的url,当url改变的时候,页面不会重新加载.但是如果我们不想hash这种以#号结尾的路径时候的话,我们可以使 ...

  6. vue项目引入TinyMCE

    1.安装 npm install @tinymce/tinymce-vue@3.0.1 -S 2.配置 <template> <!-- 富文本 --> <div> ...

  7. Vue过渡mode属性踩坑

    近期学习Vue的过渡效果的时候,mode属性的"in-out"."out-in"设置了不起作用,官网上的例子让我看了有点迷,问题解决后以此文记录之. 首先我们看 ...

  8. vue 项目实战之小坑坑

    1. Vue 多个元素动画 ,需要使用 transition-group 标签,并且需要赋值 唯一 key 值. 2. 用ajax 获取到数据赋值给data 后 ,再手动向data里添加的属性无效. ...

  9. 使用hbuilder打包vue项目容易出现的坑点

    1.打包后手机打开"该app专为旧版本安卓"问题解决(在hbuilder中设置) 打开manifest.json 然后 2.打包后app打开显示白屏. 路径问题:在webpack中 ...

随机推荐

  1. ubuntu only enable left click

    xmodmap -e "pointer = 1 0 0 0 0 0 0 0 0 0"

  2. Matlab 中movie函数的使用

    MATLAB中,创建电影动画的过程分为以下四步: step1:调用moviein函数对内存进行初始化(该步骤在Matlab5.3以上均可省略),创建一个足够大的矩阵,使之能够容纳基于当前坐标轴大小的一 ...

  3. Ubuntu更新源问题终于解决了

    原文地址:http://chenrongya.blog.163.com/blog/static/8747419620143185103297/ 不同的网络状况连接以下源的速度不同, 建议在添加前手动验 ...

  4. 将服务器文件上传到ftp shell操作

    date cd /home/data today_now=`date +%Y%m%d` #当前日期 cur_date=${today_now::} #echo ${cur_date} #判断是否文件生 ...

  5. python 学习地址

    本章主要记录学习python过程中借鉴的一些网站,并感谢这些博主辛勤付出. python官网:https://www.python.org/ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ...

  6. Token防止表单重复提交和CSRF攻击

    Token,可以翻译成标记!最大的特点就是随机性,不可预测,一般黑客或软件无法猜测出来. Token一般用在两个地方: 1: 防止表单重复提交 2: anti csrf攻击(Cross-site re ...

  7. 神经网络3_M-P模型

    sklearn实战-乳腺癌细胞数据挖掘(博客主亲自录制视频教程,QQ:231469242) https://study.163.com/course/introduction.htm?courseId ...

  8. usb驱动程序小结(六)

    title: usb驱动程序小结 tags: linux date: 2018/12/20/ 17:59:51 toc: true --- usb驱动程序小结 linux中为usb驱动也提供了一套总线 ...

  9. Java NIO系列教程(一) Java NIO 概述

    <I/O模型之四:Java 浅析I/O模型> 一.阻塞IO与非阻塞IO 阻塞IO: 通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据.同样,写入调用将会阻塞直至数 ...

  10. CopyOnWriteArrayList真的完全线程安全吗

    我之前书上看到的说法是:Vector是相对线程安全,CopyOnWriteArrayList是绝对线程安全 这种说法其实有些问题,CopyOnWriteArrayList在某些场景下还是会报错的 Co ...