客户端脚本

客户端脚本在用户计算机浏览器的 JavaScript 运行时中运行。它们非常适合处理界面事件以及更改 DOM 元素和微件属性。

客户端脚本还可以通过应用制作工具 API 触发与服务器的交互。例如,客户端脚本可以提取和修改数据库中的数据,或者调用服务器脚本。

异步操作和回调函数

客户端脚本可以在应用制作工具中异步运行,以保持应用用户界面及时响应。在某些浏览器应用中,当操作需要等待某一外部资源时,整个应用都会随之停滞。界面亦会等待,并且没有响应。

使用异步代码时,界面和其他应用操作会保持及时响应,但您的脚本可能不会按编写的顺序运行。要在异步操作完成后运行脚本(例如处理操作结果的代码),请使用回调函数。

回调函数是在操作完成时运行的客户端脚本。您可以指定操作成功时运行的函数,或者指定针对成功和失败场景运行不同函数的对象。

成功专用回调函数

如果您添加成功专用回调函数,则仅在操作成功时该函数才会执行。如果操作失败,则没有任何反应。举个简单的例子,您可以按照下列方式使用 createItem () 异步创建记录并发送有关的提醒:

widget.datasource.createItem(function (record) {
      alert('Record with ID ' + record.id + ' was created in the database.');
    });
    

成功和失败回调函数

以下是上述示例的扩展,使用这种方法可以在失败时得到通知并执行错误报告、清理或回滚。

widget.datasource.createItem({
      success: function (record) {
        alert('Record with ID ' + record.id + ' was created in the database.');  // executes if record was created
      },
      failure: function (error) {
        console.info("No new record for you!"); // executes if record wasn't created
      }
    });
    

无回调函数

如果省略回调或传递 null 作为回调,则代码将继续,无需等待操作完成。例如:

    widget.datasource.createItem();
        console.info("warning, record is probably not yet created!");
    

问题排查

以下是排查客户端脚本以查找和修复错误的一些策略:

  • 在脚本编辑器中查找语法错误。如果键入内容存在语法问题,应用制作工具脚本编辑器会自动警告。检查脚本行号左侧的警告标签。这些警告可以识别遗忘括号等常见错误。

  • 在浏览器的 JavaScript 控制台中查找运行时错误。打开应用的预览实例或已部署版本,然后打开浏览器的 JavaScript 控制台。在 Chrome 中,按 Ctrl+Shift+jAlt-Cmd-j 打开 JavaScript 控制台。常见的运行时异常包括 null 解引用 (dereferencing) 或是未捕获的 throw 语句。您可以向脚本添加 debugger; 语句以中断脚本执行并找出问题区域(DevTools 必须向 debugger; 语句开放才能生效)。如需详细了解调试说明,请参阅 Chrome DevTools 中的调试 JavaScript 使用入门

  • 使用日志和提醒跟踪脚本的执行情况。使用浏览器的内置 console.log() 函数将日志发送到 JavaScript 控制台。或者使用 alert() 打开一个提醒框,该提醒框会中断脚本的执行,直到您将其解除为止。

客户端脚本示例

在绑定表达式中使用脚本

您可以使用 JavaScript 执行绑定表达式中的计算。举例来说,如果 Name 字段为空,或者 Age 字段的值小于 18,则按钮 enabled 属性的以下表达式将停用该按钮:

(@widget.parent.children.NameTextBox.value).length != 0 &&
    @widget.parent.children.AgeTextBox.value >= 18
    

应用制作工具解析器首先解析以 @ 开头的绑定表达式,以查找 NameAge 的值。然后它将替换表达式中的这些值,并评估 JavaScript 的其他部分。

第一行的括号表示绑定表达式的结束。没有它们,应用制作工具会观察更改的 length 属性,而不是 value 属性。

调用服务器脚本

调用服务器脚本是一种异步操作,因此您必须使用回调来处理脚本的结果。

论坛示例应用包含调用服务器脚本的客户端脚本示例。在此应用的 View 页面,客户端脚本在用户点击 Submit 按钮时运行。该脚本确认应用制作工具创建了记录并运行了运行服务器脚本 notifyForumOwnerAboutNewMessageServer 的客户端函数 notifyForumOwnerAboutNewMessageClient

为了清楚起见,我们在论坛示例应用中修改了以下代码。如需在用户点击 Submit 按钮时触发客户端脚本,可以自定义 onClick 事件:

handleSubmitButtonClick(widget)
    

