论前端第三方库的技术选型 —— 以 Jodit Editor 为例

论前端第三方库的技术选型 ------ 以 Jodit Editor 为例

近期对一个后台项目的富文本编辑器 进行了一次技术升级,这一过程中涉及到前期的技术选型 和实际落地的配置和使用,在这一过程中颇有收获,随决定成文记录,汇总经验。

问题背景

该项目是一个内部管理后台,不对外开放,富文本编辑器功能的使用频率并不高,只是偶尔用来编辑一下文件内容,对复杂格式没有要求。

虽然没什么复杂需求,原来使用的 wangEditor 4富文本编辑器还是有诸多不足之处:

  1. 选定文字进行修改格式时,文字的选中状态会消失;
  2. Word 内容粘贴后格式有问题
  3. 无法以源代码模式编辑内容;
  4. 不好进行定制化开发

同时考虑到后续可能还需要进一步开发相关功能,决定对富文本编辑器进行更换。

技术选型

在自己开始找新的富文本编辑器前,首先是咨询了公司的大前端团队,看看是否有已经采购的富文本编辑器,或成熟的解决方案。如果有,那就和公司保持统一,避免重复造轮子

不过结论是并没有,所以只能自己开始做富文本编辑器的技术选型。

基于使用场景,确定富文本编辑器需要满足以下需求:

  1. 能够免费商用,避免可能的法律问题,同时因为是后台项目中简单使用的,就不考虑走采购流程了;
  2. 支持粘贴 Word 内容时保留大部分格式;
  3. 能够覆盖旧编辑器的所有功能;
  4. 支持功能扩展;
  5. 学习成本低、文档完善,便于后续维护

富文本编辑器待选列表主要参考掘金的文章------《富文本选型太难了,谁来帮帮我!》,同时准备了一个Word 文档测试用例,用来快速测试各富文本编辑器对 Word 格式的兼容情况。

最后测试结果如下:

富文本编辑器 免费商用 Word 内容保留格式 功能齐全 功能扩展 易维护性
TinyMCE ❌ 免费版不支持自托管
Quill ❌ 免费版不支持自托管
TinyMCE ❌没有默认的功能菜单,需要通过组件系统自行搭建,过于复杂
Editor.js
Slate
lexical ❌没有默认的功能菜单,需要自行封装搭建 ❌尚未推出1.0 正式版,且各种概念非常复杂,可能出现难以解决的问题
Jodit Editor ✅有基础的文档,同时免费版的仓库是公开的,可以参考其中的源码开发插件

从这里可以看出,这次技术选型时并没有对每个技术栈都进行了详尽的调研 ,而是只要稍微不符合条件就直接排除。这样做是为了加快开发效率,毕竟这次技术选型只针对这一个项目,没必要把所有技术栈的优劣都分析清楚。

Jodit Editor 的配置和使用

确定富文本编辑器使用的技术后,接下来就是正式使用了。主要参考资料包括:

Jodit Editor 学习和开发思路

由于之前没有使用 Jodit Editor 的经验,也没有已有的项目可以直接参考,所以需要边学习边开发

以下是主要的学习和开发思路:

  1. 基础配置和开发方法,参考官方文档Playground
  2. 其次就是直接使用搜索引擎找解决方法;
  3. 如果以下两种方法都解决不了问题,就需要去翻阅源码 ,找类似的例子去参考(比如插件的开发);
  4. 除了以上这些,开发时参考 TS 的代码提示和类型检查也可以辅助判断。

Jodit Editor 基本概念

依照以上方法进行开发,可以快速对 Jodit Editor 建立起以下基本概念:

  1. 通过 config 参数 可以对编辑器各方面进行定制化,包括样式、工具栏按钮、禁用的插件等,详见 xdsoft.net/jodit/docs/...
  2. Jodit Editor 默认内置了各种插件, 可以通过 config.disablePlugins 参数禁用;
  3. 内置的插件可以定制化 ,定制的方式是设置 config 中的各项参数;
  4. Jodit React 提供了通过 ref 属性获取编辑器实例的 API,可以用来对编辑器进行各种操作;
  5. 当已有配置项无法满足需求时,往往需要开发插件来进行深度定制化 ,插件的扩展自 Jodit Editor 提供的 Plugin 类,可以注册各个生命周期 事件,并且通过这个类暴露的 API,以及直接访问编辑器实例 ,从而进行各种定制化,详见 xdsoft.net/jodit/docs/...

