谷歌应用脚本学习指南-全-

谷歌应用脚本学习指南(全)

原文:zh.annas-archive.org/md5/8169fee63cb5a68b7a305cdeff22231e

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Google Apps 是一系列应用程序的集合,包括 Gmail、日历、Drive、文档、表格和表单。您可以使用 JavaScript 和 Google 定义的类来自定义或自动化 Google Apps。Google 基于 JavaScript 实现了 Google Apps Script (GAS)。

几乎所有 Google Apps 都提供了一项或多项服务。GAS 服务和 API 提供了轻松访问自动化 Google 产品和第三方服务的途径。您可以在您的 GAS 代码中使用这些服务类来自定义或自动化 Google Apps。

本书首先介绍基本知识,然后逐步通过实际代码和示例介绍高级概念。通过阅读本书,您将积累 Google Apps Script 的专业知识。祝您阅读愉快!

本书涵盖的内容

第一章, 介绍 Google Apps 脚本,介绍了 Google Apps 并为您提供了 Apps Scripts 的简介,解释了如何创建项目,并介绍了自定义公式。

第二章, 创建基本元素,涵盖了多种类型的对话框以及如何创建和显示它们,如何使用 Logger 类记录值,以及如何调试您的脚本。

第三章, 解析和发送电子邮件,讨论了 ContactApp、MailApp 和 GmailApp 服务。使用这些服务,您将创建许多有用的现实世界应用程序,包括电子邮件合并应用程序。

第四章, 创建交互式表单,讨论了通过脚本动态创建表单、将脚本发布为网络应用程序、使用 HtmlService 创建表单、创建电子投票应用程序以及创建票务预订应用程序。

第五章, 创建 Google 日历和 Drive 应用程序,教导读者如何创建日历事件并将事件从一个日历同步到另一个日历。本章还介绍了如何启用 GAS 高级服务。

第六章, 创建 Feed 阅读器和翻译器应用程序,是关于学习和创建许多有用的应用程序,包括 RSS/Atom 阅读器和语言翻译应用程序。

第七章, 创建交互式网页,讲述了如何使用 HtmlService 创建 RSS 订阅/发布者、文件上传应用程序以及一个完整的工时表应用程序。

第八章, 构建工作流应用程序,解释了如何创建工作流应用程序,并继续创建一个有用的现实世界订单处理应用程序。

第九章, 更多小贴士和技巧以及创建插件,主要关于使用包括 OAuth2 和 Apps Script 插件在内的外部库。

从下拉菜单中选择您购买此书的来源。

您需要任何现代浏览器以及基本的 HTML、CSS 和 JavaScript 工作或理论知识。

本书面向的是对 Google Apps Script 的新手,他们拥有较少的网页开发实践经验,并渴望在自定义 Google Apps 和开发 Web 应用方面积累专业知识。

注意

术语约定

在这本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

本书面向的是对 Google Apps Script 的新手,他们拥有较少的网页开发实践经验,并渴望在自定义 Google Apps 和开发 Web 应用方面积累专业知识。

代码块设置如下:

function greeting() {
  Browser
}

当我们希望将您的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:

  </head>
  <body>
    <button onclick="alert('Hello World!');">Click Me</button>
  </body>
</html>

新术语重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“转到插件 | 第二章 | 显示对话框”,并将弹出模态对话框。

要发送给我们一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。

警告或重要注意事项以如下框内形式出现。

小贴士

小贴士和技巧看起来像这样。

读者反馈

我们的读者反馈始终受到欢迎。请告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。

您需要为此书准备什么

如果您在某个主题上具有专业知识,并且您对撰写或为此书做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在,您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从www.packtpub.com的账户下载此书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“编辑器中将有默认的myFunction函数。”

  3. 悬停鼠标指针在顶部的支持标签上。

  4. 点击代码下载与勘误

  5. 选择您想要下载代码文件的书籍。

  6. 搜索框中输入书籍名称。

  7. 点击代码下载

一旦文件下载完成,请确保您使用最新版本的以下软件解压或提取文件夹:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

错误清单

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这个问题,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击错误提交表单链接,并输入您的错误清单详情来报告它们。一旦您的错误清单得到验证,您的提交将被接受,错误清单将被上传到我们的网站或添加到该标题的错误清单部分。

要查看之前提交的错误清单,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在错误清单部分。

盗版

互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的非法复制我们的作品,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过发送电子邮件到 <copyright@packtpub.com> 并附上疑似盗版材料的链接与我们联系。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面所提供的帮助。

问题

如果您在这本书的任何方面遇到问题,您可以通过发送电子邮件到 <questions@packtpub.com> 联系我们,我们将尽力解决问题。

第一章. 介绍 Google Apps Script

我知道在世界上可能没有一个人在一生中从未使用过谷歌的任何产品或服务。

谷歌以其著名的搜索引擎、视频服务门户 YouTube,以及其众多的网络应用而闻名,包括 Gmail、日历、Drive、文档、表格和表单。它还提供云计算和其他软件服务。

“谷歌”这个词甚至已经成为一个动词,指的是进行网络搜索。如今,你听到人们说“我 Googled 某事”而不是“我在网上搜索某事”。在本章中,你将了解 Google 应用、应用脚本以及如何创建自定义公式/函数。

Google 应用

Google 应用是一系列应用的集合,包括 Gmail、日历、Drive、文档、表格和表单。从现在起,我们将使用“Google 应用”或简称“Apps”这个术语。

在我们开始之前,我会快速回答你可能会有的几个问题:

  • 所有这些应用在哪里运行?在你的电脑上?

    不,所有这些应用都运行在谷歌的基于云的服务器上。

  • 你如何获取这些应用的访问权限?

    你可以通过网络浏览器与这些应用交互。除了在你的桌面、笔记本电脑、平板电脑或智能手机上安装的现代网络浏览器外,不需要特殊的硬件或软件安装。

Google 应用

Google Apps Script

你可以使用 JavaScript 脚本语言以及谷歌定义的类(称为Google Apps ScriptGAS))来自定义或自动化 Google 应用。谷歌基于 JavaScript 1.6 以及 1.7 和 1.8 的部分实现 GAS。GAS 服务和 API 提供了方便的访问,用户可以自动化跨谷歌产品和第三方服务的任务。

你可以使用 GAS 在 Google Docs、Sheets 和 Forms 中编写代码,并自动化类似于 Visual Basic for Applications 在 Microsoft Office 中执行的任务。然而,GAS 在谷歌的服务器上运行,结果在浏览器中呈现。集成的脚本编辑器允许你在浏览器内编辑和调试你的脚本,你不需要安装任何东西。你可以激活调试和测试过的脚本功能,使其基于你的交互或基于对事件或时间间隔(分钟、小时、天、周、未来日期等)的触发器运行。这些事件包括onOpenonEditonInstall等。GAS 还用于创建 Docs、Sheets 和 Forms 的附加组件。

GAS 可以帮助你自动化任务的各个方面——你甚至可以用它来在预定的时间/日期订购披萨!

Visual Basic for Applications

微软实现了应用程序的 Visual BasicVBA)来帮助自动化如 Excel 和 Word 等 Office 应用程序。对于每个相应的应用程序,VBA 被称为 Excel VBA 或 Word VBA 等等。使用 Excel VBA,您可以创建 Excel 的宏,称为“Excel 宏”。GAS 是针对谷歌应用的,其操作方式与 VBA 对微软 Office 应用程序的方式相同。尽管 VBA 和 GAS 都不需要单独的编译过程,但它们是两种非常不同的脚本语言,并使用不同的编程 API、方法和属性。

我希望你们中的许多人熟悉使用 VBA 进行 Office 应用程序;如果不熟悉,那也没关系——这并不是学习 GAS 的障碍。

GAS 相对于 VBA 的优势

  • 版本无关性:Sheets/Docs 以及脚本会自动保存在云端,附加到您的谷歌账户,并且可以从任何带有浏览器的计算机访问。您无需担心其他计算机是否安装了相同的 Sheets/Docs 版本,而我们也无法保证 Excel/Word 宏的一个版本能在另一个版本上工作。

  • 平台无关性:当您在 Windows 平台上创建 Excel/Word 的 VBA 宏时,它们可能在 Mac 平台上不起作用,反之亦然。使用 Google Sheets/Docs,您的工作平台无关紧要——它都会工作。

GAS 的限制

GAS 在谷歌的服务器上运行,因此它不能连续运行超过六分钟(这可能在将来有所变化)。您所有的函数都应该在规定时间内完成运行并返回结果。不要慌张,因为您将在后面学习如何有效地使用触发器来克服这些限制。

在以下章节中,我们将探讨最受欢迎的谷歌应用以及如何使用 GAS 来自定义和/或自动化任务。

Google Drive

Google Drive 是一个文件存储应用程序,从现在起我们将简单地称之为“Drive”,您可以在谷歌的服务器上存储和同步您的文件。让我们看看使用 Drive 的一些优点:

  • 您可以实时编辑和分享 Google Docs、Sheets 和 Forms 与您的朋友或协作者。

  • 您甚至可以在您的台式机上停止编辑一个文档,并继续使用智能手机或平板电脑,反之亦然,无论您身在何处,使用什么设备。这是因为您的文件存储在谷歌的云服务器上。

  • 使用谷歌应用创建的文件存储在 Drive 中,使用谷歌的本地格式和扩展名。例如,Google Docs(文档)文件是.gdoc,Google Sheets(电子表格)是.gsheet,等等。

  • 除了谷歌的本地文件外,您还可以从您的桌面存储或上传任何其他类型的文件到 Google Drive。

    注意

    如果你想要将计算机或设备上的文件与驱动器同步,那么你可以安装名为Google Drive 客户端应用程序的专用软件。当此应用程序在您的计算机或设备上运行时,它会同步本地存储的文件与驱动器中相同的文件。

你可能想知道同步文件的目的?有时你可能需要,或者有人可能要求你使用 GAS 解析存储在桌面上的 CSV 文件以处理数据并将其组织到表格中。在这种情况下,GAS 不会在桌面执行,但可以在 Google 服务器上执行。这样,你可以访问你的驱动器文件并在同步的 CSV 文件内解析数据。你不必每次都手动将 CSV 文件上传到驱动器。

以下截图显示了驱动器文件夹视图:

Google Drive

Gmail

Gmail 是最受欢迎的基于网络的电子邮件服务,由 Google 提供。使用它,偶尔手动给一个人或几个人发送电子邮件消息不是问题。但如果你想在你不醒的时候或在多个收件人那里发送电子邮件怎么办?考虑以下场景:

  • 你想在固定时间给你的朋友发送一个惊喜的生日问候,既不早也不晚

  • 你需要一次向数百人发送定制电子邮件

  • 你需要定期发送电子邮件

对于所有这些场景,GAS 都有解决方案:

  • 使用 GAS,你可以构建一个邮件合并应用程序,向n个数量的人发送带有定制问候语或消息的电子邮件。

  • 你可以从你的收件箱中提取电子邮件中的隐藏信息,并将它们存储和组织在 Google 表格或文档中。

  • 你甚至可以将 Google 表格或文档的数据或内容转换为 PDF 或其他文件格式,并将其作为电子邮件附件发送,或者只需将创建的文件保存在驱动器中,并在电子邮件消息中仅包含文件的 URL 作为超链接。

  • 此外,GAS 还允许你将选定的消息标记为重要或星标。你还可以使用联系人服务添加、删除和更新你的 Gmail 联系人。

以下截图显示了 Gmail 如何使用标签对消息进行分类或分组:

Gmail

Google 日历

Google 的在线日历服务与 Gmail 集成。GAS 通过使用CalendarApp类来提供对日历服务的访问。使用 GAS 代码,你可以访问和修改你的日历以及你订阅的日历。使用 GAS,你可以以编程方式创建日历事件并邀请你的朋友。或者,你可以获取事件详情并将它们填充到表格中。

Google 文档

Google 文档是一个文字处理程序,在 Google Drive 服务中的基于网络的软件上运行。文档允许你在网上创建和编辑文档,同时与其他用户实时协作。使用 GAS,你可以创建文档、格式化内容、将它们翻译成其他语言、在驱动器中保存它们,或者将它们通过电子邮件发送给你的朋友。

Google 表格

Google Sheets 是一个类似于 Microsoft Excel 的电子表格程序。您可以创建 Sheets,与他人共享,并实时编辑它们。Google 在 Sheets 中提供了内置的公式/函数。您也可以创建自己的简单到复杂的公式。换句话说,您可以创建自定义公式。在您的 GAS 代码中使用 SpreadsheetApp 类,您可以与其他应用程序交互。

Google Apps 服务

Google 提供了 Apps 服务,以使 GAS 能够与 Apps 交互。几乎所有的 Apps 都提供一项或多项服务。您可以在您的 GAS 代码中使用这些服务类来自定义或自动化 Apps。服务被分为基本高级两类。您可以直接使用基本服务,但使用高级服务之前需要先启用它们。稍后您将看到如何启用它们。

在 Drive 中创建 Google Sheets 并与您的朋友和公众共享

创建 Google Sheet 的步骤如下:

  1. 运行您喜欢的浏览器,并在地址栏中输入 drive.google.com/

    小贴士

    为了使用 Google Drive,您应该有一个 Google 账户。如果您没有账户,那么请创建一个。

  2. 现在,Google Drive 页面将打开。在左侧面板中,点击新建按钮,然后点击Google Sheets在 Drive 中创建 Google Sheets 并与您的朋友和公众共享

  3. 在创建新的 Sheet 后,右键单击它(Windows)或使用上下文菜单(Mac)并选择共享...选项:在 Drive 中创建 Google Sheets 并与您的朋友和公众共享

  4. 将会打开一个新弹窗,如下面的截图所示。之后,输入您想要共享文档的电子邮件地址,或地址。最后,点击完成按钮:在 Drive 中创建 Google Sheets 并与您的朋友和公众共享

    Google 将向您的朋友发送共享通知。当您的朋友点击提供的访问链接时,他们将能够访问您的文档。

恭喜!您已成功创建了一个新的 Sheet 并与您的朋友共享。

脚本项目

脚本被组织成项目。项目可以是两种类型之一,独立或绑定到 gtype(Google Drive 原生文件类型,例如 Sheets、Docs 和 Forms)文件。独立脚本是在单独的脚本文件中创建的,您可以在 Drive 中的其他文件列表中看到这些文件。绑定脚本嵌入在单个 gtype 文件中,并使用相应的应用程序创建。如您所见,在 Drive 中的其他文件中,您可以直接从 Drive 打开独立脚本文件,但绑定脚本只能在相应的应用程序中打开。然而,绑定脚本比独立脚本对父文件有更多的权限。例如,您可以在绑定脚本中访问活动文档,但在独立脚本中则不行。

创建独立脚本项目

要创建一个独立的脚本文件,请按照以下步骤操作:

  1. 按照前面“在 Drive 中创建 Google Sheets 并与朋友及公众分享”部分中描述的步骤进行操作。

  2. 导航到新建 | 更多 | Google Apps Script,而不是电子表格,如图所示:创建独立脚本项目

  3. 一个新无标题项目将在新的浏览器标签页或窗口中打开。新项目包含一个代码文件Code.gs,其中包含一个空白函数myFunction,如图所示:创建独立脚本项目

  4. 要保存或重命名新项目,请按键盘上的Ctrl + S或点击编辑器中的保存图标(软盘)。如果您是第一次保存项目,则会出现提示输入新项目名称。输入项目名称(您喜欢的任何名称)并点击确定按钮。新的脚本文件将保存在当前文件夹中:创建独立脚本项目

在 Sheets 中创建新项目

创建一个新的工作表或打开现有的工作表。您将在窗口顶部看到一些菜单项。现在,按照以下步骤操作:

  1. 点击工具并选择脚本编辑器...,如图所示:在 Sheets 中创建新项目

  2. 将出现一个新的带有新项目选择对话框的浏览器标签页或窗口,如图所示:在 Sheets 中创建新项目

  3. 点击空白项目或关闭对话框(您不必每次都选择空白项目,只是这次)。一个新无标题项目将在新的浏览器标签页或窗口中打开。

  4. 按照前面章节所述保存项目。

    小贴士

    虽然您可以创建尽可能多的有界项目,但每个文件一个项目就足够了。每个文件只创建一个项目可能有助于您避免重复的函数和变量名的问题。

恭喜!您已创建了一个新的脚本项目。通过遵循前面的步骤,您也可以在 Docs 和 Forms 中创建脚本项目。

在 Sheets 中创建自定义公式

打开您之前创建的电子表格并做出以下更改:

  1. 在列AB中输入一些名字的首尾。

  2. 在单元格C2中输入(包括等号)=CONCATENATE(A2," ", B2)

现在,您可以在单元格A2B2中分别看到名字的首尾,它们之间用空格连接。

CONCATENATE是 Google Sheets 的内置公式。您也可以创建自己的,称为自定义公式:

  1. 打开脚本编辑器并复制粘贴以下代码:

    function myFunction(s1,s2) {
      return s1 + " " + s2;
    }
    

    下面是同样的截图:

    在 Sheets 中创建自定义公式

  2. 在键盘上按Ctrl + S或点击编辑器中的保存图标来保存脚本。

  3. 现在返回到电子表格,并在单元格C2中输入=myFunction(A2,B2)

    这与内置公式的工作方式完全相同。您可以将您的公式扩展到 C2 以下的其它单元格。这是一个简单的公式,但您可以根据需求创建复杂的公式。

  4. 您自定义的公式应返回单个值或二维数组。以下截图显示了自定义函数的工作方式:在表格中创建自定义公式

恭喜!您已创建了一个自定义公式。

注意

要为您的自定义函数添加代码补全和/或工具提示,请在函数前面的代码行中添加以下注释:

/**
 * Concatenates two strings
 *
 * @customfunction
 */
function myFunction(s1,s2){
      …

Google 表单

Google 表单是一个 Google 应用程序,您可以使用它来收集用户信息。用户响应或答案将被收集并存储在表单本身中,然后可以填充到连接的表格中。您还可以在需要时更改响应的目标表格。您可以使用 GAS 动态创建 Google 表单。

在 Google 表格中创建表单

在您之前创建的表格中,点击工具菜单并选择创建表单选项。将自动创建一个新的表单,并将其绑定到一个新的表格。新表格的名称将与Form Responses 1类似。在新表单中,创建与表格列标题完全相同的表单字段:

在 Google 表格中创建表单

完成后,尝试使用实时表单提交数据。

一些研究

如果您被提供了一个文档的 ID 或密钥,例如 11CEeHWygGKqxGS7jmQzLpeO7Fs3cjetT4HTrWXHTDSU,您能否打开该文档,前提是它已被公开分享?

小贴士

每个 Google 文档、表格、文件夹和项目都有一个 ID 或密钥,您可以从相应项目的 URL 中获取。

摘要

在本章中,您了解了 Google 应用程序,并获得了对 GAS 的介绍,以及如何创建项目和自定义公式。还有许多其他 Google 应用程序可用,但我们只介绍了最受欢迎的。将相同的脚本概念和原则应用于其他应用程序不会很难。在下一章中,您将学习如何创建基本元素,例如自定义菜单、对话框和侧边栏。

第二章:创建基本元素

在上一章中,你学习了关于Google Apps ScriptGAS)以及如何创建脚本项目。在本章中,你将学习如何创建可点击按钮、自定义菜单、消息框、侧边栏和对话框,以及如何调试你的脚本。我们将使用 Sheets 来完成前两个任务,而其他所有任务将使用 Docs。

创建可点击按钮

在上一章中,你学习了如何在 Google Sheets 中打开脚本编辑器。对于这个任务,在新建或任何现有 Google Sheet 中打开脚本编辑器,并按照以下步骤操作:

  1. 选择单元格B3或任何其他单元格。点击插入并选择绘图…,如下面的截图所示:创建可点击按钮

  2. 将会打开一个绘图编辑器窗口。点击文本框图标,然后在画布区域内点击任意位置。输入Click Me。调整对象大小以仅包含文本,如下面的截图所示:创建可点击按钮

  3. 点击保存并关闭以退出绘图编辑器。现在,点击我图片将被插入到活动单元格(B3),如下面的截图所示:创建可点击按钮

    你可以将此图片拖动到工作表中的任何位置,除了菜单栏。

    小贴士

    在 Google Sheets 中,图片不会锚定到特定的单元格,并且可以被拖动或移动。

    如果你点击图片,右上角将出现一个下拉箭头:

    创建可点击按钮

  4. 点击分配脚本…菜单项。一个脚本分配窗口将会打开。输入greeting或任何你喜欢的名字,但请记住这个名称,因为在下一步中将会使用相同的名称来创建一个函数。点击确定创建可点击按钮

  5. 现在在同一个工作表中打开脚本编辑器。当你打开脚本编辑器时,一个项目选择对话框将会打开。你可以关闭它或选择空白项目。编辑器中将会有一个默认函数myFunction。删除编辑器中的所有内容,并插入以下代码:

    function greeting() {
      Browser
    }
    

    当你在“浏览器”旁边输入.时,代码补全提示将会打开,如下所示:

    创建可点击按钮

    如果你点击msgBox(String title, String prompt, ButtonSet buttons):String,那么msgBox(title, prompt, buttons)将会自动插入。

    小贴士

    除了代码提示功能外,你还可以使用自动缩进功能。确保在保存图标左侧的缩进图标被按下。选择你想要缩进的几行代码,然后按下键盘上的Tab键。现在你可以看到这些代码行已经自动缩进了。

    在此代码中,Browser表示您正在从Base(或基本)脚本服务中调用Browser类。msgBoxBrowser类的具有三个参数的方法。参数的名称是自解释的。title参数表示消息框的标题,prompt表示您对用户的消息,而buttons表示您希望在消息框中包含的按钮类别或按钮集。

  6. 现在编辑或替换以下代码:

    function greeting() {
      Browser.msgBox("Greeting", "Hello World!", Browser.Buttons.OK);
    }
    
  7. 点击保存图标,如果需要,请输入项目名称。您已完成了greeting函数的编码。

  8. 现在,激活电子表格标签/窗口,并点击您的点击我按钮。将打开一个授权窗口,您需要点击继续。在随后的请求权限窗口中,点击允许,如图所示:创建可点击按钮

    您只需为此特定范围执行此操作一次。范围将在相关的权限对话框/窗口中显示。在此脚本中,范围是在 Google Drive 中查看和管理您的电子表格。在范围上方,您可以看到标题第二章希望:,这意味着您的脚本项目(项目名称第二章)或应用程序希望为此特定范围获取您的权限。

    一旦您点击允许,权限对话框将关闭,您的实际问候消息框将如以下所示打开:

    创建可点击按钮

    点击确定关闭消息框。每次您点击按钮时,此消息框都会打开。

恭喜!您已创建了一个可点击的按钮,并将其与 GAS 函数关联。

点击按钮时显示提示

提示作为带有标题和消息的弹出窗口出现在活动电子表格的右下角。要创建提示对话框,编辑或替换greeting函数如下:

function greeting() {
  SpreadsheetApp.getActiveSpreadsheet()
   .toast("Hello World!", "Greeting");
}

现在如果您点击该按钮,则会出现一个类似于以下截图的提示对话框,并在 5 秒内消失(默认):

点击按钮时显示提示

您可以在toast方法中包含第三个参数,即超时秒数。这意味着提示将可见多长时间。如果您想让它永远显示,请输入一个负数。例如,toast("Hello World!", "Greeting", -1)

小贴士

提示仅适用于表格。

创建自定义菜单

您可能想知道是否可以在不使用按钮的情况下执行greeting函数。答案是肯定的。在脚本编辑器中,有一个运行菜单。如果您点击运行 | greeting,则greeting函数将被执行,并打开消息框。

为每个函数创建按钮可能不可行。尽管您无法更改或添加到应用程序的标准菜单(除了附加组件菜单)中的项目(如文件编辑查看等),但您可以添加自定义菜单和菜单项。

对于这个任务,创建一个新的 Google Docs 文档或打开一个现有的文档。打开脚本编辑器并输入以下两个函数:

function createMenu() {
  DocumentApp.getUi()
   .createMenu("PACKT")
   .addItem("Greeting","greeting")
   .addToUi();
}

function greeting() {
  var ui = DocumentApp.getUi();
  ui.alert("Greeting", "Hello World!", ui.ButtonSet.OK);
}

在第一个函数中,你正在使用 DocumentApp 类,调用 getUi 方法,然后通过方法链连续调用 createMenuaddItemaddToUi 方法。第二个函数你应该很熟悉,因为你已经在之前的任务中创建了它,但这次使用的是 DocumentApp 类及其相关方法。

小贴士

不要复制粘贴这些函数或代码;请一行一行地自己创建/编辑它们。这将帮助你熟悉脚本编辑器的代码提示和自动完成功能。

现在运行 createMenu 函数并切换到文档窗口/标签页。你将看到在 帮助 菜单旁边新增了一个名为 PACKT 的菜单项。你可以看到自定义菜单 PACKT,其中包含一个名为 Greeting 的项目,如以下截图所示。项目标签 Greeting 与函数 greeting 相关联。

