电子表格已成为现代计算中不可或缺的一部分。它们允许用户以表格形式组织、处理和分析数据。像 Google Sheets 这样的应用程序已为强大、互动的电子表格设定了标准。
在这篇博客文章中,我们将引导您通过使用 JavaScript 构建电子表格应用程序的过程。我们将重点关注关键编程概念,探索 JavaScript 特性,并提供详细的代码片段和解释。
完整的源代码可以在我的 Codepen 中 这里 获取。
什么是 Google 电子表格?
Google 电子表格是一个基于网络的应用程序,允许用户在线创建、编辑和协作电子表格。它提供了公式、数据验证、图表和条件格式等功能。
我们的项目模拟了 Google 电子表格的一些核心功能,重点关注:
- 可编辑单元格。
- 公式解析和评估。
- 通过 Pub/Sub 模型的实时更新。
- 键盘导航和单元格选择。
- 单元格之间的动态依赖评估。
本项目的特点
- 可编辑单元格: 允许用户在单元格中输入文本或公式。
- 公式支持:处理以
=
开头的公式并评估表达式。 - 实时更新: 依赖单元格的变化触发使用Pub/Sub模型的更新。
- 键盘导航: 允许使用箭头键在单元格之间移动。
- 动态评估: 确保依赖于其他单元格的公式实时更新。
- 错误处理: 为无效输入或循环依赖提供有意义的错误信息。
- 可扩展设计: 允许轻松扩展以添加更多行、列或功能。
应用程序的关键组件
1. 模式管理
JavaScript
const Mode = {
EDIT: 'edit',
DEFAULT: 'default'
};
此枚举定义了两种模式:
- 编辑: 启用对选定单元格的编辑。
- 默认: 允许在不编辑的情况下进行导航和交互。
为什么使用模式?
模式简化了UI状态的管理。例如,在默认模式下,键盘输入在单元格之间移动,而在编辑模式下,输入修改单元格内容。
2. Pub/Sub类
Pub/Sub模型处理订阅和实时更新。单元格可以订阅其他单元格,并在依赖关系变化时动态更新。
JavaScript
class PubSub {
constructor() {
this.map = {};
}
get(source) {
let result = [];
let queue = [ (this.map[source] || [])];
while (queue.length) {
let next = queue.shift();
result.push(next.toUpperCase());
if (this.map[next]) queue.unshift(this.map[next]);
}
return result;
}
subscribeAll(sources, destination) {
sources.forEach((source) => {
this.map[source] = this.map[source] || [];
this.map[source].push(destination);
});
}
}
主要特性:
- 动态依赖管理: 跟踪单元格之间的依赖关系。
- 更新传播: 当源单元格变化时更新依赖单元格。
- 广度优先搜索: 通过跟踪所有依赖节点避免无限循环。
示例用法:
JavaScript
let ps = new PubSub();
ps.subscribeAll(['A1'], 'B1');
ps.subscribeAll(['B1'], 'C1');
console.log(ps.get('A1')); // Output: ['B1', 'C1']
3. 创建行和单元格
JavaScript
class Cell {
constructor(cell, row, col) {
cell.id = `${String.fromCharCode(col + 65)}${row}`;
cell.setAttribute('data-eq', '');
cell.setAttribute('data-value', '');
if (row > 0 && col > -1) cell.classList.add('editable');
cell.textContent = col === -1 ? row : '';
}
}
class Row {
constructor(row, r) {
for (let c = -1; c < 13; c++) {
new Cell(row.insertCell(), r, c);
}
}
}
关键特性:
- 动态表格生成: 允许程序化添加行和列。
- 单元格识别: 根据位置生成ID(例如,A1,B2)。
- 可编辑单元格: 只有在有效(非表头行/列)的情况下,单元格才可编辑。
为什么使用动态行和单元格?
这种方法允许表格大小可扩展且灵活,支持在不改变结构的情况下添加行或列。
4. 交互的事件处理
JavaScript
addEventListeners() {
this.table.addEventListener('click', this.onCellClick.bind(this));
this.table.addEventListener('dblclick', this.onCellDoubleClick.bind(this));
window.addEventListener('keydown', this.onKeyDown.bind(this));
}
关键特性:
- 点击事件: 选择或编辑单元格。
- 双击事件: 启用公式编辑。
- 按键事件: 支持使用方向键导航。
5. 公式解析和评估
JavaScript
function calcCell(expression) {
if (!expression) return 0;
return expression.split('+').reduce((sum, term) => {
let value = isNaN(term) ? getCellValue(term) : Number(term);
if (value === null) throw new Error(`Invalid cell: ${term}`);
return sum + Number(value);
}, 0);
}
关键特性:
- 动态计算: 计算引用其他单元格的公式。
- 递归评估: 解决嵌套依赖关系。
- 错误处理: 识别无效引用和循环依赖。
6. 用户输入的错误处理
JavaScript
function isValidCell(str) {
let regex = /^[A-Z]{1}[0-9]+$/;
return regex.test(str);
}
关键特性:
- 验证: 确保输入引用有效的单元格ID。
- 可扩展性: 支持动态表格扩展而不破坏验证。
涵盖的JavaScript主题
1. 事件处理
管理交互,如点击和按键。
Plain Text
window.addEventListener('keydown', this.onKeyDown.bind(this));
2. DOM操作
动态创建和修改DOM元素。
Plain Text
let cell = document.createElement('td'); cell.appendChild(document.createTextNode('A1'));
3. 递归
动态处理依赖关系。
Plain Text
function calcCell(str) { if (isNaN(str)) { return calcCell(getCellValue(str)); } }
4. 错误处理
检测无效单元格和循环依赖。
Plain Text
if (!isValidCell(p)) throw new Error(`invalid cell ${p}`);
结论
该项目展示了一个强大的电子表格,使用JavaScript构建。它利用事件处理、递归和发布/订阅模式,为复杂的Web应用程序奠定基础。通过添加导出数据、图表或格式规则等功能来扩展它。
参考文献
Source:
https://dzone.com/articles/spreadsheet-application-javascript-guide