使用 Goodreads API 和 11ty 创建在线书架

2025-06-10

使用 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.logs 计算出数据的结构后,我的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 %}

// ⭐⭐⭐⭐

合并分支并推送至实时站点

为了完成这个项目,我需要将这个分支bookshelfmastergit 中的分支合并。我通过 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
PREV
设计一个表来保存数据库中的历史变化 1. 使用生效日期和生效日期字段 2. 使用历史表 3. 使用审计表 结论
NEXT
如何在 WordPress 主题中添加 React