创建自定义菜单

菜单项 Greeting 与之前任务中创建的按钮工作方式相同。使用这种方法插入自定义菜单的缺点是,为了使自定义菜单显示出来,你需要在脚本编辑器中每次都运行 createMenu。考虑一下,如果用户不知道 GAS 和脚本编辑器,他们将如何使用这个 greeting 函数。想想你的用户可能不像你一样是程序员。为了使用户能够执行选定的 GAS 函数,你应该创建一个自定义菜单,并在文档打开时立即使其可见。要做到这一点,将 createMenu 函数重命名为 onOpen,这就足够了。

小贴士

onOpen 函数是一个特殊函数名。每当用户打开文档时,GAS 解释器首先执行此函数。其他类似函数名有 onEditonInstalldoGetdoPost。前两个是与电子表格事件相关的函数,后两个是发布脚本服务的 getpost 回调函数。你不应该将这些函数名用于除预期目的之外的其他目的。

创建侧边栏

侧边栏是一个静态对话框,包含在文档编辑窗口的右侧。要创建侧边栏,请在编辑器中输入以下代码:

function onOpen() {
  var htmlOutput = HtmlService
  .createHtmlOutput('<button onclick="alert(\'Hello World!\');">Click Me</button>')
  .setTitle('My Sidebar');

  DocumentApp.getUi().showSidebar(htmlOutput);
}

在前面的代码中,你正在使用 HtmlService 并调用其方法 createHtmlOutput,然后连续调用 setTitle 方法。要测试此代码,请运行 onOpen 函数或重新加载文档。侧边栏将如以下截图所示在文档窗口的右侧打开。侧边栏布局大小是固定的,这意味着你不能更改、修改或调整它的大小。

创建侧边栏

侧边栏中的按钮是一个 HTML 元素,而不是 GAS 元素,如果点击,它将打开浏览器界面的警告框。

创建一个扩展菜单

在上一个任务中,您将 HTML 代码作为字符串参数内联包含在createHtmlOutput方法中。作为替代,您可以将此 HTML 片段放入单独的 HTML 文件中。

要创建一个新的 HTML 文件,在脚本编辑器中,前往文件 | 新建 | HTML 文件,如图所示:

创建附加菜单

然后在创建文件框中,输入您为新 HTML 文件选择的名称。对于这个任务,输入Index并点击确定按钮。.html扩展名将自动添加。

创建附加菜单

将创建一个包含几行默认 HTML 代码的新Index.html文件,如图所示:

创建附加菜单

将您的button标签插入到body标签之间,如图所示:

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <button onclick="alert('Hello World!');">Click Me</button>
  </body>
</html>

将以下代码插入到Code.gs文件中:

function onOpen(){
  DocumentApp.getUi()
  .createAddonMenu()
  .addItem("Show Sidebar", "showSidebar")
  .addToUi();
}

function showSidebar() {
  DocumentApp.getUi()
  .showSidebar(
    HtmlService.createHtmlOutputFromFile('Index')
    .setTitle('Greetings')
  );
}

要测试代码,运行onOpen函数或重新加载文档。在附加组件菜单中,将添加一个新项目,称为第二章(项目名称),如图所示:

创建附加菜单

显示侧边栏showSidebar函数的标签;点击它以显示您的侧边栏。

创建模态对话框

要创建一个阻止用户在电子表格或文档中更新任何内容的模态对话框,请按照图示更新Code.gs文件中的代码:

function onOpen(){
  DocumentApp.getUi()
  .createAddonMenu()
  .addItem("Show Dialog", "showDialog")
  .addToUi();
}

function showDialog() {
  var html = HtmlService
   .createHtmlOutputFromFile('Index');
  DocumentApp.getUi()   
   .showModalDialog(html, 'Greeting');
}

前往附加组件 | 第二章 | 显示对话框,将弹出一个模态对话框:

创建模态对话框

创建无模态对话框

现在我们将创建一个无模态对话框,并查看模态对话框和无模态对话框之间的区别。按照图示更新showDialog函数:

function showDialog() {
  var html = HtmlService.createHtmlOutputFromFile('Index');
  DocumentApp.getUi()
  .showModelessDialog(html, 'Greeting');
}

注意,showModalDialog方法已更改为showModelessDialog

无模态对话框不会阻止您做其他事情,例如编辑文档,并且您可以像图示中那样拖动对话框。

创建无模态对话框

调试脚本

在测试和调试代码时,在几个点上记录变量的值是至关重要的。Logger类是一个有用的工具来完成这项工作,并且有几个对调试代码至关重要的方法。

按照图示更新showDialog函数:

function showDialog() {
  var ui = DocumentApp.getUi();

  var response = ui.prompt(
      'Greeting', 'Will you enter your name below?', ui.ButtonSet.YES_NO
  );

  if (response.getSelectedButton() == ui.Button.YES) {
    Logger.log('Your name is %s.', response.getResponseText());
  } else if (response.getSelectedButton() == ui.Button.NO) {
    Logger.log('You clicked \'NO\' button');
  } else {
    Logger.log('You closed the dialog.');
  }
}

如常从附加组件菜单运行showDialog函数。做任何事,例如,输入您的名字并点击或关闭对话框。

调试脚本

现在,在脚本编辑器中,按Ctrl + Enter(Windows)或 Command + Enter(Mac),或从视图菜单中选择日志,然后您可以看到带有时间戳的已记录文本,如图所示:

调试脚本

要对Logger进行更详细的研究,创建一个名为debug的函数,如图所示:

function debug(){
  var square = 0;
  for(var i = 0; i < 10; i++){
    square = i * i;
    Logger.log(square);
  }
}

运行debug函数,查看如图所示的Logger结果:

调试脚本

除了记录之外,你还可以使用编辑器的debug功能。在编辑器中,你在一行或多行设置断点。要这样做,点击你想要设置断点的行号。行号左侧将切换一个红色圆点,如图所示:

调试你的脚本

选择函数选择器中,如果你还没有选择,请选择你想要调试的debug函数。点击函数选择器左侧的调试按钮(显示为昆虫形状)。函数执行到断点处然后暂停。编辑窗口水平分割,并在窗口的下半部分显示对象及其值,如图所示:

调试你的脚本

点击继续调试按钮,以查看for循环的每一轮的值。

小贴士

你可以尝试其他功能,如进入、跳过和退出。

要退出调试会话,点击停止调试按钮,并记得移除(切换)所有断点。

摘要

在本章中,你学习了多种对话框及其创建和显示方法,发现了如何使用Logger类记录值,还看到了如何调试你的脚本。在下一章,你将学习关于 Gmail 和联系人。

小贴士

下载示例代码

你可以从www.packtpub.com的账户下载此书的示例代码文件。如果你在其他地方购买了此书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。

你可以通过以下步骤下载代码文件:

  • 使用你的电子邮件地址和密码登录或注册我们的网站。

  • 将鼠标指针悬停在顶部的支持标签上。

  • 点击代码下载与勘误

  • 搜索框中输入书籍的名称。

  • 选择你想要下载代码文件的书籍。

  • 从下拉菜单中选择你购买此书籍的来源。

  • 点击代码下载

文件下载完成后,请确保使用最新版本的以下软件解压缩或提取文件夹:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

第三章。解析和发送电子邮件

在上一章中,你学习了如何创建基本的 GAS 元素,例如自定义菜单、对话框和吐司。你还学习了如何调试你的脚本代码。在本章中,你将学习许多现实世界的 Gmail 和联系人应用程序,包括邮件合并应用程序。

注意

在本章中,如果你在代码中遇到像[[ value ]]这样的左右方括号,那么将value替换为包括括号在内的实际值。

例如,如果电子邮件 ID 是example@emample.com,并且你通过My email id [[emailid]] \n进行替换,那么将其替换为My email id example@example.com \n

通过脚本创建 Gmail 联系人

你可以使用ContactsApp类的createContact方法通过脚本创建 Gmail 联系人。例如,如果名字是 Anika Sumi,电子邮件 ID 是<anika@example.com>,那么ContactsApp.createContact("Anika", "Sumi", "anika@example.com")代码将创建预期的联系人。

要了解ContactsApp类的更多可用方法,在代码编辑器中输入ContactsApp和它旁边的.(一个点)。然后,你可以查看代码提示中显示的所有可用方法及其参数细节,如下面的截图所示:

通过脚本创建 Gmail 联系人

你可以在前面的截图中看到已弃用的方法被划掉;建议你不要使用这些方法。

访问工作表、单元格、范围和偏移量

一个 Google 工作表的电子表格中包含一个或多个 Sheet 或标签页。Sheet 从左到右按索引顺序排列,从 0 开始。例如,最左边的 Sheet 通过索引 0 引用,下一个通过 1 引用,依此类推。在 GAS 中,我们可以通过索引或名称来引用 Sheet。

例如:

  • getSheets()方法返回一个包含 Sheet 对象的数组。从数组中,我们可以通过索引引用单个 Sheet。

  • getSheetByName("Contacts")函数返回一个名为Contacts的 Sheet 对象。

在 Google Sheets 中,列标签从字母A开始,从左到右按程序性方式计数,从 1 开始。例如,列A是 1,B是 2,以此类推。行通过其相应的标签数字进行标识。在 GAS 中,我们可以通过A1表示法或通过单独的行和列数字来引用单元格或单元格范围。

例如:

  • getRange('D1:F10')方法返回一个引用从D1F10Range对象。

  • getRange(1,4,10,3)方法返回一个引用相同范围的Range对象。

偏移量是一种间接引用方法,用于从基准单元格引用单元格/范围。偏移引用由它相对于基准单元格移动的行数和列数确定。

例如,如果基准单元格是D1,那么offset(10,3)方法返回范围D1:F10

读取和写入工作表数据

通常你需要从表格中读取和/或写入数据。通常,使用 getValue 方法从单元格中读取一个值,使用 getValues 方法从范围中读取值。getValue 方法返回单个值,而 getValues 方法返回一个二维数组值。要写入单个值和二维数组值,分别使用 setValuesetValues 方法。

构建 Gmail 联系人搜索应用程序

现在,我们将创建一个应用程序来搜索现有的联系人。这个应用程序能够搜索并列出你在表格中的 Gmail 联系人。创建一个新的表格,并将 Sheet1 重命名为 Contacts,并按照以下截图所示进行设置。创建一个按钮,并将之前章节中学到的函数名称 searchContacts 分配给它。

构建 Gmail 联系人搜索应用程序

按照以下内容创建 searchContacts 函数:

function searchContacts(){

  var SheetContacts = SpreadsheetApp.getActiveSpreadsheet()
      .getSheetByName("Contacts");

  // Read input from cell A3 
  var searchCriteria = SheetContacts.getRange("A3").getValue();

  //  First 10 contacts.
  //  [You can change this limit, but advised to keep small.]
  var numOfContacts = 10;

  // Clear existing sheet data
  SheetContacts.getRange(7,1,numOfContacts,4).clear();

在这里,clearRange 对象的方法,用于清除一个范围内的所有内容,包括格式和公式。你可以使用 Sheet 对象的 clear 方法来清除整个表格。或者,你也可以使用 clearContent 方法来仅清除内容。

  // Returns an array of contacts where
  // contacts name matches with search text.
  var contacts = ContactsApp.getContactsByName(searchCriteria);

  //  Limit number of contacts.
  if(contacts.length > numOfContacts) contacts.length = numOfContacts;

  var cell = SheetContacts.getRange("A7");

  for(var i in contacts){
    var name = contacts[i].getFullName();
    var email = contacts[i].getEmails()[0];

    if(email) email = email.getAddress();
    else email = "";

    // For simplicity get the first phone number
    var phone = contacts[i].getPhones()[0];

    if (phone) phone = phone.getPhoneNumber();
    else phone = "";

    // For simplicity get the first address
    var address = contacts[i].getAddresses()[0];

    if(address) address = address.getAddress();
    else address = "";

    // cell.offset(rowOffset, columnOffset)
    cell.offset(i,0).setValue(name);
    cell.offset(i,1).setValue(email);
    cell.offset(i,2).setValue(phone);
    cell.offset(i,3).setValue(address);
  }
};

小贴士

不要复制粘贴前面的代码,而是自己编辑它。这样做,你将了解 SpreadsheetAppContactAppContact 等类中可用的方法签名(方法名称和参数),借助脚本编辑器的代码提示功能。

在没有错误地编辑和保存代码后,转到电子表格窗口。如果你在 A3 单元格(搜索框)中输入一个搜索词,然后点击 搜索,那么前 10 个联系人将按以下截图所示列出(列出的联系人详细信息根据你的 Gmail 用户名和联系人而异):

构建 Gmail 联系人搜索应用程序

如果你想通过 searchContacts 函数更新列出的联系人怎么办?例如,你可能想更新联系人的电话号码和/或地址。要更新联系人字段,我们将创建另一个名为 updateContacts 的函数。在创建该函数之前,在 Contacts 表格中,在 搜索 旁边添加一个名为 更新 的按钮,并按以下截图所示分配函数名称 updateContacts

构建 Gmail 联系人搜索应用程序

更新你想要更新的字段值。现在创建以下列出的函数:

function updateContacts(){
  var SheetContacts = SpreadsheetApp.getActiveSpreadsheet()
      .getSheetByName("Contacts");

  var cell = SheetContacts.getRange("A7");

  var numOfContacts = 10;

  for(var i = 0; i < numOfContacts; i++){

    var email = cell.offset(0, 1).getValue();

    // Skip if email field is null
    if(!email) continue;

    var contact = ContactsApp.getContact(email);

    // Skip if contact is null or undefined
    if(!contact) continue;

    var name = cell.offset(i, 0).getValue();

    // Skip if name field is null
    if(!name) continue;
    contact.setFullName(name);

    var phone = cell.offset(i, 2).getValue().toString();

    // Returns phone numbers as an array
    var contPhone = contact.getPhones(ContactsApp.Field.MAIN_PHONE)[0];

    // Update main phone number if exist otherwise add.
    if(phone){

      if(contPhone){
        contPhone.setPhoneNumber(phone);
      } else {
        contact.addPhone(ContactsApp.Field.MAIN_PHONE, phone);
      }

    }

    var address = cell.offset(i, 3).getValue().toString();

    // Returns address as an array
    var contAddress = contact
        .getAddresses(ContactsApp.Field.HOME_ADDRESS)[0];

    // Update home address if exist otherwise add.
    if(address){

      if(contAddress) {
        contAddress.setAddress(address);
      } else {
        contact.addAddress(ContactsApp.Field.HOME_ADDRESS, address);
      }

    }

  }
};

前面的函数通过给定的电子邮件 ID 检索联系人;并且,对于每个联系人,它还会检索字段值,并使用表格值更新/添加这些字段值。此函数可以更新/添加全名、电话和地址字段,但不能更新电子邮件 ID。

构建 Gmail 解析应用程序

parseEmail 函数能够检查最新的 10 个收件箱线程,从未读消息中提取 发件人 字段和正文文本,并将收集到的数据放在表格最左侧的标签页中。按照以下内容创建 parseEmail 函数:

/** 
 *  Gets content of latest unread message in Gmail inbox
 *    and puts gathered data in left most tab of Sheets.
 *
 */
function parseEmail(){

  // Left most sheet/tab
  var emailSheet = SpreadsheetApp.getActiveSpreadsheet()
      .getSheets()[0];

  // Clear the entire sheet.
  emailSheet.clear();

  // Checks maximum 10 threads
  var thread = GmailApp.getInboxThreads(0,10);

  var row = 1;

  for(var thrd in thread){
    var messages = thread[thrd].getMessages();

    for (var msg in messages) {
      var message = messages[msg];

      if(message && message.isUnread())
      emailSheet.getRange(row,1).setValue(message.getFrom());

      emailSheet.getRange(row++,2)
      .setValue(message.getPlainBody());
    }
  }

};

你可以使用 RegExp 从消息正文文本中提取所需的数据。

属性服务

GAS 提供了属性服务来存储和/或检索项目相关的数据。数据以键/值对的形式组织,可以手动设置或通过脚本代码设置。以下截图显示了如何手动设置属性。要查看此对话框,请点击 文件 菜单并选择 项目属性

属性服务

注意

你可以在脚本代码中使用手动创建的项目属性,但有时代码创建的属性可能不在 项目属性 对话框中可见。你可以在代码中创建、更新或删除项目属性。

在下一个任务中,我们将使用项目属性。

将 Gmail 附件下载到 Drive

saveEmailAttachmentsToDrive 函数可以将 Gmail 附件下载到 Drive。在这个函数中,使用 PropertiesService 来避免重复下载相同的附件。createFolder_ 函数用于在 Drive 中创建具有给定名称的文件夹,如果不存在的话。

提示

如果任何函数名后附加了 _,则它将不会在 运行 菜单下列出。你不能直接运行这些函数,但可以从其他函数中调用它们。这些被称为 私有函数

你可以在与 saveEmailAttachmentsToDrive 函数相同的脚本文件中创建 createFolder_ 函数,或者在一个单独的脚本文件中,例如 Library.gs

/**
 *  Checks latest 100 inbox threads,
 *    saves attachments in 'Gmail attachments' folder,
 *
 */
function saveEmailAttachmentsToDrive(){

  // Create 'Gmail Attachments' folder if not exists.
  createFolder_('Gmail attachments');

  // Get inbox threads starting from the latest one to 100.
  var threads = GmailApp.getInboxThreads(0, 100);

  var messages = GmailApp.getMessagesForThreads(threads);

  var folderID = PropertiesService.getUserProperties()
      .getProperty("FOLDER");

  var file, folder = DriveApp.getFolderById(folderID);

  for (var i = 0 ; i < messages.length; i++) {
    for (var j = 0; j < messages[i].length; j++) {
      if(!messages[i][j].isUnread()){

        var msgId = messages[i][j].getId();

        // Assign '' if MSG_ID is undefined.
        var oldMsgId = PropertiesService.getUserProperties()
            .getProperty('MSG_ID') || '';

        if(msgId > oldMsgId){
          var attachments = messages[i][j].getAttachments();

          for (var k = 0; k < attachments.length; k++) {
            PropertiesService.getUserProperties()
              .setProperty('MSG_ID', messages[i][j].getId());

            try {
              file = folder.createFile(attachments[k]);
              Utilities.sleep(1000);// Wait before next iteration.
            } catch (e) {
              Logger.log(e);
            }
          }

        }
        else return;

      }
    }
  }

};

前面的函数使用文件夹名称作为参数调用以下 createFolder_ 函数。createFolder_ 函数会查找指定的文件夹,如果不存在则创建它,并返回其唯一 ID:

function createFolder_(name) {
  var folder, folderID, found = false;

  /*
   * Returns collection of all user folders as an iterator.
   * That means it do not return all folder names at once, 
   * but you should get them one by one.
   *
   */
  var folders = DriveApp.getFolders();

  while (folders.hasNext()) {
    folder = folders.next();
    if (folder.getName() == name) {
      folderID = folder.getId();
      found = true;
      break;
    }
  };

  if (!found) {
    folder = DriveApp.createFolder(name);
    folderID = folder.getId();
  };

  PropertiesService.getUserProperties()
    .setProperty("FOLDER", folderID);

  return folderID;
}

在前面的函数中,getFolders 方法是一个迭代器方法。迭代器不会一次性返回所有数据,而只返回当前数据。要获取后续数据,你应该反复调用 next 方法,直到 hasNext 变为 false

使用 MailApp 服务发送电子邮件

sendEmail 函数能够发送带有前缀消息的电子邮件。请记住替换电子邮件 ID 和消息文本。此服务主要用于发送具有有限方法的电子邮件(只有 sendEmailgetRemainingDailyQuota),并且无法访问你的 Gmail 账户。你可以使用 GmailApp 类进行更多操作:

function sendEmail(){
  var to = "[[reciever email id]]";
  var message = "[[message]]\n";

  MailApp.sendEmail(to, "Chapter 3", message);
}

在表单提交时发送电子邮件通知

想象一下,如果你创建了一个表单并展示给许多用户。每次都要打开响应表来验证是否有用户提交表单将会很麻烦。如果你创建了多个表单并发送给多个用户,问题会更糟。当有表单提交时,收到通知电子邮件将会很有帮助。

对于这个任务,创建一个包含三个字段的表单,如图所示:

在表单提交时发送电子邮件通知

从实时表单提交测试数据。您提交的数据将被保存在一个名为类似Form Responses 1的响应工作表中。列标题将与您的表单字段一致,如以下截图所示。数据可能因您的输入而异。

在表单提交时发送电子邮件通知

在脚本文件中,您需要进行以下更改:

  1. 从以下代码中输入sendEmail函数。

  2. 替换接收者的电子邮件地址。如果您运行此函数,它将发送包含响应工作表中最后提交的数据(最底行)的电子邮件。

  3. 检查工作表的实际名称和代码中使用的名称;它们应该完全相同。如果您不确定,右键单击工作表名称并选择重命名...

  4. 重命名对话框中复制工作表名称,并将其粘贴到以下代码中:

    function sendEmail(){
      var sheet = SpreadsheetApp.getActiveSpreadsheet()
          .getSheetByName("Form Responses 1");
    
      var lastRow = sheet.getLastRow();
      var lastCol = sheet.getLastColumn();
      var data = sheet.getRange(lastRow,1,1,lastCol)
          .getValues()[0];
    
      var to = "[[ receiver email id]]";
      var message = "Name: " + data[1] + "\n";
    
      message += "Phone: " + data[2] + "\n";
      message += "Question: " + data[3] + "\n";
    
      // MailApp.sendEmail(recipient, subject, body);
      MailApp.sendEmail(to, "Chapter 3", message);
    }
    

您创建了一个表单和一个函数,用于将响应数据发送到电子邮件地址。创建一个触发器以便在表单提交时立即运行sendEmail函数,将完成此任务。

手动创建触发器

要创建触发器,在代码编辑器中点击资源并选择当前项目的触发器,然后当前项目的触发器对话框将打开。已创建的触发器将列在此对话框中,否则将出现创建新触发器的链接。点击没有设置触发器。点击此处添加一个链接。从下拉菜单中选择选项,如以下截图所示:

手动创建触发器

运行标题下,选择您想要创建触发器的sendEmail函数。在事件标题下选择从电子表格在表单提交时,如前一张截图所示。

如果表单用户将数据提交到电子表格,触发器将运行sendEmail函数。

关于触发器的更多信息,请访问developers.google.com/apps-script/guides/triggers/.

通过脚本创建和删除触发器

您可以像以下示例代码所示,以编程方式创建或删除触发器。

/**
 * Deletes all the triggers.
 *
 */
function deleteTriggers(){
  var triggers = ScriptApp.getProjectTriggers();

  triggers.forEach(function(trigger){

    try{
      ScriptApp.deleteTrigger(trigger);
    } catch(e) {
      throw e.message;
    };

    Utilities.sleep(1000);

  });

};

function createTrigger(){
  var ss = SpreadsheetApp.getActiveSpreadsheet();

  // Create new trigger
  ScriptApp.newTrigger("sendEmail")
    .forSpreadsheet(ss).onFormSubmit().create();
};

注意

deleteTriggers函数中,使用Utilities服务的sleep方法暂时暂停脚本指定毫秒数。否则,您可能会遇到Too many service invocation…错误。

如果消息正文中找到特定关键字,则转发电子邮件

forwardEmails函数能够转发电子邮件消息,如果正文文本中找到特定关键字,则将其转发到前缀电子邮件地址。在测试代码时,请注意for循环的迭代次数,以避免错误地转发大量消息:

/**
 *  1\. Checks all unread inbox threads and messages.
 *
 *  2\. If specific keyword found then forwards it to another
 *     recipient.
 *
 *  3\. Marks that message as Read.
 *
 */
function forwardEmails() {
  var recipient = "[[forward email id]]";
  /*
   *  Use keywords separated by '|'.
   *  For example: "purchase | invoice"
   *
   */
  var words = "keywords list";
  var regExp = new RegExp(words,'g');

  var len = GmailApp.getInboxUnreadCount();

  for (var i = 0; i < len; i++) {
    // get 'i'th thread in inbox
    var thread = GmailApp.getInboxThreads(i,1)[0];

    // get all messages in 'i'th thread
    var messages = thread.getMessages();
    var msgLen = messages.length;
    var isAllMarkedRead = true;

    // iterate over each message
    // CAUTION: limit loop iterations for initial testing.
    for (var j = 0; j < 5 /* msgLen */; j++) {
      var message = messages[j];

      if(message.isUnread()){
        var bodyText = message.getPlainBody();
        var test = regExp.exec(bodyText);
        message.forward(recipient);
        isAllMarkedRead = false;
        message.markRead();
      }

    };

    if(isAllMarkedRead) len++;
    Utilities.sleep(1000);
  }

};

通过附件发送电子邮件

您可以通过设置以下代码中的选项将任何类型的文件附加到您的电子邮件消息中。以下代码将活动工作表的左侧工作表内容作为 PDF 附加。

