Skip to content

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

编辑器基础封装

遵循官方文档说明,同时参考旧版编辑器的功能配置,创建以下暴露 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 组件的按钮

效果演示:

![Kapture 2025-12-05 at 20.14.48](how-to-choose-rich-text-editor.assets/Kapture 2025-12-05 at 20.14.48.gif)

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

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

所以逻辑应该为:

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

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

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

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 的代码是如何写的,我认为这些经验才是更有价值的东西,因为这些是不受具体的技术限制的,能够跨越不同应用场景的底层方法论