我们将通过技术流程来探讨如何钩住Google Drive的API来获取网站上的内容。我们将研究一步步的实施,以及如何利用服务器端的缓存来避免主要的陷阱,如API使用限制和图片热链接。整个文章提供了一个现成的npm包、Git repo和Docker图片。
但是......为什么?
在网站开发的某个阶段,会遇到一个十字路口:当管理内容的人不懂技术的时候,如何管理内容?如果内容由开发人员无限期地管理,纯粹的HTML和CSS就足够了--但这妨碍了更广泛的团队合作;此外,没有一个开发人员愿意永远为内容的更新负责。
那么,当一个新的非技术伙伴需要获得编辑权限时,会发生什么?这可能是一个设计师,一个产品经理,一个营销人员,一个公司高管,甚至是一个最终客户。
这就是一个好的内容管理系统的作用,对吗?也许是像WordPress这样的东西。但是,这也带来了自己的一系列缺点:对你的团队来说,这是一个需要兼顾的新平台,一个需要学习的新界面,以及一个潜在攻击者的新载体。它需要创建模板,这种格式有自己的语法和特殊性。自定义或第三方插件可能需要为独特的用例进行审核、安装和配置--而这些都是复杂性、摩擦、技术债务和风险的另一个来源。所有这些设置的臃肿最终可能会使你的技术受到影响,从而与网站的实际目的相悖。
如果我们可以从已经存在的地方拉出内容呢?这就是我们在这里要讨论的问题。我工作过的许多地方都使用Google Drive来组织和分享文件,这包括像博客和登陆页内容的草稿。我们能不能利用Google Drive的API,通过一个简单的REST请求,将Google Doc直接导入到一个网站的原始HTML中?
当然可以。下面是我工作的地方是如何做到的。
你将需要什么?
在我们开始的时候,你可能想查看一些东西:
使用Google Drive的API进行认证
第一步是建立与Google Drive API的连接,为此,我们需要进行某种认证。这是使用Drive API的一个要求,即使有关的文件是公开共享的(开启了 "链接共享")。谷歌支持几种方法来做到这一点。最常见的是OAuth,它通过一个谷歌品牌的屏幕提示用户:"[某某应用]想访问你的谷歌硬盘",并等待用户同意--这并不是我们在这里需要的,因为我们想访问单个中央驱动器中的文件,而不是用户的驱动器。另外,只提供对特定文件或文件夹的访问是有点麻烦的。我们可能使用的https://www.googleapis.com/auth/drive.readonly 范围被描述为。
查看和下载你所有的Google Drive文件。
这正是它在同意屏幕上所说的。这对用户来说是潜在的警报,更重要的是,这对任何管理网站内容的中央开发人员/管理员谷歌账户来说是一个潜在的安全弱点;他们可以访问的任何东西都通过网站的CMS后端暴露出来,包括他们自己的文件和与他们共享的任何东西。不妙啊!
进入 "服务账户"
相反,我们可以利用一种稍微不常见的认证方法:谷歌服务账户。想想看,服务账户就像一个专门用于API和机器人的假谷歌账户。然而,它的行为就像一个一流的谷歌账户;它有自己的电子邮件地址,自己的认证令牌,以及自己的权限。这里最大的好处是,我们可以像其他用户一样向这个假服务账户提供文件--通过与服务账户的电子邮件地址共享文件,它看起来像这样。
google-drive-cms-example@npm-drive-cms.iam.gserviceaccount.com
当我们要在网站上显示一个文档或工作表时,我们只需点击 "共享 "按钮并粘贴该电子邮件地址。现在,该服务账户只能看到我们明确与之共享的文件或文件夹,而且可以随时修改或撤销这种访问。完美!"。
创建一个服务账户
一个服务账户可以从谷歌云平台控制台创建(免费)。这个过程在谷歌的开发者资源中得到了很好的记录,此外,在GitHub上本文的配套 repo中也有一步步的详细描述。为了简洁起见,让我们快进到成功认证服务账户之后。
Google Drive API
既然我们已经进入了,我们就可以开始修补Drive API的功能了。我们可以从Node.js快速入门示例的修改版开始,调整为使用我们的新服务账户而不是客户端OAuth。这在我们构建的 driveAPI.js的前几个方法中得到了处理,以处理我们与API的所有交互。与谷歌样本的关键区别是在authorize() 方法中,我们使用jwtClient 的实例,而不是谷歌样本中使用的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 vs. 客户端
关于这里的设置还有一点要注意--这段代码是要从服务器端的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/ 。
还注意到关于缓存图片的两行字--这是一个特殊的考虑,我们现在先跳过,在下一节再详细讨论。
下面是一个使用这种方法在外部显示为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) 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 参数与doc相同,这里新的range 参数指的是要取值的单元格范围,用Sheet A1符号表示。
例如:读取并解析此工作表,以便在此页面上呈现自定义HTML。
...还有更多
这两个端点已经让你走得很远了,并构成了一个网站的自定义CMS的骨干。但是,事实上,它只是挖掘了Drive的内容管理潜力的表面。它还具有以下功能
- 列出特定文件夹中的所有文件,并在菜单中显示它们。
- 从谷歌幻灯片演示中导入复杂的媒体,以及
- 下载和缓存自定义文件。
这里唯一的限制是你的创造力,以及这里记录的完整的Drive API的限制。
缓存
当你在使用Drive API支持的各种查询时,你可能最终会收到 "超过用户速率限制 "的错误信息。在开发阶段,通过反复的试验和错误测试,很容易遇到这个限制,乍一看,这似乎是我们的Google Drive-CMS策略的一个硬障碍。
这就是缓存的作用--每次我们在Drive上获取任何文件的_新版本_时,我们都会在本地(也就是服务器端,在Node.js进程中)进行缓存。一旦我们这样做,我们只需要检查每个文件的_版本_。如果我们的缓存已经过期,我们就会获取相应文件的最新版本,但_每个文件版本的_请求只发生_一次_,而不是每个用户请求一次。我们现在可以通过Google Drive上的更新/编辑数量作为我们的限制因素,而不是通过使用网站的人数来进行扩展。根据目前免费账户的Drive使用限制,我们可以支持每分钟300个API请求。缓存应该能使我们保持在这个限度内,而且可以通过批量处理多个请求来进一步优化。
处理图像
同样的缓存方法也适用于嵌入Google Docs的图片。getDoc 方法解析HTML响应中的任何图像URL,并进行二次请求以下载它们(如果它们已经存在,则直接从缓存中获取它们)。然后,它在HTML中重写原始URL。其结果是静态的HTML;我们从不使用谷歌图像CDN的热链接。当它到达浏览器时,图像已经被预先缓存了。
尊重和响应
缓存确保了两件事:第一,我们尊重谷歌的API使用限制,并真正利用谷歌硬盘作为编辑和文件管理的前端(该工具的目的),而不是从它那里获取免费的带宽和存储空间。它使我们的网站与谷歌的API的互动保持在必要时刷新缓存的最低限度。
另一个好处是我们网站的用户会享受到的:一个响应性强、加载时间短的网站。由于缓存的Google Docs以静态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);
请看配套的 repo 中的 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
如果你的网站是面向公众的(互联网上的任何人都可以访问),那么我们就完成了!但对于我工作的地方来说,我们的目的是要把谷歌的OAuth作为一个工具。但对于我在摩托罗拉工作的地方来说,我们正在发布一个只针对内部的文档网站,需要额外的安全性。这意味着我们所有的谷歌文档都关闭了链接共享(它们也碰巧被存储在一个与所有其他公司内容隔离的专用谷歌团队驱动器中)。
我们尽可能早地在_服务器层面_上处理了这一额外的安全层,使用NGINX拦截并反向代理所有请求,甚至在它们到达Express服务器或网站托管的任何静态内容之前。为此,我们使用Cloudflare优秀的Docker镜像,向访问任何网站资源或端点(包括Drive API Express服务器和旁边的静态内容)的所有员工展示一个Google登录屏幕。它与企业的谷歌账户和他们已经拥有的单点登录无缝整合--不需要额外的账户!
结论
我们在这篇文章中所涉及的一切,正是我们在我工作的地方所做的。这是一个轻量级的、灵活的、分散的内容管理架构,其中原始数据存在于我们团队已经工作的Google Drive中,使用的是大家已经熟悉的用户界面。这一切都被捆绑在网站的前端,在对表现形式的控制方面,保留了纯HTML和CSS的全部灵活性,并且具有最小的架构限制。作为开发者的你只需做一点额外的工作,就能为你的非开发者合作者和你的终端用户创造一个几乎无缝的体验。
这种东西对每个人都有效吗?当然不是。不同的网站有不同的需求。但是,如果我把什么时候使用Google Drive作为CMS的用例列表放在一起,它看起来会是这样的:
- 一个每天有几百到几千个用户的内部网站--如果这是全球公司网站的首页,即使每个用户对文件版本元数据的单一请求也可能接近Drive API的使用限制。进一步的技术可以帮助缓解这种情况--但这是最适合中小型网站的。
- 单页面应用程序--这种设置使我们能够在单个REST请求中查询每个数据源的版本号,_每个会话_一次,而不是_每个页面_一次。一个非单页的应用程序可以使用同样的方法,也许甚至可以利用cookies或本地存储来完成同样的 "每次访问一次 "的版本查询,但同样,这将需要一点额外的工作。
- 一个已经在使用Google Drive的团队--也许最重要的是,我们的合作者惊喜地发现,他们可以使用一个他们已经可以访问并能自如使用的账户和工作流程来为网站做贡献,包括Google的所见即所得体验的所有改进,强大的访问管理,以及其他方面。