
想在一套 Web 系统里做出“能让化学家愿意用”的结构绘制功能并不是随便放一块画布就行的。真正的痛点在于结构要画得快、画得准还要能转成 SMILES、MOL 等标准数据交给后端。今天这篇文章我们来完整拆解一款化学结构编辑器的集成思路并给出可运行的 React 实战示例。很多开发者第一次接触化学结构编辑器时第一反应是“这不就是个画板吗能画线条、能拖拽原子不就够了”。实际上化学结构编辑器处理的信息远比图形复杂每个原子是什么元素、每个键是单键还是双键、芳香环怎么识别、立体化学怎么记录、分子式怎么计算——这些都需要编辑器在底层完成语义建模。用户画出来的每一笔最终要落成标准化学结构数据才能用于后续的性质预测、数据库检索或 AI 分子生成。这篇文章会从基础概念讲起逐步带你在 React 项目中集成一款开源化学结构编辑器实现“画结构 → 生成 SMILES → 提交后端”的完整链路。整个过程会包含组件封装、事件监听、常见报错排查和工程化建议新手可以照着做有经验的开发者也能直接复用代码思路。1. 背景与核心概念1.1 什么是化学结构编辑器化学结构编辑器是一种专门用于绘制二维化学分子结构图的交互工具。它不是一个通用的画图小工具而是一套具备化学语义的编辑器。你在画布上画出一个苯环编辑器不仅能显示六边形还能识别这是六个碳原子构成的芳香环你画一条双键编辑器知道这是双键而非两条平行线。从这个角度看化学结构编辑器更像是“带化学智能的矢量绘图工具”。它要处理的元素包括原子C、N、O、S、P、卤素以及各种金属原子。键单键、双键、三键、芳香键、楔形键用于表示立体化学。环结构苯环、环己烷、杂环等常见骨架。官能团羟基、羧基、氨基、硝基、磺酸基等。电荷与同位素正负电荷、同位素标记。反应箭头与条件绘制化学反应方程式时会用到。用户画出的结构在编辑器内部会被转换为标准的数据表示。不同编辑器转换能力不同但最基本的要求是能够输出标准格式让其他化学软件能读懂。1.2 为什么说“自然”很关键一个编辑器的绘制体验是否“自然”直接影响化学家的使用意愿。传统编辑器如果想要手动绘制一个复杂分子可能需要逐个原子放置、逐个键连接效率很低。而现代编辑器会提供大量辅助功能让绘制过程更接近直觉自动补全原子价态例如画一个碳原子并连出三根键时编辑器会自动补足氢原子。智能键类型识别连续点击两个原子时会根据原子之间的距离和位置自动判断键级。常用模板库内置苯环、环己烷、氨基酸、核苷酸等常用骨架一键拖入。快捷键与搜索输入“苯”或“benzene”能直接定位到模板。结构清洗画完后点击清洗编辑器会自动规整键长、键角和原子间距。这些能力综合起来就是“自然绘制”的含义。它让用户从“画图”中解放出来把注意力放在化学问题本身。对于药物化学、天然产物研究、化学教学场景来说这种体验带来的效率提升非常明显。1.3 编辑器输出什么数据当我们谈论“输出数据”时最常见的三种化学结构格式需要先了解清楚。SMILES 是一种用 ASCII 字符串描述分子结构的线性表示法。比如乙醇可以表示为CCO苯环可以表示为c1ccccc1。SMILES 简单、紧凑非常适合在 API 间传递结构信息。MOL 文件是 MDL Molfile 格式它用文本块描述原子坐标、键连接关系和属性信息比 SMILES 更完整适合保存 2D 坐标和 3D 坐标。SDF 是 MOL 文件的集合扩展可以在一个文件里保存多个分子结构并附带键值对属性在化学数据库和虚拟筛选中使用非常广泛。一个合格的化学结构编辑器至少要能在这几种格式之间进行转换。这也是后续集成时最需要关注的能力之一。2. 核心技术选型开源编辑器怎么选2.1 常见开源方案比较在 Web 应用中选择化学结构编辑器目前主流开源方案主要有三个。Ketcher 是 EPAM 开发的开源 Web 化学结构编辑器功能全面支持原子、键、环、官能团、反应绘制、立体化学、SMILES/MOL 导入导出也有 React 友好的集成方式。这是目前社区活跃度最高、文档相对完善的方案之一。JSME 是老牌的 JavaScript 化学结构编辑器由 Peter Ertl 开发轻量、体积小、加载快适合场景简单的页面。它支持 SMILES 导入导出但界面和扩展能力相对有限。ChemDoodle Web Components 功能强大、界面美观但需要注意它的授权模式。商业项目中是否免费取决于使用规模和场景集成前需要先确认授权条款。综合来看如果你需要一套既能满足日常绘制、又方便二次开发的编辑器Ketcher 是更稳妥的选择。它支持独立的 Standalone 版本也可以嵌入到 React、Angular 等框架中。2.2 Ketcher 的工作流程Ketcher 的工作流程可以分为三层理解。最上层是画布交互层负责鼠标事件、键盘事件、模板拖拽、原子标签输入。用户在这个层面只感知到“画得好不好看、顺不顺手”。中间是化学语义层负责把图形结构解析为原子、键、环、官能团等化学对象并维护原子价态、芳香性、立体化学等信息。最底层是数据转换层负责与 SMILES、MOL、SDF、InChI 等格式互转并提供getSmiles()、getMolfile()等接口给外部调用。我们在做前端集成时重点就是操作最底层的数据接口。初始化编辑器、载入结构、获取结构、监听变更事件都是围绕这几个核心方法展开。3. 环境准备与项目初始化3.1 环境依赖本文示例以 React 项目为基础需要提前准备以下环境Node.js 16 或更高版本版本需要根据你的项目实际情况调整。一个可用的包管理器npm 或 yarn 均可。浏览器建议使用 Chrome 或 Edge 最新版本。Ketcher 的引用方式有两种。一种是通过 npm 包引入另一种是使用官方独立构建脚本。因为 Ketcher 的包名和目录结构随版本更新变化较快本文采用独立脚本 React 组件封装的方式这样即使你使用的是不同版本整体思路也完全一致。3.2 创建 React 项目如果你还没有现成的 React 项目可以先快速创建一个npx create-react-app chem-editor-demo cd chem-editor-demo npm start启动成功后浏览器会打开默认的 React 页面。接下来我们就在这个项目里加入化学结构编辑器。如果你使用的是其他构建工具比如 Vite 或 Umi核心代码是通用的直接把编辑器组件放到对应目录即可。3.3 项目结构规划为了让代码更清晰我们把编辑器相关代码单独抽成一个组件目录结构如下src/ ├── components/ │ ├── ChemEditor/ │ │ ├── index.js # 编辑器组件入口 │ │ ├── ChemEditor.js # 封装 Ketcher 的核心逻辑 │ │ └── ChemEditor.css # 画布样式调整 │ └── ResultPanel/ │ └── index.js # 展示 SMILES 和 MOL 结果的组件 ├── App.js # 页面主入口 └── index.js在实际项目中你还可以把编辑器组件发布到公司内部的组件库这样不同业务线可以直接复用。4. 在 React 中嵌入化学结构编辑器4.1 创建编辑器容器先创建一个组件文件用于承载 Ketcher 的画布。我们把画布放在一个固定高度的div中避免布局抖动。// 文件路径src/components/ChemEditor/ChemEditor.js import React, { useEffect, useRef } from react; import ./ChemEditor.css; function ChemEditor({ onChange }) { const containerRef useRef(null); const ketcherRef useRef(null); useEffect(() { // 初始化 Ketcher 的逻辑在下一节补充 }, []); return ( div classNamechem-editor-container div ref{containerRef} classNamechem-editor-canvas / /div ); } export default ChemEditor;这里的思路是通过containerRef拿到画布挂载节点再把 Ketcher 实例创建到这个节点上。ketcherRef用来保存实例后续调用方法时需要用到。4.2 初始化编辑器初始化时我们需要把 Ketcher 的全局对象挂载到指定节点。以独立脚本方式为例初始化代码大致如下useEffect(() { const initKetcher async () { if (!window.ketcher || !containerRef.current) { return; } const options { staticResourcesUrl: /static/ketcher/, }; const ketcher new window.ketcher(containerRef.current, options); ketcherRef.current ketcher; }; initKetcher(); }, []);这里有一点需要特别注意Ketcher 需要从服务器加载资源文件包括字体、图标和模板数据所以staticResourcesUrl要指向你实际部署静态资源的位置。如果资源路径配错画布能出来但图标和模板会显示异常。为了让初始化更稳定我们还可以在脚本加载完成后回调中再执行初始化。4.3 加载脚本文件在public/index.html中引入独立脚本。不同的 Ketcher 版本脚本名会不一样这里以常见发布包为例!-- 文件路径public/index.html -- script src/static/ketcher/ketcher-standalone.min.js/script引入后在浏览器刷新页面时全局会多出window.ketcher对象。注意需要确认脚本确实加载成功可以在浏览器控制台打印window.ketcher检查。4.4 载入 SMILES 结构很多业务场景需要把数据库里的结构显示到画布上。例如用户检索出一个分子希望在编辑器中展示并继续修改。这个时候可以使用setMolecule接口。const loadSmiles async (smiles) { const ketcher ketcherRef.current; if (!ketcher || !smiles) { return; } await ketcher.setMolecule(smiles); };加载成功后画布上会显示对应的分子结构。这里要注意SMILES 字符串必须合法否则编辑器会抛异常。实际项目中建议在调用前先做一个基础格式校验。4.5 获取当前结构用户画完结构后我们需要把结构数据提取出来转换成 SMILES 或 MOL 格式提交给后端。核心方法是getSmiles()和getMolfile()。const getStructureData async () { const ketcher ketcherRef.current; if (!ketcher) { return null; } const smiles await ketcher.getSmiles(); const molfile await ketcher.getMolfile(); return { smiles, molfile, }; };这里有几个细节值得注意。第一这两个方法都是异步的返回的是 Promise需要await获取结果。第二getSmiles()得到的字符串可能包含换行符和空格提交给后端前要做好清理。第三如果画布是空的不同版本的 Ketcher 返回结果可能不一样需要做空值判断。4.6 监听画布变更在实际业务中我们希望用户一画完结构界面右侧就能立刻更新对应的 SMILES 和分子式这需要监听画布变更事件。Ketcher 支持结构变更回调const handleChange async () { const ketcher ketcherRef.current; if (!ketcher) { return; } try { const smiles await ketcher.getSmiles(); onChange?.(smiles); } catch (error) { console.error(获取结构失败, error); } };初始化时把handleChange注册为变更事件回调。每次用户增删原子、修改键类型、移动结构都会触发这个回调。这样前端可以实时展示最新结果。需要注意的是因为每次按键或拖拽都会触发回调如果回调里做了重量级计算页面会变得非常卡。建议在回调里做轻量操作比如格式转换和展示重量级计算放到后端。5. 完整实战分子录入与结果展示5.1 需求说明现在我们把前面所有功能整合成一个完整的实战案例页面上左边是化学结构编辑器右边实时显示 SMILES 和 MOL 数据下方提供一个“复制结果”按钮方便用户把结构数据拿去做后续查询。组件拆分如下ChemEditor封装编辑器初始化、结构变更监听、SMILES 导入。ResultPanel展示 SMILES、MOL、分子式信息。App负责组装两个组件维护整体状态。5.2 编辑器组件完整代码// 文件路径src/components/ChemEditor/ChemEditor.js import React, { useEffect, useRef } from react; import ./ChemEditor.css; function ChemEditor({ onChange, onReady }) { const containerRef useRef(null); const ketcherRef useRef(null); useEffect(() { let mounted true; const init async () { try { await loadScript(/static/ketcher/ketcher-standalone.min.js); if (!mounted || !window.ketcher || !containerRef.current) { return; } const ketcher new window.ketcher(containerRef.current, { staticResourcesUrl: /static/ketcher/, }); ketcherRef.current ketcher; ketcher.editor.subscribe(change, async () { if (!ketcherRef.current) { return; } try { const smiles await ketcherRef.current.getSmiles(); const molfile await ketcherRef.current.getMolfile(); onChange?.({ smiles, molfile }); } catch (error) { console.error(结构读取失败, error); } }); onReady?.(ketcher); } catch (error) { console.error(Ketcher 初始化失败, error); } }; init(); return () { mounted false; }; }, []); const loadSmiles async (smiles) { const ketcher ketcherRef.current; if (!ketcher || !smiles) { return; } await ketcher.setMolecule(smiles); }; return ( div classNamechem-editor-container div ref{containerRef} classNamechem-editor-canvas / /div ); } function loadScript(src) { return new Promise((resolve, reject) { if (document.querySelector(script[src${src}])) { resolve(); return; } const script document.createElement(script); script.src src; script.onload resolve; script.onerror () reject(new Error(脚本加载失败: ${src})); document.body.appendChild(script); }); } export default ChemEditor;这里我加了动态加载脚本的逻辑避免在 HTML 里手动引脚本。这样组件更独立别人拿去用的时候不需要额外配置。另外需要注意ketcher.editor.subscribe(change, callback)这种 API 在部分版本中可用如果你使用的 Ketcher 版本不支持订阅事件可以改用一个折中方案在按钮点击时统一获取结果而不是实时监听。核心数据结构是一样的。5.3 结果展示组件完整代码// 文件路径src/components/ResultPanel/index.js import React from react; function ResultPanel({ smiles, molfile }) { const handleCopy () { const text SMILES:\n${smiles}\n\nMOLFILE:\n${molfile}; navigator.clipboard.writeText(text); }; return ( div classNameresult-panel h3结构数据/h3 div classNameresult-item labelSMILES/label textarea readOnly value{smiles || } rows{4} placeholder绘制结构后自动生成 / /div div classNameresult-item labelMOL/label textarea readOnly value{molfile || } rows{12} placeholder绘制结构后自动生成 / /div button onClick{handleCopy}复制全部结果/button /div ); } export default ResultPanel;这里用readOnly的textarea展示数据方便用户直接选中复制。实际项目中可以把这些数据直接提交给接口而不是让用户手动复制。5.4 主页面组装// 文件路径src/App.js import React, { useState } from react; import ChemEditor from ./components/ChemEditor/ChemEditor; import ResultPanel from ./components/ResultPanel; function App() { const [structure, setStructure] useState({ smiles: , molfile: , }); const handleChange (data) { setStructure(data); }; return ( div classNameapp h1化学结构编辑器实战演示/h1 div classNameapp-layout div classNameeditor-area ChemEditor onChange{handleChange} / /div div classNameresult-area ResultPanel smiles{structure.smiles} molfile{structure.molfile} / /div /div /div ); } export default App;5.5 页面样式/* 文件路径src/components/ChemEditor/ChemEditor.css */ .chem-editor-container { background: #fafafa; border: 1px solid #d9d9d9; border-radius: 8px; padding: 12px; } .chem-editor-canvas { width: 100%; height: 520px; } /* 文件路径src/App.css */ .app { max-width: 1200px; margin: 0 auto; padding: 24px; } .app-layout { display: flex; gap: 20px; margin-top: 20px; } .editor-area { flex: 1; min-width: 0; } .result-area { width: 380px; } .result-item { margin-bottom: 12px; } .result-item textarea { width: 100%; font-family: Courier New, monospace; font-size: 13px; padding: 8px; border: 1px solid #e0e0e0; border-radius: 4px; resize: vertical; }5.6 运行与验证启动项目后在画布上画一个苯环或者使用模板库拖入一个结构。此时右侧面板应该会实时出现对应的 SMILES 和 MOL 数据。例如画一个苯环SMILES 会生成类似c1ccccc1这样的字符串MOL 数据则包含原子坐标和连接信息。如果右侧数据没有更新可以按 F12 打开控制台重点看两类信息是否出现脚本加载失败或跨域报错。是否出现初始化异常堆栈。定位问题后再核对静态资源路径和 Ketcher 版本。6. 让绘制更自然的进阶配置6.1 模板库Ketcher 内置的模板库其实非常丰富但默认情况下这些模板入口比较隐蔽。如果你希望用户能快速找到某个特定结构可以在初始化时配置自定义模板。例如在业务里做一个“芳环抽屉”把常见的杂环结构放到一个独立面板中用户点击后直接进入画布。实现思路很简单点击模板按钮时根据模板的 SMILES 或 MOL 调用setMolecule或者用 Ketcher 提供的模板 API 插入到当前光标位置。虽然不同版本 API 存在差异但“点击模板 → 载入结构”这个思路是完全通用的。6.2 常用官能团快捷方式对于药物化学场景用户经常需要绘制苄基、叔丁氧羰基、甲磺酰基等官能团。把这些官能团做成快捷按钮或搜索条目可以显著提升绘制效率。例如在页面侧栏放一组可搜索的“常用基团”列表用户搜索“Boc”或“tBu”点击后就能插入对应结构。实现时可以把这些官能团的 SMILES 放在一个 JSON 文件里统一管理由前端渲染成列表。后续有新的基团需求直接改数据文件不需要改动逻辑。6.3 原子标签与同位素Ketcher 支持原子标签编辑。用户双击某个原子后可以修改元素类型、电荷、同位素等属性。在高校教学或标记实验场景中这个功能非常常用。如果业务需要限制用户操作范围比如只允许绘制有机元素可以在初始化配置中关闭部分元素或键类型具体配置项需要参考你所用版本的文档。6.4 结构清洗与坐标优化画完一个分子后由于拖动和自动连接原子的位置可能不够规整。Ketcher 一般会提供“清洗结构”功能自动调整键长、键角让整个分子看起来更接近教科书里的标准画法。在集成时建议把清洗按钮放在显眼位置很多化学家画完结构后第一件事就是点这个按钮。如果你的后端有 RDKit 或 Open Babel 服务也可以把键长优化、3D 构象生成放到后端处理前端只负责展示结果这样能获得更专业的坐标。7. 常见问题与排查思路7.1 常见报错速查表问题现象常见原因解决思路画布区域空白Ketcher 脚本未加载成功检查脚本 URL、网络请求、控制台报错图标和模板不显示staticResourcesUrl路径错误确认资源目录存在且路径正确初始化报错window.ketcher is undefined脚本未引入或版本不兼容确认脚本已加载检查全局变量名载入 SMILES 报错SMILES 字符串非法先用化学工具校验 SMILES或加 try/catch中文界面乱码资源文件没有包含中文字体检查部署资源是否完整清理浏览器缓存调用getSmiles()返回异常画布为空或结构不完整判断空值场景返回默认字符串页面卡顿变更事件回调里执行了重量级计算减少回调里的同步计算必要时防抖7.2 画布空白的排查步骤如果画布区域空白按下面的顺序排查第一步打开浏览器控制台看有没有红色的脚本加载错误。如果脚本 404说明静态资源路径不对。第二步在控制台执行window.ketcher看是否返回对象。如果返回undefined说明脚本没有在页面加载完成时执行。第三步检查初始化代码是否在 Ketcher 脚本加载完成后执行。如果是在 React 的useEffect中初始化需要确保脚本已经加载完毕否则会拿到空对象。第四步查看staticResourcesUrl是否有对应的资源文件。Ketcher 需要资源文件才能正常显示界面不能省略这一步。7.3 结构变更后数据不同步这个问题通常是事件订阅没有生效。不同版本的 Ketcher 事件机制不完全一致如果你使用的版本不支持editor.subscribe(change, callback)可以改成在关键按钮点击时统一获取数据。另一种可行方案是使用定时器轮询结构但这种方式比较消耗资源不推荐在生产环境使用。更好的做法是升级到新版本 Ketcher并按照官方文档调整订阅方式。8. 最佳实践与工程建议8.1 前后端结构校验前端编辑器输出的 SMILES 不一定百分之百准确尤其当用户画了一个价态异常的结构时不同编辑器处理结果会有差异。因此在后端接收 SMILES 后建议再次校验。如果后端使用 Python 生态可以引入 RDKit 对 SMILES 做标准校验和规范化from rdkit import Chem def validate_smiles(smiles: str): mol Chem.MolFromSmiles(smiles) if mol is None: raise ValueError(Invalid SMILES) canonical_smiles Chem.MolToSmiles(mol) return canonical_smiles把后端返回的规范化 SMILES 作为最终数据统一格式、统一标准。这个过程能过滤掉大量前端产生的脏数据。8.2 统一结构数据格式在系统内部传输时建议统一使用一种格式作为主数据。经验做法是前端与后端交互使用 SMILES。需要 2D 坐标展示时使用 MOL 文件或由后端通过 RDKit 重新生成坐标。需要批量存储时使用 SDF 文件。这样能避免不同格式之间频繁转换带来的信息损耗。8.3 版本锁定Ketcher 的版本更新较快不同版本的 API 有差异。前端工程中锁定版本非常重要不要把版本写成浮动版本。如果你通过静态资源引入建议把ketcher-standalone.min.js下载到项目本地而不是直接引用在线 CDN。这样既能避免外部资源不稳定也能保证每次构建的完全可控。8.4 性能优化结构变更回调要尽可能轻量。不要在回调里做分子式计算、模板渲染等重操作。如果业务比较复杂可以使用防抖策略用户停止绘制 500 毫秒后再统一获取结构并提交给后端降低调用频率。8.5 安全边界化学结构数据本身不涉及高危操作但也要注意两点。第一后端不能把前端传来的 SMILES 直接拼接进数据库查询语句建议先使用参数化查询。第二如果要在公网环境提供服务需要给编辑器上传接口加上权限校验避免未授权用户频繁调用计算服务造成资源浪费。8.6 与 AI 分子生成结合现在越来越多的团队在做 AI 分子生成。用户从一个种子结构出发AI 模型生成大量候选分子再把这些分子的 SMILES 批量导入编辑器进行人工评估。这个场景下开发一个“SMILES 批量展示组件”会很有价值。实现方式很简单把候选 SMILES 放在一个列表中点击列表中某个分子编辑器调用setMolecule更新画布。用户可以在编辑器里继续修改再导出为新的种子结构形成闭环。9. 总结与后续学习建议这篇文章从化学结构编辑器背后的核心概念讲起介绍了 SMILES、MOL、SDF 这些化学数据格式然后以 React 项目为载体完整演示了如何集成 Ketcher、如何加载脚本、如何把用户绘制的结构实时转换成 SMILES 和 MOL以及如何处理初始化失败、结构数据不同步等常见问题。下一步如果你希望在化学结构编辑器的基础上继续深入可以考虑几个方向。第一学习 RDKit掌握分子属性计算、子结构搜索、分子相似度比较等能力这些能让你在处理化学数据时更加得心应手。第二研究结构检索系统的实现比如用化学指纹和倒排索引实现大规模分子库检索。第三如果你对编辑器本身感兴趣可以阅读 Ketcher 源码了解它如何处理芳香性、立体化学和原子价态这些内部逻辑对理解整个化学信息学体系非常有帮助。技术工具在变但“从用户那里拿到可靠的化学结构数据”这一核心目标始终不变。回到实际项目中建议先跑通最简单的结构绘制链路再逐步加入模板库、结构清洗、后端校验和 AI 生成支持。每一次迭代都让编辑器的绘制体验更自然一分这本身就是一件很有价值的事情。