function sendEmailWithAttachments(){
  var file = SpreadsheetApp.getActiveSpreadsheet()
      .getAs(MimeType.PDF);

  // MailApp.sendEmail(recipient, subject, body, options)
  MailApp.sendEmail(
    "[[ Recipient email id ]]",
    "Chapter 3",
    "",
    {
      attachments: [file],
      name: 'Chapter 3 test attachment'
    }
  );

}

在电子邮件消息中嵌入内联图像

要在电子邮件消息中嵌入图像,如徽标,你可以使用 HTML 代码而不是纯文本。将你的图像上传到 Google Drive,检索,并在代码中使用该文件 ID:

function sendEmail(){
  var sheet = SpreadsheetApp.getActiveSpreadsheet()
      .getSheetByName("Form Responses 1");

  var lastRow = sheet.getLastRow();
  var lastCol = sheet.getLastColumn();
  var data = sheet.getRange(lastRow,1,1,lastCol).getValues()[0];

  var image = DriveApp.getFileById("[[image file's id in Drive ]]").getBlob();

  var to = "[[Recipient email id ]]";
  var message = '<img src="img/cid:logo" />';

  message += "<p>Name: " + data[1] + "</p>";
  message += "<p>Phone: " + data[2] + "</p>";
  message += "<p>Question: " + data[3] + "</p>";

  MailApp.sendEmail(
    to,
    "Chapter 3 inline image example",
    "",
    {
      inlineImages:{ logo:image },
      htmlBody:message
    }
  );
}

构建电子邮件合并应用

一次向数百个收件人发送个性化的电子邮件可能是一项耗时的工作。为每条消息编写草稿、输入主题和收件人的电子邮件 ID 可能也很繁琐。使用这个邮件合并应用,你可以向所有收件人发送相同类型的信息,但可以在一定程度上进行定制。例如,问候个人。

第一步是在你的 Gmail 中创建一个草稿,如下面的截图所示。草稿用作模板。你可以使用任何特殊字符来包围要替换的文本。在草稿中,如下面的截图所示的代码使用左(<<)和右(>>)尖括号将EmailList工作表中的First Name列数据替换为第一个名字。你可以根据需要包括任何其他占位符或字段。设置草稿,但现在不要发送:

构建电子邮件合并应用

在新的工作表或现有工作表中创建一个名为EmailList的工作表。创建如下所示的列标题:

构建电子邮件合并应用

在脚本编辑器中创建如下所示的函数,用实际值替换草稿和发件人姓名。根据你的每日电子邮件发送配额设置maxEmails(此代码使用50):

// Returns your draft text.
function getDraftBody(draftName){
  var drafts = GmailApp.getDraftMessages();

  for(var i in drafts)
    if( drafts[i].getSubject() == draftName )
      return drafts[i].getPlainBody();
}

function sendEmails(){
  // EmailList sheet column numbers, 0 based.
  const FIRST_NAME_COL = 0;
  const EMAIL_IDS_COL = 1;
  const SUB_COL = 2;
  const DATE_COL = 3;

  var maxEmails = 50;
  var draftName = "Chapter 3";// Draft's subject name

  var draftBody = getDraftBody(draftName);
  var quotaLeft = MailApp.getRemainingDailyQuota();

  var ss = SpreadsheetApp.getActive();
  var sheet = ss.getSheetByName("EmailList");

  // Gets all sheet data as a 2-dimensional array.  
  var data = sheet.getDataRange().getValues();
  var header = data.shift();

  for(var i=0,count=0; count < maxEmails && count < quotaLeft
      && i < data.length; ++i){
    var firstName = data[i][FIRST_NAME_COL];
    var recipient = data[i][EMAIL_IDS_COL];
    var subject = data[i][SUB_COL];
    var htmlBody = draftBody.replace("<<FirstName>>", firstName);

    if(recipient){
      GmailApp.sendEmail(
        recipient,
        subject,
        "",
        {
          name:"[[ Sender Name ]]",
          htmlBody:htmlBody
        }
      );

      data[i][DATE_COL] = new Date();

      ++count;
    }
  };

  // Inserts header at top of the array.
  data.unshift(header);

  // Stores values of array in sheet.
  sheet.getRange(1, 1, data.length, header.length)
    .setValues(data);
}

EmailList工作表中填充数据。要发送电子邮件,运行sendEmails函数。你的草稿中的<<FirstName>>字段将根据你在EmailList工作表中的First Name列数据替换。就是这样!

恭喜!你已经创建了一个工作的电子邮件合并应用。

摘要

在本章中,你学习了ContactsAppMailAppGmailApp类及其方法。使用这些类,你创建了多个有用的现实世界应用,包括一个电子邮件合并应用。在下一章中,你将学习如何使用FormAppHtmlService类程序化地创建表单。你还将了解doGetdoPost简单的触发函数。

第四章:创建交互式表单

在上一章中,您学习了GmailAppContactApp的许多功能,并构建了许多实际应用。在本章中,您将学习如何使用FormAppHtmlService编程创建表单。您还将了解doGetdoPost函数。

使用脚本创建表单

在第一章 引入 Google Apps Scripts 中,您手动创建了一个表单,但这次我们将通过脚本编程创建表单。首先,我们将创建一个包含四个选项和一个其他选项的表单。为了简单起见,我们将地点添加为多选单选组。每个选项只能单独选择。在电子表格代码文件中创建名为createForm的函数,如下所示:

function createForm() {

  var places = ["Place 1","Place 2","Place 3","Place 4"];

  var form = FormApp.create("Vacation Form");

  form.addMultipleChoiceItem()
    .setTitle('Where will you go for vacation?')
    .setChoiceValues(places)
    .showOtherOption(true);

}

places变量包含一些随机地点,您可以将任何地点名称和任何数量的地点作为字符串数组分配。FormApp类的create方法在您的 Google Drive 根目录(My Drive)中创建一个名为Vacation Form的表单。运行函数后,创建的表单将如下所示:

使用脚本创建表单

选项在代码中是硬编码的。如果您想更改任何选项或添加更多选项,那么您应该编辑代码以进行必要的更改。如果您需要频繁更改选项,可能会变得令人烦恼或难以每次都编辑代码。现在我们将从电子表格的数据中添加地名,而不是将它们硬编码。添加或重命名一个现有的工作表为Places,并在其中添加一些地名,如下所示:

使用脚本创建表单

现在更新createForm函数,如下所示:

function createForm() {

  var ThisSpreadsheet = SpreadsheetApp.getActive();
  var SheetPlaces = ThisSpreadsheet.getSheetByName("Places");

  // Load 'Places' sheet data as a 2-dimensional array.
  var data = SheetPlaces.getDataRange().getValues();

  // remove the header row
  data.shift();

  var places = [];

  // Populate the places array with the first column's data
  data.forEach(function(row){
    places.push(row[0]);
  });

  // Create a new form
  var form = FormApp.create("Vacation Form");

  form.addMultipleChoiceItem()
    .setTitle('Where will you go for a vacation?')
    .setChoiceValues(places)
    .showOtherOption(true);

}

前面的函数将创建一个表单,其选项文本是从工作表的数据中检索的。选项文本和/或选项数量可以根据您的工作表数据进行调整。如果您想更改选项文本,那么只需编辑工作表数据即可,您不需要编辑代码。

当您运行前面提到的函数时,它将在My Drive文件夹中创建一个名为Vacation Form的表单。要打开编辑模式的表单,双击或右键单击(上下文点击)表单名称,然后转到打开方式 | Google 表单。以下截图显示了编辑模式下的表单外观。您可以在编辑模式下进行任何调整和/或格式设置:

使用脚本创建表单

要打开实时表单,右键单击(上下文点击)表单名称,点击获取链接,复制链接,然后将链接粘贴到浏览器地址栏中。以下截图显示了实时表单的外观:

使用脚本创建表单

您可以将实时表单链接与您的用户共享。您用户的响应将存储在表单本身中。您可以在表单编辑器中查看响应,或者将其链接到电子表格以查看响应,如下所示:

使用脚本创建表单

您可以通过点击表单编辑器右上角的图标手动将表单响应链接到电子表格。在选定的电子表格中创建一个新的“表单响应”工作表。

要以编程方式链接电子表格,请使用以下代码:

form.setDestination( FormApp.DestinationType.SPREADSHEET, ThisSpreadsheet.getId() ); // Replace with your spreadsheet's ID

将脚本发布为网络应用程序

您可以通过将脚本发布为网络应用程序来创建令人惊叹的网页/应用程序。在本节中,您将了解如何发布脚本。首先,在脚本编辑器中创建一个新的工作表,并输入以下代码:

function doGet(){
  var str = "Hello world!";
  return ContentService.createTextOutput(str);
}

doGet 函数将在向脚本发送 HTTP/HTTPS 请求时执行。在上面的代码中,使用 ContentService 返回字符串给浏览器。内容服务可以用来返回任何类型的内容,包括简单的文本、HTML、XML、JSON、CSV 等等。

要发布脚本,在脚本编辑器中,导航到发布 | 部署为网络应用程序…。将打开一个新的部署为网络应用程序对话框,如下所示:

将脚本发布为网络应用程序

选择任何一个现有的项目版本或选择新建以创建一个新的项目版本。在“作为谁执行应用程序”选项下,将有两个选择用户,用于访问网络应用程序。对于此应用程序,请选择(您的用户 ID)。这意味着脚本将以您的用户 ID 运行。如果您选择了第二个选项,则脚本将以访问应用程序的用户的名义运行。在“谁可以访问应用程序”选项下选择任何人,即使是匿名用户

小贴士

在“谁可以访问应用程序”选项下,还有两个额外的选择仅我任何人可用。如果您只想访问已发布的应用程序,请选择仅我。如果您想允许他人访问,但请注意,其他人应该使用他们的 Google 用户 ID 登录。如果您选择任何人,即使是匿名用户,则您的用户可以是任何人,并且不需要登录。他们甚至不需要是 Google 用户。

最后,点击部署按钮。然后,将打开另一个对话框,如下面的截图所示:

将脚本发布为网络应用程序

小贴士

如果您是第一次发布脚本,那么您需要授权脚本。授权是在网络应用程序部署之前启动的。在授权对话框中点击允许

在这个对话框中,你可以在当前网络应用 URL文本框下看到已发布的 URL。你可以将此 URL 复制并粘贴到新的浏览器窗口/标签页地址栏中,以查看你的网络应用程序的工作情况。对于前面的代码,返回的文本将是Hello world!点击确定关闭对话框。

小贴士

如果你对你的代码进行了任何修改,那么你应该再次发布新版本,否则更新将不会生效。或者,你可以使用最新代码URL 进行开发。

HtmlService

在本章的开头,你使用脚本代码创建了一个表单。然而,这个表单是静态的,这意味着你无法在客户端添加动态格式或脚本。如果需要,你只能在服务器端执行格式化和计算。

HtmlService 允许脚本向客户端返回 HTML 或网页。出于安全原因,包括 CSS 和 JavaScript 在内的 HTML 内容在返回到客户端浏览器之前由 Caja 编译器编译和沙盒化。返回的网页可以使用 google.script.run API 方法与服务器端 GAS 函数交互。

使用 HtmlService 的优点是:

  • 你可以使用 CSS 和客户端 JavaScript

  • 你可以创建动态 HTML 表单,而不是静态表单

  • 你可以分别处理客户端 HTML 和服务器端脚本代码

HtmlService 可以从模板创建 HTML 代码。模板是混合了 HTML 标记和脚本的 HTML 文件(这些被称为脚本片段,并在服务器端执行)。

<??> 包围的脚本片段会执行,但不会向包含的 HTML 输出任何内容。换句话说,它们不会改变周围的 HTML 代码。被 <?=?> 包围的脚本片段将输出返回到周围的 HTML 代码。脚本片段内的任何函数都可以调用其他脚本片段或服务器脚本函数的函数,但服务器函数不能调用脚本片段内的函数。

要进一步了解脚本片段标签,请访问:developers.google.com/apps-script/guides/html/templates

使用 HtmlService 创建表单

创建一个名为 Form.html 的文件,我们将使用它作为 HTML 模板,并在其中输入以下代码:

<!-- Form.html  -->
<!DOCTYPE html>
<html>

  <head>
    <base target="_top">
  </head>

  <body>
    <form>
      <h4>Where will you go for vacation?</h4>

      <input type="radio" name="places" value="Place 1" />Place 1

      <br />
      <input type="radio" name="places" value="Place 2" />Place 2

      <br />
      <input type="radio" name="places" value="Place 3" />Place 3

      <br />
      <input type="radio" name="places" value="Place 4" />Place 4

      <br />
      <br />
      <input type="submit" value="SUBMIT" />
    </form>
  </body>
</html>

使用相同的名称属性值为单选类型输入字段,这样它们就会全部分组在一起。这意味着它们将仅限使用。更新 Code.gs 文件中的 doGet 函数以渲染前面提到的 HTML 表单,如下所示:

// Code.gs
function doGet() {
  var template = HtmlService.createTemplateFromFile("Form.html");
  var html = template.evaluate();

  return HtmlService.createHtmlOutput(html);
}

发布脚本,并在浏览器地址栏中输入已发布的 URL。返回的基本 HTML 表单如下所示:

使用 HtmlService 创建表单

要从电子表格数据自动填充位置,请更新 HTML 代码,如下所示:

   <form>
      <h4>Where will you go for vacation?</h4>
      <? for (var i in places) { ?>
        <input type="radio" name="places" value="<?= places[i] ?>" /><?= places[i] ?><br />
      <? } ?>
      <br />
      <input type="submit" value="SUBMIT" />
    </form>

脚本片段 <?= places[i] ?> 返回 places 数组的第 i 个元素。你还需要更新如下的 doGet 函数:

function doGet() {
  // Replace with your spreadsheet's ID.
  var ss = SpreadsheetApp.openById("spreadsheet's id");
  var SheetPlaces = ss.getSheetByName("Places");

  var data = SheetPlaces.getDataRange().getValues();

  // Remove header row.
  data.shift();

  var places = [];

  // Populate the places array with the first column's data.
  data.forEach(function(row){
    places.push(row[0]);
  });

  var template = HtmlService.createTemplateFromFile("Form.html");

  // Assign the places array to the template object.
  template.places = places;

  var html = template.evaluate();
  return HtmlService.createHtmlOutput(html);
}

doGet 函数中将 places 数组分配给模板并在 HTML 模板中引用。然后,输出变为以下内容:

使用 HtmlService 创建表单

要将此表单数据提交到电子表格,您需要向表单元素添加方法和动作属性:

    <form method="post" action="<?= pubUrl ?>" >

doGet 函数中将发布的 URL 分配给 template 对象,例如:

template.pubUrl = "https://script.google.com/macros/s/AKfycbzMqmOaaD- TTDbycMl2AxF7dtn9EqxqZTwozcQBNHxe9hg4Kbc/exec";

您也可以按照以下方式分配:

template.pubUrl = ScriptApp.getService().getUrl();

要处理提交的表单数据,请添加一个 doPost 函数。doGetdoPost 函数根据 HTTP/HTTPS 请求方法(GETPOST)执行:

function doPost(e){
  // Replace with your spreadsheet's ID.
  var ss = SpreadsheetApp.openById("spreadsheet's id");

  var SheetResponses = ss.getSheetByName("Responses");

  // Create a 'Responses' sheet if it does not exist.
  if(!SheetResponses){
     SheetResponses = ss.insertSheet("Responses");
  };

  SheetResponses.appendRow([e.parameter.places]);

  return ContentService.createTextOutput(
     "Your response submitted successfully. Thank you!"
  );

}

表单提交后,会返回一条感谢信息作为正常的文本内容:

使用 HtmlService 创建表单

使用 Google 脚本 API 方法提交表单

要使用 google.script.run API 方法提交数据,请向 提交 按钮添加 onclick 属性:

<input type="submit" value="SUBMIT" onclick="google.script.run.postFormDataToSheet(this.parentNode);" />
<!-- this.parentNode is the 'form' element -->

按照以下方式创建 postFormDataToSheet 函数:

function postFormDataToSheet(e){
  // Replace with your spreadsheet's ID.
  var ss = SpreadsheetApp.openById("spreadsheet's id");

  var SheetResponses = ss.getSheetByName("Responses");

  // Create a 'Responses' sheet if it does not exist.
  if(!SheetResponses){
    SheetResponses = ss.insertSheet("Responses");
  }

  SheetResponses.appendRow([e.places]);
}

要显示表单提交结果或错误信息,请在单独的 <script> 标签中插入 postData 函数,并添加成功和失败处理程序,如下所示:

    <script>
      function postData(form){
        google.script.run
          .withSuccessHandler(callback)
          .withFailureHandler(callback)
          .postFormDataToSheet(form);
      }

      function callback(msg){
        alert(msg);
      }
    </script>

postFormDataToSheet 函数的末尾插入一个带有消息的返回语句。

要将 User 对象添加到 google.script.run API 调用中,请添加 withUserObject 方法以及成功和失败处理程序:

包含 user 对象的完整 HTML 代码如下所示:

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">

    <script>
      function postData(form){
        google.script.run
          .withSuccessHandler(showSuccess)
          .withFailureHandler(showError)
          .withUserObject(form)
          .postFormDataToSheet(form);
      }

      /*
       * msg -  the error or success message returned *        from the server.
       *
       * elem - the reference to the user object (form).
       *
       */
      function showSuccess(msg,elem) {
        var newElement = document.createElement("div");
        newElement.innerHTML = '<font color="green">'
          + msg + '</font>';
        elem.appendChild(newElement);
      }

      /*
       * msg -  the error or success message returned *        from the server.
       *
       * elem - the reference to the user object (form).
       *
       */
      function showError(msg,elem){
        var newElement = document.createElement("div");

        newElement.innerHTML = '<font color="red">'
          + msg + '</font>';

        elem.appendChild(newElement);
      }

    </script>
  </head>

  <body>
    <form>
      <h4>Where will you go for vacation?</h4>

      <? for (var i in places) { ?>
        <input type="radio" name="places"
          value="<?= places[i] ?>" /><?= places[i] ?>
        <br />
      <? } ?>

      <br />
      <input type="button" value="SUBMIT"
        onclick="postData(this.parentNode);" />
    </form>
  </body>
</html>

postFormDataToSheet 函数的完整版本如下所示:

function postFormDataToSheet(e){
  // Replace with your spreadsheet's ID.
  var ss = SpreadsheetApp.openById("spreadsheet's id");
  var SheetResponses = ss.getSheetByName("Responses");

  // Create 'Responses' sheet if it does not exist.
  if(!SheetResponses){
    SheetResponses = ss.insertSheet("Responses");
  }

  SheetResponses.appendRow([e.places]);

  return "Your response submitted successfully. Thank you!";
}

在此脚本中,您使用了名为 Form.html 的单独文件中的 HTML 代码。此文件用作 GAS 服务器中的模板,并且只将生成的标记和脚本代码返回给用户的浏览器。从浏览器(客户端)来看,我们使用 Google 客户端 JavaScript API (google.script.run) 与 GAS 服务器进行交互。这是客户端和服务器之间类似于 AJAX 的交互。在这里,客户端和服务器分别是您的浏览器和 GAS 服务器。

大多数情况下,使用纯 JavaScript 引用 HTML 标签/元素(DOM 元素)是一项繁琐的任务。为了使生活更轻松,您可以使用 jQuery 库。此外,您不需要在 <style> 标签中自己定义 CSS 样式;相反,您可以使用任何官方支持的(由 Google 支持)第三方样式表。

使用插件 CSS 和 jQuery 库创建表单

使用 Google 插件 CSS 和 jQuery 库的相同 HTML 代码如下:

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">

    <!-- Google's Add-ons stylesheet //-->
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/ script/css/add-ons1.css" />

    <script src="img/jquery.min.js"></script>

    <script>
      // on document load, assign postData function to submit // button's onclick property.
      $(function(){
        $("#btnSubmit").click(postData);
      });

      // Calls server side function 'postFormDataToSheet'
      // with form as the argument.
      function postData(){
        google.script.run
          .withSuccessHandler(showSuccess)
          .withFailureHandler(showError)
          .withUserObject(this)
          .postFormDataToSheet(this.parentNode);
      }

      /*
       * msg -  the error or success message returned *        from the server.
       *
       * elem - the reference to the user object (form).
       *
       */
      function showSuccess(msg,elem) {
        var div = $('<div id="error"> <font color="green">' + msg + '</font></div>');
        $(elem).after(div);
      }

      /*
       * msg -  the error or success message returned *        from the server.
       *
       * elem - the reference to the user object (form).
       *
       */
      function showError(msg,elem) {
        var div = $('<div id="error" class="error">' + msg + '</div>');
        $(elem).after(div);
      }
    </script>
  </head>

  <body>
    <form>
      <h4>Where will you go for vacation?</h4>

      <? for (var i in places) { ?>
        <input type="radio" name="places"
          value="<?= places[i] ?>" /><?= places[i] ?>
        <br />
      <? } ?>

      <br />
      <input class="submit" id="btnSubmit" type="button" value="SUBMIT" />
    </form>
  </body>
</html>

创建电子投票应用程序

之前的应用程序将每个响应附加到 Responses 工作表。但是,我们需要更新每个选项的计数。如果我们能让选定的选项通过计数器增加,那么我们可以使用相同的应用程序进行电子投票。

按照以下方式编辑 Places 工作表中列 AB 的标签/标题:

创建电子投票应用程序

按照以下方式更新 Form.html 文件中的 HTML 代码:

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">

    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/ script/css/add-ons1.css" />
    <script src="img/jquery.min.js"></script>

    <script>
      $(function(){
        $("#btnSubmit").click(postData);
      });

      function postData(){
        // Remove previous messages if any
        $("#error,#success").remove();

        // Disable the submit button until server returns // anything.
        this.disabled = true;

        // Call server function
        google.script.run
          .withSuccessHandler(showSuccess)
          .withFailureHandler(showError)
          .withUserObject(this)
          .postFormDataToSheet(this.parentNode);
      }

      /*
       * msg -  the error or success message returned *        from the server.
       *
       * elem - the reference to the user object (form).
       *
       */
      function showSuccess(msg,elem) {
        elem.disabled = false;
        var div = $('<div id="success"> <font color="green">' + msg + '</font></div>');
        $(elem).after(div);
      }

      /*
       * msg -  the error or success message returned *        from the server.
       *
       * elem - the reference to the user object (form).
       *
       */
      function showError(msg,elem) {
        elem.disabled = false;
        var div = $('<div id="error" class="error">' + msg + '</div>');
        $(elem).after(div);
      }
    </script>
  </head>

  <body>
    <form>
      <h4>Where will you go for vacation?</h4>

      <? for (var i in places) { ?>
        <input type="radio" name="places" value="<?= i ?>" /><?= places[i] ?><br />
      <? } ?>

      <br />
      <input class="blue" id="btnSubmit" type="button" value="SUBMIT" />
    </form>
  </body>
</html>

对于此应用程序,保留Form.html代码不变,但更新doGetpostFormDataToSheet函数,如下所示:

function doGet() {
  // Replace with your spreadsheet's ID.
  var ss = SpreadsheetApp.openById("spreadsheet's id");
  var SheetPlaces = ss.getSheetByName("Places");

  var data = SheetPlaces.getDataRange().getValues();
  data.shift();

  var places = [];
  data.forEach(function(row){
    places.push(row[0]);
  });

  var template = HtmlService.createTemplateFromFile("Form.html");
  template.places = places;

  var html = template.evaluate();
  html.setTitle("eVoting");

  return HtmlService.createHtmlOutput(html);
}

function postFormDataToSheet(e){
  // Replace with your spreadsheet's ID.
  var ss = SpreadsheetApp.openById("spreadsheet's id");
  var SheetPlaces = ss.getSheetByName("Places");

  var data = SheetPlaces.getDataRange().getValues();

  var i = Number(e.places)+1;
  data[i][1]++;

  SheetPlaces.getRange(1, 1, data.length, data[0].length).setValues(data);

  return "Your response submitted successfully. Thank you!";
}

在此应用程序中,响应不会附加,而是在每次提交时增加计数。以下是一个示例输出:

创建电子投票应用程序

创建票务预订应用程序

此应用程序作为用户提交值到服务器的 HTML 表单。这可能包括预订演出门票、预订场馆座位、预订酒店房间以及更多目的。

创建一个电子表格,并创建如下截图所示的列标签:

创建票务预订应用程序

在代码文件中,创建doGetdoPostcancelReservation函数:

function doGet(e) {
  // Maximum available
  const MAX_TICKETS = 25;

  // 'cancel' is a query string appended with the published URL.
  var cancel = e.parameter.cancel;

  if(cancel){
    var msg = cancelReservation(cancel);
    return ContentService.createTextOutput(msg);
  }

  // Replace with your spreadsheet's ID.
  var ss = SpreadsheetApp.openById("spreadsheet's id");
  var SheetReservations = ss.getSheetByName("Reservations");

  var data = SheetReservations.getDataRange().getValues();
  data.shift();

  var template = HtmlService.createTemplateFromFile("Form.html");
  template.available = MAX_TICKETS - data.length;

  if(template.available < 1)
  return ContentService.createTextOutput ("All tickets reserved, sorry!");

  // Use the following line of code for testing purposes only
  // Replace with your development URL.
  template.pubUrl = "https://script.google.com/macros/s/ " + " AKfycbzIkrLEaMMRRYwOA_d_Tiy1TFtxUylaotB07HB4wZGW/dev";

  // Uncomment the following line for the production use.
  //template.pubUrl = ScriptApp.getService().getUrl();

  var html = template.evaluate();
  return HtmlService.createHtmlOutput(html);

}