编辑器基础封装

遵循官方文档说明,同时参考旧版编辑器的功能配置,创建以下暴露 valueonChange 接口的受控组件:

tsx 复制代码
 /**
  * 编辑器组件
  * @author: jason02.ruan
  * @date: 2025-10-16 11:02:54
  **/
 import JoditEditor from 'jodit-react';
 import { ComponentProps, useMemo, useRef } from 'react';
 ​
 /**
  * 编辑器组件
  * @description 注意,聚焦编辑器后可能会出现凭空增加了空行,但是又没有触发 onChange 的情况。是因为 jodit 内部的会对空内容的 `<p></p>` 进行处理,在里面插入一个 `<br />` 元素,但没有触发 onChange。
  *
  * 因为考虑到变化会很明显,使用者会注意到,并在提交前注意,所以这里暂时不处理。
  */
 export const Editor = ({ value, onChange }: { value?: string; onChange?: (value: string) => void }) => {
   const editor = useRef<any>(null);
 ​
   const config: ComponentProps<typeof JoditEditor>['config'] = useMemo(() => {
     return {
       language: 'zh_cn',
       height: 400,
       // 禁用关于、人工智能助手这两个无用插件
       // 禁用图片、视频、文件、图片处理器、图片属性这几个插件。
       disablePlugins: ['about', 'ai-assistant', 'image', 'video', 'file', 'image-processor', 'image-properties'],
       buttons: [
         'paragraph',
         'bold',
         'fontsize',
         'italic',
         'underline',
         'strikethrough',
         // 之所以添加一个空的 indent 组,是因为 justify 插件会默认把对齐方式按钮添加到 indent 组中,不然的话只能自己封装一个对齐方式按钮列表
         // 详见 https://xdsoft.net/jodit/docs/modules/plugins_justify.html
         // https://xdsoft.net/jodit/docs/modules/plugins_indent.html
         {
           group: 'indent',
           buttons: [],
         },
         'lineHeight',
         'brush',
         'link',
         'ul',
         'table',
         'hr',
         'undo',
         'redo',
         'source',
         'fullsize',
       ],
     };
   }, []);
 ​
   return <JoditEditor ref={editor} value={value} onChange={onChange} config={config} tabIndex={1} />;
 };

这一版本的基础封装主要参考了以下内容:

  1. Jodit Editor Playground:生成配置和测试各配置项、插件对应的功能;
  2. Justify 插件文档:查看对齐按钮相关说明;
  3. Indent 插件文档:查看 indent按钮组相关说明;
  4. 按钮系统文档:查看新增按钮相关说明。

关闭工具栏响应式变化

参考 mobile 插件文档,设置 toolbarAdaptivefalse,防止因为宽度变小导致按钮工具栏按钮被折叠

tsx 复制代码
 export const Editor = ({ value, onChange }: { value?: string; onChange?: (value: string) => void }) => {
   const config: ComponentProps<typeof JoditEditor>['config'] = useMemo(() => {
     return {
       toolbarAdaptive: false, // 关闭工具栏的响应式变化
       // ...
     };
   }, []);
 ​
   return <JoditEditor value={value} onChange={onChange} config={config} />;
 };

默认清除 Word 粘贴内容格式

由于后端生成 PDF 功能对 Word 格式内容处理有问题,所以设置所有 Word 内容粘贴时都进行格式清除。

参考 Paste From Word 插件文档,设置以下配置项:

tsx 复制代码
 import { Jodit } from 'jodit-react';
 ​
 export const Editor = ({ value, onChange }: { value?: string; onChange?: (value: string) => void }) => {
   const config: ComponentProps<typeof JoditEditor>['config'] = useMemo(() => {
     return {
       // 从 word 粘贴时,不询问粘贴方式,直接粘贴为纯净的 HTML
       // 因为后端生成 pdf 的功能无法识别 Word 格式,所以这里直接粘贴为纯净的 HTML,后续如果需要支持 Word 格式,可以考虑使用 word-content-processor 插件。
       askBeforePasteFromWord: false, // 粘贴来自 Word 的内容时不再询问
       defaultActionOnPasteFromWord: Jodit.constants.INSERT_AS_TEXT, // 粘贴来自 Word 的内容时默认粘贴方式为粘贴为纯净的 HTML
       // ...
     };
   }, []);
 ​
   return <JoditEditor value={value} onChange={onChange} config={config} />;
 };