此操作会调用以下客户端脚本中的 handleSubmitButtonClick 函数,然后调用 notifyForumOwnerAboutNewMessageClient 函数。notifyForumOwnerAboutNewMessageClient 函数调用服务器脚本中的 notifyForumOwnerAboutNewMessageServer 函数:

function notifyForumOwnerAboutNewMessageClient(forumKey, messageKey) {
      google.script.run
        .withSuccessHandler(function(result) {
          console.log('Email sent');
        })
        .withFailureHandler(function(error) {
          console.log('Email not sent ' + error.message);
        })
        .notifyForumOwnerAboutNewMessageServer(forumKey, messageKey);
    }

    function handleSubmitButtonClick(widget) {
      widget.datasource.createItem(function(createdRecord) {
        var forumKey = createdRecord.Forum._key;
        var messageKey = createdRecord._key;
        notifyForumOwnerAboutNewMessageClient(forumKey, messageKey);
      });
    }
    

以下服务器脚本会检查论坛版主是否已订阅论坛,然后通知订阅的版主该论坛有新帖:

function notifyForumOwnerAboutNewMessageServer(forumKey, messageKey) {
      var forum = app.models.Forum.getRecord(forumKey);
      if (forum.OwnerSubscribed) {
        var message = app.models.Message.getRecord(messageKey);

        try {
          MailApp.sendEmail({
            to: forum.Owner,
            subject: 'New message in your forum: "' + forum.Title +
              '" by ' + message.Author,
            htmlBody: message.Text
          });
        }
        catch (e) {
          console.log('Sending email notification for forumKey="%s" ' +
              'and messageKey="%s" failed.', forumKey, messageKey);
          console.log(e.message + '-> ' + e.stack);
        }
      }
    }
    

动态加载外部脚本

您可以使用外部 JavaScript 库同步加载多个第三方脚本,但其中有些需要额外步骤。需要回调参数的库(如 Google API 客户端库)需要通过向 Settings > General > onAppStart 添加脚本进行动态加载。

以下脚本加载了 Google+ 库:

// Suspends app loading until after the Google Client API loads.
    loader.suspendLoad();
    // Defines a callback function, for the client API. It must be global,
    // so it's explicitly attached to the window object.
    window.OnGapiClientLoad = function() {
      // Uses the Google Client API to load the Google+ library.
      gapi.client.load("plus", "v1", function() {
        // Continues loading the app once Google+ loads.
        loader.resumeLoad();
      });
    };

    var script = document.createElement("script");
    script.setAttribute("type", "text/javascript");

    // Specifies the name of the callback function in the "onload"
    // parameter of the URL.
    var url = "https://apis.google.com/js/client.js?onload=OnGapiClientLoad";
    script.setAttribute("src", url);

    document.getElementsByTagName("head")[0].appendChild(script);
    

调用 Google API

下面是最后一个示例,会稍微深入一点,让您了解实际应用制作工具脚本是什么样子。它使用了 Google JavaScript API

该脚本在页面中显示 Google 地图,并根据用户输入进行更新。它使用名为 Map 的页面,页面包含以下微件:

  • 名为 StreetZip 的两个文本字段。
  • 名为 MapDiv 的 200x200 HtmlArea,它有一个调用 loadMaps()onAttach 事件
  • 一个按钮,带有一个调用 updateMap()onClick() 处理程序

脚本:

var map;
    var geocoder;
    var marker;

    // Called by loadMaps() to set up widgets.
    function createMap() {
      var div = app.pages.Map.descendants.MapDiv.getElement();
      map = new google.maps.Map(div, {
        center: new google.maps.LatLng(-34.397, 150.644),
        mapTypeId: google.maps.MapTypeId.ROADMAP,
        zoom: 10,
      });
      geocoder = new google.maps.Geocoder();
      marker = new google.maps.Marker({map: map});
    }

    // Returns an address string from Street and Zip text fields.
    function getAddress() {
      var page = app.pages.Map;
      var street = page.descendants.Street.value;
      var zip = page.descendants.Zip.value;
      return street + ", " + zip;
    }

    // Positions the map at the given location coordinates.
    function showLocation(locations, status) {
      if (locations.length > 0) {
        var latLng = locations[0].geometry.location;
        map.panTo(latLng);
        marker.setPosition(latLng);
      }
    }

    // Sets up the Google Maps library.
    function loadMaps() {
      google.load("maps", "3", {callback: createMap});
    }

    function updateMap() {
      geocoder.geocode({address: getAddress()}, showLocation);
    }
    

脚本的重点:

  • MapDiv 使用其 onAttach() 处理程序来设置 Google Maps API
  • 该脚本使用 getAddress() 从 Street 和 Zip 字段拉取数据,以获取用户输入。