Vue 3 项目参考这里
前言
封面是UEditor
的 百度指数 折线图。虽然今天已经是 2018 年,且优秀的富文本编辑器层出不穷(包括移动端),但从图中可以看出UEditor
仍然维持着较高的搜索热度。而不少公司和个人也仍然在项目中使用UEditor
。目前,UEditor
官网的最后一次版本更新是 1.4.3.3,这已经是 2016 年的事情了,而今天的前端开发,很多小伙伴都在使用Vue
,React
这种组件化的前端框架。这就导致在这些“现代”框架中集成UEditor
变得很不平滑。所以才会有下图这些大量介绍如何在Vue
项目中集成UEditor
的博客:
为了提高代码的可复用性,也为了尽可能的不在业务代码中参杂UEditor
的相关操作,我在几个月前,公司项目的开发中撸了一个组件,可以通过v-model
双向绑定的方式来使用UEditor
,简单到就像使用input
框一样。当我撸完,感觉非常的Vue
范儿。而且看了不少博客和GitHub
项目,都没有类似的实现。于是我决定发布到 npm 上,帮助一众还在思考如何把UEditor
集成到Vue
项目中的小伙伴。几个月下来,基本已经稳定,所以,今天通过这篇博客,分享给大家。
先看效果图:
Installation
npm i vue-ueditor-wrap
# 或者
yarn add vue-ueditor-wrap
安装太慢?试试 URM
Quick Start
-
下载 UEditor下载最新编译的 UEditor。官网目前最新的版本是
1.4.3.3
,存在一些 BUG,且官方不再积极维护。针对一些常见 BUG,我进行了修复,你可以放心下载,当然你也可以自己clone
官方源码并编译。将下载的压缩包解压并重命名为
UEditor
(只需要选择一个你需要的版本,比如utf8-php
),放入你项目的static
目录下。如果你使用的是 vue-cli 3.x,可以把
UEditor
文件夹放入项目的public
目录下。 -
引入
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><img src="http://img.baidu.com/hi/jx2/j_0003.gif"/>Vue + UEditor + v-model双向绑定</h2>' } }
至此你已经可以在页面中看到一个初始化之后的
UEditor
了,并且它已经成功和数据绑定了!??? -
根据项目需求修改配置,完整配置选项查看 ueditor.config.js 源码或 官方文档
<vue-ueditor-wrap v-model="msg" :config="myConfig"></vue-ueditor-wrap>
data () { return { msg: '<h2><img src="http://img.baidu.com/hi/jx2/j_0003.gif"/>Vue + UEditor + v-model双向绑定</h2>', myConfig: { // 编辑器不自动被内容撑高 autoHeightEnabled: false, // 初始容器高度 initialFrameHeight: 240, // 初始容器宽度 initialFrameWidth: '100%', // 上传文件接口(这个地址是我为了方便各位体验文件上传功能搭建的临时接口,请勿在生产环境使用!!!) serverUrl: '//ueditor.szcloudplus.com/cos', // UEditor 资源文件的存放路径,如果你使用的是 vue-cli 生成的项目,通常不需要设置该选项,vue-ueditor-wrap 会自动处理常见的情况,如果需要特殊配置,参考下方的常见问题2 UEDITOR_HOME_URL: '/static/UEditor/' } } }
Advanced
-
如何获取
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 源码 中的示例。自定义按钮 Demo
<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 是哪个编辑器实例上的,默认是页面上所有的编辑器都会添加这个按钮 */) }
自定义弹窗 Demo
<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>
Features
-
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
值)。
FAQ(常见问题)
-
是否支持
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
,我提供了//ueditor.szcloudplus.com/cos
的临时接口,你可以用于测试,但切忌在生产环境使用!!! 关于如何搭建上传接口,可以参考官方文档。 -
单图片跨域上传失败!
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
。
总结
虽然这是一次很小的创新,UEditor
也可能是一个过气的富文本编辑器。但是在维护这个项目以及帮助一众小伙伴解决ISSUE
的过程中,我成长了很多。最令我感动的是不少小伙伴还给我邮箱发了感谢信,而且我还发现确实已经有一些人开始在项目中用了。这种被他人认可,以及帮助别人的快乐真的只有体会过的人才知道。也就在前不久,我决定开始在掘金写博客,虽然一些东西写的不那么好,或者自己认知有错误,但总有一群热心且优秀的小伙伴,会在评论区指正以及给出宝贵的意见。分享是快乐的!所以,我的这篇文章也权当抛砖引玉,如果小伙伴们有好的建议或更炫酷的操作,也欢迎PR
,不过PR
之前请先执行npm run lint
进行代码风格检测,大部分语法细节也可以通过npm run fix
修正,也要记得修改package.json
的版本号version
,方便我直接发布到npm
。当然如果你有好用的富文本编辑器,也可以在评论区推荐。
今天的文章Vue项目中最简单的使用集成UEditor方式,含图片上传分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/27032.html