添加点击后出现 React 组件的按钮

效果演示:

现在希望使用 Antd 组件库提供的 <Select> 组件 实现点击工具栏按钮后出现一个带搜索功能的数据源选择器弹窗

根据官方文档,按钮点击后出现的 dom 元素需要配置 popup 属性返回(点击按钮后会执行 popup 属性方法,然后将返回的 dom 元素显示在弹窗中 ),但是 React 组件本质上是一个渲染函数 ,并不能直接返回后渲染到弹窗中

所以逻辑应该为:

  1. popup 属性返回一个目标 dom 元素挂载在弹窗元素中;
  2. 触发组件函数执行,使用 createPortal()方法挂载组件到这个 dom 元素中。

由于生成目标 dom 元素的逻辑,和挂载组件的逻辑存在强关联,所以决定封装在一个模块中,以降低维护难度。

注:以下封装形式并非最佳实践 ,可以把 popupRef 改为 state,从而去除 trigger,具体请参考 stackblitz.com/~/github.co...

src/AntdSelectWithRef.tsx

tsx 复制代码
 import { Select as AntdSelect } from 'antd';
 import type { ComponentProps } from 'react';
 import { useCallback, useEffect, useRef, useState } from 'react';
 import { createPortal } from 'react-dom';
 ​
 /**
  * Antd Select 组件 Hook(使用 ref 版本)
  *
  * 提供一个创建 Antd Select 组件的 dom 元素容器的函数,当调用该函数创建 dom 元素容器后,
  * 会通过 createPortal 将 Antd Select 组件渲染到该 dom 元素容器中。
  *
  * 与 useAntdSelect 的区别:
  * - 使用 useRef 而不是 useState 来存储 DOM 元素引用
  * - 使用单独的 state 作为触发重新渲染的开关(值本身没有含义)
  *
  * @returns 包含 createElement 方法和 Select 组件的对象
  */
 export function useAntdSelectWithRef() {
   /** Antd 弹窗的 dom 元素引用(使用 ref 存储) */
   const popupRef = useRef<HTMLElement | null>(null);
 ​
   /** 单纯触发重新渲染的开关,值本身没有具体含义 */
   const [trigger, setTrigger] = useState(false);
 ​
   /**
    * 创建 Antd Select 组件的 dom 元素
    * @returns 创建的 HTMLElement
    */
   const createElement = useCallback(() => {
     const popupContainer = document.createElement('div');
     // 设置弹窗宽高,以保证 Antd 组件有足够的显示空间
     popupContainer.style.width = '200px';
     popupContainer.style.height = '300px';
     popupRef.current = popupContainer;
     // 触发重新渲染,然后立即重置(因为无法监听 DOM 是否被卸载)
     setTrigger((prev) => !prev);
     return popupContainer;
   }, []);
 ​
   /**
    * Select 组件
    *
    * 接收和 Antd 的 Select 组件相同的 props,并将其渲染到 popupRef 对应的 dom 元素中。
    * trigger 状态仅用于触发重新渲染,值本身没有含义。
    */
   const Select = (props: React.ComponentProps<typeof AntdSelect>) => {
     const ref: ComponentProps<typeof AntdSelect>['ref'] | null = useRef(null);
 ​
     useEffect(() => {
       ref.current?.focus();
     }, []);
 ​
     // 如果没有 DOM 元素引用,返回 null
     if (!popupRef.current) return null;
 ​
     // 使用 createPortal 将组件渲染到 ref 指向的 DOM 元素中
     // 读取 trigger 以确保组件能响应 createElement 的调用并重新渲染
     // trigger 的值本身没有含义,只用于触发重新渲染
     void trigger;
 ​
     return createPortal(
       <AntdSelect<any>
         // 设置 dropdown 的样式,防止被 jodit editor 的 popup 遮挡
         styles={{
           root: {
             zIndex: 100000000,
           },
         }}
         // 确保 dropdown 渲染在当前容器中
         getPopupContainer={(triggerNode) => {
           return triggerNode || document.body;
         }}
         // 手动设置宽度,因为只有在 form 中时,select 组件才会设置 .ant-select-in-form-item,然后才会默认设置宽度为 100%。如果不这么设置,则宽度不会被设置为父元素的宽度
         style={{
           width: '100%',
         }}
         showSearch
         // 过滤选项,不然默认的 Select 组件虽然有搜索框,但没有任何搜索功能
         filterOption={(input, option) => {
           return (
             option?.label
               ?.toString()
               .toLowerCase()
               .includes(input?.toLowerCase() || '') || false
           );
         }}
         // 默认展开下拉列表
         open
         ref={ref}
         {...props}
       />,
       popupRef.current,
     );
   };
 ​
   return {
     createElement,
     Select,
   };
 }
 ​

