论前端第三方库的技术选型 ------ 以 Jodit Editor 为例
近期对一个后台项目的富文本编辑器 进行了一次技术升级,这一过程中涉及到前期的技术选型 和实际落地的配置和使用,在这一过程中颇有收获,随决定成文记录,汇总经验。
问题背景
该项目是一个内部管理后台,不对外开放,富文本编辑器功能的使用频率并不高,只是偶尔用来编辑一下文件内容,对复杂格式没有要求。
虽然没什么复杂需求,原来使用的 wangEditor 4富文本编辑器还是有诸多不足之处:
- 选定文字进行修改格式时,文字的选中状态会消失;
- Word 内容粘贴后格式有问题;
- 无法以源代码模式编辑内容;
- 不好进行定制化开发;
同时考虑到后续可能还需要进一步开发相关功能,决定对富文本编辑器进行更换。
技术选型
在自己开始找新的富文本编辑器前,首先是咨询了公司的大前端团队,看看是否有已经采购的富文本编辑器,或成熟的解决方案。如果有,那就和公司保持统一,避免重复造轮子。
不过结论是并没有,所以只能自己开始做富文本编辑器的技术选型。
基于使用场景,确定富文本编辑器需要满足以下需求:
- 能够免费商用,避免可能的法律问题,同时因为是后台项目中简单使用的,就不考虑走采购流程了;
- 支持粘贴 Word 内容时保留大部分格式;
- 能够覆盖旧编辑器的所有功能;
- 支持功能扩展;
- 学习成本低、文档完善,便于后续维护。
富文本编辑器待选列表主要参考掘金的文章------《富文本选型太难了,谁来帮帮我!》,同时准备了一个Word 文档测试用例,用来快速测试各富文本编辑器对 Word 格式的兼容情况。
最后测试结果如下:
| 富文本编辑器 | 免费商用 | Word 内容保留格式 | 功能齐全 | 功能扩展 | 易维护性 |
|---|---|---|---|---|---|
| TinyMCE | ❌ 免费版不支持自托管 | ||||
| Quill | ❌ 免费版不支持自托管 | ||||
| TinyMCE | ✅ | ✅ | ❌没有默认的功能菜单,需要通过组件系统自行搭建,过于复杂 | ||
| Editor.js | ✅ | ❌ | |||
| Slate | ✅ | ❌ | |||
| lexical | ✅ | ✅ | ❌没有默认的功能菜单,需要自行封装搭建 | ✅ | ❌尚未推出1.0 正式版,且各种概念非常复杂,可能出现难以解决的问题 |
| Jodit Editor | ✅ | ✅ | ✅ | ✅ | ✅有基础的文档,同时免费版的仓库是公开的,可以参考其中的源码开发插件 |
从这里可以看出,这次技术选型时并没有对每个技术栈都进行了详尽的调研 ,而是只要稍微不符合条件就直接排除。这样做是为了加快开发效率,毕竟这次技术选型只针对这一个项目,没必要把所有技术栈的优劣都分析清楚。
Jodit Editor 的配置和使用
确定富文本编辑器使用的技术后,接下来就是正式使用了。主要参考资料包括:
- Jodit Editor 官方文档:xdsoft.net/jodit/docs/
- Jodit Editor Playground(生成配置):xdsoft.net/jodit/play....
- Jodit Editor 免费版源码仓库:github.com/xdan/jodit
- Jodit React 集成:github.com/jodit/jodit...
Jodit Editor 学习和开发思路
由于之前没有使用 Jodit Editor 的经验,也没有已有的项目可以直接参考,所以需要边学习边开发。
以下是主要的学习和开发思路:
- 基础配置和开发方法,参考官方文档 和 Playground;
- 其次就是直接使用搜索引擎找解决方法;
- 如果以下两种方法都解决不了问题,就需要去翻阅源码 ,找类似的例子去参考(比如插件的开发);
- 除了以上这些,开发时参考 TS 的代码提示和类型检查也可以辅助判断。
Jodit Editor 基本概念
依照以上方法进行开发,可以快速对 Jodit Editor 建立起以下基本概念:
- 通过
config参数 可以对编辑器各方面进行定制化,包括样式、工具栏按钮、禁用的插件等,详见 xdsoft.net/jodit/docs/... ; - Jodit Editor 默认内置了各种插件, 可以通过
config.disablePlugins参数禁用; - 内置的插件可以定制化 ,定制的方式是设置
config中的各项参数; - Jodit React 提供了通过
ref属性获取编辑器实例的 API,可以用来对编辑器进行各种操作; - 当已有配置项无法满足需求时,往往需要开发插件来进行深度定制化 ,插件的扩展自 Jodit Editor 提供的
Plugin类,可以注册各个生命周期 事件,并且通过这个类暴露的 API,以及直接访问编辑器实例 ,从而进行各种定制化,详见 xdsoft.net/jodit/docs/...。
编辑器基础封装
遵循官方文档说明,同时参考旧版编辑器的功能配置,创建以下暴露 value 和 onChange 接口的受控组件:
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} />;
};
这一版本的基础封装主要参考了以下内容:
- Jodit Editor Playground:生成配置和测试各配置项、插件对应的功能;
- Justify 插件文档:查看对齐按钮相关说明;
- Indent 插件文档:查看 indent按钮组相关说明;
- 按钮系统文档:查看新增按钮相关说明。
关闭工具栏响应式变化
参考 mobile 插件文档,设置 toolbarAdaptive 为 false,防止因为宽度变小导致按钮工具栏按钮被折叠。
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 组件本质上是一个渲染函数 ,并不能直接返回后渲染到弹窗中。
所以逻辑应该为:
popup属性返回一个目标 dom 元素挂载在弹窗元素中;- 触发组件函数执行,使用
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;
这一整套逻辑的核心在于以下几点:
- 利用
useRef()缓存弹窗容器 dom 元素; - 利用
createPortal()将组件渲染到弹窗容器 dom 元素中; - 利用创建一个 state ,用来触发
<Select/>这个组件重新渲染。
Word 内容处理插件
Jodit editor 默认的内部处理逻辑已经可以保留几乎所有的 Word 格式,但是如果还需要对粘贴自 Word 的内容进行定制化处理,则需要自行制作插件。
该插件实现以下功能:
-
在粘贴来自 Word 的内容和编辑器失去焦点时,执行以下格式化操作:
- 清除字体样式。部分 Word 文档会使用特殊字体,但实际最终生成 PDF 时默认使用的是宋体,清除以防止编辑器效果和 PDF 效果有冲突(其实设置根元素字体为宋体会更好);
- 清除
mso-开头的 CSS 属性。这些属性是 Office 专用的,浏览器不识别,故清除; - 转换
<br>和<hr>标签 。将<br>转换为<br/>,将<hr>转换为<hr/>,以防止生成 PDF 功能不识别这类标签; - 删除
<o:p>标签。Office 专属标签,浏览器不识别,故删除; - 将包含
name="OLE_LINK"的<a>标签替换为<span>标签 。这种<a>标签不是正常编写的链接,而是从 Word 粘贴时自动添加的,故删除; - 对编辑器内容进行 HTML 压缩。以减少生成 PDF 功能因为空格和空行生成出错的可能;
-
导出格式化操作纯函数,支持在其他模块中单独调用。
插件具体实现如下:
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(/ /gi, ' ');
/** 压缩后的 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;
// },
// }
));
该插件实现的核心在于以下几点:
- 以原生插件 paste-from-word 为基础进行改造;
- 通过
"jodit/esm/*"来导入各种 Jodit API; - 将原
processWordHTML()方法改为异步,以支持对内容的异步处理 (即示例中的formatHtmlFromWord()方法 ); - 使用
init()生命周期 钩子挂载blur事件; - 使用工厂函数传递配置 ,而不是访问
config属性,增加代码内聚性。未来不需要使用该插件时,只需要删除工厂函数执行代码即可,不用关心config内容;
formatHtmlFromWord() 方法的核心包括:
- 创建临时 div 元素 处理 HTML 字符串,以使用 dom 相关 API;
- 使用
el.outerHTML =语句覆盖原 HTML 元素; - 使用
el.remove()方法删除元素; - 使用
html-minifier-terser这个 npm 包对 HTML 进行压缩。
总结------如何对第三库进行选型、学习和开发
基于以上内容和实际操作过程中思考,可以总结出以下针对第三方库的选型、学习和开发经验:
-
选型阶段根据项目自身情况进行快速筛选,一般主要考察以下几点:
- 商业使用费用;
- 开发难度 。通过实现简单原型或查看文档中的教程,判断上手开发速度是否够快,如果快速上手开发困难,后续开发难度一般也不低;
- 可扩展性。即第三方库设计是否合理,是否支持简单的功能扩展方案,因为一般业务需求都涉及到对已有功能的深度定制,如果第三方库没有提供底层的接口,则会导致后续开发中出现困难;
- 可维护性。包括是否有 TS 代码提示和类型检查,是否有详尽的文档,是否有活跃的社区解决各种问题,是否有未压缩、混淆的源码用来进行开发参考。
-
再到开发阶段,则可以按照以下思路进行学习和开发:
- 以官方文档为基础。其他任何资料都可能出现过时和错误的情况,只有官方文档是可以完全信任的;
- 以写代看,边写边学。和任何提供了文档的项目都一样,尽快动手开发能学得更快,遇到问题再回来翻文档。因为整个文档的内容通常非常多,全部看完既浪费时间,也记不住这么多;
- 如果文档没有详细说明,就去翻源码。像是 Jodit Editor 这种有商用版本的库,其官方文档不会特别详细(比如 Plugin 系统就只有一页说明),这时就需要去查看源码;
- 源码不要看底层原理,而应该看类似功能如何实现。比如要实现一个 Jodit Editor 的插件,就应该找个类似功能的插件,看它是如何实现的;
- 善用 TS 代码提示和类型检查。在开发时严格遵守第三方库的类型检查,可以减少开发时的问题,同时可以结合相关的信息更好地推断 API 的功能和用法;
相比 Jodit Editor 的代码是如何写的,我认为这些经验才是更有价值的东西,因为这些是不受具体的技术限制的,能够跨越不同应用场景的底层方法论。