在前面的代码中,doGet函数最初检查任何查询,如附加到 URL 的cancel。如果存在cancel,则调用cancelReservation函数,否则返回 HTML 表单:

/**
 *  This function post the form data to the
 *  spreadsheet.
 *
 */
function doPost(e){
  // Replace with your spreadsheet's ID.
  var ss = SpreadsheetApp.openById("spreadsheet's id");
  var SheetReservations = ss.getSheetByName("Reservations");

  // name, phone_number and e-mail are form elements.
  var name = e.parameter.name;
  var phoneNumber = e.parameter.phone_number;
  var email = e.parameter.email;
  var ticketNumber = +new Date(); // current date as epoch number

  SheetReservations.appendRow(
    [name, phoneNumber, email, ticketNumber, "Reserved"]
  );

  // Use the following line of code for testing purposes only.
  // Replace with your development URl.
  var pubUrl = "https://script.google.com/macros/s/ " + " AKfycbzIkrLEaMMRRYwOA_d_Tiy1TFtxUylaotB07HB4wZGW/dev";

  // Uncomment the following line for production use.
  //pubUrl = ScriptApp.getService().getUrl();

  var emailBody = '<p>Thank you for registering. Your ticket number: ' + ticketNumber + '</p>';

  emailBody += '<p>You can <a href="'+ pubUrl +'?cancel=' + ticketNumber + '">click here</a> to cancel reservation.</p>';

  // Send confirmation e-mail with cancel link
  MailApp.sendEmail({
    to: email,
    subject: "Reservation Confirmation",
    htmlBody: emailBody
  });

  // Return confirmation text message to the browser.
  return ContentService.createTextOutput("Your ticket reserved and confirmation email has been sent.\nThank you!");
}

function cancelReservation(timestamp){

  // Replace with your spreadsheet id.
  var ss = SpreadsheetApp.openById("spreadsheet's id");

  var SheetReservations = ss.getSheetByName("Reservations");

  var data = SheetReservations.getDataRange().getValues();

  /*
   * Identify sheet row by timestamp if it matches
   * then mark as cancelled.
   *
   */
  for(var i = 0; i < data.length; i++){
    if(data[i][3] == timestamp) data[i][4] = "Cancelled";
  }

  // Replace the updated data in sheet
  SheetReservations.getRange(1, 1, data.length, data[0].length).setValues(data);

  return "Your reservation cancelled.";
}

前面的函数将票号(timestamp)与现有数据进行比较,如果该票号存在,则将其标记为已取消。

Form.html文件中插入以下代码:

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/ css/add-ons1.css" />
    <script src="img/jquery.min.js"></script>
  </head>

  <body>
    <form method="post" action="<?= pubUrl ?>" >

      <h4>Reservation Form</h4>
      <p>Available: <?= available ?></p>

      <input type="text" name="name" placeholder="Enter your name"/>

      <br />
      <input type="text" name="phone_number" placeholder="Enter phone number"/>

      <br />
      <input type="text" name="email" placeholder="Enter email id"/>

      <br /><br />
      <input class="blue" id="btnSubmit" type="submit" value="Reserve"/>

    </form>
  </body>
</html>

以下是一个电子邮件正文文本的示例截图:

创建票务预订应用程序

以下是一个Reservations工作表的示例输出:

创建票务预订应用程序

摘要

在本章中,你学习了如何创建许多有用的实际应用程序,包括预订系统应用程序。下一章将专注于谷歌日历。你将学习如何创建日历事件以及如何启用谷歌的高级服务。你还将学习创建谷歌驱动文件路由和搜索应用程序。

第五章:创建谷歌日历和驱动应用程序

在上一章中,你学习了如何使用FormAppContentServiceHtmlService编程创建表单。你还学习了如何使用doGetdoPost函数。

在本章中,你将学习如何:

  • 创建日历事件

  • 启用谷歌的高级服务

  • 创建几个驱动应用程序

CalendarApp

CalendarApp类提供了对日历基本服务的直接访问。此服务允许你读取和更新你的默认日历以及订阅的日历。使用 GAS,你可以创建日历事件,并通过编程方式邀请你的朋友。你甚至可以抓取事件详情并将它们填充到表格中。

从简单描述创建日历事件

你可以通过将描述作为参数传递给CalendarApp类的createEventFromDescription方法来创建一个事件:

function createCalendarEventFromDescription(){
  CalendarApp.getDefaultCalendar()
    .createEventFromDescription('Team Meeting, Monday from 3 PM to 4 PM');
}

创建简单的日历事件

你也可以通过指定标题、开始时间和结束时间来创建事件:

function createCalendarEvents() {
    var title = "Title of the event";
    var startTime = new Date("October 21, 2015 21:00:00");
    var endTime = new Date("October 21, 2015 21:30:00");

    CalendarApp.getDefaultCalendar()
      .createEvent(title, startTime, endTime);
}

使用选项创建事件

以下代码显示了如何使用指定的选项创建事件,例如描述和位置。只有在你插入嘉宾的电子邮件 ID 时才取消注释sendInvites行。如果有多个电子邮件 ID,请使用逗号分隔它们:

function createCalendarEventsWithOptions() {
  var options = {
    description : 'Description of the event',
    location : 'Event Location',
    //sendInvites : true,
    //guests : 'Comma-separated list of guest email IDs.'
  };

  var title = "Title of the event";
  var startTime = new Date("October 21, 2015 21:00:00");
  var endTime = new Date("October 21, 2015 21:30:00");

  CalendarApp.getDefaultCalendar()
    .createEvent(title, startTime, endTime, options);
}

从表格数据创建事件

要从预先填充的表格数据创建事件,创建一个名为Events的表格,并创建如所示的头列:

从表格数据创建事件

创建如所示的createCalendarEventsFromSheetData函数:

function createCalendarEventsFromSheetData() {
  /*
   * 'Events' sheet column numbers,
   * use 0 for column 'A',
   * 1 for column 'B' and so on.
   * This makes life easy to use in '0' indexed JS arrays.
   *
   */
  const TITLE = 0;
  const START_TIME = 1;
  const END_TIME = 2;
  const DESCRIPTION = 3;
  const LOCATION = 4;
  const SEND_INVITES = 5;
  const GUESTS = 6;

  var sheet = SpreadsheetApp.getActiveSpreadsheet()
              .getSheetByName("Events");

  var data = sheet.getDataRange().getValues();

  // Remove header
  var header = data.shift();

  var options = {
    description : '',
    location : '',
    sendInvites : false,
    guests : ''
  };

  for(var i in data){
    /*
     * 'data' is a 2-dim array.
     * First index for row numbers and
     * second index for column numbers.
     *
     */
    options.description = data[i][DESCRIPTION];
    options.location = data[i][LOCATION];
    options.sendInvites = data[i][SEND_INVITES];
    options.guests = data[i][GUESTS];

    var title = data[i][TITLE];
    var startTime = data[i][START_TIME];
    var endTime = data[i][END_TIME];

    CalendarApp.getDefaultCalendar()
      .createEvent(title, startTime, endTime, options);
  }  
}

从外部 CSV 文件的内容创建事件

与从表格数据创建事件不同,你可以从上传到驱动的外部 CSV 文件中创建它们。上传一个与上一个任务中相同的头列的 CSV 文件。

获取上传文件的密钥/ID,并用以下代码替换它:

function createEventsFromCsvData(){
  // CSV columns, 0 based.
  const TITLE = 0;
  const START_TIME = 1;
  const END_TIME = 2;
  const DESCRIPTION = 3;
  const LOCATION = 4;
  const SEND_INVITES = 5;
  const GUESTS = 6;

  // Put the key/ID of the CSV file placed in Drive.
  var blob = DriveApp.getFileById("[[ CSV file id ]]").getBlob();
  var str = blob.getDataAsString();

  var data = Utilities.parseCsv(str);
  // Now the data is a two-dimensional array

  // Remove header
  data.shift();

  var options = {
    description : '',
    location : '',
    sendInvites : false,
    guests : ''
  };

  for(var i in data){

    // Skip if no title
    if(!data[i][0]) continue;

    // Populate the options object
    options.description = data[i][DESCRIPTION];
    options.location = data[i][LOCATION];
    options.sendInvites = data[i][SEND_INVITES];
    options.guests = data[i][GUESTS];

    var title = data[i][TITLE];
    var startTime = data[i][START_TIME];
    var endTime = data[i][END_TIME];

    CalendarApp.getDefaultCalendar()
      .createEvent(title, startTime, endTime, options);

  }
}

启用高级谷歌服务

到目前为止,你一直在使用 GAS 的基本服务,例如GmailAppContactsApp。现在是你学习如何启用高级服务的时候了。

在这个任务中,我们将使用一个高级服务——日历服务,因此在使用之前我们必须启用它。

在脚本编辑器中,点击资源,然后点击高级谷歌服务…,将打开一个弹出窗口:

启用高级谷歌服务

高级谷歌服务弹出窗口中,所有 GAS 高级服务都将列出。查找日历 API服务,选择最新版本(默认选中),然后如果尚未启用,则启用它。在以下屏幕截图中,你可以看到日历 API 服务已启用:

启用高级谷歌服务

仅在脚本中启用高级服务是不够的,你还需要在谷歌开发者控制台中启用它,如弹出窗口中所示。为此,请点击弹出窗口中提供的链接。

然后,将打开一个新的浏览器窗口或标签页,其中列出了一些作为组的流行 API。您可以在 Google Apps APIs 组下看到 Calendar API。如果没有列出,请使用页面顶部的搜索选项搜索calendar一词。

启用高级 Google 服务

点击 Calendar API(如前一张截图中所示,用黄色突出显示),然后在随后的网页上点击 启用 API

启用高级 Google 服务

就这些;您已启用日历高级服务。

列出所有日历

启用日历高级服务后,您可以使用 listCalendars 函数记录所有您的日历:

/**
 *  Logs all of your calendars with IDs.
 *
 */
function listCalendars() {
  var calendars, pageToken = null;

  do {
    calendars = Calendar.CalendarList.list({
      maxResults: 100,
      pageToken: pageToken
    });

    if (calendars.items && calendars.items.length > 0) {
      for (var i = 0; i < calendars.items.length; i++) {
        var calendar = calendars.items[i];
        Logger.log('%s (ID: %s)', calendar.summary, calendar.id);
      }
    } else {
      Logger.log('No calendars found.');
    };

    // If more than one page, then return a token, else null.
    pageToken = calendars.nextPageToken;

  } while (pageToken);
}

Calendar.CalendarList.list 对象返回所有日历的列表,前提是日历的数量小于 maxResults 的值。如果日历的数量大于此值,则使用 nextPageToken 值作为下一迭代的页面令牌。以下是日志的示例输出:

列出所有日历

在表格中列出日历事件

要将任何日历的事件列表到表格中,创建一个名为 ExistingEvents 的新表格,并添加以下函数:

function listEventsFromOneCalendar() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet()
        .getSheetByName("ExistingEvents");

  var source = "Replace with source calendar email id";
  var srcCalId = Calendar.Calendars.get(source).id;

  var syncdays = 30;
  var now = new Date();
  var min = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  var max = new Date(now.getFullYear(), now.getMonth(), now.getDate() + syncdays);

  var srcEvents = Calendar.Events.list(srcCalId, {
    timeMin: min.toISOString(),
    timeMax: max.toISOString(),
    singleEvents: true,
    orderBy: 'startTime',
  }).items;

  /*
   * To store events data in a spreadsheet we need
   * to construct a 2-dim array
   *
   */
  var output = [];

  /*
   * 'srcEvents' is an array of event objects.
   *
   * Every event object is passed as 'e' to the anonymous
   * function.
   *
   */
  srcEvents.forEach(function(e){
    // Construct an event array (1-dim)
    var event = [];

    /*
     * Returns "" if object value is 'null' or 'undefined'
     *   otherwise returns the object value.
     *
     */
    event.push(e.summary || "");
    event.push(e.start.dateTime || "");
    event.push(e.end.dateTime || "");
    event.push(e.description || "");
    event.push(e.location || "");

    // Push each event array to output (2-dim array).
    output.push(event);
  });

  var header = [
                 "Title/Subject",
                 "Start Time",
                 "End Time",
                 "Description",
                 "Location"
               ];

  // Insert header at the top of the output.
  output.unshift(header);

  sheet.clearContents();

  sheet.getRange(1, 1, output.length, header.length)
  .setValues(output);
};

我们刚才提到的函数从日历中收集所有事件,构建一个二维数组,并将该数组存储在 ExistingEvents 表格中。以下是前面代码的示例输出:

在表格中列出日历事件

从一个日历同步事件到另一个日历

syncEvents 函数(如下面的代码中列出)同步源日历到目标日历的最后 30 天事件。为了测试此应用程序,创建主函数 syncEvents 和其他辅助函数,如 updateEvent_deleteEvent_insertEvent_。我们通过在事件标题/摘要前加上 sync: 并用方括号括起来来标记从源同步到目标的事件。例如,如果源事件是 Example event,则它将被标记为 [sync:Example event] 并插入/更新到目标日历中:

/**
 *  Replace Source and Destination with your own Calendars name.
 *
 *  You should have write access in the destination Calendar,
 *  in other words it should have been created by you.
 *
 */
function syncEvents() {
  const RATE_LIMIT = 10; // Milliseconds

  var source = "[[ Source ]]"; // Source calendar email id.
  var destination = "Destination"; // Destination calendar name.

  var srcCalId = Calendar.Calendars.get(source).id;

  // Returns calendars (matching with the name) as an array
  var dstCal = CalendarApp
                 .getCalendarsByName(destination)[0];

  var dstCalId = dstCal.getId();

  var syncdays = 30;
  var now = new Date();

  var min = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  var max = new Date(now.getFullYear(), now.getMonth(), now.getDate() + syncdays);

  // Get all source events as an array of objects.
  var srcEvents = Calendar.Events.list(srcCalId, {
    timeMin: min.toISOString(),
    timeMax: max.toISOString(),
    singleEvents: true,
    orderBy: 'startTime',
  }).items;

  // Get all destination events as an array of objects.
  var allDstEvents = Calendar.Events.list(dstCalId, {
    timeMin: min.toISOString(),
    timeMax: max.toISOString(),
    singleEvents: true,
    orderBy: 'startTime',
  }).items;

  /*
   * Get all destination events already synced from source
   * identified with the help of prefix '[sync:'
   *
   */
  var dstEvents = allDstEvents.filter(function(event){
      return /\[sync:\w+/.test(event.summary)?true:false;
  });

  // UPDATE all dstEvents with the corresponding srcEvents.
  for(var d in dstEvents){
    for(var s in srcEvents){

      if(dstEvents[d] && srcEvents[s] && dstEvents[d].id == srcEvents[s].id){
        /*
         * Update srcEvents with 'sync:' marking in the * destination calendar.
         *
         */
        srcEvents[s].summary = srcEvents[s].summary||'' + " [sync:"+source+"]";

        updateEvent_(srcEvents[s],dstCalId);

        // Delete updated dstEvents and srcEvents.
        srcEvents.splice(s,1);
        dstEvents.splice(d,1);
        Utilities.sleep(RATE_LIMIT);
      }

    }
  };

  /*
   * DELETE remaining dstEvents (those that do not exist in
   * srcEvents).
   *
   */
  for(var d in dstEvents){
    deleteEvent_(dstEvents[d],dstCalId);
    Utilities.sleep(RATE_LIMIT);
  };

  // INSERT remaining srcEvents (those do not exist in dstEvents).
  for(var s in srcEvents){
    srcEvents[s].summary = srcEvents[s].summary||'' 
+ " [sync:"+source+"]";
    insertEvent_(srcEvents[s],dstCalId);
    Utilities.sleep(RATE_LIMIT);
  }
};

之前提到的 syncEvents 函数从 SourceDestination 事件中收集事件,并按以下方式处理:

  • 它更新了源和目的地(两个数组都有的)中出现的所有事件

  • 它删除源中不存在但在目的地中存在的事件(即仅在目的地中出现)

  • 它将源中出现但不在目的地中出现的事件插入(即仅在源中出现)

辅助函数列在这里:

function updateEvent_(evt,calId){
  Calendar.Events.update( evt, calId, evt.id );
};

function deleteEvent_(evt,calId){
  Calendar.Events.remove(calId, evt.id);
};

function insertEvent_(evt,calId){
  try{
    Calendar.Events.insert(evt, calId);
  } catch(e) {
    var err = e.message;
    var newEvt = {
      summary:evt.summary,
      start:evt.start,
      end:evt.end,
      attachments:evt.attachments,
      attendees:evt.attendees,
      reminders:evt.reminders
    };

    if(err.search(/identifier already exists/gi) >= 0){
      updateEvent_(evt,calId);
    } else if(err.search(/Not Found/gi) >= 0){
      insertEvent_(newEvt,calId);
    } else if(err.search(/Invalid resource/gi) >= 0){
      insertEvent_(newEvt,calId);
    } else {
      Logger.log("%s [%s]\n",evt,err);
    };
  }
};

恭喜!您已创建了一个工作的日历同步应用程序。

DriveApp

此类允许您在您的 Drive 中创建、搜索和修改文件和文件夹。

关于DriveApp类的参考文档,请参阅网站:developers.google.com/apps-script/reference/drive/drive-app?hl=en

创建自定义 PDF 文件

假设你需要从工作表或外部数据创建自定义 PDF 文件。我们可以从 HTML 模板创建 PDF 文件。你只需格式化列标题,并在新的工作表(AddressBook)中放入一些示例数据,如下面的截图所示:

创建自定义 PDF 文件

Code.gs文件中创建createPdfs函数,具体步骤如下:

function createPdfs(){

  // 0 based column numbers
  const NAME = 0;
  const TITLE = 1;
  const COMPANY = 2;
  const ADDRESS = 3;
  const CITY = 4;
  const ZIP_PIN = 5;

  /* Get data from the sheet */
  var sheet = SpreadsheetApp.getActiveSheet();
  var data = sheet.getDataRange().getValues();
  /*
   * Alternatively you can get data * from an external CSV file or anything else.
   * 
   * Example:
   * var blob = DriveApp.getFileById(id).getBlob();
   * var text = blob.getDataAsString();
   * var data = JSON.parse(text);
   *
   */

  // Remove headers
  data.shift();

  var folderName = "Letters";
  var folder, folders = DriveApp.getFoldersByName(folderName);

  // 'folders' is an iterator
  if (folders.hasNext()){
    // Get first folder if more than 1 with same name.
    folder = folders.next();
  } else {
    // Create folder if it does not exist.
    folder = DriveApp.createFolder(folderName);
  }

  for(var i in data){
    /*
     * Set as global variables so that we will be able to access
     * in the Template.html code.
     *
     */
    name = data[i][NAME];
    title = data[i][TITLE];
    company = data[i][COMPANY];
    address = data[i][ADDRESS];
    city = data[i][CITY];
    zip_pin = data[i][ZIP_PIN];

    var html = HtmlService.createTemplateFromFile ("Template.html").evaluate();

    // Convert HTML to PDF
    var pdf = html.getAs("application/pdf") 
.setName(name + ".pdf");

    // Save in the 'My Drive | Letter' folder.
    folder.createFile(pdf);
  }

}

createPdfs函数从工作表获取数据,或者你可以修改它以从外部源获取数据。它为每一行数据创建一个 HTML 模板,将其转换为 PDF,并将其存储在 Drive 文件夹中。让我们将nametitlecompanyaddresscityzip_pin变量分配为全局变量。只有这样,我们才能在 HTML 模板中获取这些值。

创建一个名为Template.html的 HTML 文件,并输入以下代码:

<!DOCTYPE html>
<html>
  <body>
    <p>To</p>
    <p>
      <?= name ?><br />
      <?= title ?><br />
      <?= company ?><br />
      <?= address ?><br />
      <?= city ?><br />
      <?= zip_pin ?><br />
    </p>
    <p>&nbsp;</p>
    <p>Dear <?= name ?>,</p>
    <p>Your message goes here...</p>

    <p>Regards,<br />[Your name]</p>
  </body>
</html>

此代码获取我们描述的全局变量值,并返回自定义的 HTML。在适当的位置更新你的消息和姓名。从这个 HTML 模板中,createPdfs函数创建 PDF 文件,每个文件都根据工作表中的单个行数据进行了定制。所有创建的 PDF 文件都保存在 Drive(我的驱动器 | 信件)文件夹中。

根据工作表数据(第 2 行)创建的 PDF 文件之一(Aaron.pdf)的内容如下所示:

创建自定义 PDF 文件

为了示例,我们使用了这个模板来创建一封简单的信件,但你可以根据你的需求和想象力使用任何类型的模板,例如发票、简历、求职申请等。

创建一个 Drive 文件路由应用

此应用程序可以通过匹配设置选项卡中的标准术语来移动文件。首先,创建一个新的工作表或选项卡,命名为设置,并按以下方式设置列标题:

创建一个 Drive 文件路由应用

此外,创建moveDriveFiles函数,如下面的代码片段所示。如果你运行此函数,则它将根据设置工作表中的设置将文件从根文件夹移动到适当的文件夹。如果尚不存在,将创建目标文件夹。你还可以创建一个触发器,在预定时间或定期运行此函数:

function moveDriveFiles(){
  var SheetSettings = SpreadsheetApp.getActiveSpreadsheet()
        .getSheetByName("Settings");

  // Open the root folder.
  var rootFolderName = "Replace with root folder name.";
  var rootFolder, destFolder, folders = DriveApp
        .getFoldersByName(rootFolderName);

  // 'folders' is an iterator
  if (folders.hasNext()) rootFolder = folders.next();
  else {
    // Show warning "Folder does not exist."
    Browser.msgBox(
      "The root folder " + rootFolderName + " not exist."
    );

    return;
  }

  var data = SheetSettings.getDataRange().getValues();
  data.shift();// Remove header row

  for(var i in data){
    var fileName = data[i][0];
    var folderName = data[i][1];

    // Open or create the destination folder
    folders = rootFolder.getFoldersByName(folderName);

    if (folders.hasNext()) destFolder = folders.next();
    else destFolder = rootFolder.createFolder(folderName);

    /*
     * Move matching files to the destination folder
     * The filename should be enclosed in quotes.
     *
     */
    var dest, file, files = rootFolder
          .searchFiles('title contains "' + fileName + '"');
    /*
     * In the above line, the searchFiles method's argument should
     * be a string (SQL-like query), so take care to escape special
     * characters.
     * Here is an alternative way to write the method:
     * searchFiles("title contains \"" + fileName + "\"")
     *
     */

    /*
     * We cannot move files directly,
     * so copy file to the destination and remove in source.
     *
     */
    while (files.hasNext()){
      dest = destFolder;
      file = files.next();

      file.makeCopy(file, dest);
      rootFolder.removeFile(file);
    }
  }
}

创建一个 Drive 文件搜索应用

现在你将创建一个文件搜索应用。它可以在文本字段中根据某些标准搜索 Drive 中的文件。在Code.gs文件中创建onOpenshowSidebarlistDriveFiles函数,如下所示:

function onOpen(){
  SpreadsheetApp.getUi().createAddonMenu()
  .addItem("File Search", "showSidebar")
  .addToUi();
  showSidebar();
}

/**
 * Opens sidebar containing the user interface.
 *
 */
function showSidebar() {
  SpreadsheetApp.getUi().showSidebar(
    HtmlService.createHtmlOutputFromFile('Sidebar')
      .setTitle('Search Files in Drive')
  );
}

onOpen函数创建一个插件菜单并调用showSidebar函数。这意味着每次打开电子表格时,都会添加插件菜单并显示侧边栏:

/**
 *  Lists files matching with arg 'txt', in the Settings sheet.
 *  
 */
function listDriveFiles(txt){
  // 'Files' sheet column heading.
  var header = ["File", "URL"];

  var output = [header];

  var file, files = DriveApp.searchFiles ('title contains "' + txt + '"');

  // 'files' is an iterator.
  while (files.hasNext()){
    file = files.next();
    var name = file.getName();
    var link = file.getUrl();

    output.push([name,link]);
  };

  var sheet = SpreadsheetApp.getActiveSpreadsheet()
        .getSheetByName("Files");

  sheet.clearContents();

  /*
   * output.length for number of rows and
   * header.length for number of columns
   *
   */
  sheet.getRange(1, 1, output.length, header.length)
    .setValues(output);
}

创建一个名为 Sidebar.html 的新 HTML 文件,并将以下代码放入其中:

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script /css/add-ons1.css" />
    <script src="img/jquery.min.js">
    </script>

  </head>

  <body>
    <input type="text" id="txt" />
    <button class="green" id="btn">Search</button>
  </body>

  <script>
    // On document load, assign click handler to the search
    // button.
    $(function() {
      $('#btn').click(listFiles);
    });

    function listFiles() {
      this.disabled = true;
      $('#error,#success').remove();
      google.script.run
        .withSuccessHandler(function(msg,elm){
           elm.disabled = false;
         })
        .withFailureHandler(function(err,elm){
           elm.disabled = false;
           showError(err,elm);
         })
        .withUserObject(this)
        .listDriveFiles($('#txt').val());
    }

    /**
     * Inserts a div that contains success message after a given 
     * element.
     *
     * @param {string} msg - The message to display.
     * @param {object} element - The element after which to 
     * display the message.
     *
     */
    function showSuccess(msg,element) {
      var div = $('<div id="success"><font color="green">' + msg + '</font></div>');
      $(element).after(div);
    }

    /**
     * Inserts a div that contains error message after a given 
     * element.
     *
     * @param {string} msg - The error message to display.
     * @param {object} element - The element after which to 
     *  display the error.
     *
     */
    function showError(msg, element) {
      var div = $('<div id="error" class="error">' + msg + '</div>');
      $(element).after(div);
    }

  </script>
</html>

在打开电子表格或运行 showSidebar 函数时,侧边栏将打开,如以下截图所示,除了文本字段中的 Chapter 文本。你可以输入任何其他文本来搜索文件。点击 搜索 按钮后,脚本将在 Drive 中搜索包含该文本的文件,并在 Files 工作表中填充数据。

以下截图显示了侧边栏和示例输出:

创建一个驱动文件搜索应用

摘要

在本章中,你学习了并创建了许多有用的实际应用,包括一个事件同步应用。在下一章中,你将学习如何创建 RSS/Atom 阅读器和语言翻译应用。

第六章。创建 Feed 阅读器和翻译应用程序

在上一章中,你学习了如何创建日历事件和 Drive 应用程序。你构建了许多实际的应用程序。

在本章中,你将学习如何

  • 创建 Google 搜索

  • 创建股票报价滚动条

  • 创建 RSS 源阅读器

  • 创建 Atom 源阅读器

  • 创建语言翻译应用程序

  • 创建文档审阅和即时内联评论应用程序

UrlFetchApp 类

UrlFetchApp 类可用于发出 HTTP/HTTPS 请求并从任何网站/URL 获取响应。例如,UrlFetchApp.fetch ("https://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=PACKT") 代码返回了带有搜索词 PACKT 的搜索结果。响应将包含原始内容和 HTTP 标头。

如果 UrlFetchApp 类的 fetch 方法和 URL 之间的通信正常,则 URL 应返回响应代码 200。否则,任何其他数字都对应于错误类型。我们可以在使用 getResponseCode 方法之前检查响应代码。

你可以使用 getContentText 方法获取内容文本,并将其解析为 JSON 对象。以下代码片段提取了内容文本并将其解析为 JSON:

var url = "https://ajax.googleapis.com/ajax/services/search /web?v=1.0&q=PACKT";
try{
    var resp = UrlFetchApp.fetch(url);
    if(resp.getResponseCode() == 200){
      var text = resp.getContentText();
      var json = JSON.parse(text);
      Logger.log(json);
    }
  } catch(e){
    Logger.log(e);
  };

为了帮助你理解,以下提供了日志输出(文本可能被截断和格式化以节省空间):

{
  responseDetails=null,
  responseData={
    cursor={
      moreResultsUrl=http://www.google.com/search?oe=utf8&ie=utf8
        &source=uds&start=0&hl=en&q=PACKT,
      resultCount=800,000,
      pages=[{start=0, label=1}, {start=4, label=2}, {start=8, 
        label=3}, {start=12, label=4}, {start=16, label=5}, 
        {start=20, label=6}, {start=24, label=7}, {start=28, 
        label=8}],
      searchResultTime=0.29,
      currentPageIndex=0,
      estimatedResultCount=800000
    },
    results=[
      {
        visibleUrl=www.packtpub.com,
        cacheUrl=http://www.google.com/search?q=cache:rbL6l6pFt8…,
        GsearchResultClass=GwebSearch,
        title=<b>Packt</b> Publishing | Technology Books, eBook…,
        titleNoFormatting=Packt Publishing | Technology Books…,
        url=https://www.packtpub.com/,
        content=<b>Packt</b> Publishing is the leading UK provid…,
        unescapedUrl=https://www.packtpub.com/
      }, {
        visibleUrl=www.packtpub.com,
        cacheUrl=http://www.google.com/search?q=cache:wo2TeIpsCG…,
        GsearchResultClass=GwebSearch,
        title=Free Learning | <b>PACKT</b> Books - <b>Packt</b> …,
        titleNoFormatting=Free Learning | PACKT Books - Packt Pu…,
        url=https://www.packtpub.com/packt/offers/free-learning,
        content=A new free programming tutorial book every day...,
      }, {
        visibleUrl=www.packtpub.com,
        cacheUrl=http://www.google.com/search?q=cache:D7qMTpx1Nu…,
        GsearchResultClass=GwebSearch,
        title=All Books and eBooks | <b>PACKT</b> Books - <b>Pac…,
        titleNoFormatting=All Books and eBooks | PACKT Books – P…,
        url=https://www.packtpub.com/all,
        content=Packt Publishing provides technology eBooks, boo…,
        unescapedUrl=https://www.packtpub.com/all
      }
    ]
  },
  responseStatus=200
 }

JSON 的顶级对象是 responseDetailsresponseDataresponseStatus。如果 Google 返回了正确的响应,那么 responseStatus 的值应该是 200。这是由 Google 搜索服务返回的,而不是 UrlFetchApp 的状态。你也可以检查 responseStatus 的值是否等于 200,以确认响应内容是否正常。

你需要深入挖掘 responseData 对象,它包含作为对象数组的搜索结果。要获取数组,请使用 json.responseData.results,然后遍历数组以获取所需的结果数据。我们将创建一个应用程序来搜索 Google 并在表格中填充结果。

创建 Google 搜索应用程序

创建一个新的表格,将其重命名为 Google 并创建如下截图所示的标题:

创建 Google 搜索应用程序

然后,在 Code.gs 文件中输入以下代码:

function searchGoogle(){

  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var SheetGoogle = ss.getSheetByName("Google");
  var kwd = SheetGoogle.getRange("B2").getValue();

  // Encode URI components if any in kwd
  kwd = encodeURIComponent(kwd);

  // Replace space with '+'
  kwd = kwd.replace(/%20/gi, "+");

  // Remove '?' marks
  kwd = kwd.replace(/%3F/gi, "");

  var url = "https://ajax.googleapis.com/ajax/services/ search/web?v=3.0&q=" + kwd;

  try{
    var resp = UrlFetchApp.fetch(url).getContentText();
    var json = JSON.parse(resp);
    var result = json.responseData.results;
  } catch(e){
    Logger.log(e);
  };

  // We require a 2-dimensional array to store data in sheet
  var output = [];
  var visibleUrl,title,url,content;

  for(var i=0; i<result.length; i++){
    visibleUrl = result[i].visibleUrl;
    title = result[i].title;
    url = result[i].url;
    content = result[i].content;

    output.push([visibleUrl,title,url,content]);
  };

  /*
   * output.length for number of rows and output[0].length for
   * number of columns
   *
   */
  SheetGoogle.getRange(5, 1, output.length, output[0].length)
    .setValues(output);
}

以下截图显示了应用程序的示例输出:

创建 Google 搜索应用程序

你可以使用单元格 B2 中的其他关键词测试代码。你可以为 searchGoogle 函数创建一个按钮或自定义菜单,以便你可以频繁搜索。

创建股票报价滚动条应用程序

以下截图显示了从 Google 财经获取股票报价的简单函数。查询字符串 (q) 指定了 NASDAQ 作为证券交易所,GOOG(谷歌)作为股票代码。

function getStockQuote1(){
  var url = "http://finance.google.com/finance/info?q=NASDAQ:GOOG";

  var resp = UrlFetchApp.fetch(url).getContentText();
  Logger.log(resp);
}

在前面的代码中,我们正在使用 UrlFetchApp 类的 fetch 方法。日志响应如下:

 // [
{
"id": "304466804484872"
,"t" : "GOOG"
,"e" : "NASDAQ"
,"l" : "717.00"
,"l_fix" : "717.00"
,"l_cur" : "717.00"
,"s": "0"
,"ltt":"4:00PM EST"
,"lt" : "Nov 13, 4:00PM EST"
,"lt_dts" : "2015-11-13T16:00:01Z"
,"c" : "-14.23"
,"c_fix" : "-14.23"
,"cp" : "-1.95"
,"cp_fix" : "-1.95"
,"ccol" : "chr"
,"pcls_fix" : "731.23"
}
]

