ck-editor5的研究 (2):对 CKEditor5 进行设计,并封装成一个可用的 vue 组件

news2025/6/6 14:57:06

前言

在上一篇文章中—— ck-editor5的研究(1):快速把 CKEditor5 集成到 nuxt 中 ,我仅仅是把 ckeditor5 引入到了 nuxt 中,功能还不算通用。

这一篇内容将会对其进行设计,并封装成可复用的 vue 组件,实现的效果如下:
在这里插入图片描述

总共分成了 3 步

1. 第一步:搭建目录结构 (复用之前的代码)

复用上一篇的 ts 和 vue,如下:
在这里插入图片描述
在这里插入图片描述
接着,再 pages 中创建一个测试该组件的demo2.vue文件:
在这里插入图片描述
能显示到页面即可,可以看到跟上一篇是完全一样的:在这里插入图片描述

2. 第二步:正式封装组件

既然要变成通用的组件,肯定要接受一些参数,并且需要一个名字,于是我初步设定为这样:
在这里插入图片描述
继续思考,大概要做这些事:

  1. 首先要初始化,也就是创建编辑器实例对象 instance,同时合并传进来的配置项 config。
  2. 定义一些事件,同时在特定的时机,发出这些 emit 事件。
  3. 在组件销毁时,清理编辑器实例对象 instance。
  4. 性能:更改内容的时候,要不断更新,所以考虑使用一个延迟进行优化 (debounce)
  5. 监听是否被禁用,设置编辑器的状态

为了完善 ts 类型,这里加了2个依赖包:

pnpm add @ckeditor/ckeditor5-utils@37.1.0
pnpm add @ckeditor/ckeditor5-core@37.1.0

于是,我们就把 ck-editor2.client.vue 改造成以下代码:

<template>
  <component :is="tagName" ref="editorRef" />
</template>

<script setup lang="ts">
import MyClassicEditor, { type EditorConfig, type EventInfo } from './ckeditor2';
import { debounce } from 'lodash-es';

defineOptions({
  name: 'CkEditor2',
});

type PropsType = {
  tagName?: string; // 标签名
  disabled?: boolean; // 是否禁用(禁用后, 不能编辑)
  disableTwoWayDataBinding?: boolean; // 是否禁用双向数据绑定(出于性能考虑, 默认禁用)
  config?: () => {}; // 配置项)
};
const props = withDefaults(defineProps<PropsType>(), {
  tagName: 'div',
  disabled: false,
  disableTwoWayDataBinding: true,
  config: () => ({}),
});
const emit = defineEmits<{
  ready: [editor: MyClassicEditor];
  destroy: [];
  blur: [event: EventInfo, editor: MyClassicEditor];
  focus: [event: EventInfo, editor: MyClassicEditor];
  input: [data: string, event: EventInfo, editor: MyClassicEditor];
  'update:modelValue': [data: string, event: EventInfo, editor: MyClassicEditor];
}>();

const model = defineModel('modelValue', { type: String, default: '' });
// lockId 只读模式锁
const VUE_INTEGRATION_READ_ONLY_LOCK_ID = Symbol('vue-integration-read-only-lock-id');
const editorRef = useTemplateRef('editorRef');
// 编辑器实例
let instance: MyClassicEditor | null = null;
// 上一次的编辑器数据
const lastEditorData = ref('');

// 监听 disabled 变化, 设置只读模式
watch(
  () => props.disabled,
  () => {
    if (props.disabled) {
      instance?.enableReadOnlyMode(VUE_INTEGRATION_READ_ONLY_LOCK_ID);
    } else {
      instance?.disableReadOnlyMode(VUE_INTEGRATION_READ_ONLY_LOCK_ID);
    }
  },
);

// 监听 modelValue 变化, 同步数据
watch(model, (newModel) => {
  if (instance && newModel !== lastEditorData.value) {
    instance.data.set(newModel);
  }
});

/**
 * 初始化编辑器
 */