在编辑器组件中使用:

tsx 复制代码
 import JoditEditor from 'jodit-react';
 import type { IJodit } from 'jodit/esm/types';
 import type { ComponentProps } from 'react';
 import { useMemo, useRef, useState } from 'react';
 import { useAntdSelectWithRef } from './AntdSelectWithRef';
 import './App.css';
 ​
 /**
  * 主应用组件
  * 演示使用原生 DOM 方法创建元素,并通过 Portal 在元素中渲染内容
  */
 function App() {
   // 使用元素内容管理 Hook(useRef 版本)
   const { createElement: createElementWithRef, Select: SelectWithRef } =
     useAntdSelectWithRef();
   // 应用根元素的引用,用于挂载新创建的元素
   const editorRef = useRef<IJodit>(null);
 ​
   const config: ComponentProps<typeof JoditEditor>['config'] = useMemo(
     () => ({
       toolbarAdaptive: false,
       buttons: [
         {
           name: 'antd-ref',
           text: '点击后出现antd组件(ref版本)',
           exec: () => {
             return false;
           },
           popup: () => {
             const element = createElementWithRef();
             return element;
           },
         },
       ],
     }),
     [createElement, createElementWithRef],
   );
 ​
   const [value, setValue] = useState<string>('');
 ​
   return (
     <div className="app-container">
       <div style={{ marginTop: '20px' }}>
         <JoditEditor
           ref={editorRef}
           config={config}
           value={value}
           onChange={(value) => {
             setValue(value);
           }}
         />
       </div>
       <SelectWithRef
         options={Array.from({ length: 100 }, (_, index) => ({
           label: `选项(ref) ${index + 1}`,
           value: `option-ref-${index + 1}`,
         }))}
         onChange={(value) => {
           editorRef.current?.selection.insertHTML(value as string);
         }}
       />
     </div>
   );
 }
 ​
 export default App;
 ​

这一整套逻辑的核心在于以下几点:

  1. 利用 useRef() 缓存弹窗容器 dom 元素
  2. 利用 createPortal() 将组件渲染到弹窗容器 dom 元素中;
  3. 利用创建一个 state ,用来触发 <Select/> 这个组件重新渲染

Word 内容处理插件

Jodit editor 默认的内部处理逻辑已经可以保留几乎所有的 Word 格式,但是如果还需要对粘贴自 Word 的内容进行定制化处理,则需要自行制作插件。

该插件实现以下功能:

  1. 粘贴来自 Word 的内容和编辑器失去焦点时,执行以下格式化操作:

    1. 清除字体样式。部分 Word 文档会使用特殊字体,但实际最终生成 PDF 时默认使用的是宋体,清除以防止编辑器效果和 PDF 效果有冲突(其实设置根元素字体为宋体会更好);
    2. 清除 mso- 开头的 CSS 属性。这些属性是 Office 专用的,浏览器不识别,故清除;
    3. 转换 <br><hr> 标签 。将 <br> 转换为 <br/>,将 <hr> 转换为 <hr/>,以防止生成 PDF 功能不识别这类标签;
    4. 删除 <o:p> 标签。Office 专属标签,浏览器不识别,故删除;
    5. 将包含 name="OLE_LINK"<a> 标签替换为 <span> 标签 。这种 <a> 标签不是正常编写的链接,而是从 Word 粘贴时自动添加的,故删除;
    6. 对编辑器内容进行 HTML 压缩。以减少生成 PDF 功能因为空格和空行生成出错的可能;
  2. 导出格式化操作纯函数,支持在其他模块中单独调用。

插件具体实现如下:

