使用 Google Drive 作为 CMS

Avatar of Nathan Babcock
Nathan Babcock 发布

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费信用额度!

我们将逐步介绍连接 Google Drive 的 API 以获取网站内容的技术流程。 我们将检查分步实施过程,以及如何利用服务器端缓存来避免主要陷阱,例如 API 使用限制和图像热链接。 本文提供了现成的 npm 包、Git 存储库和 Docker 镜像。

但是……为什么?

在网站开发的某个阶段,会遇到一个十字路口:当管理内容的人员不具备技术能力时,如何管理内容? 如果内容由开发人员无限期管理,纯 HTML 和 CSS 就足够了——但这会阻止更广泛的团队协作; 此外,没有开发人员希望永远负责内容更新。

那么,当新的非技术合作伙伴需要获得编辑权限时会发生什么? 这可能是一名设计师、产品经理、营销人员、公司高管,甚至最终客户。

这就是一个好的内容管理系统的作用,对吧? 也许是像 WordPress 这样的东西。 但这也有其自身的缺点:它是一个您的团队需要处理的新平台,一个需要学习的新界面,以及潜在攻击者可以利用的新途径。 它需要创建模板,一种具有其自身语法和特性的格式。 可能需要审核、安装和配置自定义或第三方插件以满足独特的用例——并且这些中的每一个都是复杂性、摩擦、技术债务和风险的另一个来源。 所有这些设置的膨胀最终可能会以不利于网站实际目的的方式限制您的技术。

如果我们能够从内容所在的位置提取内容呢? 这正是我们在这里要讨论的。 我在许多工作场所都使用 Google Drive 来组织和共享文件,其中包括博客和目标网页内容草稿等内容。 我们能否利用 Google Drive 的 API 将 Google 文档直接导入到网站中作为原始 HTML,并使用简单的 REST 请求?

当然可以! 以下是我在工作中使用的方法。

您需要什么

在开始之前,您可能需要查看以下几点

使用 Google Drive API 进行身份验证

第一步是建立与 Google Drive API 的连接,为此,我们需要进行某种身份验证。 即使相关文件是公开共享的(启用了“链接共享”),使用 Drive API 也需要进行身份验证。 Google 支持多种执行此操作的方法。 最常见的是 OAuth,它会提示用户使用 Google 品牌的屏幕,显示“[某应用] 想要访问您的 Google Drive”,并等待用户同意——这并非我们在这里需要的内容,因为我们希望访问单个中央驱动器中的文件,而不是用户的驱动器。 此外,仅为特定文件或文件夹提供访问权限有点棘手。 我们可能使用的https://www.googleapis.com/auth/drive.readonly范围的描述如下

查看和下载您 Google Drive 中的所有文件。

这正是同意屏幕上显示的内容。 这对于用户来说可能是令人担忧的,更重要的是,对于管理网站内容的任何中央开发人员/管理员 Google 帐户来说,这都是潜在的安全漏洞; 他们可以访问的任何内容都通过网站的 CMS 后端公开,包括他们自己的文档以及与他们共享的任何内容。 不好!

使用“服务帐户”

相反,我们可以使用一种不太常见身份验证方法:Google 服务帐户。 可以将服务帐户视为专供 API 和机器人使用的虚拟 Google 帐户。 但是,它的行为类似于一流的 Google 帐户; 它拥有自己的电子邮件地址、自己的身份验证令牌以及自己的权限。 这里最大的优势在于,我们可以像任何其他用户一样,将文件提供给此虚拟服务帐户——通过与服务帐户的电子邮件地址共享文件,该地址如下所示

[email protected]

当我们转到在网站上显示文档或表格时,只需点击“共享”按钮并粘贴该电子邮件地址即可。 现在,服务帐户只能查看我们明确与其共享的文件或文件夹,并且可以随时修改或撤消该访问权限。 完美!

创建服务帐户

可以通过 Google Cloud Platform 控制台(免费)创建服务帐户。 此过程在 Google 的开发者资源 中有详细记录,此外,本文配套存储库中也对其进行了分步说明 在 GitHub 上。 为简洁起见,让我们快进到成功验证服务帐户后的时间。

Google Drive API

现在我们已经进入,可以开始调整 Drive API 的功能了。 我们可以从修改后的 Node.js 快速入门示例 开始,调整为使用我们的新服务帐户而不是客户端 OAuth。 这在 我们正在构建的driveAPI.js的前几个方法 中处理,用于处理我们与 API 的所有交互。 与 Google 示例的主要区别在于authorize()方法,在该方法中,我们使用jwtClient的实例而不是 Google 示例中使用的oauthClient

