使用 Goodreads API 和 11ty 创建在线书架
简介
最近,在完全迷上戴夫·鲁珀特 (Dave Rupert) 的 YouTube 缩略图(在 Twitter 上)实验之后,我发现了他的书架,我真的很喜欢!
作为一名读者(我的日常工作是在公共图书馆),我使用 Goodreads 来追踪我读完的书籍并快速评分。所以,我想,如果 Goodreads 有一个公共 API,我可以用它来练习在我的静态、由eleventy支持的网站上获取和显示数据 👍。
入门
由于我计划将其作为我网站上的公共页面(已经是一个 git 项目),所以我不需要创建新的项目目录或使用 git 对其进行初始化/初始化。
相反,我在 git 上创建了一个新分支- 通过输入:
git checkout -b bookshelf
此命令是简写形式,将创建并检出新分支(bookshelf
这是我为该分支指定的名称)。它与以下两个命令相同:
git branch bookshelf
git checkout bookshelf
这样,我就可以开始在新分支上工作,并且可以提交和推送更改,而不会直接影响我的实时网站。
我的网站最初是一个 JavaScript Node.js项目,它使用npm作为其包管理器。
API
首先,我发现 Goodreads 确实有 API,所以我查看了文档,发现我可能需要reviews.list方法。这个方法的作用是“获取会员书架上的书籍”。
为此,我还需要从 Goodreads 获取 API 密钥。作为会员,我只需登录网站并申请密钥即可。
保密 API 密钥
我还知道,在生产代码中,API 密钥最好保密。这样可以防止它们被发现和滥用——Goodreads 密钥不太可能被滥用,因为 API 是免费服务。但最好还是遵循最佳实践,养成正确的使用习惯。
保密 API 密钥的一种方法是使用一个.env
配置为被 Git 忽略的文件。为此,我安装了dotenv 包.env
,并将我的 API 密钥以键/值格式放入文件中:
// My .env file format:
GRKEY='API-Key-goes-here'
为了确保该文件被 Git 忽略,我在我的.gitignore
文件中包含了对它的引用,如下所示:
// My .gitignore file format:
node_modules
dist
.env
...
dotenv 包的简介是这样的:
Dotenv 是一个零依赖模块,它将环境变量从
.env
文件加载到process.env
.
这意味着我现在可以GRKEY
通过引用来访问我的项目中的process.env.GRKEY
。
我认为,您还必须使用require
模块并.config()
在要访问它的文件中调用该方法,如下所示:
const dotenv = require('dotenv');
dotenv.config();
向 API 发出请求
此时,我想向 API 发出一个 HTTP 请求,并确认它返回了我需要的书架信息。我之前用过node-fetch 包发出过 HTTP 请求,所以这次又用了一次。本质上,这个包将fetch Web API的功能引入到了 Node.js 中。
我使用的静态网站生成器 eleventy 拥有强大的设置,可以处理类似这样的 API 调用获取的数据。eleventy文档中提供了更多关于如何在 eleventy 项目中处理数据的信息。
通过阅读这些文档,我知道我需要创建一个文件,用于在_data
文件夹中进行 API 调用,并且需要使用该文件module.exports
来使数据可供网站的其他文件使用。我创建了文件:_data/bookshelf.js
并进行了 API 调用,并使用console.log
来查看响应。如下所示:
module.exports = async function() {
await fetch(`https://www.goodreads.com/review/list?v=2&id=${id}&shelf=read&key=${key}`)
.then(res => res.json())
.then(result => { console.log(result) };
}
对于 URL,您可以看到我使用了模板字面量并包含了三个查询。id
查询和查询是动态值(它们在此函数key
上方声明)。module.exports
这id
是我的 Goodreads ID,相当于我的 Goodreads 帐户的唯一标识符——我登录 Goodreads 帐户后,点击菜单中的“我的图书”,然后查看 URL 即可获得此 ID。例如,我现在的 URL 如下所示:
https://www.goodreads.com/review/list/41056081
最后一部分是我的 Goodreads ID。
这key
是指我的 API 密钥。
第三个查询是shelf
我设置为的read
,因为我只想返回我已经读过的书籍,而不是那些在我的“DNF”(未读完 - 遗憾)或“TBR”(待读......)书架上的书籍。
现在,当我运行 eleventy build 命令来运行代码并查看结果时,结果却与我预期的不一样。日志里有一个错误!我现在记不清具体是什么错误了,但我能看出,是.json()
我将结果解析为 JSON 对象时调用的那个方法导致了问题。
在谷歌上查了一下,我发现 Goodreads API 的响应格式不是 JSON,而是 XML。这时,我还找到了Tara 的帖子,关于如何使用 Goodreads API 选择下一本要读的书,我很高兴找到了这篇帖子,因为它真的帮了我大忙!Tara 的 HTTP 请求和我的略有不同,因为她使用了request-promise 包。
读了 Tara 的帖子后,我知道 Goodreads API 会返回 XML,而且我还了解到可以使用xml2js 包将 XML 响应转换为 json!🎉
安装并包含 xml2js 后,我编辑了我的bookshelf.js
文件:
module.exports = async function() {
await fetch(`https://www.goodreads.com/review/list?v=2&id=${id}&shelf=read&key=${key}`)
.then(res => res.text())
.then(body => {
xml2js.parseString(body, function (err, res) {
if (err) console.log(err);
console.log(body);
};
}
当我再次运行 eleventy build 命令运行代码时,我没有看到任何错误,而是一个看起来相当复杂的对象!完美。
访问和返回数据
从那里我可以访问数据,用for
循环对其进行迭代,将书架所需的部分分配给另一个对象,然后将该对象推送到我将返回的数组上。
通过返回对象数组,我可以使这些数据可供我的其他项目文件使用。
通过更多的 API 调用和console.log
s 计算出数据的结构后,我的module.exports
内部bookshelf.js
最终看起来像这样:
module.exports = async function() {
let books = [];
await fetch(`https://www.goodreads.com/review/list?v=2&id=${id}&shelf=read&key=${key}`)
.then(res => res.text())
.then(body => {
xml2js.parseString(body, function (err, res) {
if (err) console.log(err);
console.log('Getting Book List from GoodReads API');
let bookList = res.GoodreadsResponse.reviews[0].review;
for (let i=0; i < bookList.length; i++) {
books.push({
title: bookList[i].book[0].title[0],
author: bookList[i].book[0].authors[0].author[0].name[0],
isbn: bookList[i].book[0].isbn[0],
image_url: bookList[i].book[0].image_url[0],
small_image_url: bookList[i].book[0].image_url[0],
large_image_url: bookList[i].book[0].large_image_url[0],
link: bookList[i].book[0].link[0],
date_started: bookList[i].date_added[0],
date_finished: bookList[i].read_at[0],
rating: bookList[i].rating[0]
})
}
})
}).catch(err => console.log(err));
return books;
}
现在再次查看它,我认为我进行了两次错误检查,这可能没有必要🤦♂️。
这段代码的结果是,我现在可以访问一个全局数据数组:books
,它包含我在 Goodreads“已读”书架上的每本书,每个书都是一个对象,包含书名、作者和其他有用信息。我现在拥有的数据示例如下:
[
{
title: 'Modern Web Development on the JAMstack',
author: 'Mathias Biilmann',
isbn: ,
image_url: ,
small_image_url: ,
large_image_url: ,
link: 'https://www.goodreads.com/book/show/50010660-modern-web-development-on-the-jamstack',
date_started: 'April 28 2020',
date_finished: 'May 02 2020',
rating: '5'
},
{
// Another book
},
{
// Another book
},
...
]
整理数据
您可能从该示例中注意到,“JAMstack 上的现代 Web 开发”条目没有 isbn 或任何图像。数据很少是完美的,无论您从哪里获取数据,都可能存在一些缺失或异常。
在这个例子中,这本书是一本在线出版的书,因此没有 ISBN 号。这也意味着,尽管 Goodreads 在其网站上使用了封面图片,但由于某种原因,他们无法通过 API 提供该图片。
我的数据里大约有 20 本书,其中 3、4 本书就是这种情况。有些书有 ISBN,但没有图片。
我查看了其他可用的书籍封面 API,发现了一些:
我暗自怀疑亚马逊可能是图像质量最好的选择。然而,为了保持项目简洁,也为了更符合我的口味,我尝试使用 Library Thing API,但似乎没用😭。
此时,我想让书架尽快上线运行,因此我没有配置新的 API,而是决定将 Goodreads API 未自动返回的书籍封面图片托管到我自己的网站上。这对我来说很实用,因为只有当我读完一本书并将其添加到书架后,网站才会更新(这样我就可以随时确认图片是否已上传,如果没有,就添加一张)。
为了添加那些尚未上传的图片,我需要确定一个易于引用的命名约定。我决定用“spinal-case”命名我的图片。为了能够引用它们,我需要在每次 API 调用创建的对象中添加最后一个项目——spinal-case 的标题。
例如,为了能够引用“JAMstack 上的现代 Web 开发”保存的图像,我需要对象包含一个名为“spinal_title”的字段,其值为“modern-web-development-on-the-jamstack”。为此,我添加了以下函数bookshelf.js
:
function spinalCase(str) {
str = str.replace(/:/g,'');
return str
.split(/\s|_|(?=[A-Z])/)
.join("-")
.toLowerCase();
}
此函数还会删除所有冒号 (':')。
然后在 API 调用本身的对象中我还可以添加以下字段:
spinal_title: spinalCase(bookList[i].book[0].title[0]),
这引用了书名,但调用了spinalCase()
函数,以便以脊柱大小写返回书名。
对于这个个人项目来说,这种方法确实有效,但我认为需要根据项目情况找到其他解决方案。例如,在上面的例子中,我的spinalCase()
函数实际上返回的是...on-the-j-a-mstack
,所以我实际上必须重命名文件才能正确匹配。
在网站上显示数据
我不会过多地介绍模板系统的工作原理。css -tricks 上有一篇很棒的文章介绍了 nunjucks,这是我在这里使用的模板语言。Eleventy(无可挑剔!)也是一个很棒的静态网站生成器,因为你可以使用任何模板语言,正如我提到的,我使用的是nunjucks。
bookshelf.js
以下代码引用了从数组返回的数据bookshelf
,并按照模板中的指定方式迭代显示每个项目。为此,我使用了 nunjucksfor i in item
循环,{% for book in bookshelf %}
这样我就可以将每个对象引用为book
。
<div class="wrapper">
<ul class="auto-grid">
{% for book in bookshelf %}
<li>
<div class="book">
{% if '/nophoto/' in book.image_url %}
<img class="book-cover" src="/images/book-covers/{{ book.spinal_title }}.jpg" alt={{book.title}}>
{% else %}
<img class="book-cover" src={{book.image_url}} alt={{book.title}}>
{% endif %}
<p class="font-serif text-300 gap-top-300 low-line-height">{{book.title}}</h2>
<p class="text-300">{{book.author}}</p>
<p class="text-300">
{% for i in range(0, book.rating) %}
⭐
{% endfor %}
</p>
<p class="text-300 gap-bottom-base"><a href={{book.link}}>On Goodreads↗ </a></p>
</div>
</li>
{% endfor %}
</ul>
</div>
正如您所见,它与 HTML 非常相似,但拥有使用逻辑和引用数据的功能。这些逻辑和数据在构建时进行处理,并生成 HTML 页面用于构建网站。
一个有趣的部分是我如何渲染星级评分。Nunjucks 功能非常强大,你可以使用很多不同的技巧。在本例中,我使用了range 函数。
{% for i in range(0, 5) -%}
{{ i }},
{%- endfor %}
// 12345
// In my own case, where book.rating == 4:
{% for i in range(0, book.rating) %}
⭐
{% endfor %}
// ⭐⭐⭐⭐
合并分支并推送至实时站点
为了完成这个项目,我需要将这个分支bookshelf
与master
git 中的分支合并。我通过 GitHub 网站完成了这项工作。
在终端中运行我的最终提交和推送后,我转到GitHub 上的项目,在那里我创建了一个 Pull Request 以便能够合并这两个分支。
最后一件事
不过,在此之前,我还有一件事要做。我的网站是由Netlify构建和托管的。如果你还记得,我一直保密 API 密钥,所以 git 忽略了它,那么你可能还会看到,当网站文件合并并且 Netlify 尝试构建网站时,它将无法访问 API 密钥。
幸运的是,Netlify 提供了一种直接在仪表盘中添加环境变量的方法。所以我可以在这里添加 API 密钥,密钥会保密,但在网站构建期间可以访问。
成品和后续步骤
您可以在我的个人网站的书架页面上查看结果。我很想听听您的想法。
与所有项目一样,我认为这可以改进,并且我可能会寻找方法来尽快更新它,或者如果我收到看到它的人的任何反馈。
我想到的一个想法是,配置一下网站,让它每次在 Goodreads 的“阅读”书架上添加书籍时,无需我自己输入,就进行重建。要做到这一点,我可能需要在 Netlify 中添加一个构建钩子。
结尾
这篇文章比我预想的要长,但我想从 API 获取数据并在其他地方使用或显示它需要做很多工作。如果您读完了这篇文章,非常感谢!请告诉我您的想法。
我决定做这个项目是为了更多地了解 API 调用和数据显示,我想我已经实现了这个目标。Web 开发嘛,总有东西要学!
鏂囩珷鏉ユ簮锛�https://dev.to/zgparsons/using-the-goodreads-api-and-11ty-to-create-an-online-bookshelf-han