在返回的响应文本中,你可以看到前四个字符是一个换行符,两个斜杠(//)和一个空格字符,因此你必须在解析所需的 JSON 对象之前移除它们。从返回的字符串中移除前四个字符会使事情变得更容易。你可以使用 substr 方法来移除前四个字符。JavaScript JSON(JavaScript 对象表示法)对象的 parse 方法将字符串解析为 JSON 对象。

获取 Google 股票的 最后交易价格LTP)的修改后代码如下:

function getStockQuote2(){
  var url = "http://finance.google.com/finance/info?q=NASDAQ:GOOG";

  var resp = UrlFetchApp.fetch(url).getContentText().substr(4);
  var json = JSON.parse(resp);
  Logger.log(json[0].l);
}

你可以在查询字符串中使用多个股票代码(以逗号分隔)。例如,http://finance.google.com/finance/info?q=NASDAQ:GOOG,AMD,MCHP。然后,响应将是一个股票报价对象的数组。你必须遍历数组以获取每个股票代码的数据。

我们将创建一个完整的股票报价应用程序。创建或添加一个新的表格,并将其重命名为 Quotes,填充 SYMBOL 列,并按以下格式进行格式化:

创建股票报价滚动应用

根据 listed here 修改 Code.gs 文件中的 getStockQuotes 函数:

function getStockQuotes(){
  var SheetQuotes = SpreadsheetApp.getActiveSpreadsheet()
                    .getSheetByName("Quotes");

  var data = SheetQuotes.getDataRange().getValues();

  // Remove the header from data.
  var header = data.shift();

  // Extracts all symbols from sheet data.
  var aScrips = [];
  for(var i in data) aScrips.push(data[i][0]);

  // Join all scrip names with comma.
  var sScrips = aScrips.join(",");

  // Fetch data with scrip names as query.
  var url = "http://finance.google.com/finance/info?q=NASDAQ:"+sScrips;

  // Send the request to the url
  try{
    var resp = UrlFetchApp.fetch(url).getContentText().substr(4);
    var json = JSON.parse(resp);
  } catch (e) {
    Logger.log(e.message);
    return;
  }

  // We require a 2-dimensional array to store data in sheet.
  var output = [];

  // Traverse through all JSON objects.
  for(var i in json){
    var q = json[i];

    // Symbol, price and traded time.
    output.push([q.t,q.l,q.ltt]);
  };

  // Restore the header again.
  output.unshift(header);

  // Save the output in sheet.
  SheetQuotes.getDataRange().setValues(output);
}

以下是该应用程序的样本输出:

创建股票报价滚动应用

对于重复的报价跳动,你可以为 getStockQuotes 函数创建一个按分钟触发的触发器。

记录比特币报价

你听说过比特币、数字或虚拟货币吗?是否曾经观察过比特币价格跳动来买卖比特币?比特币是一种数字资产,正在全球范围内推广一种新的支付系统。这里有一个有趣的记录实时比特币价格的应用程序。在这个应用程序中,我们将记录 Bitstamp 交易平台的价格。www.bitstamp.net 提供获取比特币实时价格跳动的 API。

function getBitCoinPrice(){
  // BitStamp api url
  var url = "https://www.bitstamp.net/api/ticker/";

  var resp = UrlFetchApp.fetch(url);

  if(resp.getResponseCode() == 200){
    var json = JSON.parse(resp);
    Logger.log(json);
  }
}

解析为 JSON 的 Bitstamp API 的响应在此提供:

{volume=6209.19457553, high=422.00, last=420.39, low=414.00, vwap=419.15, ask=421.48, bid=420.23, open=421.15, timestamp=1455894447}

解析所述 JSON 对象到表格的完整代码在此提供:

/**
 *  Log bitcoin price ticks in sheet
 *
 */
function getBitCoinData(){
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var SheetBitCoin = ss.getSheetByName("Bitcoin");

  // Header labels at the top row of the sheet.
  var header = [
      "Timestamp",
      "High",
      "Low",
      "Volume",
      "Bid",
      "Ask"
    ];

  // Insert headers at the top row of the Bitcoin sheet.
  SheetBitCoin.getRange(1,1,1,6).setValues([header]);
  // setValues accept 2-dim array

  // BitStamp api url
  var url = "https://www.bitstamp.net/api/ticker/";

  try{
    var resp = UrlFetchApp.fetch(url);

    // Proceed if no errors returned.
    if(resp.getResponseCode() == 200){

      var json = JSON.parse(resp);

      var output = [];

      /*
       * Bitstamp returns timestamp in seconds
       * (elapsed since epoch), but JavaScript Date accepts in
       * milliseconds, so multiply by 1000.
       *
       */
      output.push( new Date(json.timestamp *= 1000) );

      // last 24 hours high.
      output.push(json.high);

      // last 24 hours low.
      output.push(json.low);

      // last 24 hours volume.
      output.push(json.volume);

      // highest buy order.
      output.push(json.bid);

      // lowest sell order.
      output.push(json.ask);

      // Append output to Bitcoin sheet.
      SheetBitCoin.appendRow(output);
    }

  } catch(e){
    // Log errors to examine and debug it later.
    Logger.log(e);

    throw e;
  }
};

这里提供了一个样本记录的比特币数据。最高价最低价成交量值是过去 24 小时内的最高价、最低价和总成交量。买入价卖出价是实时值。为 getBitCoinData 函数的重复执行创建一个触发器。

记录比特币报价

RSS 和 Atom 源

RSS 代表 丰富网站摘要。它用于发布经常更新的信息。用户需要独立的特殊软件应用程序或浏览器插件,称为 RSS 阅读器,来读取任何源的信息。

源文档包含摘要或全文、元数据、发布日期、作者姓名等。使用源,用户可以接收网站的及时更新或聚合多个网站的数据。用户不需要频繁手动检查这些网站的更新,但需要订阅源。源阅读器会频繁检查订阅的网站是否有新数据,并检索它们。

RSS feed 文档的骨架

这里列出了一个 RSS 馈送文档的示例:

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Title of the channel</title>
    <description>A brief description of the channel</description>
    <language>en-US</language>
    <item>
      <title>Item title</title>
      <link>Link to the item</link>
      <pubDate>Fri, 30 Oct 2015 19:41:00 +0000</pubDate>
      <creator><![CDATA[Author Name]]></creator>
      <category><![CDATA[NEWS]]></category>
      <description><![CDATA[encoded description…]]></description>
    </item>
    <item> … </item>
    <item> … </item>
    <item> … </item>
  </channel>
</rss>

第一行指定了版本,因为这个是一个 XML 文档,对处理软件应用程序来说。

所有其他元素都包含在 rss 根元素内。

channel 元素内部的 titledescriptionlanguage 元素分别指定了频道标题、描述以及频道发布的语言。

item 元素是重复的,它包含单个馈送项信息,例如 title(项的标题)、link(项的链接)、comments(关于项的评论)、pubDate(项的发布日期)、creator(项的创建者)、category(指定项属于哪个类别)、description(关于项的简要描述)以及更多。

创建一个 RSS 阅读器应用程序

您必须使用 XmlServiceUrlFetchApp 返回的 XML 文档中解析所需内容。实际代码如下所示:

function readRssFeedContents(){
  var SheetData = SpreadsheetApp.getActiveSpreadsheet()
                    .getSheets()[0];

  var title, posturl, author, row, output = [];

  // Prefix namespace.
  var dc = XmlService.getNamespace('http://purl.org/dc/elements/1.1/');

  // Fetch feed document.
  var xml = UrlFetchApp.fetch("http://siliconangle.com/feed/")
            .getContentText();

  // Parse the response text from the URL.
  var doc = XmlService.parse(xml);

  // Get child elements from the root element.
  var items = doc.getRootElement().getChild('channel')
              .getChildren('item');

  // Process the required data.
  for(var i=0; i<items.length; i++){
    title = items[i].getChild('title').getText();
    posturl = items[i].getChild('link').getText();
    author = items[i].getChild('creator', dc).getText();
    row = [title].concat(posturl, author);

    output.push(row);
  };

  // Write new data to sheet
  SheetData.getRange(2, 1, output.length, output[0].length)
    .setValues(output);
}

在前面的代码中,使用 XmlService 解析任何良好结构的 XML 内容。在提到的示例源中,我们专注于 channel 元素和一个或多个 item 元素。item 元素本身包含更多信息。要获取所需数据,您必须按照 channel | item | title | link | creator 的顺序挖掘内容。creator 元素以 dc 前缀开头,因此我们必须在代码的最上面几行分配 dc 命名空间 URL。

此应用程序的示例输出如下所示:

创建一个 RSS 阅读器应用程序

Atom 馈送文档的骨架

Atom 馈送类似于 RSS 馈送,具有许多高级功能。root 元素是 feed 而不是 channel。这里展示了一个 Atom 馈送文档的示例:

<?xml version="1.0" encoding="utf-8"?>
<feed >
  <title>Atom Feed</title>
  <subtitle>Subtitle of the feed</subtitle>
  <link href="http://example.com/" />
  <updated>2015-11-13T06:30:02Z</updated>
  <entry>
    <title>Title of the item</title>
    <link href="http://example.com" />
    <updated>2015-11-13T06:30:22Z</updated>
    <summary>Summary text of the item</summary>
    <author>
      <name>Author name</name>
      <email>example@example.com</email>
    </author>
  </entry>
  <entry>...</entry>
  <entry>...</entry>
  <entry>...</entry>
</feed>

创建一个 Atom 馈送阅读器应用程序

此应用程序解析 Google 热门趋势 Atom 馈送。以下是从 Google 热门趋势 Atom 馈送读取内容的代码:

function readAtomFeedContents(){
  // SheetData refers left most sheet.
  var SheetData = SpreadsheetApp.getActiveSpreadsheet()
                  .getSheets()[0];

  // Set column titles.
  var title, description, output = [["Trends", "Related Searches"]];

  // Fetch data from the feed url.
  var xml = UrlFetchApp.fetch("http://www.google.com/trends/ hottrends/atom/feed").getContentText();

  // Parse the result as xml content.
  var doc = XmlService.parse(xml);

  // Get item elements from the root element.
  var items = doc.getRootElement().getChild('channel')
              .getChildren('item');

  // Clear existing sheet data and sets new values.
  SheetData.clearContents();

  // Store new data.
  SheetData.getRange(1, 1, 1,output[0].length).setValues(output);

  /*
   * Dig into 'item' element and parse all the required data.
   * Get other related search terms.
   *
   */
  for(var i=0; i<items.length; i++){
    title = items[i].getChild('title').getText();
    description = items[i].getChild('description').getText();
    output = [title].concat(description.split(','));

    // Sets output data in sheet.
    SheetData.getRange(i+2, 1, 1, output.length)
      .setValues([output]);
  }
};

此应用程序的示例输出如下所示:

创建一个 Atom 馈送阅读器应用程序

使用 UrlFetchApp 类的可选参数

所有上述应用程序从公共 URL 获取结果;这意味着它们不需要凭据。如果网站在发送响应之前需要您的凭据,例如用户名和密码,怎么办?您可以将凭据和其他参数作为 UrlFetchApp 类的可选参数提供,例如 UrlFetchApp.fetch(url, params)params 参数类似于以下内容:

var params = {
  method: "GET",
  headers: headers
}

headers 变量(HTTP/HTTPS 请求头)可以是一个 JavaScript 键/值映射。您可以将登录凭据作为头信息提供:

var headers = {
  // Basic authentication
  Authorization: "Basic "
    + Utilities.base64Encode("username:password");
}

您的用户名和密码将被编码但不会被加密,因此请优先使用 HTTPS 而不是 HTTP。

注意

并非所有网站都支持基本身份验证;它们大多支持 OAuth。UrlFetchApp类的内置 OAuth 服务已弃用,并转移到名为 GAS 库的开源库中。有关如何导入外部库和使用 OAuth2 开源库的更多信息,请参阅第九章更多技巧和窍门以及创建插件:

LanguageApp

LanguageApp类提供了translate方法,用于将任何文本从一种语言翻译成另一种语言。

例如,要将Google apps script for beginners翻译成法语,请使用以下代码:

function translateToFrench(){
  var text = "Google apps script for beginners";
  var sourceLanguage = "en";
  var targetLanguage = "fr";
  var french = LanguageApp
    .translate(text, sourceLanguage, targetLanguage);

  Logger.log(french);
}

记录的输出将是:Google Apps Script pour les débutants

注意

Google 翻译服务支持许多语言。有关支持的语言的更多信息,请访问:

cloud.google.com/translate/v2/using_rest#language-params

创建语言翻译应用程序

此应用程序将文本从一种语言翻译成另一种语言。我们将使用一个文档作为源/源文档,另一个文档作为目标/目标文档。我们将开发此应用程序作为插件。尽管内置的翻译服务可用(在文档应用程序中导航到工具 | 翻译文档…),但我们提供了语言翻译应用程序来探索 GAS 的功能。

在源文档(文档)的Code.gs文件中输入以下代码:

/*
 * Replace with the id/key of the target document in which the
 * translated text to be saved.
 *
 */
var targetDocumentId = "Replace with target document id";

之前的代码将目标文档的 ID 设置为targetDocumentId全局变量。目标文档是你打算将翻译文本转移到的文档。将双引号内的文本替换为目标文档的 ID 字符串:

/**
 * Creates a menu entry in the Google Docs UI when the document
 * is opened.
 *
 */
function onOpen(e) {
  DocumentApp.getUi().createAddonMenu()
      .addItem('Start', 'showSidebar')
      .addToUi();
}

之前的onOpen函数创建了一个插件菜单,其中有一个名为开始的菜单项。它与showSidebar函数相关联:

/**
 * Opens a sidebar in the document containing the add-on's user
 * interface.
 *
 */
function showSidebar() {
  var ui = HtmlService.createTemplateFromFile('Sidebar')
           .evaluate()
           .setTitle('Translate');

  DocumentApp.getUi().showSidebar(ui);
}

之前的showSidebar函数创建了带有所需控制元素的侧边栏。

/**
 * Gets the stored user preferences for the destination language,
 * if exist.
 *
 */
function getPreferences() {
  var userProperties = PropertiesService.getUserProperties();

  var languagePrefs = {
    destLang: userProperties.getProperty('destLang')
  };

  return languagePrefs;
};

getPreferences函数获取并返回用户的语言偏好。此处显示的runTranslation函数将文本从源语言翻译成目标语言。语言用它们的两位字母简称表示。例如,英语是en,德语是de,默认为自动,这意味着 Google 翻译服务将自动检测源语言。如果savePrefs参数为true,则用户语言偏好将被保存:

function runTranslation(dest, savePrefs) {
  if (savePrefs == true) {
    var userProperties = PropertiesService.getUserProperties();
    userProperties.setProperty('originLang', 'en');
    userProperties.setProperty('destLang', dest);
  }

  var srcFile = DocumentApp.getActiveDocument();
  var tgtFile = DocumentApp.openById(targetDocumentId);

  var srcBody = srcFile.getBody();
  var tgtBody = tgtFile.getBody();

  tgtBody.appendParagraph("");
  tgtBody.clear();

  var item = srcBody.getChild(0);

  while(item){
    var type = item.getType();

    if(type == "LIST_ITEM"){
      var attrib = item.getAttributes();  
var srcText = item.getText(); 
var transText = LanguageApp.translate(srcText, "en", dest); 
tgtBody.appendParagraph(transText).setAttributes(attrib);

    item = item.getNextSibling();
  };

  tgtBody.getChild(0).removeFromParent();
};

下面的include辅助函数将来自其他文件的外部 JS/CSS 内容(文件名作为参数给出)放入 HTML 文件中:

function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename)
    .getContent();
}

