使用 Excel 加载项共同创作

当多个用户和外接程序在同一 Excel 工作簿中工作时,一个用户所做的更改可能会在另一个用户的外接程序实例中创建意外行为。 当共同创作者修改工作簿时,加载项的缓存值可能会过时。 这种过时的数据会导致加载项显示不正确的数据、根据过时的信息做出决策或创建合并冲突。

为什么外接程序开发人员需要处理共同创作

Excel 支持在 OneDrive、OneDrive for Business 或 SharePoint Online 上存储的工作簿中共同创作。 启用自动保存后,更改会实时同步。 外接程序代码不会自动知道共同创作者何时修改工作簿

如果加载项:

  • 在 JavaScript 变量中缓存工作簿值 (过时数据) 的风险。
  • 将状态存储在隐藏的工作表中, (丢失同步) 的风险。
  • 使用 TableRowCollection.add () 合并冲突的风险向表添加行。
  • 显示 UI 以响应数据更改 (所有用户) 出现意外对话框的风险。

如果外接程序在启动时仅读取一次数据,或者很少在共享工作簿中运行,则共同创作支持优先级较低,但仍应受到监视。

重要

在 Microsoft 365 专属 Excel 中,自动保存会实时同步更改。 打开“自动保存”后,共同创作问题变得更加频繁和明显。 在启用“自动保存”的情况下测试加载项,以确定潜在问题。 用户可以通过 Excel 窗口左上角的开关切换自动保存。

Excel 同步工作簿内容,而不是加载项的内存

Excel 自动在所有共同创作者之间同步工作簿内容 (,例如单元格值、格式设置、表格数据) 。 但是, Excel 不会同步加载项的 JavaScript 变量、对象或内存中状态。 每个用户使用单独的内存运行自己的外接程序实例。

问题:缓存变量中的过时数据

在以下代码中,用户 A 的 cachedValue 变量永远不会自动更新。 如果外接程序逻辑用于 cachedValue 计算、显示或决策,则它使用的是过时的信息。

// User A's add-in reads a value.
const range = context.workbook.worksheets.getActiveWorksheet().getRange("A1");
range.load("values");
await context.sync();

const cachedValue = range.values[0][0]; // Stores "Contoso".
console.log(cachedValue); // "Contoso"

// Meanwhile, User B (coauthor) changes A1 to "Fabrikam".
// User B's change synchronizes to the workbook.

// User A's add-in still has the old value.
console.log(cachedValue); // Still "Contoso" - STALE!

// The workbook has the new value.
range.load("values");
await context.sync();
console.log(range.values[0][0]); // "Fabrikam" - CURRENT

每个共同创作者都有自己的单独的外接程序实例。 将工作簿值复制到 JavaScript 变量时,这些副本不会与工作簿保持同步。 必须显式刷新值或使用事件来检测更改。

解决方案:使用事件检测共同创作者更改

若要在共同创作者修改工作簿时保持外接程序的状态同步,请使用 Excel 事件。 当工作簿内容发生更改时,事件会通知加载项,以便你可以刷新缓存的数据或更新 UI。

应用场景 要使用的事件 Reason
隐藏的工作表存储设置 BindingDataChanged 检测共同创作者何时更改配置
仪表板显示单元格值 BindingDataChanged 使显示与工作簿保持同步
监视更改的特定范围 WorksheetChanged 更灵活地用于复杂更改检测
跟踪任何工作表修改 WorksheetChanged 更广泛的更改意识

示例:使仪表板保持同步

场景:加载项显示仪表板显示单元格 A1:C10 中的数据。 如果没有事件处理,仪表板在共同创作者更新这些单元格时显示过时的数据。