async function initEditor() {
  try {
    if (!editorRef.value) {
      return;
    }
    // 合并配置对象
    const config: EditorConfig = Object.assign({}, MyClassicEditor.defaultConfig, props.config);
    // 如果 model 有值, 则设置初始数据
    if (model.value) {
      config.initialData = model.value;
    }

    // 创建编辑器实例
    instance = await MyClassicEditor.create(editorRef.value as HTMLElement, config);

    // 设置编辑器事件
    setUpEditorEvents(instance);

    // 同步内容, 如果创建实例期间 modelValue 被改变的话
    if (model.value !== config.initialData) {
      instance.data.set(model.value);
    }

    // 设置禁用状态
    if (props.disabled) {
      instance.enableReadOnlyMode(VUE_INTEGRATION_READ_ONLY_LOCK_ID);
    }

    // 触发 ready 事件
    emit('ready', instance);
  } catch (error: any) {
    console.log('error :>> ', error.stack);
  }
}

/**
 * 设置编辑器事件
 * @param editor 编辑器实例, 自带了一些方法和属性, 可以打印看一看
 *   editor.model: 编辑器模型
 *   editor.model.document.on('change:data', emitDebouncedInputEvent); 表示监听编辑器文档数据变化事件
 *   editor.data.get(): 获取编辑器数据
 *   editor.data.set(): 设置编辑器数据
 *   editor.editing.view.document: 编辑器视图, 也可以监听事件
 *   editor.data.on(): 监听编辑器数据变化
 *   editor.data.off(): 移除编辑器数据变化监听
 *   editor.data.destroy(): 销毁编辑器数据
 *   editor.enableReadOnlyMode(): 设置只读模式
 */
function setUpEditorEvents(editor: MyClassicEditor) {
  // 延迟 300ms 触发更新
  const emitDebouncedInputEvent = debounce(
    (evt: EventInfo) => {
      // 如果禁用双向数据绑定, 则不触发更新
      if (props.disableTwoWayDataBinding) {
        return;
      }
      lastEditorData.value = editor.data.get();
      const data = lastEditorData.value;
      emit('update:modelValue', data, evt, editor);
      emit('input', data, evt, editor);
    },
    300,
    { leading: true },
  );

  // 监听编辑器数据变化, 触发 update:modelValue 和 input 事件
  editor.model.document.on('change:data', emitDebouncedInputEvent);
  editor.editing.view.document.on('focus', (evt: EventInfo) => {
    emit('focus', evt, editor);
  });
  editor.editing.view.document.on('blur', (evt: EventInfo) => {
    emit('blur', evt, editor);
  });
}

// 暴露编辑器实例和上一次的编辑器数据
defineExpose({ instance, lastEditorData });

onMounted(() => {
  nextTick(() => {
    initEditor();
  });
});

onBeforeUnmount(() => {
  if (instance) {
    instance.destroy();
    instance = null;
  }
  emit('destroy');
});
</script>

3. 第三步:改造 demo2.vue 测试文件

把 pages 下的 demo2.vue 文件改造一下,变成可交互式的形式,方便测试 ck-editor2.client.vue 组件:

<template>
  <div class="space-y-4">
    <h1 class="text-xl font-bold">demo2: 把 ckeditor5 封装成一个可配置的、有一些事件的 vue 组件</h1>

    <!-- 测试控制面板 -->
    <div class="bg-gray-50 !mt-0 space-y-2 rounded-lg border p-4">
      <h3 class="m-0 font-medium">事件日志:</h3>
      <div class="bg-white h-32 overflow-y-auto rounded border p-2">
        <div v-for="(log, index) in eventLogs" :key="index" class="text-sm">
          <span class="text-gray-500">{{ log.time }}</span>
          <span class="ml-2 text-blue-600">{{ log.event }}:</span>
          <span class="ml-2">{{ log.message }}</span>
        </div>
      </div>
      <button @click="clearLogs" class="text-white bg-red-500 hover:bg-red-600 rounded px-4 py-1 text-sm">
        清除日志
      </button>
      <div class="flex flex-col gap-2">
        <label class="flex items-center gap-2">
          <span class="text-sm font-medium">编辑器内容:</span>
          <input type="text" v-model="model" class="rounded border px-3 py-1" />
          <button
            @click="setRandomContent"
            class="text-white cursor-pointer rounded border-none bg-blue-500 px-4 py-1 text-sm text-[#fff] outline-none hover:bg-blue-600">
            设置随机内容
          </button>
        </label>
        <label for="disabled" class="flex items-center gap-2">
          <span class="text-sm font-medium">是否禁用:</span>
          <input type="checkbox" id="disabled" v-model="disabled" class="rounded border px-3 py-1" />
        </label>
        <label for="disableTwoWayDataBinding" class="flex items-center gap-2">
          <span class="text-sm font-medium">是否禁用双向数据绑定:</span>
          <input
            type="checkbox"
            id="disableTwoWayDataBinding"
            v-model="disableTwoWayDataBinding"
            class="rounded border px-3 py-1" />
        </label>
      </div>
    </div>

    <!-- 编辑器组件 -->
    <ClientOnly>
      <ck-editor2
        v-model="model"
        :disabled="disabled"
        :disableTwoWayDataBinding="disableTwoWayDataBinding"
        @ready="onEditorReady"
        @focus="onEditorFocus"
        @blur="onEditorBlur"
        @input="onEditorChange" />
    </ClientOnly>
  </div>
</template>

<script setup lang="ts">
const model = ref('初始化的值');
const eventLogs = ref<Array<{ time: string; event: string; message: string }>>([]);
const disabled = ref(false);
const disableTwoWayDataBinding = ref(false);

// 添加事件日志
const addLog = (event: string, message: string) => {
  const now = new Date().toLocaleTimeString();
  eventLogs.value.unshift({
    time: now,
    event,
    message,
  });
};

// 清除日志
const clearLogs = () => {
  eventLogs.value = [];
};

// 编辑器事件处理
const onEditorReady = () => {
  addLog('ready', '编辑器初始化完成');
};

const onEditorFocus = () => {
  addLog('focus', '编辑器获得焦点');
};

const onEditorBlur = () => {
  addLog('blur', '编辑器失去焦点');
};

const onEditorChange = (event: any) => {
  addLog('change', `内容已更改: ${model.value.slice(0, 50)}...`);
};

// 设置随机内容
const setRandomContent = () => {
  const contents = [
    '<h1>标题内容</h1><p>这是一段测试文字</p>',
    '<p>Hello, CKEditor!</p>',
    '<blockquote>这是一段引用文字</blockquote>',
    '<ul><li>列表项 1</li><li>列表项 2</li></ul>',
    '<a href="https://www.baidu.com">百度</a>',
    '<h1>标题1</h1><h2>标题2</h2><h3>标题3</h3><h4>标题4</h4><h5>标题5</h5><h6>标题6</h6>',
    '<p>Hello, CKEditor! 这是一段测试文字</p>',
  ];
  const randomIndex = Math.floor(Math.random() * contents.length);
  model.value = contents[randomIndex];
};
</script>

<style lang="less" scoped>
// 样式
</style>

最终测试效果

可以看到事件日志、变更属性,以及 v-model 都可起作用的:
在这里插入图片描述
到此为止,这个组件已经完成了,并且我们可以拿到数据,再恰当的时间发起请求,保存数据。

不容易啊!

本来以为写文章很快,随意讲一下即可,没想到竟花了我端午节一整天时间…

这让我想起了凯奇演的一部电影里面的一句话。 “成年人的世界里,没有容易二字”。

马马虎虎,不仅做不成事,浪费了时间,还会把生活搞得一团糟。不妨把节奏放慢点,也算是一种成长的体验。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2399148.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java-redis实现限时在线秒杀功能

1.使用redisson pom文件添加redisson <!--redisson--><dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.23.4</version></dependency> 2.mysql数据库表设…

simulink mask、sfunction和tlc的联动、接口

