远程服务的 Batch 自定义函数调用

使用批处理将远程服务的调用分组为单个网络请求。 这可以减少到远程服务的网络往返次数,并帮助工作表更快地完成重新计算。

下面是批处理示例方案:如果 100 个单元格调用自定义函数,请发送一个列出所有 100 个作的请求。 该服务在单个响应中返回 100 个结果。

要点

若要为自定义函数设置批处理,需要编写三个主要的代码部分。

  1. 一个 推送作 ,用于在 Excel 每次调用自定义函数时向调用批添加新作。
  2. 一个 函数,用于在批处理准备就绪时发出远程请求
  3. 用于响应批处理请求、计算所有作结果并返回值的服务器代码。

重要

请注意,以下平台上可以使用 Excel 自定义函数。

  • Office 网页版
  • Windows 版 Office
    • Microsoft 365 订阅
    • 零售永久 Office 2016 及更高版本
    • 批量许可的永久/LTSC Office 2021及更高版本
  • Mac 版 Office

以下各项当前不支持 Excel 自定义函数:

  • iPad 版 Office
  • Windows 上批量许可的 Office 2021 或更早版本的永久版本

注意

Microsoft 365 的统一清单目前不支持自定义函数项目。 必须对自定义函数项目使用仅外接程序清单。 有关详细信息,请参阅 Office 外接程序清单

创建本文中所述的批处理模式

在以下部分中,你将了解如何一次构造一个示例代码。 建议使用 Office 加载项生成器的 Yeoman 生成器 创建一个全新的自定义函数项目。 若要创建新项目,请参阅 开始开发 Excel 自定义函数。 可以使用 TypeScript 或 JavaScript。

提示

若要查看已完成的示例,请使用 Office 外接程序的 Yeoman 生成器创建新的自定义函数项目。将代码示例复制并粘贴到项目中,然后运行代码并试用。

或者,在 自定义函数批处理模式中下载或查看完整的示例项目。 如果要在继续之前查看整个代码,请查看 脚本文件

批处理对自定义函数的每次调用

自定义函数的工作原理是调用远程服务来执行作并返回结果。 这为它们提供了一种将每个请求的运算存储到批处理中的方法。 稍后,你将看到如何创建 _pushOperation 函数来批处理这些运算。 首先,查看以下代码示例,了解如何从自定义函数调用 _pushOperation

在下面的代码中,自定义函数执行除法,但实际计算依赖于远程服务。 它调用 _pushOperation,从而将该运算与其他运算一起批处理到远程服务。 它将该运算命名为“div2”。 你可以为运算使用任何所需的命名方案,只要远程服务也使用相同的方案即可(稍后将对远程服务方面进行详细介绍)。 此外,还将传递远程服务运行该运算所需的参数。

div2添加自定义函数

