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

如果自定义函数调用远程服务,可以使用批处理模式来减少对远程服务的网络调用次数。 为了减少网络往返,你可以将所有调用批处理为对 Web 服务的单个调用。 当重新计算电子表格时,此方法非常合适。

例如,如果有人在电子表格的 100 个单元格中使用了自定义函数,然后重新计算电子表格,则自定义函数将运行 100 次并进行 100 次网络调用。 通过使用批处理模式,可以将这些调用组合起来,在单次网络调用中完成总共 100 次计算。

重要

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

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

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

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

查看已完成的示例

若要查看已完成的示例,请遵循本文并将代码示例粘贴到自己的项目中。 例如,若要为 TypeScript 创建新的自定义函数项目,请使用 Office 外接程序的 Yeoman 生成器,然后将本文中的所有代码添加到项目中。 运行代码并试用。

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

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

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

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

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

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

自定义函数通过调用远程服务来执行运算并计算其所需的结果。 这为它们提供了一种将每个请求的运算存储到批处理中的方法。 稍后,你将看到如何创建 _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, you will need to modify this function appropriately 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 主体,而不是数组。
  • 修改函数以执行运算(或调用执行运算的函数)。
  • 应用相应的身份验证机制。 确保只有正确的调用者才可以访问该函数。
  • 将代码放入远程服务。

后续步骤

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

另请参阅