HTML 服务:模板化 HTML

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

您可以轻松将 Apps 脚本代码和 HTML 搭配使用,以生成动态网页。如果您使用过混合了代码和 HTML 的模板语言(例如 PHP、ASP 或 JSP),则应该熟悉这种语法。

脚本

Apps 脚本模板可以包含三种特殊标记,称为 scriptlet。在 Scriptlet 中,您可以编写任何可以在正常的 Apps 脚本文件中运行的代码:scriptlet 可以调用其他代码文件中定义的函数、引用全局变量,或使用任何 Apps Script API。您甚至可以在 Scriptlet 中定义函数和变量,但请注意,在代码文件或其他模板中定义的函数不能调用它们。

如果您将以下示例粘贴到脚本编辑器中,<?= ... ?> 标记(打印脚本)的内容将以斜体显示。在相应网页向用户提供之前,该斜体代码会在服务器上运行。由于 Scriptlet 代码在网页调用之前执行,因此在每个网页上只能运行一次;与您通过 google.script.run 调用的客户端 JavaScript 或 Apps 脚本函数不同,scriptlet 代码在网页加载后无法再次执行。

Code.gs

function doGet() {
  return HtmlService
      .createTemplateFromFile('Index')
      .evaluate();
}

Index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    Hello, World! The time is <?= new Date() ?>.
  </body>
</html>

请注意,模板化 HTML 的 doGet() 函数与创建和提供基本 HTML 的示例有所不同。此处显示的函数会从 HTML 文件生成一个 HtmlTemplate 对象,然后调用其 evaluate() 方法来执行此脚本脚本并将模板转换为 HtmlOutput 对象,脚本可以向用户提供该对象。

标准脚本

使用脚本 <? ... ?> 的标准脚本小程序在执行代码时不会明确向网页输出内容。不过,如下例所示,scriptlet 内的代码结果仍可能会影响 Scriptlet 之外的 HTML 内容:

Code.gs

function doGet() {
  return HtmlService
      .createTemplateFromFile('Index')
      .evaluate();
}

Index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <? if (true) { ?>
      <p>This will always be served!</p>
    <? } else  { ?>
      <p>This will never be served.</p>
    <? } ?>
  </body>
</html>

打印脚本

输出使用语法 <?= ... ?> 的脚本小程序会使用上下文转义功能将其代码的结果输出到网页中。

上下文转义意味着 Apps 脚本会跟踪网页上输出内容的上下文(HTML 属性内、客户端 script 标记内或任何其他位置),并自动添加转义字符以防范跨站脚本攻击 (XSS)

在此示例中,第一个输出 Scriptlet 直接输出一个字符串;紧接着是输出一个数组和循环的标准脚本,然后输出另一个输出脚本的内容。

Code.gs

function doGet() {
  return HtmlService
      .createTemplateFromFile('Index')
      .evaluate();
}

Index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <?= 'My favorite Google products:' ?>
    <? var data = ['Gmail', 'Docs', 'Android'];
      for (var i = 0; i < data.length; i++) { ?>
        <b><?= data[i] ?></b>
    <? } ?>
  </body>
</html>

请注意,输出 scriptlet 只输出其第一个语句的值;任何剩余语句的行为都类似于它们包含在标准脚本中。例如,scriptlet <?= 'Hello, world!'; 'abc' ?> 仅会输出“Hello, world!”

强制打印脚本

强制打印使用 <?!= ... ?> 的语法与输出 setlet 类似,不同之处在于前者避免上下文转义。

如果您的脚本允许不受信任的用户输入,则上下文转义非常重要。相比之下,如果脚本输出的内容有意包含您指定的 HTML 或脚本,您需要强制打印。

一般而言,除非您知道自己需要输出 HTML 或 JavaScript,否则请使用输出 Scriptlet,而不是强制打印。

Scriptlet 中的 Apps 脚本代码

Scriptlet 不限于运行普通的 JavaScript;您还可以使用以下三种技术之一向模板提供 Apps 脚本数据的访问权限。

不过请注意,由于模板代码会在网页呈现给用户之前执行,因此这些技术只能将初始内容提供给网页。如需以交互方式访问网页中的 Apps 脚本数据,请改用 google.script.run API。

从模板调用 Apps 脚本函数

Scriptlet 可以调用 Apps 脚本代码文件或库中定义的任何函数。此示例展示了一种将电子表格中的数据提取到模板中,然后根据数据构建 HTML 表格的方法。

Code.gs

function doGet() {
  return HtmlService
      .createTemplateFromFile('Index')
      .evaluate();
}

function getData() {
  return SpreadsheetApp
      .openById('1234567890abcdefghijklmnopqrstuvwxyz')
      .getActiveSheet()
      .getDataRange()
      .getValues();
}

Index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <? var data = getData(); ?>
    <table>
      <? for (var i = 0; i < data.length; i++) { ?>
        <tr>
          <? for (var j = 0; j < data[i].length; j++) { ?>
            <td><?= data[i][j] ?></td>
          <? } ?>
        </tr>
      <? } ?>
    </table>
  </body>
</html>

直接调用 Apps Script API

您也可以直接在 Scriptlet 中使用 Apps 脚本代码。此示例在模板本身中加载数据而不是通过单独的函数完成与上一个示例相同的结果。

Code.gs

function doGet() {
  return HtmlService
      .createTemplateFromFile('Index')
      .evaluate();
}

Index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <? var data = SpreadsheetApp
        .openById('1234567890abcdefghijklmnopqrstuvwxyz')
        .getActiveSheet()
        .getDataRange()
        .getValues(); ?>
    <table>
      <? for (var i = 0; i < data.length; i++) { ?>
        <tr>
          <? for (var j = 0; j < data[i].length; j++) { ?>
            <td><?= data[i][j] ?></td>
          <? } ?>
        </tr>
      <? } ?>
    </table>
  </body>
</html>

将变量推送到模板

最后,您可以将变量作为模板分配给 HtmlTemplate 对象,从而将其推送到模板中。同样,此示例可实现与前面示例相同的结果。

Code.gs

function doGet() {
  var t = HtmlService.createTemplateFromFile('Index');
  t.data = SpreadsheetApp
      .openById('1234567890abcdefghijklmnopqrstuvwxyz')
      .getActiveSheet()
      .getDataRange()
      .getValues();
  return t.evaluate();
}

Index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <table>
      <? for (var i = 0; i < data.length; i++) { ?>
        <tr>
          <? for (var j = 0; j < data[i].length; j++) { ?>
            <td><?= data[i][j] ?></td>
          <? } ?>
        </tr>
      <? } ?>
    </table>
  </body>
</html>

调试模板

由于编写的代码不是直接执行的,因此模板可能很难调试;相反,服务器会将模板转换为代码,然后执行生成的代码。

如果模板如何解读脚本小程序,HtmlTemplate 类中的两种调试方法有助于您更好地了解正在发生的情况。

getCode()

getCode() 会返回一个字符串,其中包含服务器根据模板创建的代码。如果您记录代码,然后将其粘贴到脚本编辑器中,则可以像运行常规 Apps 脚本代码一样运行它并进行调试

以下简单模板再次显示了 Google 产品列表,后跟 getCode() 的结果:

Code.gs

function myFunction() {
  Logger.log(HtmlService
      .createTemplateFromFile('Index')
      .getCode());
}

Index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <?= 'My favorite Google products:' ?>
    <? var data = ['Gmail', 'Docs', 'Android'];
      for (var i = 0; i < data.length; i++) { ?>
        <b><?= data[i] ?></b>
    <? } ?>
  </body>
</html>

日志(已评估)

(function() { var output = HtmlService.initTemplate(); output._ =  '<!DOCTYPE html>\n';
  output._ =  '<html>\n' +
    '  <head>\n' +
    '    <base target=\"_top\">\n' +
    '  </head>\n' +
    '  <body>\n' +
    '    '; output._$ =  'My favorite Google products:' ;
  output._ =  '    ';  var data = ['Gmail', 'Docs', 'Android'];
        for (var i = 0; i < data.length; i++) { ;
  output._ =  '        <b>'; output._$ =  data[i] ; output._ =  '</b>\n';
  output._ =  '    ';  } ;
  output._ =  '  </body>\n';
  output._ =  '</html>';
  /* End of user code */
  return output.$out.append('');
})();

getCodeWithAnnotation()

getCodeWithComments()getCode() 类似,但返回的评估代码是与原始模板并排显示的注释。

浏览评估的代码

在任意一个评估的代码示例中,您首先会发现由 HtmlService.initTemplate() 方法创建的隐式 output 对象。此方法未记录,因为只有模板本身才需要使用。output 是一个特殊的 HtmlOutput 对象,其中包含两个不为人知的属性,即 __$,它们是调用 append()appendUntrusted() 的简写形式。

output 还有一个特殊属性 $out,它引用一个不包含这些特殊属性的常规 HtmlOutput 对象。模板会在代码末尾返回该普通对象。

现在您已经了解了此语法,代码的其余部分应该相当容易遵循。Scriptlet 之外的 HTML 内容(例如 b 标记)使用 output._ = 附加(不使用上下文转义),并且 Scriptlet 附加为 JavaScript(无论是否使用上下文转义,具体取决于 textlet 的类型)。

请注意,评估的代码会保留模板中的行号。如果您在运行评估的代码时遇到错误,该行将与模板中的等效内容相对应。

评论的层次结构

由于求值的代码会保留行号,因此,scriptletlet 内的注释可以注释掉其他 scriptlet,甚至是 HTML 代码。以下示例展示了注释的一些令人惊讶的效果:

<? var x; // a comment ?> This sentence won't print because a comment begins inside a scriptlet on the same line.

<? var y; // ?> <?= "This sentence won't print because a comment begins inside a scriptlet on the same line.";
output.append("This sentence will print because it's on the next line, even though it's in the same scriptlet.”) ?>

<? doSomething(); /* ?>
This entire block is commented out,
even if you add a */ in the HTML
or in a <script> */ </script> tag,
<? until you end the comment inside a scriptlet. */ ?>