根据是否使用 JavaScript TypeScript) ,将以下代码添加到functions.js或 functions.ts 文件 (。

/**
 * Divides two numbers using batching
 * @CustomFunction
 * @param dividend The number being divided
 * @param divisor The number the dividend is divided by
 * @returns The result of dividing the two numbers
 */
function div2(dividend, divisor) {
  return _pushOperation("div2", [dividend, divisor]);
}

添加用于跟踪批处理请求的全局变量

接下来,将两个全局变量添加到 functions.jsfunctions.ts 文件。 _isBatchedRequestScheduled 对于对远程服务的批处理调用进行计时,这一点非常重要。

let _batch = [];
let _isBatchedRequestScheduled = false;

_pushOperation添加 函数

当 Excel 调用自定义函数时,需要将作推送到批处理数组中。 以下 _pushOperation 函数代码演示如何从自定义函数添加新作。 它会创建一个新的批处理条目,创建一个新的承诺来解决或拒绝相应运算,并将该条目推送到批处理数组中。

此段代码还会检查是否对批处理进行了安排。 在本例中,将每个批处理安排为每 100 毫秒运行一次。 可以根据需要调整此值。 值越大,发送到远程服务的批处理越大,用户查看结果的等待时间越长。 较低的值倾向于向远程服务发送更多的批处理,但可为用户提供较快的响应时间。

函数创建一个 invocationEntry 对象,该对象包含要运行的作的字符串名称。 例如,如果有两个分别名为 multiplydivide 的自定义函数,则可以在批处理条目中将它们作为运算名称重复使用。 args 保存从 Excel 传递到自定义函数的参数。 最后, resolvereject 方法存储包含远程服务返回的信息的承诺。

将以下代码添加到 functions.jsfunctions.ts 文件。

// This function encloses your custom functions as individual entries,
// which have some additional properties so you can keep track of whether or not
// a request has been resolved or rejected.
function _pushOperation(op, args) {
  // Create an entry for your custom function.
  console.log("pushOperation");
  const invocationEntry = {
    operation: op, // e.g., sum
    args: args,
    resolve: undefined,
    reject: undefined,
  };

  // Create a unique promise for this invocation,
  // and save its resolve and reject functions into the invocation entry.
  const promise = new Promise((resolve, reject) => {
    invocationEntry.resolve = resolve;
    invocationEntry.reject = reject;
  });

  // Push the invocation entry into the next batch.
  _batch.push(invocationEntry);

  // If a remote request hasn't been scheduled yet,
  // schedule it after a certain timeout, e.g., 100 ms.
  if (!_isBatchedRequestScheduled) {
    console.log("schedule remote request");
    _isBatchedRequestScheduled = true;
    setTimeout(_makeRemoteRequest, 100);
  }

  // Return the promise for this invocation.
  return promise;
}

发出远程请求

_makeRemoteRequest 函数的目的是将一批运算传递给远程服务,然后将结果返回给每个自定义函数。 它首先创建批处理数组的副本。 这样,来自 Excel 的并发自定义函数调用便可以立即在新数组中开始批处理。 然后将副本转换为不包含承诺信息的较简单的数组。 将这些承诺传递给远程服务是没有意义的,因为它们不会发生作用。 _makeRemoteRequest 将根据远程服务返回的内容拒绝或解决每个承诺。

将以下代码添加到 functions.jsfunctions.ts 文件。

// This is a private helper function, used only within your custom function add-in.
// You wouldn't call _makeRemoteRequest in Excel, for example.
// This function makes a request for remote processing of the whole batch,
// and matches the response batch to the request batch.
function _makeRemoteRequest() {
  // Copy the shared batch and allow the building of a new batch while you are waiting for a response.
  // Note the use of "splice" rather than "slice", which will modify the original _batch array
  // to empty it out.
  try{
  console.log("makeRemoteRequest");
  const batchCopy = _batch.splice(0, _batch.length);
  _isBatchedRequestScheduled = false;

  // Build a simpler request batch that only contains the arguments for each invocation.
  const requestBatch = batchCopy.map((item) => {
    return { operation: item.operation, args: item.args };
  });
  console.log("makeRemoteRequest2");
  // Make the remote request.
  _fetchFromRemoteService(requestBatch)
    .then((responseBatch) => {
      console.log("responseBatch in fetchFromRemoteService");
      // Match each value from the response batch to its corresponding invocation entry from the request batch,
      // and resolve the invocation promise with its corresponding response value.
      responseBatch.forEach((response, index) => {
        if (response.error) {
          batchCopy[index].reject(new Error(response.error));
          console.log("rejecting promise");
        } else {
          console.log("fulfilling promise");
          console.log(response);

          batchCopy[index].resolve(response.result);
        }
      });
    });
    console.log("makeRemoteRequest3");
  } catch (error) {
    console.log("error name:" + error.name);
    console.log("error message:" + error.message);
    console.log(error);
  }
}

根据自己的解决方案修改 _makeRemoteRequest

_makeRemoteRequest 函数调用 _fetchFromRemoteService,正如稍后将会看到的,后者只是一个表示远程服务的模拟。 这使得研究和运行本文中的代码更加容易。 但是,如果要将此代码用于实际的远程服务,则应进行以下更改。

  • 决定如何通过网络将批处理运算序列化。 例如,你可能希望将数组放入 JSON 主体中。
  • 你不需要调用 _fetchFromRemoteService,而是需要对传递批量运算的远程服务进行实际的网络调用。

处理远程服务上的批处理调用

最后一步是处理远程服务中的批处理调用。 下面的代码示例展示了 _fetchFromRemoteService 函数。 此函数会解包每个运算,执行指定的运算,并返回结果。 出于学习目的,在本文中,_fetchFromRemoteService 函数适用于在 Web 加载项中运行并模拟远程服务。 可以将此代码添加到 functions.jsfunctions.ts 文件,以便可以研究并运行本文中的所有代码,而无需设置实际的远程服务。

将以下代码添加到 functions.jsfunctions.ts 文件。

// This function simulates the work of a remote service. Because each service
// differs, modify this function as needed to work with the service you are using.
// This function takes a batch of argument sets and returns a promise that may contain a batch of values.
// NOTE: When implementing this function on a server, also apply an appropriate authentication mechanism
//       to ensure only the correct callers can access it.
async function _fetchFromRemoteService(requestBatch) {
  // Simulate a slow network request to the server.
  console.log("_fetchFromRemoteService");
  await pause(1000);
  console.log("postpause");
  return requestBatch.map((request) => {
    console.log("requestBatch server side");
    const { operation, args } = request;

    try {
      if (operation === "div2") {
        // Divide the first argument by the second argument.
        return {
          result: args[0] / args[1]
        };
      } else if (operation === "mul2") {
        // Multiply the arguments for the given entry.
        const myResult = args[0] * args[1];
        console.log(myResult);
        return {
          result: myResult
        };
      } else {
        return {
          error: `Operation not supported: ${operation}`
        };
      }
    } catch (error) {
      return {
        error: `Operation failed: ${operation}`
      };
    }
  });
}

function pause(ms) {
  console.log("pause");
  return new Promise((resolve) => setTimeout(resolve, ms));
}

根据自己的实时远程服务修改 _fetchFromRemoteService

若要修改函数 _fetchFromRemoteService 以在实时远程服务中运行,请进行以下更改。

  • 根据服务器平台(Node.js 或其他平台),将客户端网络调用映射到此函数。
  • 删除作为模拟的一部分来模拟网络延迟的 pause 函数。
  • 修改函数声明,以便在出于网络目的更改传递的参数时使用该参数。 例如,它可能是要处理的批量运算的 JSON 主体,而不是数组。
  • 修改函数以执行运算(或调用执行运算的函数)。
  • 应用相应的身份验证机制。 确保只有正确的调用者才可以访问该函数。
  • 将代码放入远程服务。

何时避免批处理

批处理会添加少量延迟和一些额外的代码。 在以下情况下避免批处理。

应用场景 批处理的负面影响 建议
单个或极少数调用 额外等待计时器 如果列表仍为空,则直接调用服务
每次调用的输入数据非常大 请求可能会变得太大 限制大小或单独发送这些调用
某些调用比其他调用慢得多 一个缓慢的呼叫延迟较快的调用 单独对慢速类型进行分组
需要近即时结果 (小于 50 毫秒) 计时器增加延迟 使用较短的计时器或跳过批处理
服务器已合并工作 无权益 跳过客户端上的批处理

后续步骤

了解可以在自定义函数中使用的各种参数。 或者查阅通过自定义函数进行 Web 调用后面的基础知识。

另请参阅