前言
在一些后台管理系统尤其像博客、社区、新闻和广告等内容管理平台中,大都会有集成富文本编辑器的需求。从0到1开发一个富文本编辑器还是很复杂也很耗时的,不是大咖级的程序员还不一定搞得出来。好在已经有了很多开源的富文本编辑器可以让我们直接集成到项目中以组件的方式使用。目前主流的开源富文本编辑器主要有:CKEditor
、TinyMCE
、Quill
、wangEditor
、UEditor
和 Kindeditor
等。
笔者发现 CKEditor
是功能最丰富也是文档最齐全的一款富文本编辑器,而且最新版的CKEditor5
还新增了AI助理插件的功能,可以只能生成用户需要的内容。但是一些复杂的功能需要自己添加一些插件才能实现,文档也是全英文版的,有一定的使用门槛。只是无奈自己已经投入了不少时间学习了如何使用CKEditor5
并把demo跑了起来,如果要改用其富文本编辑器还得去看对应的官方文档,时间成本会更高。
于是笔者就打算把CKEditor5
的常用功能用法一次研究透,今天就出一篇关于如何扩展CKEditor插件并集成到Vue项目中的文章,希望对有这方面需求的读者朋友们能有帮助。
CKEditor
简介
CKEditor
是一个开源的富文本编辑器,它允许用户在网页上进行所见即所得的编辑,它具有以下特点:
- 功能丰富:
CKEditor
提供了多种功能,包括格式化文本、插入图片和视频、创建表格、添加超链接等。它还支持自定义样式和工具栏按钮,以满足不同需求。 - 可扩展性:
CKEditor
可以通过插件进行扩展,用户可以根据自己的需求添加或删除插件。这样可以定制编辑器的功能,使其更符合特定的应用场景。 - 跨平台支持:
CKEditor
可以在各种操作系统和浏览器上运行,包括Windows、Mac、Linux以及常见的主流浏览器如Chrome、Firefox、Safari等。 - 易于集成:
CKEditor
提供了简单易用的API,可以方便地将编辑器集成到现有的项目中。它还提供了丰富的事件和回调函数,可以实现与其他组件的交互。 - 多语言支持:
CKEditor
支持多种语言,用户可以选择编辑器界面和提示信息的语言。这使得CKEditor成为国际化项目的理想选择。
总的来说,CKEditor
是一款强大而灵活的富文本编辑器,适用于各种Web应用开发场景。它提供了丰富的功能和可扩展性,并具有跨平台、易于集成和多语言支持等特点。
CKEditor
目前已更新到5版本来了,官方文档地址:https://ckeditor.com/docs/ckeditor5/latest/index.html
CKEditor5
提供了 ClassicEditor
、InlineEditor
和 BalloonEditor
等几个 常用的富文本编辑器。其中以 经典编辑器 ClassicEditor
用得最多。官方提供的ClassicEditor
富文本编辑器长下面这样:
但是我们发现 一些重要的功能如段落对齐方式、字体大小、字体颜色和插入源码等编辑功能,官方提供的这款ClassicEditor
富文本编辑器并没有,那怎么办呢?
答案是需要我们开发者在editor5-build-classic
源码的基础上通过添加我们需要的插件并增加相应的配置后自定义构建出我们需要的 ClassicEditor
组件才能实现我们预期的富文本编辑器功能。至于如何构建自定义的ClassicEditor
编辑器我们后面会讲到。
CkEditor 集成 Vue3
安装依赖包
# npm 安装
npm install --save @ckeditor/ckeditor5-vue @ckeditor/ckeditor5-build-classic
# yarn 安装
yarn add @ckeditor/ckeditor5-vue @ckeditor/ckeditor5-build-classic
主要包括@ckeditor/ckeditor5-vue
和 @ckeditor/ckeditor5-build-classic
两个依赖包
然后通过ES6模块导入的方式在Vue
项目中安装并启用CKEditor
组件
ES6 模块导入
main.js
文件中创建App
应用,安装CKEditor
插件并将应用挂载到Dom
节点下
import { createApp } from 'vue';
import CKEditor from '@ckeditor/ckeditor5-vue';
createApp( { /* options */ } ).use( CKEditor ).mount( /* DOM element */ );
页面组件中使用 CKEditor
组件
<template>
<div id="app">
<ckeditor :editor="editor" v-model="editorData" :config="editorConfig"></ckeditor>
</div>
</template>
<script>
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
export default {
name: 'app',
data() {
return {
editor: ClassicEditor,
editorData: '<p>Content of the editor.</p>',
editorConfig: {
// The configuration of the editor.
}
};
}
}
</script>
上面这种方式在实践过程中我们发现,使用ClassicEditor
编辑器后在editConfig
变量中配置的自定义安装的扩展插件很配置项都无法生效。查了CKEditor5
官方文档发现必须使用在线自定义构建或源码构建的方式才能让自定义配置的扩展插件功能生效。CKEditor5
在线构自定义建链接如下,感兴趣的同学可以自己去尝试。
https://ckeditor.com/ckeditor-5/online-builder/
实践过程中在线自定义构建的方式构建的富文本编辑器,笔者在本地运行时发现有一些功能都不生效,比如heading
标题栏和字体颜色和字体背景色都不生效,于是转而采用源码构建的方式。
源码本地构建
克隆源码并安装依赖包
这种方式需要先把一种编辑器CKEditor
的源码克隆到本地磁盘,然后在源码的基础上通过添加自己想要的扩展插件并添加配置项, 最后打包发布到npm
仓库就可以在自己的Vue
项目中使用定制化的CKEditor
了。
在本地电脑D盘创建一个github
文件夹,进入此文件夹后鼠标右键->Open Git Bash Here
打开一个命令控制台,然后执行如下命令把ClassicEditor
编辑器的源码克隆下来。
git clone -b stable https://github.com/ckeditor/ckeditor5-build-classic.git #从github克隆ckeditor5-build-classic源码并切换到stable分支
cd ckeditor5-build-classic #切换到 ckeditor5-build-classic 目录
然后执行如下命令安装ckeditor5-build-classic
项目中的依赖包
# yarn 安装依赖包
yarn install
# npm 安装
npm install
然后在添加一些我们需要的插件安装包,如Alignment
、Highlight
、Font
和 Code-Block
等
# yarn 安装
yarn add -D @ckeditor/[email protected] @ckeditor/[email protected] @ckeditor/[email protected] @ckeditor/[email protected]
# npm 安装
npm install -save-dev @ckeditor/[email protected] @ckeditor/[email protected] @ckeditor/[email protected] @ckeditor/[email protected]
注意因为ckeditor5-build-classic
项目stable
版本源码package.json
文件中的version字段代表的版本号为19.0.0
版本, 因此我们安装的扩展ckeditor5-xx
插件也必须与ckeditor5-build-classic
的版本号保持一致,否则打开sample/index.html
页面查看富文本编辑器时会报plugincollection-plugin-name-conflict
错误,富文本不可用,原因就是安装的ckeditor5
扩展插件与主版本不一致导致使用富文本编辑器运行时出错。
修改配置文件
主要是修改 src/editor.js
文件中的ClassicEditor.builtinPlugins
和 ClassicEditor.defaultConfig
两个变量,
向第一个变量中添加扩展插件,向第二个变量中添加配置项。
export default class ClassicEditor extends ClassicEditorBase {}
// Plugins to include in the build.
ClassicEditor.builtinPlugins = [
Essentials,
UploadAdapter,
Autoformat,
Bold,
Font,
Italic,
BlockQuote,
CKFinder,
EasyImage,
Heading,
Image,
ImageCaption,
ImageStyle,
ImageToolbar,
ImageUpload,
Indent,
Link,
List,
MediaEmbed,
Paragraph,
PasteFromOffice,
Table,
TableToolbar,
TextTransformation,
Alignment,
CodeBlock,
Highlight
// SimpleUploadAdapter
];
// Editor configuration.
ClassicEditor.defaultConfig = {
alignment: {
options: [ 'left', 'right', 'center' ]
},
toolbar: {
items: [
'heading',
'|',
'alignment',
'bold',
'italic',
'highlight',
'fontSize',
'fontFamily',
'fontColor',
'fontBackgroundColor',
'link',
'bulletedList',
'numberedList',
'|',
'indent',
'outdent',
'|',
'imageUpload',
'blockQuote',
'codeBlock',
'insertTable',
'mediaEmbed',
'undo',
'redo'
]
},
heading: {
options: [
{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
{ model: 'heading1', view: 'h2', title: 'Heading 1', class: 'ck-heading_heading1' },
{ model: 'heading2', view: 'h3', title: 'Heading 2', class: 'ck-heading_heading2' },
{ model: 'heading3', view: 'h4', title: 'Heading 3', class: 'ck-heading_heading3' }
]
},
fontFamily: {
options: [
'default',
'Arial, Helvetica, sans-serif',
'Courier New, Courier, monospace',
'Georgia, serif',
'Lucida Sans Unicode, Lucida Grande, sans-serif',
'Tahoma, Geneva, sans-serif',
'Times New Roman, Times, serif',
'Trebuchet MS, Helvetica, sans-serif',
'Verdana, Geneva, sans-serif'
],
supportAllValues: true
},
fontSize: {
options: [ 10, 12, 14, 16, 18, 20, 24, 28, 32, 36 ],
supportAllValues: true
},
fontColor: {
colorPicker: { format: 'hex' },
colors: [
{
color: '#FF0000',
label: 'Red'
},
{
color: '#FFFF00',
label: 'Yellow'
},
{
color: '#0000FF',
label: 'Blue'
},
{
color: '#008000',
label: 'Green'
},
// 省略其他颜色配置
],
columns: 10,
documentColors: 24
},
fontBackgroundColor: {
colorPicker: 'hex',
colors: [
{
color: '#FF0000',
label: 'Red'
},
{
color: '#FFFF00',
label: 'Yellow'
},
{
color: '#0000FF',
label: 'Blue'
},
{
color: '#008000',
label: 'Green'
},
// 其他颜色配置省略
],
columns: 10,
documentColors: 24
},
image: {
toolbar: [
'imageStyle:full',
'imageStyle:side',
'|',
'imageTextAlternative'
],
upload: {
types: [ 'png', 'jpg', 'jpeg' ]
}
},
table: {
contentToolbar: [
'tableColumn',
'tableRow',
'mergeTableCells'
]
},
// This value must be kept in sync with the language defined in webpack.config.js.
language: 'en',
codeBlock: {
languages: [
{ language: 'plaintext', label: 'Plain text', class: 'plain-text' }, // The default language.
{ language: 'c', label: 'C', class: 'c' },
{ language: 'cs', label: 'C#' },
{ language: 'cpp', label: 'C++' },
{ language: 'css', label: 'CSS' },
{ language: 'diff', label: 'Diff' },
{ language: 'html', label: 'HTML' },
{ language: 'java', label: 'Java', class: 'java' },
{ language: 'javascript', label: 'JavaScript', class: 'js javascript' },
{ language: 'php', label: 'PHP' },
{ language: 'python', label: 'Python' },
{ language: 'ruby', label: 'Ruby' },
{ language: 'typescript', label: 'TypeScript' },
{ language: 'xml', label: 'XML' }
]
},
highlight: {
options: [
{
model: 'yellowMarker',
class: 'marker-yellow',
title: 'Yellow marker',
color: 'var(--ck-highlight-marker-yellow)',
type: 'marker'
},
{
model: 'greenMarker',
class: 'marker-green',
title: 'Green marker',
color: 'var(--ck-highlight-marker-green)',
type: 'marker'
},
// 省略其他高亮颜色配置
]
}
// simpleUpload: {
// uploadUrl: '',
// withCredentials: true,
// headers: {
// 'X-CSRF-TOKEN': 'CSRF-Token',
// 'Authorization': ''
// }
// }
};
在ClassicEditor.defaultConfig
配置项里笔者之所以不用simpleUpload
配置,是因为这样做就必须把uploadUrl
给写死了,而我们自定义扩展构建的ClassicEditor
最终是作为一个组件去使用,用户如果用了我们这个编辑器,那就必须按照这里写死的uploadUrl
定义自己的图片上传接口请求url
,而且如果图片上传接口要带上Authorization
请求头认证信息的话,我们在这里却是拿不到用户登录成功后的认证信息的。
所以我们还是自定义一个ImageUploadAdapter
,在构造函数中传入uploadUrl
和Authorization
请求头认证token ,并在CKEditor
组件的ready
事件回调方法中注册上ImageUploadAdapter
适配器。
修改好 src/editor.js
文件中的源码后就可以回到ckeditor5-build-classic
项目的根目录下执行构建源码命令并查看效果了。
yarn run build
构建成功后命令控制台中会出现如下所示的信息:
Entrypoint main [big] = ckeditor.js ckeditor.js.map
[9] (webpack)/buildin/harmony-module.js 573 bytes {0} [built]
[10] (webpack)/buildin/global.js 472 bytes {0} [built]
[106] ./src/ckeditor.js + 636 modules 3.23 MiB {0} [built]
| ./src/ckeditor.js 23.7 KiB [built]
| + 636 hidden modules
+ 564 hidden modules
Done in 5.99s.
然后进入sample
目录使用Google浏览器打开index.html
文件即可看到我们定制的富文本编辑器效果
可以看到在我们ClassicEditor
的基础上定制的Editor
具备了对齐方式、高亮、字体家族、字体颜色和字体背景以及代码块等ClassicEditor
编辑器不具备的扩展功能。
新建FullClassicEditor
项目并发布到 npm 仓库
新建FullClassicEditor
项目的目的是为了把我们以ckeditor5-build-classic
项目源码为基础定制的编辑器能以一个独立包的形式发布到npm
仓库中去。
同样在D盘github
目录下新建一个FullClassicEditor
文件夹,然后将ckeditor5-build-classic
项目build
目录下的文件全部拷贝到FullClassicEditor
目录下。鼠标右键->Open Git Bash Here
,打开命令控制台后执行npm init
命令
npm init
执行该命令后控制台会分步骤提示我们输入项目信息,主要项内容填写如下:
- name: full-classic-editor
- version: 1.0.0
- description: A full function classic editor which extends @ckeditor/build-classic
- main: ckeditor.js
- repository.url: https://github.com/heshengfu26/FullClassicEditor.git
- keywords: [“full”, “function”, “rich-text”, “Editor”]
- author: heshengfu26
- license: Apache-2.0
- bugs.url: https://github.com/heshengfu26/FullClassicEditor/issues
- homeage: https://github.com/heshengfu26/FullClassicEditor#readme
初始化完成之后package.json
文件内容如下:
{
"name": "full-classic-editor",
"version": "1.0.0",
"description": "A full function classic editor which extends @ckeditor/build-classic",
"main": "ckeditor.js",
"scripts": {
"test": "console.log('this is a custom full function rich text editor which extends @ckeditor/build-classic-editor');"
},
"repository": {
"type": "git",
"url": "git+https://github.com/heshengfu26/FullClassicEditor.git"
},
"keywords": [
"full",
"function",
"rich-text",
"Editor"
],
"author": "heshengfu26",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/heshengfu26/FullClassicEditor/issues"
},
"files": [
"dist",
"src"
],
"homepage": "https://github.com/heshengfu26/FullClassicEditor#readme"
}
初始化npm项目的时候需要登录自己的github
个人账号创建一个名称为FullClassicEditor
的代码仓库,对应package.json
文件中的
repository.url
的值。
为了将项目以 npm
包的形式发布到 npm
仓库,需要我们先注册一个npm
账号,大家进入npmjs
官网自行注册,官网链接:
https://www.npmjs.com/
然后我们就可以在命令控制台中依次执行下面两个命令后将我们定制的full-classic-editor
项目包发布到 npm
仓库
npm login
# 输入万npm login 后浏览器会自动打开npmjs官网进入登录页面,用户输入自己注册npm账号时填写的邮箱账号收到的随机数字密码后就能登录成功
npm publish # 登录成功后就可以将包发布到 npm 仓库
发布成功后,可以在 npm
个人中心看到。后面每次更新构建后需要更新package.json
文件中的verrsion
字段才能发布成功,且版本号必须比之前发布的版本号数字要大。
上图是笔者发了1.2.0版本之后在npmjs
官网个人中心的结果。
自定义图片上传适配器
我们参照@ckeditor/ckeditor5-upload/adapters/simpleuploadadapter.js
文件中的源码定制自己的图片上传适配器ImageUploadAdapter
首先安装@ckeditor/ckeditor5-upload
依赖包
yarn add -D @ckeditor/[email protected]
然后在FullClassicEditor
项目的根目录下新建src
文件夹, 并在该文件夹下新建 interfaces
和 plugin
两个文件夹,分别在这两个文件夹下新建UploadOptions.ts
和 ImageUploadAdapter.ts
两个文件,两文件中的源码如下:
UploadOptions.ts
export interface UploadOptions {
uploadUrl: string,
withCredentials: boolean,
headers: object
}
ImageUploadAdapter.ts
import FileLoader from '@ckeditor/ckeditor5-upload/src/filerepository'
import {UploadOptions} from '../interfaces/UploadOptions'
export class ImageUploadAdapter {
loader: FileLoader
options: UploadOptions
xhr: XMLHttpRequest
/**
* ImageUploadAdapter 构造函数
* @param loader 文件加载器 FileLoader类型
* @param options 文件上传选项参数 UploadOptions 类型
*/
constructor(loader: FileLoader, options: UploadOptions){
this.loader = loader
this.options = options
}
/**
* 开始上传文件
* @returns {Promise}
*/
upload() {
return this.loader['file']
.then( file => new Promise( ( resolve, reject ) => {
this.initRequest();
this.initListeners( resolve, reject, file );
this.sendRequest( file );
} ) );
}
/**
* 初始化 XMLHttpRequest 类型文件上传请求
* @private
*/
initRequest(){
const xhr = this.xhr = new XMLHttpRequest();
xhr.open( 'POST', this.options.uploadUrl, true );
xhr.responseType = 'json';
}
/**
* 初始化文件上传请求 xhr 的监听器
* @private
* @param {Function} resolve Callback function to be called when the request is successful.
* @param {Function} reject Callback function to be called when the request cannot be completed.
* @param {File} file Native File object.
*/
initListeners(resolve, reject, file){
const xhr = this.xhr;
const loader = this.loader;
const genericErrorText = `Couldn't upload file: ${ file.name }.`;
xhr.addEventListener( 'error', () => reject( genericErrorText ) ); // 监听文件上传出错事件
xhr.addEventListener( 'abort', () => reject() ); // 监听文件上传放弃事件
xhr.addEventListener( 'load', () => { // 监听文件上传加载事件
const response = xhr.response;
if ( !response || response.error ) {
return reject( response && response.error && response.error.message ? response.error.message : genericErrorText );
}
// 解析文件上传成功响应数据,返回数据必须是一个具备 url 或 urls 字段的对象
resolve( response.url ? { default: response.url } : response.urls );
} );
// 支持监听文件上传进度事件
if ( xhr.upload ) {
xhr.upload.addEventListener( 'progress', evt => {
if ( evt.lengthComputable ) {
loader.uploadTotal = evt.total;
loader.uploaded = evt.loaded;
}
} );
}
}
/**
* 放弃上传
* @returns {Promise}
*/
abort() {
if ( this.xhr ) {
this.xhr.abort();
}
}
/**
* 发送文件上传请求
* @private
* @param {File} file File instance to be uploaded.
*/
sendRequest( file ){
// Set headers if specified.
const headers = this.options.headers || {};
for ( const headerName of Object.keys( headers ) ) {
this.xhr.setRequestHeader( headerName, headers[ headerName ] );
}
// Prepare the form data.
const data = new FormData();
data.append( 'upload', file );
// Send the request.
this.xhr.send( data );
}
}
package.json
文件更新version
字段为 1.2.0 后 重新执行npm publish
命令发布full-classic-editor
包到npm
仓库。
Vue3
项目整合CKEditor5 编辑器
项目骨架搭建
参考Vite官网知道文档链接 https://cn.vitejs.dev/guide/ 使用 Vite 搭建自己的 Vue3
项目
yarn create vite exam-vue-admin --template vue
# 切换到exam-vue-admin目录
cd exam-vue-admin
# 安装全局依赖包
yarn add axios element-plus node-sass nprogress pinia vue-router@latest
# 安装开发依赖包
yarn add -D @ckeditor/ckeditor5-vue @rollup/plugin-commonjs @ckeditor/vite-plugin-ckeditor5 @highlightjs/vue-plugin full-classic-editor highlight.js sass
修改vite.config.js
文件源码,修改后的源码如下:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
import commonjs from '@rollup/plugin-commonjs'
// import prismjs from 'vite-plugin-prismjs'
// import ckeditor5 from '@ckeditor/vite-plugin-ckeditor5'
// https://vitejs.dev/config/
export default defineConfig({
server: {
port: 3000,
strictPort: true,//是否是严格的端口号,如果true,端口号被占用的情况下,vite会退出
host: 'localhost',
cors: true, //为开发服务器配置 CORS , 默认启用并允许任何源
open: true //是否自动打开浏览器
},
publicDir: 'public',
base: './',
plugins: [vue(),
commonjs()
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
兼容性注意
Vite 需要 Node.js 版本 18+,20+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。
项目文件结构如下:
exam-vue-admin
├─.env
├─.env.development
├─index.html
├─package.json
├─README.md
├─vite.config.js
│ yarn.lock
├─dist
├─node_modules
├─public
└─src
├─App.vue
├─main.js
├─style.css
├─api
│ ├─user.js
├─assets
│ ├─vue.svg
│ └─css
│ └─main.scss
├─components
│ └─HelloWorld.vue
├─router
│ └─index.js
├─store
│ ├─index.js
│ └─modules
│ └─user.js
├─utils
│ └─request.js
└─views
├─About.vue
├─Home.vue
└─Login.vue
这个 Vue 项目笔者已经上传到 gitee
个人代码仓库, 地址:https://gitee.com/heshengfu1211/exam-vue-admin.git
有需要的读者朋友可以从gitee
上克隆下来, 项目中登录页面和页面路由等功能笔者都已经做好了,大家直接使用即可,通过在项目根目录命令控制台执行下面的命令接口直接把项目在本地跑起来。
# yarn 安装启动
$ yarn install
$ yarn run dev
# npm 安装启动
$ npm install
$ npm run dev
main.js
文件中创建Vue应用实例并安装Element-Plus
、Vue-Router
和 CKEditor
等插件
import { createApp } from 'vue';
import './style.css'
import App from './App.vue'
// 导入路由列表
import router from '@/router/index'
// 代替Vuex的本地响应式缓存
import { createPinia } from 'pinia'
// 导入element-plus UI组件
import ElementPlus from 'element-plus'
// 导入element-plus UI样式
import 'element-plus/dist/index.css'
// 导入CKEditor5的Vue插件
import CKEditor from '@ckeditor/ckeditor5-vue'
// 导入代码高亮样式
import 'highlight.js/styles/atom-one-light.css'
// 调用createApp方法创建Vue应用
const app = createApp(App)
// 安装Pinia组件
app.use(createPinia())
// 安装ElementPlus组件
app.use(ElementPlus)
// 安装路由组件
app.use(router)
// 安装CKEditor组件
app.use(CKEditor)
// 将app挂载到id为app的容器中,也就是index.html中id=app的div容器
app.mount('#app')
Home页面整合富文本编辑器
我们在src/view
目录下的Home.vue
文件中实现集成我们之前定制的富文本编辑器功能。我们在home页面要实现的功能不仅是富文本编辑器,还包括图片上传和代码高亮显示等功能。
Home.vue
文件源码如下
<script setup>
import { ref,reactive } from 'vue'
// 从full-classic-editor包中导入我们定制的ClassicEditor
import Editor from 'full-classic-editor'
import { onMounted } from 'vue'
// 导入代码高亮库
import hljs from "highlight.js"
// 导入我们之前在full-classic-editor项目中自定义的图片上传适配器
import {ImageUploadAdapter} from 'full-classic-editor/src/plugin/ImageUploadAdapter'
const editor = Editor
// 编辑器响应式数据
let editorData = ref('<p>Content of the editor</p>');
// 代码块字符串数组
let codeBlocks = reactive([])
// 编辑器配置变量
let editorConfig = reactive({
language: 'zh-cn'
})
onMounted(()=>{
})
const editorReady = (editor)=>{
window.ckeditor = editor
editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
const uploadOptions = {
// 文件上传路径
uploadUrl: import.meta.env.VITE_BASE_URL+'/upload/ckEditor/simpleUploadAdapter',
withCredentials: true,
headers: {
// 身份认证请求头
Authorization: sessionStorage.getItem('authToken')
}
}
return new ImageUploadAdapter(loader, uploadOptions)
}
}
// 代码高亮显示
const highlightCode = ()=> {
codeBlocks.value = []
var editorMain = document.getElementsByClassName('ck ck-content ck-editor__editable')[0]
let blocks = editorMain.querySelectorAll("pre code")
for(var i=0; i<blocks.length;i++){
var block = blocks[i]
var dataLanguage = block.parentNode.getAttribute('data-language')
var className = block.getAttribute('class') + ' hljs'
if(block.innerText==null || block.innerText=='' || block.innerText=='\n'){
return
}
var highlightCode = hljs.highlightAuto(block.innerText).value;
// 代码高亮后的字符串'双引号'被替换成了'"', 单引号被替换成了''', 所以需要将其还原回来
highlightCode = highlightCode.replaceAll('"', '"').replaceAll(''', '\'')
console.log(highlightCode)
block.innerText = ''
block.innerHTML = highlightCode
codeBlocks.value.push({
dataLanguage: dataLanguage,
className: className,
codeBlock: highlightCode
})
console.log(codeBlocks)
}
}
</script>
<template>
<div id="editor-container">
<Ckeditor :editor="editor" v-model="editorData" :config="editorConfig" @ready="editorReady"></Ckeditor>
<el-button type="primary" @click="highlightCode" style="margin-top: 10px;margin-bottom: 10px;">代码高亮显示</el-button>
<div class="code-area">
<!--遍历语法高亮后的代码数组-->
<pre v-for="(item, index) in codeBlocks.value" :key="index" :data-language="item.dataLanguage">
<code :class="item.className" style="text-align: left;" v-html="item.codeBlock"></code>
</pre>
</div>
</div>
</template>
笔者在后台服务登录接口用户登录成功后返回一个jwtToken
public class FormLoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
CustomUser user = (CustomUser) authentication.getPrincipal();
Map<String, Object> successMap = new HashMap<>();
successMap.put("status", 200);
successMap.put("userInfo", user);
successMap.put("msg", "success");
String jwtToken = JwtTokenUtil.genAuthenticatedToken(user);
successMap.put("authToken", jwtToken);
String successMapString = JSON.toJSONString(successMap);
out.write(successMapString);
out.flush();
out.close();
}
}
同时在用户登录成功后需要在sessionStorage
中存下authToken
:
views/Login.vue
pwdLoginFormRef.value.validate((valid)=>{
if(valid){
const userStore = useUserStore()
pwdLogin(loginForm).then(res=>{
if(res.status==200){
const {userInfo, authToken} = res
userStore.setAuthToken(authToken)
const user = {
username: userInfo.username,
nickname: userInfo.nickname,
phoneNum: userInfo.phoneNum,
currentRole: userInfo.currentRole,
email: userInfo.email,
userface: userInfo.email
}
userStore.setUserInfo(user)
userStore.setLogined(true)
ElMessage({
message: '登录成功!',
type: 'success'
})
router.push('/home')
}else{
ElMessage.error('密码登录失败')
}
})
}else{
ElMessage.error('表单验证失败')
}
})
store/modules/user.js
actions: {
setAuthToken(authToken){
this.authToken = authToken
sessionStorage.setItem('authToken', authToken)
},
setUserInfo(userInfo){
this.userInfo = userInfo
sessionStorage.setItem('userInfo', JSON.stringify(userInfo))
},
setRoles(roles){
this.roles = roles
sessionStorage.setItem('roles', JSON.stringify(roles))
},
setLogined(loginFlag){
this.logined = loginFlag
sessionStorage.setItem('logined', loginFlag)
}
}
后端项目blogserver
源码笔者已上传到了个人gitee代码仓库:blogserver项目源码地址
图片上传服务端笔者使用阿里的对象存储服务实现,图片上传接口如下:
@RestController
@RequestMapping("/upload")
public class UploadFileController {
@Resource
private OssClientService ossClientService;
@PostMapping("/ckEditor/simpleUploadAdapter")
public Map<String, String> uploadCkEditorImage(HttpServletRequest request){
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
MultipartFile file = multipartRequest.getFile("upload");// 获取上传文件对象
assert file != null;
String imageUrl = ossClientService.uploadImageFile(file, "simpleUploadAdapter");
Map<String, String> resultMap = new HashMap<>();
resultMap.put("url", imageUrl);
return resultMap;
}
}
这里需要注意:根据CKEditor5
的官方文档说明,图片上传接口必须返回一个包含url
或urls
字段的json
对象,若是前者对应的值是一个图片链接url;若是后者,其对应的值是一个包含多张图片链接url
的数组。
富文本编辑器效果测试
在启动后端服务前需要在本地安装Mysql
数据库服务和Redis
服务,Mysql
数据库笔者安装的8.0版本,Redis
安装的是7.0版本。然后执行数据库初始化脚本,也就是执行blogserver
项目src/main/resources/sql
目录下的create_database_and_user.sql
和create_tables_and_init.sql
两个文件中的sql
脚本。
启动服务时先运行后台项目BlogServerApplication
类中的main
方法,启动后台服务。后台服务启动成功后使用ApiPost调用注册接口注册一个zhangsan的用户
点击【发送】按钮完成用户zhangsan的注册,注册成功之后我们就可以使用该账号在前端页面进行登录了。
然后在exam-vue-admin
项目根目录右键->Open Git Bash Here
打开命令控制台执行如下命令启动前端服务
$ yarn run dev
前端服务启动成功后会自动打开浏览器进入系统登录页面
输入用户名:zhangsan 和 登录密码 zhangsan1990 后点击登录进入,登录成功后系统会通过路由进入Home.vue
组件对应的页面,也就是我们定制的富文本编辑页面。
然后我们在富文本编辑器中输入内容,并使用Paragraph、Highlight、Font color 以及 Block quote 等菜单按钮格式化输入内容,效果如下图所示:
然后继续点击Insert code block
按钮插入代码,插入前在下拉框中选中 JavaScript
语言,并在插入的代码块中拷贝一段JavaScript代
码进去,插入代码后的效果如下图所示:
接着我们继续插入一段 Java 代码,如下图所示:
然后我们点击下面的【代码高亮显示】蓝色按钮,可以看到下面的代码显示区域出现了代码语法高亮显示出来了
最后我们看一下上传图片效果,点击 Insert image 菜单按钮插入一张本地jpg格式图片,可以看到图片在富文本编辑器中成功显示出来了
最后我们看一下上传图片效果,点击 Insert image 菜单按钮插入一张本地jpg格式图片,可以看到图片在富文本编辑器中成功显示出来了
本文首发个人微信公众号【阿福谈Web编程】,刚兴趣的读者朋友可以加个关注,通过笔者的个人公众号菜单栏中的【联系作者】按钮就可以添加我的微信,大家一起交流技术,共同进步!
小结
可以看到我们定制后的CKEditor
代码编辑器的功能已经是非常强大了,后面我们可以将使用这个富文本编辑器编辑好的内容以字符串文本的格式保存到数据库中持久化,需要在前台展示的时候通过接口查询出富文本文本内容数据,然后在Vue
页面组件中通过v-html
指令直接渲染成html
格式的内容。例如博客系统和面试题小程序,我们通过富文本编辑器就可以把文章和面试题答案内容通过渲染html
格式的富文本内容通过一个页面动态渲染不同的博客文章和面试题内容明细。
需要注意的是:CKEditor5
无法在编辑器中实现代码的语法高亮显示,即时通过hljs#hilightAuto
方法处理从Dom节点中取到的代码块内容,然后又将hljs#hilightAuto
方法处理过的代码语法高亮显示的html
文本数据设置到双向绑定数据editorData
变量也无法使编辑器插入代码块中的代码语法高亮显示,这一点官方文档中的Code blocks
特性部分也进行了说明,笔者之前为了实现在编辑器内代码块中实现代码语法高亮显示折腾了很久,也仔细阅读了CKEditor5
官方文档的API
,并试图在代码块中插入代码后尝试修改数据富文本编辑器中的模型仍然面临失败的结果,最后只是证明的官方文档的这一结论。
[可以看到我们定制后的CKEditor
代码编辑器的功能已经是非常强大了,后面我们可以将使用这个富文本编辑器编辑好的内容以字符串文本的格式保存到数据库中持久化,需要在前台展示的时候通过接口查询出富文本文本内容数据,然后在Vue
页面组件中通过v-html
指令直接渲染成html
格式的内容。例如博客系统和面试题小程序,我们通过富文本编辑器就可以把文章和面试题答案内容通过渲染html
格式的富文本内容通过一个页面动态渲染不同的博客文章和面试题内容明细。
需要注意的是:CKEditor5
无法在编辑器中实现代码的语法高亮显示,即时通过hljs#hilightAuto
方法处理从Dom节点中取到的代码块内容,然后又将hljs#hilightAuto
方法处理过的代码语法高亮显示的html
文本数据设置到双向绑定数据editorData
变量也无法使编辑器插入代码块中的代码语法高亮显示,这一点官方文档中的Code blocks
特性部分也进行了说明。
笔者之前为了实现在编辑器内代码块中实现代码语法高亮显示折腾了很久,也仔细阅读了CKEditor5
官方文档的API
,并试图在代码块中插入代码后尝试修改数据富文本编辑器中的模型仍然面临失败的结果,最后只是证明的官方文档的这一结论。
https://ckeditor.com/docs/ckeditor5/latest/features/code-blocks.html
虽然在编辑器中代码语法高亮不会生效,但是将代码高亮处理后的html
格式代码块内容在前端渲染却能看到代码语法高亮效果。如果必须要在富文本编辑器中的插入代码块后实现代码高亮效果则需要将 CKEditor5
改为 CKEditor4
, 同时配置codesnippt
插件才能生效。感兴趣的同学可以参考CKEditor4
有关代码语法高亮特性部分文档来实现, 文档链接:
CKEditor4代码高亮化参考文档
阿里云服务器推荐
作为一名程序员,想要拥有自己的独立站点,最好能有一台自己的云服务器。不论是部署自己开发的项目还是部署二次开发的开源项目,要做成一个产品,最终都需要我们部署到自己的云服务器上。就算你暂时不需要把项目部署到自己的云服务器上,就算练习安装一些中间件服务,深入学习计算机运维技术,我们最好也能有一台云服务器,方便我们在Linux系统上运行各种指令。
笔者最近趁着【阿里云】的春节促销活动,购入了两台阿里云的服务器,加上之前购入的【腾讯云】服务器,目前手上已经4台云服务器了,但是考虑到腾讯云服务器目前每个月的月租比较贵,笔者打算下个月开始停掉【腾讯云】的服务器,一年下来可以节约一两千开支。
这次的【阿里云】服务器亲身体验过,不仅好用,而且价格非常实惠。新人购买一台2核4G配置的阿里云服务器3个月只需要60多块钱,而购买一台2核2G配置的ECS云服务器一年只需要99元,非常划算。
有一台属于自己的云服务器,不仅有助于提高我们独立开发网站的能力,而且有一天说不定还能还能依靠自己的网站变现给自己带来一份收入不低于主业的副业收入,也不用担心有一天因为自己年龄太大被裁员了再也找不到工作。
我们必须未雨绸缪,为将来终会到来的中年失业危机做准备,小伙伴们有想法就赶紧去行动吧!点击下面的【阿里云小站】站外链接就可以去选购自己需要的【阿里云服务器】了!
参考文章
【1】Vue.js 3+ rich text editor component