从零打造 Medium 级富文本编辑器:grande.js 全方位实战指南
你是否曾惊叹于 Medium 编辑器的优雅体验?一键格式化、智能工具栏、流畅交互——这些看似简单的功能背后,藏着复杂的 DOM 操作与事件处理逻辑。本文将带你深入剖析 grande.js(GitHub 加速计划旗下富文本编辑库)的实现原理,从环境搭建到高级定制,用 1000 行代码构建属于你的沉浸式编辑体验。
为什么选择 grande.js?
| 特性 | grande.js | 传统编辑器 | 现代富文本框架 |
|---|---|---|---|
| 体积 | ~15KB (未压缩) | 50KB+ | 100KB+ |
| 依赖 | 原生 JS | jQuery | React/Vue |
| 定制难度 | 中等(API 友好) | 高(需重写核心) | 低(组件化) |
| 兼容性 | IE9+ | IE8+ | ES6+ |
| 核心优势 | Medium 同款交互 | 功能全面 | 生态完善 |
grande.js 作为 Medium 编辑器的轻量级实现,完美平衡了功能深度与集成难度。它不依赖任何框架,通过原生 API 实现了格式化工具栏、选区检测、样式切换等核心能力,特别适合博客系统、内容管理平台等场景的快速集成。
环境准备与安装
快速上手
# 克隆仓库
git clone https://gitcode.com/gh_mirrors/gr/grande.js.git
cd grande.js
# 安装依赖(如需构建)
npm install
# 直接使用浏览器打开演示页面
open index.html
项目结构解析
grande.js/
├── css/ # 样式文件
│ ├── editor.css # 编辑器核心样式
│ ├── menu.css # 工具栏样式
│ └── icomoon/ # 图标字体
├── js/
│ └── grande.js # 核心库文件
├── index.html # 演示页面
└── test/ # 测试用例
核心文件仅需引入 grande.js 和配套 CSS 即可工作,无需复杂的构建流程。
基础使用指南
1. 初始化编辑器
<!-- HTML 结构 -->
<article contenteditable="true" id="editor"></article>
<!-- 引入资源 -->
<link rel="stylesheet" href="css/editor.css">
<link rel="stylesheet" href="css/menu.css">
<script src="js/grande.js"></script>
<!-- 初始化代码 -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// 绑定编辑器元素
const editor = document.getElementById('editor');
grande.bind([editor], { animate: true });
});
</script>
2. 核心 API 解析
| 方法 | 参数 | 描述 |
|---|---|---|
grande.bind() | nodes: NodeList, options: Object | 绑定可编辑元素并初始化 |
grande.select() | - | 触发文本选择事件(用于测试) |
配置选项:
{
animate: true, // 是否启用工具栏动画
allowImages: false // 是否支持图片上传(当前开发中)
}
实现原理深度剖析
工具栏交互机制
grande.js 的浮动工具栏是其标志性特性,实现逻辑包含三个关键步骤:
核心代码实现:
// 选区变化检测
document.onmouseup = function(event) {
setTimeout(function() {
triggerTextSelection(event); // 延迟执行确保选区稳定
}, 1);
};
// 工具栏定位
function setTextMenuPosition(top, left) {
textMenu.style.top = top + "px";
textMenu.style.left = left + "px";
textMenu.className = options.animate ? "text-menu active" : "text-menu";
}
富文本格式化原理
通过 document.execCommand() 实现基础格式化,但做了大量兼容性处理:
// 样式切换核心逻辑
function triggerTextStyling(node) {
const className = node.className;
for (const tag in tagClassMap) {
if (tagClassMap[tag] === className) {
// 检查当前元素是否已应用该样式
if (hasParentWithTag(getFocusNode(), tag)) {
document.execCommand("formatBlock", false, "p"); // 移除样式
} else {
document.execCommand("formatBlock", false, tag); // 应用样式
}
break;
}
}
}
支持的格式化标签映射:
const tagClassMap = {
"b": "bold",
"i": "italic",
"h1": "header1",
"h2": "header2",
"blockquote": "quote",
"a": "url"
};
高级功能与定制
自定义工具栏按钮
- 扩展 CSS 样式(menu.css):
.text-menu .code {
background: #f5f5f5;
padding: 2px 8px;
}
- 修改工具栏模板(grande.js):
// 在 toolbarTemplate 中添加
<button class='code'>code</button>
- 绑定格式化事件:
// 在 bindTextStylingEvents 中添加
if (className === 'code') {
document.execCommand('formatBlock', false, 'pre');
}
图片上传功能(实验性)
虽然官方 roadmap 中标记为开发中,但可通过以下方式扩展:
// 监听文件选择事件
imageInput.onchange = function(event) {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = function(e) {
const figEl = document.createElement("figure");
figEl.innerHTML = `<img src="${e.target.result}"/>`;
editNode.insertBefore(figEl, imageBound.bottomElement);
};
reader.readAsDataURL(file);
};
常见问题与解决方案
Q: 工具栏位置偏移?
A: 检查 getBoundingClientRect() 计算是否考虑页面滚动,修复代码:
// 修正坐标计算
setTextMenuPosition(
clientRectBounds.top - 5 + root.pageYOffset, // 添加滚动偏移
(clientRectBounds.left + clientRectBounds.right) / 2
);
Q: Firefox 下格式错乱?
A: Firefox 对 execCommand 支持差异,需特殊处理:
// 文本属性检测兼容
function getTextProp(el) {
if (el.nodeType === Node.TEXT_NODE) return "data";
return isFirefox ? "textContent" : "innerText";
}
性能优化建议
- 事件节流:文本选择事件触发频率高,添加节流控制:
let isProcessing = false;
document.onmouseup = function(event) {
if (!isProcessing) {
isProcessing = true;
setTimeout(() => {
triggerTextSelection(event);
isProcessing = false;
}, 50);
}
};
- 样式缓存:减少 DOM 查询次数:
// 缓存工具栏元素
const textMenuButtons = document.querySelectorAll(".text-menu button");
未来展望与扩展方向
grande.js 目前处于维护状态,但核心架构仍具扩展性:
推荐扩展方向:
- 集成
marked.js实现 Markdown 双向转换 - 添加代码高亮插件(如 Prism.js)
- 实现本地存储自动保存
总结
grande.js 以精简的代码实现了 Medium 编辑器的核心体验,其事件驱动的架构和原生 API 的巧妙运用,为我们展示了轻量级富文本编辑器的设计哲学。通过本文的解析与实践,你不仅能够快速集成这一工具,更能深入理解 DOM 操作、选区管理等前端核心技术。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