这里全部是讲的level2 sfunction&#xff08;用m语言编写&#xff09;&#xff0c;基于matlab 2020a。 1.mask的参数操作 1&#xff09;mask通过set_param和get_param这2个函数接口对mask里面定义的Parameters&Dialog的参数的大部分属性进行读写&#xff0c;一般是Value值…

VMWare安装常见问题

如果之前安装过VMWare软件&#xff0c;只要是 15/16 版本的&#xff0c;可以正常使用的&#xff0c;不用卸载&#xff01;&#xff01;&#xff01; 如果之前安装过&#xff0c;卸载了&#xff0c;一定要保证通过正常的渠道去卸载&#xff08;通过控制面板卸载软件&#xff09…

【北邮 操作系统】第十二章 文件系统实现

一、文件的物理结构 1.1 文件块、磁盘块 类似于内存分页&#xff0c;磁盘中的存储单元也会被分为一个个“块/磁盘块/物理块”。很多操作系统中&#xff0c;磁盘块的大小与内存块、页面的大小相同 内存与磁盘之间的数据交换(即读/写操作、磁盘I/0)都是以“块”为单位进行的。即…

Docker 插件生态:从网络插件到存储插件的扩展能力解析

Docker 容器技术以其轻量、快速、可移植的特性,迅速成为构建和部署现代应用的核心工具。然而,尽管 Docker Engine 自身功能强大,但在面对多样化的生产环境和复杂业务需求时,仅靠核心功能往往无法满足所有场景。 例如,跨主机的容器网络通信、异构存储系统的持久化数据管理…

WordPress搜索引擎优化的最佳重定向插件:进阶指南

在管理网站时&#xff0c;我们经常需要调整网页地址或修复错误链接。这时&#xff0c;通过重定向不仅能有效解决这些问题&#xff0c;还能显著提升网站在搜索引擎中的排名。对于熟悉基础重定向插件的用户来说&#xff0c;一些功能更强大的工具可以帮助你更全面地管理网站&#…

org.junit.runners.model.InvalidTestClassError:此类问题的解决

不知道大家是否遇见过以上这种情况&#xff0c;我也是今天被这个错误搞得很烦&#xff0c;后来通过网上查找资料终于找到了问题所在————就是简单的Test注解的错误使用 Test注解的注意情况 &#xff1a;1 权限必须是public 2 不能有参数 3 返回值类型是void 4 本类的其他的…

用户管理页面(解决toggleRowSelection在dialog用不了的隐患,包含el-table的plus版本的组件)

新增/编辑/删除/分配角色&#xff0c;图片上传在此文章分类下另一个文章 1.重点分配角色&#xff1a; <template><!-- 客户资料 --><div class"pageBox"><elPlusTable :tableData"tableData" :tablePage"tablePage" onSi…

Linux-GCC、makefile、GDB

GCC gcc -E test.c -o test.i预处理(-o指定文件名) gcc -S test.i -o test.s编译gcc -c test.s -o test.o汇编gcc test.o -o test链接(生成一个可执行程序的软连接) gcc test.c -o test一条指令可以完成以上所有内容 gcc *.c -I(大写的i) include由于在main.c中找不到当前文件…

[MySQL初阶]MySQL(7) 表的内外连接

标题&#xff1a;[MySQL初阶]MySQL(7)表的内外连接 水墨不写bug 文章目录 一. 内连接 (INNER JOIN)二. 外连接 (OUTER JOIN)关键区别总结 三、 如何选择 在 MySQL 中&#xff0c;连接&#xff08;JOIN&#xff09;用于根据两个或多个表之间的相关列组合行。内连接&#xff08;I…

【R语言编程绘图-mlbench】

mlbench库简介 mlbench是一个用于机器学习的R语言扩展包&#xff0c;主要用于提供经典的基准数据集和工具&#xff0c;常用于算法测试、教学演示或研究场景。该库包含多个知名数据集&#xff0c;涵盖分类、回归、聚类等任务。 包含的主要数据集 BostonHousing 波士顿房价数据…