以下代码使用 BindingDataChangedbindingDataChanged) (事件。 每当任何用户 (本地或共同创作) 修改绑定范围时,此事件将激活。 事件处理程序刷新缓存的数据,以便所有用户都能看到最新信息。

let cachedData = null;

// Initial load.
async function loadDashboard() {
  await Excel.run(async (context) => {
    const range = context.workbook.worksheets.getActiveWorksheet().getRange("A1:C10");
    range.load("values");
    await context.sync();
    
    cachedData = range.values;
    updateDashboardDisplay(cachedData);
  });
}

// Set up event to detect changes from coauthors.
async function setupCoauthoringSupport() {
  await Excel.run(async (context) => {
    const sheet = context.workbook.worksheets.getActiveWorksheet();
    const range = sheet.getRange("A1:C10");
    
    // Create a binding to enable change detection.
    const binding = context.workbook.bindings.add(range, Excel.BindingType.range, "DashboardData");
    await context.sync();
    
    // Register event handler for data changes.
    binding.onDataChanged.add(handleDataChange);
    await context.sync();
  });
}

// This activates when coauthors change the bound range.
async function handleDataChange(event) {
  await Excel.run(async (context) => {
    const binding = context.workbook.bindings.getItem("DashboardData");
    const range = binding.getRange();
    range.load("values");
    await context.sync();
    
    // Update cached data and refresh display.
    cachedData = range.values;
    updateDashboardDisplay(cachedData);
  });
}

function updateDashboardDisplay(data) {
  // Update your UI with the current data.
  console.log("Dashboard refreshed with current data");
}

不在事件处理程序中显示 UI

当共同创作处于活动状态时,当任何用户进行更改时,事件处理程序将针对所有用户运行。 此行为会创建关键设计约束。

❌ 不要这样做

binding.onDataChanged.add(async (event) => {
  // This is a bad pattern. It shows a dialog to all users when any user changes data.
  Office.context.ui.displayDialogAsync("https://contoso.com/validation.html");
});

当用户 B 更改单元格时,即使用户 A 未进行任何更改,用户 A 也会意外看到验证对话框。 这种体验令人困惑和破坏性。

✅ 请改为执行此作

let cachedData = null;

binding.onDataChanged.add(async (event) => {
  await Excel.run(async (context) => {
    const range = event.binding.getRange();
    range.load("values");
    await context.sync();
    
    // Update internal state silently.
    cachedData = range.values;
    
    // Update displayed values without dialogs or alerts.
    document.getElementById("dashboard").textContent = JSON.stringify(cachedData);
  });
});

// Only show UI in response to explicit user actions.
document.getElementById("showData").onclick = async () => {
  // Now it's OK to show UI - user clicked a button.
  Office.context.ui.displayDialogAsync("https://contoso.com/validation.html");
};

使用事件更新加载项的内部状态和被动显示。 仅显示对话框、警报或模式 UI 以响应显式用户作,例如按钮单击或菜单选择。

避免共同创作方案中的表行冲突

当加载项在共同创作者编辑同一表格或附近单元格时使用TableRowCollection.add时,Excel 会检测合并冲突。 用户看到一个黄色条形图,提示他们刷新,并且最近的更改可能会丢失。

API 以 TableRowCollection.add 与同时编辑冲突的方式更改表结构。 当用户 A 的加载项在编辑单元格 B5 时添加行时,Excel 无法安全地合并这两个更改。

使用 Range.values 添加行

不要使用表 API,而是在表正下方的范围内设置值。 Excel 会自动展开表,而不会产生冲突。

❌ 请勿执行此作, (它会导致) 冲突

const table = context.workbook.tables.getItem("SalesData");
table.rows.add(null, [["Product", 100, "=B2*1.2"]]);
// This is a bad pattern. This code causes coauthoring conflicts.

✅ 使用此方法

await Excel.run(async (context) => {
  const table = context.workbook.tables.getItem("SalesData");
  const tableRange = table.getRange();
  tableRange.load("rowCount, address");
  await context.sync();
  
  // Get the range directly below the table.
  const sheet = context.workbook.worksheets.getActiveWorksheet();
  const newRowRange = table.getDataBodyRange().getRowsBelow(1);
  
  // Set values - table automatically expands without conflicts.
  newRowRange.values = [["Product", 100, "=B2*1.2"]];
  await context.sync();
});

其他要求

若要使 Range.values 方法可靠工作:

  1. 表下方没有数据验证规则:从表下面的单元格中删除 数据验证规则 ,或者将验证应用于整个列而不是特定单元格区域。

  2. 处理表下方的现有数据:如果用户在表下方有数据,请首先插入一个空白行。

    // Insert empty row to push existing data down.
    let insertRange = table.getDataBodyRange().getRowsBelow(1);
    insertRange.insert(Excel.InsertShiftDirection.down);
    await context.sync();
    
    // Now set your data.
    insertRange = table.getDataBodyRange().getRowsBelow(1);
    insertRange.values = [["Product", 100, "=B2*1.2"]];
    
  3. 无法添加真正空的行:表仅在设置实际数据时自动展开。 如果需要空行,请使用解决方法。

    • 将临时数据 (如空格字符) 置于隐藏列中。
    • 使用用户可以清除的占位符数据。

排查常见的共同创作问题

症状 可能的原因 修补程序
加载项显示过时的值 JavaScript 变量中缓存的值 实现事件处理程序以在更改时刷新
黄色“刷新”栏经常出现 使用 TableRowCollection.add 切换到 以 Range.values 添加行
对话框意外弹出 在事件处理程序中显示 UI 仅显示用户启动的作的 UI
设置不会在用户之间同步 未监视更改的隐藏工作表 在设置范围上添加 BindingDataChanged 事件
共同创作期间丢失的更改 表修改产生的合并冲突 遵循表行最佳做法

另请参阅