文件菜单创建一个新的 HTML 文件(Sidebar.html)并输入以下代码:

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script src="img/webcomponents.js"></script>

    <link rel="import" href="//polymerstaticfiles.appspot.com/bower_components /polymer/polymer.html">

    <link rel="import" href="//polymerstaticfiles.appspot.com/ bower_components/font-roboto/roboto.html">

    <link rel="import" href="//polymerstaticfiles.appspot.com/ bower_components/paper-input/paper-input.html">

    <link rel="import" href="//polymerstaticfiles.appspot.com/ bower_components/paper-button/paper-button.html">

    <link rel="import" href="//polymerstaticfiles.appspot.com/ bower_components/paper-checkbox/paper-checkbox.html">

    <link rel="import" href="//polymerstaticfiles.appspot.com/ bower_components/paper-radio-group/paper-radio-group.html">

    <link rel="import" href="//polymerstaticfiles.appspot.com/ bower_components/paper-radio-button/paper-radio-button.html">

    <link rel="import" href="//polymerstaticfiles.appspot.com/ bower_components/paper-input/paper-input-decorator.html">

    <!-- Insert CSS code -->
    <?!= include('Sidebar.css.html'); ?>
  </head>

  <body>
    <div class="sidebar">
      <h4>Translate into</h4>
      <paper-radio-group id="dest">
        <paper-radio-button name="en" id="radio-dest-en" label="English"></paper-radio-button>

        <paper-radio-button name="fr" id="radio-dest-fr" label="French"></paper-radio-button>

        <paper-radio-button name="de" id="radio-dest-de" label="German"></paper-radio-button>

        <paper-radio-button name="ja" id="radio-dest-ja" label="Japanese"></paper-radio-button>

        <paper-radio-button name="es" id="radio-dest-es" label="Spanish"></paper-radio-button>
      </paper-radio-group>

      <br /><br />
      <hr />

      <paper-checkbox id="save-prefs" label="Use this language by default"></paper-checkbox>

      <div id="button-bar">
        <paper-button raised class="colored" id="run- translation">Translate</paper-button>
      </div>
    </div>

    <!--  Insert JS code -->
    <?!= include('Sidebar.js.html'); ?>
  </body>
</html>

在前面的代码中,使用了 Google 的聚合物组件库。尽管我们可以使用简单的 HTML 元素,但我们使用了聚合物组件来获得侧边栏的美观外观和感觉。您无需担心该库的功能,只需按照所示包含 URL 即可。include 辅助函数插入作为参数给出的相应文件的内容。

创建另一个 HTML 文件,并将其命名为 Sidebar.css,包括 .css 扩展名。脚本编辑器将添加 .html 扩展名,因此文件名将是 Sidebar.css.html。在文件中输入以下代码:

<style>
  body {
    font-family: 'RobotoDraft', sans-serif;
    margin: 0;
    padding: 0;
  }

  h4 {
    text-align: center;
    margin: 0;
  }

  paper-button {
    margin: 0;
    margin-top: 10px;
  }

  .sidebar {
    -moz-box-sizing: border-box;
    box-sizing: border-box;
    overflow-y: auto;
    padding: 12px;
    position: absolute;
    width: 100%;
  }

  #dest {
    margin-top: 5px;
  }

  .error {
    color: #dd4b39;
    font-size: small;
    margin-top: 8px;
  }

  .colored {
    background: #4285f4;
    color: #ffffff;
  }
</style>

前面的 CSS 代码定义了 Sidebar.html 文件中 HTML 元素的样式。CSS 在相关的大括号内定义元素样式。