ts 复制代码
 /*
  * Jodit Editor (https://xdsoft.net/jodit/)
  * Released under MIT see LICENSE.txt in the project root for license information.
  * Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
  */
 ​
 /**
  * Word 内容处理插件
  * 提供从 Word 粘贴内容的处理和自定义格式化功能
  * @packageDocumentation
  * @module plugins/word-content-processor
  */
 ​
 import { INSERT_AS_HTML, INSERT_AS_TEXT, INSERT_ONLY_TEXT } from 'jodit/esm/core/constants';
 import { applyStyles, cleanFromWord, isHtmlFromWord, isString, stripTags } from 'jodit/esm/core/helpers';
 import { Plugin } from 'jodit/esm/core/plugin';
 import type { PastedData, PasteEvent } from 'jodit/esm/plugins/paste/interface';
 import type { IJodit, InsertMode } from 'jodit/esm/types';
 ​
 import { askInsertTypeDialog, pasteInsertHtml } from 'jodit/esm/plugins/paste/helpers';
 import { minify } from 'html-minifier-terser';
 import { watch } from 'jodit/esm/core/decorators';
 ​
 /** 格式化 html 字符串方法 */
 export const formatHtmlFromWord = async (htmlStr: string) => {
     // ...
 };
 ​
 /** 配置类型 */
 type Config = {
   /** 是否在 onBlur 事件中调用 formatHtmlFromWord 方法处理粘贴的内容 */
   isFormatHtmlFromWordOnBlur?: boolean;
   /** 格式化从 word 中复制的内容的逻辑 */
   formatHtmlFromWord?: (html: string) => Promise<string> | string;
 };
 ​
 /** 默认配置 */
 const defaultConfig: Config = {
   isFormatHtmlFromWordOnBlur: true,
   formatHtmlFromWord,
 };
 ​
 /**
  * Word 内容处理插件的工厂函数
  *
  * 基于原 paste-from-word 插件进行改造,使用该插件时需要**禁用 paste-from-word 插件**,否则会出现粘贴两次的情况。
  *
  * 本插件增加了以下功能:
  * 1. 处理从 Word 粘贴的内容
  * 2. 提供自定义的 Word 内容格式化逻辑
  */
 export const createWordContentProcessor = (config: Config = {}) => {
   /** 归一化后的配置 */
   const normalizedConfig = { ...defaultConfig, ...config };
   class WordContentProcessor extends Plugin {
     static override requires = ['paste'];
 ​
     init(jodit: IJodit): void {
       normalizedConfig.isFormatHtmlFromWordOnBlur &&
         jodit.events.on('blur', async () => {
           const content = jodit.getEditorValue();
           const formattedHtml = await formatHtmlFromWord(content);
           jodit.setEditorValue(formattedHtml);
         });
     }
 ​
     protected override afterInit(jodit: IJodit) {}
     protected override beforeDestruct(jodit: IJodit) {}
 ​
     /**
      * 处理来自 Word 的 HTML 内容
      * @param e - 粘贴事件对象
      * @param text - 被粘贴的文本内容
      * @param texts - 粘贴数据的详细内容
      * @returns 是否处理了 Word 的 HTML 内容
      */
     @watch([':processHTML'])
     protected async processWordHTML(e: PasteEvent, text: string, texts: PastedData) {
       const { jodit } = this;
       const { processPasteFromWord, askBeforePasteFromWord, defaultActionOnPasteFromWord, defaultActionOnPaste, pasteFromWordActionList } = jodit.options;
 ​
       if (processPasteFromWord && isHtmlFromWord(text)) {
         if (askBeforePasteFromWord) {
           askInsertTypeDialog(
             jodit,
             'The pasted content is coming from a Microsoft Word/Excel document. ' + 'Do you want to keep the format or clean it up?',
             'Word Paste Detected',
             async (insertType) => {
               await this.insertFromWordByType(e, text, insertType, texts);
             },
             pasteFromWordActionList
           );
         } else {
           await this.insertFromWordByType(e, text, defaultActionOnPasteFromWord || defaultActionOnPaste, texts);
         }
 ​
         return true;
       }
 ​
       return false;
     }
 ​
     /**
      * 根据插入模式清理 Word 内容的额外样式和标签
      */
     protected async insertFromWordByType(e: PasteEvent, html: string, insertType: InsertMode, texts: PastedData): Promise<void> {
       switch (insertType) {
         case INSERT_AS_HTML: {
           html = applyStyles(html);
 ​
           // 先和原插件保持一致,先经过 beautifyHTML 事件处理
           const value = this.j.events?.fire('beautifyHTML', html);
 ​
           // 当 config.formatHtmlFromWord 存在时,使用 config.formatHtmlFromWord 处理,否则使用默认的 formatHtmlFromWord 处理
           /** 经过pasteFromWord事件处理后的值 */
           const afterPasteFromWord = await normalizedConfig.formatHtmlFromWord?.(value);
 ​
           html = afterPasteFromWord || '';
 ​
           break;
         }
 ​
         case INSERT_AS_TEXT: {
           html = cleanFromWord(html);
           break;
         }
 ​
         case INSERT_ONLY_TEXT: {
           html = stripTags(cleanFromWord(html));
           break;
         }
       }
 ​
       pasteInsertHtml(e, this.j, html);
     }
   }
 ​
   return WordContentProcessor;
 };

