1. 从零手搓一个能用的 HTML Editor,到底难在哪
很多人第一次听到「HTML Editor」这个词,脑子里浮现的是 CodeMirror、Monaco、Quill 这类庞然大物,觉得没有几万行代码根本做不出来。其实浏览器早就内置了一套最原始的富文本编辑能力,核心就两个东西:contentEditable属性和document.execCommand()方法。前者让一个普通的div变成可输入、可编辑的区域,后者负责执行加粗、斜体、下划线、列表这些格式化命令。把这两个拼起来,一个能跑、能用的简易 HTML Editor 就成型了。
这篇要解决的真实场景是:你需要在后台管理系统、评论框、邮件模板编辑页里塞一个轻量编辑器,不想引入几百 KB 的第三方库,也不想被框架绑定。用原生contentEditable+execCommand是最省事的路径。它适合前端初学者练手 DOM 操作,也适合老手做快速原型。我会把可复制的编辑器骨架代码、命令绑定与状态同步逻辑、以及用 TaoToken 统一 Key/API 通道做配置验证的完整流程都写出来,你照着敲就能跑。
需要提前说清楚一点:execCommand在规范层面已经被标记为废弃(deprecated),但截至现在主流浏览器依然完整支持,做轻量工具完全够用。如果你要的是生产级、跨端一致的富文本,那还是得上成熟库;但如果只是「够用就好」,这套原生方案性价比极高。
2. TaoToken 前置准备:统一 Key 与 API 通道
在写编辑器之前,先把模型调用这条链路打通。我习惯把编辑器做成「本地编辑 + 一键调用模型润色/翻译/续写」的形态,所以需要一个稳定的 API 入口。TaoToken 提供统一的 Key 和 API 通道,官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api 。
你需要先拿到一个 API Key。登录后进入控制台,在 API Keys 页面创建一个新 Key,复制保存好,后面配置里要用。控制台入口:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console 。创建 Key 的具体页面:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys 。
拿到 Key 之后,我建议用config.toml的方式集中管理,而不是把 Key 硬编码进前端 JS。原因很简单:前端代码一旦部署,Key 就等于公开了。正确做法是前端只调用你自己的后端接口,后端再拿着config.toml里的 Key 去请求 TaoToken。下面是一个可直接复制的配置片段:
# config.toml [taotoken] base_url = "https://taotoken.net/api" api_key = "sk-你的Key粘贴在这里" default_model = "claude-3-5-sonnet" timeout_seconds = 60 [editor] max_input_chars = 8000 enable_polish = true enable_translate = true如果你更想先验证模型通不通,可以直接用模型对话页面手动发一条消息试试:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models 。这一步能确认 Key 有效、网络可达,再去写代码心里有底。
注意:
config.toml属于服务端配置文件,务必放在后端项目里,不要提交到公开仓库,也不要用VITE_之类的前缀暴露到前端环境变量中。
3. 可复制的编辑器骨架:div + execCommand 命令绑定
现在进入正题。先写最核心的 HTML 结构:一个可编辑的div,加一排命令按钮。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>简易 HTML Editor</title> <style> #editor { width: 640px; min-height: 180px; border: 1px solid #ccc; border-radius: 6px; padding: 12px; font-size: 15px; line-height: 1.7; outline: none; } #editor:focus { border-color: #4a90d9; } .toolbar { margin-bottom: 8px; } .toolbar button { margin-right: 6px; padding: 4px 10px; cursor: pointer; } .toolbar button.active { background: #4a90d9; color: #fff; border-color: #4a90d9; } </style> </head> <body> <div class="toolbar"> <button>const editor = document.getElementById('editor'); const buttons = document.querySelectorAll('.toolbar button'); // 1. 命令绑定 buttons.forEach((btn) => { btn.addEventListener('mousedown', (e) => { // 阻止默认行为,避免点击按钮时编辑区失焦 e.preventDefault(); const cmd = btn.dataset.cmd; document.execCommand(cmd, false, null); editor.focus(); syncToolbarState(); }); }); // 2. 状态同步:根据当前光标位置刷新按钮高亮 function syncToolbarState() { buttons.forEach((btn) => { const cmd = btn.dataset.cmd; let isActive = false; try { isActive = document.queryCommandState(cmd); } catch (err) { isActive = false; } btn.classList.toggle('active', isActive); }); } // 3. 光标移动、键盘输入、鼠标抬起时都刷新状态 ['keyup', 'mouseup', 'input'].forEach((evt) => { editor.addEventListener(evt, syncToolbarState); }); // 4. 初始化一次 syncToolbarState();这里有两个容易踩的坑。第一,按钮点击要用mousedown而不是click,并且preventDefault(),否则点击按钮的瞬间编辑区会失去焦点,execCommand作用不到你选中的文本上。第二,queryCommandState必须在编辑区有焦点、光标在格式化文本内时才准确,所以要在keyup、mouseup这些事件里持续同步。
如果你想让编辑器支持「选中文字后调用模型润色」,可以在工具栏再加一个按钮,把editor.innerHTML或选中的纯文本发给后端,后端用config.toml里的 Key 请求 TaoToken,再把结果execCommand('insertHTML', ...)插回去。这条链路打通后,你的编辑器就从「能排版」升级成「能干活」了。
4. 验证请求:逐项点击命令并核对 DOM 输出
代码写完不算完,得验证。我习惯用「点击 + 看 DOM」的方式逐项核对,比肉眼看样式靠谱得多。
第一步,打开页面,在编辑区输入一行字,比如「测试加粗效果」。选中「加粗」两个字,点工具栏的 B 按钮。此时打开浏览器开发者工具,在 Elements 面板里看编辑区的 DOM,应该能看到类似这样的结构:
<div id="editor" contenteditable="true"> 测试<b>加粗</b>效果 </div>如果看到<b>或<strong>包裹,说明execCommand('bold')生效了。不同浏览器可能生成<b>或<strong>,这属于正常差异。
第二步,验证状态同步。把光标停在刚加粗的文字中间,工具栏的 B 按钮应该自动高亮(active类生效)。把光标移到普通文字上,高亮消失。这一步验证的是queryCommandState和syncToolbarState是否正常工作。
第三步,验证列表命令。点「无序列表」按钮,DOM 里应该出现<ul><li>...</li></ul>结构:
<div id="editor" contenteditable="true"> <ul><li>测试加粗效果</li></ul> </div>第四步,验证清除格式。选中带格式的文字,点「清除」,DOM 里的<b>、<i>等标签应该被移除,只剩纯文本。
第五步,验证模型调用链路。如果你接了后端,选中一段文字点「润色」,观察 Network 面板里请求是否发到了你的后端,后端是否成功返回 TaoToken 的结果。这一步能同时验证config.toml配置和 API 通道是否通畅。想单独测模型通道的话,也可以直接去模型对话页面发一条消息:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models 。
5. 本篇常见错误排查
错误一:点按钮没反应,或者格式加到了错误的位置。九成是用了click事件且没preventDefault(),导致编辑区失焦。改成mousedown+preventDefault()即可。
错误二:document.execCommand is not a function。检查是不是在iframe里操作了跨域的文档,或者editor变量拿到的不是可编辑元素。确认contenteditable="true"拼写正确,注意是contenteditable不是contentEditable(HTML 属性不区分大小写,但 JS 里isContentEditable是驼峰)。
错误三:按钮高亮状态不刷新。检查是否绑定了keyup、mouseup、input事件。另外queryCommandState对某些命令(如removeFormat)返回false是正常的,它本身没有「激活态」概念,可以在syncToolbarState里跳过这类命令。
错误四:粘贴内容带进来一堆乱七八糟的样式。contentEditable默认会保留粘贴源的 HTML。可以在paste事件里拦截,只取纯文本:
editor.addEventListener('paste', (e) => { e.preventDefault(); const text = (e.clipboardData || window.clipboardData).getData('text/plain'); document.execCommand('insertText', false, text); });错误五:TaoToken 请求返回 401。大概率是 Key 没配对,或者config.toml里的api_key带了多余空格。检查 Key 是否从 API Keys 页面正确复制:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys 。另外确认base_url写的是https://taotoken.net/api,不要多加斜杠或路径。
错误六:请求超时。编辑器里如果一次性把整篇长文发给模型,很容易超时。建议在config.toml里设max_input_chars,前端做长度校验,超长就提示用户分段处理。
6. 把编辑器接上模型:下一步怎么走
到这里,一个能加粗、斜体、列表、清除格式,并且能核对 DOM 输出的简易 HTML Editor 就跑起来了。如果你还想让它具备「选中即润色」「一键翻译」「续写」这些能力,核心就是把编辑区的文本通过后端转发给 TaoToken,再把结果插回编辑区。
长期做编码类工具、Agent 类应用的话,建议直接上 Coding Plan,把模型调用、额度管理、多模型切换都统一起来,省得自己维护一堆 Key:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan 。接入细节和参数说明可以翻接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc 。如果你用的是 Claude Code 这类工具,Anthropic 兼容通道的配置也在文档里有对应说明:https://taotoken.net/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode-anthropic 。
最后留一个我自己的实用习惯:编辑器骨架代码先跑通、DOM 核对无误,再去接模型。顺序反了的话,一旦出问题你分不清是编辑器逻辑错了还是 API 配置错了,排查成本翻倍。先把contentEditable和execCommand玩明白,剩下的都是水到渠成。