构建一个功能完善的代码编辑器是前端工程中的挑战性任务。本文介绍编辑器的核心架构设计,包括数据模型、光标实现、撤销重做与命令模式等关键模块。
一、整体架构设计
编辑器采用经典的 MVC + 命令模式架构:
- Model(模型):文档数据、选择范围、撤销栈
- View(视图):DOM 渲染、滚动管理、高亮绘制
- Controller(控制器):输入处理、命令分发、状态管理
/* 编辑器核心类结构 */ class Editor { constructor(container) { this.document = new TextDocument(); this.selection = new Selection(); this.undoManager = new UndoManager(); this.view = new EditorView(container, this); this.commands = new CommandRegistry(); this.bindEvents(); } }
二、文档数据模型
文档模型需要支持高效的插入、删除、查询操作。使用 rope(绳索)数据结构可以优化大文件的修改性能:
/* 简单的行数组实现 */ class TextDocument { constructor() { this.lines = ['']; /* 初始空行 */ } getText() { return this.lines.join('\n'); } insert(pos, text) { const {line, col} = this.resolvePos(pos); const newLines = text.split('\n'); const first = this.lines[line]; this.lines[line] = first.slice(0, col) + newLines[0]; this.lines.splice(line + 1, 0, ...newLines.slice(1).map((l, i) => l + (i === newLines.length - 1 ? first.slice(col) : '') ); } delete(start, end) { /* 删除指定范围的文本 */ } }
三、光标与选区实现
光标位置使用行号 + 列号表示,需要处理各种边界情况:
/* 选区管理 */ class Selection { constructor() { this.anchor = {line: 0, col: 0}; /* 选区起点 */ this.head = {line: 0, col: 0}; /* 选区终点 */ } isCollapsed() { return this.anchor.line === this.head.line && this.anchor.col === this.head.col; } normalize() { /* 确保 anchor 在 head 之前 */ } moveTo(line, col) { this.anchor = {line, col}; this.head = {line, col}; } extendTo(line, col) { this.head = {line, col}; } }
光标渲染技巧
使用 CSS 实现闪烁光标效果:
/* CSS 光标动画 */ .cursor { position: absolute; width: 2px; background: #34d399; animation: blink 1s step-end infinite; } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
四、撤销与重做系统
使用命令模式实现撤销重做,将每个操作封装为可逆的命令对象:
/* 命令基类 */ class Command { execute(editor) {} undo(editor) {} } /* 插入命令 */ class InsertCommand extends Command { constructor(pos, text) { super(); this.pos = pos; this.text = text; } execute(editor) { this.savedSel = editor.selection.get(); editor.document.insert(this.pos, this.text); } undo(editor) { editor.document.delete(this.pos, this.pos + this.text.length); editor.selection.set(this.savedSel); } } /* 撤销管理器 */ class UndoManager { constructor(maxSize = 100) { this.undoStack = []; this.redoStack = []; this.maxSize = maxSize; } execute(command, editor) { command.execute(editor); this.undoStack.push(command); this.redoStack = []; /* 新命令清除重做栈 */ if (this.undoStack.length > this.maxSize) { this.undoStack.shift(); } } undo(editor) { const cmd = this.undoStack.pop(); if (cmd) { cmd.undo(editor); this.redoStack.push(cmd); } } redo(editor) { const cmd = this.redoStack.pop(); if (cmd) { cmd.execute(editor); this.undoStack.push(cmd); } } }
五、输入处理与事件流
处理键盘输入时需要考虑组合键、输入法与移动端虚拟键盘:
/* 输入处理流程 */ class InputHandler { handleKeyDown(e) { /* 处理快捷键 */ if (e.ctrlKey || e.metaKey) { return this.handleShortcut(e); } /* 导航键 */ if (['ArrowUp', 'ArrowDown'].includes(e.key)) { return this.handleNavigation(e); } return false; /* 让浏览器处理其他键 */ } handleInput(text) { /* 处理实际输入的文本(可能包含组合字符) */ const cmd = new InsertCommand(editor.selection.get().getHead(), text); editor.undoManager.execute(cmd, editor); editor.view.render(); } }
六、视图渲染优化
将视图层与数据层分离,使用 requestAnimationFrame 实现平滑渲染:
/* 视图渲染器 */ class EditorView { render() { if (this.scheduled) return; this.scheduled = true; requestAnimationFrame(() => { this.doRender(); this.scheduled = false; }); } doRender() { /* 只更新变化的 DOM 节点 */ } }
核心原则:数据驱动视图。每次数据变化后,视图根据数据重新渲染,而非直接操作 DOM。
七、总结
- 采用 MVC + 命令模式,分离关注点
- 文档模型使用行数组,支持高效文本操作
- 撤销重做通过命令模式实现,支持无限撤销
- 输入处理要区分快捷键和普通输入
- 视图渲染使用 requestAnimationFrame 优化性能
- 光标使用 CSS 动画,无需 JavaScript 参与