其中的核心 formatHtmlFromWord() 如下:

ts 复制代码
 /** 格式化 html 字符串方法 */
 export const formatHtmlFromWord = async (htmlStr: string) => {
   /** 一个临时 div 元素,用来对 html 进行处理 */
   const temp = document.createElement('div');
   temp.innerHTML = htmlStr;
   temp.querySelectorAll('[style]').forEach((el) => {
     const element = el as HTMLElement;
 ​
     // 如果 font-family 不是 Wingdings 则将 font-family 设置为 initial
     // 因为 Wingdings 是 word 中默认的符号字体,在生成无序列表时,会使用这种字体作为标记,所以不能修改
     // 而之所以不使用 removeProperty 方法,是因为可能出现继承自父元素的 font-family 属性的情况,所以需要使用 initial 来重置
     if (element.style.fontFamily !== 'Wingdings') {
       element.style.fontFamily = 'initial';
     }
 ​
     const styles = element.getAttribute('style');
     // 因为 mso- 开头的属性不是css标准中的属性,所以不能使用 style.removeProperty 方法去除
     const cleanedStyles = styles
       // 清除 mso- 开头的属性
       ?.replace(/mso-.+?\s*:\s*[^;]+;?/gi, '')
       // 清除多余的 ;
       .replace(/;;+/g, ';');
     // 如果 cleanedStyles 为空,则删除 style 属性
     if (cleanedStyles) {
       element.setAttribute('style', cleanedStyles);
     } else {
       element.removeAttribute('style');
     }
   });
 ​
   // 处理 <br> 标签
   const brs = temp.querySelectorAll('br');
   brs.forEach((br) => {
     br.outerHTML = '<br/>';
   });
 ​
   // 处理 <hr> 标签
   const hrs = temp.querySelectorAll('hr');
   hrs.forEach((hr) => {
     hr.outerHTML = '<hr/>';
   });
 ​
   // 删除<o:p>标签
   const oP = temp.querySelectorAll('o\:p');
   oP.forEach((oP) => {
     oP.remove();
   });
 ​
   /**
    * 将包含 name="OLE_LINK" 的 <a> 标签替换为 <span> 标签的方法
    *
    * 因为这种 <a> 标签不是正常编写的链接,而是从 word 粘贴时自动添加的
    */
   const replaceAbnormalAWithSpan = (html: HTMLElement) => {
     const aTags = html.querySelectorAll('a[name^="OLE_LINK"]');
 ​
     aTags.forEach((a) => {
       const span = document.createElement('span');
       // 将 a 标签的内容转换为纯文本
       span.innerHTML = a.innerHTML;
       a.replaceWith(span);
     });
   };
 ​
   replaceAbnormalAWithSpan(temp);
 ​
   /** 完成替换后的 html 字符串 */
   const replacedHtml = temp.innerHTML.replace(/&nbsp;/gi, '&#160;');
 ​
   /** 压缩后的 HTML 字符串 */
   const minifiedHtml = await minify(replacedHtml);
 ​
   return minifiedHtml;
 };

使用方法:

ts 复制代码
 import { createWordContentProcessor } from '@/lib/jodit/plugins/word-content-processor';
 ​
 Jodit.plugins.add(
   'creditDataSource',
   createWordContentProcessor(
     // 可传递配置项,以自定义插件行为
     // {
     // isFormatHtmlFromWordOnBlur: true,
     // formatHtmlFromWord: (content: string) => {
     //   return content;
     // },
     // }
 ));

