本文永久连接:github.com/HaoChuan942…javascript
封面是UEditor
的 百度指数 折线图。虽然今天已是 2018 年,且优秀的富文本编辑器层出不穷(包括移动端),但从图中能够看出UEditor
仍然维持着较高的搜索热度。而很多公司和我的也仍然在项目中使用UEditor
。目前,UEditor
官网的最后一次版本更新是 1.4.3.3,这已是 2016 年的事情了,而今天的前端开发,不少小伙伴都在使用Vue
,React
这种组件化的前端框架。这就致使在这些“现代”框架中集成UEditor
变得很不平滑。因此才会有下图这些大量介绍如何在Vue
项目中集成UEditor
的博客:php
为了提升代码的可复用性,也为了尽量的不在业务代码中参杂UEditor
的相关操做,我在几个月前,公司项目的开发中撸了一个组件,能够经过v-model
双向绑定的方式来使用UEditor
,简单到就像使用input
框同样。当我撸完,感受很是的Vue
范儿。并且看了很多博客和GitHub
项目,都没有相似的实现。因而我决定发布到 npm 上,帮助一众还在思考如何把UEditor
集成到Vue
项目中的小伙伴。几个月下来,基本已经稳定,因此,今天经过这篇博客,分享给你们。css
先看效果图:html
npm i vue-ueditor-wrap
# 或者
yarn add vue-ueditor-wrap
复制代码
下载 UEditorjava
下载最新编译的 UEditor。官网目前最新的版本是
1.4.3.3
,存在诸多 BUG,例如 Issue1,且官方再也不积极维护。为了世界的和平,针对一些常见 BUG,我进行了修复,并把编译好的文件放在了本仓库的assets/downloads
目录下,你能够放心下载,固然你也能够本身clone
官方源码并编译。git
将下载的压缩包解压并重命名为 UEditor
(只须要选择一个你须要的版本,好比 utf8-php
),放入你项目的 static
目录下。github
若是你使用的是 vue-cli 3.x,能够把
UEditor
文件夹放入项目的public
目录下。web
引入VueUeditorWrap
组件
import VueUeditorWrap from 'vue-ueditor-wrap' // ES6 Module
// 或者
const VueUeditorWrap = require('vue-ueditor-wrap') // CommonJS
复制代码
你也能够经过直接引入
CDN
连接的方式来使用,它会暴露一个全局的VueUeditorWrap
变量(具体如何使用你能够阅读个人这篇博客或参考这个仓库)。
<script src="https://cdn.jsdelivr.net/npm/vue-ueditor-wrap@latest/lib/vue-ueditor-wrap.min.js"></script>
复制代码
注册组件
components: {
VueUeditorWrap
}
// 或者在 main.js 里将它注册为全局组件
Vue.component('vue-ueditor-wrap', VueUeditorWrap)
复制代码
v-model
绑定数据
<vue-ueditor-wrap v-model="msg"></vue-ueditor-wrap>
复制代码
data () {
return {
msg: '<h2>Vue + UEditor + v-model双向绑定</h2>'
}
}
复制代码
至此你已经能够在页面中看到一个初始化以后的
UEditor
了,而且它已经成功和数据绑定了!👏👏👏
根据项目需求修改配置,完整配置选项查看 ueditor.config.js 源码或 官方文档
<vue-ueditor-wrap v-model="msg" :config="myConfig"></vue-ueditor-wrap>
复制代码
data () {
return {
msg: '<h2>Vue + UEditor + v-model双向绑定</h2>',
myConfig: {
// 编辑器不自动被内容撑高
autoHeightEnabled: false,
// 初始容器高度
initialFrameHeight: 240,
// 初始容器宽度
initialFrameWidth: '100%',
// 上传文件接口(这个地址是我为了方便各位体验文件上传功能搭建的临时接口,请勿在生产环境使用!!!)
serverUrl: 'http://35.201.165.105:8000/controller.php',
// UEditor 资源文件的存放路径,若是你使用的是 vue-cli 生成的项目,一般不须要设置该选项,vue-ueditor-wrap 会自动处理常见的状况,若是须要特殊配置,参考下方的常见问题2
UEDITOR_HOME_URL: '/static/UEditor/'
}
}
}
复制代码
如何获取 UEditor
实例?
<vue-ueditor-wrap @ready="ready"></vue-ueditor-wrap>
复制代码
methods: {
ready (editorInstance) {
console.log(`编辑器实例${editorInstance.key}: `, editorInstance)
}
}
复制代码
设置是否在组件的 beforeDestroy
钩子里销毁 UEditor
实例
<vue-ueditor-wrap :destroy="true"></vue-ueditor-wrap>
复制代码
选取 v-model
的实现方式。双向绑定的实现依赖对编辑器内容变化的监听,因为监听方式的不一样,会带来监听效果的差别性,你能够自行选择,但建议使用开箱即用的默认值。
<vue-ueditor-wrap mode="listener"></vue-ueditor-wrap>
复制代码
可选值:observer
,listener
默认值:observer
参数说明:
observer
模式借助 MutationObserver API。优势在于监听的准确性,缺点在于它会带来一点额外的性能开销。你能够经过 observerDebounceTime
属性设置触发间隔,还能够经过 observerOptions
属性有选择的设置 MutationObserver 的监听行为。该 API 只兼容到 IE11+,但 vue-ueditor-wrap
会在不支持的浏览器中自动启用 listener
模式。
<vue-ueditor-wrap mode="observer" :observerDebounceTime="100" :observerOptions="{ attributes: true, characterData: true, childList: true, subtree: true }" >
</vue-ueditor-wrap>
复制代码
listener
模式借助 UEditor 的 contentChange 事件,优势在于依赖官方提供的事件 API,无需额外的性能消耗,兼容性更好,但缺点在于监听的准确性并不高,存在以下方 [常见问题 5] 中的提到的 BUG。
是否支持 Vue SSR
?
自 2.4.0
版本开始支持服务端渲染!本组件提供对 Nuxt
项目开箱即用的支持。但若是你是本身搭建的 Vue SSR
项目,你可能须要自行区分服务端和客户端环境并结合 forceInit
属性强制初始化编辑器,但大几率你用不到该属性,即便是本身搭建的 SSR 项目,更多问题欢迎提交 ISSUE。
如何进行二次开发(添加自定义按钮、弹窗等)?
本组件提供了 beforeInit
钩子,它会在 UEditor
的 scripts 加载完毕以后、编辑器初始化以前触发,你能够在此时机,经过操做 window.UE 对象,来进行诸如添加自定义按钮、弹窗等的二次开发。beforeInit
的触发函数以 编辑器 id 和 配置参数 做为入参。下面提供了一个简单的自定义按钮和自定义弹窗的示例,DEMO 仓库中也提供了自定义“表格居中”按钮的示例,若是有更多二次开发的需求,你能够参考官方 API 或者 UEditor 源码 中的示例。
<vue-ueditor-wrap v-model="msg" @beforeInit="addCustomButtom"></vue-ueditor-wrap>
复制代码
addCustomButtom (editorId) {
window.UE.registerUI('test-button', function (editor, uiName) {
// 注册按钮执行时的 command 命令,使用命令默认就会带有回退操做
editor.registerCommand(uiName, {
execCommand: function () {
editor.execCommand('inserthtml', `<span>这是一段由自定义按钮添加的文字</span>`)
}
})
// 建立一个 button
var btn = new window.UE.ui.Button({
// 按钮的名字
name: uiName,
// 提示
title: '鼠标悬停时的提示文字',
// 须要添加的额外样式,可指定 icon 图标,图标路径参考常见问题 2
cssRules: "background-image: url('/test-button.png') !important;background-size: cover;",
// 点击时执行的命令
onclick: function () {
// 这里能够不用执行命令,作你本身的操做也可
editor.execCommand(uiName)
}
})
// 当点到编辑内容上时,按钮要作的状态反射
editor.addListener('selectionchange', function () {
var state = editor.queryCommandState(uiName)
if (state === -1) {
btn.setDisabled(true)
btn.setChecked(false)
} else {
btn.setDisabled(false)
btn.setChecked(state)
}
})
// 由于你是添加 button,因此须要返回这个 button
return btn
}, 0 /* 指定添加到工具栏上的哪一个位置,默认时追加到最后 */, editorId /* 指定这个 UI 是哪一个编辑器实例上的,默认是页面上全部的编辑器都会添加这个按钮 */)
}
复制代码
<vue-ueditor-wrap v-model="msg" @beforeInit="addCustomDialog"></vue-ueditor-wrap>
复制代码
addCustomDialog (editorId) {
window.UE.registerUI('test-dialog', function (editor, uiName) {
// 建立 dialog
var dialog = new window.UE.ui.Dialog({
// 指定弹出层中页面的路径,这里只能支持页面,路径参考常见问题 2
iframeUrl: '/customizeDialogPage.html',
// 须要指定当前的编辑器实例
editor: editor,
// 指定 dialog 的名字
name: uiName,
// dialog 的标题
title: '这是一个自定义的 Dialog 浮层',
// 指定 dialog 的外围样式
cssRules: 'width:600px;height:300px;',
// 若是给出了 buttons 就表明 dialog 有肯定和取消
buttons: [
{
className: 'edui-okbutton',
label: '肯定',
onclick: function () {
dialog.close(true)
}
},
{
className: 'edui-cancelbutton',
label: '取消',
onclick: function () {
dialog.close(false)
}
}
]
})
// 参考上面的自定义按钮
var btn = new window.UE.ui.Button({
name: 'dialog-button',
title: '鼠标悬停时的提示文字',
cssRules: `background-image: url('/test-dialog.png') !important;background-size: cover;`,
onclick: function () {
// 渲染dialog
dialog.render()
dialog.open()
}
})
return btn
}, 0 /* 指定添加到工具栏上的那个位置,默认时追加到最后 */, editorId /* 指定这个UI是哪一个编辑器实例上的,默认是页面上全部的编辑器都会添加这个按钮 */)
}
复制代码
弹出层中的 HTML 页面 customizeDialogPage.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<!--页面中必定要引入internal.js为了能直接使用当前打开dialog的实例变量-->
<!--internal.js默认是放到 UEditor/dialogs 目录下的-->
<script type="text/javascript" src="./UEditor/dialogs/internal.js"></script>
</head>
<body>
<h1>hello vue-ueditor-wrap</h1>
<script> //能够直接使用如下全局变量 //当前打开dialog的实例变量 console.log('editor: ' + editor); //一些经常使用工具 console.log('domUtils: ' + domUtils); console.log('utils: ' + utils); console.log('browser: ' + browser); dialog.onok = function() { editor.execCommand('inserthtml', '<span>我点击了肯定</span>'); }; dialog.oncancel = function() { editor.execCommand('inserthtml', '<span>我点击了取消</span>'); }; </script>
</body>
</html>
复制代码
v-model
双向数据绑定!你不须要考虑实例化,也不须要考虑什么时候 getContent
,什么时候setContent
,简单到像使用 input
框同样!
彻底听从官方 API
,全部的配置参数和实例方法与官方彻底一致。经过给 vue-ueditor-wrap
组件的 config
属性传递一个对象,你就能够获得一个彻底独立配置的 UEditor
编辑器。经过监听 ready
事件你就能够获得初始化后的 UEditor
实例并执行实例上的各类方法。
自动添加依赖文件。你不须要本身在 index.html
或 main.js
里引入 UEditor
的 JS 文件。更重要的是即便你在一个页面里同时使用多个 vue-ueditor-wrap
组件,它所依赖的 JS 文件也只会加载一次。这么作的缘由在于你不须要当用户一打开项目就先加载大量 UEditor
相关的资源,全部的资源文件只会在 vue-ueditor-wrap
组件第一次被激活时才加载。固然,若是你在 index.html
或 main.js
里引入了相关资源,vue-ueditor-wrap
也会准确判断,你不用担忧它会重复加载。
每一个 vue-ueditor-wrap
组件是彻底独立的。你甚至能够在上面使用 v-for
指令一次渲染 99个 兔斯基(不要忘记添加 key
值)。
是否支持 IE
等低版本浏览器?
与 Vue
相同,总体支持到 IE9+
👏👏👏
为何我会看到这个报错?
这是 UEDITOR_HOME_URL
参数配置错误致使的。在 vue cli 2.x 生成的项目中使用本组件,默认值是 '/static/UEditor/'
,在 vue cli 3.x 生成的项目中,默认值是 process.env.BASE_URL + 'UEditor/'
。但这并不能知足全部状况。例如你的项目不是部署在网站根目录下,如"http://www.example.com/my-app/"
,你可能须要设置为"/my-app/static/UEditor/"
。是否使用了相对路径、路由是否使用 history
模式、服务器配置是否正确等等都有可能会产生影响。总而言之:不管本地开发和部署到服务器,你所指定的 UEditor
资源文件是须要真实存在的,vue-ueditor-wrap
也会在 JS 加载失败时经过 console 输出它试图去加载的资源文件的完整路径,你能够借此分析如何填写。当须要区分环境时,你能够经过判断 process.env.NODE_ENV
来分别设置。
我该如何上传图片和文件?为何我会看到后台配置项返回格式出错
?
上传图片、文件等功能是须要与后台配合的,而你没有给 config
属性传递正确的 serverUrl
,我提供了http://35.201.165.105:8000/controller.php
的临时接口,你能够用于测试,但切忌在生产环境使用!!! 关于如何搭建上传接口,能够参考官方文档。
单图片跨域上传失败!
UEditor
的单图上传是经过 Form 表单 + iframe 的方式实现的,但因为同源策略的限制,父页面没法访问跨域 iframe 的文档内容,因此会出现单图片跨域上传失败的问题。我经过 XHR 重构了单图上传的方式,下载最新编译的 UEditor 资源文件便可在 IE10+
的浏览器中实现单图跨域上传了。具体细节,点此查看。固然你也能够经过配置 toolbars
参数来隐藏单图片上传按钮,并结合上面介绍的“自定义按钮”,曲线救国,如下代码仅供参考。
var input = document.createElement('input')
input.type = "file"
input.style.display = 'none'
document.body.appendChild(input)
input.click()
input.addEventListener('change',(e)=>{
// 利用 AJAX 上传,上传成功以后销毁 DOM
console.log(e.target.files)
})
复制代码
为何我输入的"? ! $ #"
这些特殊字符,没有成功绑定?
当你使用 listener
模式时,因为 v-model
的实现是基于对 UEditor
实例上 contentChange
事件的监听,而你输入这些特殊字符时一般是按住 shift
键的,UEditor
自己的 contentChange
在 shift
键按住时不会触发,你也能够尝试同时按下多个键,你会发现 contentChange
只触发一次。你可使用 observer
模式或移步 UEditor。
单图片上传后 v-model
绑定的是 loading
小图标。
这个也是 UEditor
的 BUG
。我最新编辑的版本,修复了官方的这个 BUG
,若是你使用的是官网下载的资源文件,请替换资源文件或参考 Issue1。
更多问题,欢迎提交 ISSUE 或者去 聊天室 提问。但因为这是一个我的维护的项目,我平时也有本身的工做,因此并不能保证及时解决大家的全部问题,若是小伙伴们有好的建议或更炫酷的操做,也欢迎
PR
,若是你以为这个组件给你的开发带来了实实在在的方便,也很是感谢你的Star
,固然还有咖啡:
代码修改请遵循指定的
ESLint
规则,PR
以前请先执行npm run lint
进行代码风格检测,大部分语法细节能够经过npm run fix
修正,构建以后,记得修改package.json
里的版本号,方便我Review
经过后麻溜溜的发布到npm
。
虽然这是一次很小的创新,UEditor
也多是一个过气的富文本编辑器。可是在维护这个项目以及帮助一众小伙伴解决ISSUE
的过程当中,我成长了不少。最令我感动的是很多小伙伴还给我邮箱发了感谢信,并且我还发现确实已经有一些人开始在项目中用了。这种被他人承认,以及帮助别人的快乐真的只有体会过的人才知道。也就在前不久,我决定开始在掘金写博客,虽然一些东西写的不那么好,或者本身认知有错误,但总有一群热心且优秀的小伙伴,会在评论区指正以及给出宝贵的意见。分享是快乐的!因此,个人这篇文章也权当抛砖引玉,若是小伙伴们有好的建议或更炫酷的操做,也欢迎PR
,不过PR
以前请先执行npm run lint
进行代码风格检测,大部分语法细节也能够经过npm run fix
修正,也要记得修改package.json
的版本号version
,方便我直接发布到npm
。固然若是你有好用的富文本编辑器,也能够在评论区推荐。