说实话,我一直想有一个能随手打开、随时记录技术笔记的本地编辑工具。市面上的 Markdown 编辑器五花八门,但总有那么几个功能不合我的习惯:有的太重,打开要等半天;有的默认主题太花哨,写代码笔记时反而干扰视线;还有的编辑区和预览区布局不合理,左右切换非常别扭。后来我想,与其一直找“现成的”,不如自己动手写一个纯粹、简单的 Markdown 编辑器。
更关键的是,这次我换了一种开发思路:不全是自己一行一行写,而是把需求拆清楚之后,让豆包先帮我搭出代码骨架,我负责跑起来、看效果、查问题、补功能。原本可能要折腾两三天的小工具,这次只花了一个下午就完成了核心版本。这篇文章就把整个开发过程完整记录下来,包括提示词怎么写、代码怎么审查、遇到了哪些坑、最终的可运行代码长什么样。如果你也想体验“用 AI 辅助开发一个小工具”的完整流程,这篇文章应该能提供一条非常清晰的参考路径。
1. 背景与核心概念
1.1 为什么要用 AI 做一个编辑器
很多开发者听到“自己写一个编辑器”的第一反应是:编辑器不是很简单吗?一个 textarea 加一个按钮不就行了?
确实,最基础的文本编辑功能并不复杂,但一个真正能日常使用的编辑器,需要处理很多细节:语法格式转换、选区替换、快捷键、粘贴清理、安全过滤、文件下载、自动保存……这些功能叠加起来,工作量并不小。如果是从零手写,仅打字机效果的工具栏交互就足够让人花上大半天调试。
用生成式 AI 辅助开发,最大的价值不是“让 AI 直接交给你一个完整产品”,而是把工作量边界重新分配:
- AI 负责把通用且成熟的功能块快速生成出来,比如文本选区包裹语法、Markdown 转换渲染、文件下载逻辑。
- 人负责提清楚需求、跑起来验证、找 Bug、做安全审查、按自己的使用习惯做调整。
豆包作为一款面向对话和内容生成的 AI 助手,完全适合这种协作方式。我把需求描述给它,它会返回可以运行的 HTML/JavaScript 代码;遇到报错时,我直接把错误信息贴回去,它也会给出修改建议。整个过程有点像一个懂点前端的“开发搭子”在配合你干活。
1.2 编辑器与编译器的区别
在正式开始之前,有必要先把一个基础概念弄清楚,因为很多初学者在搜索“编辑器”资料时,经常会把“编辑器”和“编译器”混在一起。
编辑器(Editor)是给人编辑文本、代码、文档使用的工具,它负责接收和整理文字内容,本质上是“写东西”的软件。常见的文本编辑器有 VSCode、Sublime Text、Vim,以及本文要开发的 Markdown 编辑器。
编译器(Compiler)则是把高级语言编写的源代码,翻译成机器可以执行的代码的程序,它负责“翻译并生成可运行程序”。常见的有 GCC、JDK 中的 javac 等。
简单来说:你可以在编辑器里写代码,然后用编译器把代码变成可运行的程序。这两者分工完全不同。本文要做的编辑器,聚焦在“文本编辑 + 文档预览”这个方向上,不涉及代码编译逻辑。
1.3 为什么选择 Markdown 编辑器这个形态
市面上的编辑器有很多形态,比如富文本编辑器、代码编辑器、Markdown 编辑器。我之所以选择 Markdown 编辑器,有这几个原因:
第一,Markdown 语法简单但表达力很强,写技术博客、项目文档、学习笔记都非常合适;第二,Markdown 在纯前端环境下很好落地,只需要把 Markdown 文本转换成语义化的 HTML 就能实现预览;第三,转换逻辑可以复用成熟的第三方库(比如 marked),这样 AI 生成代码时不需要发明轮子,生成结果的稳定性更高。
整体技术方案也比较轻量。我们不需要 Nginx、不需要数据库、不需要编译打包工具,只需要一个 HTML 文件、一个 Markdown 解析库、再加一点点 JavaScript 逻辑,就能做出一个可用的工具。这个特点对初学者非常友好,也方便后续按自己的需求改造扩展。
2. 环境准备与项目设计
2.1 开发环境和工具准备
由于这是一个纯前端项目,环境准备非常简单,不需要安装 Node.js、不需要配置 Java 环境。
我实际使用的工具如下:
- 操作系统:Windows 11,理论上任一现代操作系统都适用。
- 浏览器:Chrome 或 Edge,用于打开 HTML 文件并调试。
- 代码编辑器:VSCode,用于保存和修改 HTML、JavaScript。
- AI 辅助工具:豆包网页版或客户端。
如果你还没有 VSCode 也没关系,哪怕用系统自带的记事本,只要能保存.html文件,就可以完成本教程。重点是理解开发思路,而不是纠结某个特定工具。
另外需要提醒一点:本文最终代码通过 CDN 引入 marked 和 DOMPurify 两个第三方库,打开 HTML 时需要能正常访问公共 CDN。如果网络环境受限导致依赖加载慢,可以先把对应 JS 文件下载到本地,再把<script>的 src 路径改成相对路径。
2.2 功能范围规划
动工之前,我先确定这个编辑器第一版需要哪些功能。功能范围宁少勿多,先把核心链路跑通,再加周边能力。
第一版规划如下:
- 左侧为编辑区,使用 textarea 接收 Markdown 原文。
- 右侧为预览区,将 Markdown 实时渲染为 HTML 页面效果。
- 顶部为工具栏,包含加粗、斜体、链接、行内代码、图片、无序列表、标题等常用按钮。
- 点击工具栏按钮时,把对应语法包裹到当前选中的文字上,形成一个“快捷插入”的交互。
- 底部状态栏展示当前输入字数。
- 提供“下载 .md 文件”和“复制全文”两个出口,方便把写好的内容带走。
这个功能范围既有编辑器的核心流程(输入、转换、预览),也有相对繁琐的选区交互逻辑(工具栏按钮操作 textarea),非常适合用来体验“AI 生成 + 人工审查修改”的开发模式。
2.3 项目结构设计
为了降低初学者上手成本,第一版我采用单文件方案,把所有 HTML、CSS、JavaScript 都写在markdown-editor.html一个文件里。这样只要双击文件,就能在浏览器中立即运行,不需要处理相对路径问题。
如果你想进一步工程化,也可以拆分成三个文件:
markdown-editor/ ├── index.html ├── style.css └── main.js从单文件改成多文件属于非常自然的演进过程。第一版先保证能跑通,后面再按需拆分。
3. 用豆包生成第一版编辑器
3.1 怎么写提示词
很多人用 AI 生成代码时,会说“帮我写一个编辑器”,然后得到一段泛泛的代码,结果根本跑不起来。问题往往出在提示词上没有给出足够的上下文约束。
我写提示词时,会尽量包含这几个要素:技术栈、功能列表、布局结构、依赖库、交付形式。
下面是我实际使用过的一段提示词,你可以参考:
请用纯 HTML/CSS/JavaScript 实现一个 Markdown 编辑器,要求: 1. 使用单个 HTML 文件完成; 2. 顶部有工具栏,包含加粗、斜体、链接、行内代码、图片、无序列表、标题按钮; 3. 页面主体左右分栏,左侧是 textarea 输入框,右侧是预览区; 4. 引入 marked 库,把左侧 Markdown 内容实时渲染到右侧预览区; 5. 点击工具栏按钮时,将对应 Markdown 语法包裹到当前选中文本的外面; 6. 页面采用现代简洁风格,使用 flex 布局,适配常见屏幕。为什么这样写?
“纯 HTML/CSS/JavaScript”限定了技术栈,AI 不会给你返回一个需要构建工具的 React 项目;明确“单个 HTML 文件”约束了交付形态;“工具栏按钮”和“左右分栏”规定了页面结构;“实时渲染”点明了核心交互逻辑;“现代简洁风格”给出了 UI 风格的期待。
提示词越具体,返回结果离可用状态越近。实际操作中,AI生成结果一定会与需求不完全一致。这一步不要指望 AI 直接交付一个完美产品,拿到代码后我们还需要手动检查和调整。
3.2 拿到生成结果后怎么处理
豆包返回代码之后,我一般会按三个步骤处理。
第一步,先把代码完整地保存为.html文件,双击用浏览器打开,看页面能否正常渲染。如果打开后内容为空或者样式错乱,先检查浏览器控制台有没有红色报错信息。按 F12 打开开发者工具,切到 Console 面板,任何 JavaScript 异常都会显示在这里。
第二步,逐行看关键逻辑是否成立。AI 生成代码最常见的坑是引用了一个不存在的函数、调用了一个与库版本不匹配的 API,或者把某个 CSS 类写错了。不要因为 AI 返回了代码就直接信任,必须把核心函数从头到尾读一遍。
第三步,如果发现代码跑不通,把报错信息完整复制给豆包,并附上相关代码片段,让它重新修改。例如我会这样补充:
代码报错了:Uncaught TypeError: marked is not a function 我的代码是:html = marked(content) 请检查 marked 库的调用方式,并给出正确写法。这个反馈循环本身也是 AI 辅助开发流程的关键一环:把错误信息变成新的上下文,再交给 AI 分析,往往一两轮就能定位问题。
3.3 第一版核心逻辑说明
拿到豆包生成的第一版代码后,我先不急着加功能,而是确认几个核心函数是否正常工作。
整个编辑器的核心链路是:textarea 输入事件触发 render 方法,render 方法把 Markdown 文本交给 marked 解析,然后把解析后的 HTML 字符串写入右侧预览区。
这段逻辑对应了编辑器的灵魂:
const editor = document.getElementById('editor'); const preview = document.getElementById('preview'); function render() { const mdText = editor.value; const html = parseMarkdown(mdText); preview.innerHTML = html; } editor.addEventListener('input', render);注意在调用 marked 时,版本不同,API 也不同。早期版本直接调用marked(mdText),而较新版本推荐使用marked.parse(mdText)。为了兼容各种版本,我在代码里做了一次函数封装:
function parseMarkdown(mdText) { if (typeof marked.parse === 'function') { return marked.parse(mdText); } return marked(mdText); }这是一个非常典型的“审查 AI 代码时需要注意版本兼容性”的案例。AI 返回的代码可能基于它训练时见过的某个版本,你需要结合当前实际加载的库版本做适配。
4. 核心功能拆解与代码讲解
4.1 工具栏按钮与选区包裹原理
工具栏按钮的核心交互是:当用户选中一段文字,然后点击按钮时,编辑器自动在选中文字的外面包裹对应的 Markdown 语法。
比如选中“豆包”两个字,点击“加粗”按钮,文本变成**豆包**;点击“标题”按钮,则在文字所在行开头插入##。
要实现这个交互,关键是操作 textarea 的selectionStart和selectionEnd属性,再借助setRangeText方法替换选中的文本。
function insertText(before, after) { const start = editor.selectionStart; const end = editor.selectionEnd; const selected = editor.value.substring(start, end) || '在这里输入文本'; editor.setRangeText(before + selected + after, start, end, 'end'); render(); editor.focus(); }这段代码的逻辑是:读出光标位置和选中范围,然后取选中的文本,如果什么都没选中就给一个默认占位文字,最后用before + selected + after拼接后的新文本替换原来的选区内容。
setRangeText方法会自动更新 textarea 的值,并支持在替换后把光标移动到末尾位置,这样用户体验比较自然。点击工具栏对应按钮时,只需要传入不同的前后缀即可:
document.getElementById('btn-bold').addEventListener('click', () => insertText('**', '**')); document.getElementById('btn-italic').addEventListener('click', () => insertText('*', '*')); document.getElementById('btn-code').addEventListener('click', () => insertText('`', '`'));这种写法简洁、统一,新增一个按钮只需要增加一行事件绑定,扩展性很好。
4.2 预览区 XSS 安全问题
当你使用preview.innerHTML = html这种方式渲染内容时,有一个安全隐患:如果用户粘贴了包含<script>标签或onerror事件的文本,这些内容会被浏览器当作页面可控代码执行,形成 XSS 攻击。
例如用户复制了下面这一行内容进编辑器:
<img src="x" onerror="alert('恶意代码')">如果不做过滤,预览区会把这段内容渲染为真实 DOM,从而触发onerror事件。这就是典型的 XSS 注入。
解决方案是引入 DOMPurify 这个专门做 HTML 清理的库。它会把危险标签和危险属性过滤掉,保留安全部分。
在页面中引入:
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>然后修改 render 逻辑:
function render() { const mdText = editor.value; const rawHtml = parseMarkdown(mdText); const cleanHtml = DOMPurify.sanitize(rawHtml); preview.innerHTML = cleanHtml; wordCount.textContent = editor.value.length + ' 字'; }这一步非常关键。AI 生成的初版代码通常不会主动考虑 XSS 问题,如果你直接把生成结果部署上线,会留下明显安全漏洞。养成“AI 生成后必做安全审查”的习惯非常重要。
4.3 下载与复制功能
编辑器里的内容不能只停留在页面上,需要能导出带走。我实现了两个功能:下载为.md文件、复制全文到剪贴板。
下载功能使用 Blob 对象。Blob 是浏览器提供的二进制大数据对象,可以把文本内容封装成文件。下载的核心思路是:把文本内容放入 Blob,通过 URL.createObjectURL 生成临时下载链接,再用程序模拟点击。
document.getElementById('btn-download').addEventListener('click', () => { const blob = new Blob([editor.value], { type: 'text/markdown;charset=utf-8' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'markdown-note.md'; a.click(); URL.revokeObjectURL(a.href); });需要注意:type属性中指定charset=utf-8,否则下载的 Markdown 文件在用其他编辑器打开时可能出现中文乱码。如果下载文件名中文乱码,可以检查浏览器设置或手动重命名。
复制功能使用navigator.clipboard.writeText,这是现代浏览器提供的剪贴板 API,比传统的document.execCommand('copy')更可靠:
document.getElementById('btn-copy').addEventListener('click', async () => { try { await navigator.clipboard.writeText(editor.value); alert('内容已复制到剪贴板'); } catch (err) { alert('复制失败:' + err.message); } });这个 API 对运行环境有要求:在 HTTPS 环境下或者 localhost 环境下才能正常工作。如果你直接用file://协议双击 HTML 文件打开页面,部分浏览器的剪贴板 API 会被限制。如果遇到复制失败,可以改用 fallback 方案,或者启动一个本地静态服务器来访问页面。
4.4 自动保存到本地
写内容最怕的是“写了一半浏览器崩溃”。为了减少损失,可以加一个简单的自动保存功能,把编辑内容写入 localStorage。
localStorage 是浏览器提供的本地存储能力,数据会持久保存在用户本机,无需服务器参与。这个方案很适合单文件小工具。
function saveDraft() { localStorage.setItem('markdown-editor-draft', editor.value); } function loadDraft() { const saved = localStorage.getItem('markdown-editor-draft'); if (saved) { editor.value = saved; } } editor.addEventListener('input', () => { render(); saveDraft(); }); loadDraft();增加这段逻辑后,用户刷新页面或重新打开编辑器,内容都能从 localStorage 中恢复。这是编辑器类工具非常实用的体验优化。
5. 完整代码与运行验证
5.1 完整可运行代码
下面给出一个可运行的完整版本。我在豆包生成代码的基础上,把 Markdown 解析兼容、XSS 过滤、选区包裹、下载复制、自动保存等功能整合在一起。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Markdown 编辑器 - 豆包辅助开发</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif; height: 100vh; display: flex; flex-direction: column; background: #f5f6f8; color: #333; } .toolbar { display: flex; align-items: center; gap: 8px; padding: 10px 16px; background: #fff; border-bottom: 1px solid #e0e0e0; flex-wrap: wrap; } .toolbar button { padding: 6px 12px; border: 1px solid #d0d7de; border-radius: 6px; background: #fff; cursor: pointer; font-size: 14px; transition: background 0.2s; } .toolbar button:hover { background: #f0f2f5; } .container { flex: 1; display: flex; overflow: hidden; } .editor-pane, .preview-pane { width: 50%; padding: 16px; overflow-y: auto; background: #fff; } .editor-pane { border-right: 1px solid #e0e0e0; } #editor { width: 100%; height: 100%; border: none; outline: none; resize: none; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 14px; line-height: 1.6; } #preview { line-height: 1.8; word-break: break-word; } #preview pre { background: #f6f8fa; padding: 12px; border-radius: 6px; overflow-x: auto; } #preview blockquote { margin: 8px 0; padding-left: 12px; border-left: 4px solid #d0d7de; color: #57606a; } .statusbar { padding: 6px 16px; background: #fff; border-top: 1px solid #e0e0e0; font-size: 12px; color: #777; display: flex; justify-content: space-between; } </style> </head> <body> <div class="toolbar"> <button id="btn-bold">加粗</button> <button id="btn-italic">斜体</button> <button id="btn-link">链接</button> <button id="btn-code">行内代码</button> <button id="btn-img">图片</button> <button id="btn-list">无序列表</button> <button id="btn-heading">标题</button> <button id="btn-download">下载 .md</button> <button id="btn-copy">复制</button> </div> <div class="container"> <div class="editor-pane"> <textarea id="editor" placeholder="# 开始写作..."></textarea> </div> <div class="preview-pane"> <div id="preview"></div> </div> </div> <div class="statusbar"> <span id="wordCount">0 字</span> <span>Markdown 编辑器</span> </div> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script> <script> const editor = document.getElementById('editor'); const preview = document.getElementById('preview'); const wordCount = document.getElementById('wordCount'); editor.value = '# 欢迎使用 Markdown 编辑器\n\n' + '这是一个 **由豆包辅助开发** 的编辑器。\n\n' + '- 实时预览\n' + '- 常用语法快捷插入\n' + '- 支持下载与复制\n' + '- 自动保存草稿'; function parseMarkdown(mdText) { if (typeof marked.parse === 'function') { return marked.parse(mdText); } return marked(mdText); } function render() { const mdText = editor.value; const rawHtml = parseMarkdown(mdText); const cleanHtml = DOMPurify.sanitize(rawHtml); preview.innerHTML = cleanHtml; wordCount.textContent = editor.value.length + ' 字'; } editor.addEventListener('input', render); function insertText(before, after) { const start = editor.selectionStart; const end = editor.selectionEnd; const selected = editor.value.substring(start, end) || '在这里输入文本'; editor.setRangeText(before + selected + after, start, end, 'end'); render(); editor.focus(); } document.getElementById('btn-bold').addEventListener('click', () => insertText('**', '**') ); document.getElementById('btn-italic').addEventListener('click', () => insertText('*', '*') ); document.getElementById('btn-link').addEventListener('click', () => insertText('[链接文字](https://', ')') ); document.getElementById('btn-code').addEventListener('click', () => insertText('`', '`') ); document.getElementById('btn-img').addEventListener('click', () => insertText('') ); document.getElementById('btn-list').addEventListener('click', () => insertText('\n- ', '') ); document.getElementById('btn-heading').addEventListener('click', () => insertText('\n## ', '') ); function saveDraft() { localStorage.setItem('markdown-editor-draft', editor.value); } function loadDraft() { const saved = localStorage.getItem('markdown-editor-draft'); if (saved) { editor.value = saved; } } editor.addEventListener('input', saveDraft); document.getElementById('btn-download').addEventListener('click', () => { const blob = new Blob([editor.value], { type: 'text/markdown;charset=utf-8', }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'markdown-note.md'; a.click(); URL.revokeObjectURL(a.href); }); document.getElementById('btn-copy').addEventListener('click', async () => { try { await navigator.clipboard.writeText(editor.value); alert('内容已复制到剪贴板'); } catch (err) { alert('复制失败:' + err.message); } }); loadDraft(); render(); </script> </body> </html>你只需要把这段代码复制到markdown-editor.html文件里,保存后在浏览器中打开,就能看到一个基于现代浏览器原生能力的 Markdown 编辑器。
5.2 运行与验证步骤
打开页面后,我建议按下面这个验证清单逐一测试:
- 检查页面是否正常显示初始内容。如果左侧 textarea 和右侧预览区同时出现内容,说明基础渲染链路正常。
- 在左侧文本末尾输入
**新内容**,观察右侧预览区是否实时变成加粗效果。这是验证input事件与 render 逻辑是否生效。 - 用鼠标在左侧选中一段文字,点击“加粗”按钮,确认选中文字被包裹成
**文字**格式。 - 在编辑器里输入
<img src="x" onerror="alert(1)">,确认右侧预览区没有弹出 alert 弹窗。弹窗成功说明 DOMPurify 没有生效,没有弹窗说明安全过滤正常。 - 点击“下载 .md”按钮,确认得到
markdown-note.md文件,且用系统记事本打开后中文正常。 - 刷新页面,确认左侧内容仍然保留,说明 localStorage 自动保存生效。
这一步建议每个功能都实际点一遍。AI 辅助开发有一个特点,就是生成代码不一定全部符合你的预期,通过测试清单可以快速把问题和预期差异暴露出来。
5.3 预期效果描述
完成上述验证后,你手上就有一个功能完整的 Markdown 编辑器了。它具备三个明显特点:
第一,轻量。整个工具只有一个 HTML 文件,不依赖任何构建工具或后端服务,拷贝到任何安装了浏览器的电脑上都能运行。
第二,实时预览。左侧输入 Markdown,右侧马上看到渲染后的效果。无论是写技术博客草稿、整理 README,还是做会议记录,这个工作流都非常顺手。
第三,自带安全防护。由于在渲染链路中加入了 DOMPurify,就算从网上复制了包含恶意脚本的内容,也不会在编辑器里执行。
6. 常见问题与排查思路
6.1 问题排查汇总表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 页面打开后预览区空白 | marked 库未加载成功 | 在 Console 面板检查是否报错,确认 CDN 能正常访问,或改用本地依赖文件 |
| 点击工具栏按钮没有反应 | setRangeText方法不兼容或按钮未绑定事件 | 确认浏览器是较新版本,检查控制台有无 JavaScript 异常 |
| 输入内容后预览不更新 | 没有正确监听 input 事件,或 render 函数没被调用 | 检查editor.addEventListener('input', render)是否存在 |
粘贴<script>后弹窗执行 | 缺少 DOMPurify 过滤 | 在 render 逻辑中加入DOMPurify.sanitize()过滤步骤 |
| 复制按钮提示失败 | 剪贴板 API 在非 HTTPS 环境下被限制 | 使用 localhost 方式打开页面,或启动本地静态服务器 |
| 下载的文件中文乱码 | Blob 的 type 未指定 charset,或操作系统编码问题 | 确保 type 为text/markdown;charset=utf-8,检查浏览器默认编码设置 |
| marked 调用报错 | 库版本不同,API 不兼容 | 使用函数封装兼容marked()与marked.parse()两种写法 |
6.2 标记库版本兼容问题详解
在 AI 辅助开发过程中,我遇到最多的问题就是第三方库的版本兼容。标记库的 API 变化在版本迭代中并不罕见。
以 marked 为例:
// 老版本写法 var html = marked(mdText); // 新版本写法 var html = marked.parse(mdText);如果你的项目加载了较新版 marked,却使用旧 API 调用,浏览器会抛出marked is not a function之类的错误。反过来,如果加载了老版本却调用marked.parse,也会得到 undefined。
编写代码时,最好用函数封装做一次兼容适配,就像我在前面给出的parseMarkdown那样。这一点在 AI 生成代码时必须注意:AI 无法感知你项目里实际加载的库版本,所以它给出的代码可能并不匹配你的环境。遇到报错先看版本,再改调用方式。
6.3 CDN 加载异常处理
很多纯前端小工具都依赖 CDN 引入第三方库,如果加载失败,整个页面往往表现为“白屏”或者“预览区无法渲染”。
排查步骤如下:
- 在浏览器按 F12 打开开发者工具,切到 Network 面板刷新页面,找到请求 marked 和 dompurify 的文件记录。
- 查看请求状态,如果是红色加载失败,说明当前网络环境无法访问该 CDN。
- 解决方法是手动下载这两个 JS 文件到本地,并修改 HTML 中的 src 路径,指向本地相对路径。
<script src="./js/marked.min.js"></script> <script src="./js/dompurify.min.js"></script>这种本地化方式适合离线环境或内部网络受限的办公场景。
7. 最佳实践:把 AI 当好开发搭子
7.1 提示词设计经验
用 AI 生成代码,提示词质量基本决定了代码可用度。我总结了一套较为有效的提示词编写方法。
第一,一次性只做一件事。让 AI 编写一个初版、追加一个功能、修复一个 Bug,不要在一句话里混入太多要求。比如先让 AI 生成基础编辑器,跑通后再让它添加下载功能,这样每轮生成结果都容易验证。
第二,明确写清“不要做什么”。例如“不要使用 jQuery”“不要引入 React 等框架”“不要生成多余的 CSS 库”,这些负向约束同样重要。因为 AI 有可能会选择它认为方便的库,但这可能并不是你希望引入的。
第三,要求 AI 解释关键代码。如果你对某段逻辑不理解,可以在提示词里加上“请在关键代码处添加中文注释”。有了注释,后续自己维护会省心很多。
7.2 拿到 AI 代码后的三轮审查
AI 生成的代码不能直接信任,建议从三个角度做审查。
第一轮是功能性审查。把代码跑起来,对照你的需求清单逐项测试。看核心功能能否使用、交互是否符合预期、有没有明显报错。
第二轮是安全性审查。如果代码涉及innerHTML、eval、动态执行等操作,需要格外关注。本项目中preview.innerHTML是必然存在的,但必须配合 DOMPurify 才能保证安全。审查时要问自己:这段代码中是否有外部输入直接进入危险函数?
第三轮是依赖审查。检查 AI 引入了哪些第三方库,版本是否可靠,CDN 是否有效。不明来源的 CDN 可能有被劫持的风险,尽量选择官方维护的公共 CDN,或把依赖下载到本地托管。
7.3 从单文件走向工程化
当你的编辑器越改越复杂时,单文件方案会逐渐变得难以维护。这时可以按功能把代码拆分成三个文件:HTML 负责结构,CSS 负责样式,JavaScript 负责逻辑。
再进一步,如果你想长期维护这个工具,建议用 Git 做版本管理。每次用 AI 生成代码或修改功能后,及时提交一个版本。这样即使某次修改把页面改崩了,也可以随时回滚。把 AI 生成的不同版本代码放到 Git 分支里对比,也是一种不错的实验方式。
对于想要深入学习的开发者,还可以继续扩展:引入代码高亮库 highlight.js,让预览区中的代码片段也能有丰富的配色;支持导出 HTML 和 PDF;增加全屏模式;使用 Electron 封装成桌面客户端。每一步都可以继续用豆包辅助生成代码,再按自己的需求调整。
7.4 AI 辅助开发的边界在哪里
最后想聊聊我对 AI 辅助开发的边界理解。AI 可以非常高效地生成通用型代码,但它不适合独自负责复杂业务逻辑和强安全敏感模块。比如解析二进制协议、处理支付回调、设计用户权限模型,这类任务需要对业务有深度理解,直接让 AI 生成容易埋下隐患。
我的习惯是:AI 负责“搭积木”,我负责“想结构和验结果”。需求拆分、技术选型、代码审查、安全验证这些环节,尽量自己牢牢把握。当 AI 给了代码,我不急着运行,先问自己三个问题:这段代码解决什么问题?依赖了什么外部条件?如果输入异常会怎么处理?三个问题都能回答出来,才算是真正理解了这段代码,也才能真正地在项目里使用它。
8. 写在最后
从“想找一个顺手的 Markdown 编辑器”到“自己动手做了一个出来”,整个过程比我预想的要顺利。豆包在里面承担了代码生成、报错分析、功能扩展建议等多项工作,让一个原本可能花费大量时间的基础项目,在一下午之内跑通了核心版本。
而我自己的核心收获是:用 AI 做开发,并不是把“写代码”这件事外包出去,而是把工作重心从“逐行敲代码”转移到“拆需求、写提示词、审代码、验效果”上。编辑器本身只是一个例子,这个协作方式几乎可以复用到任何小工具开发中。
如果你也想尝试,我建议不要从太复杂的项目开始。先给自己定一个小目标,让 AI 做一个能跑的最小版本,再加上一两个自己真正需要的功能,然后投入日常使用。过程中遇到问题,就把报错信息整理好,继续和 AI 对话。几次反复修改之后,你会发现,不光是代码能力在提升,对工具设计的理解也会变得不一样。希望这篇文章能给你一些启发,也欢迎把你的实践过程和踩坑经验分享出来一起交流。