authorize(credentials, callback) {
  const { client_email, private_key } = credentials;

  const jwtClient = new google.auth.JWT(client_email, null, private_key, SCOPES)

  // Check if we have previously stored a token.
  fs.readFile(TOKEN_PATH, (err, token) => {
    if (err) return this.getAccessToken(jwtClient, callback);
    jwtClient.setCredentials(JSON.parse(token.toString()));
    console.log('Token loaded from file');
    callback(jwtClient);
  });
}

Node.js 与客户端

关于此设置的另一个说明——此代码旨在从服务器端 Node.js 代码中调用。 这是因为服务帐户的客户端凭据必须保密,并且不能泄露给我们网站的用户。 它们保存在服务器上的credentials.json文件中,并通过 Node.js 中的fs.readFile加载。 它也列在.gitignore中,以防止敏感密钥进入源代码管理。

获取文档

设置好阶段后,从 Google 文档加载原始 HTML 就变得非常简单。 类似这样的方法会返回 HTML 字符串的 Promise

getDoc(id, skipCache = false) {
  return new Promise((resolve, reject) => {
    this.drive.files.export({
      fileId: id,
      mimeType: "text/html",
      fields: "data",
    }, (err, res) => {
      if (err) return reject('The API returned an error: ' + err);
      resolve({ html: this.rewriteToCachedImages(res.data) });
      // Cache images
      this.cacheImages(res.data);
    });
  });
}

Drive.Files.export 端点在这里为我们完成了所有工作。 我们传递的id只是您在浏览器地址栏中打开文档时显示的内容,它显示在https://docs.google.com/document/d/之后。

还要注意关于缓存图像的两行——这是一种特殊的考虑因素,我们现在先跳过,并在下一节中详细讨论。

这是一个 Google 文档 使用此方法在外部 显示为 HTML 的示例。

获取表格

使用 Spreadsheets.values.get 获取 Google 表格几乎一样简单。我们只需稍微调整一下响应对象,将其转换为简化的 JSON 数组,并使用表格第一行中的列标题进行标记。

getSheet(id, range) {
  return new Promise((resolve, reject) => {
    this.sheets.spreadsheets.values.get({
      spreadsheetId: id,
    range: range,
  }, (err, res) => {
    if (err) return reject('The API returned an error: ' + err);
    // console.log(res.data.values);
    const keys = res.data.values[0];
    const transformed = [];
    res.data.values.forEach((row, i) => {
      if(i === 0) return;
      const item = {};
      row.forEach((cell, index) => {
        item[keys[index]] = cell;
      });
       transformed.push(item);
      });
      resolve(transformed);
    });
  });
}

id 参数与文档的相同,这里新的 range 参数指的是要从中获取值的单元格范围,使用 表格 A1 表示法

示例此表格 被读取并解析,以便在 此页面 上呈现自定义 HTML。

…以及更多!

这两个端点已经可以让你走得很远,并且构成了网站自定义 CMS 的基础。但实际上,它只触及了 Drive 在内容管理方面的潜力的表面。它还能够

  • 列出给定文件夹中的所有文件并在菜单中显示它们,
  • 从 Google Slides 演示文稿导入复杂媒体,以及
  • 下载和缓存自定义文件。

唯一的限制是你的创造力和完整 Drive API 的约束 此处有文档记录

缓存

在使用 Drive API 支持的各种查询时,你可能会收到“用户速率限制已超出”错误消息。在开发阶段通过重复的试错测试很容易达到此限制,并且乍一看,它似乎会对我们的 Google Drive-CMS 策略构成一个硬性障碍。

这就是缓存的作用——每次我们获取 Drive 上任何文件的新版本时,我们都会将其缓存在本地(即服务器端,在 Node.js 进程中)。一旦我们这样做,我们只需要检查每个文件的版本。如果我们的缓存已过期,我们将获取相应文件的最新版本,但此请求仅每个文件版本发生一次,而不是每个用户请求一次。我们不再需要根据使用网站的人数进行扩展,而是可以根据 Google Drive 上的更新/编辑次数作为我们的限制因素进行扩展。在免费套餐帐户的当前 Drive 使用限制下,我们每分钟最多可以支持 300 个 API 请求。缓存应该可以让我们保持在此限制范围内,并且可以通过 批量处理多个请求 进一步优化。

处理图像

相同的缓存方法应用于嵌入在 Google 文档中的图像。getDoc 方法解析 HTML 响应以查找任何图像 URL,并发出辅助请求下载它们(如果它们已存在,则直接从缓存中获取)。然后它重写 HTML 中的原始 URL。结果是静态 HTML;我们永远不会使用热链接到 Google 图片 CDN。在到达浏览器之前,图像已经预先缓存。

