将匿名/动态函数设置为菜单

Jay*_*der 3 javascript add-on menuitem google-sheets google-apps-script

我想在 Google Sheets 插件中为动态菜单设置动态功能。我正在使用以下代码:

function onOpen(e) {
  var menu = SpreadsheetApp.getUi().createAddonMenu();
  
  for (var i = 0; i < array.length; i++) {
        const element = array[i];
        var functionName = "_" + element.name;
        var args = element.args;
        
        this[functionName] = dynamicItem(args); //didn't work
        //this[functionName] = function () {myopen(args);} //didn't work
        //eval("function " + functionName + "() { myopen('" + args + "') }"); //didn't work
        menu.addItem(element.name, functionName);
      }
   menu.addToUi();
 }

 function dynamicItem(args) {
    return function () {
       myopen(args);
    };
 }
Run Code Online (Sandbox Code Playgroud)

当我单击菜单项时,出现以下异常:

“未找到脚本函数:函数名称

我从匿名函数动态菜单动态更新自定义菜单获得了帮助,但我不知道为什么它对我不起作用。

任何帮助将不胜感激。

谢谢。

The*_*ter 8

概括:

Google Apps 脚本在环境中运行stateless。存储在全局对象中的任何内容都不会跨会话维护。如果您在会话期间向全局对象添加某些内容,则该内容在下一次会话运行中不可用。this在用户界面实际调用任何函数之前,使用全局范围内的立即调用函数或调用函数来填充全局范围(对象)。

解释:

mr...@bbtv.com 在本问题的 comment#17中提到的解决方法以及 Tanaike 在本答案中提到的解决方法都利用闭包/立即调用函数 (IIFE) 填充全局范围。

要理解这一点,您需要了解何时读取和加载脚本函数名称。以下步骤按顺序发生:

  1. 菜单点击(按钮点击似乎跳过了步骤1和2,因此似乎不可拦截)

  2. 执行脚本编辑器中的所有脚本,并this创建全局中的函数名称列表(在此步骤中,没有运行/调用任何函数,但所有脚本都被完全执行)。这相当于使用脚本加载网页:<script>...code.gs...</script>

  3. 检查当前调用的按钮/菜单的功能名称是否存在于 global 中this

  4. 如果存在,则执行该函数(即调用链接按钮/菜单的函数名称所引用的函数)。这就像myFunction()在已加载的脚本末尾添加一样。如果没有找到,则抛出错误:Script function not found

  5. 脚本结束。这就像关闭加载的网页一样。所有“状态”都丢失了。没有全局范围或this 永久存储。

使用 动态添加菜单项时this[function-name],了解何时添加该功能非常重要。如果您在 期间添加它onOpen,那么在执行this期间这些函数将处于全局范围内onOpen,但在onOpen脚本执行完成后它会立即丢失。

function onOpen(){
  this['a'] = () => 'a';
  SpreadsheetApp.getUi()
    .createMenu("Test")
    .addItem("Call function a","a")
    .addToUi()
}
Run Code Online (Sandbox Code Playgroud)

这将成功地将a功能添加到Ui菜单,但请注意,该功能仅a执行期间添加到全局this范围。执行完成并创建新的(全局作用域)后,下次调用任何函数时(重复步骤 1 到 5),该值就会丢失。因此,当单击菜单时,步骤 2 创建一个新函数并查找所有脚本中指定的函数,但找不到任何函数,因为这个新创建的函数没有(因为已声明,但未执行,因此是这次没有添加)。 onOpenthisthisthisathisaonOpenathis

解决方案:

在步骤 2 期间或之前,您需要将该函数添加到全局this

function onOpen(){
  SpreadsheetApp.getUi()
    .createMenu("Test")
    .addItem("Call function a","a")
    .addToUi()
}
(function IIFE(){
  this['a'] = () => 'a';
})(); 
Run Code Online (Sandbox Code Playgroud)

上面的 IIFE 函数“每次”都会拦截步骤 2,调用任何函数。因此,a总是出现在this步骤 3 中或步骤 3 之后。在Tanaike 的解决方案中,这是installFunctions()在全局范围内完成的。每次调用任何函数时都会执行该函数。comment#17createMenuFunctions(this);中的情况也是如此。

文档摘录:

附加文档链接

警告:当 onOpen(e) 函数运行时,将加载整个脚本并执行所有全局语句。这些语句在与 onOpen(e) 相同的授权模式下执行,如果模式禁止它们,则会失败。这会阻止 onOpen(e) 运行。如果您发布的附加组件无法添加其菜单项,请查看浏览器的 JavaScript 控制台以查看是否引发了错误,然后检查您的脚本以查看 onOpen(e) 函数或全局变量是否调用了不允许的服务在 AuthMode.NONE 中。

示例脚本:

/**Runs every time any script function is called*/
(function IIFE(scope) {
  'use strict';
  scope['options'] = ['a', 'b', 'c']; //pollute current scope
  options.forEach(
    option =>
      (scope[option] = () =>
        SpreadsheetApp.getUi().alert(`You clicked option ${option}`))
  );
})(this);//pass global scope

function onOpen() {
  const testMenu = SpreadsheetApp.getUi().createMenu('Test');
  options.forEach(option =>
    testMenu.addItem('Call function ' + option, option)
  );
  testMenu.addToUi();
}
Run Code Online (Sandbox Code Playgroud)

参考: