您可以混合使用 Apps 脚本代码和 HTML,以最少的精力生成动态网页。如果您曾使用过混合了代码和 HTML 的模板语言(例如 PHP、ASP 或 JSP),那么这种语法应该会很熟悉。
Scriptlet
Apps 脚本模板可以包含三个特殊标记(称为 scriptlet)。在 scriptlet 中,您可以编写任何可在常规 Apps 脚本文件中运行的代码:scriptlet 可以调用在其他代码文件中定义的函数、引用全局变量或使用任何 Apps 脚本 API。您甚至可以在 scriptlet 中定义函数和变量,但需要注意的是,它们无法被代码文件或其他模板中定义的函数调用。
如果您将以下示例粘贴到脚本编辑器中,<?= ... ?>
标记的内容(一个打印 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()
方法来执行 scriptlet 并将模板转换为脚本可向用户提供的 HtmlOutput
对象。
标准 scriptlet
使用 <? ... ?>
语法的标准 scriptlet 会执行代码,但不会将内容明确输出到网页。不过,如本例所示,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>
打印 scriptlet
使用 <?= ... ?>
语法的打印 scriptlet 会使用上下文转义将代码的结果输出到网页中。
上下文转义是指 Apps 脚本会跟踪网页上输出的上下文(在 HTML 属性内、在客户端 script
标记内或任何其他位置),并自动添加转义字符以防范跨站脚本攻击 (XSS)。
在此示例中,第一个打印 scriptlet 直接输出一个字符串;随后是一个设置数组和循环的标准 scriptlet,之后是另一个打印 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!”。
强制打印 scriptlet
强制打印 scriptlet 使用 <?!= ... ?>
语法,与打印 scriptlet 类似,但会避免上下文转义。
如果您的脚本允许不受信任的用户输入,那么上下文转义就非常重要。相比之下,如果您的 scriptlet 的输出有意包含您想要完全按指定方式插入的 HTML 或脚本,则需要强制打印。
一般来说,除非您知道需要原封不动地打印 HTML 或 JavaScript,否则请使用打印小程序,而不是强制打印小程序。
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 脚本 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>
调试模板
模板可能难以调试,因为您编写的代码不会直接执行;相反,服务器会将您的模板转换为代码,然后执行生成的代码。
如果您不清楚模板是如何解读您的 scriptlet 的,可以使用 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>
LOG(已评估)
(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('');
})();
getCodeWithComments()
getCodeWithComments()
与 getCode()
类似,但会以注释的形式返回评估后的代码,这些注释与原始模板并排显示。
浏览已评估的代码
在任何评估代码示例中,您首先会注意到方法 HtmlService.initTemplate()
创建的隐式 output
对象。此方法未记录在文档中,因为只有模板本身需要使用它。output
是一个特殊的 HtmlOutput
对象,具有两个名称不寻常的属性 _
和 _$
,它们是调用 append()
和 appendUntrusted()
的简写形式。
output
还有一个特殊属性 $out
,它指的是不具备这些特殊属性的常规 HtmlOutput
对象。该模板会在代码末尾返回该普通对象。
现在,您已经了解了这种语法,其余代码应该很容易理解。脚本程序块之外的 HTML 内容(例如 b
标记)使用 output._ =
(不含上下文转义)附加,而脚本程序块则作为 JavaScript 附加(含或不含上下文转义,具体取决于脚本程序块的类型)。
请注意,评估后的代码会保留模板中的行号。如果您在运行评估后的代码时遇到错误,该行将对应于模板中的等效内容。
评论层次结构
由于评估后的代码会保留行号,因此 scriptlet 中的注释可能会注释掉其他 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. */ ?>