尊重和响应式

缓存确保两件事:首先,我们正在尊重 Google 的 API 使用限制,并真正利用 Google Drive 作为编辑和文件管理的前端(工具的预期用途),而不是免费获取带宽和存储空间。它使我们的网站与 Google API 的交互保持在刷新缓存所需的最低限度。

另一个好处是我们网站的用户将会享受:具有最短加载时间的响应式网站。由于缓存的 Google 文档作为静态 HTML 存储在我们的服务器上,因此我们可以立即获取它们,而无需等待第三方 REST 请求完成,从而将网站加载时间保持在最低限度。

封装在 Express 中

由于所有这些调整都在服务器端的 Node.js 中进行,因此我们需要一种方法让我们的客户端页面与 API 交互。通过将 DriveAPI 封装到它自己的 REST 服务中,我们可以创建一个中间人/代理服务,该服务可以抽象掉缓存/获取新版本的所有逻辑,同时将敏感的身份验证凭据安全地保存在服务器端。

一系列 express 路由,或你喜欢的 Web 服务器中的等效项,将完成这项工作,并具有一系列类似这样的路由

const driveAPI = new (require('./driveAPI'))();
const express = require('express');
const API_VERSION = 1;
const router = express.Router();

router.route('/getDoc')
.get((req, res) => {
  console.log('GET /getDoc', req.query.id);
  driveAPI.getDoc(req.query.id)
  .then(data => res.json(data))
  .catch(error => {
    console.error(error);
    res.sendStatus(500);
  });
});

// Other routes included here (getSheet, getImage, listFiles, etc)...

app.use(`/api/v${API_VERSION}`, router);

请参阅配套存储库中的完整 express.js 文件.

额外内容:Docker 部署

为了部署到生产环境,我们可以将 Express 服务器 与你现有的静态 Web 服务器一起运行。或者,如果方便的话,我们可以轻松地将其包装在一个 Docker 镜像中

FROM node:8
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./
RUN npm install
# If you are building your code for production
# RUN npm ci --only=production
# Bundle app source
COPY . .
CMD [ "node", "express.js" ]

…或使用此 在 Docker Hub 上发布的预构建镜像

额外内容 2:NGINX Google OAuth

如果你的网站面向公众(互联网上的任何人都可以访问),那么我们就完成了!但对于我们在摩托罗拉的工作目的,我们正在发布一个仅限内部的文档站点,该站点需要额外的安全性。这意味着在所有 Google 文档上都关闭了链接共享(它们也恰好存储在与所有其他公司内容分开的隔离且专用的 Google 团队驱动器中)。

我们在服务器级别尽早处理了这一额外的安全层,使用 NGINX 在请求到达 Express 服务器或网站托管的任何静态内容之前拦截并反向代理所有请求。为此,我们使用 Cloudflare 的 优秀的 Docker 镜像 向访问任何网站资源或端点(Drive API Express 服务器和与之一起的静态内容)的所有员工显示 Google 登录屏幕。它与他们已经拥有的公司 Google 帐户和单点登录无缝集成——无需额外帐户!

结论

本文中我们介绍的所有内容正是我们在工作中所做的事情。这是一个轻量级、灵活且分散的内容管理架构,其中原始数据存储在 Google Drive 中,我们的团队已经在那里工作,使用每个人都熟悉的 UI。所有这些都与网站的前端结合在一起,在控制演示文稿方面保留了纯 HTML 和 CSS 的全部灵活性,并且架构约束最少。作为开发人员,你只需多做一些工作,就可以为你的非开发人员协作者和最终用户创造几乎无缝的体验。

这种方法是否适用于所有人?当然不是。不同的网站有不同的需求。但是,如果我需要列出何时使用 Google Drive 作为 CMS 的用例,它将如下所示

  • 拥有数百到数千名每日用户的内部网站——如果这是全球公司网站的首页,即使每个用户仅请求一次文件版本元数据也可能会接近 Drive API 使用限制。其他技术可以帮助缓解这种情况——但它最适合中小型网站。
  • 单页应用程序——此设置使我们能够每个会话一次性查询每个数据源的版本号,而不是每个页面一次。非单页应用程序可以使用相同的方法,甚至可以利用 cookie 或本地存储来实现相同的“每次访问一次”版本查询,但同样,这需要一些额外的工作。

  • 已经在使用 Google Drive 的团队——也许最重要的是,我们的合作者惊喜地发现,他们可以使用自己已经拥有并习惯使用的帐户和工作流程来为网站做出贡献,包括 Google 所见即所得体验的所有改进、强大的访问管理等等。