例如:

  • body 样式名称定义了主体元素的样式

  • h4 定义了第四级标题元素的样式

  • 带有点(.)前缀的样式名称定义了属于该类的元素的样式

  • 带有井号符号(#)前缀的名称定义了与样式名称相同的元素的样式

小贴士

要进一步了解 CSS,请参阅 developer.mozilla.org/en-US/Learn/CSS

创建另一个 HTML 文件,并将其命名为 Sidebar.js。在新建的 Sidebar.js.html 文件中输入以下代码:

<script src="img/jquery.min.js"></script>

<script>
  /**
   * On document load, assign click handlers to each button and
   * try to load the user's origin and destination language
   * preferences, if previously set.
   *
   */
  $(function() {
    $('#run-translation').click(runTranslation);

    google.script.run
      .withSuccessHandler(loadPreferences)
      .withFailureHandler(showError).getPreferences();
  });

  /**
   * Callback function that populates the origin and destination
   * selection boxes with user preferences from the server.
   *
   */
  function loadPreferences(languagePrefs) {
    if (languagePrefs.destLang){ 
      $('#dest').prop('selected', languagePrefs.destLang);
    }
  }

  /**
   * Runs a server-side function to translate the text.
   *
   */
  function runTranslation() {
    this.disabled = true;
    $('#error').remove();

    var dest = $('#dest').prop('selected');
    var savePrefs = $('#save-prefs').prop('checked');

    google.script.run
      .withSuccessHandler(
        function(msg, element) {
          element.disabled = false;
        })
      .withFailureHandler(
        function(msg, element) {
          showError(msg, $('#button-bar'));
          element.disabled = false;
        })
      .withUserObject(this)
      .runTranslation(dest, savePrefs);
  }

  /**
   * Inserts a div that contains an error message after a given
   * element.
   *
   */
  function showError(msg, element) {
    var div = $('<div id="error" class="error">' + msg + '</div>');

    $(element).after(div);
  }
</script>

在没有错误地输入所有代码后,重新加载文档。在 扩展程序 菜单下将出现一个新的条目 翻译。点击 开始 打开侧边栏,其中将包含您运行应用程序所需的所有控件。要测试应用程序,请在源文档(其中您已输入代码)中输入一些文本。选择您想要将文本翻译成的语言,然后点击 翻译 按钮。

翻译后的文本将放置在目标文档中,其 ID 在代码中已输入。打开该文档以查看翻译后的文本。

下面的截图显示了侧边栏:

创建语言翻译应用程序

下面的截图显示了翻译前的英文源文档文本:

创建语言翻译应用程序

现在如果您打开目标文档,您可以在其中查看保存的翻译文本。前面截图中的示例文本已翻译成日语,如下所示:

创建语言翻译应用程序

创建一个文档审查和即时内联评论应用程序

我们将创建一个文档审查和评论应用程序。文档可以是文章投稿、学生提交的理解测试、研究人员提交的论文,或者任何在接收前需要审查的内容。审查者可以审查并插入预定义的评论或他/她的自定义评论,即时完成。审查者选择文档中的某些文本,然后点击任何一个按钮(预定义评论)进行高亮。然后,脚本将高亮显示文本,并在实际评论旁边内联插入评论文本。您可以在评论面板(右上角)看到创建的评论。最后,审查者可以在文档末尾插入评分。

如往常一样,在代码文件中创建onOpen触发函数,它将打开侧边栏:

function onOpen() {
  var ui = HtmlService.createHtmlOutputFromFile('Sidebar')
    .setTitle('Review and Comment');

  DocumentApp.getUi().showSidebar(ui);
}

insertComment函数将实际评论插入到文档中。由于 Drive 是一个高级服务,所以在使用它之前,您应该启用 Drive API。我希望您还记得如何启用高级服务。如果您不记得,请从第五章,创建 Google 日历和 Drive 应用程序中复习:

function insertComment(comment, selectedText){
  // You should enable this advanced service (Drive API).
  Drive.Comments.insert(
    {
      "content": comment,
      "context": {"type":"text/html", "value":selectedText},
    },
    DocumentApp.getActiveDocument().getId()
  );
}

insertText函数使用所选文本或光标所在位置的文本作为参数:

/**
 * Replaces the text of the current selection with the provided
 * text, or inserts text at the current cursor location.
 * (There will always be either a selection or a cursor.)
 * If multiple elements are selected, only inserts the text in the
 * first element that can contain text.
 *
 * @param {string} newText The text with which to replace the
 *                 current selection.
 *
 */
function insertText(newText) {
  var selection = DocumentApp.getActiveDocument().getSelection();

  // If any text selected then get selected text else cursor.
  if (selection) {
    var elements = selection.getRangeElements();
    for (var i = 0; i < elements.length; i++) {
      var startIndex = elements[i].getStartOffset();
      var endIndex = elements[i].getEndOffsetInclusive();

      // If picture/image element selected.
      if(startIndex == endIndex) throw "Error: Select text only.";

      // Highlight the selected text.
      var element = elements[i].getElement()
         .setBackgroundColor(startIndex, endIndex, '#f6d2ab');

      // Insert selected comment next to the selected text.
      element.insertText(endIndex+1, '[' + newText +']')
         .setBackgroundColor(
            endIndex+1, endIndex+newText.length+2, '#bbffbb'
          );

      var text = element.getText()
         .substring(
            startIndex,endIndex+1
          );

      // Call insertComment function      
      insertComment(newText,text);
    }

  } else {

    var curr = DocumentApp.getActiveDocument().getCursor();

    // Exit if document not active or cursor not in document.
    if(!curr) return;

    // Insert comment and call insertComment function.
    curr.insertText('[' + newText +']')
      .setBackgroundColor('#bbffbb');

    insertComment(newText);
  }
}

insertScore函数在文档末尾插入一条水平线,并根据文档是否符合预期插入评分:

function insertScore(newText){
  var doc = DocumentApp.getActiveDocument();
  var body = doc.getBody();

  body.appendHorizontalRule();
  body.appendParagraph(newText)
    .setAttributes({FONT_SIZE:24,FOREGROUND_COLOR:'#6aa84f'});
}

Sidebar.html文件中创建以下代码:

<!-- Sidebar.html -->
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">

    <!-- Google's add-on stylesheet -->
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css" />

    <!-- jQuery UI stylesheet -->
    <link rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/jqueryui/ 1.10.4/themes/smoothness/jquery-ui.css" />

    <!-- jQuery base library -->
    <script src="img/jquery.min.js"></script>

    <!-- jQuery UI library -->
    <script src="img/jquery-ui.min.js"></script>

    <!-- Add additional styles -->
    <style>
      select{ height:35px; }

      textarea{
        width:100%;
        margin-top: 3px;
        margin-bottom: 3px;
      }

      .blue{
        -moz-border-radius: 3px;
        -webkit-border-radius: 3px;
        border-radius: 3px;
      }

      .blue + .blue{
        margin: .5px -.5px;
      }

      .ui-accordion .ui-accordion-header {
        display: block;
        cursor: pointer;
        position: relative;
        margin-top: 1px;
        padding: .4em .25em .4em .25em;
        min-height: 0; /* support: IE7 */
      }

      .ui-accordion .ui-accordion-icons {
        padding-left: 2em;
      }

      .ui-accordion .ui-accordion-content {
        padding: .5em .5em;
        overflow: auto;
      }
    </style>
  </head>

accordion块的结构在代码注释中提供:

  <body>
    <!-- To comply with the jQuery UI library,
        The accordion should be in the form:
      <div id="accordion">
        <h3>Section 1</h3>
        <div>
          ...
        </div>

        <h3>Section 2</h3>
        <div>
          ...
        </div>
      </div>
    -->

    <div id="accordion">

      <h3>Comments</h3>
      <div>
        <b>Highlight text and click the appropriate comment</b>
        <div id="button-bar">
          <button class="blue comment-button"
            value = "Awkward">Awkward</button>

          <button class="blue comment-button"
            value = "Citation Needed">Citation Needed</button>

          <button class="blue comment-button"
            value="Improper Citation">Improper Citation</button>

          <button class="blue comment-button"
            value="Commonly Confused">Commonly Confused</button>

          <button class="blue comment-button"
            value="Delete">Delete</button>

          <button class="blue comment-button"
            value="Run-on">Run-on</button>

          <button class="blue comment-button"
            value="Vague">Vague</button>
        </div>

        <div>
          <textarea rows="3" id="insert-text"
            placeholder="Type your comment here"></textarea>
        </div>

        <div>
          <button class="blue" id="insert-button">Comment</button>
        </div>
      </div>

      <h3>Scores</h3>
      <div>
        <div id="score-bar">
          Does the document meet the expectation?
          <br />
          <button class="green insert-score" value="Meets"
            >Yes</button>

          <button class="green insert-score" value="Not Yet"
            >No</button>
        </div>
      </div>
    </div>

    <script>
      // On document load assign the events.
      $(function(){

        /**
         * Which accordion block should be active/expanded by
         * default, here the first one.
         *
         */
        $("#accordion").accordion({ active: 0 });

        // Assign a click event to buttons.
        $(".comment-button").click(insertButtonComment);
        $("#insert-button").click(insertCustomComment);
        $(".insert-score").click(insertScore);

      });

      /**
       *  Runs a server-side function to insert pre-defined
       *  comment into the document at the cursor *  or after the selection.
       *
       */
      function insertButtonComment() {
        this.disabled = true;
        $('#error').remove();

        google.script.run
          .withSuccessHandler(
            function(returnSuccess, element) {
              element.disabled = false;
            }
           )
          .withFailureHandler(
             function(msg, element) {
               showError(msg, $('#button-bar'));
               element.disabled = false;
             }
           )
          .withUserObject(this)
          .insertText($(this).val());
      }

      /**
       *  Runs a server-side function to insert custom comment
       *  into the document on pointing the cursor or 
       *  after the selection.
       *
       */
      function insertCustomComment() {
        this.disabled = true;
        $('#error').remove();

        google.script.run
          .withSuccessHandler(
             function(returnSuccess, element) {
               element.disabled = false;
             }
           )
          .withFailureHandler(
             function(msg, element) {
               showError(msg, $('#button-bar'));
               element.disabled = false;
             }
           )
          .withUserObject(this)
          .insertText($('#insert-text').val());
      }

      /**
       *  Runs a server-side function to insert the score
       *
       */
      function insertScore() {
      this.disabled = true;
      $('#error').remove();

      google.script.run
        .withSuccessHandler(
           function(returnSuccess, element) {
             element.disabled = false;
           }
         )
        .withFailureHandler(
           function(msg, element) {
             showError(msg, $('#score-bar'));
             element.disabled = false;
           }
         )
        .withUserObject(this)
        .insertScore($(this).val());
      }

      /**
       *  Inserts a div that contains an error message after a
       *  given element.
       *
       *  @param msg-The error message to display.
       *  @param element-The element after which to display the
       *                 error.
       *
       */
      function showError(msg, element) {
        var div = $('<div id="error" class="error">'
            + msg + '</div>');

        $(element).after(div);
      }
    </script>
  </body>
</html>

以下截图显示了侧边栏和文档:

创建文档审查和即时内联评论应用程序

您可以通过点击文档窗口右上角角落的评论按钮来查看插入的评论。

摘要

在本章中,您学习了并创建了多个有用的应用程序,包括 RSS/Atom 阅读器、语言翻译应用程序以及文档审查和评论应用程序。在下一章中,您将学习如何创建交互式网页、RSS 源、文件上传和考勤应用程序。

第七章. 创建交互式网页

在上一章中,你学习了如何创建 RSS/Atom 源阅读器、股票报价器、语言翻译器,以及创建文档审查和评论应用程序。

在本章中,你将学习:

  • 要创建返回电子表格数据作为 HTML、JSON 和 PDF 的网络应用程序

  • 要发送带有查询字符串的 HTTP/HTTPS 请求

  • 要创建一个 RSS 源

  • 要创建一个文件上传应用程序

  • 要创建一个时间表应用程序

创建一个将电子表格数据渲染为 HTML 的网络应用程序

我们将创建一个应用程序,在浏览器中返回电子表格数据作为 HTML。创建一个电子表格,将其重命名为 Data,并填充一些测试数据,如下一个截图所示。你可以使用具有名为 First NameLast NameFull Name 的三列的任何随机数据填充电子表格:

创建一个将电子表格数据渲染为 HTML 的网络应用程序

Code.gs 文件中创建如下所示的 doGet 函数:

function doGet() {
  /*
   *  This spreadsheet may not be active while this function
   *  executes, so you cannot get access to active spreadsheet,
   *  use open by id.
   *
   */
  var ss = SpreadsheetApp
      .openById("Replace with this spreadsheet id");

  var SheetData = ss.getSheetByName("Data");

  var data = SheetData.getDataRange().getValues();

  var html = '<!DOCTYPE html><html><body><table border=1>';

  // Each row data passed as argument to the anonymous function.
  data.forEach(function(row){
    html += '<tr>';
    html += '<td>' + row[0] + '</td>';
    html += '<td>' + row[1] + '</td>';
    html += '<td>' + row[2] + '</td>';
    html += '</tr>';
  });

  // Let's close table, body and html tags.
  html += '</table></body></html>';

  // Return as HTML document.
  Return HtmlService.createHtmlOutput(html);

}

HtmlService 函数可以用来创建任何 HTML 内容。前面的 doGet 函数将 HtmlService 创建的 HTML 内容返回给浏览器。按照前面解释的方法发布脚本,并在浏览器地址栏中输入 URL。你可以看到如下所示的截图中的结果。显示的数据可能因你的输入数据而异。

创建一个将电子表格数据渲染为 HTML 的网络应用程序

创建一个返回 JSON 的网络应用程序

现在,我们将看到如何返回 JSON 字符串而不是 HTML 内容。在 Data 电子表格中添加另一个名为 DOB 的列,如下所示:

创建一个返回 JSON 的网络应用程序

创建如下所示的 doGet 函数:

function doGet(){
  /*
   *  This spreadsheet may not be active while this function 
   *  executes, so you cannot get access to active spreadsheet, 
   *  use open by id.
   *
   */
  var ss = SpreadsheetApp
      .openById("Replace with this spreadsheet id");

  var SheetData = ss.getSheetByName("Data");

  var data = SheetData.getDataRange().getValues();

  // Remove header
  data.shift();

  var date = new Date();
  var currYear = date.getFullYear();

  var output = {};

  data.forEach(function(row){
    var dob = new Date(row[3]);
    var dobYear = dob.getFullYear();

    /*
     * Create full name property within output object.
     * Again the full name property is an object.
     *
     */
    output[row[2]] = {};

    /*
     * Assign DOB property to full name object.
     * Change time zone and date format as per your preference.
     *
     */
    output[row[2]].dob = Utilities
      .formatDate(row[3], "UTC", "MM/dd/yyyy");

    // Let's calculate age.
    output[row[2]].age = currYear - dobYear;
  });

  // We can return only string to browser, so convert to string.
  var json = JSON.stringify(output);

  return ContentService.createTextOutput(json);
}

浏览器中的输出将是以下 JSON 字符串:

创建一个返回 JSON 的网络应用程序

将电子表格数据转换为 PDF 文件

你可以创建一个应用程序,将电子表格数据转换为 PDF 文件并存储在 Drive 中,并将 PDF 文件的 URL 返回给用户:

Code.gs 文件中创建如下所示的 doGet 函数:

function doGet() {
  /*
   *  This spreadsheet may not be active while this function 
   *  executes, so you cannot get access to active spreadsheet, 
   *  use open by id.
   *
   */
  var ss = SpreadsheetApp.openById("[[ this spreadsheet id ]]");

  var SheetData = ss.getSheetByName("Data");

  var template = HtmlService
      .createTemplateFromFile("Template.html");

  // Assign 'data' to the template object
  template.data = SheetData.getDataRange().getValues();

  // Evaluate template object as html content
  var html = template.evaluate();

  // Convert html content to pdf
  // var pdf = html.getAs("application/pdf")
  //    .setName("Test_Data.pdf");

  // Or use this code
  var pdf = html.getAs(MimeType.PDF).setName("Test_Data.pdf");

  // Create pdf file in the "My Drive" folder and share it with //public.
  var file = DriveApp.createFile(pdf);

  // Let's set sharing access as anyone can view the pdf.
  file.setSharing(
    DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW
  );

  // Create and return html content with link to the pdf file.
  return HtmlService.createHtmlOutput(
    'Click <a target="_top" href="'
    + file.getUrl()
    +'">here</a> to view pdf file.'
  );
}

创建一个新的 HTML 文件,Template.html,并输入以下 HTML 代码。在此代码中,data 数组是一个二维数组,已经分配给 doGet 函数中的 template 对象:

<!DOCTYPE html>
<html>
  <body>
    <table>
      <? for(var i in data) {?>
        <tr>
          <? for(var j in data[i]) { ?>
            <td><?= data[i][j] ?></td>
          <? } ?>
        </tr>
      <? } ?>
    </table>
  </body>
</html>

在提到的代码中,模板标记 <??> 包围脚本代码,这与正常 HTML 代码中的 script 标签相同。包围的代码会执行,但不返回任何内容。标记 <?=?> 返回包围代码的结果。例如,<?= data[i][j] ?> 返回二维 data 数组的第 i 行第 j 列的值。

为了理解前一个代码中没有模板标记的服务器脚本,这里将其重新呈现:

for(var i in data) {
  for(var j in data[i]) {
    data[i][j] 
  }
}

发布并将在浏览器地址栏中输入发布的 URL。结果将如下所示。点击超链接在 Drive 中打开 PDF 文件:

将电子表格数据转换为 PDF 文件

根据表格数据创建的 PDF 的示例输出如下所示截图。输出可能因您的输入数据而异:

将表格数据转换为 PDF 文件

带有查询字符串的 HTTP/HTTPS 请求发送

您可以发送一个带有查询字符串的 HTTP/HTTPS 请求。为此,将发布的 URL 与您的查询字符串附加在一起。

例如:https://script.google.com/macros/s/AKfycbxa4ErKHiX_0gQ0JUU-Q1qMhvRrOsrx3HXuVZp7pzX8UVxMu4w/exec?fname=John

function doGet(e){
  Logger.log(e);
}

已记录的 HTTP/HTTPS 请求的event对象的示例如下所示:

带有查询字符串的 HTTP/HTTPS 请求发送

以下代码片段中列出的doGet函数显示了您如何使用event对象获取进一步处理所需的参数:

function doGet(e){

  // Get the fname value from the query string.
  var firstName = e.parameter.fname;

  /*
   *  There is no active spreadsheet, so you should open by id.
   *  Use the id of the spreadsheet in which your script resides.
   *
   */
  var ss = SpreadsheetApp.openById("Replace spreadsheet id");

  var SheetData = ss.getSheetByName("Data");

  var data = SheetData.getDataRange().getValues();

  // Remove header
  data.shift();

  var date = new Date();

  // Let's get the year in 4 digits.
  var currYear = date.getFullYear();

  var output = {};

  // Let's populate output with dob and age properties.
  data.forEach(function(row){

    // Skip if first name not match.
    if(firstName !== row[0]) return;

    var dob = new Date(row[3]);
    var dobYear = dob.getFullYear();

    output[row[2]] = {};
    output[row[2]].dob = Utilities
      .formatDate(row[3], "UTC", "MM/dd/yyyy");

    output[row[2]].age = currYear - dobYear;
  });

  var json = JSON.stringify(output);

  return ContentService.createTextOutput(json);
}

所提到的doGet函数从查询字符串中获取fname参数,并返回与fname匹配的age值和dob

带有查询字符串的 HTTP/HTTPS 请求发送

使用 ContentService 创建 RSS 源

您在第六章创建订阅阅读器和翻译器应用程序中创建了一个 RSS 阅读器应用程序。现在,您可以创建一个发布 RSS 源的应用程序。将 RSS 数据放入如下所示的表格中:

使用 ContentService 创建 RSS 源

还可以编辑/输入以下doGet函数:

function doGet() {
  /*
   *  There is no active spreadsheet, so you should open by id.
   *  Use the id of the spreadsheet in which your script resides.
   *
   */
  var ss = SpreadsheetApp.openById([[ this spreadsheet id ]]);

  var SheetRss = ss.getSheetByName("RSS Data");

  var rssData = SheetRss.getDataRange().getValues();

  // Remove header.
  rssData.shift();

  var strRss = '<?xml version="1.0" encoding="UTF-8"?>';

  // Root element.
  strRss += '<rss>';

  // Open channel element.
  strRss += '<channel>';

  // Add description and language elements.
  strRss += '<description>A brief description of the channel</description>';
  strRss += '<language>en-US</language>';

  // Each row data is passed as an argument to the anonymous
  //function.
  rssData.forEach(function(row){
    strRss += '<item>';
    strRss += '<title>' + row[0] + '</title>';
    strRss += '<link>' + row[1] + '</link>';
    strRss += '<creator>' + row[2] + '</creator>';
    strRss += '</item>';
  });

  // Close channel and root (rss) elements.
  strRss += '</channel></rss>';

  // Return as RSS xml document.
  return ContentService
    .createTextOutput(strRss)
    .setMimeType(ContentService.MimeType.RSS);
}

按照之前的方式发布脚本。您可以将发布的 URL 用作上一章中构建的 RSS 阅读器应用程序的 RSS URL。

创建文件上传应用程序

您可以创建一个应用程序,从浏览器上传任何文件到 Drive。在Code.gs文件中创建doGetuploadFiles函数,如下所示:

Code.gs文件中添加以下代码:

function doGet() {
  // Let's return html page created from the Form.html file.
  return HtmlService.createHtmlOutputFromFile('Form.html')
    .setTitle("File Upload");
};

function uploadFiles(form) {
  // You can change the folder name as you like.
  var folderName = "Uploaded Files";

  var folder, folders = DriveApp.getFoldersByName(folderName);

  // folders is an iterator.
  if (folders.hasNext()) folder = folders.next();
  // Let's create a folder if it does not exist.
  else folder = DriveApp.createFolder(folderName);

  // Let's create the file, got from the form, within the folder.
  var file = folder.createFile(form.file);

  // Let's return the file's url
  return file.getUrl();
}

uploadFiles函数查找名为Uploaded Files的现有文件夹。如果没有找到,则在根目录My Drive文件夹中创建相同的文件夹。随后,它创建传递给参数的文件,并返回创建的文件的 URL。

更新Form.html文件中的代码:

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">

    <link rel="stylesheet" href="//ssl.gstatic.com/docs/script/css/add-ons1.css"/>
    <script src="img/jquery.min.js"></script>
  </head>

  <body>
    <div class="sidebar">
      <form>
        <input type="file" name="file">
        <br /><br />
        <input type="button" id="upload" class="submit" value="Upload">
      </form>
    </div>

    <script>
      $(function(){
        $("#upload").click(fileUpload);
      });

      function fileUpload(){
        this.disabled = true;
        google.script.run
          .withSuccessHandler(function(msg, element){
             element.disabled = false;
             showSucces(msg);
           })
          .withFailureHandler(function(msg, element) {
             element.disabled = false;
             showError(msg, element);
           })
          .withUserObject(this)
          .uploadFiles(this.parentNode);
      }

      function showSucces(msg) {
        alert("File uploaded successfully.\n The file url is: " + msg);
      }

      function showError(msg, element) {
        var div = $('<div id="error" class="error">' + msg + '</div>');

        $(element).after(div);
      }

    </script>
  </body>
</html>

上述代码渲染了上传表单控件,如果点击上传,则调用uploadFiles服务器函数。

文件上传表单的控件示例如下所示:

创建文件上传应用程序

点击浏览…按钮选择任何本地存储的文件。然后,点击上传按钮将文件上传到 Drive。所选文件将被上传到My Drive文件夹中的Uploaded Files文件夹。

上传成功后,将显示一个包含上传文件 URL 的警告框,如下所示截图。您可以使用此 URL 验证文件上传是否成功。

创建文件上传应用程序

创建员工考勤应用程序

通过创建前面的应用程序所积累的知识和经验,您可以创建这个完整的考勤应用。此应用可用于组织或公司记录员工在一天或班次中的工作时间。每日出勤数据将被保存在 Backup 工作表中以供将来参考。

创建一个新的工作表,命名为 EmployeesList,并填充员工姓名。所有这些姓名将自动在用户界面中以下拉列表的形式列出。

创建员工考勤应用

创建另一个名为 TimeSheet 的工作表,并按照以下截图所示排列列标题。确保列 CDEF 格式化为 date,否则日期可能显示为纪元数字。将列 A 留空,因为它将由脚本用于标记班次状态,例如 sb(班次开始)、bb(休息开始)、be(休息结束)、se(班次结束)。

创建员工考勤应用

创建另一个名为 Backup 的工作表,用于从 TimeSheet 工作表备份每天的班次数据。按照以下所示排列列。请记住将列 BCDE 格式化为 date

创建员工考勤应用

创建另一个新的工作表,命名为 Message,用于传递给员工的消息(如果有):

创建员工考勤应用

Code.gs 文件中,创建全局变量以及 doGetgetEmpNames 函数。将 [[ this spreadsheet id ]] 替换为实际的工作表 ID/键(作为字符串):

var ssid = "[[ this spreadsheet id ]]";

// Change date format as per your preference.
var DF = "MM/dd/yyyy HH:mm:ss";
var TZ = Session.getScriptTimeZone();

var ss = SpreadsheetApp.openById(ssid);
var TimeSheet = ss.getSheetByName("TimeSheet");
var EmpSheet = ss.getSheetByName("EmployeesList");
var BackupSheet = ss.getSheetByName("Backup");
var MessageSheet = ss.getSheetByName("Message");

getEmpList 函数创建并返回员工姓名数组:

/**
 *  Get employee names from the EmployeesList sheet,
 *  construct the data as an array and return.
 *
 */
function getEmpList(){
  var emp = [];
  var data = EmpSheet.getDataRange().getValues();

  for(var i in data) if(data[i][0]) emp.push(data[i][0]);

  return emp;
}

doGet 函数中,消息和员工列表被分配给 template 对象,并返回评估后的 HTML 内容:

function doGet(){
  var template = HtmlService.createTemplateFromFile("Timesheet");
  template.message = MessageSheet.getRange("A2").getValue();
  template.empList = getEmpList();

  var html = template.evaluate();
  return html;
}

getEmpStatus 函数返回员工班次状态数组:

// Returns employee shift status as an array [status, name].
function getEmpStatus(emp){
  var empData = EmpSheet.getDataRange().getValues();
  var timeData = TimeSheet.getDataRange().getValues();

  // Remove header
  timeData.shift();

  for(var i in timeData){
    if(timeData[i][1] == emp) 
      return [timeData[i][0],empData[j][1]];
  }

  // Return null if employee not in shift
  return ["",""];
}

fmtDate_ 函数是一个辅助函数,它返回格式化的日期字符串:

function fmtDate_(d, format){
  // Set the default date format, if 'format' not passed.
  var fmt = format || DF;

  return Utilities.formatDate(d, TZ, fmt);
}

postTime 函数根据员工姓名和用户点击的按钮填充考勤表,这些值作为参数(nameval)提供。此函数也会抛出错误(如果有)。

注意

关键字 throw 返回一个错误对象并终止执行。

function postTime(name, val){
  var time = fmtDate_(new Date());
  var data = TimeSheet.getDataRange().getValues();

  // If 'shift start' clicked
  if(val == "sb"){
    // Update start time if clicked again.
    for(var i in data){
      if(data[i][1] == name && data[i][0] == "sb" ){
        data[i][2] = time;
        TimeSheet.getRange(1, 1, data.length, data[0].length)
          .setValues(data);
        return [val,name];
      }
    };

    // Else insert new name and update start time.
    TimeSheet.appendRow([val,name,time]);

    return [val,name];
  }

  // If 'break start' clicked.
  if(val == "bb"){
    for(var i in data){
      // Update break start time only if employee is in shift.
      if(data[i][0] == "sb" && data[i][1] == name ){
        data[i][0] = val;
        data[i][3] = time;

        TimeSheet.getRange(1, 1, data.length, data[0].length)
          .setValues(data);

        return [val,name];
      }
    };

    // If 'break start' clicked before 'shift start'.
    throw "Please start your shift.";
  }

  // If 'break end' clicked
  if(val == "be"){
    for(var i in data){
      if(data[i][0] == "bb" && data[i][1] == name ){
        data[i][0] = val;
        data[i][4] = time;
        TimeSheet.getRange(1, 1, data.length, data[0].length)
          .setValues(data);
        return [val,name];
      }
    };

    // If 'break end' clicked before 'break start'.
    throw "Please start your break.";
  }

  // If shift end clicked
  if(val == "se"){
    for(var i in data){
      if(data[i][1] == name 
           && (data[i][0] == "sb"|| data[i][0] == "be") ){
        var backup = [];
        backup.push(
          data[i][1],    // Name
          data[i][2],    // Shift Start
          data[i][3],    // Break Start
          data[i][4],    // Break End
          time,          // Shift end
          '=(E2-B2)*24', // Col F formula,
          '=(D2-C2)*24', // Col G formula
          '=F2-G2'       // Col H formula
        );

        /*
         * Copy Timesheet data to Backup sheet.
         * Insert a new row before row 2,
         * so that the inserted formulas ever work.
         *
         */
        BackupSheet.insertRowBefore(2);

        BackupSheet.getRange(2, 1, 1, backup.length)
          .setValues([backup]);

        /*
         * Tidy timesheet.
         * Ensure at least one data row before deleting,
         *  to avoid error.
         *
         */
        if(i<2) TimeSheet.appendRow(['']);

        // Delete copied row
        TimeSheet.deleteRow(Number(i)+1);

        return [val,name];
      }
    };

    // If 'shift end' clicked before 'break end'.
    if(data[i][0] == "bb")
      throw "Please end your break.";

    // If 'shift end' clicked without starting shift.
    throw "Please start your shift.";
  }
}

前面的 postTime 函数根据用户点击的按钮将数据填充到 TimeSheet 工作表中。如果班次时间有冲突,它还会抛出错误。例如,用户不能在点击 Break End 之前点击 Break Start,也不能在不结束上一个班次的情况下点击 Shift Start,等等。

创建一个新的 HTML 文件,命名为 Timesheet,并在其中输入以下代码:

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css" />
    <script src="img/jquery.min.js"></script>
  </head>

  <body>
    <div>
      <fieldset style="padding-bottom:25px;">
        <legend>Timesheet</legend>
        <select id="employee" name="employee">
          <? for(var i in empList){ ?>
              <option value="<?= empList[i] ?>" > <?= empList[i] ?></option>
          <? } ?>
        </select>
        <br /><br />
        <button id="sb" value="sb"><span>Shift Start</span></button>

        <button id="bb" value="bb"><span>Break Start</span></button>

        <button id="be" value="be"><span>Break End</span></button>
        <button id="se" value="se"><span>Shift End</span></button>
      </fieldset>

      <fieldset>
        <div id="message"><?!= message ?></div>
      </fieldset>
    </div>

    <script>
      $(function() {
        // Disable all buttons.
        $('#sb,#bb,#be,#se').prop("disabled", true);

        // Set drop-down change event.
        $('#employee').change(getStatus);

        // Set buttons click event.
        $('#sb,#bb,#be,#se').click(postTime);

        getStatus();
      });

      function getStatus(){
        // Remove all previous error messages.
        $('#error,#success').remove();

        // Disable all buttons.
        $('#sb,#bb,#be,#se').prop("disabled", true);

        // Get employee shift status.
        google.script.run
          .withSuccessHandler(function(status){
            updateStatus(status);
           })
          .getEmpStatus($("#employee").val());
      }

      function postTime(){
        // Remove all previous error messages.
        $('#error,#success').remove();

        // Disable all buttons.
        $('#sb,#bb,#be,#se').prop("disabled", true);

        // Post shift time to sheet.
        google.script.run
          .withSuccessHandler(function(msg){
             updateStatus(msg[0]);
           })
          .withFailureHandler(function(msg, elm){
             showError(msg, elm);
           })
          .withUserObject(this)
          .postTime($("#employee").val(),$(this).val());
      }

      function updateStatus(status){
        // Enable appropriate buttons only.
        switch(status){
          case "sb": $('#bb,#se').prop("disabled", false); break;
          case "bb": $('#be').prop("disabled", false); break;
          case "be": $('#se').prop("disabled", false); break;
          default: $('#sb').prop("disabled", false);
        }      
      }

      function showError(msg, elm) {
        var span = $('<span id="error" class="error">' + msg + '</span>');
        $(elm).after(span);
      }

    </script>
  </body>
</html>

发布脚本并将发布的 URL 输入到浏览器的地址栏中,然后你将看到截图所示的工时应用被加载。通过从下拉菜单中选择员工姓名以及点击其旁边的按钮进行实验。对于每个用户操作,工时和/或备份表的数据将被更新。

创建员工工时应用

以下是工时数据的示例输出:

创建员工工时应用

当用户点击班次结束时,工时表中的相应数据将被转移到备份表中,并为班次小时数休息时间工作时间列创建公式。这些公式计算日期差异并将其乘以 24,以显示为小时值。以下是备份表的示例输出:

创建员工工时应用

摘要

在本章中,你学习了并创建了许多实用的实际应用,包括 RSS 发布者和一个完整的工时应用。在下一章中,你将创建一个订单处理工作流应用。

第八章. 构建工作流应用程序

在上一章中,您学习了如何使用ContentServiceHtmlServicedoGetdoPost函数创建交互式网页。您还构建了 RSS 源和考勤应用程序。

在本章中,您将学习:

  • 创建工作流应用程序

  • 订单处理系统涉及的工作流程

Google 表格包含了创建工作流应用程序所需的所有数据,涉及各个步骤。它作为订单处理系统的骨架。

小贴士

在处理已发布的 Web 应用程序时,请注意以下脚本代码版本是相互独立的:

  • 已保存的版本

  • 发布版本

  • 最后保存的代码

因此,请记住每次对代码进行更新时都要发布应用程序。

订单处理工作流 - 步骤说明

以下是在订单处理工作流程中涉及的步骤:

  1. 用户打开在线表格,通过提及项目、数量、送达地址和支付方式来发送订单。

  2. Google 表格向用户账户部门发送确认电子邮件。

  3. 账户部分验证支付并将支付信息转发到订单处理部分。

  4. 订单处理部门将订单发送到送达地址并更新发货详情。

  5. 用户确认交付。

您还可以参考以下图像中这些步骤的图示表示:

订单处理工作流 - 步骤说明

配置 Google 表格

以下解释了各种表格、电子邮件及其组成部分:

  • 用户表格:

    • 项目

    • 单价

    • 数量

    • 总价(计算得出)

    • 送达地址

    • 电话

    • 电子邮件

    • 支付详情

    备注

    在订单提交后,脚本向用户账户部门发送确认电子邮件。

  • 发送给用户的确认电子邮件:

    • 订单号

    • 项目

    • 单价

    • 数量

    • 总价

    • 送达地址

    • 电话号码

    • 支付详情

  • 发送给账户部门的电子邮件:

    它与用户确认电子邮件相同;然而,还包括一个到发货表格的链接。

    备注

    在收到订单电子邮件后,账户部分会验证支付详情是否正确,然后将该电子邮件转发到订单处理部分。订单处理/发货部分会点击链接打开发货表格,填写发货详情,并提交表格。

  • 发货表格:

    • 订单号

    • 项目

    • 数量

    • 送达地址

    • 发货详情

    备注

    在提交发货表格时,脚本会在表格中更新发货详情,并向用户发送发货通知电子邮件。

  • 发货后发送给用户的电子邮件:

    • 订单号

    • 送达地址

    • 发货详情

    • 确认交付(链接)

    备注

    用户点击确认链接,然后脚本会更新表格中对应订单号行的交付日期。

现在,创建一个新的 Google 表格,包含两个名为订单库存的工作表/标签。按以下截图所示格式化订单表头:

配置 Google 表格

格式化库存表列并按如下所示填充测试数据:

配置 Google 表格

项目和相应的单价将在订单表中填充。

创建订单表单

Code.gs文件中,定义以下全局变量:

// Replace with your spreadsheet's ID.
var ss = SpreadsheetApp
    .openById("spreadsheet's id");

var SheetOrders = ss.getSheetByName("Orders");
var SheetStock = ss.getSheetByName("Stock");

创建doGet函数:

function doGet(){
  var template = HtmlService.createTemplateFromFile("Order");
  var html = template.evaluate();
  return HtmlService.createHtmlOutput(html);
}

前面的函数从Order.html模板返回订单表单。创建一个名为Order的新 HTML 文件,并在其中输入以下代码:

<!-- Order.html -->
<!DOCTYPE html>
<html>

  <head>
    <base target="_top">
  </head>

  <body>
    <form>
      <table>
        <tr>
        <td><label>Select Item:</label></td>
        <td>
          <select>
            <option value="Item 1">Item 1</option>
            <option value="Item 2">Item 2</option>
            <option value="Item 3">Item 3</option>
            <option value="Item 4">Item 4</option>
            <option value="Item 5">Item 5</option>
          </select>
        </td>
        </tr>

        <tr>
        <td><label>Unit price:</label></td>
        <td><input type="text" /></td>
        </tr>

        <tr>
        <td><label>Quantity:</label></td>
        <td><input type="number" value="1" /></td>
        </tr>

        <tr>
        <td><label>Total price:</label></td>
        <td><input type="text" /></td>
        </tr>

        <tr>
        <td><label>Deliver to:</label></td>
        <td><textarea placeholder="Enter delivery address.">
        </textarea></td>
        </tr>

        <tr>
        <td><label>Phone:</label></td>
        <td><input placeholder="Enter phone number." /></td>
        </tr>

        <tr>
        <td><label>E-Mail:</label></td>
        <td><input placeholder="Enter email address." /></td>
        </tr>

        <tr>
        <td><label>Payment details:</label></td>
        <td><input type="text"
              placeholder="Enter payment details." /></td>
        </tr>
      </table>

      <br />
      <input type="button" value="Submit" />
    </form>
  </body>

</html>

使用以下设置发布脚本:

  • 以以下方式执行应用:

    (你的电子邮件 ID)

  • 哪些人可以访问该应用:

    任何人,即使是匿名用户

点击部署按钮,如果需要则进行授权。渲染的应用程序将类似于以下截图:

创建订单表单

此表单是一个基本的,不是花哨的。项目的选择是硬编码的;只有五个固定项目。用户可能不知道供应商目前有哪些项目,列表中添加了哪些新项目,或者每个项目的当前单价。更重要的是,我们没有为提交按钮添加任何功能。

增强订单表单

为了增强订单表单,更新doGet函数如下:

function doGet(){
  var template = HtmlService.createTemplateFromFile("Order");
  template.pricelist = getPrice();

  var html = template.evaluate();
  return HtmlService.createHtmlOutput(html);
}

价格列表被分配给模板作为二维数组,并由以下函数返回:

function getPrice(){
  var data = SheetStock.getDataRange().getValues();

  // remove header row.
  data.shift();

  return data;
}

Order.html文件中,更新select标签标记,如以下代码片段所示:

<td>
  <select id="item" name="item">
    <? for(var i in pricelist){ ?>
      <option value="<?= pricelist[i][0] ?>" > <?= pricelist[i][0] ?></option>
    <? } ?>
  </select>
</td>

下拉菜单将反映库存表中包含或更新的内容。默认项将是列表中最上面的或第一项。因此,我们必须为该项设置一个默认单价。因此,更新如此处所示的单价输入字段:

<td><input id="unit_price" name="unit_price" type="text" readonly value="<?= pricelist[0][1] ?>" /></td>

用户不会在订单表单中输入单价,因此它应该是只读的。我们将数量的默认值设置为1,将总价的默认值设置为单价。

更新body标签如下:

  <body>
    <form>
      <table>
        <tr>
        <td><label>Select Item:</label></td>
        <td><select id="item" name="item">
          <? for(var i in pricelist){ ?>
              <option value="<?= pricelist[i][0] ?>" ><?= pricelist[i][0] ?></option>
          <? } ?>
        </select></td>
        </tr>

        <tr>
        <td><label>Unit price:</label></td>
        <td><input id="unit_price" name="unit_price" type="text" readonly value="<?= pricelist[0][1] ?>" /></td>
        </tr>

        <tr>
        <td><label>Quantity:</label></td>
        <td><input id="quantity" name="quantity" type="number" value="1" /></td>
        </tr>

        <tr>
        <td><label>Total price:</label></td>
        <td><input id="total_price" name="total_price" type="text" readonly value="<?= pricelist[0][1] ?>" /></td>
        </tr>

        <tr>
        <td><label>Deliver to:</label></td>
        <td><textarea name="delivery_address"
              placeholder="Enter delivery address.">
              </textarea></td>
        </tr>

        <tr>
        <td><label>Phone:</label></td>
        <td><input name="phone" type="phone"
              placeholder="Enter phone number." /></td>
        </tr>

        <tr>
        <td><label>E-Mail:</label></td>
        <td><input name="email" type="email"
              placeholder="Enter email address." /></td>
        </tr>

        <tr>
        <td><label>Payment details:</label></td>
        <td><input name="payment_details" type="text" placeholder="Enter payment details." /></td>
        </tr>
      </table>

      <br />
      <input class="blue" id="btnSubmit" type="button" value="Submit" />
    </form>
  </body>

对于select元素,我们需要一个onchange事件处理器,以便如果用户选择任何项目,相应的单价应从电子表格中检索并显示在单价输入字段中。同时,根据数量和单价计算总价。在head元素中添加脚本处理器以及 CSS 样式表。更新head标签的代码如下:

  <head>
    <base target="_top">

    <link rel="stylesheet" href="//ssl.gstatic.com/docs/script/css/add-ons1.css" />

    <script src="img/jquery.min.js"></script>

    <script>
        // On document load, assigns events to elements.
        $(function(){
        $("#item").change(getUnitPrice);
        $("#quantity").change(calcTotalPrice);
        $("#btnSubmit").click(submit);
      });

      /*
       * Retrieves corresponding unit price for the selected item
       * and calculates the total price.
       *
       */
      function getUnitPrice(){
        google.script.run
        .withSuccessHandler(function(price){
          $("#unit_price").val(price);
          calcTotalPrice();
        })
        .getPrice( $("#item").prop("selectedIndex") );
      };

      function calcTotalPrice(){
        $("#total_price").val( $("#unit_price").val() * $("#quantity").val() );
      };

      function submit(){
        // Remove already displayed messages, if any.
        $("#success,#error").remove();

        this.disabled = true;

        google.script.run
          .withSuccessHandler(function(msg,elm){
             elm.disabled = false;
             showSuccess(msg,elm);
           })
          .withFailureHandler(function(msg, elm){
             elm.disabled = false;
             showError(msg, elm);
           })
          .withUserObject(this)
          .postOrder( this.parentNode );
          // submit button's parent, i.e. form.
      }

      function showSuccess(msg,elm) {
        var span = $('<span id="success"> <font color="green"> ' + msg + '</font></span>');

        $(elm).after(span);
      }

      function showError(msg,elm) {
        var span = $('<span id="error" class="error"> ' + msg + '</span>');

        $(elm).after(span);
      }
    </script>
  </head>

getPrice服务器函数需要识别所选项目索引作为参数,因此我们将更新如下所示:

function getPrice(index){
  var data = SheetStock.getDataRange().getValues();

  // remove header row.
  data.shift();

  return typeof index == "undefined" ? data : data[index][1];
}

现在,这个函数在从doGet函数调用以及从前面的 HTML 客户端代码调用时都起作用。当从doGet函数调用时,它返回完整的价格列表,否则只返回所选项目的单价。

此辅助函数验证电子邮件。如果有效则返回true,否则返回false

function isValidEmail_(email) {
  var regex = /^([\w-\.]+@([\w-]+\.)+[\w-]{2,6})?$/;
  return regex.test(email);
}

接下来,添加一个表单提交处理函数(postOrder)。如果订单已下,则此处理程序应更新电子表格并发送电子邮件确认给用户以及会计部门

function postOrder(form){

  // Validate user email
  if( !isValidEmail_(form.email) )
    throw "please provide a valid email id.";

  /*
   *  Date used as order number, 
   *  which helps to have distinctive number.
   *  However, you may use any other number or string.
   *
   *  Prepend 'new' with '+' to get 'value' (number) of the date.
   *
   */
  var orderNumber = +new Date();

  // Construct form element values in an array.
  var order = [
    orderNumber,
    form.item,
    form.unit_price,
    form.quantity,
    form.total_price,
    form.delivery_address,
    form.phone,
    form.email,
    form.payment_details
  ];

  SheetOrders.appendRow(order);

  var htmlBody = "<p>Order number: " + orderNumber + "</p>";
  htmlBody += "<p>Item: " + form.item + "</p>";
  htmlBody += "<p>Unit price: " + form.unit_price + "</p>";
  htmlBody += "<p>Quantity: " + form.quantity + "</p>";
  htmlBody += "<p>Total price: " + form.total_price + "</p>";
  htmlBody += "<p>Delivery address: " + form.delivery_address
              + "</p>";

  htmlBody += "<p>Phone number: " + form.phone + "</p>";
  htmlBody += "<p>Payment details: " + form.payment_details
              + "</p>";

  htmlBody += "<p>Please quote the order number in your " + "correspondence.</p>";

  // Send an e-mail to the user.
  MailApp.sendEmail({
    to: form.email,
    subject: "Order placed",
    htmlBody: htmlBody
  });

  htmlBody += "<p>&nbsp;</p>";
  htmlBody += '<p>Click <a href="'
              + ScriptApp.getService().getUrl()
              + '?order_number=' + orderNumber
              + '" >here</a> to dispatch the order.</p>';

  /*
   * Send an e-mail to the Accounts department with the same * content as to the user e-mail, additionally a clickable URL * with the order number appended as a query to the published * URL.
   *
   */
  MailApp.sendEmail({
    to: "Accounts department email id",
    subject: "Order - " + orderNumber,
    htmlBody: htmlBody
  });

  // Return confirmation message to user.
  return "Order placed successfully and more details " \+ "has been sent to " + form.email;
};

记得再次发布脚本,使用新版本。现在,表单应如下所示:

增强订单表单

电子表格中提交的样本数据如下:

增强订单表单

用户电子邮件样本内容如下:

增强订单表单

电子邮件内容与会计部门部分的不同之处在于附加了一个分发表单的链接(你可以在样本电子邮件内容截图如下看到)。(我们将在下一部分创建此表单)。

增强订单表单

到目前为止,我们已经设置如下:

  • 用户可以提交Order表单

  • 脚本将提交的数据附加到电子表格中

  • 脚本向用户和会计部门发送确认电子邮件

创建分发表单

如前所述,我们现在将创建分发表单。创建一个名为Dispatch的新 HTML 文件,并在其中输入以下代码:

<!-- Dispatch.html -->
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <link rel="stylesheet" href="//ssl.gstatic.com/docs/script/css/add-ons1.css" />
    <script src="img/jquery.min.js"></script>

    <script>
      // On document load, assign submit function to the submit
      //  button's click event
      $(function(){
        $("#btnSubmit").click(submit);
      });

      function submit(){
        // Remove already displayed messages, if any.
        $("#success,#error").remove();
        this.disabled = true;

        google.script.run
          .withSuccessHandler(function(msg,elem){
             elem.disabled = false;
             showSuccess(msg,elem);
           })
          .withFailureHandler(function(msg, elm){
             elm.disabled = false;
             showError(msg, elm);
           })
          .withUserObject(this)
          .dispatchOrder( this.parentNode );
      }

      function showSuccess(msg,elm) {
        var span = $('<span id="success"> <font color="green"> ' + msg + '</font></span>');
        $(elm).after(span);
      }

      function showError(msg,elm) {
        var span = $('<span id="error" class="error"> ' + msg + '</span>');
        $(elm).after(span);
      }
    </script>
  </head>

  <body>
    <form>
      <table>
        <tr>
        <td><label>Order number:</label></td>
        <td><input name="order_number" type="text" readonly value="<?= order[0] ?>" /></td>
        </tr>

        <tr>
        <td><label>Item:</label></td>
        <td><input type="text" readonly
             value="<?= order[1] ?>" /></td>
        </tr>

        <tr>
        <td><label>Quantity:</label></td>
        <td><input type="number" readonly
             value="<?= order[3] ?>" /></td>
        </tr>

        <tr>
        <td><label>Deliver to:</label></td>
        <td><textarea readonly value="<?= order[5] ?>"> </textarea></td>
        </tr>

        <tr>
        <td><label>Shipment details:</label></td>
        <td><textarea name="shipment_details" placeholder="Enter shipment details." >
              </textarea></td>
        </tr>

        <tr>
        <td><input name="email" type="hidden"
             value="<?= order[7] ?>" /></td>
        </tr>
      </table>

      <br />
      <input class="blue" id="btnSubmit" type="button" value="Submit" />
    </form>
  </body>
</html>

脚本处理程序是Order表单处理程序的子集,并且大多数 HTML 元素都是只读的,除了运输详情。分发表单如下所示:

创建分发表单

在脚本项目或 Web 应用程序中,我们只能使用一个doGet函数,但我们必须使用两个具有相同发布 URL 的表单。我们如何使用相同的发布 URL 打开两个不同的表单?

你记得我们在postOrder处理程序中向发布 URL 附加了订单号。带有订单号作为查询字符串的分发 URL 示例如下:

https://script.google.com/macros/s/AKfycbwRUVS6z5rjrAw8M- au9_ICYzixTVB3msLOCmoF5JCBVFNzY_7k/exec?order_number=1451875765851

前面的 URL 只是带有订单号附加为查询字符串的发布 URL。

我们将更新doGet函数以解析此查询字符串。如果存在订单号,则返回分发表单;否则,返回Order表单:

function doGet(e){
  var orderNumber = e.parameter.order_number;

  if(orderNumber){

    /*
     *  If order number present in query string
     *  then serve dispatch form to order processing unit.
     *
     */
    var template = HtmlService.createTemplateFromFile("Dispatch");
    var data = SheetOrders.getDataRange().getValues();

    for(var i in data){
      if( data[i][0] == orderNumber ){
        template.order = data[i];
        break;
      }
    };

  } else {

    /*
     *  If order number not present in query string
     *  then serve order form to the user.
     *
     */
    var template = HtmlService.createTemplateFromFile("Order");
    template.pricelist = getPrice();

  };

  var html = template.evaluate();
  return HtmlService.createHtmlOutput(html);
}

现在,doGet函数可以处理这两种情况。

分发文章

一旦分发人员将运输详情输入到分发表单并提交,脚本应更新电子表格中的运输详情,并向用户发送通知电子邮件。因此,我们将添加dispatchOrder服务器函数来处理这些任务:

function dispatchOrder(form){
  // Shipment details column number minus 1.
  const SHIPMENT_DETAILS = 9;

  var orderNumber = form.order_number;
  var deliveryAddress = form.delivery_address;
  var userEmail = form.email;
  var shipmentDetails = form.shipment_details;

  var data = SheetOrders.getDataRange().getValues();

  for(var i = 0; i < data.length; i++){
    if(data[i][0] == orderNumber){
      SheetOrders.getRange(i+1, SHIPMENT_DETAILS+1)
        .setValue(shipmentDetails);

      var htmlBody = "<p>Order number: "
          + orderNumber + " has been dispatched to </p>"
          + "<p>" + deliveryAddress + "</p>"
          + "<p>By " + shipmentDetails + "</p>"
          + "<p>&nbsp;</p>"
          + '<p>Click <a href="' + ScriptApp.getService().getUrl()
          + '?order_number=' + orderNumber
          + '&delivered=true" >here</a> '
          + 'to acknowledge the delivery.</p>';

      // Send email to the user
      MailApp.sendEmail({
        to: userEmail,
        subject: "Order dispatched",
        htmlBody: htmlBody
      });

      // Return confirmation to the dispatch team.
      return "Shipment details updated and user notified by " \+ "an e-mail.";
    }
  };

  // Displays error if query order_number not found in sheet.
  throw "Order number not found.";
};

这里显示了分发通知电子邮件的样本内容:

分发文章

启用用户确认文章交付

如果用户收到物品,则他/她点击链接以确认。这次也使用相同的发布 URL,并附加一个交付查询字符串。

例如:

https://script.google.com/macros/s/AKfycbwaqlj_kBAn9LLav0qv6GmXlWk-hwIosHA- 1_1YoMutiiuGy84/exec?order_number=1451875765851&delivered=true

为了处理此查询,doGet函数应再次更新如下:

function doGet(e){
  var delivered = e.parameter.delivered;

  if(delivered){
    // If order delivered then just update delivery date.
    updateDelivery(e);

    // Returning text content is enough, HtmlService not needed.
    return ContentService.createTextOutput("Thank you!");
  }

  var orderNumber = e.parameter.order_number;

  if(orderNumber){

    /*
     *  If order number present in query string
     *  then serve dispatch form to order processing unit.
     *
     */
    var template = HtmlService.createTemplateFromFile("Dispatch");
    var data = SheetOrders.getDataRange().getValues();

    for(var i in data){
      if( data[i][0] == orderNumber ){
        template.order = data[i];
        break;
      }
    };

  } else {

    /*
     *  If order number not present in query string
     *  then serve order form to the user.
     *
     */
    var template = HtmlService.createTemplateFromFile("Order");
    template.pricelist = getPrice();

  };

  var html = template.evaluate();
  return HtmlService.createHtmlOutput(html);
}

应添加另一个处理程序,即updateDelivery函数,如下所示:

function updateDelivery(e){
  // Delivery date column number minus one.
  const DELIVERED_ON = 10;

  var orderNumber = e.parameter.order_number;
  var deliveryDate = new Date();
  var data = SheetOrders.getDataRange().getValues();

  // Update delivery date on matched order number.
  for(var i = 0; i < data.length; i++){
    if(""+data[i][0] == orderNumber){
      SheetOrders.getRange(i+1, DELIVERED_ON+1)
        .setValue(deliveryDate);
    }
  };
}

此函数将当前日期更新为交付日期。以下是一个已更新“交付日期”列的示例电子表格:

启用用户确认文章交付

恭喜!您已创建了一个完整的、真实的订单处理工作流应用程序。

摘要

在本章中,您学习了并创建了一个有用的真实世界订单处理应用程序。在下一章中,您将学习如何克服最大执行时间限制,并学习如何使用来自其他脚本文件或库的脚本代码,包括 OAuth 库。您还将学习如何创建附加组件。

第九章。更多技巧和窍门以及创建插件

在上一章中,你构建了一个订单处理工作流应用程序。在本章中,你将学习:

  • 要克服脚本最大执行时间限制

  • 要使用来自其他脚本文件或库的脚本代码

  • 创建使用 OAuth2 外部库的插件

克服“脚本超出最大执行时间”错误

如果你的脚本函数中存在一个导致无限(不终止)执行的错误,例如无限循环和/或 while 循环。除了仔细检查循环终止语句外,没有其他补救措施。

有时候,你的脚本可能完美无瑕或无错误,但如果它需要处理大型电子表格或外部数据,它可能需要很长时间才能完成执行。你的脚本连续运行的最大允许时间是 6 分钟。如果超过这个限制,GAS 会抛出“超出最大执行时间”异常。

小贴士

有关其他限制的列表,请访问:developers.google.com/apps-script/guides/services/quotas#current_limitations

要克服这个瓶颈,你可以遵循以下步骤。例如,如果你的doLengthyProcess函数需要很长时间才能完成,那么你可以手动为doLengthyProcess函数创建一个分钟的触发器,使其每 10 分钟执行一次。你的函数应该定期检查自开始以来的经过时间。如果在时间限制内成功完成,则在函数结束时删除触发器。否则,触发器值存储在专门的 Sheet 中的循环计数器或脚本属性中。当函数再次被触发时,应该读取并分配给循环计数器。

这里给出这样一个函数的示例结构:

var ss = SpreadsheetApp.getActiveSpreadsheet();

// A dedicated sheet to store values temporarily.
var sheet = ss.getSheetByName("Settings");

function doLengthyProcess() {
  // Prefix '+' to get date as epochy number.
  var elapsedTime, startTime = +new Date();

  // Loop variable.
  // Load value of 'i' from spreadsheet cell, or default 0.
  var i = sheet.getRange("A1").getValue() || 0;

  for(; i<1000; i++){
    // Your time consuming process goes here.
    …
    …
    …
    …

    // Recalculate elapsedTime.
    elapsedTime = +new Date() - startTime;

    if(elapsedTime> 300000){ // 300000 ms or 5 minutes.
      sheet.getRange("A1").setValue(i);
      return;
    }
  };

  // Loop completed successfully, so delete trigger.
  deleteTriggers_();
}

// Helper function
function deleteTriggers_(){
  var triggers = ScriptApp.getProjectTriggers();
  triggers.forEach(function(trigger){
    ScriptApp.deleteTrigger(trigger);
    /*
     * Wait a moment before calling deleteTrigger again.
     * Otherwise you may get warning message something like
     * "Service invoked too many times..."
     *
     */
    Utilities.sleep(1000); // In millisecond.
  });

};

总结来说,你手动创建一个“每分钟”触发器,然后函数执行直到成功完成。

如果你犹豫是否手动创建触发器,你可以按照我们在第三章中讨论的方法,通过脚本创建它们,解析和发送电子邮件,但这次创建每分钟的触发器:

function createTrigger_(funcName,minutes){
// Delete already created triggers if any.
deleteTriggers_();
  ScriptApp.newTrigger(funcName).timeBased()
    .everyMinutes(minutes).create();
}

在这里,你为那些不是立即开始而是有延迟的函数创建了一个触发器。然而,如果你想自动化一切,这意味着创建一个触发器并立即调用函数。创建另一个函数startProcess,如下所示:

function startProcess(){
  createTrigger_("doLengthyProcess",10);
  doLengthyProcess();
}

现在,你只需要运行startProcess函数。此外,你还可以为此函数分配一个菜单。

配置你的脚本项目以使用外部库

有时候,你可能希望在你的项目中重用来自其他脚本项目或其他程序员代码的代码。你可以将外部代码导入当前项目中,你需要在当前项目中进行简单的配置。

例如,我们将解释如何将上一章的代码导入当前项目。

  1. 在脚本编辑器中打开你之前创建的任何脚本(例如,第八章 构建工作流应用程序),如果你还没有保存,请保存一个版本。

  2. 现在,点击文件菜单,然后项目属性项目属性对话框将打开,如下所示:配置脚本项目以使用外部库

  3. 复制项目密钥值(这个值对于你的项目应该是不同的)。

  4. 打开当前脚本,导航到资源 | 库…,然后包含的库对话框将打开,如下所示:配置脚本项目以使用外部库

  5. 项目密钥(你在第 1 步中已经复制)粘贴到查找库文本框中,然后点击选择按钮。

现在,Chapter 8 项目将包含在库列表中,如下所示:

配置脚本项目以使用外部库

选择版本(你将在当前项目中使用的版本);如果你愿意,你还可以更改标识符(Chapter8)。关闭开发模式意味着使用所选版本;将其设置为开启以覆盖所选版本并使用当前版本。点击保存按钮以保存更改。

现在所有函数(除了私有函数,即以"_"结尾的函数名)和全局变量都可以在当前项目中使用。例如,你可以通过在标识符前加上前缀来使用来自第八章 构建工作流应用程序doGet 函数。这意味着你可以将 doGet 函数作为 Chapter8.doGet() 使用,将 getPrice 函数作为 Chapter8.getPrice() 使用,依此类推。

如果你需要更多解释,请参考以下示例用法:

function test(){

  var pricelist = Chapter8.getPrice();

  Logger.log(pricelist);
}

使用 JSDoc 注解

在前面的测试函数中,你可以看到,一旦你在标识符名称(Chapter8)旁边输入一个'.',代码提示就会立即激活。这显示了外部库中所有可用的函数和全局变量,如下所示:

使用 JSDoc 注解

上述代码提示是通用的,例如,index 显示为 Object。对于详细的代码提示,你应该使用 JSDoc 风格的文档(函数定义顶部的注解或注释)。

例如,如果你在第八章 构建工作流应用程序中对 getPrice 函数使用了以下注解:

/**
 *  Returns price list data from the Stock tab/sheet
 *
 *  @param {number} index 
 *  @return {array}
 *
 */
function getPrice(index){
          …
}

然后,代码提示将如下所示:

使用 JSDoc 注解

现在,你可以注意到代码提示如何返回有关 getPrice 函数的有用信息。

小贴士

要进一步了解 JSDoc,请访问:developers.google.com/closure/compiler/docs/js-for-compiler

使用开源 OAuth 库

如果您的应用程序与 Google 基本服务以外的外部库交互,则应该进行认证。换句话说,如果您的应用程序代表用户运行,那么该用户应该授权您的应用程序访问其数据。GAS 不提供任何内置的认证服务,但您可以使用开源的 OAuth 库。

创建、测试和发布插件

如果您需要在当前项目中使用其他外部库,您需要知道项目密钥,并且您应该至少有对该项目的读取权限。同时,除非客户端选择当前版本,否则主项目的每个新版本都不会反映在客户端项目中。插件可以覆盖此配置麻烦。

插件可以通过点击按钮安装,无需配置。您可以在其他程序员发布的 Sheets、Docs 和/或 Forms 中,或从 Google Chrome Web Store 安装插件。

从 Chrome Web Store 安装插件

要从 Chrome Web Store 安装插件,打开文档(Sheets、Docs 或 Forms),然后从 插件 菜单点击 获取插件…。从 插件 对话框中选择您喜欢的任何一个插件(如果您将鼠标悬停在任何一个插件上,则应用程序上会出现一个加号符号;点击它并在需要时进行授权)。这里显示了一个示例 插件 对话框,但实际包含的插件可能会随时间而变化。

从 Chrome Web Store 安装插件

您将安装该插件并将其添加到文档的 插件 菜单中。每个插件都附带易于使用的菜单项。例如,如果您安装了 autoCrat,则菜单项将类似于此处显示的截图:

从 Chrome Web Store 安装插件

创建自定义插件

您可以自己创建插件,在您的其他文档中使用,或与其他用户共享。您的用户可以使用您的插件,但不能看到代码。因此,您可以保持您的知识产权(即代码和数据)的秘密。

将菜单项添加到 插件 菜单,以便:

function onOpen(e){
  SpreadsheetApp.getUi().createAddonMenu()
  .addItem("Show Sidebar", "showSidebar")
  .addToUi();
}

addItem 方法的第一个参数是菜单项的标签,下一个是函数名。如果您打算为 Chrome Web Store 发布插件,请添加 onInstall 事件函数:

function onInstall(e){
  onOpen(e);
}

前面的函数在 Sheets、Docs 或 Forms 中首次安装插件时调用 onOpen 函数。如果您的插件需要用户界面,则创建侧边栏对话框:

/**
 *  Opens sidebar in the document containing the add-on's
 *   user interface.
 *
 */
function showSidebar() {
  SpreadsheetApp.getUi().showSidebar(
    HtmlService.createHtmlOutputFromFile('Sidebar')
  );
}

要样式化侧边栏(创建 Sidebar.html),您可以使用从 ssl.gstatic.com/docs/script/css/add-ons1.css 网址提供的官方支持的 CSS 包。

注意

如需更多关于此包的帮助,请访问 developers.google.com/apps-script/add-ons/css

测试您的插件

要在脚本编辑器中测试您的插件,请转到 发布 | 测试为插件… 菜单,然后在出现的对话框中选择您想要测试插件的文档,如下所示:

测试您的插件

要与他人共享您的插件,只需共享文档即可。更多的话,如果您想发布脚本,请按照 发布 | 部署为插件 菜单操作。在出现的对话框中填写所需字段并遵循提供的指南。您的插件应严格遵守 Chrome Web Store 的内容和风格指南,并在上市和向公众提供之前经过审查流程。

注意

如需有关插件的更多信息,请访问 developers.google.com/apps-script/add-ons/

创建使用 OAuth2 外部库的插件

为了获得上述所有概念的实际操作经验,我们将创建一个插件,可以将活动电子表格作为 PDF 附件发送到活动用户的电子邮件地址。

在 Sheets 中创建一个新的脚本项目。在脚本编辑器中,我们首先创建几个全局变量,如下所示:

var ss = SpreadsheetApp.getActiveSpreadsheet();
var activeSheet = ss.getActiveSheet();
var activeSheetName = activeSheet.getSheetName();

接下来,查看 onOpenonInstall 触发器函数。

/**
 * Creates a menu entry in the Google Sheets UI when the document
 * is opened.
 *
 * @param {object} e The event parameter for a simple onOpen
 * trigger.
 *
 */
function onOpen(e){
  // Create an Add-on menu item and associate a function.
  SpreadsheetApp.getUi().createAddonMenu()
  .addItem("Sheet To PDF", "sendSheetAsPdfToActiveUser")
  .addToUi();
}

/**
 * Runs when the add-on is installed.
 *
 * @param {object} e The event parameter for a simple onInstall
 * trigger.
 *
 */
function onInstall(e){
  onOpen(e);
}

onOpen 触发器中,我们将 sendSheetAsPdfToActiveUser 函数关联到 Sheet To PDF 菜单项。我们现在将创建该函数:

/** 
* Sends PDF attachment to the active user e-mail id. 
* 
*/ 
function sendSheetAsPdfToActiveUser(){ 
  // Get active user's email id. 
  var mailTo = Session.getActiveUser().getEmail(); 
  // Returns either pdf or false. 
  var attachments = getAttachments(); 
  // Send only if there is attachment. 
  if(attachments){ 
    MailApp.sendEmail( 
        mailTo, activeSheetName, '', {attachments:attachments} 
); 
  } 
}

该功能通过 getAttachments 函数返回的 PDF 附件发送带有附件的电子邮件。以下是一个示例:

/**
 * Authorizes the application for the first time or the token
 *  expires. If authentication token is valid then returns the pdf
 *  file with other attachment parameter otherwise prompts the 
 *  user to authorize.
 *
 *  @return {Object} Array of attachment objects.
 */
function getAttachments(){
  // Authenticated service object.
  var service = getGoogleService();

  // Proceed further only if authenticated, otherwise prompt for
  // authentication.
  if (service.hasAccess()) {

    // The url to download activesheet as pdf.
    var url = ss.getUrl()
        .replace("edit", "export?gid=" + activeSheet.getSheetId()
          + "&format=pdf&attachment=false");

    // The access token should be sent on every request.
    var headers = {
        Authorization:'Bearer ' + service.getAccessToken()
    };

    // Send request to the pdf url with the access token.
    var response = UrlFetchApp.fetch(url, { headers:headers });

    // Returned content.
    var content = response.getContent();

    // Returns as an array of objects.
    return [{
      fileName: activeSheetName + ".pdf",
      content: content,
      mimeType:"application/pdf"
    }];

  } else {

    // Authorization url from the service object.
    var authorizationUrl = service.getAuthorizationUrl();

    // Side bar with the authorization link.
var template = HtmlService 
    .createTemplate( 
    '<a href="<?= authorizationUrl ?>" 
    target="_blank">Authorize</a>.' 
);

    // Authorization url assigned to template
    template.authorizationUrl = authorizationUrl;

    // Finally evaluate the template and show sidebar.
    var page = template.evaluate();
    SpreadsheetApp.getUi().showSidebar(page);

    // Attracting user attention.
    Browser.msgBox('Authorize on sidebar and run again.');

    // Return false, so no need to send e-mail.
    return false;
  }
}

该功能提示用户在首次使用或已有一个有效的认证令牌但已过期时对应用程序进行认证。如果认证令牌有效,则返回 PDF 附件,否则返回 false。

现在,我们唯一剩下要做的就是实现 OAuth2 流程。我们将为同一目的创建一个函数:

/**
 * Executes OAuth2 flow.
 *
 * @return {Object} Authentication service object.
 *
 */
function getGoogleService(){
  /*
   * Create a new service with the given name (here 'PACKT').
   * The name will be used when persisting the authorized token,
   * so ensure it is unique within the scope of the property
   * store.
   *
   */
  return OAuth2.createService("PACKT")

// Endpoint URLs are same for all Google services. 
.setAuthorizationBaseUrl( 
'https://accounts.google.com/o/oauth2/auth' 
)
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
  /*
   * Replace with your client ID and secret got from developers
   * console.
   *
   */
  .setClientId('...')
  .setClientSecret('...')

  // A callback function to complete the OAuth2 flow.
  .setCallbackFunction('authCallback')

  // A place to store authenticated tokens.
  .setPropertyStore(PropertiesService.getUserProperties())

  /*
   * Scopes to request, separate with space if more than one
   * scope.
   *
   */
  .setScope('https://docs.google.com/feeds/')

  /*
   * Google-specific parameters.
   *
   * Sets the login hint, which will prevent the account chooser
   * screen from being shown to users if logged in with multiple
   * accounts.
   *
   */
  .setParam('login_hint', Session.getActiveUser().getEmail())

  // Requests offline access.
  .setParam('access_type', 'offline')

  /*
   * Forces the approval prompt every time to show up.
   * This is useful for testing, but not desirable in a production
   * application.
   *
   */
  .setParam('approval_prompt', 'force');
}

不要忘记替换您自己的客户端 ID 和密钥。我们将看到如何获取它们。您还记得在 第五章 创建 Google 日历和 Drive 应用程序 中您做了什么来启用高级服务吗?在这里使用相同的步骤,但增加一些额外的任务。

在脚本编辑器中,转到 资源 | 开发者控制台项目… 并在打开的对话框中点击 查看开发者控制台

创建使用 OAuth2 外部库的插件

这应该会带您到开发者控制台仪表板,您可以在其中点击 启用和管理 API 选项。

创建使用 OAuth2 外部库的插件

一旦启用,点击控制台仪表板左侧的 凭证

创建使用 OAuth2 外部库的插件

为了完成 OAuth2 流程,此回调函数将被调用,并向用户显示一条消息。然后,OAuth2 客户端 ID 被列为Apps Script。点击Apps Script以查看如下所示的详细信息:

创建使用 OAuth2 外部库的插件

您需要为此项目添加一个授权的重定向 URL。输入如下所示的 URL,但替换为您的项目密钥:

https://script.google.com/macros/d/[[PROJECT KEY]]/usercallback

在点击保存之前,getGoogleService函数中需要客户端 ID 和客户端密钥的副本。一旦完成,点击保存;之后您任何时候都可以返回此仪表板。

还需添加如下所示的authCallback函数:

function authCallback(request) {
  var service = getGoogleService();
  var isAuthorized = service.handleCallback(request);

  if (isAuthorized) {
return HtmlService 
  .createHtmlOutput('Success! You can close this tab.'); 
  } else {
return HtmlService 
  .createHtmlOutput('Denied. You can close this tab'); 
  }
}

在测试插件之前,使用项目密钥导入 OAuth2 客户端库:

MswhXl8fVhTFUH_Q3UOJbXvxhMjh3Sh48

小贴士

以下是一些示例密钥:

  • OAuth1 库密钥:Mb2Vpd5nfD3Pz-_a-39Q4VfxhMjh3Sh48

  • OAuth2 库密钥:MswhXl8fVhTFUH_Q3UOJbXvxhMjh3Sh48

注意

有关开源 OAuth2 外部库的更多信息,请访问github.com/googlesamples/apps-script-oauth2

现在,您已完成了所有设置步骤。刷新电子表格窗口,以便您的插件出现在插件菜单中。

此外,一旦认证,您可以使用此功能重置或撤销认证:

function clearService(){
  OAuth2.createService('PACKT')
  .setPropertyStore(PropertiesService.getUserProperties())
  .reset();
}

注意这里使用的相同的服务名称(PACKT)。

其他有用链接

摘要

在本章中,您学习了如何克服脚本最大运行时间限制,如何导入外部库,如何使用 OAuth,以及如何创建插件。我们希望您喜欢阅读这本书,学习,并在 Google Apps Script 的大部分方面积累实践经验。祝您编码愉快,享受编程!

posted @ 2025-10-26 08:50  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报