该插件实现的核心在于以下几点:

  1. 以原生插件 paste-from-word 为基础进行改造;
  2. 通过 "jodit/esm/*" 来导入各种 Jodit API;
  3. 将原 processWordHTML() 方法改为异步,以支持对内容的异步处理 (即示例中的 formatHtmlFromWord() 方法 );
  4. 使用 init() 生命周期 钩子挂载 blur 事件;
  5. 使用工厂函数传递配置 ,而不是访问 config 属性,增加代码内聚性。未来不需要使用该插件时,只需要删除工厂函数执行代码即可,不用关心 config 内容;

formatHtmlFromWord() 方法的核心包括:

  1. 创建临时 div 元素 处理 HTML 字符串,以使用 dom 相关 API
  2. 使用 el.outerHTML = 语句覆盖原 HTML 元素
  3. 使用 el.remove() 方法删除元素
  4. 使用 html-minifier-terser 这个 npm 包对 HTML 进行压缩。

总结------如何对第三库进行选型、学习和开发

基于以上内容和实际操作过程中思考,可以总结出以下针对第三方库的选型、学习和开发经验:

  1. 选型阶段根据项目自身情况进行快速筛选,一般主要考察以下几点:

    1. 商业使用费用
    2. 开发难度 。通过实现简单原型或查看文档中的教程,判断上手开发速度是否够快,如果快速上手开发困难,后续开发难度一般也不低
    3. 可扩展性。即第三方库设计是否合理,是否支持简单的功能扩展方案,因为一般业务需求都涉及到对已有功能的深度定制,如果第三方库没有提供底层的接口,则会导致后续开发中出现困难;
    4. 可维护性。包括是否有 TS 代码提示和类型检查,是否有详尽的文档,是否有活跃的社区解决各种问题,是否有未压缩、混淆的源码用来进行开发参考。
  2. 再到开发阶段,则可以按照以下思路进行学习和开发:

    1. 以官方文档为基础。其他任何资料都可能出现过时和错误的情况,只有官方文档是可以完全信任的;
    2. 以写代看,边写边学。和任何提供了文档的项目都一样,尽快动手开发能学得更快,遇到问题再回来翻文档。因为整个文档的内容通常非常多,全部看完既浪费时间,也记不住这么多;
    3. 如果文档没有详细说明,就去翻源码。像是 Jodit Editor 这种有商用版本的库,其官方文档不会特别详细(比如 Plugin 系统就只有一页说明),这时就需要去查看源码;
    4. 源码不要看底层原理,而应该看类似功能如何实现。比如要实现一个 Jodit Editor 的插件,就应该找个类似功能的插件,看它是如何实现的;
    5. 善用 TS 代码提示和类型检查。在开发时严格遵守第三方库的类型检查,可以减少开发时的问题,同时可以结合相关的信息更好地推断 API 的功能和用法;

相比 Jodit Editor 的代码是如何写的,我认为这些经验才是更有价值的东西,因为这些是不受具体的技术限制的,能够跨越不同应用场景的底层方法论

相关推荐
用户8168694747251 小时前
React 如何用 MessageChannel 模拟 requestIdleCallback
前端·react.js
heyCHEEMS1 小时前
手搓 uniapp vue3 虚拟列表遇到的坑
前端
Duck不必1 小时前
紧急插播:CVSS 10.0 满分漏洞!你的 Next.js 项目可能正在裸奔
前端·next.js
幸运小圣1 小时前
动态组件【vue3实战详解】
前端·javascript·vue.js·typescript
用户413079810611 小时前
终于不漏了-Android开发内存泄漏详解
前端
孟祥_成都1 小时前
nest.js / hono.js 一起学!hono的设计思想!
前端·node.js
努力glow .1 小时前
彻底解决VMware下ROS2中gazebo启动失败的问题
前端·chrome
阿笑带你学前端1 小时前
开源记账 App 一个月迭代:从 v1.11 到 v2.2,暗黑模式、标签系统、预算管理全面升级
前端
AAA阿giao1 小时前
浏览器底层探秘:Chrome的奇妙世界
前端·chrome·gpu·多进程·单进程·v8引擎·浏览器底层