检索器组件深入学习与使用技巧 BaseRetriever 检索器基类

1. BaseRetriever 检索器基类 在 LangChain 中&#xff0c;传递一段 query 并返回与这段文本相关联文档的组件被称为 检索器&#xff0c;并且 LangChain 为所有检索器设计了一个基类——BaseRetriever&#xff0c;该类继承了 RunnableSerializable&#xff0c;所以该类是一个 …

【Doris基础】Doris中的Replica详解:Replica原理、架构

目录 1 Replica基础概念 1.1 什么是Replica 1.2 Doris中的副本类型 2 Doris副本架构设计 2.1 副本分布机制 2.2 副本一致性模型 3 副本生命周期管理 3.1 副本创建流程 3.2 副本恢复机制 4 副本读写流程详解 4.1 写入流程与副本同步 4.2 查询流程与副本选择 5 副本…

【中国·广州】第三届信号处理与智能计算国际学术会议 (SPIC2025) 即将开启

第三届信号处理与智能计算国际学术会议 (SPIC2025) 即将开启 在信息技术飞速发展的当下&#xff0c;信号处理与智能计算作为前沿科技领域&#xff0c;正深刻改变着我们的生活与产业格局。为汇聚全球顶尖智慧&#xff0c;推动该领域进一步突破&#xff0c;第三届信号处理与智能…

Android12 Launcher3显示所有应用列表

Android12 Launcher3显示所有应用列表 1.前言&#xff1a; 最近在Android12Rom定制时需要显示所有桌面应用的图标&#xff0c;并且不能去掉抽屉&#xff0c;在手机上面抽屉和所有应该列表是两种不同模式&#xff0c;用户基可以自行选择&#xff0c;但是在自定义的launcher中这…

智能快递地址解析接口如何用PHP调用?

一、什么是智能快递地址解析接口 随着互联网技术的普及和电子商务的迅猛发展&#xff0c;网购已成为现代人日常生活的重要组成部分。然而&#xff0c;在这个便捷的背后&#xff0c;一个看似不起眼却影响深远的问题正悄然浮现——用户填写的快递地址格式混乱、信息不全甚至错漏…

华为OD机试真题——模拟消息队列(2025B卷:100分)Java/python/JavaScript/C++/C语言/GO六种最佳实现

2025 B卷 100分 题型 本文涵盖详细的问题分析、解题思路、代码实现、代码详解、测试用例以及综合分析; 并提供Java、python、JavaScript、C++、C语言、GO六种语言的最佳实现方式! 2025华为OD真题目录+全流程解析/备考攻略/经验分享 华为OD机试真题《模拟消息队列》: 目录 题…

基于VU37P的高性能采集板卡

基于VU37P的高性能采集板卡是一款最大可提供20路ADC接收通道的高性能采集板卡。每路A/D通道支持1GS/s的采样率&#xff0c;分辨率为14bit&#xff0c;模拟输入带宽可达500MHz&#xff0c;交流耦合&#xff0c;输入阻抗50欧姆。 产品简介 可提供20路ADC接收通道的高性能采集板…

2025-05-31 Python深度学习10——模型训练流程

文章目录 1 数据准备1.1 下载与预处理1.2 数据加载 2 模型构建2.1 自定义 CNN 模型2.2 GPU加速 3 训练配置3.1 损失函数3.2 优化器3.3 训练参数 4 训练循环4.1 训练模式 (model.train())4.2 评估模式 (model.eval()) 5 模型验证 本文环境&#xff1a; Pycharm 2025.1Python 3.1…

K8S StatefulSet 快速开始

其实这篇文章的梗概已经写了很久了&#xff0c;中间我小孩出生了&#xff0c;从此人间多了一份牵挂。抽出一些时间去办理新生儿相关手续。初为人父确实艰辛&#xff0c;就像学技术一样&#xff0c;都需要有极大的耐心&#xff0c;付出很多的时间。 一、引子 1.1、独立的存储 …