Jamestack-之书-全-
Jamestack 之书(全)
原文:The Jamstack Book
译者:飞龙
前置内容
前言
为布莱恩和雷蒙德关于 Jamstack 的新书撰写前言对我来说是一件非常愉快的事情。布莱恩和雷蒙德都一直是这个在过去 5 到 10 年间改变现代 Web 面貌的动态运动的一部分。
我第一次遇见布莱恩是在 2015 年初,当时他在旧金山的 HTML 5 meet up 上谈论静态网站引擎。那时正值 Netlify 的早期阶段,产品还在私人测试版,而且在我甚至还没有提出“Jamstack”这个术语之前,正是在这样一个时刻,整个行业中只有少数早期采用者开始相信,如果我们接受将 Web UI 与后端基础设施和业务逻辑解耦的想法,Web 可以变得更简单、更快、更安全,并且更适合开发。
与布莱恩、雷蒙德以及许多其他在 SaaS 应用、无头 CMS、实时 Web 数据库、Web 上的交互式体验等领域工作的早期采用者会面和交谈,帮助我的合著者和我有信心认为即将发生一场广泛的、行业范围内的变革,我们需要为其命名并围绕它建立一套术语。
有一天晚上,在与一个朋友的交谈中,我想出了“Jamstack”这个术语,正如人们所说,其余的就是历史了。我们开始在我们联系的人以及当时已经开始围绕 Netlify 形成的社区中传播这个术语——布莱恩和雷蒙德也在其中——这个术语开始传播开来。
今天,Jamstack 生态系统处于一个充满潜力和紧张关系的有趣点。一方面,毫无疑问,Jamstack 架构已经使现代 Web 变得更好:我们看到了大量平台、框架、API、Web 数据库、内容和商业平台在这个类别中涌现并成长,而 Web 现在是开发的首选平台。另一方面,关于简单性和预先准备好的前端的一些初始原则正受到基于按需边缘渲染层和混合构建工具的不同方法的挑战,在这些方法中,开发者有时不得不逐页导航不同的渲染模型。
我一直知道布莱恩和雷蒙德是好奇心旺盛的人,他们总是在寻找简单且易于使用的工具链,这些工具链紧贴 Web 的基本原理。这本书在很大程度上是一本关于 Jamstack 的实践指南,你可以跟随这两位经验丰富的 Jamstack 开发者游览当今 Jamstack 景观中遇到的不同构建工具和网站生成器,学习如何提高生产力,并在不同项目中选择适合自己的不同方法。
网络工具和框架的格局始终在变化和演变,没有工具会适合你遇到的每一个问题。了解每个工具带来的优势和权衡,以及培养对每个工具感觉的良好直觉,将帮助你更好地评估和导航快速发展的 Jamstack 生态系统,因为现有框架在变化和创新,以及新的工具链出现。
——马蒂亚斯·比伊尔曼·克里斯蒂安森
净地(Netlify)的首席执行官和联合创始人
前言
布莱恩和我都很幸运,多年来一直从事网页开发行业。我们见证了好的方面(永恒的浏览器!),坏的方面(你将在一两年后获得那些新功能!),以及更糟糕的方面(表格布局完全合理)。作为有些经验的开发者(以及有些白发),我们对 Jamstack(最初被称为静态站点)的引入感到非常兴奋。从许多方面来看,它是对我们在职业生涯开始时热爱的构建网络的一种现代诠释——一种基于简单文件,我们在文本编辑器中精心手工制作的文件。但 Jamstack 通过利用现代开发框架的最佳特性并采用现代最佳实践,也变得更加实用和合理。
作为 Jamstack 的支持者,我们都认为,现在是开发者参与的最佳时机。尽管 Jamstack 已经存在了几年,但作为一个整体,它仍然非常新,与之相关的工具和技术现在才刚刚成熟并获得主流采用。我们都认为,Jamstack 是一个构建网站的强大框架,可能对大多数开发者都很有用。话虽如此,同时也迫切需要一本书,既能向读者介绍 Jamstack,又能展示多个使用它的示例。
由于我们都是不同的人,有不同的观点,你在这里会看到讨论的不同工具,这可能会让你对有抱负的 Jamstack 开发者可用的选项范围有所认识。我们都认为,几乎总是有办法用 Jamstack 解决问题;关键是要找到最适合你和你团队的解决方案。在阅读这本书的例子时,请尝试关注正在解决的问题,如果你对“如何”并不满意,那么对总是有更好的替代方案可供选择这一事实感到满意。
——雷蒙德·卡姆登
致谢
这本书,就像任何好的(且来之不易)的项目一样,是许多人的共同努力成果。首先,我们都要感谢我们尊敬的(并且非常耐心)的编辑,凯蒂·约翰逊。感谢你抽出时间解释流程,以及忍受我们的(完全合理!)借口。我们也感谢曼宁出版社所有帮助出版这本书的人的辛勤工作。
罗纳德想感谢布莱恩同意与他合作,尽管他的直觉告诉他不然。下一本书将会更有趣,对吧?
布莱恩想感谢罗纳德推动他写这本书,即使他在写作过程中抱怨了很多,他也依然心存感激。
我们俩都想感谢所有在 Jamstack 社区中工作的人,他们为这本书构建的工具或撰写了文章。没有伟大的社区支持,任何技术都无法成功,Jamstack 肯定有这一点。
致所有审稿人:亚历克斯·卢卡斯、阿米特·兰巴、安舒曼·普鲁希特、巴斯卡尔·拉奥·丹德拉穆迪、C.丹尼尔·蔡斯、凯西·伯内特、康纳·雷德蒙德、大卫·卡布雷罗、法布里斯·古埃达尔、弗朗斯·奥伊林基、乔治·托马斯、贾森·格雷茨、约翰·麦克纽、乔纳森·库克、马里奥·鲁伊斯、马泰·斯特拉谢克、纳文·库马尔·纳马奇瓦亚姆、罗德尼·韦斯、萨钦·辛吉、萨蒂杰·萨胡、斯科特·斯特罗兹、塞尔吉奥·阿尔贝奥、希克·乌德曼·阿里·M、史蒂夫·阿尔贝斯、西奥·德索皮迪斯、特里斯坦·V.戈麦斯和佐希布·艾纳波雷,你们的建议帮助使这本书变得更好。
关于本书
《Jamstack 书籍》 的编写旨在帮助读者理解与 Jamstack 一起工作的真正含义,同时提供多个实际应用案例。它首先通过几个不同示例,展示了使用流行的静态站点生成器构建常见网站架构的方法。然后,我们探讨了 Jamstack 生态系统中的其他部分,一旦开发者开始使用 Jamstack 工具创建实际项目,这些部分将变得非常有用。
适合阅读本书的人群
《Jamstack 书籍》 是面向希望采用或至少考虑使用 Jamstack 方法的 Web 开发者的。读者应该对网络基础知识(HTML、JavaScript 和 CSS)有基本的了解,但不需要在特定方面成为专家。本书将通过提供多个示例,如博客和文档站点,帮助巩固使用 Jamstack 的许多好处。
本书组织结构:路线图
本书共有 10 个独立章节:
-
第一章解释了 Jamstack 的确切含义以及为什么开发者应该考虑它。
-
第二章介绍了 Eleventy,并演示了一个非常简单的宣传册网站。
-
第三章介绍了 Jekyll,并指导你构建一个博客。
-
第四章使用了 Hugo 静态站点生成器,并解释了如何构建文档站点。
-
第五章展示了使用 Jamstack 进行电子商务,并使用了 Next.js。
-
第六章解释了如何将 Jamstack 站点移入生产环境。
-
第七章展示了如何将动态元素添加回静态网页。
-
第八章介绍了无服务器计算,重点关注它是如何补充 Jamstack 的。
-
第九章讨论了内容管理系统(CMS)如何与 Jamstack 集成。
-
第十章通过探讨如何迁移到 Jamstack 来结束全书。
开发者可以选择从头到尾直接阅读这本书;然而,第二章到第五章作为使用 Jamstack 可以构建的不同类型站点的示例,可以按任何顺序阅读。
关于代码
本书使用的工具的安装说明如下:
-
第二章介绍了 Eleventy 的安装。最新的安装说明可以在
www.11ty.dev/docs/getting-started/找到。 -
第三章介绍了 Jekyll 的安装。最新的安装说明可以在
jekyllrb.com/docs/installation/找到。 -
第四章介绍了 Hugo 的安装。最新的安装说明可以在
gohugo.io/getting-started/installing/找到。 -
第五章介绍了 Next.js 的安装。最新的安装说明可以在
nextjs.org/docs/getting-started找到。
本书包含许多源代码示例,无论是编号列表还是与普通文本并列。在这两种情况下,源代码都使用固定宽度字体格式化,如这样,以将其与普通文本区分开来。有时代码也会加粗,以突出显示与章节中先前步骤相比已更改的代码,例如当新功能添加到现有代码行时。
在许多情况下,原始源代码已经被重新格式化;我们添加了换行并重新调整了缩进,以适应书中的可用页面空间。在极少数情况下,即使这样也不够,列表中还包括了行续续标记(➥)。此外,当代码在文本中描述时,源代码中的注释通常也会从列表中移除。许多列表旁边都有代码注释,突出显示重要概念。
您可以从本书的 liveBook(在线)版本中获取可执行的代码片段livebook.manning.com/book/the-jamstack-book。本书的完整代码可以从 GitHub 仓库github.com/cfjedimaster/the-jamstack-book下载。
liveBook 讨论论坛
购买The Jamstack Book可获得对 liveBook(Manning 的在线阅读平台)的免费访问。使用 liveBook 的独特讨论功能,您可以在全局或特定部分或段落中添加评论。为自己做笔记、提问和回答技术问题以及从作者和其他用户那里获得帮助都非常简单。要访问论坛,请访问livebook.manning.com/book/the-jamstack-book/discussion。您还可以在livebook.manning.com/discussion了解更多关于 Manning 论坛和行为准则的信息。
Manning 对读者的承诺是提供一个平台,让读者之间以及读者与作者之间可以进行有意义的对话。这并不是对作者参与特定数量活动的承诺,作者在论坛上的贡献是自愿的(且未付费)。我们建议您尝试向作者提出一些挑战性的问题,以免他们的兴趣转移!只要这本书还在印刷中,论坛和之前的讨论存档将可通过出版社的网站访问。
其他在线资源
有多个地方可以让人们了解更多关于 Jamstack 的信息,并跟上最新的变化。以下是一些可以考虑的资源列表:
-
Jamstack.org 是一个关于 Jamstack 的优秀高级网站,提供了许多指向其他资源的链接。您还可以加入它的 Discord 频道。
-
The New Dynamic (
www.tnd.dev/) 是另一个拥有自己 Slack 的优秀“元”资源。 -
JAMstacked (
jamstack.email/) 是由 Brian 精选的每周时事通讯,其中包含关于 Jamstack 博客、事件等最新消息。
关于作者

Raymond Camden 是 Adobe 的高级开发者传教士。他致力于开发强大的(通常是猫相关的)PDF 演示文稿。他是多本关于网络开发的书籍的作者,并且已经活跃地博客和演讲了近 20 年。Raymond 可以通过他的博客(www.raymondcamden.com)、Twitter 上的 @raymondcamden 或通过电子邮件 raymondcamden@gmail.com 联系。他已经结婚,有八个孩子(是的,您没看错)和多个毛茸茸的生物。

Brian Rinaldi 是 LaunchDarkly 的开发者体验工程师,拥有超过 20 年的网络开发经验。Brian 积极参与社区,通过 CFE.dev 和 Orlando Devs 举办开发者聚会。他还是 JAMstacked 时事通讯的编辑,这是一份双周的专注于 Jamstack 的时事通讯。
关于封面插图
《The Jamstack Book》封面上的插图是“Femme des Environs de Rome”,或“罗马周边的女性”,取自 Jacques Grasset de Saint-Sauveur 的作品集,该作品集于 1797 年在法国出版。每一幅插图都是手工精心绘制和着色的。
在那些日子里,人们通过他们的服饰很容易就能识别出他们住在哪里,以及他们的职业或社会地位。Manning 通过基于几个世纪前丰富的地方文化多样性的封面设计,以及像这样的收藏品中的图片,庆祝当今计算机行业的创新精神和主动性。
1 为什么选择 Jamstack?
本章涵盖
-
将 Jamstack 定义为网络应用的架构,而不是一种规定性的技术堆栈
-
Jamstack 是如何形成的,以应对变得笨拙、缓慢和不安全的动态网页开发
-
Jamstack 的好处,包括页面速度、安全性和成本
-
探索使用 Jamstack 构建的知名网站
随着近年来 Jamstack 的普及,对其的一种常见批评是它只是一个营销术语。事实是,他们是对的。正如我们将要探讨的,Jamstack 是一个被发明出来的术语,目的是为了“重新命名”许多开发者已经使用来构建网站的架构,因为现有的术语已经变得具有误导性。虽然称其为营销可能是一种公平的批评,但 Jamstack 仍然是一种正在迅速被网络开发者采用的构建网站的方式。
1.1 什么是 Jamstack?
定义 Jamstack 并非易事。没有 Jamstack 安装程序。没有一组预定义的工具你应该安装来构成 Jamstack。甚至没有与开发 Jamstack 应用程序相关的特定语言。(是的,JavaScript 扮演着核心角色,但也可以涉及任何数量的语言,包括 Ruby、Go、Python 或其他。)最终,有无数的工具和语言组合可以结合起来创建一个可以合法称为 Jamstack 的网站。
实际上,Jamstack 更像是一种创建网站的架构模式或方法论。尽管对此有很多持续的争论,但以下是我们定义的关键要素:
-
Jamstack 网站主要基于静态资源构建。Jamstack 网站始终以静态文件的形式部署。这意味着当用户请求页面时,它们不是由应用服务器动态生成的;相反,网站文件是在构建时生成的。对于 Jamstack 网站,浏览器中请求特定页面的每个用户都会收到相同的静态资源。然而,这并不意味着内容是静态的。事实上,现代 Jamstack 网站提供了多种页面内容的渲染选项,包括完全静态和服务器端渲染。
-
Jamstack 网站使用静态站点生成器构建。Jamstack 网站中的静态资源是通过静态站点生成器(SSG)生成的。在非常基础的层面上,一个 SSG 是一个工具,它将模板与内容结合起来。内容可以存储为 Markdown、YAML 或 JSON 文件,或者从 API 中提取。内容和模板结合在一起,动态生成网站的 HTML、CSS 和 JavaScript 资产。这与动态网络服务器(如 PHP)在每次用户请求时可能经历的过程类似,但,相反,这个过程的大部分是在网站部署之前在构建时发生的。
-
Jamstack 网站利用 API。区分 Jamstack 网站和简单静态网站的不同之处在于,尽管它由静态资源组成,但它可以非常动态。创建这种动态功能的第一关键要素是使用 API。这些 API 可以在运行时由浏览器客户端调用,甚至可以在构建时由静态网站生成器调用。
-
Jamstack 网站使用 JavaScript 实现动态功能。使 Jamstack 网站动态化的第二个关键要素是其能够在客户端通过 JavaScript 异步调用 API 和其他服务。JavaScript 允许静态资源通过文档对象模型 (DOM) 操作动态更改。客户端 JavaScript 驱动着诸如用户登录或购物车等功能。
显然,这个定义有很多灵活性,在我看来,这也是 Jamstack 吸引力的一部分。几乎肯定有一组 Jamstack 工具和服务可以满足你的项目需求以及你的语言、工具和部署偏好。
然而,这种灵活性是有代价的。没有一种单一的方法可以教授某人 Jamstack,众多选项可能会让新手的学习曲线变得有些陡峭。此外,在创建一个利用各种 API 和服务并使用 JavaScript 在客户端动态更新内容的网站时,可能存在额外的复杂性。
那么为什么选择 Jamstack 呢?Jamstack 的发展部分是为了解决许多人都觉得动态网页变得缓慢、昂贵且不安全的这些问题。为了更好地理解对 Jamstack 的需求,我们需要了解它是如何以及为什么演变的。
1.2 Jamstack 的简要历史
通过了解“Jamstack”这个术语最初是如何被创造出来的,我们可以更好地理解它是什么以及为什么它迅速获得了人气。这尤其正确,因为虽然 Jamstack 是一种利用许多最新技术趋势的现代架构,但在其他方面,它也回溯到了互联网刚刚发明时我们构建网页的方式。
最早的网页只是简单地部署到网络服务器上的 HTML。例如,如图 1.1 所示的第一个网站只是一个基本的静态网站。每个访问该网站的人都会收到相同的资源。

图 1.1 第一个网站是一个静态网站。它仍然可在 info.cern.ch/hypertext/WWW/TheProject.html 上找到。
随着网络需求的发展,支撑它的技术也发生了变化。Web 应用服务器和服务器端脚本语言,如 PHP 和 Ruby,允许网站动态生成内容。这使得每个用户都能收到在发送到个人浏览器之前在服务器上动态渲染的定制资源。今天,这通常被称为 服务器端渲染 (SSR)。
让我们看看典型的服务器端渲染的 Web 应用在 2008 年是如何工作的(为什么是 2008 年?我稍后会解释):
-
用户会从浏览器请求一个页面。
-
浏览器会击中 Web 应用服务器,该服务器会加载使用某种形式的脚本语言构建的请求页面。
-
脚本语言会调用数据库以获取用户信息、产品信息以及/或内容。
-
数据和脚本会被组合起来生成发送给用户的 HTML。
这个过程会在每个页面请求上重复。它允许从单个脚本文件中提供高度个性化和动态的内容,但这也带来了成本:
-
性能——这个过程中的每一部分都涉及小的性能成本,从应用服务器处理请求到数据库处理查询,再到生成最终的 HTML。由于这个过程在每次页面请求中都会为每个用户重复,成本可能会迅速增加,当 Web 应用服务器或数据库负载过重时,这些成本可能会进一步增加。
-
安全性——这些应用天生就留下了很大的攻击面。这包括像 Web 应用服务器、脚本语言或框架中的漏洞,数据库也可能通过 SQL 注入等直接攻击方法被攻击。
-
可扩展性——由于每个用户的每个请求都需要唯一的响应,因此随着使用量的增长,这些应用可能会变得昂贵且复杂。通常,服务器是在内部维护的,因此扩展意味着需要新的硬件,这意味着应用不能快速或容易地根据需求扩展。
当然,所有这些问题都有合适的资源可以解决,但在当时,网络中有很大一部分已经依赖于 SSR,以至于仅仅在简单的博客上查看文本内容就需要依赖整个服务器渲染架构,比如使用 WordPress,它的采用率正在迅速增长。
1.2.1 静态站点生成器的兴起
2008 年是后来被称为 Jamstack 的关键年份,因为那一年汤姆·普雷斯顿-沃纳发布了 Jekyll。当然,在 Jekyll 之前已经有了静态站点生成器,但 Jekyll 基于以下原则,这些原则帮助推动了大多数现代静态站点生成器的发展:
首先,我所有的写作都会存储在 Git 仓库中。这将确保我可以在舒适的编辑器和命令行中尝试不同的想法,探索各种帖子。我可以通过简单的部署脚本或 post-commit 钩子发布帖子。复杂性将保持在绝对最小,因此静态站点比需要持续维护的动态站点更可取。
—汤姆·普雷斯顿-沃纳(《像黑客一样博客”,mng.bz/ExJO)
Jekyll 得到了广泛的应用,尤其是在作为 WordPress 的替代品用于博客方面。这种采用部分是由 GitHub 推动的,Preston-Werner 是 GitHub 的联合创始人兼首席执行官,当 GitHub Pages 在 2008 年添加了对 Jekyll 的支持时。
GitHub Pages 的支持还引入了一种新的持续开发工作流程,这种工作流程已经成为现代 JAMstack 的普遍做法。开发者不再需要运行本地构建并通过 FTP 推送生成的 HTML、CSS 和 JavaScript,他们只需将更改提交到 GitHub 仓库,Jekyll 构建就会为他们运行和部署。
Jekyll 之后,出现了一长串——我确实是指很长——遵循类似原则的静态站点生成器。截至本文撰写时,最全面的静态站点生成器列表(https://staticsitegenerators.net/)列出了 460 个,几乎使用了所有可用的编程语言(甚至包括 Swift,这是一种用于开发 iOS 原生应用的编程语言,也有静态站点生成器)。这些包括一些至今仍在广泛使用的流行选项,例如 2009 年的 Middleman(与 Jekyll 一样用 Ruby 编写),2010 年的 Pelican(用 Python 编写),以及 2013 年的 Hugo(用 Go 编写)。
1.2.2 从静态站点到 JAMstack
回到 2016 年,Netlify 已经是一家专注于为使用静态站点生成器等工具的开发者提供持续部署的快速成长型初创公司,但“静态站点”这个术语已经变得有问题。越来越多的工具和服务正在使使用静态站点生成器构建的网站具备动态功能。结果证明,静态站点在现实中可能远非静态。
为了解决这个问题,Netlify 的 Matt Biilmann 提出了一个新术语:JAMstack。这伴随着一个新的网站jamstack.org,其中包含了他对新术语的定义宣言。他发布的原始版本如图 1.2 所示。

图 1.2 2016 年推出的原始jamstack.org网站。
JAMstack 中的“JAM”代表以下含义:
-
JavaScript—这是这些网站大部分动态功能的关键。JavaScript 使得从 API 异步加载内容以及客户端上 HTML 的动态更新成为可能。
-
APIs—这些可以是来自第三方提供的现有 API,也可以是执行自定义业务逻辑的云函数。这些 API 为网站提供了所需的数据和业务逻辑。
-
Markup—这包括从包含网站内容的 Markdown 和 YAML/TOML 到静态站点生成器使用的模板语言(Liquid、Handlebars 等)的一切。静态站点生成器对于这一方面至关重要,即使它在 JAM 缩略语中的存在有些模糊——也许是有意为之。
在 Netlify 在生态系统中的影响力,以及该领域其他公司的合作下,希望这个术语能够帮助重新定义这些工具在 Web 开发者社区中的看法。这得到了 2017 年 5 月 JAMstack Conference(由 Netlify 领导的另一个倡议)的巨大帮助,该会议随后在世界各地产生了许多后续活动。
JAMstack 一直被批评为仅仅是一个营销术语。正如我们所看到的,这有一些真实性,但自从这个术语被引入以来,其采用率已经大幅增长,形成了一个以 JAMstack 开发者为目标的公司生态系统。
1.2.3 从 JAMstack 到 Jamstack
有一个最终、看似微小但最终重要的术语变化需要讨论。在 2020 年初,管理 jamstack.org 站点的 Netlify 团队开启了一场关于改变术语书写方式的讨论,从 JAMstack 变为 Jamstack。许多社区成员纷纷发表意见,并做出了改变的决定。截至本文撰写时,大多数公司和组织已经效仿,但这一用法尚未得到统一采用。
值得理解这种变化的背后原因。JAM 这个缩写已经显示出可能成为一些混淆的源头。首先,JavaScript、API 和标记看起来可以描述几乎任何为网络构建的网站;这个缩写并没有清楚地做出区分。其次,以 JavaScript 开头似乎给人一种印象,即 Jamstack 等同于 JavaScript 框架,并且仅基于 JavaScript 框架的静态站点生成工具。最后,JAMstack 实际上不是一个“堆栈”,因为在 LAMP 中并没有预设的工具组。实际上,它更像是一种架构,甚至是一种方法。
希望通过改变术语的字母大小写,可以降低对缩写和“堆栈”的强调,并使这个术语的使用寿命更长,就像人们仍然使用 Ajax 而不是 AJAX,尽管 XML 在很大程度上已经不再相关。从现在起,在这本书中,我们将坚持使用 Jamstack 的大写形式。
1.3 Jamstack 架构的优势
现在我们已经了解了 Jamstack 是如何从简单的静态站点发展到现代的复杂站点构建架构的,让我们来回答这个问题:“为什么你应该选择 Jamstack?”以下是一些关键的好处。
1.3.1 性能
有三个重要的方面需要理解 Jamstack 应用程序的性能:
-
静态资源比动态资源加载更快。不需要进行任何处理来将动态模板转换为 HTML、CSS 和 JavaScript,也不需要在运行时进行数据库调用。所有这些资源都是预渲染的,这是 Jamstack 社区中常用的一个术语,意味着大多数页面渲染是在构建时而不是运行时发生的。
-
Jamstack 网站是从边缘提供的。 由于资产是静态的,它们可以从 CDN 提供服务,这意味着每个最终用户都是从离他们最近的服务器获取网站资产。这结合了当发布新版本时立即缓存失效,以确保用户总是从 CDN 获取网站的最新版本。
-
Jamstack 网站默认可扩展。 当你的网站从 CDN 提供服务时,无需创建额外的服务器来应对高流量负载。此外,Jamstack 网站依赖于像云函数这样的服务,这些服务是为了扩展而构建的,用于动态处理和功能。
1.3.2 安全性
安全是一个难以做出广泛断言的话题,因为没有完全安全的选项。一个正确修补和维护的 WordPress 网站可以是安全的,但现实是,最近的数据显示“73.2% 的最受欢迎的 WordPress 安装存在漏洞” (mng.bz/7WRg)。如图 1.3 所示,成千上万的顶级网站容易受到已知漏洞的攻击。

图 1.3 WordPress 网站可以是安全的,但 WP WhiteSecurity 的数据显示,Alexa 排名前位的网站中,WordPress 安装的版本很多仍然容易受到攻击,因为它们没有更新。然而,Jamstack 网站不需要这些类型的更新,因为资产是静态的 (mng.bz/7WRg)。
这些问题并非仅限于 WordPress。传统网站有很多需要维护和修补的移动部件,无论内容管理系统和应用程序框架如何。例如,对于 WordPress 或 Drupal 网站,这些部件可能包括 PHP 网络服务器和 MySQL 数据库。对于 Django 网站,可能是 Python 和 PostgreSQL。
相比之下,Jamstack 网站受益于攻击面的大幅减少。没有
-
没有需要被破坏的 web 服务器
-
需要利用可能未修补的安全漏洞的 web 应用程序服务器或 web 应用程序框架
-
没有数据库可以访问
-
由于网站是从多个 CDN 提供服务,因此没有中心化的真实来源可以黑客攻击
是的,Jamstack 网站可以依赖第三方服务,这些服务可能容易受到攻击。然而,这也给了 Jamstack 网站利用这些服务领域专业知识的能力。例如,他们可以不必实现自定义身份验证,而是利用像 Auth0 或 Netlify Identity 这样的专门从事身份验证并实施行业最佳实践的服务。此外,作为软件即服务 (SaaS),开发者无需担心修补。
1.3.3 成本
好的,让我们谈谈我对 Jamstack 最喜欢的个人好处:它可以大大降低成本,甚至经常是免费的。由于不需要网络应用服务器和数据库服务器,Jamstack 的托管成本通常可以忽略不计,甚至不存在。Netlify、Vercel 和 Render 等持续部署服务都提供慷慨的免费计划,可以满足许多网站的需求,并且通常根据使用量或额外功能进行定价。一些服务,如 GitHub Pages,提供免费的持续部署和托管(当然,有一些限制)。
许多在 Jamstack 网站中流行的第三方服务也是如此:商业产品提供慷慨的免费层,完全免费或开源的选项也存在,尽管通常有一些限制。例如,像 Algolia 这样的搜索服务或 Sanity 这样的内容管理服务提供免费层,这使得它们对于许多网站来说可以作为免费或低成本选项。同时,像 Lunr 这样的搜索工具和 Netlify CMS 这样的内容管理工具提供免费和开源的替代方案。
服务和托管只是 Jamstack 可以提供潜在节省的两个地方。许多 Jamstack 案例研究引用了降低开发和维护成本。例如,Netlify 最近的一份白皮书提到,Loblaw Digital 单个活动的提前期减少了“从典型的一年减少到一个月,代表着将上市时间缩短了 10 倍,[并且] 每月节省成本 38,000 美元”(www.netlify.com/whitepaper/)。
1.4 当 Jamstack 可能不是最佳选择时
最近,关于何时使用和何时不使用 Jamstack 进行明确推荐变得愈发困难。Jamstack 工具和服务的改进能力使得几乎任何类型的网站都可以通过 Jamstack 方法实现。事实上,像 Next.js、Nuxt 和 Gatsby 这样的工具现在使得创建混合解决方案成为可能,开发者可以选择将一些路由设置为静态,而其他路由则由服务器端渲染。但有时,Jamstack 方法可能并不合适:
-
高度依赖用户生成内容的应用程序——完全可以将用户生成内容作为静态资源构建,或者通过 API 拉取。有一些 Jamstack 网站的例子添加了诸如用户生成评论或写入文件的帖子,这些文件会触发重建或从 API 动态拉取。在用户生成内容是周期性和补充性的情况下,这可能是有意义的,但对于主要专注于用户生成内容的网站,Jamstack 解决方案可能证明过于复杂且难以实施。
-
内容持续更新的应用程序——与用户生成内容的网站类似,一个持续更新内容的网站(例如,实时新闻网站)可能不适合 Jamstack 方法。是的,这些内容可以通过客户端 API 调用或通过 SSR 实时更新,但这可能很难正确实现,并且可能会抵消 Jamstack 的一些整体性能优势。
-
高度依赖服务器端处理的仪表板——一些仪表板应用程序作为 Jamstack 是完全合理的。在许多情况下,这些仪表板调用 API 来填充图表和数据表,这些图表和数据表在客户端处理是有意义的。然而,在其他情况下,这可能会不必要地给客户端带来过重的负担,并且不是最佳解决方案。
如您所见,什么可以成为 Jamstack 应用程序的分界线是模糊的。这些示例中的每一个都可以作为 Jamstack 应用程序来构建。我会考虑的是,与传统的服务器端解决方案相比,Jamstack 解决方案将有多复杂,以及我将在客户端 API 调用或 SSR 与作为静态资产生成的内容之间分配多少工作量。不要仅仅为了使网站符合 Jamstack 模式而过度设计网站,因为你可能会发现解决方案脆弱且难以维护。不要将大部分内容显示工作卸载到客户端 API 调用,因为这可能会抵消 Jamstack 静态、基于 CDN 的方法的一些主要优势。这些都是指导方针,而不是铁的规则,但它们可以帮助你评估 Jamstack 的好处是否超过了你的项目的缺点。对于大多数项目,我确信它们会,但在某些情况下可能不会。
1.5 使用 Jamstack 构建的流行网站
判断一个项目是否是用 Jamstack 构建的可能会很困难。这是因为
-
Jamstack 网站看起来与其动态替代品没有区别。事实上,这个想法是,通过选择 Jamstack,你不会在设计或功能上做出妥协。
-
构建 Jamstack 网络应用的方法不计其数。例如,目前 staticsitegenerators.net 上列出了 460 个静态网站生成器。列出与 Jamstack 网站通常相关联的无头 CMS 系统(内容管理系统)的 Headlesscms.org 目前列出了 74 个选项。Jamstack 网站的部署选项包括 Netlify、Vercel、Render、AWS、Azure 等。Jamstack 网站可能结合了这些工具和服务的任意组合或其他工具和服务。
-
在网站的代码中,往往没有明显的指标表明它是用 Jamstack 构建的。一些流行的工具,如 Gatsby,可以通过它们加载的 JavaScript 来检测;其他如 Hugo 和 Jekyll,可以通过它们的元生成器标签来检测,但还有一些如 Eleventy 则完全无法检测。
尽管如此,成功的网站示例通常有助于帮助论证采用 Jamstack 的理由。以下是一些您可能认识的网站示例,它们都是使用 Jamstack 构建的(截至本文撰写时)。
1.5.1 Smashing Magazine
Smashing Magazine 是一个流行的设计师和开发者网站,它从 WordPress 迁移到了使用 Hugo 静态站点生成器和 Netlify CMS(一个专为 Jamstack 网站设计的流行开源内容管理系统)构建的 Jamstack 网站(见图 1.4)。(您可以在他们的迁移信息中了解更多:mng.bz/NxEX。)

图 1.4 Smashing Magazine 使用 Hugo 静态站点生成器和 Netlify CMS 内容管理系统重新启动了他们的网站。
1.5.2 Nike
Nike 的网站,包括电子商务,使用 Gatsby,一个非常流行的基于 React 的静态站点生成器(见图 1.5)。

图 1.5 Nike.com 使用 Gatsby,一个流行的基于 React 的静态站点生成器,建立了他们的网站。
1.5.3 Impossible Foods
一家流行的素食食品品牌 Impossible Foods 也使用 Gatsby(图 1.6)建立了他们的网站。

图 1.6 Impossible Foods 使用 Gatsby,一个基于 React 的静态站点生成器,建立了他们的网站。
1.5.4 餐饮品牌国际公司(RBI)
餐饮品牌国际公司(RBI),这家公司旗下拥有诸如 Popeyes、Burger King 和 Tim Hortons 等知名餐厅,于 2018 年将他们的数字服务迁移到了 Jamstack。他们在 2019 年的 Jamstack 大会上讨论了这一点。这使得他们能够为所有网站创建一个单一的平台,这些网站在各个品牌属性之间共享组件、内容和数据。此外,驱动他们 Jamstack 网站网站内容的相同内容 API 也可以在移动应用中重用(见图 1.7)。

图 1.7 Popeyes.com 是 RBI 拥有的多个使用 Jamstack 技术重建的网站属性之一。
1.5.5 Digital.gov
Digital.gov 是一个政府网站,为在美国政府提供数字服务的人员提供指导、培训和社区支持。它使用 Hugo,一个流行的基于 Go 的静态站点生成器(图 1.8)。(您可以在他们的案例研究中了解更多信息:gohugo.io/showcase/digitalgov/。)

图 1.8 Digital.gov
1.6 本书您将学到什么
在本章中,我们看到了为什么以及如何将 Jamstack 发展成为一种解决方案,以解决困扰 Web 应用程序开发的问题——从简单的静态网站生成器开始,发展到今天更加完整的应用程序架构。我们现在也了解了一些选择使用 Jamstack 的好处,并知道了一些使用 Jamstack 的流行网站。但如何使用它呢?
Jamstack 的本质意味着对于那个问题的答案并不简单。你可以使用各种工具和服务的组合来创建一个符合 Jamstack 标准的网站。这就是为什么许多关于 Jamstack 的书籍和教程往往倾向于单一解决方案。在这本书中,我们旨在采取不同的方法,并为你提供探索和使用各种 Jamstack 工具和服务的机遇,以解决实际用例。目标不是让你成为每个工具或服务的专家,而是提供你需要的能力,以便能够评估哪些解决方案最适合你的项目需求和你的个人偏好。让我们开始编码吧!
摘要
-
Jamstack 是一种构建网站的架构,它结合了标记、API 和模板,使用静态网站生成器输出静态 HTML、CSS 和 JavaScript 资产。
-
Jamstack 网站利用 API 和 JavaScript 的组合来实现与动态生成版本相当的功能性。
-
虽然“Jamstack”一词最初是为了重新命名“静态网站”而创造的,但这个概念随着对动态网站速度、成本和安全性的担忧而演变。
-
采用 Jamstack 的好处包括性能(由于使用从 CDN 提供的静态资源),安全性(由于没有网站服务器、Web 应用程序服务器、Web 应用程序框架和数据库服务器被利用),以及成本(由于托管静态资源的低成本和诸如无服务器计算之类的增量成本)。
-
虽然 Jamstack 网站可能难以识别,但许多知名公司已经采用 Jamstack 架构进行网站开发。
2 构建基本的 Jamstack 网站
本章涵盖
-
使用 Jamstack 构建简单网站
-
如何安装和使用 Eleventy
-
使用 Eleventy 创建咖啡店网站
我们将在 Jamstack 中构建的第一个网站将是之前所说的宣传册网站。在过去,这被用来描述那些仅仅是营销手册数字版本的网站。虽然网络是一个极其强大的平台,并能够实现强大的协作,但有时简单就是你所需要的。有许多例子表明,一个或两个页面的网站已经足够满意:
-
一个仅显示营业时间、地址和菜单(但请不要是 PDF 格式!)的餐厅网站
-
一个即将推出的服务的临时网站,可能包含一个简单的表单,让人们可以注册在发布时收到通知
-
一个移动应用的着陆页,仅包含指向相应应用商店的链接
这些例子中的每一个都可以很容易地由一个一到两页的网站处理。实际上,静态网站生成器对于这样一个小的项目来说可能甚至有些过度。但正如许多开发者所知,项目往往会随着时间的推移而增长。通过从静态网站生成器开始,你将更好地准备好随着时间的推移适应网站并添加额外的页面和功能。对于我们的第一个静态网站生成器,我们将使用极其灵活的 Eleventy。
2.1 使用 Eleventy
Eleventy(图 2.1)是一个简单但功能强大的静态网站生成器,极其灵活。Eleventy 可以用来构建几乎你能想象到的一切,并不一定针对某一类网站。这种灵活性使其成为学习 Jamstack 的开发者的绝佳首选。它没有关于你将构建什么类型网站的先入之见,几乎允许你开发任何你需要的东西。它丰富的模板语言也意味着你可以使用最适合你写作风格的那个。它还依赖于 npm 进行安装,这是网络开发者中非常常见的工具,这使得设置更加容易。这就是为什么我选择了 Eleventy 作为本书中介绍的第一个静态网站生成器,也是我个人博客背后的引擎,这个博客我已经运营了近 20 年,拥有超过 6000 篇博客文章。你可以在www.11ty.dev/找到 Eleventy 的首页。

图 2.1 Eleventy 网站
要开始使用 Eleventy,你需要通过 npm 安装 CLI 工具。npm代表“Node 包管理器”,是一个常见的安装实用工具的工具。你不需要是 Node 开发者就可以使用 npm,但通过nodejs.org安装 Node 是安装 npm 的最简单方法。假设你在终端中有 npm 可用,你可以这样安装 Eleventy 的 CLI:
npm install -g @11ty/eleventy
你可以通过在终端中输入 eleventy --help 来确认它是否工作,如图 2.2 所示。

图 2.2 Eleventy CLI 的帮助输出
Eleventy CLI 的基本功能可以归结为两个。它可以将目录中的源代码构建成静态网站,或者设置一个可以执行相同操作并允许热重载(即在检测到文件更改后重新加载浏览器)的 Web 服务器。通常,在构建网站时,你会使用 Web 服务器功能,而在只需要网站静态文件输出时执行构建。
Eleventy 允许你使用许多不同的模板处理器来编写内容。这些模板语言允许你在 HTML 中添加基本的逻辑和变量。默认情况下,Eleventy 支持 Markdown、Liquid、Nunjucks、Handlebars、Mustache、EJS、HAML 和 Pug。你还可以使用基本的 HTML 或甚至 JavaScript。你选择的语言取决于你,你甚至可以在 Eleventy 项目中混合使用不同的语言。Eleventy 文档(www.11ty.dev/docs/)不会帮助你学习这些语言,所以如果你不熟悉它们,你需要访问你想要使用的处理器的相应网页。对于本章,我们将使用 Markdown 和 Liquid。选择 Liquid 纯粹是个人决定;如果你不喜欢它,请记住你有其他选择!
2.1.1 创建你的第一个 Eleventy 网站
让我们从创建一个简单的网站来测试 Eleventy 开始。创建一个空文件夹(或克隆书籍仓库)并添加一个新文件,index.md。".md"扩展名代表 Markdown,这是一种在许多不同平台上非常常见的模板语言。你可以在www.markdownguide.org/找到关于 Markdown 的良好入门指南。
列表 2.1 初始 Markdown 文件,index.md
## Hello World
This is my first page in lovely markdown.
Here's a list for fun:
* Cats
* Dogs
* More Cats
现在在同一目录下创建另一个文件,cats.md。
列表 2.2 第二个 Markdown 文件,cats.md
## Cats
Cats are the best, aren't they?
如果不明显,你可以随意更改此文本,使其符合你的需求。保存两个文件后,在终端中输入 eleventy 来执行构建。确保你位于这两个文件所在的同一目录中。Eleventy 会报告其执行的操作以及所需时间。你可以在图 2.3 中看到一个示例。

图 2.3 运行 Eleventy 命令的输出
这里有几个需要注意的重要事项。首先,Eleventy 将其结果输出到名为 _site 的目录中。你可以通过传递给 Eleventy 命令行程序的标志来自定义它。其次,注意 Eleventy 如何转换文件名。第一个文件,index.md,在输出目录的根目录中写为 index.html。第二个文件,cat.md,写入 cats 子目录中。最终结果是,主页可在网站的根目录中访问,而 cats 页面可在 /cats 中访问。Eleventy 允许你完全控制页面的输出方式,因此你可以调整这些选项以满足你的需求。然而,了解默认设置很重要,这样你就可以从一个页面链接到另一个页面。例如,索引页面可以像这样链接到 cats 页面:
Read about our cats
你可以启动一个 Web 服务器(我的首选是 httpster,通过 npm 安装)来查看输出,但让我们使用 Eleventy CLI 的另一个主要功能并启动开发服务器。在你的终端中运行 eleventy --serve。你应该会看到类似于图 2.4 的输出。

图 2.4 运行 Eleventy 服务器启动了一个用于网站文件的 Web 服务器,并允许你在浏览器中验证输出。
如果你复制 CLI 输出的本地 URL,你可以在浏览器中打开它并查看你的网站。你需要按 CTRL+C 来停止服务器。此外,请注意 Eleventy 正在写入 _site,这意味着当你完成开发后,你不需要再次运行 eleventy 来生成最终结果。如果你完全按照指定的方式创建了你的 Markdown 文件,你将在浏览器中看到图 2.5 描述的内容。

图 2.5 由 Eleventy 处理的 Markdown 文件生成的 HTML 输出
如果你编辑文件并重新加载浏览器,你会看到变化。Eleventy 支持热重载,这意味着你不需要手动重新加载浏览器,但该功能需要一个包含 body 标签的“完整”HTML 页面。当我们到达布局时,你会看到如何添加它。
2.1.2 使用模板语言
到目前为止,你所看到的是 Markdown 的转换,这本身并不那么令人兴奋。如果我们加入模板语言会怎样呢?模板语言在构建 HTML(和其他)文件时给我们提供了更多的灵活性。它们允许你使用变量、条件和循环。让我们构建一个示例,以便你可以看到它的实际应用。你会看到如何在 HTML 文件中添加逻辑和编程语句,以便 Eleventy 可以处理它们并将它们转换为静态文件。创建一个新的目录(或使用 GitHub 仓库中的 chapter2/demo2 文件夹),并将以下内容添加为 index.liquid。
列表 2.3 Liquid 模板语言的示例
<h2>Liquid Demo</h2>
{% assign name = "ray" %}
<p>
Hello, {{ name }}!
</p>
{% assign cool = true %}
{% if cool %}
<p>
Yes, you are cool.
</p>
{% endif %}
{% assign cats = "Fluffy,Muffy,Duffy" | split: ',' %}
<ul>
{% for cat in cats %}
<li>{{ cat }}</li>
{% endfor %}
</ul>
Liquid (shopify.github.io/liquid/) 是一个开源模板语言,被许多项目使用,包括我们将在本书后面讨论的另一个静态站点生成器 Jekyll。在列表 2.3 中,您可以看到 Liquid 使用的一些语法。第一行(忽略 HTML)将值 ("ray") 赋给一个变量(name)。赋值之后,变量立即输出。Liquid 使用 {% ... %} 标记来包裹命令,并使用 {{ ... }} 来包裹变量。基本上,如果您正在应用任何类型的逻辑,请使用 {%; 否则使用 {{。
下一段代码将布尔值(true)赋给一个变量(cool),然后下一个块检查它是否为真。最后,从一组名字列表创建一个数组,然后遍历并显示在列表中。如果这个语法看起来有点奇怪,请记住,这些模板语言并不打算取代一个“正确”的完整开发语言。相反,它们提供了一些基本功能,以便您创建动态 HTML。
如果您仍在运行 Eleventy 命令行,请使用 CTRL+C 来停止它,然后在新目录中运行 eleventy --serve,您应该会看到类似于图 2.6 的输出。同样,您应该可以自由地修改一些值来观察它们的变化。

图 2.6 液态模板输出
Eleventy 使用 Liquid 处理器将您的输入转换为纯 HTML 输出。我们只看了您可以在 Liquid 中做的一小部分,但请记住,如果您不喜欢这种写作风格,Eleventy 提供了多种其他选项。在我使用 Liquid 之前,我是一名 Handlebars 的粉丝,但我发现它在某些时候限制性很强。Handlebars 不希望您在模板中使用很多逻辑,并鼓励您在其他地方做这件事。我理解这种思考的逻辑,但与此同时,我更喜欢 Liquid 的灵活性。Liquid 不能做所有事情,所以当我需要一些真正定制化的东西时,我会使用 EJS (ejs.co/)。我真的很不喜欢 EJS 的外观。这是一个纯粹的个人观点,但我发现 EJS 代码很丑。然而,它无疑是选项中最灵活的,所以我感激在我需要的时候可以使用它。
2.1.3 添加布局和包含
到目前为止,你已经看到了 Eleventy 如何将 Markdown 或其他语言文件转换为 HTML。现在,是时候通过使用布局和包含来稍微改进一下了。布局是“包装”文件,可以用来为你的文件添加网站布局。每个布局都会有一个标记,用于包含你的页面内容。这意味着你可以有一个布局文件,然后让网站的其他部分使用它。希望这种力量的明显性是显而易见的。你只需修改那个文件,就可以快速改变整个网站的外观和感觉。你还可以更快地纠正网站中的错误,比如标题或页脚中的错别字。包含只是你在模板中包含的其他文件。如果你网站上有几个需要包含相同法律文本(读者会忽略)的表单,包含是一个简化该过程的不错方式。
要开始使用布局文件,我们首先需要告诉我们的模板使用它们。Eleventy 使用许多静态网站生成器支持的功能:前缀部分。前缀部分是关于你的文件的元数据,包含在顶部。在几乎所有情况下,前缀部分使用特定的标记来开始和结束块,然后使用简单的“键:值”格式来指定值。以下是一个特定文件的前缀部分可能看起来如何的示例。
列表 2.4 前缀部分的示例
---
something: a value!
anotherSomething: another value
---
This is the rest of the file.
在这个极其简单的示例中,前缀部分指定了两个值,something 和 anotherSomething。Eleventy 或其他任何静态网站生成器通常会查看这些值,并根据你设置的值执行不同的操作。对于它们不理解的价值,这些值将被忽略,但可用于你的代码。(你将在数据部分看到这一点。)当文件生成 HTML 时,前缀部分将完全删除。让我们看看这个例子。
创建一个新的目录(或者使用你的仓库副本)命名为 demo3。在这个目录中,从第一个示例复制 index.md 和 cats.md 文件。打开第一个文件,并修改它以添加前缀部分。
列表 2.5 带有前缀部分的更新 index.md 文件
---
layout: main
---
## Hello World
This is my first page in lovely markdown.
Here's a list for fun:
* Cats
* Dogs
* More Cats
这里的变化是在顶部添加了前缀部分,并指定了一个值:layout。cats.md 文件也可以类似地进行修改。
列表 2.6 更新的 cats.md 文件
---
layout: main
---
## Cats
Cats are the best, aren't they?
当 Eleventy 解析这些文件时,它会读取元数据并注意到布局设置。这将告诉 Eleventy 尝试加载一个名为 main dot something 的布局文件。为什么是“dot something”?如前所述,Eleventy 支持许多不同的模板格式,具有不同的扩展名。通过在元数据中仅指定“main”,Eleventy 将检查所有支持的模板类型。它在哪里检查?默认情况下,布局文件是在名为 _includes 的目录中搜索的。这是你的项目根目录,也是你运行 Eleventy 命令行的地方。由于你正在 demo3 文件夹中工作,_includes 文件夹将位于那里。如果你选择,你可以更改这个目录名称。让我们使用 Liquid 来构建一个布局文件,并将其存储在 _includes/main.liquid 中。列表 2.7 展示了我们构建的布局文件。
列表 2.7 网站布局文件
<html>
<head>
<title>My Site</title>
<style>
body {
background-color: #ffaa00;
margin: 50px;
}
footer {
background-color: #c0c0c0;
padding: 10px;
}
</style>
</head>
<body>
{{ content }} ❶
<footer>
<a href="/">Home</a> | <a href="/cats">Cats</a>
</footer>
</body>
</html>
❶ 这就是页面内容将被包含的地方。
对于大多数情况,这看起来像是一个普通的 HTML 模板。你可以看到一些样式(当然,很丑)和页脚,但特别要注意{{ content }}。当 Eleventy 解析使用元数据和指定此布局的文件时,它将页面生成的 HTML 放入一个名为 content 的变量中。我的 Liquid 模板可以简单地在任何合适的地方输出这个变量。在这种情况下,我是在 body 标签之后和页脚之前做的。为了清楚起见,这是任意的。如果你在这个目录中启动一个 Eleventy 服务器(记得按 CTRL+C 退出任何之前的测试),你会在图 2.7 中看到为什么我不被允许设计网站。

图 2.7 应用了布局的我们的网站
如果你愿意,尝试更改样式声明并选择不同的(更好的)颜色选择。现在你已经使用了一个完整的 HTML 页面,你将最终看到 Eleventy 的热重载功能在行动。
在离开布局主题之前,请注意,你还可以有嵌套的布局。例如,你可以为页面指定一个主布局,然后指定另一个布局,该布局本身指定主布局。第一个将运行,包含你的页面内容,然后将 HTML 返回给主布局。你稍后会看到一个这样的例子。
现在我们来演示包含。这通常相当简单。你创建一个要包含的文件,并将其存储在布局所在的 _includes 文件夹中。如何包含文件取决于你的模板引擎。对于 Liquid,它看起来像这样:
{% include footer %}
其中 footer.liquid 应该在 _includes 文件夹中。对于 Handlebars,它看起来略有不同:
{{> footer}}
在 Handlebars 中,这些被称为部分,但它们的作用方式相同。Eleventy 为其所有支持的模板语言提供了文档(www.11ty.dev/docs/languages/),所以请检查一下你选择的模板语言如何支持这个功能。
2.1.4 在 Eleventy 网站中使用集合
到目前为止,你已经看到了如何将输入文件(Markdown 和其他语言)转换为 HTML。现在,是时候展示另一个强大的功能——集合。集合正如其名:一组按某种逻辑模式组合的文件。在 Eleventy 中,有几种不同的方法来做这件事,但最简单的方法是通过前置信息和使用标签值。考虑以下前置信息示例:
---
layout: main
tags: pressReleases
title: Press Release One
---
在这里,我使用了三个前置信息值。第一个,布局,我们之前已经讨论过了。第二个,标签,指定了这个文件的标签值。任何使用相同值的文件都将位于同一个集合中。如果你想为文件选择多个标签,它将可以在多个集合中可用。最后一个值,标题,是我之前没有展示过的。正如你可以猜到的,这个值指定了文件的标题,并且如果你选择的话,它将在你的模板中可用以输出。
默认情况下,这本身不会做任何事情。但你的模板可以遍历集合并创建列表。让我们构建一个与集合一起工作并动态创建列表的示例。
创建一个名为 demo4 的新目录(或者使用仓库中的源文件)。这个目录不应该与 demo3 在同一个文件夹中。从 demo3 复制相同的文件内容,并将 index.md 重命名为 index.liquid。我们将向我们的网站添加新闻稿,所以添加一个名为 press-releases 的子目录。在这个目录中,创建一些文件。文件名并不重要,内容也不重要,但如果你想跟随仓库,第一个文件可以命名为 cats-are-cool.md。
列表 2.8 第一份新闻稿
---
layout: pr
tags: pressReleases
title: Cats are Cool
---
Just some text here for filler.
正如我们之前讨论的,我们正在使用文件的前置信息来指定一个标签,以便将文件添加到某个集合中。我们还指定了布局和标题。重复几次(仓库中有三个文件),但文件名、标题和内容并不重要。仓库中有一个名为 dogs-are-not-cool.md 的文件和 have-we-said-how-cool-cats-are.md。
下一步是添加布局。我们之前提到过,布局可以包含其他布局。对于我们的新闻稿,我们希望它们有一些额外的布局。
列表 2.9 “pr”布局在 _includes/pr.liquid
---
layout: main
---
<h2>Press Release: {{ title }}</h2> ❶
{{ content }}
❶ 输出标题
注意,它指定了 main 作为其布局。这意味着新闻稿将首先运行 pr.liquid 模板,然后将输出发送到 main.liquid。同时注意,标题是在布局中输出的。这个值来自新闻稿前置信息中的标题。我们也可以在我们的核心布局文件中使用这个值。
列表 2.10 支持标题的更新布局文件 (_includes/main.liquid)
---
title: Default title
---
<html>
<head>
<title>{{ title }}</title>
<style>
body {
background-color: #ffaa00;
margin: 50px;
}
footer {
background-color: #c0c0c0;
padding: 10px;
}
</style>
</head>
<body>
{{ content }}
<footer>
<a href="/">Home</a> | <a href="/cats">Cats</a>
</footer>
</body>
</html>
在这个文件版本中只有两个更改。首先,在前置信息中指定了一个标题值。如果模板没有指定自己的标题,这个值才会被使用。接下来,标题在标题标签之间输出。
最终结果是,我们的新闻稿文件将自动获得一些额外的布局,然后使用网站其余部分的主布局。现在我们已经有了新闻稿,并且知道它们在一个集合中,下一步是通过创建一个新的主页来将此暴露给用户,该主页使用 Liquid 模板引擎来输出这些值。
列表 2.11 带有新闻稿支持的 index.liquid(/index.liquid)
layout: main
title: Home Page
Welcome to our home page. Here's our press releases:
<ul>
{% for pr in collections.pressReleases %} ❶
<li><a href="{{ pr.url }}">{{ pr.data.title }}</a></li>
{% endfor %}
</ul>
❶ 注意 collections.pressReleases 的使用。
当 Eleventy 遇到使用标签的前置内容的模板时,它会将这些模板放入一个名为 collections 的对象中。然后,您可以通过使用 collections.nameoftag 来访问这些数据。由于我们使用了 pressReleases 作为标签值,这个集合将包含三个项目。(再次提醒,您可以自由地创建更多新闻稿并查看变化。)我们遍历每个项目,并将对象分配给一个名为 pr 的变量。这个变量包含多个不同的属性。其中一个属性是 url,它由 Eleventy 创建,表示页面的位置。数据属性将包含在页面前置内容中定义的任何数据。由于我们的新闻稿有标题,我们可以在这里输出它们。还有更多您可以使用的变量,您可以查看 collections 文档(www.11ty.dev/docs/collections/)以获取详细信息。最终结果如图 2.8 所示。

图 2.8 由 Eleventy 集合驱动的带有新闻稿的主页
如果您点击其中一个新闻稿,您可以看到生效的多层布局(图 2.9)。

图 2.9 一个示例新闻稿
现在您已经看到了一些使用前置内容和 collections 的示例,是时候从高层次上查看 Eleventy 中的数据了。
2.1.5 与数据一起工作
您已经对 Eleventy 中数据的工作方式有了一个简要的介绍,但现在我们将更深入地探讨。向 Eleventy 页面提供数据有多种方式。前置内容是一种方式,并且是针对特定页面的。集合是另一种方式,提供可以迭代和使用的数据。向 Eleventy 网站提供数据的另一种方式是通过创建一个特殊文件夹的文件:_data。您在这里可以创建两种类型的文件,JSON 和 JavaScript。文件的名称将定义它对网站的可访问性。例如,想象一下在您的 _data 文件夹中有一个名为 site.json 的文件。它包含以下内容:
{
"siteName": "The Cat Site",
"siteOwner": "Raymond Camden",
"siteEmail": "raymondcamden@gmail.com"
}
这个 JSON 文件定义了三个变量。它也可以是一个数组,或者是一个对象的数组——基本上是任何有效的 JSON。因为文件名为 site.json,所以您的模板可以像这样访问值(再次假设使用 Liquid 语法):{{ site.siteName }}。
JavaScript 数据文件的工作方式略有不同,因为当然,JavaScript 是一种编程语言,而 JSON 只是静态信息。JavaScript 数据文件允许您执行可能需要的任何操作,并且当操作完成后,文件的结果是最终可用的数据。想象一个名为 build.js 的示例 JavaScript 文件:
module.exports = function() {
let buildTime = new Date();
let randomNumber = Math.random();
return {
buildTime,
randomNumber
}
}
此 JavaScript 文件创建了两个变量,一个代表当前时间,另一个是随机数。然后返回一个包含这两个值的对象。因为文件名为 build.js,所以我们的模板可以使用这些值,如 {{ build.buildTime }} 或 {{ build.randomNumber }}。请注意,此逻辑在网站构建时评估(文件名与此无关),因此当静态网站部署时,这些值不会改变。让我们看看两种数据文件在实际操作中的例子。
首先,再次将上一个目录(demo4)复制到一个新的目录(demo5)中,或者简单地使用 GitHub 仓库代码。在 demo5 中创建一个名为 _data 的新文件夹,该文件夹将存储数据文件。如果需要,可以在 Eleventy 中配置用于数据文件的文件夹名称。我们将添加的第一个文件是 site.json。
列表 2.12 site.json 定义 (_data/site.json)
{
"siteName":"The Cat Fan Site",
"contactAddress":"raymondcamden@gmail.com"
}
这些值是任意的,希望它们是自我解释的。我们已为网站定义了一个名称以及一个联系地址。现在定义一个新的文件,breeds.js。对于此文件,我们将使用 Cat API 网站 (thecatapi.com/)。该网站提供与猫相关的免费 API,但您必须先注册 (thecatapi.com/signup) 获取密钥,如图 2.10 所示。

图 2.10 Cat API 的注册页面
他们提供的 API 之一是返回猫品种列表。API 很简单,可以通过此 URL 调用:api.thecatapi.com/v1/breeds?limit=5&api_key=yourkey。URL 中的 limit 值限制了返回的数据量,而 key 值应替换为您获得的值。
列表 2.13 获取猫品种列表 (_data/breeds.js)
const fetch = require('node-fetch');
require('dotenv').config(); ❶
const KEY = process.env.CAT_API_KEY; ❷
module.exports = async function() {
let breedUrl = 'https://api.thecatapi.com/v1/breeds?limit=5' +
'&api_key=' + KEY;
let resp = await fetch(breedUrl);
let data = await resp.json();
return data;
}
❶ 加载 'dot.env' 包
❷ 读取 .env 键值
通常,这是一个简单的 Node 脚本,它使用 node-fetch 来简化 HTTP 调用。函数体执行调用并返回结果。(它也可以修改以转换代码。)这里重要的一点是第 2 行使用了 'dot.env' 包。这将查找位于项目根目录中的名为 .env 的文件。这是一个简单的名称值对集合。您在 GitHub 仓库中找不到它,因为它是在代码中包含密钥的标准方式。Node 脚本将读取此文件,并将每个值设置为 process.env 范围内的值。在 demo5 文件夹根目录的 .env 文件中,添加以下内容:
CAT_API_KEY=yourkey
将 yourkey 替换为您从 Cat API 获取的密钥。
还有一个最后的步骤要做。我们的函数使用了 node-fetch 和 'dotenv'。为了使它们可用,您需要通过 npm 安装它们。在终端中,在 demo5 文件夹所在的目录下运行
npm install node-fetch@2
npm install dotenv
这将安装所需的依赖项。
现在我们已经设置了数据文件,让我们使用它们。为了保持简单,让我们在 index.liquid 中同时使用它们。
列表 2.14 利用 Eleventy 数据文件
---
layout: main
title: Home Page
---
Welcome to our home page. Here's our press releases:
<ul>
{% for pr in collections.pressReleases %}
<li><a href="{{ pr.url }}">{{ pr.data.title }}</a></li>
{% endfor %}
</ul>
Here's a list of cat breeds:
<ul>
{% for breed in breeds %}
<li>{{ breed.name }} - {{ breed.description }}</li>
{% endfor %}
</ul>
You can contact me at {{ site.contactAddress }}
修改从发布显示之后开始,尽管请注意逻辑基本上是相同的:遍历每个项目的值并输出数据。Cat API 返回了大量关于猫品种的数据,但为了保持简单,我们只输出了名称和描述。最后,网站联系地址在最后显示。图 2.11 展示了结果。

图 2.11 由动态数据文件驱动的更新后的主页
一定要查看 Eleventy 文档的“使用数据”部分([www.11ty.dev/docs/data/](https://www.11ty.dev/docs/data/))以获取更多示例。Eleventy 还支持在目录级别指定数据以及其他选项。
2.2 让我们构建 Camden Grounds
现在我们已经看到了一些(当然不是全部!)Eleventy 可以做什么,是时候看看如何构建我们的简单餐厅网站了。在本章中,我们将构建一个名为 Camden Grounds 的虚构咖啡馆网站。这个网站将遵循大多数餐厅网站的典型模式,提供一些基本功能:
-
一个主页,大部分只是漂亮的图片,以帮助吸引访客。
-
一个产品提供的菜单,再次配有漂亮的图片。
-
一个显示不同 Camden Grounds 商店位置的页面。
-
一个关于我们页面的页面,讲述商店的历史。没有人会真正阅读这个页面,但店主坚持要这么做。
首先,你需要确定网站的设计。如果你像我一样,设计能力有限,最简单的解决方案是找到一个你可以使用的模板。幸运的是,有很多网站满足这一需求。我们将使用一个名为“Shop Homepage”的简单 Bootstrap 商店模板(http://mng.bz/Dxy0)。你可以在图 2.12 中看到它的预览。请注意,这是它在模板形式下的样子,而不是我们将要创建的最终形式。

图 2.12 带有产品和顶部旋转横幅的商店模板
注意:自从本章创建以来,模板已更新到具有略微不同设计的较新版本。本章使用的版本仍可在 mng.bz/2j6w 找到并使用。另外,请记住,模板可以在本书的完成代码中找到,位于 mng .bz/1j5R。
这个模板可以自由下载和修改。最终演示的完整源代码可以在 GitHub 仓库的 chapter2/camdengrounds 文件夹中找到。由于有很多样板 HTML 代码(Bootstrap 很好,但有点冗长),我们将关注模板的修改,而不是分享每一行代码。
注意:虽然模板看起来很漂亮,但在你提交之前,请查看代码。它可能很难根据你的需求进行修改。
如前所述,你可以将一个典型模板视为围绕你的页面的“包装”。你确定内容可以填充的位置,插入 Eleventy 将用于放置页面内容的标记,然后确保你的页面指定了该布局。
你可以在 _includes/main.liquid 文件中看到卡姆登场地的模板。由于这个文件中有大量的 HTML 代码,接下来的几个列表将突出显示重要部分。首先,模板的顶部指定了前导内容中的标题。
列表 2.15 布局的元数据
---
title: Camden Grounds
---
这将在个别页面未指定时用作默认值。你可以在几行后看到这一点。
列表 2.16 动态标题
<title>{{ title }}</title>
现在让我们看看导航菜单。这个模板支持的一个功能是在特定页面上突出显示一个标签。我们如何在静态网站上使用这个功能?Eleventy 提供了对页面变量的访问,该变量提供了有关当前正在渲染的页面的信息。其中一个值是页面的相对 URL。鉴于我们可以使用模板语言并检查该值,我们可以创建一个动态菜单。
列表 2.17 模板的动态菜单
<ul class="navbar-nav ml-auto">
<li class="nav-item {% if page.url == '/' %}active{% endif %}">
<a class="nav-link" href="/">Home
</a>
</li>
<li class="nav-item {% if page.url == '/about/' %}active{% endif %}">
<a class="nav-link" href="/about">About</a>
</li>
<li class="nav-item {% if page.url == '/services/' %}active{% endif %}">
<a class="nav-link" href="/services">Services</a>
</li>
<li class="nav-item {% if page.url == '/contacts/' %}active{% endif %}">
<a class="nav-link" href="/contact">Contact</a>
</li>
</ul>
我们的网络导航由四个主要页面组成。对于每个页面,使用 Liquid 代码查看 URL 值,当它与所讨论页面的相对路径匹配时,会添加活动类。我们将分享的模板的最后部分是包含内容的地方。
列表 2.18 在模板中包含页面内容
<div class="col-lg-9">
{{ content }}
</div>
实话实说,找到添加内容变量的最佳位置需要一些挖掘。正如所述,Bootstrap 是一个伟大的设计框架,但有时可能会很冗长且有点复杂。强烈建议你打开官方 Bootstrap 网站的一个标签页(getbootstrap.com/)。它有广泛的文档和示例,可以帮助你更好地使用该框架。
主页将会比较复杂,所以让我们逐个查看这三个简单的页面。
列表 2.19 关于页面(about.html)
---
layout: main
title: About Camden Grounds
---
<div class="row my-4">
<div class="col">
<h2>The About Page</h2>
<p>
Let's talk about the site.
</p>
</div>
</div>
如果你查看布局文件源代码,你会发现它几乎有 100 行长。这个模板要短得多,因为 Eleventy 会处理用内容包装它。如果你愿意,可以查看 contact.html 和 services.html;它们遵循完全相同的格式。图 2.13 展示了关于页面如何显示(注意突出显示的菜单标签)。

图 2.13 卡姆登场地关于页面
然而,在这一切工作之前,我们需要指出 Eleventy 的一个有趣的问题。当它遇到一个不知道如何处理的文件时,它会忽略它。这最终成为一个大问题,因为我们的 Bootstrap 模板有多个 CSS、JavaScript 和图片文件夹,这些文件夹构成了设计的视觉和感觉。
我们需要做的是告诉 Eleventy 直接复制这些文件。Eleventy 通过在项目根目录中指定一个 .eleventy.js 文件来支持动态配置。虽然你可以在这个文件中做很多事情,但我们想要启用的主要功能被称为 Passthrough File Copy (www.11ty.dev/docs/copy/)。在 Camden Grounds 网站的根目录中,有以下的配置。
列表 2.20 Eleventy 配置文件 (.eleventy.js)
module.exports = function(eleventyConfig) {
eleventyConfig.addPassthroughCopy("css");
eleventyConfig.addPassthroughCopy("vendor");
eleventyConfig.addPassthroughCopy("img");
};
我们指定了三个路径,css、vendor 和 img,应该直接复制。Eleventy 不会尝试处理这里的任何内容,而只是将文件递归地复制到生成的一 _site 文件夹中。
好吧,我们已经完成了最容易的部分(总是一个好主意),但现在我们需要处理主页。如果你回顾一下图 2.12,你会看到主页顶部有一个旋转的图片横幅,然后是一系列的图片。如果你查看模板原始代码中的 index 页面的源代码,你会看到动画图片(称为轮播)被显示出来。
列表 2.21 图片轮播代码
<div class="carousel-inner" role="listbox">
<div class="carousel-item active">
<img class="d-block img-fluid" src="http://placehold.it/900x350"
alt="First slide">
</div>
<div class="carousel-item">
<img class="d-block img-fluid" src="http://placehold.it/900x350"
alt="Second slide">
</div>
<div class="carousel-item">
<img class="d-block img-fluid" src="http://placehold.it/900x350"
alt="Third slide">
</div>
</div>
你可以看到每个图片都使用了占位符服务来显示临时图像。我们将用来自 Unsplash (unsplash.com/) 的三张图片来替换这些。Unsplash 提供免费图片供网站使用,只需简单的要求你进行归属即可。我搜索了“coffee”并找到了三张图片,并将它们放在了 img 文件夹中。
列表 2.22 使用我们新指定的图片的图片轮播
<div class="carousel-inner" role="listbox">
<div class="carousel-item active">
<img class="d-block img-fluid" src="img/coffee1.jpg"
alt="Coffee!">
</div>
<div class="carousel-item">
<img class="d-block img-fluid" src="img/coffee2.jpg"
alt="Coffee beans">
</div>
<div class="carousel-item">
<img class="d-block img-fluid" src="img/coffee3.jpg"
alt="Coffee cup">
</div>
</div>
现在是产品的时间了。原始模板显示了六个产品,使用的是通常所说的卡片格式。我们想要让它变得动态,所以让我们在 _data/products.json 中添加一个包含产品的 JSON 文件。
列表 2.23 Camden Grounds 的产品
[
{
"name" : "Coffee",
"price" : 2.99,
"description" : "Lorem ipsum dolor sit amet, consectetur adipisicing
➥ elit. Amet numquam aspernatur!",
"thumbnail" : "http://placehold.it/700x400",
"image" : "http://placehold.it/900x350"
},
{
"name" : "Espresso",
"price" : 3.99,
"description" : "Lorem ipsum dolor sit amet, consectetur adipisicing
➥ elit. Amet numquam aspernatur!",
"thumbnail" : "http://placehold.it/700x400",
"image" : "http://placehold.it/900x350"
},
{
"name" : "Americano",
"price" : 5.99,
"description" : "Lorem ipsum dolor sit amet, consectetur adipisicing
➥ elit. Amet numquam aspernatur!",
"thumbnail" : "http://placehold.it/700x400",
"image" : "http://placehold.it/900x350"
},
{
"name" : "Double Espresso",
"price" : 8.99,
"description" : "Lorem ipsum dolor sit amet, consectetur adipisicing
➥ elit. Amet numquam aspernatur!",
"thumbnail" : "http://placehold.it/700x400",
"image" : "http://placehold.it/900x350"
},
{
"name" : "Tea",
"price" : 1.99,
"description" : "For those who prefer tea.",
"thumbnail" : "http://placehold.it/700x400",
"image" : "http://placehold.it/900x350"
}
]
注意,文本为了节省空间略有缩减。我们不是为每个产品找到自定义图片,而是使用来自 placehold.it 的图片占位服务,它产生一个基本的灰色图像,对于临时图像很有用。现在这已经设置好了,Eleventy 通过 products 变量提供对这些图片的访问。我们可以修改 index 模板(camdengrounds/index.html)来循环创建每个产品的卡片。
列表 2.24 显示产品
{% for product in products %}
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100">
<a href="products/{{ product.name | slug }}"><img class="card-img-top"
src="http://placehold.it/700x400" alt=""></a>
<div class="card-body">
<h4 class="card-title">
<a href="products/{{ product.name | slug }}">{{ product.name }}</a>
</h4>
<h5>${{product.price}}</h5>
<p class="card-text">{{product.description}}</p>
</div>
</div>
</div>
{% endfor %}
我们基本上只是移除了样板文本,并用循环本身定义的变量替换了它。如果我们为我们的产品有独特的图片(希望如此),它们也可以在 JSON 文件中指定并在这里使用。
网站的最后一个方面是每个单独产品的页面。您可以在 2.25 列表中看到使用的链接:products/{{ product.name | slug }}。首先要注意的是 slug 部分。这被称为 过滤器,它接受提供的输入(product .name)然后通过一个格式化函数传递。slug 函数创建一个 URL 安全的字符串版本。例如,值“Double Espresso”变为“double-espresso。”但页面本身是从哪里来的呢?
Eleventy 支持强大的分页系统 (www.11ty.dev/docs/pagination/),可以以多种方式使用。它可以接受数据列表并创建页面(例如,将数百份新闻稿分成每页 10 份的页面)或接受列表并生成每个页面。Eleventy 还提供对链接到页面的支持,无论是下一页还是上一页,甚至可以轻松地判断您是否位于页面列表的开头或结尾。与 Eleventy 中的大多数事物一样,它非常强大且灵活,但我们将使用一个简单的示例在我们的商店中,以帮助为我们的每个产品创建一个页面。
要使用此功能,页面必须在前面部分定义分页设置,如下所示。
列表 2.25 产品分页页面
---
layout: main
pagination:
data: products
size: 1
alias: product
permalink: "products/{{ product.name | slug }}/index.html"
---
<div class="row my-4">
<div class="col">
<div class="card mt-4">
<img class="card-img-top img-fluid"
src="http://placehold.it/900x400" alt="">
<div class="card-body">
<h3 class="card-title">{{ product.name }}</h3>
<h4>${{product.price}}</h4>
<p class="card-text">
{{ product.description }}
</p>
</div>
</div>
</div>
前面部分通过首先指定要迭代的 数据,然后指定大小(每页多少个),最后指定在页面本身中使用别名以方便使用来使用分页。使用页面大小为 1 实际上意味着我们希望每页只有一个产品。这里还展示了 Eleventy 的另一个功能:永久链接。Eleventy 通过让您指定文件应该放在哪里来精确控制文件的生成。模板的其余部分基本上是之前使用的卡片的一个修改版本,但肯定可以更加独特。
如果您启动 Eleventy 服务器并点击其中一个产品,您将看到一个产品页面,如图 2.14 所示。

图 2.14 Camden Grounds 产品
2.3 使用 Eleventy 进一步深入
在本章中,您已回顾了 Eleventy 静态网站生成器,并使用它为一家虚构的商店构建了一个简单但灵活的商店。然而,Eleventy 中还有许多我们未涉及的功能:
-
过滤器和简码
-
插件以提供附加功能
-
使用事件自定义构建
-
以及更多
请务必查看 Eleventy 文档 (www.11ty.dev/docs/) 以获取更多信息。我还建议关注 Eleventy 的官方 Twitter 账户 (@eleven_ty) 以及加入 Discord 频道 (www.11ty.dev/news/ discord/)。最后,您可以在他们的 GitHub 仓库中查看源代码和当前开放的问题:github.com/11ty/eleventy/.
在下一节中,你将了解另一个静态站点生成器,Jekyll。Jekyll 是专为博客定制的,所以你将构建一个博客作为帮助你学习的方式。最好的是,Jekyll 也使用了 Liquid,所以如果你喜欢在 Eleventy 中使用它,你将有机会更多地使用它!
摘要
-
Eleventy 是一个灵活的静态站点生成器,也是构建 Jamstack 站点的好方法。
-
Eleventy 支持不同类型的模板语言,并允许你选择最适合你风格的其中一个。
-
如果你发现你需要从多个选项中获取特定的功能,一个项目中可以使用多种不同的模板语言。
-
简单的站点,即使是只有几页的站点,也是 Jamstack 的绝佳候选者。
3 构建博客
本章涵盖
-
使用静态站点生成器构建博客
-
如何安装和使用 Jekyll
-
使用 Jekyll 创建基本博客
博客是互联网上最受欢迎的网站形式之一,仅 WordPress 平台就有超过 7500 万人使用。博客在结构上通常相当简单,但在本质上非常个性化。就像日记一样,博客按时间顺序列出帖子,通常在主页上以最新帖子为首选。博客通常会使用类别和标签来分类和组织内容,让读者能够快速找到与特定帖子相关的相关内容。
在本章中,我们将专注于使用 Jamstack 构建博客。我们将使用的静态站点生成器默认构建博客,因此我们将为我们完成大量工作,这让我们有更多时间专注于博客主题(外观)和其他方面的定制。
3.1 使用 Jekyll 进行博客写作
Jekyll (jekyllrb.com) 是一个专门针对博客的静态站点生成器(如图 3.1)。虽然您当然可以使用 Jekyll 构建其他类型的网站(以及使用其他生成器构建博客),但您会发现 Jekyll 在构建 Jamstack 博客时特别出色。Jekyll 是 GitHub Pages 网页的驱动程序,这对于已经在 GitHub 上使用项目的人来说可能非常有用。

图 3.1 Jekyll 网站 (jekyllrb.com)
Jekyll 的安装比大多数静态站点生成器要复杂一些,在 Windows 上可能会有一些问题。(有关特定操作系统的详细说明,请参阅在线文档jekyllrb.com/docs/installation/。)目前,Jekyll 在 Windows 上尚未官方支持。我个人已经使用 Jekyll 在 Windows 上了一段时间,它运行良好,但在您决定使用 Jekyll 之前,缺乏支持是一个需要考虑的因素。现代 Windows 机器支持 WSL(Windows 子系统 for Linux),如果您在使用 Ruby 时遇到问题,这可能会提供更平滑的体验。
安装 Jekyll 需要先安装 Ruby。有关说明,请参阅www.ruby-lang.org/en/downloads/。然后您需要 RubyGems ( rubygems.org/pages/download)。如果您以前从未使用过 Ruby,请不要担心,因为使用 Jekyll 创建网站时不需要 Ruby。虽然 Jekyll 本身运行在 Ruby 上,但除非您开始自定义 Jekyll 引擎本身,否则您不需要编写任何 Ruby 代码。
一旦安装了先决条件,您可以使用以下命令安装 Jekyll 本身:
gem install jekyll bundler
注意,有针对多个操作系统的安装指南(jekyllrb.com/docs/installation/#guides),其中包含更多详细信息。
作为最后一步,确保 Jekyll 正确安装,请在终端中运行 jekyll,如图 3.2 所示。

图 3.2 在您的终端中运行 jekyll 命令的默认输出
Jekyll 支持在 HTML 和 Markdown 中创建内容,但也支持 Liquid 模板。如果您已经阅读了上一章,您已经对 Liquid 有了一定的了解,但至少在本章中,您将学习如何嵌入 Liquid 标签以向模板添加逻辑和动态特性。Jekyll,像大多数静态网站生成器一样,也使用元数据作为定义特定页面元数据的方式。
3.2 您的第一个 Jekyll 网站
Jekyll 的命令行程序可以通过为您搭建一个完整但小巧的博客来帮助您开始开发。这包括布局(具有最小设计)、主页(列出博客文章)和一个示例文章。通过使用命令 jekyll new sitename 来搭建,其中 sitename 应该是您网站的名称。您为 sitename 使用的值也将用于命名您的网站文件夹。(请注意,Jekyll 似乎在文件夹中存在空间问题,因此在创建新项目时请避免使用空格。)执行命令后,Jekyll 将创建目录,创建一些文件,然后安装它运行所需的各种依赖项和其他项目。图 3.3 展示了这应该看起来是什么样子。

图 3.3 使用 jekyll new 命令搭建新博客的输出
命令运行完成后,切换到新创建的目录。您可以通过使用 serve 命令 jekyll serve 测试您的新 Jekyll 博客。这将启动一个本地应用程序服务器来处理您的文件,并在本地 web 服务器上提供它们。图 3.4 展示了您在终端中应看到的输出。

图 3.4 运行 jekyll serve 命令的示例输出
注意,Jekyll CLI 的输出提供了一个服务器地址和端口号。如果您运行 jekyll serve --help,您将看到可以传递给 CLI 的许多不同选项,包括如果您愿意修改端口的选项。在浏览器中打开该地址将显示博客的主页,如图 3.5 所示。

图 3.5 默认博客主页
注意,您在这里看到的是默认用户界面。您绝对可以更改这里看到的内容,我们将在继续的过程中详细说明。要查看示例文章,请点击“欢迎使用 Jekyll!”链接,您将看到一个文章,如图 3.6 所示。

图 3.6 新建 Jekyll 网站默认欢迎博客文章
注意,你不需要使用 jekyll new 命令来创建一个新的网站。你也可以简单地在一个包含现有 HTML 文件的目录中运行 jekyll serve,Jekyll 将从那里运行其服务器。新命令的好处是,它为你提供了一些常见的默认设置(一个主页、一篇文章,甚至一个 RSS 源),这将为你节省一些繁琐的工作。
如果你查看你用 CLI 创建的骨架文件夹(由你刚刚使用的 CLI 创建的目录),你会看到一组文件和文件夹,它们定义了默认的博客。虽然具体细节可能会在未来发生变化,但以下是你通常可以期待看到的内容:
-
_posts 是你的博客文章的目录。
-
_site 是你的 Jekyll 博客的静态版本存储的地方。
-
_config.yml 是一个基于 YAML 的配置文件。我们稍后会深入了解。
-
404.html、about.markdown 和 index.markdown 是表示 404 处理程序(缺失文件)、简单的关于页面和博客主页的文件。
-
Jekyll 还使用了一些你可以现在忽略的文件。
在本章继续学习 Jekyll 的过程中,你将使用具有特殊意义的 Jekyll 的新目录。
你不会看到处理网站布局的文件。Jekyll 的默认博客使用基于“gem”的主题,其中文件存储在博客本身的文件系统之外。当我们在本章的后面开始自定义博客时,我们将与目录中的文件一起工作。你不必担心基于 gem 的主题的位置,因为当你构建 Jekyll 网站时,所有代码和资源都会复制到生成的文件夹中。
3.3 编写 Jekyll 文章
你的新博客有一篇简单的文章,所以让我们添加一篇新的文章并看看它的反应。在 _posts 文件夹中,你会看到一个名为类似 2020-08-22-welcome-to-jekyll.markdown 的文件。我之所以说“类似”,是因为 Jekyll 会在生成文件时使用当前日期,所以文件名中的日期值应该是你的当前日期和时间。Jekyll 预期博客文章的格式是 YEAR-MONTH-DAY-TITLE。请注意,月份和日期都应该是两位数字,所以对于小于 10 的数字,包括前面的 0。
创建一个包含当前年份、月份和日期的新文件,但标题为 helloworld,例如,2020-08-22-helloworld.markdown。Jekyll 文章需要在顶部包含前导内容,最初,你可以从第一个文件复制前导内容。这里是一个你可以使用的完整文件,但请记住更新日期为你的当前时间。
列表 3.1 一篇新的博客文章
---
layout: post
title: "Hello World"
date: 2020-08-22 9:00:29 -0500
categories: general
---
Hello World. This is my first *awesome* post!
注意,标题完全由你决定,帖子的内容是任意的。然而,请注意时间。在我的测试中,Jekyll 创建的第一个帖子的时间戳是:2020-08-22 08:44:29 -0500。我想让我的帖子更新一些,所以我使用了几个小时后的时间。但是,Jekyll 足够聪明,能够识别出日期值是未来的,因此没有发布博客帖子。这是一个很棒的功能,但你在测试时可能会意外遇到。如果你想,你也可以使用过去的时间。Jekyll 会根据这个值自动排序你的帖子。最后,为什么我们有基于日期的文件名,而原稿部分也有日期值呢?首先,让文件名基于日期将给你一个视觉提示,当你查看目录时,可以知道什么内容是在什么时候写的。其次,如果你在原稿部分省略了日期值,Jekyll 将使用文件名中的日期为帖子。在制作帖子时,你可以决定你是否关心帖子的时间值,因为 Jekyll 对这两种指定日期的方式都可以接受。
注意 Jekyll 有一个命令行标志,可以在本地工作时显示未来的帖子。你可以通过运行 jekyll serve --future 来启用此功能。
布局的值指定了如何渲染帖子。记住,默认主题“隐藏”了这些细节,但当我们开始构建自己的布局时,我们将能够自定义这些。最后,分类——正如你所猜到的——是一种对帖子进行分类的方式。许多博客都会有分类页面,让你可以浏览来自一个分类的帖子。Jekyll 也支持这一点,但默认主题并不是直接支持。
保存你的文件后,回到浏览器并重新加载。你应该能在主页上看到新帖子,如图 3.7 所示。

图 3.7 添加新帖子后,它将显示在主页上。
如果你点击帖子,你会看到它被渲染。图 3.8 展示了 Jekyll 如何将 Markdown 渲染成 HTML 并自动应用主题布局。

图 3.8 Jekyll 渲染的新博客帖子
记下你新帖子的 URL。再次提醒,记住日期会有所不同;它应该看起来像这样:http://localhost:4000/general/2020/08/22/helloworld.html。注意 Jekyll 如何解析你的初始文件名,并基于它创建路径,同时还包括帖子的分类。我个人并不喜欢这种格式,但幸运的是,Jekyll 允许你自定义这种格式。
3.3.1 液体模板语言复习
Jekyll 使用 Liquid 模板语言来允许在生成静态页面时使用动态代码。在前一章中,我们在 2.1.2 节中介绍了使用 Liquid 的基础知识,但如果你跳过了那一章,让我们快速回顾一下。
首先,Liquid 代码使用两种类型的标记。要输出一个简单的值,你会使用 {{ variable }},其中 variable 是你想要输出的值。要执行一行代码,使用稍有不同的形式:{% %}。这个命令将给一个变量赋值:{% assign foo = "cat" %}。Liquid 支持条件语句和循环。接下来,我们修改你刚刚创建的博客文章,以包含上一章中的相同示例代码。
列表 3.2 带有 Liquid 命令的博客文章
---
layout: post
title: "Hello World"
date: 2020-08-22 9:00:29 -0500
categories: general
---
Hello World. This is my first *awesome* post!
{% assign name = "ray" %}
<p>
Hello, {{ name }}!
</p>
{% assign cool = true %}
{% if cool %}
<p>
Yes, you are cool.
</p>
{% endif %}
{% assign cats = "Fluffy,Muffy,Duffy" | split: ',' %}
<ul>
{% for cat in cats %}
<li>{{ cat }}</li>
{% endfor %}
</ul>
如果你保存并重新加载,你将在图 3.9 中看到结果;请随意修改值并尝试查看实际的变化。

图 3.9 一个带有 Liquid 代码的 Jekyll 博客文章,为静态页面添加动态特性
Jekyll 将解析你的博客文章和网站上常规页面的 Liquid。如果你使用 HTML 文件而不是 Markdown,这个功能同样适用。
3.4 与布局和包含文件一起工作
到目前为止,内容显示的细节已经被抽象化。我们解释了 Jekyll 使用默认主题,并且主题文件本身存储在“其他地方”,但它们实际上是如何工作的,以及你将如何修改它们?让我们再次使用 CLI 创建一个新的博客:jekyll new blog1。(本节中的所有列表都可以在本书的 GitHub 仓库的 chapter3/blog1 文件夹中找到。)当它完成新站点的脚手架搭建后,使用 jekyll serve 启动服务器。要开始使用布局,创建一个名为 _layouts 的新目录。默认情况下,Jekyll 将在此文件夹中查找布局文件。(如果你愿意,可以自定义此文件夹的名称。)如果你打开 CLI 生成的 index.html 文件,你会注意到它指定了一个名为 home 的布局(注意,我已经移除了注释):
---
layout: home
---
让我们将此值更改为默认。Jekyll 文档建议使用名为 default 的布局,并在你有不同的布局时扩展它。你将在稍后看到这个示例。但就目前而言,将前导内容中的值更改为 default:
---
layout: default
---
现在让我们创建一个布局文件。列表 3.3 展示了一个非常简单但定制的布局。由于我们指定了默认布局,文件名必须匹配,因此将此文件命名为 default.html。
列表 3.3 新的布局文件 (_layouts/default.html)
---
title: Default Title
---
<html>
<head>
<title>{{ page.title }}</title> ❶
</head>
<body>
{{ content }} ❷
</body>
</html>
❶ 页面.title 变量将来自使用该布局的页面。
❷ 内容变量是使用布局的页面内容本身。
这个模板可以说是最简单的了。实际上没有任何 CSS 或布局,只是几个 HTML 标签。这里使用了两个变量。第一个,page.title,将被使用该布局的页面的标题所替换。第二个,content,将被页面本身的内容所替换。如果你现在重新加载你的博客,你会看到...什么都没有!为什么?你之前看到的逻辑(创建博客文章列表)是由默认主题完成的。移除它后,你的主页不幸地变成了空白的。让我们通过添加文章来修复这个问题。
列表 3.4 新的首页 (/index.markdown)
---
layout: default
title: My Blog
---
<h1>Posts</h1>
<ul>
{% for post in site.posts %}
<li><a href="{{ post.url }}">{{ post.title }}</a>, written
{{ post.date}}</li>
{% endfor %}
</ul>
在这里我们首先添加了页眉部分的一个标题。布局文件会注意到这一点,并将其用于页面标题。接下来,我们使用 Liquid 语句遍历一个变量,site.posts。Jekyll 会自动提供这个变量,基于你 _posts 目录中的文件。每篇帖子都有一个 url 和标题值,可以用来渲染帖子的链接和标题。最后,你还可以显示帖子的日期。使用无序列表来显示帖子并不十分美观,但能完成任务,如图 3.10 所示。

图 3.10 我们的新、无聊的首页
3.4.1 布局继承
Jekyll 支持布局继承的概念,这只是一个更华丽的说法,即一个布局包裹另一个布局。让我们通过自定义博客文章的显示来构建一个简单的例子。如果你打开 CLI 生成的默认博客帖子,你会看到它已经指定了一个独特的布局,post。在你的 _layouts 文件夹中,创建一个名为 post.html 的新文件。
列表 3.5 帖子布局 (_layouts/post.html)
---
layout: default
---
<h1>Blog Post: {{ page.title }}</h1>
{{ content }}
再次强调,这是一个相当无聊的布局,但请注意它如何指定一个布局本身。这意味着帖子布局将显示其内容,然后将它们传递给下一个布局。在这种情况下,我们的帖子布局只是在顶部包含一个带有帖子标题的 h1 标签。图 3.11 展示了我们的新帖子布局。

图 3.11 使用新自定义布局的帖子
3.4.2 使用 includes
现在我们来看看 includes。与包裹内容的布局不同,include 只是将内容插入到文件中的简单内容。包含文件的格式是以下 Liquid 代码:{% include file %}。Jekyll 将在这些文件的新文件夹 _includes 中查找这些文件。让我们通过向我们的网站添加版权声明来测试这一点。首先,创建新的 _includes 目录,然后使用以下代码作为内容。
列表 3.6 版权文件 (_includes/copyright.html)
<p>
Copyright {{ "now" | date:"%Y" }}
</p>
这里使用的 Liquid 代码显示的是基于网站上次构建时的当前年份。过滤器 date 接收一个格式化字符串,在这种情况下,只是请求年份。值 "now" 代表当前时间。记住,静态网站是静态的,所以你的静态生成网站不会在纽约市新年钟声敲响后的第二秒神奇地显示新的值,但一旦你重建网站,每个使用此 include 的模板都将具有正确的值。要使用此值,修改默认布局文件。
列表 3.7 更新的默认布局文件 (_layouts/default.html)
---
title: Default title
---
<html>
<head>
<title>{{ page.title }}</title>
</head>
<body>
{{ content }}
{% include copyright.html %} ❶
</body>
</html>
❶ 此 include 指定内容将直接在关闭 body 标签之前。
如果你现在重新加载你的博客,你会在每个页面上看到版权声明,如图 3.12 所示。

图 3.12 非常重要的版权声明,现在显示在我们的网站上
3.5 创建附加文件
当你使用 Jekyll CLI 搭建一个博客时,它会创建一个名为 about.html 的附加文件。当你在不是博客文章的页面上工作时,你可以在你网站的根目录中简单地创建任何 HTML 或 Markdown 文件,Jekyll 会将其包含在最终的构建中。虽然这基本上就是全部内容,但让我们向博客添加一个新页面以确保这个过程深入人心。列表 3.8 显示了一个在网站上常见的联系我们页面的内容。
列表 3.8 新的联系方式页面 (/contact.md)
---
layout: default
title: Contact Us
---
## Contact Us
Please send us an email at some random email address
that never gets checked. Or you call us at 555-555, but we
probably won't answer. We don't do faxes because it's 2020.
首先,请注意,虽然 Jekyll 使用 .markdown 扩展名,但如果你更喜欢,你也可以使用 .md。一旦保存,Jekyll 会自动使页面在 /contact.html 路径下可用。如果你打开 Jekyll 生成的关于页面(about.markdown),你会注意到它在前置部分使用了一个 permalink 值来指定另一个路径,/about/。如果你喜欢这种文件命名风格,你还可以将其添加到联系页面的前置部分。
提示:如果你遇到与 Jekyll 相关的问题、错误或关于 Gemfiles 的警告,一个常见的解决方案是运行 bundle update 或使用 bundle exec jekyll serve。
3.6 与数据一起工作
你已经看到了如何在 Jekyll 中处理博客文章和简单页面,但 Jekyll 还允许你处理数据。这些数据可以是任何东西:可能是一份博客作者的列表及其联系信息。你的网站页面可以读取这些数据,然后在模板中显示它。
首先,你需要创建一个 _data 文件夹。在这个文件夹中,你可以创建 JSON、CSV、TSV 或 YAML 格式的文件。文件的名称将控制它在模板中如何可用。让我们考虑一个简单的例子。(本节中的所有列表都可以在本书的 GitHub 仓库的 chapter3/blog2 文件夹中找到。)
列表 3.9 我们的博客作者 (/_data/authors.json)
[
{
"name":"Raymond Camden",
"website":"https://www.raymondcamden.com",
"twitter":"raymondcamden"
},
{
"name":"Brian Rinaldi",
"website":"https://remotesynthesis.com/",
"twitter":"remotesynth"
}
]
这里的数据是任意的,可以是任何对你网站有意义的任何内容。要访问这些数据,Jekyll 将使其在模板中可用,作为 site.data.authors。你还可以在数据文件夹中使用子目录,如果你这样做,子目录的名称将被添加到变量结构中。如果我们的 authors.json 文件在一个名为 people 的子目录中,那么访问该数据的新的变量将是 site.data.people.authors。现在让我们编辑博客的主页以列出我们的作者。
列表 3.10 更新后的主页,包含作者 (/index.markdown)
---
layout: default
title: My Blog
---
<h1>Posts</h1>
<ul>
{% for post in site.posts %}
<li><a href="{{ post.url }}">{{ post.title }}</a>, written
{{ post.date}}</li>
{% endfor %}
</ul>
<h2>Our Authors</h2>
<ul>
{% for author in site.data.authors %} ❶
<li>
<a href="{{site.website}}">{{ author.name }}</a> -
<a href="https://twitter.com/{{author.twitter}}">@{{author.twitter}}</a>
</li>
{% endfor %}
</ul>
❶ 遍历作者数组
变化从帖子列表之后开始。你可以看到正在使用 Liquid 标签遍历站点数据。对于每个作者,我们显示他们的名字,并提供链接到他们的网站以及他们的 Twitter 个人资料。图 3.13 展示了这是如何显示的。

图 3.13 显示的作者是由一个 JSON 文件驱动的。
3.7 配置你的 Jekyll 博客
如本章前面所述,Jekyll CLI 创建了一个配置 Jekyll 如何工作的文件。这个文件名为 _config.yml,正如你可以通过扩展名看到的,它使用 YAML 进行格式化。
注意:YAML 是一种“简单”的基于文本的格式,但有时可能会有些令人困惑。要了解更多关于语法的知识,请参阅官方网站yaml.org/。
你可以配置许多值,所以为了获得设置的全列表,请确保查看 Jekyll 文档jekyllrb.com/docs/configuration/options/,但以下是一些你可能立即想要了解的值:
-
title—你可以通过在配置文件中指定一个值来为网站指定一个全局标题。这将在模板中作为 site.title 可用。请注意,配置文件中指定的任何值都可作为 site 变量使用。
-
exclude 和 include—指定要忽略或强制包含的文件和文件夹。默认情况下,点文件(Dotfiles)不会被包含,因此当指定时,这允许你将它们包含在内。当与非常大的站点一起工作时,排除功能非常好,因为它允许你在开发中忽略某些文件集,但在生产中不忽略。
-
永久链接—在本章前面,我们演示了 Jekyll 博客文章如何在 URL 中包含分类。这是因为永久链接的默认值:/:categories/:year/:month/:day/:title:output_ext。为了“修复”这个问题,你可以指定一个永久链接值,移除 :categories: 部分。你可以在你的永久链接字符串中使用多个标记,并且你可以在
jekyllrb.com/docs/permalinks/#placeholders找到它们的文档。
3.8 生成你的网站
当你在运行 Jekyll 服务器时,你可能已经注意到它将结果输出到名为 _site 的目录。这是你的 Jekyll 构建默认输出目录。你可以通过在命令行中运行 jekyll build 来手动生成构建。你将获得操作报告,如图 3.14 所示。

图 3.14 请求 Jekyll 构建你的网站的输出
你可以通过使用 --destination 标志来配置输出目录,例如,jekyll build --destination output。图 3.15 展示了上一节博客的这种外观。

图 3.15 由 Jekyll 构建命令生成的 HTML 输出
3.9 构建 Jekyll 博客
由于 Jekyll 可以直接搭建一个博客,技术上我们在这里没有太多需要讨论的,但演示某人如何通过修改外观和感觉来修改博客的过程会很好。你之前已经看到了如何构建自定义布局,但有许多主题可以给你一个完整的视觉和感觉,而你几乎不需要做任何工作。
这的一个很好的例子是 Start Bootstrap 的“Clean Blog Jekyll”主题(startbootstrap.com/themes/clean-blog-jekyll/)。这是一个免费的博客主题,你可以用它来开始你的项目,当然,你也可以将主题作为起点,在这里那里做一些小的修改,使其恰到好处。
你可以在主题的 GitHub 仓库中找到安装说明,包括将主题添加到由 CLI 构建的博客以及使用仓库副本。这个选项相对容易一些,所以下载一个仓库的 zip 文件并解压它。
解压后,切换到你的终端中的目录并使用 bundle exec jekyll serve 运行 Jekyll。这将启动本地博客,如图 3.16 所示。

图 3.16 运行下载的 Jekyll 主题的输出
特别注意服务器地址。与之前从网络服务器根目录开始的示例不同,这个是从子目录开始的。我们稍后会解决这个问题,但为了确保一切正常,请在浏览器中打开 URL,你应该会看到使用默认博客文章的主题(图 3.17)。

图 3.17 本地运行的 Clean Blog
现在是时候开始定制这个博客并使其成为你自己的了。使用 ctrl+c 关闭当前运行的服务器并打开 _config.yml 文件。以下代码显示了更改后以作者个人化为目的的配置文件上半部分。
列表 3.11 新的配置值(/_config.yml)
title: My Blog
email: raymondcamden@gmail.com
description: My New Blog!
author: Raymond Camden
#baseurl: "/startbootstrap-clean-blog-jekyll"
#url: "https://startbootstrap.github.io"
# Social Profiles
twitter_username: raymondcamden
github_username: cfjedimaster
facebook_username:
instagram_username:
linkedin_username:
前四个值已被更改为与我的信息匹配,但你应该绝对在这里输入其他内容。由于 baseurl 和 url 在我们的测试中不需要,所以我将它们都注释掉了,我们可以接受 Jekyll 将使用的默认值。在社交资料中,我指定了一些我想分享的值。博客主题将根据这些值进行不同的渲染。保存你的更改并再次运行服务器。现在当你查看它时(注意新的 URL),你会看到自定义的值,如图 3.18 所示。

图 3.18 博客现在有自定义的标题和其他值。你的将看起来不同。
这很简单,对吧?你在这里看到的是主题在配置中暴露的值,然后被选中并用于显示。让我们再进行一个更改。虽然那个头部图片很漂亮,但我们可以选择另一个。有一个很棒的网站,Unsplash.com,它提供免费的美丽股票摄影。如图 3.19 所示,你可以看到我选择的图片(unsplash.com/photos/V705bwrTnQI)由 Annie Spratt (unsplash.com/@anniespratt)提供。

图 3.19 来自Unsplash.com网站的免费股票摄影
下载图片后,将其重命名为 xmastree.jpg,并保存在博客的 img 文件夹中。Unsplash 的默认图片可能相当大,所以书中仓库中的副本已经被调整大小。博客的主页可以在 index.html 文件中找到。打开它,你会看到它相当简短。
列表 3.12 博客主页 (/index.html)
---
layout: home
background: '/img/bg-index.jpg'
---
在这种情况下,博客的主题处理获取博客文章和渲染它们的逻辑,所以为了更新顶部的较大图片,你只需简单地编辑背景值。
列表 3.13 更新博客主页 (/index.html)
---
layout: home
background: '/img/xmastree.jpg'
---
保存此更改并重新加载博客(图 3.20)。

图 3.20 我们的新博客标题(而且,你可以选择任何东西!)
我们最后要做的就是做一些清理工作。博客自带六篇博客文章。通常在处理一个新的博客主题时,我会删除除了保留作为后续博客文章模板的那一篇之外的所有文章。例如,删除 2020-01-26-dinosaurs.html。打开它,移除示例内容,并更改标题和日期值。
列表 3.14 更新的博客文章
---
layout: post
title: "Welcome to my blog"
subtitle: "I'm so excited!"
date: 2020-08-24 12:00:00 -0400
background: '/img/posts/01.jpg'
---
<p>
This is my cool blog!
</p>
如前所述多次,请随意使用你自己的标题、副标题和内容。日期也应该更适合你阅读这本书的时候。你也可以更改图片,如果你愿意,换成你喜欢的。最后,虽然不是必需的,你应该重命名文件。记住,在 front matter 中的日期将优先于文件名中的日期。但为了维护,文件名应该更好地代表内容,例如,2020-08-24-welcome.html。保存你的更改后,你可以重新加载博客并点击你剩余的博客标题,以欣赏它的全部风采(图 3.21)。

图 3.21 新编辑的博客条目
要继续这个博客,你只需继续添加新的博客文章,主题会自动为你添加它们。
3.10 进一步使用 Jekyll
虽然我们展示了 Jekyll 的一些酷炫功能,但还有很多我们没有空间涵盖。以下是一些这些主题:
-
虽然 Jekyll 专注于构建博客,但它有一个名为 Collections (
jekyllrb.com/docs/collections/)的功能,允许你以你想要的任何形式定义自己的数据列表/集合。这对于文档网站或其他相关内容可能很有用。 -
Jekyll 还内置了对分页的支持(
jekyllrb.com/docs/pagination/),提供了一种快速设置每页有多少项以及创建代码可以用来创建链接到内容的前一页和下一页的变量的方法。 -
对于 Jekyll 无法满足的情况,它有一个插件系统 (
jekyllrb.com/docs/plugins/),允许您扩展其功能。但这将需要您编写 Ruby。 -
最后,别忘了 GitHub 对基于 Jekyll 的网站有原生支持,适用于包含内容(如文档)的仓库。
如需更多信息,包括支持链接,请参阅 Jekyll 社区页面 jekyllrb.com/docs/community/.
在下一章中,您将了解如何构建一个专注于提供文档的网站。您将查看 Hugo 静态网站生成器,它可能是目前最快的生成器之一(这使得它非常适合大型网站)。
摘要
-
Jekyll 是一个专注于构建博客的静态网站生成器。
-
Jekyll 使用 Liquid 作为模板语言,并允许您使用 Markdown 或 HTML 编写其他页面。
-
Jekyll 使用 Ruby 进行安装,因此在 Windows 下可能(可能)会遇到更困难的情况。
-
Jekyll 支持使用 JSON、CSV、TSV 和 YAML 编写的全局数据文件。
内页背板

4 构建文档网站
本章涵盖了
-
理解典型文档网站的需求
-
为管理文档选择无头 CMS
-
为文档网站选择静态网站生成器
-
安装和配置 Hugo
-
设置 Netlify CMS 开放编辑和建模
-
配置 Netlify 和 GitHub 以进行用户身份验证
-
在 Netlify CMS 中编辑内容
Jamstack 在内容为中心的网站上一直表现出色,甚至从静态网站生成器的早期阶段开始就是这样。静态 HTML 和 CSS 完美地适用于快速高效地显示内容;因此,内容网站非常适合使用 Jamstack 工具进行预渲染。这就是为什么文档网站一直是 Jamstack 最明显的用例之一。
文档网站在始终具有使用 Jamstack 的额外优势:
-
通过源控制轻松地对基于文件的内容进行版本控制
-
通过如 GitHub 拉取请求等流程接受贡献和修正的方式
-
事实上,在许多情况下,作者对这些开发工具的技术能力很强
选择 Jamstack 作为文档网站的最大缺点通常是编辑内容所需的工具不够先进,无法满足内容作者和编辑的需求。然而,近年来,用于文档网站(或一般以内容为中心的网站)的 Jamstack 工具、服务和库已经得到了极大的改进。这些好处仍然适用,但现代 Jamstack 工具使得网站内容的编辑更加容易,接受贡献也更加方便——甚至对于那些可能不熟悉代码的人来说也是如此。在本章中,我们将探讨您使用 Jamstack 开发文档网站可用的选项,并介绍如何构建一个文档网站。
4.1 文档网站的需求
显然,没有一种单一的文档网站类型。例如,有技术文档,如软件文档或 API 文档,以及最终用户文档,如用户手册。这些需求可能有所不同,但它们也有一些共同点:
-
文档网站往往有多个,通常是众多的贡献者。在项目或政策文档的情况下,贡献者可能仅限于公司员工。然而,在开源日益主导的软件世界中,许多文档网站通常有大量的外部贡献者。
-
贡献者编辑网站内容时可能具有不同程度的技术专长。
-
文档网站旨在优化快速轻松地访问信息,优先考虑简单直接的功能和设计。
-
布局和设计侧重于可读性而非风格。
-
除了评论或可运行的示例之外,许多文档网站不包括复杂或动态的功能。
-
许多文档网站有很多内容,但这些内容相对不经常改变。一个典型的网站可能会定期进行重大更新,偶尔之间会有一些小更新。
4.1.1 示例网站需求
我们将为一种名为 LOLCODE 的冷门编程语言构建技术文档(www.lolcode.org/)(见图 4.1)。LOLCODE 旨在对基于 lolspeak(一种与猫的互联网迷因相关的语法错误语言)的编程语言进行幽默的诠释。这些文档基于 Justin Meza 编写的 LOLCODE 规范(mng.bz/RE7D)。

图 4.1 展示 LOLCODE 语言、安装细节以及我们将用于示例文档网站的规范链接的lolcode.org网站
随着 LOLCODE 的采用不可避免地扩大,我预计这个网站会变得相当大,因此优化构建速度将成为一个重点。由于这是一个开源语言,我还希望能够使第三方作者能够轻松贡献。尽管我预计大多数贡献者都将具有高度的技术专长,但他们可能不是构建 Jamstack 网站方面的专家。尽管如此,我仍希望通过使他们能够不经过手动分叉、本地安装、更新然后创建拉取请求的 Git 工作流程来鼓励贡献。
现在我们已经了解了我们文档网站的需求,让我们来看看我们有哪些工具可以满足这些需求。
4.2 选择合适的工具
我们的需求并不特别复杂。我们需要能够为广泛的文档生成可能很多的内容页面,并且我们需要第三方内容贡献者能够编辑内容,而无需深入了解网站是如何构建的。为了实现这一点,我们需要一个合适的静态网站生成器和无头内容管理系统(CMS)。CMS 将提供编辑界面,使内容贡献者能够更轻松地在网站上编写和编辑内容。
4.2.1 什么是无头 CMS?
我们已经讨论了很多关于静态网站生成器的内容,但还没有讨论无头 CMS。无头 CMS是一个相对较新的概念。这个名字来源于它们将后端(即 CMS 提供的实际内容编辑和管理工具)与“头部”(即应用程序的前端,在这种情况下是一个网站)解耦的想法。
传统的内容管理系统(CMS)几乎完全是为了管理网页内容而创建的。正因为如此,内容的管理与其显示方式紧密相连。例如,在一个典型的 WordPress 网站上,后端的内容管理由 WordPress 提供,但前端网站也是基于 WordPress 构建的(即,与 WordPress 紧密耦合)。
这种紧密耦合意味着内容不可重用。主页上的标题也可能出现在着陆页上,但更新一个实例不会更新另一个实例。由于内容是为网络设计的,因此它不能轻易地用于像移动应用这样的东西。
传统的 CMS 也不是为 Jamstack 设计的。传统 CMS 的前端页面是服务器端渲染的,无法利用 Jamstack 架构提供的改进速度和安全性能。
无头 CMS 通过提供与前端网站解耦的后端内容编辑和管理工具来解决这些问题。目前有越来越多的无头 CMS 选项可供选择,但它们分为两个不同的类别,这些类别决定了前端网站如何访问内容:
-
基于 API 的无头 CMS—您的内容由 CMS 提供商存储,并通过 API 由您的网站、移动应用或其他应用程序访问。由于基于 API 的无头 CMS 中的内容与物理文件无关,它们能够轻松处理内容对象的重复使用,并更容易管理内容对象之间或内容块内内容对象的复杂关系。
-
基于 Git 的无头 CMS—这些 CMS 不存储您的内容。相反,内容存储在 Git 仓库中,通常作为 Markdown 用于长篇内容,以及 YAML 或 JSON 用于数据。CMS 本质上是一层工具,通过易于理解的内容编辑器通过 Web 界面管理内容,这些编辑器可能不习惯手动编辑基于文件的内容。
注意:要深入了解基于 Git 和基于 API 的无头 CMS 的优缺点,请参阅 Bejamas 的这篇详细文章:mng.bz/2j19。
对于我们的示例用例,基于 Git 的 CMS 的一个好处是它仍然允许基于 Git 的编辑工作流程和版本历史记录,这些历史记录可以跟踪更改,并且可以通过 GitHub 或其他 Git 项目托管提供商公开访问。这对于技术文档项目来说可能是理想的,尤其是对于开源项目,正如我们的案例一样。因此,我们的示例项目将使用基于 Git 的解决方案。
4.2.2 无头 CMS 选项
既然我们已经确定基于 Git 的解决方案是我们的选择,让我们来看看可用的选项。
Forestry
Forestry (forestry.io) 是一个商业化的基于 Git 的 CMS 解决方案。它内置了对所有最流行的静态站点生成器的支持,并与大多数主要的静态托管提供商集成(图 4.2)。在撰写本文时,它提供免费账户,支持多达五个编辑器,尽管免费网站在三个月不活跃后会自动存档。

图 4.2 使用 Forestry 的所见即所得(WYSIWYG)页面编辑器编辑内容页面
Publii
Publii (getpublii.com/),如图 4.3 所示,与其他选项不同,它是一个可安装的开源桌面应用程序,而不是基于网络的界面。它提供了多种编辑内容的选择,包括 WYSIWYG 编辑器、类似于 WordPress 新“Gutenberg”界面的块编辑器以及直接的 Markdown 编辑器。Publii 不仅提供与其他基于 Git 的 CMS 一样的编辑工具,还充当静态站点生成器,支持广泛的托管选项。

图 4.3 Publii 的桌面应用程序界面提供了多种编辑内容的选择:WYSIWYG 编辑器、类似于 WordPress 新“Gutenberg”界面的块编辑器以及直接的 Markdown 源代码编辑。
Prose
Prose (prose.io/),如图 4.4 所示,也是一个独特的选项。这是一个完全免费的工具,可以连接到您的 GitHub 账户,为您提供访问任何与您的账户关联的存储库中任何文件的基于网络的编辑器。虽然可以使用 Prose 编辑器编辑代码和数据文件,但其重点是提供带有元数据(通常称为前端元数据)的 Markdown 内容的更好的编辑体验。它确实提供了一个简单的 Markdown 预览,但不是真正的 WYSIWYG 体验。

图 4.4 Prose 为您 GitHub 账户中的任何存储库中的任何 Markdown 文件提供了带有简单预览功能和前端元数据编辑的 Markdown 编辑器。
Netlify CMS
Netlify CMS (www.netlifycms.org/),如图 4.5 所示,是一个开源的内容管理系统工具。其独特之处在于能够配置它以与几乎所有静态站点生成器协同工作。它由 Netlify 构建,并由 Netlify 主要维护,因此一些功能与 Netlify 的托管服务无缝集成,但它也可以配置与其他提供商一起工作。

图 4.5 Netlify CMS 提供了多种高级小部件,用于编辑帖子的长格式 Markdown 内容以及前端元数据。
4.2.3 为什么选择 Netlify CMS?
我们的项目将是一个关于神秘编程语言 LOLCODE 的技术文档。对于这个项目,我们将使用 Netlify CMS。选择 Netlify CMS 的原因有很多:
-
Netlify CMS 是一个自由许可的开源项目(它使用 MIT 许可证)。这意味着我们可以将整个文档项目开源,而无需担心许可限制。
-
Netlify CMS 有一个开放作者功能,允许我们给任何拥有 GitHub 账户的人访问权限,以便他们对文档做出贡献。用户将完全访问内容管理系统,但他们的贡献将自动以他们的名义提交为拉取请求,这意味着更改不会影响网站,直到我们接受它们。
-
没有限制允许做出贡献的用户数量,也没有与用户数量相关的成本。
4.2.4 静态站点生成器(SSG)选项
任何 SSG 都可以用于文档站点。尽管如此,实际上有许多 SSG 是专门针对文档的:
-
Docsify (
docsify.js.org)—一个基于 JavaScript 的 SSG,将文档作为单页应用(SPA)提供服务。Docsify 与其他解决方案的不同之处在于它不会生成静态 HTML 文件,而是在浏览器运行时解析 Markdown 内容,这意味着应用程序不需要重建以反映更改或新内容。 -
Slate (
github.com/slatedocs/slate)—一个基于 Ruby 的解决方案,它是在 Middleman SSG 的基础上构建的,专门用于创建 API 文档。它还作为一个支持多种语言代码示例的单页应用(SPA)运行,允许用户切换到与他们相关的语言标签。 -
MkDocs (
www.mkdocs.org/)—一个基于 Python 的解决方案,强调在生成大量页面时的速度。它提供了许多主题,包括许多社区构建的主题。 -
Docusaurus (
docusaurus.io/)—一个基于 JavaScript 的静态站点生成器,使用 React,Docusaurus 自带许多文档相关的功能和布局,包括文档版本控制和国际化(i18n)等功能。 -
Hugo (
gohugo.io/)—一个流行的基于 Go 的 SSG,也专注于极快的构建过程,包括内置的资产管理。虽然它不是专门的文档解决方案,但 Hugo 有一个庞大的用户社区,拥有大量的社区构建的主题,其中许多是为文档特别设计的。
4.2.5 为什么选择 Hugo?
我们将使用 Hugo 来处理我们的 LOLCODE 技术文档项目。它将轻松处理我们不断增长的内容量,同时保持构建时间短。Hugo 通过二进制文件安装,这意味着希望在本地上运行项目的贡献者不需要复杂的环境。它还拥有详尽和详细的文档以及大量的社区帖子,这使得我们能够轻松找到解决可能遇到任何潜在问题的解决方案。
即使它不是一个专门的文档解决方案,Hugo 也有大量的社区主题,这些主题提供针对文档站点的设计和功能。以下是一些例子:
-
Ace Documentation 是基于 Bootstrap 的文档主题。
-
DocuAPI 针对多语言 API 文档设计。
-
Dot 旨在用于支持中心或知识库形式的文档。
-
Hugo Book 是一个具有内置搜索功能的极简主义书籍风格主题。
-
Techdoc 也是一个极简主义书籍风格主题。
-
Kraiklyn 是为创建单页文档而设计的。
在我们的示例中,我们将选择 Hugo Book 主题。我选择这个主题并不是出于任何技术原因,而是因为我认为它提供的简单、干净的布局非常适合我们正在创建的语言文档。
4.3 构建示例站点
让我们开始构建我们的文档站点。我们将从安装 Hugo 和设置主题开始,然后配置站点以与 Netlify CMS 一起工作。
4.3.1 安装 Hugo
安装 Hugo 有多种方式,包括简单地下载二进制文件。虽然这种方法适用于所有支持的平台(MacOS、Windows 和 Linux),但它有一些复杂性,因为你可能希望将其放置在路径上,以便只需使用 hugo 命令行命令(而不是二进制文件的完整路径)即可轻松调用。需要注意的是,如果你选择二进制安装或已经安装了 Hugo,你需要 Hugo 0.68 或更高版本的扩展版。
使用包管理器安装 Hugo 是更好的选择。
在 MacOS 或 Linux 上安装
你可以使用 Homebrew 在 MacOS 或 Linux 上安装 Hugo:
brew install hugo
在 Windows 上安装
你可以使用 Chocolatey 在 Windows 上安装 Hugo:
choco install hugo-extended -confirm
确认安装
确认你的 Hugo 安装工作正常:
hugo version
这应该返回以下类似的内容(请注意,版本号自本文撰写以来可能已更改):
Hugo Static Site Generator v0.74.3/extended darwin/amd64 BuildDate: unknown
4.3.2 创建新的 Hugo 网站
我们计划使用 Hugo 作为我们的静态站点生成器,Netlify CMS 作为基于 Git 的无头 CMS,以及 Hugo Book 作为我们的站点主题,来构建 LOLCODE 稀有语言的科技文档网站。第一步将是使用 Hugo 为我们的网站生成一个新的站点骨架。
要使用 Hugo 创建新站点,请使用 new site 命令后跟你想要站点创建的目录名称:
hugo new site lolcode-docs
cd lolcode-docs
这将创建一个包含以下内容的 Hugo 站点骨架:
├── archtetypes
│ └── default.md
├── content
├── data
├── layouts
├── static
├── themes
├── config.toml
正如你所见,骨架中没有默认内容或主题。大部分情况下,Hugo 只会生成目录结构和基本的配置文件。以下是这些文件和文件夹的用途:
-
在 Hugo 中,架构体(archetypes)代表应用程序中的不同内容类型。这些是网站将包含的不同类型内容的元数据(即元数据)模板。例如,你可能有一个用于博客文章的架构体,它定义了博客文章将包含的元数据。虽然不是必须为所有内容创建架构体,但这样做允许你使用 hugo new 命令与该类型一起生成具有正确设置的新的页面。所以,如果我们有一个帖子类型,我们可以输入命令 hugo new post/my-new-post.md 来创建一个名为“my-new-post”的博客文章。
-
Hugo 网站的全部内容都存在于内容文件夹中。这些内容可以位于该文件夹内的任何目录结构中。当网站生成时,内容文件夹中的每个内容项都会创建一个页面。例如,位于 /content/posts/my-new-post.md 的 Markdown 文件将在网站中生成一个位于 /posts/my-new-post/ 的页面。
-
数据文件夹包含所有数据文件(YAML、TOML 或 JSON)。Hugo 通过 .Site.Data 对象将这些文件提供给网站。例如,一个 authors.yaml 的数据文件将作为 .Site.Data.Authors 可用。
-
布局文件夹包含 Hugo 将用于生成页面的所有布局模板。通常,这个文件夹用于没有安装主题的网站。如果两者都存在,Hugo 将首先使用布局中的更具体文件(我们稍后会利用这一点)。Hugo 布局使用 Go 模板语言编写以生成标记。
-
静态文件夹包含所有应该直接移动到网站中而不需要处理的文件。这些通常是图像、JavaScript 或样式表等资产,你不想让 Hugo 修改。静态文件夹中的所有内容都将放置在网站根目录下。例如,如果你有一个 /static/images 文件夹,里面装满了网站的图像,那么这些图像最终会出现在网站的 /images 下。为了说明这一点,请下载 LOLCODE 标志并保存到 /static/images(生成的文件应该是 /static/images/logo.png)。
-
主题文件夹是放置你下载的第三方主题的地方。你可以在 themes.gohugo.io 找到大量这些主题。你还可以在这个目录中创建自己的主题。要设置网站的主题,你需要在 Hugo 的 config.toml 中定义一个主题变量(我们稍后会看到这一点)。
-
config.toml 是 Hugo 的配置文件,使用 TOML 编写。Hugo 提供的基本配置包括 baseUrl、languageCode 和 title。
-
让我们先为我们的文档网站添加一些默认内容。我已经提供了一个包含书中示例网站 Markdown 内容的 zip 文件,位于书籍 GitHub 仓库的
mng.bz/1jRy。下载这个 zip 文件并将其解压到您新网站的内容文件夹 /content 中。现在您应该有 /content/_index.md,这是主页,以及 /content/docs,它将包含一些带有网站文档的 Markdown 文件。
4.3.3 设置 Hugo Book 主题
我们将安装 Hugo Book 主题作为一个子模块。在我们能够这样做之前,我们需要确保我们的新项目被初始化为一个 Git 仓库。使用项目目录根部的终端/命令行,初始化一个新的仓库:
git init .
接下来,让我们将 hugo-book 添加为一个子模块。以这种方式安装主题将允许我们在 GitHub 仓库中的主题有任何更改时保持我们的项目更新:
git submodule add https://github.com/alex-shpak/hugo-book themes/book
最后,让我们配置 Hugo 以使用新安装的主题。我们将打开项目根目录中的 config.toml 并进行以下更改:
-
将标题更改为“LOLCODE 文档。”
-
添加一个主题变量来设置主题为“book”。
-
添加额外的 Hugo Book 配置。该主题提供了大量的配置选项,但我们只会添加使用 BookSearch 参数进行搜索的能力。
这是完成后的配置文件的样子:
baseURL = “http://example.org/”
languageCode = “en-us”
title = “LOLCODE Documentation”
theme = “book”
[params]
BookSearch = true
目前,我们保持 baseURL 值不变。此值代表站点的主机名和根路径,可以在 Hugo 布局代码中使用。一旦我们将网站部署到 Netlify,我们可以更新它,但现在它不会影响我们的项目。
现在我们已经准备好测试我们的网站。提醒一下,确保你已经下载了 LOLCODE 标志并将其保存在 /static/images 中。从项目根目录的终端/命令行运行 hugo serve 以启动 Hugo 的本地网络服务器。这将构建您的网站并在 http://localhost:1313 上使页面可查看。如果您在浏览器中打开网站,它应该看起来像图 4.6。

图 4.6 在我们的本地 Hugo 网络服务器上运行的 LOLCODE 文档网站
随意浏览或搜索。你会发现我们现在有一个完全功能的文档网站。我们可以选择让它保持原样,并在 GitHub 上简单地维护内容,但正如我们之前提到的,我们想要包含一个 CMS 并允许第三方贡献者访问。让我们这么做。
4.3.4 安装 Netlify CMS
现在我们有了内容和功能网站,我们可以启用它使用 Netlify CMS 进行编辑。在我们这样做之前,我们需要确保我们已经将我们的项目发布在 GitHub 上。如果您还没有这样做,请执行此操作。
Netlify CMS 没有安装程序。相反,你创建管理员并添加必要的文件到其中。以下是步骤:
-
创建一个名为 admin 的文件夹。由于这是 Hugo,我们需要将其放置在静态目录中(即 /static/admin),因为我们不希望文件被 Hugo 处理。
-
在 /static/admin 中添加一个 index.html 文件,该文件将加载运行 CMS 管理员的脚本。我们将使用 Netlify CMS 文档中提供的代码:
<!doctype html> <html> <head> <meta charset=”utf-8” /> <meta name=”viewport” content=”width=device-width, ➥ initial-scale=1.0” /> <title>Content Manager</title> </head> <body> <!--Include the script that builds the page and powers Netlify CMS -> <script s”c="https://unpkg.com/netlify-cms@².0.0/dist/netlify- ➥ cms”js"></script> </body> </html> -
在 /static/admin 中创建一个 config.yml 文件,该文件将包含 Netlify CMS 的配置。这最终将包含我们的完整内容模型定义,但现阶段我们只添加所需的基配置信息(注意:您需要将 remotesynth/lolcode-docs 替换为您自己的 GitHub 仓库信息):
publish_mode: editorial_workflow media_folder: static/images public_folder: /images backend: name: github branch: master repo: remotesynth/lolcode-docs open_authoring: true
让我们来看看这个配置文件中有什么内容:
-
将 publish_mode 设置为 editorial_workflow 会为内容创建一个草稿、审核和批准的工作流程。如果不设置此选项,内容将在保存时自动发布。编辑工作流程是启用开放撰写的必要条件。
-
media_folder 定义了网站源中可以上传图片和其他媒体文件的文件夹。然后 public_folder 定义了发布网站中 media_folder 的路径。
-
后端部分定义了 Netlify CMS 使用的作为网站后端的仓库细节。默认是 Git Gateway,这是一个开源 API,它代理了您网站上的用户和 Git 仓库之间的请求。它与 Netlify Identity(Netlify 的身份验证解决方案)无缝配合。然而,为了在 Netlify CMS 中实现开放撰写,我们必须使用 GitHub,它使用 GitHub 的 OAuth 身份验证允许用户访问。然后我们定义网站的分支和仓库(示例代码指向我的仓库,所以请确保仓库反映了您的 GitHub 仓库)。最后,我们将 open_authoring 设置为 true 以允许外部贡献者,而无需邀请他们。
-
我们已经配置了 Netlify CMS 的基本设置,但暂时还不能使用,原因有两个:我们尚未为 Netlify CMS 模型任何内容,因此它不知道正在编辑的内容是什么,并且我们尚未设置 Netlify 或 GitHub 以允许用户进行身份验证。
4.3.5 在 Netlify CMS 中建模内容
在开始编辑内容之前,Netlify CMS 需要了解其结构。它通过在 config.yml 中定义集合和字段来实现这一点。根据您网站复杂性的不同,为 Netlify CMS 建模内容可能是一项相当复杂的工作。幸运的是,我们的文档网站的内容相当简单。
集合
集合是 Netlify CMS 中的内容类型。这可以代表单个页面、具有共同属性的页面组或数据文件(例如 YAML、TOML 或 JSON)。Netlify CMS 中有两种集合类型:
-
文件夹集合代表一组内容文件,这些文件都位于单个文件夹中。重要的是要注意,截至本文撰写时,Netlify CMS 不支持子文件夹,这意味着如果你有/content/docs/topic-one 和/content/docs/topic-two,它们不能都使用/docs 文件夹定义,并且需要三个单独的集合定义。
-
文件集合代表一个或多个单个文件:Markdown 或 HTML 中的一个页面(或多个页面),或者 YAML、TOML 或 JSON 中的一个数据文件(或多个文件)。当在 Markdown 或 HTML 中引用页面时,通常使用特定页面的文件类型,该页面不与其他任何网站页面共享属性,例如网站的主页。
我们的文档站点有两种内容类型,一种代表主页,它是一个文件类型,另一种代表文档,它是一个文件夹类型。将此置于后端配置块下方,紧接在包含 open_authoring: true 的行之后:
collections:
- name: pages
label: Pages
files:
- nam“: "h”me"
labe“: "Home P”ge"
fil“: "content/_index”md"
- name: docs
label: Docs
folder: /content/docs
create: true
extension: md
slug: '{{slug}}'
让我们探索到目前为止我们已经配置了什么:
-
每个集合都必须有一个名称,该名称是 Netlify CMS 中集合的唯一标识符。你可以使用任何你选择的名称,但你应该避免使用空格或除破折号或下划线之外的特殊字符。同时,标签定义了在 CMS 中向用户显示集合的方式。你可以随意命名它。
-
文件夹集合代表一个包含多个文件的单一文件夹。用户将能够创建新页面(创建设置为 true),并且这些新页面将以 md 文件扩展名的 Markdown 格式。slug 字段定义了 Netlify CMS 将如何生成新文件名。在我们的例子中,我们说的是生成内容标题的安全 URL 版本(这意味着我们的字段定义必须包含一个标题字段)。
-
文件集合必须定义它包含的不同特定文件。可以有多个,并且每个都可以定义自己的字段(我们将在稍后讨论这一点)。这意味着文件集合中的每个文件不需要共享属性,但集合是从编辑角度将它们分组在一起的一种方式。我们的文档站点只有一个文件代表主页。
我们的配置仅触及了您可用的选项中的一小部分。请查看文档以获取完整的集合配置选项列表(mng.bz/7W9m)。
字段
字段表示内容对象上的不同数据属性(元数据)。例如,一篇博客文章可能有一个标题和日期属性,以及其他需要在 Netlify CMS 中定义为字段的属性。
每个字段都由一个小部件表示。Netlify CMS 中的小部件决定了如何编辑特定字段。例如,文本小部件将是一个 HTML 文本区域字段,布尔小部件将是一个切换开关,图像小部件将是一个文件选择器。Netlify CMS 自带 16 个默认小部件,涵盖了大多数用例,但您也可以定义自己的自定义小部件。
我们在内容模型中定义的每个字段都具有以下常见属性:
-
一个小部件属性,用于定义在 CMS 用户界面中用于此字段的哪个小部件。
-
一个名称,这是 Netlify CMS 中的字段名称,并且在这个字段组中应该是唯一的。您可以将其命名为任何内容,但请避免使用空格或除破折号或下划线之外的特殊字符。
-
一个必填属性,用于指定字段是否必填。如果不包括此属性,则默认为 true。
-
一个提示字段,用于定义当小部件在 CMS 用户界面中显示时将出现在工具提示中的文本。这是可选的,可以用来为用户提供额外的帮助或上下文,以便输入值。
-
一个模式字段,可以定义用于验证输入的正则表达式(regex)模式,以及当验证失败时显示的错误消息。这是可选的。
此外,每种类型的小部件都可以有特定于小部件的配置属性。请查阅文档以获取完整选项列表(www.netlifycms.org/docs/widgets/)。
我们的字段定义都很简单,因为我们的文档内容模型并不特别复杂。以下是包含集合和字段的完整配置文件(请确保使用您自己的 GitHub 存储库更新存储库)。
列表 4.1 完成的 Netlify CMS 配置文件(/static/admin/config.yml)
publish_mode: editorial_workflow
media_folder: static/images
public_folder: /images
backend:
name: github
branch: master
repo: remotesynth/lolcode-docs
open_authoring: true
collections:
- name: pages
label: Pages
files:
- name: "home"
label: "Home Page"
file: "content/_index.md"
fields:
- widget: string
name: title
label: Title
required: true
hint: >-
The title of the page
- widget: markdown
name: body
label: Content
required: true
hint: Page content
- name: docs
label: Docs
folder: /content/docs
create: true
extension: md
slug: '{{slug}}'
fields:
- widget: string
name: title
label: Title
required: true
hint: >-
The title of the page that will appear in the left hand
➥ navigation
- widget: number
name: weight
label: Weight
required: false
hint: >-
The navigation order of the page.
- widget: boolean
name: bookToc
label: Table of Contents
required: false
hint: >-
If false, the right hand table of contents will not show.
➥ Defaults to true if empty.
- widget: boolean
name: bookHidden
label: Hidden?
required: false
hint: >-
If true, the page will not list on the left hand navigation
- widget: markdown
name: body
label: Content
required: true
hint: Page content
配置就绪后,我们应该能够从命令行运行 hugo serve,然后导航到 http://localhost:1313/admin,并看到管理员界面的登录界面。
点击使用 GitHub 登录目前还不能工作(图 4.7),因为我们还没有为 Netlify 或 GitHub 配置身份验证。让我们接下来进行配置。

图 4.7 使用 GitHub 进行用户身份验证的 Netlify CMS 登录页面/admin
4.3.6 部署到 Netlify
可以使用 Netlify CMS 而不部署到 Netlify,但由于 Netlify 创建了项目,因此在启用允许用户通过 CMS 编辑内容的身份验证方面,它具有最直接的路径。我们将在后面的章节中深入探讨部署,但现在是时候介绍允许我们的 Netlify CMS 管理员中的用户的基本内容了。
首先,请确保您已将项目发布到 GitHub。再次强调,Netlify CMS 允许使用其他 Git 托管提供商,但我们将使用 GitHub 进行身份验证,因此在这种情况下,发布到那里是必需的。由于存储库将由第三方编辑,请确保将存储库设置为公开。
如果你还没有账户,你需要创建一个 Netlify 账户。Netlify 提供了一个慷慨的免费计划,这将使你能够完成这个教程。一旦你创建了 Netlify 账户,点击“从 Git 新建站点”,选择 GitHub,然后定位你的已发布仓库。如果你是第一次使用 Netlify,你需要完成一些授权步骤,以允许 Netlify 访问你的 GitHub 仓库,如图 4.8 所示。

图 4.8 在 Netlify 中从 GitHub 仓库创建新站点
Netlify 在识别我们使用的静态站点生成器及其默认设置方面做得很好。然而,我经常在使用 Hugo 的最新构建时遇到问题,所以我发现设置一个与本地运行的 Hugo 版本匹配的环境变量是最好的。从命令行输入 hugo version 以查看你正在运行的版本。例如,我的返回以下内容:
Hugo Static Site Generator v0.74.3/extended darwin/amd64 BuildDate: unknown
在设置步骤的部署设置中,点击如图 4.9 所示的“显示高级”按钮。

图 4.9 在 Netlify 中创建 Hugo 站点时的默认部署设置
点击“新建变量”按钮,然后添加一个名为 HUGO_VERSION 的变量,其值为运行 hugo version 命令返回的版本号。例如,在我的情况下,版本是 0.74.3,我已经在图 4.10 中输入了它。

图 4.10 在 Netlify 的部署设置中设置 Hugo 版本变量
最后,点击“部署站点”。几分钟后,你的站点应该已经部署完成。获取 Netlify 为你生成的站点 URL(可以在 Netlify 仪表板的“站点概览”页面找到)。让我们通过将配置文件 config.toml 中根目录下的 baseURL 值设置为 Netlify 上的 URL 来修复它。例如,我的 URL 是clever-thompson-493f7c.netlify.app/。这将修复在站点最初部署时可能看到的任何缺失样式表。我们还需要 GitHub 中配置身份验证的 URL。
4.3.7 配置 GitHub 以进行身份验证
在我们可以在 Netlify 上设置身份验证之前,我们需要在 GitHub 中设置一个 OAuth 应用程序。为此,转到设置 > 开发者设置 > OAuth 应用,并点击显示为“注册新应用程序”或“新建 OAuth 应用”的按钮,或者访问github.com/settings/applications/new。
我们需要为我们新的 OAuth 应用命名;可以是任何你想要的名称。在主页 URL 字段中,放置你的 Netlify 站点 URL(我们在上一节中收到)。描述也可以是任何你想要的。最后,授权回调 URL 需要是 https://api.netlify.com/auth/done。你可以在图 4.11 中看到这些设置。

图 4.11 在 GitHub 中设置新的 OAuth 应用程序,该应用程序可用于 Netlify 的身份验证
点击注册应用程序后,我们将获得我们的 OAuth 应用程序的客户端 ID 和客户端密钥。我们需要这些信息来设置 Netlify。
4.3.8 配置 Netlify 以进行身份验证
在我们新网站的 Netlify 控制台中,我们需要转到网站设置 > 访问控制 > OAuth。点击安装提供者按钮。
如图 4.12 所示,提供者应为 GitHub,并在客户端 ID 和密钥字段中填写我们从 GitHub OAuth 应用程序收到的客户端 ID 和客户端密钥。最后,点击安装。

图 4.12 在 Netlify 中添加 OAuth 提供者。客户端 ID 和密钥来自我们在 GitHub 中创建的 OAuth 应用程序。
4.3.9 作为管理员编辑内容
现在我们已准备好访问我们的文档网站上的内容管理员(请确保您已将我们已做的任何更改推送到 GitHub)。管理员在 /admin 下可用。例如,我的 Netlify 网站的 URL 是 clever-thompson-493f7c.netlify.app/,因此我的 Netlify CMS 管理员 URL 将是 clever-thompson-493f7c.netlify.app/admin。在继续之前,请确保您已将 GitHub 仓库设置为 Netlify CMS 配置中 repo 的值 (/admin/config.yml)。
点击“使用 GitHub 登录”按钮,我们将收到基于我们创建的 GitHub OAuth 应用的 GitHub 授权窗口,如图 4.13 所示。

图 4.13 第一次点击“使用 GitHub 登录”时显示的 GitHub 授权窗口,显示了我们在创建 GitHub OAuth 应用程序时输入的信息。
点击授权后,我们将登录并进入 Netlify CMS 编辑仪表板,如图 4.14 所示。

图 4.14 登录后的 Netlify CMS 仪表板。集合是我们已在 Netlify CMS 配置中定义的。
默认情况下,我们处于内容标签页,该标签页显示了我们在 config.yml 配置文件中先前定义的内容集合:页面和文档。默认选中页面集合。你可能还记得,这个集合只定义了一个内容项,即主页内容。你不能向页面集合中添加新页面。
当我们点击文档集合时,我们会看到一个完整的文档页面列表以及创建新文档页面的按钮。请随意点击一个进行编辑。编辑页面如图 4.15 所示。

图 4.15 在 Netlify CMS 中编辑 Docs 页面之一。我们可以看到所有在 config.yaml 字段中定义的内容属性的小部件。
页面左侧的每个小部件都代表我们在 config.yml 配置文件中为文档内容类型定义的属性。内容字段提供了一个所见即所得风格的 Markdown 内容编辑界面。页面的右侧提供了一个正在编辑的内容的预览。
尝试对内容进行一些更改。一旦您进行了更改,点击“保存”按钮。由于我们使用的是编辑流程,文档将作为草稿保存在工作流程中。我们可以将状态更改为“待审阅”或“就绪”。在我们可以将更改发布到页面之前,我们需要将状态设置为“就绪”。
此外,通过使用 Netlify 的部署预览功能,我们可以在发布之前预览网站上的更改。在页面顶部的工具栏中,我们将看到一个“检查预览”链接。点击该链接将带我们到 Netlify 的更改部署预览链接,这可能需要几秒钟。点击“查看预览”将打开网站的部署预览,并将包括我们的更改,以便我们在发布之前进行审查。
当您准备好时,将状态更改为“就绪”,点击“发布”,并选择“立即发布”。这将把更改提交到我们的 GitHub 仓库,然后触发我们的 Netlify 网站的构建并发布更改到我们的实时网站。
4.3.10 开放式作者工作流程
我们外部用户的工作流程将略有不同。让我们来看看这会是什么样子。您不需要跟随这一部分,因为这需要第二个 GitHub 账户。
一旦他们使用 GitHub 登录并授权我们的 GitHub OAuth 应用,他们将被要求分叉仓库,如图 4.16 所示。在此处点击“不分叉仓库”将退出流程,用户将无法进行任何编辑。

图 4.16 外部用户将被要求分叉仓库以获取访问 Netlify CMS 管理员权限并提交对网站的编辑。
点击“分叉仓库”将自动在用户的账户上创建我们仓库的分叉。这是用户所做的所有更改将被保存的地方。
一旦用户分叉了仓库,Netlify CMS 管理员将与我们之前用来编辑网站的相同。然而,当他们对网站进行更改时,他们没有将状态设置为“就绪”或任何发布选项的选项。相反,他们只能将状态设置为“待审阅”。这样做将自动向我们的主仓库提交包含用户所做的更改的拉取请求,如图 4.17 所示。

图 4.17 当第三方贡献者将更改设置为“待审阅”时,主 GitHub 仓库将自动提交拉取请求。为了接受更改,我们可以合并拉取请求。
为了将用户的更改合并到我们的网站上,我们需要合并拉取请求。这将更新我们的 GitHub 仓库,触发我们 Netlify 网站的更新,并将更改发布到我们的实时网站上。
4.3.11 简化开放作者工作流程
在我们完成之前,让我们对我们的网站进行最后一次编辑。目前,任何希望为文档做出贡献的外部作者都需要知道去 /admin 登录。如果我们能在文档内部提供快速链接来添加或编辑内容,那么贡献的可能性会更大。为此,我们将对模板进行一些小的修改。
我们依赖于 Hugo-Book 主题进行布局,并将其作为 Git 子模块安装。因此,我们无法直接更改 Hugo-Book 代码。尽管如此,Hugo 有一个查找顺序,用于主题模板,它首先查找最具体的匹配项。这意味着我们可以将文件放置在 /layouts 文件夹中,以覆盖 /themes 文件夹中的模板文件。当我们看到它在实际中的应用时,这会更有意义。
Hugo-Book 主题有布局文件,这些文件专门用于在模板输出中的特定点注入内容,例如在内容之前或之后。您可以在 /themes/book/layouts/partials/docs/inject 中看到这些文件。让我们通过 content-after.html 模板在每个内容页面的末尾添加链接。
要覆盖此模板,在您的 /layouts 文件夹中创建一个具有相同目录结构和名称的文件:/layouts/partials/docs/inject/content-after.html。由于 Hugo 认为 /layouts 中的文件比 /themes 中的文件更具体,通过创建一个完全相同的路径文件,Hugo 将使用它来替代主题文件。
将以下模板代码放置在文件中:
{{ if ne .RelPermalink "/"}}
{{ $edit_url := print "/admin/#/edit" .RelPermalink }}
<p><a href="{{$edit_url}}" class="book-btn">Edit this Page</a>
<a href="/admin/#/collections/docs/new" class="book-btn">Add a New
➥ Page</a></p>
{{ end }}
让我们看看这个模板代码在做什么。首先,它通过检查 Hugo 提供的名为 .RelPermalink 的变量来查看我们是否在主页上,该变量包含当前页面的相对路径。在 Hugo 中,函数 ne 表示“不等于”,因此我们检查相对路径是否不等于“/”。我们这样做是为了不在主页上提供编辑链接。其次,我们根据当前页面组装一个指向管理员的 URL。最后,我们使用该 URL 添加一个“编辑此页”链接,这将直接带他们进入编辑当前查看的页面内容。我们还包含了一个“添加新页面”链接,该链接直接链接到创建新的文档页面。
在将此页面提交并推送到我们的仓库或本地运行后,我们应该看到这些新链接添加到我们网站上的任何文档页面,例如图 4.18 中所示。

图 4.18 一旦我们添加了模板代码,文档页面将会有“编辑此页”和“添加新页面”链接。
4.4 接下来是什么?
正如我们所见,Jamstack 可以成为文档网站的一个强大解决方案。像 Netlify CMS 这样的开源解决方案允许使用基于 Git 的工作流程,同时仍然让内容编辑者享受到易于使用的所见即所得编辑体验,甚至允许第三方贡献——这在非 Jamstack 解决方案中并不容易实现。
虽然 Netlify CMS 的编辑体验功能齐全,但有些人可能觉得它缺乏一些非 Jamstack 工具(如 WordPress)的打磨。值得记住的是,有许多提供不同编辑用户体验的替代方案。如果你在寻找更类似 WordPress 的体验,务必探索基于 API 的无头 CMS 选项,如 Contentful、Sanity 或 AgilityCMS,或者甚至服务如 Stackbit。
正如我们所展示的,Jamstack 是内容导向网站(如文档网站)的一个优秀解决方案,但你可能想知道它是否能够处理具有更复杂和动态用户交互的网站。在下一章中,我们将通过使用 Jamstack 工具构建电子商务网站来探讨这样一个例子。
摘要
-
文档网站一直是并且继续是 Jamstack 的一个完美用例,因为它们高度关注内容,并可以从版本控制等 Jamstack 工作流程的核心功能中受益。
-
无头 CMS 是一种内容管理系统,它提供的内容编辑工具与网站的客户端显示独立。基于 API 的无头 CMS 通过 API 将内容提供给前端,而基于 Git 的无头 CMS 则直接在网站的 Git 仓库中编辑内容。
-
Netlify CMS 是由 Netlify 创建和维护的开源、基于 Git 的无头 CMS,它提供了开放作者工作流程的选项。这可以让外部贡献者有机会编辑和提交网站内容的更改。
-
有很多针对文档的特定静态网站生成器。虽然基于 Go 的静态网站生成器 Hugo 不是针对文档的,但由于其构建速度和大量可用的文档模板,它通常是这些类型项目的首选工具。
-
Netlify CMS 通过 YAML 进行配置,并且必须有一个内容模型,该模型是 CMS 能够编辑的网站上的内容。我们为我们的 LOLCODE 技术文档网站使用的基礎文档内容模型配置了 Netlify CMS。
-
Netlify CMS 上的开放作者允许第三方贡献者访问 CMS 以进行内容贡献。我们配置了 Netlify 和 GitHub 进行身份验证,允许第三方使用开放作者登录我们的 CMS。
-
Netlify 的管理界面使用小部件来简化对页面元数据(前文)和内容的编辑。Markdown 编辑使用所见即所得风格的编辑界面。网站所有者和第三方作者的 CMS 用户体验几乎相同,除了标记更新为“就绪”并发布,这项功能仅限于网站所有者。
5 构建电子商务网站
本章涵盖
-
确定典型电子商务网站的需求
-
比较无头电子商务系统以管理产品和结账
-
为电子商务网站选择静态网站生成器
-
使用 Next.js 创建和配置新网站
-
在 Next.js 中构建产品列表、产品详情和购物车
-
在 Next.js 网站中导入和使用 Markdown 内容
电子商务网站有许多可能使其看起来不适合基于静态资产的架构(如 Jamstack)的要求。尽管内容方面,如产品列表和详情页面,可以轻松地适应静态生成的网站,但像购物车、结账处理和订单历史这样的功能似乎过于动态和交互,没有服务器端渲染就无法运行。
直到三四年前,这正是我会给出的建议:静态网站生成器不适合电子商务网站。然而,Jamstack 不仅仅是用静态网站生成器构建网站。Jamstack 的核心功能之一是能够在客户端使用 JavaScript 和 API 来实现动态功能,这些功能使用静态网站工具是做不到的。因此,今天完全有可能使用 Jamstack 构建功能齐全的电子商务网站。
说某事是可能的,并不意味着它就是正确的。然而,我会争辩说,在许多情况下,如果不是大多数情况下,采用 Jamstack 进行电子商务是正确的想法。这是因为有一整个小行业的研究表明性能可以对转化率以及因此产生的销售额产生影响。例如,一项分析发现,“如果一个网站每天赚 10 万美元,[页面速度]提高一秒就能带来 7000 美元的日收入”(mng.bz/Nxj7)。换句话说,Jamstack 提供的性能改进不仅会提升用户体验,而且这种改进的用户体验还可以提高你的利润。
5.1 电子商务网站的要求
电子商务网站在复杂性上可以有很大差异。一些网站只提供少量产品或服务以及简单的结账流程,没有任何真正的功能装饰。其他网站提供各种动态内容,如用户评论、个性化推荐、愿望清单等。所有这些功能都使用 Jamstack 实现,我们讨论的一些工具甚至提供专门的 API 来实现这些功能。
让我们看看典型电子商务网站的一些核心要求:
-
电子商务网站有一系列产品或服务。这些内容应通过某种形式的内容管理系统轻松更新和管理,以便非技术用户能够快速轻松地更新网站。
-
电子商务网站除了产品列表之外,还有常规的内容页面,如关于页面或服务条款。在大多数情况下,这些页面更新不频繁,因此可能不需要集成到外部内容管理中。
-
电子商务网站通常有一个购物车,用户可以动态添加和删除商品,然后进行结账流程。
-
电子商务网站必须有一个结账流程来完成购买。在大多数情况下,这集成在网站上,但在某些情况下,最终的结账流程可以外包给第三方进行处理和确认。
5.1.1 示例网站要求
我们将为本章构建的示例是一个简单的店面,称为“果酱店”,用于销售玩具人偶和橡皮鸭。它包括所有讨论的基本要求。目前,我们只有四个商品的有限库存,但我们预计它会扩大(见图 5.1)。

图 5.1 我们这个示例电子商务项目的最终结果是出售玩具人偶的商店。
我们网站将集成内容管理以添加和编辑我们的产品列表。然而,我们的其他内容页面——我们的关于页面——更新不频繁,因此它将作为一个简单的 Markdown 文件进行管理。
用户将从产品详情页面添加商品到购物车。他们可以通过“我的购物车”页面修改数量或删除商品,然后再进行结账。由于我们规模较小(以及为了简化),结账流程将外包给第三方来管理最终的运输和订单确认细节。
5.2 选择合适的工具
建立一个 Jamstack 电子商务网站有无数种方法,包括从头开始构建自定义解决方案。然而,电子商务中有许多复杂性,可能会使这项任务显得有些令人畏惧。Jamstack 通常倾向于尽可能利用现有服务以简化开发,在这种情况下,我们将使用一种称为无头电子商务的服务。
5.2.1 什么是无头电子商务?
在第四章中,我们了解了一个名为无头 CMS 的概念,其中 CMS 提供了与前端展示解耦的后端内容管理工具。无头电子商务是将此概念应用于电子商务工具。一个 无头电子商务解决方案 提供了管理购物车、订单、运输以及在很多情况下,与特定前端解决方案无关的产品/服务及库存的工具,允许您根据需要自定义前端。
我认为无头电子商务的类型比无头 CMS 的类型更多样化,在无头 CMS 中我们只有两种主要类型(基于 API 和基于 Git)。没有普遍接受的无头 CMS 类别,但——借鉴 François Lanthier Nadeau 的研究成果(snipcart.com/blog/headless-ecommerce)——SnipCart(我们将讨论的解决方案之一)的 CEO(一位无头电子商务领域的专家),以下是广泛的类型:
-
全栈解决方案——正如其名所示,这些解决方案提供全面的服务来管理电子商务网站的各个方面,从内容到产品,再到订单、运输等等。在大多数情况下,这些工具更倾向于提供全栈解决方案(耦合)的网站,既提供前端也提供后端,但它们也提供 API 以作为解耦的无头替代方案,可以在 Jamstack 网站中使用。
-
增强型解决方案——这些工具提供完整的购物车和结账解决方案,实际上是在你的网站之上运行的。这通常是通过包含一个嵌入覆盖层的脚本来完成的。这些工具通常不管理内容,也不一定要求你通过他们的服务管理你的产品列表。
-
基于 API 的解决方案——与它们的无头 CMS 对应物一样,这些服务的所有功能都仅通过 API 调用提供。虽然后端管理从产品到购物车、订单和运输的各个方面,但它们对前端构建方式没有假设。从添加和删除购物车中的项目到结账过程,所有这些操作都是通过调用 API 来处理的,在 Jamstack 网站的情况下使用 JavaScript。
从 Jamstack 的角度来看,无论是全栈解决方案还是基于 API 的解决方案,它们的消费方式都是相同的:一个 Jamstack 网站将使用全栈解决方案提供的 API,而不是依赖任何前端开发工具。
增强型解决方案通常需要的开发工作量最少,但通常以可定制性为代价。一旦你用产品/服务列表构建了网站,你只需简单地将其连接到管理其余部分的头颈电子商务工具。另一方面,全栈和基于 API 的解决方案都需要更多的开发工作量,因为像购物车和结账这样的前端需要定制构建,但通过仅依赖 API,开发者可以按照自己的选择创建前端。
提示:想深入了解一系列无头电子商务选项,请查看 Bejamas 的这篇详细文章:bejamas.io/blog/jamstack-ecommerce/。
5.2.2 无头电子商务选项
在我们讨论我们将选择哪种选项之前,让我们先看看一些最受欢迎的无头电子商务解决方案,每个类别中各一个。
Shopify
Shopify (shopify.dev/) 是市场上最受欢迎的全功能电子商务选项之一。Shopify 的服务可以通过他们的 Storefront API (shopify.dev/api/storefront) 在 Jamstack 网站中使用。这是一个提供对 Shopify 产品信息、订单和结账等全面服务的 GraphQL API。Shopify 还提供了一个用于 Storefront API 的 JavaScript SDK (http://mng.bz/Dxja),它简化了与他们的 API 交互所需的代码。Butcher Box(见图 5.2)和 Victoria Beckham Beauty 是两个使用 Jamstack 构建,并访问 Shopify Storefront API 的店面。

图 5.2 Butcher Box 是一个使用 Jamstack 构建,并使用 Gatsby 访问 Storefront API 的在线商店的例子。
Snipcart
Snipcart (snipcart.com/) 是一个附加电子商务解决方案的例子。在最基本的情况下,使网站与 Snipcart 一起工作只需要包含 Snipcart 的 JavaScript 和 CSS 文件,然后向“添加到购物车”按钮添加自定义 HTML 属性。就这样。链接将触发 Snipcart 购物车覆盖层出现,其中包含所有内置的购物车管理和结账功能。Snipcart 还包括一些定制选项,以及一个 JavaScript SDK,如果您想通过编程方式访问其任何功能而不是通过嵌入的购物车和结账。
注意:如果您想快速了解使用 Hugo 构建,并使用 Snipcart 进行电子商务功能的 Jamstack 网站的设置教程,请查看这篇教程和示例应用程序:www.stackbit.com/blog/ecommerce-jamstack/。
Commerce.js
Commerce.js (commercejs.com/) 是一个基于 API 的无头电子商务解决方案的例子,它已被用于构建如图 5.3 所示的 INGMARSON 网站等。它提供了构建电子商务网站所需的所有工具——产品管理、折扣、购物车、结账等,这些可以通过基于网络的后端进行管理,但在您的应用程序中通过 JavaScript SDK 和命令行工具访问。为了帮助您开始,他们还提供了使用流行的网络框架如 React 和 Vue 的预构建示例网站。我们的示例应用程序将使用 Commerce.js。

图 5.3 INGMARSON 是一个使用 Gatsby 和 Snipcart 的 Jamstack 构建在线服装零售商,它启用了其电子商务功能。
5.2.3 为什么选择 Commerce.js?
使用 commerce.js 的决定并不仅仅是由我们示例应用程序的技术需求驱动的。除了功能性之外,示例应用程序的目标是教授使用 Jamstack 工具和框架所需的一些基本概念。通过需要更多自定义代码,使用基于 API 的解决方案将使我们能够探索构建 Jamstack 应用程序所需的一些代码。
如果您正在评估选择哪种类型的解决方案,以下是我的建议:
-
如果您的优先级是快速轻松地构建网站而不是定制购物车或结账流程,请选择一个附加解决方案。
-
如果您的优先级是保持对设计和用户体验的控制,包括购物车和结账流程,并且您对需要额外代码感到舒适,请选择基于 API 的解决方案。
-
从技术角度考虑,您会选择一个一站式解决方案,原因与您选择基于 API 的解决方案相同。然而,在某些情况下,可能会有额外的功能提供或现有的商业关系,这可能使一站式解决方案更适合。
5.2.4 静态网站生成器选项
我所了解的没有特定的静态网站生成器(SSG)专门针对电子商务,所以实际上任何 SSG 都可以工作。尽管如此,我们计划在构建时与 API 集成——以填充产品列表——以及在客户端集成——以启用购物车功能。基于 JavaScript 的 SSG 在构建时与 API 集成变得容易,而且更具体地说,基于 JavaScript 框架的选项可以提供使一些客户端脚本更容易的工具。让我们看看这些选项中的几个:
-
Gatsby—Gatsby 是一个非常流行的基于 React 的 SSG。使 Gatsby 独特的一些事情是它使用 GraphQL 来访问数据,包括内容和其他内部数据结构,以及其插件系统。Gatsby 庞大的社区已经创建了数千个插件(截至本文撰写时超过 2,500 个),几乎覆盖了您可能需要的任何功能或集成。
-
Next.js—Next.js 是一个基于 React 的元框架(一个框架之上的框架)。它不仅仅是一个静态网站生成器。Next.js 可以用于创建具有服务器端渲染(SSR)的标准 React 单页应用程序(SPA)。它还提供了生成作为 Jamstack 应用程序静态资源的工具,但甚至允许网站确定特定的路由(即应用程序内的路径)应该是动态的还是静态的。这允许开发者构建“混合”应用程序,结合 SSR 和静态。
-
Nuxt.js—正如其名称可能暗示的那样,Nuxt.js 与 Next.js 有很多相似之处,包括能够用于 SSR 或静态(Nuxt 3 还增加了混合 SSR/SSG 方法的兼容性)。然而,Nuxt.js 使用 Vue 框架而不是 React。
-
Gridsome——Gridsome 与 Gatsby 有很多共同之处。它包括许多定义 Gatsby 的核心功能——它的 GraphQL 使用、它对生成 Jamstack 静态资产的关注以及它的插件生态系统——但使用 Vue 而不是 React。
-
Scully——虽然有许多基于 React 和 Vue 的 SSG,但截至本文撰写时,Scully 是唯一一个面向喜欢使用 Angular 框架的开发者的选项。它专注于开发基于纯静态的 Jamstack 应用程序,并拥有类似于 Gatsby 和 Gridsome 的插件生态系统。
我们将使用 Next.js 来构建我们的示例应用程序。
5.2.5 为什么选择 Next.js?
那么,你应该使用哪个静态站点生成器呢?说实话,这主要取决于个人喜好。除非你的电子商务网站有特定的需求,而这个需求可以通过 Next.js 或 Nuxt.js 中 SSR 的可用性来满足,否则列出的任何选项都能同样好地工作。那么问题就变成了:你更喜欢 React、Vue 还是 Angular?除此之外,这只是关于功能(GraphQL、插件)或风格偏好的问题。
Next.js 的最新版本还提供了一种新的渲染形式,称为增量静态再生(ISR),这对于拥有大量产品因而页面众多的电子商务网站尤其有用。ISR 实质上是在用户首次请求页面时才进行渲染。这意味着拥有数千个产品的电子商务网站可能只需要生成最受欢迎的 200 个产品页面,并在首次请求时渲染剩余的页面。这意味着第一个请求页面的用户可能会在收到页面时看到轻微的延迟,但后续用户将像静态生成一样收到页面。您可以在 Vercel 文档中了解更多关于 ISR 的信息(mng.bz/lanB)。
5.3 准备构建示例电子商务网站
现在我们已经做出了工具选择——使用 Commerce.js 进行电子商务和 Next.js 进行 SSG——让我们开始构建示例应用程序。我们需要做的第一件事是设置好一切,以便我们可以开始编码。
5.3.1 设置 Next.js
Next.js 的安装方式与之前章节中使用的某些示例不同,那些示例使用了二进制文件或全局 npm 安装。相反,Next.js 提供了一个名为 Create Next App 的工具(nextjs.org/docs/api-reference/create-next-app),用于生成一个新的网站,无论是空白网站还是从长长的起始模板列表中选择的一个。这不需要你安装任何东西,而是需要一个 npx 命令:一个内置在 npm 中的包运行器,可以在不要求安装的情况下运行 npm 脚本。你可以不带任何参数运行 npx create-next-app,它将创建一个简单的单页网页应用程序,包含基本的默认 Next.js 应用程序文件和文件夹结构。
Create Next App 还可以使用模板。这可以是任何 GitHub 仓库,但 Next.js 团队已经提供了超过 250 个示例 (https://github.com/vercel/next.js/tree/main/examples),你可以使用这些示例。我们将使用这些示例之一,with-tailwindcss,作为我们示例电子商务商店的基础,该商店销售玩具人偶。我们将构建产品列表页面、产品详情页面和购物车,但为了使这更容易,我们将利用 Next.js 提供的启动模板。我们选择的特定启动模板包括 Tailwind CSS 库,这将为我们提供一些基本的样式构建块来工作。这对我们来说是一个巨大的帮助:虽然我是许多事物,但设计师不是其中之一。想了解更多关于 Tailwind 的信息,请访问 tailwindcss.com。
让我们从运行 create-next-app 并指定 Tailwind CSS 示例开始。在你的网页项目存储位置运行以下命令:
npx create-next-app -e with-tailwindcss
Create Next App 会要求你输入项目名称。这将用作项目放置的文件夹名称。在这个例子中,让我们使用 next-ecommerce。Create Next App 不仅会生成项目文件,还会为我们安装所有依赖项。我们只需要将目录切换到项目文件夹并运行它:
cd next-ecommerce
yarn dev
如果你更喜欢使用 npm 而不是 Yarn,你可以运行 npm run dev 作为替代。
默认情况下,网站将在 http://localhost:3000 上运行。如果你在浏览器中打开它,你应该会看到图 5.4 中显示的标准 Next.js 启动画面。

图 5.4 运行 npx create-next-app 生成的默认 Next.js 网站
让我们从为这个网站添加一个基本的导航组件开始(列表 5.1)。我们将链接到的页面尚未创建,但我们很快就会解决这个问题。首先,在网站的根目录下创建一个名为 components 的文件夹,并在该文件夹内创建一个名为 nav.js 的文件。我们将有两个导航项,在 links 变量中设置为“我的购物车”和“关于”。这些将分别链接到 /cart 和 /about。我们使用 JavaScript 的 map() 函数遍历这个数组中的链接来创建导航。
列表 5.1 /components/nav.js 中的更新后的导航
import Link from 'next/link';
const links = [
{ href: '/cart', label: 'My Cart' },
{ href: '/about', label: 'About' },
];
export default function Nav() {
return (
<nav>
<ul className="flex items-center justify-between p-8">
<li>
<Link href="/">
<a className="text-blue-500 no-underline text-accent-1 dark:text-
➥ blue-300">
Jam Store
</a>
</Link>
</li>
<ul className="flex items-center justify-between space-x-4">
{links.map(({ href, label }) => (
<li key={`${href}${label}`}>
<Link href={href}>
<a className=" no-underline px-4 py-2 font-bold text-white
➥ bg-blue-500 rounded">{label}</a>
</Link>
</li>
))}
</ul>
</ul>
</nav>
);
}
我们将使用这个组件来在页面间显示导航。让我们先清除默认网站主页上的大部分默认内容,并包含导航组件。目前,页面只导入 nav.js 组件并在主部分上方显示它,目前这部分包含一些占位文本。
列表 5.2 在 /pages/index.js 中包含导航
import Nav from '../components/nav';
export default function IndexPage({ products }) {
return (
<div>
<Nav />
<section className="text-gray-700 body-font">
<div className="container px-5 py-24 mx-auto">
<div className="flex flex-wrap -m-4">
The product list will go here.
</div>
</div>
</section>
</div>
);
}
现在我们已经设置了项目代码,我们需要设置并填充 Commerce.js,以便我们可以在页面上填充数据。
5.3.2 设置 Commerce.js
要开始,我们需要通过 commercejs.com 注册一个 Commerce.js 账户。不用担心:免费账户对于本例的目的来说已经足够慷慨了。注册后,您将被带到仪表板。这就是我们将创建将填充我们的电子商务商店的类别和产品的地方。我将为您提供一些关于如何填充这些信息的指导,但应该注意的是,您可以使用您选择的任何内容;我们编写的代码中没有任何内容要求您使用我使用的产品和类别。
为了使我们的电子商务玩具人偶店能够运行,它需要一些产品,每个产品都将被分配到一个类别。我们需要通过它们的基于 Web 的后端在 Commerce.js 中填充这些信息。
让我们从创建一些类别开始。正如您在图 5.1 中看到的,我们的商店由一些玩具、人偶和橡皮鸭组成。因此,我们将创建两个类别:人偶和鸭子。要创建类别,请点击 Commerce.js 仪表板左侧的“产品”导航项,然后选择“类别”。我们需要提供类别名称,它将自动为我们填写一个别名。我们可以使用它提供的默认值(图 5.5)。

图 5.5 在 Commerce.js 仪表板中创建一个新的产品类别。
接下来,让我们创建一些产品。点击左侧导航栏中的“产品”项,然后点击“产品”。我们可以提供很多详细信息(见图 5.6),但就我们的目的而言,我们不需要填写所有这些信息。

图 5.6 在 Commerce.js 仪表板中创建一个新的产品。请注意,为了可读性,只显示了页面上的相关部分。
在我们创建新产品时,需要完成的关键项如下:
-
名称—您可以随意命名产品。
-
描述—再次,您可以自由添加您选择的任何类型的描述。
-
类别—假设您使用了建议的类别,这将要么是“插图”要么是“鸭子”。
-
价格—提供您想要的任何价格。
-
图片—我在本书的 GitHub 仓库中提供了一些示例图片 (https://github.com/cfjedimaster/the-jamstack-book)。对于我们的设计,我们需要为每个产品提供两张图片:一张是 400 × 400 px,另一张是 350 × 192px。
-
自定义永久链接—如果您不提供,系统会自动生成一个随机永久链接,但最好我们还是提供。这将用于应用程序中构建查看产品的路径。
一旦我们创建了一些产品,我们就可以回到我们生成的项目并开始编码。但首先,我们需要我们的 API 密钥,以便我们的项目可以访问 Commerce.js 中的数据。点击左侧导航菜单中的“开发者”项,然后选择“API 密钥”。我们只需要复制公钥。
5.3.3 设置 Next.js 环境变量
如果你仍然在运行本地 Web 服务器来运行你的网站,请现在停止它,因为我们将对网站的配置进行更改。
为了从我们的网站连接到 Commerce.js API,我们需要 API 密钥。然而,我们需要将这个密钥放在一个不会直接在我们的代码中暴露的地方,这个代码将被提交到 GitHub。Next.js 包括自动加载环境变量的能力(见nextjs.org/docs/basic-features/environment-variables),这可以用来存储像 API 密钥或你打算在整个应用程序中重复使用的通用配置变量等秘密。我们将把我们的 Commerce.js 公开 API 密钥存储为环境变量。
在你的项目根目录下创建一个名为.env.local 的文件。默认情况下,我们在这个文件中存储的任何变量都可以通过 process.env.ENV_VARIABLE_NAME 访问。例如,名为 CHEC_PUBLIC_KEY 的变量可以通过 process.env.CHEC_PUBLIC_KEY 访问。这仅在 Node.js 环境中可用,无论是作为 SSR 应用程序的服务器端,还是在使用 Next.js 作为 SSG 时的构建过程中。如果你需要通过客户端 JavaScript 访问这个变量,你可以将变量的名称前缀为 NEXT_PUBLIC_。
我们需要这个变量在构建时配置 Commerce.js SDK,因此我们不需要将其设置为公开。让我们在.env.local 文件中放入以下内容,将{{MY_API_KEY}}替换为你的 Commerce.js 公开 API 密钥:
CHEC_PUBLIC_KEY={{MY_API_KEY}}
.env 文件对于存储像 API 密钥这样的东西很有用,你不想在项目存储库中发布。虽然这是一个公开密钥,并且可以从客户端检查我们发出的任何 API 调用的人访问,但仍然建议将其从发布代码中排除。此外,这给我们提供了一个存储变量的单一位置,并在应用程序中需要时重复使用它。.env.local 应该已经包含在项目生成的.gitignore 文件中。
5.3.4 加载 Commerce.js SDK
Commerce.js 提供了一个 Commerce.js SDK(commercejs.com/docs/),它帮助我们通过 JavaScript 更容易地与 Commerce.js API 交互。让我们通过 Yarn 安装 SDK。运行以下命令。确保你在运行此命令时位于网站的根目录:
yarn add @chec/commerce.js
为了配置 SDK,我们需要传递我们在上一节中存储为环境变量的 API 密钥。我们不希望在每个使用 SDK 的页面上传递这个密钥,所以让我们创建一个 lib 文件来配置 SDK,这样我们就可以在整个应用程序中重复使用它。
在你的项目根目录下创建一个名为 lib 的文件夹,然后创建一个名为 commerce.js 的文件。这个文件将导入 SDK 并在 Commerce 对象的新实例中设置 API 密钥:
import Commerce from '@chec/commerce.js';
export default new Commerce(process.env.CHEC_PUBLIC_KEY);
现在,每当我们需要使用 Commerce.js SDK 时,我们只需要导入 lib/commerce,我们就可以访问已经配置好的 Commerce.js SDK 对象。
5.4 构建“Jam Store”电子商务网站
一切都已为我们的示例电子商务网站搭建完毕,该网站销售玩具人偶。我们已运行create-next-app来生成一个项目,其中包含一些基本的网站代码,以便我们开始,这包括用于一些样式的 Tailwind CSS。我们还用一些示例产品和类别填充了 Commerce.js,以填充我们的商店。最后,我们在项目中设置了 Commerce.js SDK,并将 API 密钥存储为环境变量,以便我们可以从我们的网站连接到 Commerce.js。现在我们可以开始创建我们的产品列表页面、产品详情页面和购物车,以使我们的电子商务网站完整。
在接下来的章节中,我们将涵盖大量的代码。这里的目的是探索一些你需要了解如何使用 Next.js 构建网站的关键方面。并不是你必须理解每个代码列表的每一行。我将指出任何关键概念和需要你注意的部分。
5.4.1 创建产品列表组件
让我们构建一个主页,该主页将列出从 Commerce.js 获取的所有可用产品。我们将从 Tailblocks (tailblocks.cc/)获取一些帮助,Tailblocks 是一个为 Tailwind 构建的现成代码块存储库。我们的产品列表和产品详情页面将基于他们电子商务类别中的某些代码示例。
产品列表将遍历产品并显示产品卡。让我们创建一个可重用的产品卡组件。在 components 目录中创建一个名为 products 的文件夹,然后在文件夹中创建一个名为 ProductList.js 的文件。
让我们看看用于生成产品列表中每个产品卡的 ProductList.js 组件的代码。该组件接收通过 props 传递的产品对象,并在产品卡中填充值。
我们将编写 ProductList 来接受属性(props),当使用此组件时我们将传递这些属性。这就是我们将传递将填充页面的产品的方式。
组件将返回由组件渲染的 HTML,使用传递的产品属性中的产品详细信息填充各种动态元素。
列表 5.3 /components/products/ProductList.js 中的产品卡组件
import Link from 'next/link';
export default function ProductList({ ...props }) {
const thumbnail = props.product.assets.filter((item, index) => {
return (item.image_dimensions.width === 350);
})[0];
return (
<div className="lg:w-1/4 md:w-1/2 p-4 w-full">
<Link href={'/product/' + props.product.permalink}>
<a className="block relative h-48 rounded overflow-hidden">
<img
alt={props.product.name}
className="object-cover object-center w-full h-full block"
src={thumbnail.url}
/>
</a>
</Link>
<div className="mt-4">
<h3 className="text-gray-500 text-xs tracking-widest title-font mb-1
➥ uppercase">
{props.product.categories[0].name}
</h3>
<h2 className="text-gray-900 title-font text-lg font-medium">
{props.product.name}
</h2>
<p className="mt-1">{props.product.price.formatted_with_symbol}</p>
</div>
</div>
);
}
你可能会注意到我们导入了 next/link 组件 (nextjs.org/docs/api-reference/next/link)。这是一个内置在 Next.js 中的辅助组件,用于大多数单页应用中常见的客户端路由转换,从而实现更快的页面加载。在这个组件中,我们使用链接组件通过在 Commerce.js 中设置的产品上的永久链接属性来链接到产品详情页面。
值得注意的是,Commerce.js 返回的每个产品都将有一个与之关联的图像数组——在我们的例子中是两个,因为我们为每个产品添加了两张图片。对于产品列表,我们需要宽度为 350 像素的小缩略图。为了获取这个缩略图,我们使用 JavaScript 数组过滤函数 (mng.bz/Bx8r) 遍历产品资源数组,并过滤掉任何不是 350 像素宽度的图像。我们将缩略图变量设置为返回数组中的第一个项目。
5.4.2 构建产品列表
让我们将 ProductList 组件和 commerce 库文件结合起来创建主页列表。让我们看看 index.js 的最终代码。这段代码替换了之前创建的 index.js 中的现有内容。这个页面在 Next.js 的一个特殊方法 getStaticProps() 中调用 Commerce.js API。
getStaticProps() 方法是 Next.js 内置的数据获取方法之一。它在构建时被调用,因此专门设计用于 Next.js 应用程序中的静态路由。由于我们正在构建一个典型的 Jamstack 应用程序,所有路由都作为静态资源生成。我们可以使用 getStaticProps() 获取页面所需的任何数据,并将其添加到 props 对象中。在这种情况下,我们只需要传递给输出的产品数组,我们将遍历这些产品,将每个产品传递给之前创建的 ProductList 组件以输出产品卡片。
列表 5.4 主页,包括 /pages/index.js 中的产品列表
import Nav from '../components/nav';
import commerce from '../lib/commerce';
import ProductList from '../components/products/ProductList';
export default function IndexPage({ products }) {
return (
<div>
<Nav />
<section className="text-gray-700 body-font">
<div className="container px-5 py-24 mx-auto">
<div className="flex flex-wrap -m-4">
{products.map((product, index) => (
<ProductList product={product} key={index} />
))}
</div>
</div>
</section>
</div>
);
}
export async function getStaticProps() {
const products = await commerce.products.list();
return {
props: {
products: products.data,
},
};
}
值得注意的是,你需要导入 /lib/commerce.js,它设置我们的 Commerce.js SDK 以连接到 API,以及 /components/products/ProductList.js,这是我们之前创建的产品卡片组件。当我们遍历,将每个产品作为属性传递给 ProductList 时,我们还设置了一个唯一的键属性。这有助于 React 识别虚拟 DOM 中的变化。
你可能还会注意到,我们的 getStaticProps() 方法被设置为异步。这样做是为了我们可以使用 JavaScript 的 await 操作符。这允许我们通过等待返回 JavaScript promise 的 API 调用的结果来减少所需的代码量,正如 Commerce.js SDK 所做的那样。因此,我们的 products 常量等待 commerce.products.list() 方法返回结果,防止我们在收到 API 响应之前尝试在 props 中返回值。
如果我们重新启动服务器并在浏览器中重新加载页面,我们现在应该会看到通过 Commerce.js 控制台输入的产品列表。它应该看起来像我们在图 5.1 中看到的图像。
5.4.3 构建产品详情页面
我们需要为 Commerce.js 返回的每个产品动态生成一个产品详情页(见列表 5.5)。为此,我们将使用 Next.js 的一个名为 动态路由 的功能 (nextjs.org/docs/routing/dynamic-routes)。动态路由可以通过文件名被方括号包围来识别。方括号之间的文本将是我们将用于生成页面的参数。让我们看看它是如何工作的。
如果您还记得我们创建 ProductList 组件的时候,我们希望产品详情页的路径为 /product/[permalink],其中 [permalink] 是在 Commerce.js 中设置的产品属性中的永久链接值。我们将通过创建一个 /pages/products 文件夹并在该文件夹内创建一个 [permalink].js 文件来实现这一点。
在这个页面上将发生许多事情,我们需要解决这些问题,以便可以将项目添加到购物车中,但,现在,让我们专注于让详情页显示出来。我们渲染将要传递给组件的产品(我们稍后会讨论这一点),它根据图像宽度拉取页面上的正确图像。
列表 5.5 /pages/product/[permalink].js 中的产品详情页
import Nav from "../../components/nav";
export default function ProductDetail({ product }) {
const fullImage = product.assets.filter((item, index) => {
return item.image_dimensions.width === 400;
})[0];
return (
<div>
<Nav />
<section className="text-gray-700 body-font overflow-hidden">
<div className="container px-5 py-24 mx-auto">
<div className="lg:w-4/5 mx-auto flex flex-wrap">
<img
alt="ecommerce"
className="..."
src={fullImage.url}
/>
<div className="...">
<h2 className="...">
{product.categories[0].name}
</h2>
<h1 className="...">
{product.name}
</h1>
<div
className="leading-relaxed"
dangerouslySetInnerHTML={{
__html: product.description,
}}
></div>
<div className="flex">
<span className="...">
{product.price.formatted_with_symbol}
</span>
<button
className="..."
>
Add to Cart
</button>
</div>
</div>
</div>
</div>
</section>
</div>
);
}
可能您注意到了我们使用的那个名为 dangerouslySetInnerHTML 的奇怪属性。因为产品描述可能包含 HTML 格式,我们需要在 DOM 中使用 innerHTML 来渲染它。然而,简单来说,由于 React 使用虚拟 DOM,我们需要通知 React 我们正在设置 innerHTML,这需要使用 dangerouslySetInnerHTML 方法。(您可以阅读这篇文章 [mng.bz/doYv] 以获得更深入的说明。)
到目前为止,我们只是渲染了 HTML,但由于这是一个动态路由,我们需要告诉 Next.js 在输出我们网站的静态文件时渲染哪些页面。在我们的情况下,我们计划为每个产品渲染一个页面,使用每个产品的永久链接来确定页面的文件名。为此,我们需要添加另一个内置 Next.js 的用于生成静态站点的特殊方法,即 getStaticPaths(),如列表 5.6 所示。
列表 5.6 getStaticPaths() 和 getStaticProps() 方法
export async function getStaticPaths() {
const products = await commerce.products.list();
// create paths with `permalink` param
const paths = products.data.map((product) => `/product/${product.permalink}`);
return {
paths,
fallback: false,
};
}
export async function getStaticProps({ ...ctx }) {
const { permalink } = ctx.params;
const product = await commerce.products.retrieve(permalink, {
type: 'permalink ',
});
return {
props: {
product: product,
},
};
}
getStaticPaths() 方法返回一个路径字符串数组。我们将使用 Commerce.js 返回产品列表,然后使用产品永久链接填充这些路径字符串。我们还需要返回一个回退键。当这个键为 false,正如我们设置的,任何此方法未返回的路径将返回 404。如果它是 true,Next.js 将返回页面的回退版本而不是 404。如果我们正在生成大量页面,这会很有用,因为它会导致构建缓慢。对于任何尚未渲染的页面,当 getStaticProps() 被调用以填充页面时,用户将看到加载指示器。
说到 getStaticProps(),我们需要创建该函数以填充将要渲染的产品详情。为了获取此页面的特定产品详情,我们将使用通过页面上下文变量(ctx)传入的 permalink 变量。这个变量由我们使用产品 permalink 生成的/product/[permalink]路径填充。然后我们可以使用 permalink 查询 Commerce.js 以获取匹配的产品详情。
将这两个方法添加到我们创建的[permalink].js 页面中。您可以将它们放置在列表 5.5 中的代码下方。
我们创建的产品详情看起来像图 5.7。

图 5.7 我们产品详情列表之一
5.4.4 启用添加到购物车功能
到目前为止,我们编写的所有代码都在构建时运行,但每个购物车都是针对每个用户的。因此,添加到购物车功能不能静态预渲染,需要在客户端(即浏览器)上运行。为此,我们需要使用环境变量来配置带有我们的 API 密钥的 Commerce.js 客户端脚本,该密钥目前仅在构建时可用。
为了使我们的 CHEC_PUBLIC_KEY 环境变量可供此脚本访问,我们需要在项目根目录中创建一个 Next.js 配置文件,命名为 next.config.js。在这个文件中,我们将告诉 Next.js 在客户端使这个环境变量可用。
列表 5.7 Next.js 配置文件在 next.config.js 中
module.exports = {
env: {
CHEC_PUBLIC_KEY: process.env.CHEC_PUBLIC_KEY,
}
};
注意,我们无法使用 Next.js 提供的 NEXT_PUBLIC_ 快捷方式,因为 Commerce.js 期望使用特定的变量名来使用密钥。
如果您的本地服务器仍在运行,您需要在进行配置更改后停止并重新启动它。
让我们回到我们的/pages/product/[permalink].js 文件,并添加一些方法以启用添加到购物车功能。由于我们的配置更改,我们现在可以从客户端调用 Commerce.js。让我们在[permalink].js 中的 ProductDetail 函数中添加一个新方法。但在我们这样做之前,我们需要在文件顶部导入 Commerce.js 库:
import commerce from "../../lib/commerce";
您可以将我们的 handleAddToCart()函数的代码放置在 return 之前。此方法将告诉 Commerce.js 将当前产品添加到我们的购物车中。目前,我们将只是将完整的购物车内容输出到浏览器控制台,以便我们可以看到它是否正常工作:
const handleAddToCart = async (e) => {
let cart = await commerce.cart.add(product.id, 1);
console.log(cart);
};
每次我们调用此方法时,我们都在告诉 Commerce.js 将此产品的一项添加到购物车中。这将添加一个新的产品,或者如果该产品已经在购物车中,则增加该产品的数量。在同一文件中,让我们通过修改“添加到购物车”按钮来调用此方法。用以下代码替换 render()方法中的当前按钮代码:
<button
onClick={handleAddToCart}
className="..."
>
Add to Cart
</button>
保存文件并在浏览器中刷新后,打开浏览器开发者工具控制台。当您点击“添加到购物车”按钮时,您应该收到以下类似响应:
{success: true, event: "Cart.Item.Added", line_item_id:
➥ "item_7RyWOwmK5nEa2V", product_id: "prod_Op1YoV9x4wXLv9", product_name:
➥ "Bat Duck", ...}
当然,将结果直接输出到浏览器控制台并不是理想的用户交互方式。我们需要做的是让用户知道项目已成功添加。我们将使用一个状态变量来完成此操作,该变量将通过 React 的 useState 钩子设置。首先,我们需要在文件顶部添加对钩子的导入:
import { useState } from "react";
现在,我们可以创建 cartText 状态变量和 setCartText() 函数,这将允许我们使用此钩子更改此状态变量的值。在 React 中,不应直接更改状态变量,因此需要设置方法。此外,useState 钩子允许我们传入一个默认值,在这种情况下,将是“添加到购物车”。将此行直接添加到 export default 函数行下方:
const [cartText, setCartText] = useState("Add to Cart");
接下来,让我们再次更新按钮,使用该变量而不是硬编码的文本:
<button
onClick={handleAddToCart}
className="..."
>
{cartText}
</button>
仅此本身不会显示与我们之前显示的任何不同,因为我们没有在状态更改时更新值。为了做到这一点,我们需要通过移除 console.log 并添加一些代码来更改 cartText 状态变量,当项目成功添加时来修改我们的 handleAddToCart() 方法。我们不直接修改状态,而是使用 setCartText() 方法来更新它:
const handleAddToCart = async (e) => {
let cart = await commerce.cart.add(product.id, 1);
let cartText = "Added! (" + cart.quantity + ")";
setCartText(cartText);
};
现在,如果我们尝试运行页面,购物车文本将显示已添加项目,并显示购物车中该项目的数量。点击“添加到购物车”按钮应显示“已添加(1)”或,如果该产品已经在您的购物车中,“已添加(2)。”
5.4.5 构建购物车
到目前为止,用户可以查看我们所有的产品,点击查看具体产品的详细信息,并将产品添加到他们的购物车中。接下来,让我们允许他们查看他们购物车中的项目。为此,我们将从 Codepen 中的此笔借用设计和布局:codepen.io/abdelrhman/pen/BaNPVJO。
创建一个 /pages/cart.js 文件。这个页面将不同于之前的页面,因为它不会使用 getStaticProps()。为什么?因为填充此页面的所有属性都无法静态预渲染。购物车的内容必须在客户端检索,因为它与特定用户相关。
我们将不会使用 props,而是会大量使用 state 来填充页面,显示用户的购物车详细信息。这将允许我们在获取购物车内容时以及用户与页面交互以修改或从购物车中删除项目时更新状态。让我们首先创建组件的结构并设置我们需要的状态变量。
列表 5.8 在 /pages/cart.js 中创建初始购物车页面
import { useState, useEffect } from "react";
import Nav from "../components/nav";
import commerce from "../lib/commerce";
export default function ShoppingCart() {
const [items, setItems] = useState([]);
const [subtotal, setSubtotal] = useState({});
const [total, setTotal] = useState({});
const [checkoutURL, setCheckoutURL] = useState("");
return (
);
}
接下来,让我们填充 return()。我们的 return() 方法通过遍历包含购物车项目的 items 数组来创建购物车,并用其他状态变量填充输出剩余部分的内容。请注意,为了可读性,省略了一些部分(用“...”表示)。您可以在 GitHub 仓库中找到完整的代码(mng.bz/8lRB)。
列表 5.9 /pages/cart.js 中 render() 方法的代码内容
return (
<div>
<Nav />
<div className="container mx-auto mt-10">
<div className="flex shadow-md my-10">
<div className="w-3/4 bg-white px-10 py-10">
...
{items.map((item, index) => (
<div ... key={index}
>
<div className="flex w-2/5">
<div className="w-20">
<img className="h-24" src={item.media.source} alt="" />
</div>
<div className="...">
<span className="font-bold text-sm">
{item.product_name}
</span>
<a href="#" ...>Remove</a>
</div>
</div>
<div className="flex justify-center w-1/5">
<button>...</button>
<input className="..." type="text" value={item.quantity} />
<button>...</button>
</div>
<span className="text-center w-1/5 font-semibold text-sm">
{item.price.formatted_with_symbol}
</span>
<span className="text-center w-1/5 font-semibold text-sm">
{item.line_total.formatted_with_symbol}
</span>
</div>
))}
...
<div id="summary" className="w-1/4 px-8 py-10">
<h1 className="...">Order Summary</h1>
<div className="flex justify-between mt-10 mb-5">
<span className="...">{items.length} items</span>
<span className="font-semibold text-sm">
{subtotal.formatted_with_symbol &&
subtotal.formatted_with_symbol}
</span>
</div>
<div className="border-t mt-8">
<div ...>
<span>Total cost</span>
<span>
{total.formatted_with_symbol && total.formatted_with_symbol}
</span>
</div>
<button ...>Checkout</button>
</div>
</div>
</div>
</div>
</div>
);
我们已经设置了布局和必要的状态变量,但如果你通过点击“我的购物车”导航按钮浏览到购物车,你会看到页面没有内容——即使你的购物车中有项目(图 5.8)。

图 5.8 静态预渲染的购物车页面没有详细信息。这些信息必须通过浏览器为每个用户加载。
我们接下来需要做的是在页面准备好时从 Commerce.js 加载用户的购物车内容。为此,我们将使用 useEffect() 钩子,它告诉 React 在渲染完成后执行某些操作。
我们的 useEffect() 钩子将调用 Commerce.js 获取用户的购物车,然后更新组件状态变量以在页面上填充用户的购物车项目和数量。我们将此代码放置在钩子内的一个单独的方法中,因为我们需要异步调用它。(这是因为异步方法总是返回一个承诺,但 useEffect() 只能返回一个函数,因此我们无法使 useEffect() 异步。)让我们将其直接放置在返回语句上方:
useEffect(() => {
async function fetchCart() {
let cart = await commerce.cart.retrieve();
console.log(cart);
setItems(cart.line_items);
setSubtotal(cart.subtotal);
setTotal(cart.subtotal);
setCheckoutURL(cart.hosted_checkout_url);
}
fetchCart();
}, []);
当页面重新加载时,你现在应该能看到你添加到购物车中的项目列表(图 5.9)。

图 5.9 购物车的内容通过 useEffect() 钩子在客户端加载,并在页面上填充。
到目前为止,一切顺利,但用户无法添加或减去(最终移除)购物车中的项目。让我们在我们的 useEffect() 方法上方添加一个方法来处理这个问题。此方法将调用 Commerce.js 更新数量,然后更新相关的状态变量:
const handleUpdateQuantity = async (id, quantity) => {
let res = await commerce.cart.update(id, { quantity: quantity });
let items = res.cart.line_items;
setItems(items);
setSubtotal(res.cart.subtotal);
setTotal(res.cart.subtotal);
};
我们的 handleUpdateQuantity 方法将处理添加、减去或甚至手动提供数量的操作。首先,从移除链接中调用它。移除它只是将数量设置为零:
<a
href="#"
onClick={() => handleUpdateQuantity(item.id, 0)}
className="..."
>
Remove
</a>
接下来,我们将通过从当前数量中减一来将其添加到减号按钮:
<button
onClick={() =>
handleUpdateQuantity(item.id, item.quantity - 1)
}
>
最后,通过将当前数量加一来将其添加到加号按钮:
<button
onClick={() =>
handleUpdateQuantity(item.id, item.quantity + 1)
}
>
我们必须以不同的方式处理允许用户手动指定新数量的输入字段。我们需要一个方法,当值更改时将被调用,并且如果提供的值是数字,则更新数量。此方法可以放在 handleUpdateQuantity() 方法下方:
const handleQuantityChange = (id, e) => {
const quant = parseInt(e.target.value.trim());
if (!isNaN(quant)) handleUpdateQuantity(id, e.target.value.trim());
};
当用户通过在文本框中更改值时,我们需要调用该方法。为此,我们需要在输入字段上添加一个 onChange 事件处理器:
<input
className="mx-2 border text-center w-8"
type="text"
value={item.quantity}
onChange={(e) => handleQuantityChange(item.id, e)}
/>
现在,您应该能够增加、减少、删除和更新购物车中任何商品的库存。
我们需要做的最后一件事是启用结账流程。Commerce.js 提供了使用托管结账选项的能力。这意味着当用户点击结账按钮时,他们将通过 Commerce.js 的流程进行引导,而不是通过自定义结账。当然,如果您想的话,您可以创建一个自定义结账,但为了简单起见,我们将使用托管选项。
要做到这一点,我们只需要一个简单的处理函数,我们可以将其放置在 handleQuantityChange() 方法之下。此方法简单地使用 Commerce.js 提供的结账 URL 打开一个新窗口:
const handleCheckout = () => {
// for now we're just opening a new window to the hosted checkout
window.open(checkoutURL);
};
然后,我们需要给结账按钮添加点击事件,以便在按钮被点击时触发该方法:
<button
onClick={handleCheckout}
className="..."
>
Checkout
</button>
点击结账按钮现在将打开一个新的标签页,在 Commerce.js 上进行托管结账(图 5.10)。

图 5.10 我们的电子商务商店使用 Commerce.js 的托管结账而不是自定义结账。
5.4.6 添加 Markdown 内容
我们的电子商务体验已经完成,但我们还需要在我们的网站上添加一个最后的修饰:由基于文件的 Markdown 内容驱动的关于页面。Next.js 没有内置 Markdown 支持,但添加它相对容易。让我们看看如何。
我们需要三个 npm 插件来启用此支持:
-
raw-loader—此包将使我们能够在 Webpack 配置中将原始 Markdown 文件作为字符串导入。
-
gray-matter—我们的 Markdown 文件将包含前端元数据。此库将使我们能够轻松解析这些元数据。
-
react-markdown—正如我们之前讨论的,React 应用程序使用虚拟 DOM,这个库将在 React 的虚拟 DOM 中渲染 Markdown,这意味着 React 只能正确更新更改的 DOM 元素。
如果您仍在运行本地站点,您需要先停止它。接下来,安装所有三个库:
yarn add raw-loader gray-matter react-markdown
让我们先使用 raw-loader。为此,我们需要编辑我们的 Next.js 网站的 Webpack 配置。Webpack 是一个流行的模块打包器,Next.js 使用它来打包其浏览器中的 JavaScript 文件。要编辑 Webpack 配置,打开 next.config.js 并添加一个新规则,该规则查找具有 .md 扩展名的文件,并使用 raw-loader 加载它们。
列表 5.10 在 next.config.js 中的 Next.js 配置文件以加载 Markdown 文件
module.exports = {
env: {
CHEC_PUBLIC_KEY: process.env.CHEC_PUBLIC_KEY,
},
webpack: function (config) {
config.module.rules.push({
test: /\.md$/,
use: 'raw-loader',
});
return config;
},
};
在 /pages/about.js 中为我们的关于页面创建一个新文件。在 getStaticProps() 方法中,我们可以使用文件系统从 /content/about.md 加载原始 Markdown 文件。然后,我们将使用 gray-matter 库读取原始 Markdown 并将前端元数据与内容分开。我们将把前端元数据和 Markdown 内容作为 props 传递给页面,其中 ReactMarkdown 用于将 Markdown 渲染为 React 组件。
列表 5.11 在 /pages/about.js 中加载 Markdown 内容的关于页面
import fs from 'fs';
import Nav from '../components/nav';
import matter from 'gray-matter';
import ReactMarkdown from 'react-markdown';
export default function About({ frontmatter, content }) {
return (
<div>
<Nav />
<div className="content container px-5 py-24 mx-auto">
<h1>{frontmatter.title}</h1>
<ReactMarkdown children={content} />
</div>
</div>
);
}
export async function getStaticProps() {
const file = fs.readFileSync(`${process.cwd()}/content/about.md`, 'utf8');
const data = matter(file);
return {
props: {
frontmatter: data.data,
content: data.content,
},
};
}
如您从代码中猜到的,在页面工作之前,我们需要在 /content/about.md 创建一个 Markdown 文件。将内容文件放在 /content 文件夹是 Next.js 站点加载 Markdown 内容的典型结构。我的 about.md 文件相当简单,但请随意通过添加您自己的更多 Markdown 标记来实验:
---
title: About the Jam Store
---
The Jam Store is built with:
* Next.js
* Commerce.js.
让我们使用 yarn dev 重新启动本地站点,并通过点击“关于”导航项来查看“关于”页面(图 5.11)。

图 5.11 我们的“关于”页面从 about.md 文件中的 Markdown 前置内容和 Markdown 标记的组合中渲染页面标题和正文。
5.5 接下来是什么?
现在我们已经拥有了一个功能齐全的电子商务网站,但我们可以通过多种方式继续改进它。首先,也是最明显的方法是构建自定义的结账过程,而不是使用托管结账。我们还从未解决过诸如折扣、销售税和运输等问题。这些都是可以通过 Commerce.js 控制台进行管理和定制的,然后集成到网站上。
电子商务网站有很多潜在的复杂性,我们在这里没有空间涵盖。我们选择使用 Commerce.js 作为基于 API 的无头 CMS,部分原因是因为它有助于说明使用 Next.js 构建网站时的许多要求。然而,在构建自己的 Jamstack 电子商务网站时,您将做出的最大决定之一是,在实现速度较快的扩展解决方案(如 Snipcart)和基于 API 的工具(如 Commerce.js)的粒度化可定制性之间如何权衡。没有正确答案;这完全取决于您网站的需求。
摘要
-
电子商务网站的动态用户界面需求非常适合基于 JavaScript 框架的 SSGs。例如,React 框架自带工具,可以更容易地在浏览器中动态更新 DOM,这有助于构建高度动态的页面组件,如购物车。
-
无头电子商务系统为 Jamstack 电子商务网站提供后端。有三种类型的无头电子商务系统:
-
一体化解决方案通常用于构建电子商务网站的前端和后端,但通常提供无头选项。
-
扩展解决方案旨在通过提供整个购物车和结账过程的 UI 和管理来简化实现。
-
基于 API 的解决方案允许通过 API 访问其所有数据和管理工作能力来实现粒度化的定制,但需要更多的代码来实现。
-
-
Next.js 是一个基于 React 的元框架,它提供了构建服务器端渲染或静态预渲染的工具,甚至可以两者结合。Next.js 通过内置函数如 getStaticPaths() 和 getStaticProps(),使得与像 Commerce.js 这样的基于 API 的外部数据源集成变得相对简单。
-
Next.js 没有内置支持加载 Markdown 内容,但可以通过使用多个 npm 库来实现。raw-loader 库提供了通过 Webpack 导入原始文本文件的能力。gray-matter 库可以从 Markdown 文件中读取元数据。最后,react-markdown 组件在 React 虚拟 DOM 中渲染 Markdown。
6 部署
本章涵盖
-
托管您的 Jamstack 网站的选项
-
使用基本的 Web 服务器
-
考虑云文件存储提供商
-
选择适合 Jamstack 的选项
恭喜!您已经采用了 Jamstack 的方式,并发现了使用本地静态网站生成器将动态内容转换为简单文件的乐趣。下一步是将这些文件上传到互联网,使它们成为真正的网站,而不是您设备上的一组数据。
在本章中,我们将介绍多种将文件上传到互联网并使其对公众可用的不同选项。我们将讨论每个选项的优缺点,以便您可以确定哪个选项适合您和您的项目。
6.1 Web 服务器——经过考验和验证的方法
托管您的 Jamstack 内容的第一个和最简单的解决方案是自互联网开始以来我们就一直在使用的解决方案:一个简单的 Web 服务器。像 Apache (httpd.apache.org/) 和 IIS (www.iis.net/) 这样的 Web 服务器已经存在了几十年,为从最小的粉丝网站到互联网上最大的电子商务网站提供了动力。
使用这些选项意味着只需将您的静态网站生成器(HTML、CSS 和其他相关文件)的结果复制到现有 Web 服务器的“Web 根”下。
就这些。没有更多了。您可以创建管道来自动执行此操作并为您处理复制,或者您可以手动完成所有操作并使用 FTP 客户端将您的文件上传到您的生产服务器。最终,这是最简单的选项,如果您已经有一个现有的网站和服务器并只想将您的 Jamstack 网站作为现有解决方案的一部分,那么这将是最合适的选择。
作为这个问题的具体例子,你可以想象一个现有的使用 PHP 为电子商务网站的用户提供动态网页的 Web 服务器。PHP 将处理购物、结账等方面。然后你可以使用 Jamstack 来处理网站上销售产品的文档。这些不需要 PHP,可以是简单的静态文件。显然,整个网站可以是 Jamstack(实际上,上一章讨论了使用 Jamstack 进行电子商务),但也可以有使用多个解决方案的网站!
6.2 云文件存储提供商
另一种托管 Jamstack 网站的选择是利用云文件存储提供商。这是一种简单的服务,只需让您在云端存储文件,并通过 HTTP 使其可访问。对于需要存储文件而无需担心管理磁盘驱动器的开发者来说,云文件存储提供商是一个有吸引力的解决方案。通过提供(几乎)无限的空间,开发者可以简单地上传文件,无需担心驱动器满载。(当然,只要他们能承受价格,当然。)
许多这些提供商现在支持将一组文件作为网站托管的选择。让我们来看看这些选项。
6.2.1 Amazon S3
作为最古老和最成熟的服务之一,Amazon S3 可能是开发者在考虑基于云的文件存储时首先想到的东西。Amazon S3(aws.amazon.com/s3/)是一个非常灵活、非常健壮的文件存储解决方案,具有很好的定价(aws.amazon.com/s3/pricing)。Amazon 还提供了一个定价计算器(calculator.aws),让您可以估算您的成本。
虽然每个网站都不同,但我可以提供我自己的使用示例。我在 S3 上有略少于 1GB 的文件,一个月内有大约 70,000 次请求。那笔账单大约是 3 美分。是的,3 美分。为了为这些请求启用 HTTPS,我的价格飙升到 73 美分。再次强调,您在做出承诺之前需要查看您的网站、流量、大小等因素,但您的成本很可能是最低的。此外,请注意,Amazon 提供了各种不同的“免费层”(aws.amazon.com/free/)选项。在本书撰写时,Amazon 提供 12 个月的免费 5GB 存储空间。
首先,您需要创建一个 Amazon AWS 账户。一旦创建,您就可以登录到 AWS 管理控制台(图 6.1)。

图 6.1 AWS 仪表板
控制台可能会有些令人不知所措,但您可以使用查找服务搜索框来查找 S3。这样做,您最终会进入 S3 仪表板本身。根据您是否之前使用过 S3 以及 Amazon 是否更新了他们的 UI(图 6.2),您的仪表板可能看起来略有不同。

图 6.2 S3 仪表板控制台。在您创建第一个存储桶之后,这将会发生变化。
Amazon 的 S3 服务允许您在存储桶中存储文件。您可以将存储桶想象成您项目的磁盘驱动器或文件夹。虽然您可以给存储桶命名任何您想要的名称,但要托管网站,您需要将存储桶的名称与您的域名相同。例如,如果我在 S3 上托管我的博客,我必须将我的存储桶命名为 raymondcamden.com。
注意:为了支持raymondcamden.com和[www.raymondcamden.com],您需要创建两个存储桶。www.raymondcamden.com的存储桶将重定向到第一个存储桶。由于我们将只使用子域名进行测试,我们只会创建一个存储桶,但我们会展示如何设置该设置,以便您可以了解如何自己操作!
首先,点击创建存储桶按钮。存储桶的名称应与您网站的域名匹配。在图 6.3 中,我使用了名为 jamstack.raymondcamden.com 的子域名。您可以将其他设置保持不变。

图 6.3 定义您的新 S3 存储桶的名称
点击下一步,完成接下来的两个步骤,完成后点击创建存储桶。然后你会看到你的存储桶被列出(图 6.4)。

图 6.4 你新创建的 S3 存储桶
选择存储桶旁边的复选框,并在出现的弹出菜单中,选择属性。这会带你到一个选项集,包括静态网站托管(图 6.5)。

图 6.5 你的 S3 存储桶的各种选项
如果你点击那个框,你会得到一个新的选项来启用网站托管,如图 6.6 所示。

图 6.6 为存储桶启用网站托管的对话框
如果你启用了网站托管,那么你需要指定索引文档是什么。这是在加载网站且没有请求特定文件时加载的页面。几乎总是应该是 index.html。输入这个值,然后你可以点击保存(图 6.7)。

图 6.7 完成选项以启用网站托管
注意:这也是你处理将 www 子域名重定向到主存储桶的地方。在这个例子中我们不涉及这一点,但现在你已经看到了设置在哪里,如果你需要,你可以自己这样做。
保存你的更改后,你可以立即使用对话框中显示的 URL 访问你的网站。如果你没有复制它,只需再次点击打开静态网站托管选项即可。你应该会看到类似于图 6.8 的内容。

图 6.8 加载新网站时出现 403 错误
你得到这个错误的原因是因为你实际上还没有上传网站!我们将在稍后介绍这一点,但在我们这样做之前,我们需要进行另一个更改。
默认情况下,你的存储桶创建时所有公共访问都被拒绝。这是一件好事,也是安全的。然而,在我们开始处理文件之前,我们需要编辑存储桶,以便公众可以读取它。这样我们就可以使我们的文件公开了。
在你的存储桶中选择权限选项卡,并在“阻止公共访问”部分中,点击编辑按钮来关闭此设置(图 6.9)。这将给你一个令人恐惧的警告,但由于我们知道我们在做什么(允许对静态资源进行公共访问),所以可以安全地继续。

图 6.9 禁用“阻止所有公共访问”
完成这些后,是时候将东西放入存储桶了。你可以将文件放入 S3 存储桶的多种方式;最简单的是使用上传选项,只需将你的文件和文件夹拖放到浏览器中。对于较小的网站,这应该足够了(图 6.10)。记住,UI 可能不同。亚马逊不断更新他们的 UI,甚至为不同的用户显示不同的屏幕。如果你看到的内容与预期不符,请尽力找到类似命名的选项。

图 6.10 您的 S3 存储桶的上传对话框
你可以选择书中的任何前一个例子或任何其他网站。对于这次演示,我们将使用第二章中 Camden Grounds 示例的输出。记住,Eleventy 存储库的输出在 _site 文件夹中。你可以选择所有文件和文件夹,并将它们拖入对话框。在点击上传之前,点击下一步,这样你就可以设置权限。在底部“管理公共权限”处,将默认值更改为“授予此对象(s)公共读取访问权限。”然后你可以点击上传按钮(图 6.11)。

图 6.11 设置权限后,是时候上传文件了。
根据你的带宽和你选择上传的内容,一旦你收到上传完成的确认,你可以重新加载之前的请求,现在你应该能看到你的网站(图 6.12)。

图 6.12 S3 部署的网站已上线!
这基本上就是流程,但你还可以做更多。例如,亚马逊有一个 CDN 服务(CloudFront),它也允许你使用 https。你还可以将重定向添加到你的网站上。既然我们已经看了亚马逊,让我们看看你的一些其他选项。
6.2.2 其他云文件存储托管选项
虽然 Amazon S3 是最知名的基于云的文件存储选项,但它并不是唯一的选择。另一个可以考虑的选项是 Google Cloud Storage (cloud.google.com/storage)。像 Amazon S3 一样,Google Cloud Storage 允许你存储任意大小的文件,并具有全球访问权限。而且,就像 S3 是 AWS 的一部分一样,Google Cloud Storage 只是整个 Google Cloud 平台的一个方面。
有价格信息(cloud.google.com/storage#section-10)可用,并且像 S3 一样,你支付的费用包括存储量和文件访问的频率。有一个免费层(mng.bz/Ex7j),老实说,一开始找起来有点困难,但确实可以让你测试项目,看看你是否喜欢这个平台。
Google 提供了托管网站的文档(mng.bz/Nx77),其模式与亚马逊平台类似。创建一个存储桶,上传你的文件,然后公开分享它们(图 6.13)。

图 6.13 Google Cloud 分享文件的说明
另一个基于云的文件服务选项是 Azure Blob Storage([azure.microsoft.com/en-us/services/storage/blobs/](https://azure.microsoft.com/en-us/services/storage/blobs/))。关于如何使用它来托管网站的文档可以在这里找到:http://mng.bz/Dx7a。或者,作为一个替代方案,你可能想考虑使用微软的一个新选项,即 Azure Static Web apps 服务([azure.microsoft.com/en-us/services/app-service/static/](https://azure.microsoft.com/en-us/services/app-service/static/)),它专门针对 Jamstack 风格的解决方案。
你现在已经看到了基于云的文件服务如何成为托管静态网站的可行解决方案。但,从本质上讲,它们主要做的就是显示文件。让我们看看更针对 Jamstack 的服务。
6.3 Azure Static Web Apps
Azure Static Web Apps([https://docs.microsoft.com/en-us/azure/static-web-apps/](https://docs.microsoft.com/en-us/azure/static-web-apps/))是微软在 Azure 平台上推出的一项新服务。它与 GitHub 仓库连接,并使用 GitHub Actions 在提交时自动执行网站更新。它支持简单的静态文件(例如,本书中之前提到的任何生成器的输出),以及静态站点生成器本身。
例如,你可以发布一个 Jekyll 站点,Azure Static Web Apps 可以被配置为执行 Jekyll 的构建操作以生成最终的静态 HTML。这一点(以及本章后续提到的服务)使其更适合 Jamstack。最后,它也与微软的无服务器平台 Azure Functions 很好地结合在一起。
在查看 Azure Static Web Apps(或 Azure 本身)之前,你需要一个免费的 Azure 账户。你可以注册([https://azure.microsoft.com/free/](https://azure.microsoft.com/free/)),然后进入你的仪表板(https://portal.azure.com),并确保你至少有一个订阅。将订阅视为在 Azure 中使用事物的高级集合。注册后,你可以创建一个免费层订阅(图 6.14)。

图 6.14 添加免费订阅的 Azure 界面
Azure,就像 AWS 一样,功能强大但同时也非常复杂。幸运的是,你可以通过使用同名的 Visual Studio Code 扩展来避免这种复杂性:Azure Static Web Apps。Visual Studio Code([code.visualstudio.com/](https://code.visualstudio.com/))是微软的一个免费、开源的代码编辑器。最近它已经成为网页开发者中的热门选择。你只需在扩展面板中搜索即可将其添加到你的 Visual Studio Code 安装中(图 6.15)。

图 6.15 通过 Visual Studio Code 本身发现扩展
安装后,您将看到一个 A 形图标(Azure 标志,如图 6.15 底部所示),点击它将打开一个新面板。扩展程序将引导您完成身份验证过程,并让您选择您之前创建的订阅。
为了测试这个服务,让我们再次使用用于 Amazon S3 示例的 Eleventy 网站。然而,这次它将是一个更强大的集成。我们不会将 Eleventy 的输出推送到 S3 存储桶,而是将使用 Azure 静态 Web 应用的 GitHub 集成来创建一个过程,其中对仓库的提交将自动触发代码构建站点并部署到 Azure。
为了使事情变得简单一些,第二章中的 Camden Grounds 网站已被复制到新的 GitHub 仓库github.com/cfjedimaster/eleventy-for-azure。我们将其克隆到本地文件系统,并做了两个更改。
首先,我们通过运行 npm install --save-dev @11ty/eleventy 在项目本身本地安装 Eleventy。这会将 Eleventy 列为项目 package.json 文件中的开发依赖项。接下来,我们需要编辑 scripts 块以添加一个新的脚本命令,指定如何构建站点:
"scripts": {
"start": "node_modules/.bin/gulp watch",
"build":"npx @11ty/eleventy"
},
启动脚本已经存在于我们构建 Camden Groups 所使用的主题中。新增的是以 build 开头的第二行。Azure 将在与仓库一起工作时注意到这一点,并使用它来构建站点。那么我们如何将此项目添加到 Azure 中呢?
在 Visual Studio Code 中,打开包含仓库的文件夹。点击 Azure 图标,在 Azure 静态 Web 应用面板中,点击加号符号以创建一个新的 Web 应用。您首先会被提示输入名称。输入 CamdenGroundsAzure(图 6.16)。如果被要求分叉和克隆仓库,请这样做。

图 6.16 命名新应用
下一个提示将询问您要使用的分支。GitHub 正在使用 main 作为分支,但如果您的项目较旧,默认分支可能是 master。它将在对话框中显示分支,您可以简单地点击它。
接下来,它将提示您输入应用程序代码目录。由于整个仓库都是应用程序,您将想要选择第一个选项:/(图 6.17)。

图 6.17 选择应用程序目录
下一个提示将询问您 Azure Functions 代码的位置。我们目前不使用它,所以选择“暂时跳过”。
最终提示将用于您网站的输出路径:这是您的静态站点生成器输出最终 HTML 的地方。对于 Eleventy,这是 _site,所以在图 6.18 所示的对话框中输入它。

图 6.18 输入在构建站点后可以找到生成文件的路径
最后的提示指的是 Azure 将在其全球基础设施中部署您的网站的位置。在这里保留默认设置,这可能会是中部美国。
到目前为止,扩展和 Azure 开始做一些工作。他们首先将适当的 GitHub 操作添加到您的项目中(如果您打开仓库可以看到),这些操作处理在文件提交时推送到 Azure。要查看 Azure 上的您的项目,点击打开 Azure 面板中的免费试用节点,您将看到您的新项目。右键单击它,并选择“在门户中打开”(图 6.19)。

图 6.19 右键单击您的新的项目以查看各种选项,包括一个打开门户的选项。
该门户包含大量信息,但您需要关注的是右侧的 URL,您可以使用它来测试您的网站(图 6.20)。

图 6.20 网站 Azure 门户
如果您打开该链接,应该会再次看到 Camden Grounds。但是,这里有一个转折点:如果您编辑您的 Eleventy 网站并将更改提交到 GitHub,Azure 的集成将启动,获取最新代码,运行构建命令,并再次部署——这一切都不需要您动手!
6.4 使用 Vercel 部署
Vercel 是我们将要关注的两个公司中的第一家,它们专注于专门为 Jamstack 和一般 Web 开发社区提供服务。Vercel([vercel.com/](https://vercel.com/)),以前称为 Zeit,为需要托管其网站的 Jamstack 开发者提供了一系列有用的服务。以下是一些这些服务的简要列表:
-
从源代码管理提供商(GitHub、GitLab、Bitbucket)部署。就像 Azure 静态 Web 应用服务一样,这意味着您可以将代码提交到您的仓库,然后 Vercel 可以自动更新您的生产网站。
-
自动预览构建用于拉取发布和分支,使共享和测试网站更改变得容易。
-
自动使用 CDN 可以使您的网站在全球范围内可用。
-
无服务器函数支持(您将在第八章中看到这个示例)以向您的网站添加额外的功能。
-
非常慷慨的免费层(https://vercel.com/pricing),这使得您可以轻松地看到 Vercel 是否适合您。
当然还有很多。但我想强调 Vercel 的一个特性,我认为这真正让它与其他产品区分开来。Vercel 内置了对超过 30 个不同的 Jamstack 平台的支持。这种支持包括识别正在使用的框架,并执行正确的操作来构建和托管网站。从实际的角度来看,这意味着你可以进入你的项目目录,部署,而 Vercel 会知道该做什么。当你需要快速将网站在线分享给他人查看和测试时,这非常强大。Vercel 仍然允许你手动配置项目,这意味着它将支持自定义实现,但你可以进入一个 Jamstack 项目,部署一个网站,并在一分钟左右将其上线,这真是太棒了。
让我们快速测试一下。前往 Vercel 的主页,点击注册(图 6.21)。请注意,你将需要使用 Vercel 使用的三个源代码管理提供商之一进行登录。

图 6.21 Vercel 的注册选项需要 GitHub、GitLab 或 Bitbucket 账户。
在你注册了 Vercel 之后,你可以使用他们的基于 Web 的仪表板从源代码管理提供商导入项目,但我们想测试的是通过 CLI 可用的快速部署选项。CLI 指令(vercel.com/docs/cli)详细说明了工具提供的所有各种选项,但就现在而言,首先通过以下命令安装它:
npm install -g vercel
接下来,你需要为你的账户配置 CLI。通过运行以下命令来完成:
vercel login
你将被要求输入与你的 Vercel 账户关联的电子邮件账户。输入它,CLI 将启动一个进程来发送你一封电子邮件(图 6.22)。

图 6.22 通过 CLI 的 Vercel 登录过程会发送一封电子邮件来处理确认。
一旦你通过电子邮件中发送给你的链接确认,CLI 会告诉你它已经准备好(图 6.23)。

图 6.23 配置好的、准备就绪的 Vercel CLI
登录后,通过使用它来部署我们在第三章中构建的 Jekyll 网站来快速测试 Vercel。在你的终端中,切换到你在书中构建(或从 GitHub 仓库下载)的最终演示所在的目录。对我来说,这是 ~/projects/the-jamstack-book/chapter3/startbootstrap-clean-blog-jekyll-master。一旦进入该目录,使用 vercel deploy 部署网站。虽然你可以使用 CLI 的多个选项,但我们真正想指出的是默认选项的强大功能。
你首先会被提示是否要设置和部署当前目录(图 6.24)。只需点击 Enter。

图 6.24 部署过程首先会与你确认,以确保当前目录是你想要部署的。
接下来,它将询问你部署的范围。这与你当前登录的账户相关,并将根据你的信息而有所不同。只需点击 Enter 以接受默认设置。
下一个提示会询问你是否希望连接到现有的项目。由于我们正在构建新的内容,再次选择默认(N)并点击 Enter(图 6.25)。如果你再次运行部署,Vercel 将正确地将其识别为现有项目。

图 6.25 继续部署流程并指定这是一个新项目
然后,你将被要求为这个项目命名。名称将默认为目录名称,在我们的例子中有点长。这个名称将用作项目名称和默认 URL。稍后,你可以为你的 Vercel 网站分配一个“真实”的域名。让我们给它一个稍微短一点的名称 verceltest 并点击 Enter。
现在,你将被询问代码所在的目录。我们是在正确的目录中启动 CLI 的,因此你可以再次点击 Enter 以接受默认设置。CLI 将对代码库进行一些检查,确定它是 Jekyll,然后询问你是否想在部署之前覆盖任何设置。再次点击 Enter 以接受默认设置(图 6.26)。

图 6.26 在 Vercel 部署之前,你有一次最后的机会修改设置。
现在,Vercel CLI 将开始工作。它会将本地目录中的代码推送到服务器。它识别出你正在运行 Jekyll,并执行所需操作以支持你网站上的 Jekyll。它会构建你的网站,最后你将得到一份关于你的网站部署位置的报告(图 6.27)。

图 6.27 将部署到 Vercel 的最终输出
CLI 提供了一份关于它所执行的操作及其所需时间的详细报告。你还会得到一个可以立即打开并查看你网站的 URL。在测试中,URL 是 verceltest-gules.vercel.app,但对你来说可能有所不同。
在这一点上,你可以添加一篇新文章,修改现有文章,或进行任何你希望的改变。例如,我们可以修改博客文章的标题和文本(_posts/2020-08-24-welcome.html,但你的日期会有所不同)以简单地添加单词 Vercel:
---
layout: post
title: "Welcome to my Vercel blog"
subtitle: "I'm so excited!"
date: 2020-08-24 12:00:00 -0400
background: '/img/posts/01.jpg'
---
<p>
This is my cool Vercel blog!
</p>
保存后,你可以再次运行 vercel deploy。这次你将不会被提示任何内容,但结果是一个 预览 构建,正如你在图 6.28 的输出中可以看到的那样。

图 6.28 预览构建的结果
真正酷的是,你现在可以在不同的标签页中打开两个 URL,以便比较差异。CLI 还非常明确地告诉你如何使你的更改在生产构建中可见,即运行 vercel --prod。运行这个命令将会更快,因为它将预览移动到生产环境,完成后,你可以重新加载原始 URL 来查看你的更改(图 6.29)。

图 6.29 我们更新的博客文章在 Vercel 上的实时状态
在继续之前,我想明确指出,这种更新方式(更改文件,重新运行 CLI)并不是用于涉及大量用户的实际生产网站。在这种情况下,你会使用源代码控制。Vercel 可以监听仓库的更改并自动更新你的网站。但这里展示的 CLI 方法对于测试、与他人共享网站等非常有用。正如我一开始所说的,Vercel 拥有一套强大的功能,所以请务必查阅其文档(vercel.com/docs)以获取更多信息。
6.5 使用 Netlify 部署
让我从一句大胆且极具个人色彩的陈述开始。Netlify (www.netlify.com/) 是托管 Jamstack 网站的黄金标准。这并不是说它完美无缺或能满足所有需求。但对我来说,它提供了性价比最高的功能集合,并且是我评价其他服务的标准。让我们看看它的服务列表;就像 Vercel 一样,我只会指出一些可用的功能:
-
能够将网站连接到 Git 仓库,并在提交后自动构建。
-
能够将网站连接到 Git 仓库,并基于分支创建预览构建。
-
创建复杂重定向文件的能力。(这对于可能从传统应用服务器迁移到 Jamstack 并确保旧 URL 仍然正确工作的人来说特别有用。)
-
使用 JavaScript 和 Go 编写的无服务器函数。你将在第八章中了解更多关于这方面的内容。
-
网站分析。
-
提供用户管理和安全性的自定义用户身份服务。
-
处理表单提交的能力。
-
对大型二进制文件提供特殊支持。
-
自动使用 CDN 以及 JavaScript 和 CSS 的压缩。这还包括压缩图片的能力。
-
对团队多人运行的网站提供出色的支持。
-
适用于本地开发的优秀 CLI。
为了明确起见,Netlify 的内容远不止于此,但你可以查看他们定价页面上的更全面列表(www.netlify.com/pricing/)。
为了测试 Netlify,我们再次使用第二章中的 Eleventy 示例,并部署 Camden Grounds。如果你还记得本章前面的内容,当我们把 Camden Grounds 部署到 Amazon S3 时,我们部署了 Eleventy CLI 的输出。如果我们对网站进行了更改,我们需要重新运行 CLI 并将文件复制到 S3。虽然这不是一项繁重的任务,但它仍然是手动操作且容易出错。让我们来了解一下在 Netlify 上创建网站的流程,它将自动从 GitHub 仓库构建。
如果你还没有这样做,首先注册 Netlify(app.netlify.com/signup)。他们有一个免费层,这将完全满足你的需求。图 6.30 显示了你可以注册的多种方式。

图 6.30 在 Netlify 上注册支持多种登录选项。
在注册(并确认你的电子邮件)之后,你就可以登录了。Netlify 的主要界面是一个列出所有网站及其当前状态的仪表板。作为一个新用户,你的仪表板看起来可能相当空(图 6.31)。

图 6.31 新用户的 Netlify 仪表板
为了进行比较,这里是我的个人仪表板(图 6.32)。

图 6.32 更充分使用的 Netlify 仪表板
注意列出的每个网站都包含当前外观和感觉的截图。在右侧是整个账户中最新的构建列表。回顾图 6.31,注意 Git 按钮中的突出显示的新网站。让我们利用这个方法来测试 Netlify 的持续部署功能。
虽然这本书有一个 GitHub 仓库,但为了测试这个功能,你需要创建自己的仓库。大多数读者可能熟悉 GitHub,但如果不知道,你可以在 GitHub 网站上阅读一篇优秀的指南,网址为guides.github.com/activities/hello-world/。此外,请注意,像 Vercel 一样,Netlify 有一个支持从终端部署的 CLI。
如前所述,我们将使用第二章中的 Camden Grounds 作为我们的测试。这个网站的文件可以在本书的仓库中找到,位于 chapter2/camdengrounds。在你的 GitHub 账户中,为 Camden Grounds 创建一个新的仓库。一旦创建,只需将之前目录中的文件复制到新仓库中。在你提交这些新文件之前,你应该添加一个小小的修改。Git 仓库支持忽略本地目录和文件,以确保它们不会被推送到仓库。这是通过一个名为.gitignore 的文件来完成的。当 Eleventy 输出构建时,它默认使用名为 _site 的文件夹。由于这是生成的输出,我们不需要它在 GitHub 仓库中。在将 Camden Grounds 提交到仓库之前创建的文件内容如下。注意我们还在.gitignore 文件中添加了 node_modules,如列表 6.1 所示。这是必需的,这样 Eleventy 就不会尝试解析其构建过程中生成的任何 Node 相关文件。
列表 6.1 Camden Grounds 的.gitignore 文件
_site
node_modules
在此操作完成后,将这些文件提交到你的新仓库中。最终结果应该看起来像图 6.33(当然,用户名会有所不同)。

图 6.33 新的 Camden Grounds 仓库
如果您只想复制我用于此测试的仓库,您可以在github.com/cfjedimaster/Camden-Ground-Test找到它。现在让我们将其连接到 Netlify!
如前所述,Netlify 会提示您从 Git 构建新站点,因此点击该按钮以开始过程。在图 6.34 中,您将看到三个不同的选项以开始过程;选择 GitHub 作为提供商。

图 6.34 从存储库创建新站点的过程开始
在您通过 GitHub 进行身份验证后,您将看到所有仓库的列表。找到您创建的仓库并点击它。下一屏幕将提示您选择要部署的分支(默认为 master 或 main),然后请求基本的构建设置。这些构建设置用于构建您的站点的命令以及结果可以找到的位置。为了告诉 Netlify 如何构建站点,我们可以使用 npx,这是使用 npm 的另一种方式。这在 Eleventy 文档(www.11ty.dev/docs/usage/)中有介绍,看起来是这样的:npx @11ty/eleventy。默认情况下,Eleventy 将输出到 _site,因此我们将使用它作为发布目录设置。您可以在图 6.35 中看到这两个设置。

图 6.35 Netlify 站点的配置设置
点击“部署站点”以开始过程。然后您可以坐下来观看。Netlify 仪表板将更新您有关从 GitHub 获取文件和运行构建命令的过程。完成后,您将在仪表板顶部看到站点的 URL 和预览(图 6.36)。

图 6.36 站点更新后的 URL 和截图
如果您点击该链接,您可以浏览站点并查看 Camden Grounds。为了真正看到 GitHub 集成的力量,让我们进行一个快速测试。打开 _data/products.json 并修改第一个产品。您更改的内容并不重要;只需进行修改即可:
{
"name" : "Netlify Powered Coffee!",
"price" : 2.99,
"description" : "This coffee is powered by Netlify!",
"thumbnail" : "http://placehold.it/700x400",
"image" : "http://placehold.it/900x350"
},
保存文件,将其检入您的 GitHub 仓库,然后观察 Netlify 仪表板(图 6.37)。

图 6.37 Netlify 仪表板显示您的构建信息
关于图 6.37,我想指出两点额外的事情。注意最早的报告显示了一个错误。虽然错误的性质并不重要,Netlify 会处理这个错误,并允许您点击构建以获取详细信息。而且——这很重要——如果在构建过程中出现问题,它不会破坏现有的网站。在这个特定的情况下,这是第一次构建,所以没有网站,但您可以放心,如果您真的搞砸了什么,您的现有网站将继续正常工作。Netlify 只有在完全且成功构建后才会更新。这意味着没有停机时间。其次,您在最新构建下看到的消息(“修改产品”)直接来自我的 GitHub 提交。通常,您应该编写好的、具体的 GitHub 提交信息,这些信息将在 Netlify UI 中显示。
构建完成后,您可以再次打开网站并查看您的新产品(图 6.38)。

图 6.38 基于最后 GitHub 提交的更新网站
让我们看看 Netlify 的一个更酷的功能:表单处理。在第七章中,我们将更多地讨论处理静态生成网站的动态方面,但由于 Netlify 使这个特定功能的使用变得非常简单,我们想在这里介绍它。如果您还记得,Camden Grounds 的“联系我们”页面没有任何实际信息。让我们通过添加一个表单来改变这一点,如表 6.2 所示。
列表 6.2 联系我们表单 (/contact.html)
---
layout: main
title: Contact
---
<div class="row my-4">
<div class="col">
<h2>The Contact Page</h2>
<form>
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name" name="name">
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" class="form-control" id="email" name="email">
</div>
<div class="form-group">
<label for="comments">Comments</label>
<textarea class="form-control" id="comments" name="comments">
</textarea>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
将此更改提交到您的仓库,并等待 Netlify 完成构建。一旦完成,您将看到您的新表单在线上(图 6.39)。当然,您也可以测试表单在本地看起来如何。通常,您会这样做,以便快速查看您的更改。由于我们正在构建一个简单的表单,我们目前不需要担心这一点。

图 6.39 简单的联系方式表单
我们已经完成了 UI,但如果您实际尝试提交页面,则没有任何反应。幸运的是,Netlify 使处理表单提交变得非常简单。通过在表单标签中添加 data-netlify="true",Netlify 将识别表单提交并自动保存结果。您只需这样做,但您也可以进行更多配置。Netlify 还支持向您的表单添加 captcha 和 honeypot 垃圾邮件陷阱。让我们快速修改 contact.html 以展示这一功能。您需要做的唯一更改是表单标签:
<form action="/contact_received.html" data-netlify="true" name="Contact">
这里有三点不同:
-
首先,我们为表单指定一个动作。在 Netlify 处理并保存表单输入后,它将用户重定向到这个页面。(这个页面位于 GitHub 仓库中,并包含一条简短的感谢信息。)
-
接下来,我们添加 data-netlify 属性来告诉 Netlify 处理提交。
-
最后,我们为表单提供一个名称。这有助于 Netlify 在您的网站有多个表单时识别不同的表单。
再次提交此更改,等待构建部署,现在当您提交表单时,您将结束在操作页面上。那么您的表单数据去哪里了?
在 Netlify 控制台中,点击顶部的“表单”链接。您最初会看到网站上活跃的表单列表,目前只有一个(图 6.40)。

图 6.40 Netlify 处理的表单列表
点击“联系表单”链接以获取表单提交的列表。如果您点击一个提交项,您可以看到详细信息(图 6.41)。

图 6.41 表单提交的详细信息
Netlify 提供了更多功能,包括表单,例如将提交的副本发送到电子邮件地址并将数据传递到无服务器函数。有关更多信息,请参阅文档(docs.netlify.com/forms/setup/#html-forms)。在下一章中,我们将讨论处理 Jamstack 网站动态方面的其他方法,包括表单处理、搜索等。
摘要
-
开发者有许多部署他们的 Jamstack 网站的选择,每种选择都有不同的优缺点。这些选项包括老式但仍然功能齐全的纯网页服务器安装、基于云的文件托管提供商以及专门为 Jamstack 设计的服务。
-
基于文件的提供商可能是简单的解决方案,但提供的功能远不止基本的文件存储。对于不需要重定向或其他高级功能的简单网站,基于文件的提供商可能是一个足够好的解决方案。
-
微软提供了一个与 Visual Studio Code 集成良好的不错选项。开发者可以直接从他们(很可能)已经使用的编辑器中连接和部署网站。
-
Vercel 和 Netlify 都专门针对 Jamstack 和前端开发者,提供众多使托管变得有吸引力的功能。表单处理、重定向规则和无服务器函数只是其中的一些例子。
7 添加动态元素
本章涵盖
-
将动态元素添加回静态网页
-
通过多个提供商处理表单提交
-
为静态网站创建搜索界面
在过去(你知道,两三年前),我们现在所知道的 Jamstack 还要简单一些。通常我们指的是静态网站和静态网站生成器。这些术语的问题在于它们暗示了一个静态、不变的网站,无法响应用户的需求。那时不是这样,现在也不是这样。
有许多选项(有些免费,有些商业)旨在为网页提供交互性。在本章中,您将看到这些服务的不同示例以及它们如何集成到一些之前的演示中。我们将讨论在选择特定产品之前开发者需要了解的权衡、价格和其他考虑因素。
7.1 表单,表单,更多表单
在 1990 年代初期作为专业网页开发者的早期工作中,我做的第一个任务就是表单处理。当时,我们的网站和现在很相似,都是简单的 HTML,为了给网站添加功能,比如处理表单,我们会使用用 Perl 编写的程序。我对 Perl 很有天赋,所以在许多项目中,我会专注于这个领域。表单自互联网开始以来就存在了,处理这些表单的需求也随之产生。让我们看看为您的 Jamstack 网站添加处理功能的几种不同选项。广泛地说,我们将探讨两种不同的方法:一种是在您网站上托管并嵌入表单,另一种是作为仅处理表单数据的服务的形式。
7.1.1 使用 Google 表单
Google 表单(forms.google.com/)是 Google 提供的一项免费服务,与更通用的 Google Docs 服务相关。开发者可以创建包含多种类型问题和不同样式的表单。表单数据会自动存储在 Google 表格(他们的 Microsoft Excel 版本)中,也可以直接通过电子邮件发送给需要获取结果的人。
在 Jamstack 中使用 Google 表单意味着在 Google 的网站上创建和设计您的表单,然后将嵌入代码添加到 HTML 文件中。这也可以在非 HTML 文件中完成。例如,如果您在使用 Liquid 的 SSG,您可以在那里添加嵌入代码,这将作为构建网站时最终 HTML 文件输出的一个部分。让我们一步步创建一个基本的表单,它将模仿网站上常见的“联系我们”表单。
首先,访问 Google 表单网站(使用此服务需要 Google 账户)。你应该会看到一个模板列表以及任何最近的表单(图 7.1)。

图 7.1 Google 表单主页,列出模板和之前的表单(如果有)
现在先点击空白模板。包含的模板相当不错,但最好从简单开始。这将带您进入 Google 表单编辑体验(图 7.2)。

图 7.2 初始空白表单
谷歌的表单编辑器做得非常好。你可以在不同类型的问题(简短回答、长文本、多项选择等)之间进行选择,并且可以自由地输入你看到的问题和答案文本。谷歌还会对你的问题进行一些相当出色的解析,使你能够更快地编写答案。例如,如果你的问题暗示了是或否的回答,谷歌会注意到这一点并建议这些作为答案。
正如我们所说,我们的第一个例子将是一个联系表单。这些表单已经存在很长时间了,通常的格式是要求提供你的联系信息,并提供一个地方来提出你的问题或发送你的反馈。
表单的设计可能是一个复杂的话题。你需要问多少个问题?需要哪些问题?你将如何为特定问题选择语言?如果任何一个环节出错,你的用户可能会简单地离开,或者更糟糕的是,花费时间填写答案然后半途而废。对于这个特定的例子,让我们使用以下问题:
-
你的名字是什么? 这将是一个简短文本回答,并且是必填项。
-
你的电子邮件地址是什么? 这也将是一个简短文本回答,并且是必填项,并且这对于我们能够联系用户并回应他们的评论非常重要。
-
你喜欢我们的网站吗? 这不是必填项,将作为一个快速的方式来衡量用户是否在使用我们的网站。它是可选的,因此用户可以跳过它,但我们应该准备好结果可能偏负面。为什么?如果一个用户在使用我们的网站时感到满意并且没有问题,他们可能不会麻烦联系我们告诉他们这一点,所以我们可以合理地假设填写此表单的大部分用户可能存在某种问题,并且可能并不开心。再次强调,这就是表单复杂性的体现:你不仅要考虑填写表单的人的用户体验,还要考虑他们为什么这样做的原因心理学。
-
你的评论。最后一个字段将是一个必填的长文本字段,供网站访客提问或提供反馈使用。
首先,为表单设置一个名称,将“未命名表单”替换为“联系表单”。可选地,你可以输入表单描述,但现在不必担心这一点。然后在表单构建器的第一个字段中输入第一个问题的文本。确保将其设置为必填项。你会注意到,一旦你输入问题的文本,智能功能就会启动,谷歌会默认将其类型设置为简短回答。这只是谷歌在试图帮助你,但你也可以将其更改为你想要的任何类型。图 7.3 显示了完成后的样子。

图 7.3 编辑后的表单
重复此步骤以请求用户的电子邮件地址。要添加新问题,请点击如图 7.3 所示的右侧加号图标,然后添加关于用户是否喜欢网站的问题。谷歌不仅会确定这是一个是/否问题,还会建议这些答案以及“可能”。你可以点击建议快速添加它们(图 7.4)。

图 7.4 谷歌智能表单编辑器识别你的问题并建议答案
在添加“是”和“否”作为答案后,添加最后一个问题。谷歌应该建议“段落”作为类型,但请确保将其设置为“必填”。完成后,点击顶部的预览图标(它是一个眼睛图标),将打开一个新标签页,显示你的表单(图 7.5)。

图 7.5 完整的表单
这个预览是一个完全功能性的表单,你现在就可以使用并提交。你可以通过故意省略一些内容来查看验证逻辑是如何工作的。提交你的表单。当你这样做时,你会得到一个简单的确认(图 7.6)。

图 7.6 用户提交表单后看到的内容
这样做几次后,返回你正在编辑表单的标签页。你会看到“回复”标签页会注意到已经收到了提交(图 7.7)。

图 7.7 高亮显示“回复”标签页
点击“回复”标签页将显示回复的摘要(图 7.8)。谷歌在显示这些数据方面做得令人钦佩,并认识到“你喜欢我们的网站吗?”这个问题适合作为饼图,但奇怪的是,将电子邮件地址显示为柱状图。

图 7.8 表单回复的摘要
点击“问题”标签页将允许你一次查看一个问题的结果,而“单个”则一次显示一个完整的回复。顶部的绿色图标允许你创建一个 Google 表格,并自动将回复(以及未来的回复)连接到电子表格。然而,在大多数情况下,开发者(或网站的所有者)更愿意收到电子邮件回复。要设置此功能,请点击绿色电子表格图标右侧的三点菜单,并在弹出菜单中选择“为新电子邮件回复获取电子邮件通知”(图 7.9)。

图 7.9 设置和配置电子邮件回复
点击此按钮将启用未来提交的电子邮件通知,但请注意,它会发送到谷歌账户的所有者。现在,如果你再次填写表单,你会收到一封电子邮件(图 7.10)。

图 7.10 提交表单后的收到的电子邮件
不幸的是,你仍然需要点击才能看到回复,但至少你知道已经提交了回复。
现在您的表单已经构建完成,并且可以接收提交的内容,下一步是什么?虽然我们的网站可以简单地链接到表单,但这意味着用户将离开我们的网站,而这通常不是我们希望看到的情况。相反,我们可以直接在我们的网站上放置表单。这可以通过 iframe 嵌入代码来实现。你可能会觉得这个代码并不明显,但它可以通过表单编辑器顶部的“发送”按钮找到。点击此按钮会打开一个对话框,默认是通过电子邮件发送表单,但点击“<>”图标会显示 iframe 代码(图 7.11)。

图 7.11 发送对话框的 HTML 选项
使用此代码,您现在可以将其嵌入到任何您想要的网页中,包括 Jamstack 网站内。但让我们从一个使用典型 HTML 页面布局的简单示例开始。Google 表单的嵌入代码作为页面的一部分包含在内。请确保将其替换为您自己的嵌入代码。
列表 7.1 Google 表单嵌入示例 (/chapter7/forms/test1.html)
<!DOCTYPE html>
<html>
<head>
<title>A Regular Page</title>
<style>
body {
background-color: bisque;
font-family: Verdana, Geneva, Tahoma, sans-serif;
}
footer {
margin-top: 10px;
font-size: 10px;
}
</style>
</head>
<body>
<h1>My Site's Contact Form</h1>
<iframe src="https://docs.google.com/forms/d/e/1FAIpQLSc-
➥ 2J3cgrJE22fbVJKBOtZFlmzpiqE36SE-eAmJHqTh6NZHrA/viewform?embedded=true"
➥ width="640" height="879" frameborder="0" marginheight="0"
➥ marginwidth="0">Loading...</iframe>
<footer>
<p>Copyright Whenever</p>
</footer>
</body>
</html>
这是一个相当短的模板,包含页眉和页脚,以及 Google 表单中的 iframe。我们在这里也使用了一些 CSS 来美化页面。这样做的原因只是为了展示当表单添加到具有独特样式的页面时,表单的外观。Google 表单允许在外观和感觉上进行一些定制,因此您可能需要稍微处理一下,但我们想展示一个无需定制的即插即用集成。图 7.12 展示了它在页面上的渲染效果。

图 7.12 Google 表单
如您所见,它集成得很好。有一个滚动条,但可以通过修改 iframe 的高度属性或使用 CSS 来调整。如果您提交此表单,整个过程将在 iframe 内完成,用户永远不会离开网站。虽然它在页面上确实显得有些突兀,但这是一种快速简单的解决方案。更好的是,表单可以被非技术用户编辑,并且由于它是嵌入的,因此您网站上不需要进行任何更改来反映他们的更改。现在您已经看到了一个远程托管表单的示例,让我们考虑一些更集成的方案。
7.1.2 集成 FormCake
虽然 Google 表单通过外部宿主在 iframe 中显示,但开发者可能希望对自己的表单设计和设置有更多的控制。现在存在多个服务,它们提供端点,或发送您的表单数据的地方,然后接收数据,进行“操作”(它们做什么取决于服务),然后将用户重定向回您的网站。对于大多数人来说,他们并不知道发生了什么。他们只是点击了您的提交按钮,然后被展示了一个感谢或确认页面。但在幕后,相关的服务解析了表单,对数据进行了一些操作,然后将网站访问者直接重定向回您的网站。
这些服务之一是 FormCake (formcake.com)。这项服务提供表单处理,包括文件上传支持、垃圾邮件保护和在数据上执行操作的能力。至少,它可以电子邮件发送数据给你(或网站的拥有者),但它还可以与 Zapier(一种自动化服务,允许你在工作流中连接不同的应用程序,例如,在表单提交时将信息发送到 Salesforce)等解决方案集成。FormCake 目前有三个不同的定价层级(更多详情请见formcake.com/pricing),但他们的免费层允许无限数量的表单,100 次提交和基本的垃圾邮件保护。这对我们来说已经足够了,所以让我们试一试。
首先,注册(你可以使用电子邮件和密码组合或你的 GitHub 账户)。完成之后,你将被带到包含一个已创建表单的仪表板(图 7.13)。

图 7.13 FormCake 的表单仪表板
点击进入表单将为你提供许多设置和集成说明,以便与它一起工作(图 7.14)。

图 7.14 如何使用表单的说明
指令主要归结为确保你的表单使用 POST,并复制第一步中显示的操作端点。让我们构建一个使用此操作的表单。Google 表单的 iframe 已被完全手动编写的表单和 FormCake 提供的端点所取代。它提出的问题与之前相同。
注意:端点是针对作者的,应该替换为你从 FormCake 获得的端点。
列表 7.2 FormCake 表单 (/chapter7/forms/test2.html)
<!DOCTYPE html>
<html>
<head>
<title>A Regular Page</title>
<style>
body {
background-color: bisque;
font-family: Verdana, Geneva, Tahoma, sans-serif;
}
footer {
margin-top: 10px;
font-size: 10px;
}
</style>
</head>
<body>
<h1>My Site's Contact Form</h1>
<form method="post" action =
"https://api.formcake.com/api/form/451d4b55-3e2a-4eee-b7c4-
➥ 1b54041a5365/submission">
<p>
<label for="name">What is your name?</label>
<input name="name" id="name" required>
</p>
<p>
<label for="email">What is your email address?</label>
<input name="email" id="email" type="email" required>
</p>
<p>
Do you like our site?<br/>
<input name="like" id="likeyes" value="yes" type="radio">
<label for="likeyes">Yes</label><br/>
<input name="like" id="likeno" value="no" type="radio">
<label for="likeno">No</label><br/>
</p>
<p>
<label for="comments">Your comments</label>
<textarea name="comments" id="comments" required></textarea>
</p>
<p>
<input type="submit">
</p>
</form>
<footer>
<p>Copyright Whenever</p>
</footer>
</body>
</html>
如果你启动一个本地 Web 服务器来测试这个(我建议www.npmjs.com/package/httpster),你可以查看表单,提交它,然后最终到达 FormCake 的默认提交页面(图 7.15)。

图 7.15 默认的 FormCake 响应
如果你回到 FormCake 仪表板,你可以点击“提交”标签并查看你的响应(你可能需要刷新)。点击它将给你一个详细视图(图 7.16)。

图 7.16 FormCake 的提交视图
为了使这个表单看起来更美观,让我们在表单设置中进行两项更改。在“设置”选项卡中,向下滚动到“成功重定向”,并添加一个 URL 以将用户重定向回他们提交表单的位置。目前我们正在本地测试,因此我们可以使用 localhost URL,但在生产环境中这不会起作用。在我的环境中,我的表单可在 http://localhost:3333/chapter7/forms/test2.html 处访问。我创建了一个名为 test2_thankyou.html 的新文件(你可以在网站的 GitHub 仓库中找到这个文件。文件中的内容并不重要;它只需要存在),然后将其用作重定向值:http://localhost:3333/chapter7/forms/test2_thankyou.html(图 7.17)。

图 7.17 指定重定向的表单设置
接下来,我们需要设置它,以便每次表单提交时你都能收到一封电子邮件。点击“操作”选项卡,然后在“添加操作”下拉菜单中选择“电子邮件通知操作”。这将提示你为操作输入名称、发送信息的电子邮件地址和主题。将名称输入为“电子邮件通知”,输入你自己的电子邮件地址,主题为“表单提交”。现在你可以再次填写你的表单,提交后,你将被重定向到你的感谢页面。最终用户不会看到 FormCake 网站,甚至不知道你正在使用该服务。为了清楚起见,它并不是隐藏的,开发者可以轻松地监控网络流量,但普通用户不会知道(或关心)。不久之后,你会收到一封电子邮件通知(图 7.18)。

图 7.18 表单提交的电子邮件副本
如果你想要一些更定制的功能,FormCake 甚至允许你定义一个使用 Liquid 进行变量替换的电子邮件模板。这是一个相当强大的功能!
7.1.3 其他选项
当然,还有其他处理表单的选项。有许多类似于 FormCake 的选项,你只需为你的表单使用特定的操作。还有 Wufoo(www.wufoo.com/),它更像 Google Forms,但具有强大的设计和编辑功能。如前一章所述,Netlify 作为 Jamstack 主机内置了表单处理。在下一章中,我们将讨论无服务器函数,这是另一种响应表单提交的方式。
7.2 添加搜索
在处理基本表单输入后,搜索可能是 Jamstack 网站最重要的功能之一。如果你的网站有超过几页,为用户提供快速找到他们想要的内容的能力变得很重要。在本节中,我们将讨论两种不同的搜索选项,并将遵循与上一节类似的格式。我们将从一个“直接插入”的解决方案开始,再次来自 Google:可编程搜索引擎(programmablesearchengine.google.com/about/)。
Google 的可编程搜索引擎之前被称为自定义搜索引擎,因此如果您在本书的教程之后进行更多研究,您可能会找到提到该服务或 CSE 的文章。这是 Google 提供的一项服务,允许您定义,基本上是 Google 搜索引擎的一部分,用于您自己的网站。
要开始,只需从可编程搜索引擎网站点击“开始使用”按钮,并使用您的 Google 凭据登录。登录后,您将被带到仪表板(图 7.19)。

图 7.19 可编程搜索引擎仪表板
通过点击“添加”按钮开始使用此服务。您将遇到的第一个提示是搜索的网站,这里事情变得有趣。您可以在这里输入任何您想要的网站。没错——即使您正在域名 X 上构建自己的网站,您也可以为您的搜索引擎输入域名 Y(以及更多)。您可能想使用自己的域名,但 Google 允许您决定任何有意义的选项。注意(如图 7.20 所示),您还可以输入子目录。

图 7.20 设置可编程搜索引擎的初始屏幕
正如我们所说,您在这里可以输入任何您想输入的内容。如果您已经部署了自己的 Jamstack 网站,即使是在临时位置,您也可以输入 URL。为了本书的目的,并且为了给我们提供大量内容,我们将使用我的博客raymondcamden.com。对于搜索引擎的名称,让我们使用 JamstackSearch1。这完全是随机的,只是为了帮助您区分您的可编程搜索引擎与其他搜索引擎。在现实世界的场景中,我通常会使用与网站本身相同的名称。输入您的值后,点击创建按钮。然后您将看到一条成功消息(图 7.21)。

图 7.21 恭喜,您已经构建了自己的搜索引擎(说实话,是 Google 帮您构建的)。
在这一点上,请注意三个选项。第一个(获取代码)将为您提供将搜索添加到您网站所需的代码,我们将在下一部分介绍。第三个选项(控制面板)是您配置选项的地方;我们也会演示这一点。然而,中间的(公共 URL)是另一回事。一旦您构建了一个可编程搜索引擎,Google 就会提供一个 URL,让您可以立即使用您的搜索引擎。如果您想测试它的工作效果、结果排名等,这非常棒。在您构建网站的同时,您也可以与客户分享这个 URL,让他们也能看到。
现在,点击“获取代码”。在列表 7.3 中,您将看到一个简短的代码片段。您将使用这段代码并将其放入一个简单的 HTML 页面中。与上一节相同的基本外壳被使用。
列表 7.3 搜索测试页面 (/chapter7/search/test1.html)
<!DOCTYPE html>
<html>
<head>
<title>A Regular Page</title>
<style>
body {
background-color: bisque;
font-family: Verdana, Geneva, Tahoma, sans-serif;
}
footer {
margin-top: 10px;
font-size: 10px;
}
</style>
</head>
<body>
<h1>Search</h1>
<script
async src="https://cse.google.com/cse.js?cx=9fda4c9699117d517"></script>❶
<div class="gcse-search"></div> ❷
<footer>
<p>Copyright Whenever</p>
</footer>
</body>
</html>
❶ Google 代码片段的开始部分
❷ 代码片段的结尾
与 Google 表单一样,您可以在您认为合适的地方插入代码片段。启动本地网络服务器,然后运行页面。您将看到一个完全由 Google 驱动的基本表单体验(图 7.22)。

图 7.22 Google 提供的默认搜索体验
在搜索框中输入一些内容。根据您输入的域名,您应该尝试一些有意义的内容。如果您使用了我的域名 (raymondcamden.com),您可以尝试“vue.js”作为输入。图 7.23 展示了您可能会看到的内容。

图 7.23 搜索可编程搜索引擎的结果
立刻您会注意到,UI 与您可能预期的略有不同。它不是在页面上显示结果,而是使用一个浮动的模态框。如果您不喜欢这样(我当然不喜欢),我们可以调整它,我们将在稍后这样做。您可能注意到的下一件事,尤其是在图 7.23 中,是每个可见的结果都是一个广告。是的,就像主要的 Google.com 网站,您会在结果中看到广告。图中未显示的是,如果您滚动过四个广告(是的,四个,当然,您的结果可能会有所不同),您确实会得到一些很好的代码片段和图像(图 7.24)。

图 7.24 在广告之后可以找到“真实”的结果。Google 需要钱,这是您知道的。
让我们开始自定义搜索引擎。回到仪表板,点击“外观”。在这里,您将找到多个选项来自定义搜索引擎的显示方式,但让我们先从将布局从“覆盖”更改为“全宽”开始(图 7.25)。

图 7.25 为您的搜索引擎指定新的布局
图 7.25 中未显示,但很方便使用的是,在仪表板的右侧有一个您搜索引擎的实时示例。如果您想测试,可以在这里进行,或者只需点击“保存并测试”回到简单的 HTML 页面。您的更改将立即显示(图 7.26)。

图 7.26 带有内联结果的搜索引擎
让我们再调整一下。如果您点击其中一个结果,您会看到它将在新标签页中打开。虽然这确实可能是您想要的,但让我们将其更改为在当前标签页中打开。在仪表板中,转到“搜索功能”,“高级”,“网络搜索设置”。在“链接目标”字段中输入“_self”(图 7.27)。

图 7.27 通过仪表板修改搜索结果行为
如前所述,保存并重新加载您的测试 HTML 文件,然后点击一个结果。它应该在同一个标签页中打开。
如果你想要修改片段的 HTML,谷歌的可编程搜索引擎(Programmable Search Engine)提供了更多的自定义选项。开发者指南(developers.google.com/custom-search/docs/overview)详细介绍了可以实现的功能。你还可以使用一个返回纯 JSON 结果的 API,但这仅是一个商业功能。现在我们已经考虑了一个即插即用的解决方案,让我们再次看看一个更集成的选项。
7.2.1 使用 Lunr 进行搜索
在之前的搜索示例中,搜索服务本身完全由第三方处理。你很可能还会依赖谷歌几年,但如果你想要一个完全自托管、自包含的解决方案怎么办?一个例子是 Lunr (lunrjs.com/)(图 7.28)。

图 7.28 Lunr 的辉煌简单的网页
Lunr 是一个完全客户端解决方案(它也可以在服务器端使用,但通常这不是典型用例),用于向网站添加搜索功能。它从创建索引开始。这是你决定要搜索什么的地方。例如,一个开发者服务网站可能包含一些营销页面和一套深入的文档。虽然你可以创建一个包含所有内容的索引,但你可能只想将索引集中在开发者文档上。
即使你确定了要搜索的内容,你仍然需要考虑索引的大小以及如何使其更小。回到之前的例子,如果你的开发者文档覆盖了数百页的文档,你可能只考虑索引每页文档的第一段。
一旦你确定了要索引的内容,你就将这个信息提供给 Lunr。Lunr 将解析文本并对它进行魔法般的处理。好吧,不是魔法,但它将解析内容使其更容易搜索。一旦创建了索引,你就可以使用简单的搜索词或更复杂的搜索查询(“找到所有提及猫且属于 API 类别的文档”)来对其搜索。如果你愿意,还可以根据其质量(Lunr 将为每个结果评分)进行过滤。让我们看看如何将 Lunr 添加到 Jamstack 网站。
将 Lunr 添加到 Eleventy 网站
在第二章中,你被介绍到了静态网站生成器 Eleventy (www.11ty.dev/)。为了测试将搜索添加到 Jamstack,我们将从一个现有的 Eleventy 网站开始。这个网站使用了来自 GI Joe 维基百科(mng.bz/laAB)的文本,这是一个与 GI Joe 系列相关的在线资源。我复制了六个不同角色的部分描述来构建一个非常基础的 GI Joe 人物网站(图 7.29)。

图 7.29 网站的主页,仅列出角色
点击单个角色链接会显示一些基本文本和一张图片(图 7.30)。

图 7.30 单个角色页面
创建此网站的初始代码由几个基本页面组成。(记住,你可以在本书的 GitHub 仓库中找到这些内容。)首先,主页加载所有角色并将它们显示在列表中。
列表 7.4 网站主页(/chapter7/search/lunr/index.liquid)
---
layout: main
---
<h1>Characters</h1>
<ul>
{% for character in collections.characters %}
<li><a href="{{ character.url }}">{{ character.data.title }}</a></li>
{% endfor %}
</ul>
Eleventy 创建相关内容集合的一种方式是使用标签。你可以看到其中一个角色的源代码。在最上面的前导部分,标签的值设置为 characters。这在所有角色中都是重复的。(注意,我们已删除一些文本以节省空间。)
列表 7.5 示例角色页面(/chapter7/search/lunr/characters/destro.md)
---
layout: character
title: Destro
faction: Cobra
tags: characters
image:
➥ https://vignette.wikia.nocookie.net/gijoe/images/c/ca/RAH_Destro02.jpg/
➥ revision/latest?cb=20080730134919
---
Destro is one of the most cunning foes the Joe Team has ever faced. He is
the power behind M.A.R.S. (Military Armament Research Syndicate), one of
the largest manufacturer of state-of-the-art weaponries. His business is
fueled by inciting unstable countries to wage wars against each other and
then getting them to purchase weapons from him. To him, war is simply man's
expression of his most natural state. It is the perfect example of where the
fittest survive and where many technological advances are made. His biggest
client, thus far, is Cobra with whom he maintains an alliance of
convenience. Despite being a manipulative person, Destro maintains a sense
of honor and actually respects the Joe Team for their skills and
expertise, if not their motivation.
用于显示角色和主页本身的模板都可以在 GitHub 仓库的 chapter7/search/lunr/_includes 目录中找到。这些文件处理页眉和页脚以及角色的显示,但与我们即将添加的搜索功能无关。然而,当我们添加对 Lunr 客户端方面的支持时,我们将很快展示主要模板。
如前所述,Lunr 要求你创建你数据的索引。这是你网站上想要搜索的内容的数据友好视图。我们的网站由一个主页、多个角色页面以及最终的一个搜索页面组成。但我们只想搜索我们的角色数据。在列表 7.4 中,你看到你可以在一个集合中循环遍历这些信息。我们可以使用相同的逻辑来创建一个 Lunr 最终可以用来创建索引的 JSON 版本,如下所示。
列表 7.6 创建角色 JSON 列表(/chapter7/search/lunr/lunr.liquid)
---
permalink: /index.json ❶
---
[
{% for character in collections.characters %}
{
"title":"{{character.data.title}}",
"url":"{{character.url}}",
"content":"{{ character.templateContent | json }}" ❷
} {% if forloop.last == false %},{% endif %} ❸
{% endfor %}
]
❶ 指定模板输出的目标
❷ 使用过滤器以 JSON 安全的方式输出内容
❸ 在除了最后一个项目之外的所有项目中包含逗号
在此模板中需要注意的第一点是前导部分中使用的永久链接。这告诉 Eleventy 为此模板更改其正常的文件命名行为,并将结果存储在网站根目录下的 index.json 文件中。在模板中,我们使用 Liquid 动态输出角色数组。对于每个角色,我们输出标题、角色的 URL(Eleventy 为我们提供了这个集合),然后是内容。请注意,我们使用一个过滤器,json,来操作输出。Eleventy 提供的 templateContent 值包括角色的渲染内容。这包括 HTML 和换行符,我们都不希望包含在内。我们很快将创建这个过滤器。最后,请注意,我们使用了一个 Liquid 循环特性,它可以检测我们是否处于最后一个迭代。我们这样做是因为我们需要在数组中的每个项目之间放置一个逗号,但不想在最后一个项目后放置一个。查看完成后的 JSON 看起来可能有助于理解。
列表 7.7 生成的 JSON 文件
[
{
"title":"Baroness",
"url":"/characters/baroness/",
"content":"The Baroness serves as..."
} ,
{
"title":"Cobra Commander",
"url":"/characters/cobra_commander/",
"content":"Not much is known of the ..."
}
]
为了支持列表 7.6 中使用的 JSON 安全过滤器,我们必须在 Eleventy 配置文件中定义此内容。
列表 7.8 Eleventy 配置(/chapter7/search/lunr/.eleventy.js)
module.exports = function(eleventyConfig) {
eleventyConfig.addPassthroughCopy("css"); ❶
eleventyConfig.addFilter("json", value => {
//remove html and line breaks
return value.replace(/<.*?>/g, '').replace(/\n/g,'');
})
};
❶ 这行代码只是告诉 Eleventy 复制用于此简单站点的 CSS。
过滤器通过首先删除所有 HTML 和换行符来工作。注意,这里也可以使用其他方法。例如,如果角色的描述非常长怎么办?或者如果我们有超过六个字符怎么办?为了保持 JSON 的大小,你可以调用只返回角色的描述的前一千个字母。最终,Lunr 并不关心,但发送给它的数据越多,它需要处理的数据就越多!
现在我们有了用作源数据文件,是时候构建实际的搜索引擎了。图 7.31 展示了它将如何看起来。我们有一个简单的字段在顶部,一个按钮,至少最初就是这样。

图 7.31 初始搜索表单
输入一些内容并点击搜索后,将显示带有指向相应页面链接的结果(图 7.32)。

图 7.32 在表单下方显示搜索结果
下面的列表展示了这是如何构建的。
列表 7.9 搜索页面(/chapter7/search/lunr/search.liquid)
---
layout: main
---
<h1>Search</h1>
<input type="search" id="search">
<button id="searchBtn">Search</button>
<div id="results">
</div>
<script src="https://unpkg.com/lunr/lunr.js"></script> ❶
<script>
document.addEventListener('DOMContentLoaded', init, false);
let searchField, searchButton, resultsDiv;
let docs, idx;
async function init() { ❷
searchField = document.querySelector('#search');
searchButton = document.querySelector('#searchBtn');
resultsDiv = document.querySelector('#results');
let result = await fetch('/index.json');
docs = await result.json();
// assign an ID so it's easier to look up later,
// it will be the same as index
idx = lunr(function () { ❸
this.ref('id');
this.field('title');
this.field('content');
docs.forEach(function (doc, idx) {
doc.id = idx;
this.add(doc);
}, this);
});
searchButton.addEventListener('click', search, false);
}
function search() { ❹
let term = searchField.value;
if(!term) return;
console.log('search for '+term);
let results = idx.search(term);
console.log(results);
// we need to add title, url from ref
results.forEach(r => {
r.title = docs[r.ref].title;
r.url = docs[r.ref].url;
});
let result = '<h2>Results</h2><ul>';
results.forEach(r => {
result += `
<li><a href="${r.url}">${r.title}</a></li>
`;
});
result += '</ul>';
resultsDiv.innerHTML = result;
}
</script>
❶ 加载 Lunr 库。
❷ 此代码在浏览器页面加载时运行。
❸ 这是定义 Lunr 索引的地方。
❹ 此代码处理用户输入并执行搜索。
模板从一段非常短的 HTML 代码开始。我们有搜索字段、按钮,以及它们下面的一个空 div 块。这个块将被用来渲染结果。列表的大部分是 JavaScript 代码。
在我们进入 JavaScript 之前,我们必须添加对 Lunr 本身的支持。这是通过指向 CDN 上的库来完成的,网址为unpkg.com。我们也可以下载库并将其放置在我们的网站上。
我们的代码首先指定一个在页面加载时运行的函数(init)。还定义了一些全局变量,以便稍后使用。
init 函数执行了各种操作。首先,它创建了指向 DOM 中我们需要与之交互的元素的指针:搜索字段、按钮和空 div。
接下来,它使用 fetch API 加载我们之前定义的 JSON。它通过解析 JSON 将其加载并转换为 JavaScript 数据。一旦我们有了这些,我们就可以创建 Lunr 索引。我们首先定义数据的主键。这是索引中每个项目的唯一标识符。我们将其命名为“id”。然后我们定义将要被搜索的内容。那将是我们的数据中的标题和内容字段。
在这一点上,索引行为已经定义,但实际上并没有填充任何内容。为了做到这一点,我们遍历 JSON 数组中的每个项目,并将其添加到索引中。为了创建主键,我们只需使用循环索引。这个索引被添加到我们的文档对象中,然后添加到索引中。
在 init 函数中完成的最后一件事是为搜索按钮添加事件处理器。当用户点击该按钮时,我们首先检查他们是否输入了任何内容,如果没有,就简单地退出搜索函数。
对 Lunr 进行搜索非常简单;实际上只是一行代码:
let results = idx.search(term);
这为我们提供了一个结果数组,如果有什么匹配的话。注意在 console.log 之后的用法。这让我们可以使用浏览器的开发者工具来检查结果。这是一种了解如何使用 Lunr 找到的内容的好方法(图 7.33)。

图 7.33 浏览器开发者工具显示的 Lunr 搜索结果
现在我们有了结果,我们必须处理它们,以便我们可以显示找到的内容。关于 Lunr 可能会让人惊讶的一点是,搜索结果实际上并不包含文档本身。在图 7.33 中,你可以看到一个 ref 值。记得我们为内容定义了主键并手动添加了循环索引吗?这就是我们将如何显示结果的方式。Lunr 对 ref 的使用基本上要求你将结果与原始数据关联起来。这就是搜索之后循环所做的事情。对于每个结果,我们添加了原始文档集中的标题和 URL。有了这些,我们就可以使用一些简单的 HTML 来显示结果。
当然,在这个模板中你还可以做更多的事情。我们可以添加支持让用户知道没有找到任何内容。我们可以添加支持搜索我们内容的一个特定部分(例如,GI Joe 角色被分为团队,我们的搜索界面可以让你指定一个要搜索的部分)。在这一点上,Lunr 非常灵活,几乎可以支持你网站上需要的任何功能。
7.2.2 其他选项
就像表单一样,在添加搜索时,你还有许多其他选项需要考虑。其中一个特别引人注目的选项可能对 Jamstack 开发者感兴趣:Algolia (www.algolia.com/)。Algolia 是一个商业服务(提供慷慨的免费层),它使用类似于 Lunr 的索引。与 Lunr 不同,Algolia 在其服务器上托管你的索引,并提供 API 来编辑该索引和对其搜索。它还提供了有关访客如何在你的网站上搜索的强大分析。
如果你更喜欢类似 Google 的服务,但又不想使用 Google,Microsoft 提供了类似的服务,即 Bing Custom Search (www.customsearch.ai/)。像 Google 一样,它也提供这项服务的免费版本。
7.3 其他动态选项
在本章中,我们专注于两种将动态内容添加回你的 Jamstack 网站的方法。显然,还有许多其他方法可以实现这一点。在第五章中,你看到了如何将电子商务添加到你的网站中。在下一章中,你将看到如何通过添加无服务器功能来几乎实现任何事情。以下是一些其他形式的动态内容:
-
日历—Google Calendar (
calendar.google.com/) 允许你将完整的日历嵌入到 HTML 页面中。像其他 Google 服务一样,你有一些基本的样式选项。你也可以使用像 FullCalendar (fullcalendar.io/) 这样的开源库,为你的日历提供完全独特的设计,同时仍然可以由 Google Calendar 数据驱动。 -
评论—虽然通常只在博客中使用,但你可能希望在你的 Jamstack 网站中添加评论。最知名的可能就是 Disqus (
disqus.com/)。这是一个相当标准的互联网评论工具,可以免费使用。如果你愿意自己构建解决方案,还有其他选项,如 Commento (commento.io) 和 FastComments (fastcomments.com),但要做好一些工作的准备。Matt Mink 的一篇文章 (css-tricks.com/jamstack-comments/) 描述了这样一个系统。 -
聊天—网站(通常是商业网站)上的另一个常见工具是聊天框。这通常是一个位于网站右下角的谈话气球,允许用户点击与网站管理员交谈。此类服务的商业例子是 LiveChat (
www.livechat.com/)。这些服务可以用预设的回复(“当被问及 foo 时,回复 goo”)进行编程,也可以连接到真实的人类。 -
APIs—Jamstack 中的 A 代表 API,意味着任何可以通过 JavaScript 调用的远程资源(如天气、股票数据等),并且可以在你的 Jamstack 网站中调用。
摘要
-
一个简单的静态 HTML、CSS 和 JavaScript 网站,仍然可以包含动态元素(表单、搜索、日历等)。
-
许多服务使得将动态特性添加到静态网站变得极其简单。例如,Google 提供了多个服务(表单、搜索、日历),你只需要复制一些 HTML 并将其粘贴到模板中即可。像 WuFoo、FormCake 和 FormKeep 这样的服务只是处理表单输入的几个选项之一,这是动态支持的一个例子。
-
选择使用哪种服务将取决于你的需求、预算以及哪种服务最能提供你需要的成果。
8 使用无服务器计算
本章涵盖
-
定义无服务器
-
解释无服务器对 Jamstack 的意义
-
描述 serverless 选项
-
使用 Netlify 构建 serverless 函数
-
使用 Vercel 构建 serverless 函数
在上一章中,我们讨论了几种不同的方法,可以将动态元素重新添加到你的 Jamstack 网站中。这些是通过为满足特定需求而专门设置的外部服务完成的,例如,Google 表单让你设计和接受表单的反馈。
尽管这些服务的可用性在数量和种类上都在持续增长,但有些事情作为外部服务来说根本就不合理,包括只有你知道如何开发的针对你的 Jamstack 网站的非常特定的业务逻辑。这就是无服务器计算,特别是作为服务的函数,发挥作用的地方。
8.1 什么是无服务器计算?
让我们先把这个问题讲清楚。在无服务器计算中仍然存在服务器。就像我们在真正的云中实际上并不构建基础设施一样,无服务器技术并不会神奇地消除对物理硬件的需求。无服务器计算是关于消除对服务器的担忧。它是在不维护服务器的枯燥工作中,获得服务器提供的一切好处。
有许多服务可以归入无服务器定义的范畴。这里只列举一些:
-
能够完全从其网站 (mongodb.com) 中配置和使用 MongoDB 数据库,而无需自己安装任何东西。
-
在 Pipedream (pipedream.com) 中,通过事件驱动的复杂工作流程的能力。Pipedream 允许开发者通过组合各种步骤来构建工作流程,这些步骤可以包括自定义代码。
-
当然,还有使用无服务器计算的第一家真正的企业级提供商 Amazon Lambda (
aws.amazon.com/lambda/) 来构建几乎任何东西的能力。
虽然无服务器计算有许多解释和实现,但本章将重点关注最广为人知和使用的例子:函数即服务(FaaS)。FaaS 允许你专注于特定功能的业务逻辑,而不必担心托管或路由。让我们考虑一个简单的例子。
假设你想构建一个返回当前时间的服务。首先,不要这样做。浏览器知道时间。但这为我们提供了一个简单的构建要求。如果我用 Node.js 构建,设置可能如下所示:
-
部署 Linux 虚拟机。
-
确保 Node 可用。
-
编写一个 Node 脚本,该脚本将监听端口和特定路径上的请求。
-
确保虚拟机已设置以响应该 HTTP 请求。
-
当发生这种情况时,执行业务逻辑(获取当前时间)。
使用 FaaS 通常会产生相同的结果
- 执行业务逻辑。
就这样。显然,这是一个有些牵强的例子,但想法是开发者不再需要担心服务器、操作系统、网络路由等问题,而只需关注他们实际构建的逻辑。这并不总是那么容易,但你应该明白了。
那么,这对 Jamstack 有什么影响呢?虽然市面上有多个服务提供各种服务的 API,但有时你需要非常具体的逻辑,而这只有你自己才能创建。通过使用无服务器计算和函数即服务,开发者可以精确地编写他们需要的代码,将其暴露为 API,然后从他们的 Jamstack 网站使用 JavaScript 调用它。让我们看看使用这种功能的一种形式:Netlify 函数。
8.2 使用 Netlify 构建 serverless 函数
我们在前面一章讨论了 Netlify,我们暗示的一个功能是能够编写无服务器函数。这个功能称为 Netlify 函数 (functions.netlify.com/),它允许你包含用 JavaScript 或 Go 编写的函数。Netlify 处理将你的代码通过 HTTP 提供出来,而你无需做任何额外的工作。Netlify 在其免费层(有限制)上支持这些函数,并为更高成本提供更高层级的选项 (www.netlify.com/pricing/#add-ons-functions)。Netlify 函数建立在 Amazon Lambda 之上,但你无需了解该服务即可使用它们。
Netlify 函数支持 URL 和表单参数,这意味着你的客户端代码可以在执行调用时以多种方式传递参数。你的代码还可以访问请求的完整详细信息,并以各种形式返回数据,尽管通常你会返回 JSON。
在我们的第一次测试中,我们将使用 Netlify CLI。虽然不是必需的,但在这个章节中,它将有助于我们更快地开始,并更容易测试我们的代码。要安装 Netlify CLI,请使用 npm:
npm install -g netlify-cli
安装完实用程序后,你将能够从你的终端运行 netlify function(图 8.1)。

图 8.1 Netlify CLI 的默认输出为其命令提供基本帮助。
在确认你已正确安装 CLI 后,运行 netlify login。这将提示你进行身份验证,所以如果你在部署章节中没有为他们创建账户,现在就创建一个。一旦登录,你将来就不需要再次进行身份验证。
如您在图 8.1 中所见,CLI 做了很多事情,但我们在本章中只将涉及其中的一些方面。特别是,CLI 可以为我们搭建函数,包括许多示例应用程序以帮助入门。
您可以将 Netlify 函数存储在任何您想要的文件夹中,但最近 Netlify 开始支持默认位置 netlify/functions。不幸的是,在本书编写时,CLI 还不支持了解这个默认值,这意味着我们需要指定它。虽然这可以通过 Netlify 的基于 Web 的仪表板完成,但我们还可以使用 Netlify 的另一个新功能,即基于文件的配置。
Netlify 允许您在项目的根目录中指定一个 netlify.toml 文件。几乎所有的设置都可以在这里设置(有关完整详情,请参阅mng.bz/XWM6上的文档),但在这个案例中,我们将只指定 Netlify 函数的根文件夹。为了开始我们的第一次测试,创建一个新的空目录并添加一个空的 netlify.toml 文件(或使用 GitHub 仓库中的 chapter8/test1 文件夹)。我们将假设您创建的文件夹命名为 test1。
列表 8.1 配置 netlify.toml(chapter8/test1/netlify.toml)
[build]
functions = "netlify/functions"
配置文件指定了构建组中的函数设置,并设置了 Netlify 在准备加载函数时应查找的位置。同样,这个值现在是 Netlify 的默认值,但在 CLI 中不被识别。
在这一点上,如果您愿意,可以创建函数文件夹,但 CLI 如果需要也会创建它。如果您决定自己创建,您应该得到一个类似于图 8.2 中所示的架构。

图 8.2 函数测试项目的文件夹结构。请注意,netlify.toml 位于 test1 的根目录;在截图上可能不太清晰。
现在我们终于可以测试 CLI 的脚手架功能了。首先,从 test1 文件夹中运行 netlify functions:create。这将打开一个提示,要求您选择一个示例(图 8.3)。

图 8.3 浏览函数示例选择
第一个和默认选项,hello-world,是最简单且最适合开始的,所以先选择它。接下来,您将被提示为您的函数命名;由于我们正在测试,请使用默认的 hello-world。按 Enter 键,CLI 将报告新搭建的函数(图 8.4)。

图 8.4 CLI 已完成函数的脚手架搭建。
我们将在稍后讨论 Netlify 函数的一般形式,但到目前为止,请了解这个函数会在查询字符串中查找一个名称值,如果不存在,则使用默认值 'World'。然后它返回一个包含一个值 message 的 JSON 对象,该对象简单地对该名称值说 hello(再次强调,这可以是查询字符串中提供的,或者默认值)。
列表 8.2 hello-world 函数
const handler = async (event) => {
try {
const subject = event.queryStringParameters.name || 'World'
return {
statusCode: 200,
body: JSON.stringify({ message: `Hello ${subject}` }),
// // more keys you can return:
// headers: { "headerName": "headerValue", ... },
// isBase64Encoded: true,
}
} catch (error) {
return { statusCode: 500, body: error.toString() }
}
}
module.exports = { handler }
那么,我们如何测试这个?CLI 的另一个有用功能是 dev 命令。这个命令启动一个本地 Web 服务器,并允许你在本地测试你的 Jamstack 网站。在你的终端中,确保你可以运行 netlify dev,你应该会看到如图 8.5 所示的输出。请注意,你仍然应该在之前的目录中。

图 8.5 使用 Netlify CLI 运行本地开发服务器
除了运行本地 Web 服务器外,CLI 还可能在你的浏览器中打开一个标签页。目前网站还没有任何 HTML 文件,所以你可能收到“未找到”的消息,但我们可以暂时忽略它。要执行 Netlify 函数,你可以在/.netlify/functions/nameOfFunction 处找到它们。鉴于图 8.5 表明我们的网站在本地主机 8888 端口上运行,我们的测试函数的完整 URL 将是:http://localhost:8888/.netlify/functions/hello-world。注意,结尾没有“.js”。URL 使用函数名而不是扩展名。如果你运行这个,你应该得到以下输出:
{"message":"Hello World"}
如果你记得,我们说过该函数通过 URL 传递一个名称参数。你可以通过在 URL http://localhost:8888/.netlify/functions/hello-world?name=Ray(或你自己的名字)中添加?name=Ray 来测试这一点。输出应该会更新:
{"message":"Hello Ray"}
让我们构建一些使用这个函数的东西。我们的网站现在只包含函数(和配置文件),所以让我们添加一个 index.html 文件,包含一个简单的 JavaScript 示例。
最佳实践规定我们应该(通常)将我们的 HTML 和 JavaScript 分开;对于这个简单的演示,一个文件就足够了。我们的 HTML 由一个输入字段、一个按钮和一个空的 div 元素组成。输入字段将是用户输入他们的名字的地方。按钮将触发对函数的请求。最后,空的 div 将显示结果。
JavaScript 设置了一个监听器,当文档加载时,当它准备好时,监听按钮的点击事件。这个函数使用 fetch API 调用我们的 Netlify 函数,其名称相当糟糕,名为 testApi。如果你记得,结果是包含键 message 的 JSON 对象。我们可以将这个调用的结果写入我们的 DOM。
列表 8.3 使用 HTML 和 JavaScript 代码中的我们的函数
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<input type="text" id="name" placeholder="Enter your name..."> ❶
<button id="testBtn">Test Function</button>
<div id="result"></div>
<script>
document.addEventListener('DOMContentLoaded', init, false);
let textField, resultDiv;
function init() {
textField = document.querySelector('#name');
resultDiv = document.querySelector('#result');
document.querySelector('#testBtn').addEventListener('click',
testApi, false); ❷
}
function testApi() { ❸
let name = textField.value.trim();
if(!name) return;
fetch(`/.netlify/functions/hello-world?name=${name}`)
.then(res => res.json())
.then(res => {
resultDiv.innerHTML = `Function returned: ${res.message}`;
});
}
</script>
</body>
</html>
❶ 一个人可以输入他们的名字的字段
❷ 我们在哪里监听按钮的点击
❸ 当按钮被点击时执行的代码。图 8.6 展示了这个简单的例子。

图 8.6 从简单的 JavaScript 测试我们的 Netlify 函数
你现在拥有了一个简单的静态网站,它使用了无服务器函数!让我们再次看看这个无服务器函数。在顶部,我们使用箭头函数风格定义了该函数(你可以在mng.bz/Bx7r了解更多关于这种风格的信息)。如果你对这种定义函数的风格不熟悉,你可以这样重写:
const handler = async function (event) {
Netlify 不要求使用箭头函数,但 CLI 默认使用该格式。事件对象是发送给每个 Netlify 函数的两个参数之一。Netlify 文档将其定义为类似于 Amazon AWS API Gateway 事件,但如果你之前从未使用过,请了解它将包含以下值:
-
Path—函数本身的路径
-
httpMethod—调用函数时使用的 HTTP 方法,在关心函数是否通过表单提交调用时很有用
-
headers—本次执行的所有请求头
-
queryStringParameters—如所示,通过查询字符串传递的任何值
-
body—任何请求负载的 JSON 字符串
-
isBase64Encoded—一个 true/false 标志,指定 body 是否以 base64 编码
传递给 Netlify 函数的第二个参数是一个上下文对象。该对象包含有关函数上下文本身的信息,例如,与后台的 AWS Lambda 函数相关的事项(Netlify 为你隐藏了所有这些)。上下文对象不是你需要使用的东西(因此甚至没有在函数中显示),所以你很少在函数中实际使用它。它真正被用到的地方是与 Netlify 的用户管理系统 Identity 相关。这里我们不涉及该内容,但你可以在文档中了解更多信息(docs.netlify.com/visitor-access/identity/)。
函数的最后一部分是你返回数据的地方。如列表 8.4 所示,返回的对象包含以下部分:
-
statusCode—这应该是一个有效的 HTTP 状态码,表示函数的结果状态。200 表示良好状态,而 500(及其他)可以表示错误和其他状态。通常,你会返回 200。
-
Body—表示你的数据结果的 JSON 字符串。调用者将解析此 JSON 为有效数据。你不需要返回 JSON 字符串,但对于作为 API 的服务器端函数来说,这是相当典型的。
-
headers 和 multiValueHeaders—这些允许你返回与数据一起发送的头部。headers 值用于简单的键/值头部(某个头部有某个值),而 multiValueHeaders 用于头部有多个值的情况。一些 API 将使用头部来指定内容类型或与许可和其他状态相关的其他数据。
-
isBase64Encoded—另一个 true/false 标志,这次是针对结果,指定结果是否以 base64 编码。
大多数开发者只需关注 statusCode 和 body,这正是 CLI 在默认的函数模板中使用的。
列表 8.4 hello-world 函数
const handler = async (event) => { ❶
try {
const subject = event.queryStringParameters.name || 'World' ❷
return { ❸
statusCode: 200,
body: JSON.stringify({ message: `Hello ${subject}` }),
// // more keys you can return:
// headers: { "headerName": "headerValue", ... },
// isBase64Encoded: true,
}
} catch (error) {
return { statusCode: 500, body: error.toString() }
}
}
module.exports = { handler }
❶ 这是定义事件对象的函数声明。
❷ 事件中可用的值之一是 queryStringParameters。
❸ 无服务器函数应返回状态和 body。
如果你愿意,你可以修改你的无服务器函数,将其整个事件对象输出到控制台:console.log(event);。如果你切换到你运行 netlify 命令的终端,你可以看到输出,但请注意,它可能相当大,尤其是在请求头部分,如图 8.7 所示。

图 8.7 服务器端函数的调试输出
8.2.1 将无服务器计算添加到 Camden Grounds
在第二章中,你学习了 Eleventy 并构建了一个名为 Camden Grounds 的简单咖啡店。让我们看看我们如何使用 Netlify 函数来增强我们的 Jamstack 网站。我们将修改网站,使得每个产品页面现在如果产品可用将显示一个动态消息。我们将使用硬编码的逻辑(除了茶之外的所有产品都可用)和客户端 JavaScript 来显示结果。一个示例如图 8.8 所示。

图 8.8 双倍浓缩咖啡产品可用。
在你的第八章文件夹中创建网站的新副本(或者确保你已经从 GitHub 仓库下载了最终代码)。添加 netlify.toml 文件,指定函数的目录。由于 CLI 可以创建此文件夹,因此你不需要手动创建它。
再次使用 Netlify CLI 来构建应用程序,但这次我们将通过指定函数名称作为参数来节省一个步骤:netlify functions:create product-availability。这应该在您创建的新文件夹中运行,该文件夹是 Camden Grounds 的副本。您仍然会被提示选择要创建的函数类型,并再次接受默认值(hello-world)。结果将是一个名为 product-availability 的文件夹,以及其中名为 product-availability.js 的文件,如下所示。
列表 8.5 产品可用性函数
const handler = async (event) => {
try {
const product = event.queryStringParameters.product; ❶
let available = true;
if(product.toLowerCase() == 'tea') available = false; ❷
return {
statusCode: 200,
body: JSON.stringify({ available }), ❸
// // more keys you can return:
// headers: { "headerName": "headerValue", ... },
// isBase64Encoded: true,
}
} catch (error) {
return { statusCode: 500, body: error.toString() }
}
}
module.exports = { handler }
❶ 在查询字符串中检查产品值
❷ 硬编码的逻辑表示茶不可用
❸ 返回可用的值
如前所述,我们检查查询字符串中的值,这次是产品。如果正在检查的产品是茶,我们返回 false。注意结果体使用了一种简写符号,用来替换使用相同键和值的代码,因此,例如,我们不再需要写{name: name},现在我们可以写{name}来指定相同的内容。基本上,一个键名和值由 name 变量表示。Netlify 不要求这种语法,但如果你想要使用它,它是存在的。
要使用此功能,我们将修改用于我们网站上每个产品的 products.liquid 模板文件(列表 8.6)。我们的第一个更改是在产品模板中添加一个空的段落标签。然后稍后由 JavaScript 代码进行编辑。此模板为每个产品执行一次,你实际上可以在代码本身中看到一些 Liquid。虽然 JavaScript 代码在源代码中是动态的,但在产品部署时,它是一个硬编码的产品名称。
JavaScript 向 serverless 函数发送请求,获取结果,然后使用可用性更新 DOM。
列表 8.6 新产品模板 (/chapter8/camdengrounds/products.liquid)
---
layout: main
pagination:
data: products
size: 1
alias: product
permalink: "products/{{ product.name | slug }}/index.html"
---
<div class="row my-4">
<div class="col">
<div class="card mt-4">
<img class="card-img-top img-fluid"
src="http://placehold.it/900x400" alt="">
<div class="card-body">
<h3 class="card-title">{{ product.name }}</h3>
<h4>${{product.price}}</h4>
<p class="card-text">
{{ product.description }}
</p>
<p id="availability"></p> ❶
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', init, false);
let availabilityP;
async function init() {
availabilityP = document.querySelector('#availability');
let api = '/.netlify/functions/product-availability?';
//add specific product
api += 'product={{product.name}}'; ❷
let resp = await fetch(api);
let data = await resp.json();
let result = 'This product is available!';
if(!data.available)
result = 'Sorry, this product is <strong>not</strong> available.';
availabilityP.innerHTML = result; ❸
}
</script>
❶ 将通过 JavaScript 更新的新空段落
❷ 网站部署后将是静态的 JavaScript 代码
❸ 实际更新发生的地方
您已经看到了一个产品可用的示例,但如果您访问茶页(如果您还没有,别忘了用 netlify dev 启动您的站点),您现在会看到它不可用(图 8.9)。

图 8.9 没有茶可以喝!
这引发了一个有趣的情况。我们的 Camden Grounds 网站现在既有静态产品数据也有动态产品数据。静态部分位于 _data 文件夹中,是硬编码的 JSON。动态部分是产品的可用性。在生产环境中,产品列表将是静态的,但它们的可用性将在每次访问时进行检查。这对于 Camden Grounds 来说是合理的,因为他们很少更改他们销售的产品,但有时会缺货。这并不适合所有 Jamstack 网站,但显示了 Jamstack 的一般灵活性。
8.2.2 更多关于 Netlify 函数的内容
Netlify 函数既易于使用,在实现上又功能强大。您可以浏览示例(functions.netlify.com/examples/)以获得更多灵感,并在他们的游乐场(functions.netlify.com/playground/)中尝试一些实时示例。
最近,Netlify 宣布支持“后台函数”功能(docs.netlify.com/functions/background-functions/)。此功能目前处于测试阶段,允许您运行需要更长时间执行时间的函数。当被调用时,它们会立即返回 202 状态,然后开始处理。这些函数不会用于需要向客户端 JavaScript 返回结果的函数,但如您所猜,对于需要在后台发生的操作可能很有用。再次提醒,此功能目前处于测试阶段,因此请谨慎使用。
8.3 使用 Vercel 构建 serverless 函数
与 Netlify 一样,Vercel 支持轻松部署和使用与您的 Jamstack 网站一起使用的 serverless 函数。您可以使用 Node.js、Go、Python 和 Ruby 编写函数。对于想使用 Node.js 的人来说,您可以使用 JavaScript 或 TypeScript,如果您愿意的话。(TypeScript 也可以与 Netlify 函数一起使用,只是目前不是官方支持。)与 Netlify 一样,Vercel 为其函数提供免费层;详细信息可以在其定价页面上找到(vercel.com/pricing)。
Vercel 的无服务器支持在文档中有记录(vercel.com/docs/serverless-functions/introduction),但在撰写本章时,该文档的一些部分有些误导。让我们从 Vercel 无服务器函数的基本介绍开始。
要创建一个 Vercel 无服务器函数,您首先需要在您的项目中添加一个 api 文件夹。(对于使用 Next.js 的开发者,您将使用 pages/api 代替。)在那个文件夹中,然后您可以创建您的无服务器函数。文档(再次,在出版时)暗示函数的名称(不包括扩展名)并不重要。这并不正确。您的文件名将影响您用于调用 API 的 URL。Vercel 将在路径/api/filenameMinusExtension 上部署您的无服务器函数。例如,如果您创建了一个名为 func1.js 的文件,并且您的域名是 raymondcamden.com,您的无服务器函数将可在 https://raymondcamden.com/api/func1 处访问。文档演示了一个不包括文件名减去扩展名的路径,但这仅当您将文件命名为 index.js 时才会有效。在我看来,除非您确信您将在网站上只有一个无服务器函数,否则使用此文件名没有意义。即使您现在只计划一个,将来您可能还需要更多。
忽略这种混淆,Vercel 无服务器函数看起来是什么样子?这里是从其自己的文档中的示例:
module.exports = (req, res) => {
res.json({
body: req.body,
query: req.query,
cookies: req.cookies,
})
}
再次强调,这里使用的是箭头函数,但这不是必需的。您可以将其重写如下:
module.exports = function(req, res) {
res.json({
body: req.body,
query: req.query,
cookies: req.cookies,
})
}
使用您最舒适的形式。该函数接收两个参数,req 和 res,它们映射到 Node.js 请求和响应对象,并提供了 Vercel 提供的额外“辅助工具”。例如,req.query 映射到请求的查询字符串。这些辅助工具在文档中有记录(mng.bz/doOv),并且目前仅适用于使用 Node.js 为其无服务器函数的开发者。让我们看看它是如何工作的。
8.3.1 您的第一个 Vercel 无服务器函数
在第六章中,您通过安装其 CLI 工具来测试了 Vercel 服务。如果您还没有这样做,请参阅上一章中的说明并设置它。您需要安装 CLI 并确保运行登录命令,这样您才能正确地继续前进。
Vercel 的零配置功能意味着使用其大多数功能相当简单,包括其无服务器支持。在新的文件夹中(或使用 GitHub 仓库中的/chapter8/test2 文件夹),创建一个 API 子目录和一个新文件,func1.js。
列表 8.7 Vercel 无服务器函数 (/chapter8/test2/api/func1.js)
module.exports = (req, res) => {
res.json({
body: req.body,
query: req.query,
cookies: req.cookies,
})
}
此函数与 Vercel 文档中的代码相同。当请求时,它会回显请求体、查询字符串和发送的任何 cookie。为了测试,请在终端中运行 vercel dev,确保你位于你创建的文件夹中,而不是 api 文件夹,而是其上级目录。vercel dev 命令与 Netlify 版本类似:它允许你在本地运行你的站点并测试你的站点。第一次运行该命令时,你将需要回答有关站点的问题,你可以接受所有默认设置(图 8.10)。

图 8.10 第一次运行 vercel dev
后续运行 vercel dev 将跳过所有这些问题。为了测试你的函数,打开你的浏览器到 http://localhost:3000/api/func1。URL 末尾的“func1”是我们文件名减去扩展名。结果将是查询字符串的内容(在我们的第一次测试中没有),你的浏览器发送的任何 cookie,以及没有人,因为在请求中没有发送。你的 cookie 可能会有所不同,但查询值的输出应该看起来像一个空对象:
"query":{}
如果你将浏览器中的 URL 更改为包含查询字符串,例如,http://localhost:3000/api/func1?msg=hello&name=world,你会看到查询值发生变化:
"query":{"msg":"hello","name":"world,"}
你看到的 JSON 响应来自使用 res.json 的服务器端函数。传递给此的参数被转换为 JSON 并返回在结果对象中。你不需要返回 JSON,但大多数无服务器函数都这样做。
现在我们通过复制上一节中做的事情来测试 Vercel 的无服务器函数。在本章早期,你在一个简单的前端应用程序中使用了 Netlify 函数的脚手架,传递了一个名称值。列表 8.4 演示了一个 Netlify 函数,它在查询字符串中查找名称,默认值为 World。然后它返回一个包含键 message 和值为"Hello Name"的 JSON 对象,其中 Name 是查询字符串中的值或默认值。新函数检查 req.query 对象中的名称值,并在不存在时将其默认为"World"。然后返回一个包含消息键的字符串,其中包含名称。正如你所看到的,至少在这个例子中,它比 Netlify 版本要瘦得多。
列表 8.8 使用 Vercel 的 Hello 函数 (/chapter8/test2/api/hello.js)
module.exports = (req, res) => {
let name = req.query.name || "World"; ❶
res.json({
message:`Hello ${name}`
})
}
❶ 检查查询字符串中的名称值,如果不存在则默认它。
如果你将此文件命名为 hello.js,你可以在 http://localhost:3000/api/hello?name=Ray 处访问它。如果你停止了 Vercel 开发服务器,请记住使用 vercel dev 命令再次运行它。点击 URL 后,你的输出应该是:
{
"message": "Hello Ray"
}
现在我们需要构建简单的前端。这个版本将与列表 8.3 相同,只有一个微小的区别:URL。你可以在 GitHub 仓库中的文件/chapter8/test2/index.html 中找到这个新版本。
列表 8.9 调用我们的 Vercel 无服务器函数 (/chapter8/test2/index.html)
function testApi() {
let name = textField.value.trim();
if(!name) return;
fetch(`/api/hello?name=${name}`) ❶
.then(res => res.json())
.then(res => {
resultDiv.innerHTML = `Function returned: ${res.message}`;
});
}
❶ 修改后的 URL
我们在这里唯一改变的是 fetch 命令中使用的 URL。Netlify 和 Vercel 函数返回了相同“形状”的结果,这使得这次更新更容易。现在让我们看看 Vercel 无服务器支持的一个有趣特性。
8.3.2 支持动态路径的 Vercel 函数
在前面的示例中,您看到了一个使用查询字符串作为输入的简单 Vercel 无服务器函数。虽然这可行,但您可以使用不同的 URL 语法,允许动态路径,例如,/api/somefunction/somevalue 和 /api/somefunction/ anothervalue。在这些示例中,我们想要运行一个函数,somefunction,而最后的值(somevalue 和 anothervalue)是提供给函数的输入。这使得 URL 语法稍微简单一些,可能比查询字符串更受欢迎。
为了支持这一点,您必须做两件事。首先,创建一个子目录来存储您的函数。这将最终成为 URL 路径的一部分。根据示例,我们的目录是 /api/somefunction。接下来,您为您的无服务器函数创建一个文件。文件名必须用方括号括起来,文件名将决定您如何在代码中访问它。例如,如果我用 [value].js,那么我的无服务器代码将把路径 value 作为 req.query.value 访问。让我们通过创建一个新版本的先前无服务器函数来测试这一点,这个函数允许我们使用路径而不是查询字符串。在 api 文件夹下,创建一个新的文件夹,greeter。名称必须包括方括号,然后将被我们的代码用来获取值。创建一个名为 [name].js 的文件。这个文件的内容与列表 8.8 相同,可以在 GitHub 仓库的 /chapter8/test2/api/greeter/[name].js 找到。一旦保存,您就可以在 http://localhost:3000/api/greeter/Ray 访问您的函数。输出将与上一个示例相同:
{
"message": "Hello Ray"
}
如果将 Ray 改为 Lindy(http://localhost:3000/api/greeter/Lindy),您可能可以猜到您会得到什么:
{
"message": "Hello Lindy"
}
如果您想测试这个版本,GitHub 仓库中有 index2.html,它只是简单地重复了在切换 fetch 调用请求位置时对 index.html 的更改。
8.3.3 再次将无服务器函数添加到 Camden Grounds
让我们通过利用 Camden Grounds 来创建另一个使用无服务器函数的 Jamstack 的真实世界示例。这次我们将向我们的网站添加一个 API。这个 API 将返回所有可用的产品,格式为 JSON,或者根据查询字符串值返回一个过滤后的列表。
首先,从你的第二章文件夹中复制 Camden Grounds 网站,或者使用 GitHub 中的 chapter8/camdengrounds2 文件夹。如果你不使用 GitHub 中的第 8 版本,我们必须对我们的早期版本进行一些快速更改。首先,删除现有的 package.json 文件。这个文件来自我们使用的模板源,会妨碍我们的新版本。从头开始创建新的 package.json 文件的最简单方法是使用命令 npm init -f。接下来,我们需要添加 Eleventy 作为依赖项。Vercel 的一个特性是它可以根据你的文件确定你项目的大部分信息。package.json 可以帮助 Vercel 了解你的项目是如何工作的。我们有一个 Eleventy 网站,所以让我们通过运行 npm install @11ty/eleventy --save 来设置 Eleventy 作为依赖项。
到目前为止,你的 package.json 文件应该看起来像这样:
{
"name": "camdengrounds2",
"version": "1.0.0",
"description": "",
"main": ".eleventy.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@11ty/eleventy": "⁰.12.1"
}
}
现在,你可以运行 vercel dev 来启动本地服务器。和之前一样,你会被提示几次,你应该接受默认值。一旦 CLI 完成,它将运行 Eleventy,这次你会在输出中注意到一些不太对劲的地方(图 8.11)。

图 8.11 Eleventy CLI 的错误输出
你所看到的是使用 Vercel 和 Eleventy 的一个有趣的副作用。当你运行 vercel dev 并完成其初始设置时,它创建了一个名为 .vercel 的新文件夹,其中包含一些配置信息。你不必担心这一点。但它还添加了一个 .gitignore 文件,指定了 .vercel。.gitignore 文件是 Git 仓库使用的一种特殊文件,作为标记不应提交到源仓库的文件和文件夹的方式。这个动作随后触发了 Eleventy 中的某些操作。默认情况下,Eleventy 会忽略 npm 使用的 node_modules 文件夹,除非你有 .gitignore 文件。错误信息来自 Eleventy 没有忽略 node_modules 文件夹,并被一个随机文件所困扰。幸运的是,修复很简单:将 node_modules 文件添加到你的 .gitignore 文件中。
列表 8.10 固定的 .gitignore 文件 (/chapter8/camdengrounds2/.gitignore)
.vercel
node_modules
到目前为止,你可以按终端中的 Ctrl 或 CMD-C 来停止 vercel dev 并重新运行它;错误应该会消失。
现在 Camden Grounds 应该已经启动并运行。让我们添加我们的无服务器函数。创建一个 api 文件夹,然后创建一个名为 products.js 的文件。该文件以从 Eleventy _data 文件夹(代码列表中的一些文本被裁剪以使其更短)中的产品数据副本开始。在产品列表之后,函数的主要内容开始。首先,它查找一个名为 filter 的查询字符串值。如果存在,则用于过滤数组。最后,产品作为 JSON 返回。
列表 8.11 产品 API (/chapter8/camdengrounds2/api/products.js)
let products = [
{
"name" : "Coffee",
"price" : 2.99,
"description" : "Lorem ipsum dolor sit amet, consectetur adipisicing
➥ elit. Amet numquam aspernatur!",
"thumbnail" : "http://placehold.it/700x400",
"image" : "http://placehold.it/900x350"
},
{
"name" : "Espresso",
"price" : 3.99,
"description" : "Lorem ipsum dolor sit amet, consectetur adipisicing
➥ elit. Amet numquam aspernatur!",
"thumbnail" : "http://placehold.it/700x400",
"image" : "http://placehold.it/900x350"
},
{
"name" : "Americano",
"price" : 5.99,
"description" : "Lorem ipsum dolor sit amet, consectetur adipisicing
➥ elit. Amet numquam aspernatur!",
"thumbnail" : "http://placehold.it/700x400",
"image" : "http://placehold.it/900x350"
},
{
"name" : "Double Espresso",
"price" : 8.99,
"description" : "Lorem ipsum dolor sit amet, consectetur adipisicing
➥ elit. Amet numquam aspernatur!",
"thumbnail" : "http://placehold.it/700x400",
"image" : "http://placehold.it/900x350"
},
{
"name" : "Tea",
"price" : 1.99,
"description" : "For those who prefer tea.",
"thumbnail" : "http://placehold.it/700x400",
"image" : "http://placehold.it/900x350"
}
]
module.exports = (req, res) => {
let filter = req.query.filter;
if(filter) {
products = products.filter(
p => p.name.toLowerCase().indexOf(filter.toLowerCase()) >= 0
);
}
res.json({
products
})
}
注意,在测试过程中,我发现第一次添加我的无服务器函数时,CLI 没有识别出它已被添加。我停止了 Vercel CLI 并重新运行,它顺利地识别了它。如果你也遇到这种情况,请尝试同样的方法。
函数保存后,你可以在浏览器中通过 http://localhost:3000/api/products 访问它。你会看到所有产品。接下来,尝试过滤 http://localhost:3000/api/products?filter=espresso。这将返回一个更短的列表:
{
"products": [
{
"name": "Espresso",
"price": 3.99,
"description": "Lorem ipsum dolor sit amet, consectetur adipisicing...",
"thumbnail": "http://placehold.it/700x400",
"image": "http://placehold.it/900x350"
},
{
"name": "Double Espresso",
"price": 8.99,
"description": "Lorem ipsum dolor sit amet, consectetur adipisicing...",
"thumbnail": "http://placehold.it/700x400",
"image": "http://placehold.it/900x350"
}
]
}
现在你的 "静态" Jamstack 网站已经有了自己的 API,其他网站可以使用它!
在下一章中,你将了解一些强大且用户友好的 CMS(内容管理系统)产品,这些产品使得 Jamstack 更易于使用,特别是对于那些没有技术背景的人来说。
摘要
-
无服务器计算为你提供了服务器的所有功能,而不需要设置服务器的繁琐工作。它还减轻了你维护服务器的担忧。
-
FaaS 是一个通用术语,描述了使用无服务器计算作为编写执行某些操作并随后通过 URL 公开这些函数的方式。这些函数可以是特定于站点的实用工具,而不是外部服务。例如,一个营业时间不寻常(比如说天气相关)的商店可以使用无服务器函数来处理这种逻辑。它还可以使用另一个无服务器函数来处理检查产品可用性。
-
Netlify 提供无服务器函数的支持,并有一个 CLI 可以用来本地搭建和测试。Netlify 允许你在 Jamstack 网站中构建多个函数,并为它们提供标准 URL 以供访问。
-
Vercel 支持无服务器函数,并且可以通过 CLI 在本地进行测试。与 Netlify 类似,它遵循一个标准,即代码放置的位置以及如何访问代码。
9 添加内容管理系统
本章涵盖
-
两种类型无头 CMS 的优缺点
-
在 Contentful 和 Sanity API 基于的无头 CMS 之间进行选择
-
使用 WordPress 作为无头 CMS
-
使用 Gatsby 的 WordPress 集成构建网站
-
使用网站构建器创建与 CMS 连接的 Jamstack 网站
Jamstack 通常与基于文件的文件内容管理系统相关联,该系统使用手动编辑的 Markdown 和 YAML 文件的组合。这是因为许多静态网站生成器默认使用这种手动内容管理,即使今天也是如此。但现实是,某种形式的无头内容管理系统已成为大多数 Jamstack 网站的事实上的集成。
Jamstack.org 维护了一个无头 CMS 列表(jamstack.org/headless-cms),你可以通过类型和许可证(即它们是开源还是闭源)进行搜索。目前,它列出了 87 个选项。不用说,这有很多选择,远远超出了我们在一章中能涵盖的范围。相反,我们将探讨不同类型的无头 CMS,然后探索一些选项,以给你一个了解它们方法差异的感觉,并帮助你做出明智的选择。
9.1 无头 CMS 的类型
正如我们在第四章中讨论的,有两种广泛类型的无头 CMS:
-
基于 Git 的无头 CMS—这些 CMS 将所有内容都存储在 Git 仓库中的文件中。CMS 是一个编辑层,允许用户通过基于浏览器的所见即所得(WYSIWYG)风格界面编辑包含网站内容的 Markdown、JSON 和 YAML 文件。在许多情况下,这些编辑层与网站的文件一起部署,或者存在于用户的机器上。用户的更改由 CMS 提交回 Git 仓库。Git 处理内容的版本控制。由于更改提交到 Git,部署内容通常作为为网站设置的任何持续部署过程的一部分进行。例如,Netlify 将检测仓库中的更改,并自动构建和部署更新的内容。
-
基于 API 的无头 CMS—这些 CMS 在其系统中存储内容。这类似于传统的 CMS,但系统没有前端;相反,内容只能通过 API 访问。内容编辑通常通过 CMS 提供商网站上的管理仪表板进行。例如,如果一个网站使用了 Contentful,一个流行的基于 API 的无头 CMS,该网站的内容编辑将登录到contentful.com以更改内容。当用户更改 Jamstack 网站的内容时,Jamstack 通常需要通过连接到网站持续部署过程的 web hook 触发重建。用户不需要等待重建完成就可以查看和测试他们的更改。大多数基于 API 的无头 CMS 都有一个实时预览功能,可以在使用 React 或 Vue 等流行前端框架构建的 Jamstack 网站上显示未发布的内容更改。
什么是 web hook?构建 hook?部署 hook?
web hook被定义为通过 HTTP 发送的用户定义的回调。本质上,一个应用程序将调用一个端点,通常传递一些数据,作为对事件的响应。例如,一个无头 CMS 通常会允许你配置一个 web hook,以便在发布内容发生变化时被调用。
构建 hook(Netlify 这样称呼它)或部署 hook(Vercel 这样称呼它)是一个专门的 web hook 端点,可以通过 HTTP POST 请求调用,从而触发在部署提供商上指定网站的重建。你可以结合使用你的无头 CMS 提供商的 web hook 和你的部署提供商的构建 hook,当在 CMS 中对网站的发布内容进行更改时触发网站重建,从而将新内容纳入发布网站。
你应该为你的项目选择哪种类型?这取决于你的具体需求,但以下是一些指导方针。
9.1.1 基于 Git 的无头 CMS 的优缺点
优点:
-
它们 很便宜。由于它们依赖于 Git 存储库来存储和版本控制所有内容,基于 Git 的无头 CMS 通常很便宜,在某些情况下,完全免费和/或开源。
-
它们非常容易设置。集成通常包括确保编辑层理解你文件中的内容形式(即内容模型)。例如,你可能需要告诉它 Markdown 文件的前置部分包含哪些类型的数据或你的 JSON 和 YAML 数据文件中的数据类型,尽管一些系统甚至尝试自动检测这些。
-
他们有一个很短的学习曲线。因为它们是基于文件的,所以对于内容编辑来说通常更容易学习,因为数据模型通常没有它们的基于 API 的替代品那么复杂。
-
它们提供了外部贡献的简单路径。第三方贡献者可以通过 Netlify CMS 等 CMS 中的开放作者功能直接访问编辑内容,或者可以通过 GitHub 或 GitLab 等网站上的 Git 存储库直接进行文件编辑。这对于像文档站点这样的内容来说尤其重要,正如我们在第四章中探讨的那样。
-
它们不是专有的。您的内容存储在您自己的 Git 存储库中的文件中,这意味着您可以轻松地切换到另一个基于 Git 的 CMS 解决方案,而不会对您站点的代码或内容产生直接影响。
缺点:
-
内容重用有限。基于 Git 的头无头 CMS 中的内容通常仅针对 Web,这意味着内容在网站和移动应用等跨平台重用可能很困难。此外,网站本身的内容重用也有限。例如,我可能有一个在网站多个位置使用的营销标题和口号,但这类重用通常无法使用基于 Git 的 CMS 实现。
-
数据模型难以强制执行。由于内容是基于文件的,直接更改 Git 存储库中的文件可能会破坏数据模型的完整性,可能导致构建失败。例如,CMS 可能要求每个博客文章在 front matter 中定义作者,但在存储库的直接文件更改中无法强制执行这一点。如果有人手动更改文件并删除作者字段,网站构建可能会失败。
-
内容关系难以维护。大多数基于 Git 的 CMS 都有一种定义两块内容之间关系的方式。例如,一篇博客文章可能与一个作者页面相关联,该页面包含作者姓名和简介。然而,通常没有强制执行这些关系的方法,这意味着即使分配给它的帖子仍然存在,也可能有人删除作者页面。
9.1.2 基于 API 的无头 CMS 的优缺点
优点:
-
它们 是为内容重用而设计的。这意味着在多个属性之间重用内容,包括多个网站或网站和移动应用,以及同一网站。为了在不同类型的应用程序(例如,移动应用和 Web 应用)之间实现内容重用,基于 API 的无头 CMS 通常将内容与表示(即内容不存储在 HTML 或 Markdown 等仅生成 HTML 的格式中)分离。这使得内容可以在不同类型的应用程序或同一应用程序的不同部分中使用,而无需担心内容的显示方式。
-
它们确保内容完整性。内容模型可以被设计成不仅确保内容类型始终包含某些字段,而且这些字段包含正确的值。例如,我可能想要确保内容项的一个属性是数字,但同时也位于特定的数字范围内。CMS 提供了定义字段验证的工具,确保在用户能够发布项目之前,值是正确的。
-
它们强制执行内容关系。强制执行内容关系使得它们易于维护。例如,它可以确保帖子始终有一个作者,并禁止删除分配给已发布帖子的作者。CMS 被设计成防止删除会损害任何引用完整性的内容。在删除内容之前,必须删除或修改引用。它们还可以允许在富文本或结构化内容中建立关系,这些内容类型允许与 HTML 无关的丰富格式化以及嵌入在 CMS 中定义的可重复使用的内容项。
-
它们可以处理复杂的内容工作流程。基于 Git 的 CMS 系统通常只支持非常基本的工作流程,但基于 API 的系统通常可以处理甚至复杂和定制的审批工作流程。
缺点:
-
它们可能很昂贵。许多这些解决方案是为公司定价的,而不是为个人开发者定价。虽然它们可能有慷慨的免费层,但定价可能很快就会上升。
-
它们通常有陡峭的学习曲线。由于这些是为满足甚至大型企业的需求而设计的,它们通常充满了可能使它们对普通内容编辑员来说有点复杂的特性。
-
它们是专有的。存在内置的供应商锁定,很大程度上是因为从一个系统迁移到另一个系统可能是一个复杂且昂贵的项目。这不仅包括将内容从专有系统迁移到另一个系统,包括数据模型、用户、验证规则和工作流程。还可能涉及对网站代码的重大修改,提取 SDK 和/或完全重写获取内容的 API 调用。
9.2 探索流行的无头 CMS
由于我们在第四章讨论了流行的基于 Git 的无头 CMS,让我们探索一些广泛使用的基于 API 的解决方案。我们还将查看一些选项,以在本书中讨论的一些静态站点生成器(SSG)中开始实施它们,尽管如何在每个特定的 SSG 中实现每个无头 CMS 的完整讨论超出了本章的范围。
比较无头 CMS 选项
Jamstack 网站上的无头 CMS 列表(jamstack.org/headless-cms)是一个非常全面的头无 CMS 选项列表,但它可能不会提供足够的细节来帮助您做出选择。幸运的是,Bejamas(一家 Jamstack 咨询公司)的同事们启动了一个名为 Discover Hub(bejamas.io/discovery)的项目。它提供了对 24 种不同头无选项的极其详细的评估(以及其他类别工具和服务,包括 SSG 和托管)。在做出选择时,绝对要查看它。
使用任何无头 CMS 的第一步是构建内容模型。让我们看看您如何在两种具有截然不同方法的头无 CMS 选项中构建内容模型,以便给您一个关于您可用的不同选项的感觉。
9.2.1 Contentful
在我看来,Contentful(www.contentful.com/)最强大的卖点之一是定义内容模型的简便性。其基于 Web 的视觉界面足够易于使用,以至于非技术用户也可以构建内容模型。
一旦您登录到 Contentful,您需要创建您的第一个“空间”。这是 Contentful 分组项目的方式。在 Jamstack 项目的案例中,一个空间可能是一个站点的后端。然而,从技术上讲,空间可以作为任何逻辑分组站点和应用的后端。请注意,Contentful 目前在其免费计划下仅提供一种有限的“空间”。
创建空间后,您需要添加内容类型。在“内容模型”标题下,点击蓝色的“添加内容类型”按钮。这将打开一个窗口,让您为类型命名、提供 API 标识符和描述。名称可以是您喜欢的任何内容。API 标识符实际上是您内容类型的缩写,将自动生成,尽管您可以自定义它。
创建类型后,您需要向其中添加字段。每个字段都必须分配九种不同类型之一。在 Contentful 系统中,每种类型都有可应用于它们的有限验证集合(如图 9.1 所示)。

图 9.1 Contentful 提供的九种不同类型的字段
-
富文本——重要的是要注意,这并不是 HTML,而是一种存储在 JSON 格式中的富内容,它允许将其转换为所需的任何格式。这允许富文本包含嵌入的资产和富文本格式内的条目。对于内容创建者来说,这看起来和感觉就像是一个标准的 WYSIWYG 编辑体验。除了简单地使其成为必需的或限制特定字符数之外,还有许多验证选项,所有这些都与嵌入的链接、条目或资产相关。
-
文本—Contentful 中有两种标准文本字段:短文本,限制为 255 个字符,和长达 50,000 个字符的长文本。你可以对短文本字段进行排序并搜索精确匹配;而你不能对长文本字段进行排序,但你可以进行全文搜索。长文本主要用于项的长篇内容,如博客文章正文或作者简介(两者都可以是富文本)。如果你正在存储 Markdown 内容,你将使用长文本字段而不是富文本字段。验证选项包括确保文本字段与特定模式匹配(例如,电子邮件或 URL)或禁止特定模式(例如,防止使用脏话)。
-
数字—这可以是整数或小数。验证可以要求值是唯一的(没有两个条目可以具有相同的数字),必须在特定范围内,或者只能接受特定值。
-
日期—日期没有很多选项或验证,尽管你可以将其限制在特定范围内。
-
位置—这将在 Contentful 中以纬度和经度的形式存储,尽管内容编辑可以通过地址或地图上的位置选择位置。提供的唯一验证是使其成为必填项。
-
媒体—这是你可以上传的任何类型的媒体,例如图片或 PDF 文件。你可以选择允许上传单个文件或多个文件。验证允许你指定文件大小限制,仅接受特定类型的文件,或限制图像上传到特定尺寸。
-
布尔值—这是一个基本的“是/否”响应。可以设置的唯一验证是使其成为必填项。
-
JSON 对象—这允许内容编辑通过 JSON 编辑器直接编写 JSON 对象。JSON 验证内置在编辑器中。你可以通过验证限制 JSON 对象具有的属性数量。
-
参考—这是 Contentful 中与其他内容项之间的关系。例如,一篇文章可能引用其作者或作者们。你可以有一个对单个项(一对一关系)或多个项(一对多关系)的引用。验证允许你将选项限制为特定类型(例如,作者关系将限制你选择作者类型的内容项)。
一旦你构建了内容模型,你和你/你的内容编辑就可以通过内容编辑器开始向 Contentful 添加内容项,如图 9.2 所示。

图 9.2 在 Contentful 管理 UI 中编辑“博客文章”内容类型的内容
Contentful 提供了七个不同的 API(mng.bz/raAe)用于检索和管理内容和资产。对于从 Jamstack 网站的前端拉取数据的目的,你将主要对以下三个 API 感兴趣:
-
内容交付 API 是一个 REST API,它只允许您检索已发布的内容。这是您在为已发布网站生成静态页面时将用于拉取内容的 API。(有关更多详细信息,请参阅
mng.bz/VlmW。) -
内容预览 API 是一个 REST API,允许您拉取内容的不发布更改。这对于允许内容编辑通过预览版本查看其内容更改在实时网站上的外观非常重要,例如,在另一个 URL 上可用的网站预览版本或主网站上登录用户可用的版本。(有关更多详细信息,请参阅
mng.bz/xvAg。) -
GraphQL 内容 API 是一个 GraphQL API,允许您检索已发布和未发布的内容。如果您对 GraphQL 工作感到舒适,这可以替代 REST API。(有关更多详细信息,请参阅
mng.bz/ Ax7g。)
在大多数情况下,您可能不会直接与 API 交互,而是会使用 Contentful 提供的其中一个 SDK (mng.bz/Zzv5)。例如,您可以将 JavaScript SDK (mng.bz/mxAM) 与像 Eleventy 或 Next.js 这样的 SSG 集成。Gatsby 已经有一个名为 gatsby-source-contentful (mng.bz/5KRD) 的插件,它已经设计好可以与 GraphQL 内容 API 一起工作,以启用已发布和预览内容。Next.js 有一个您可以用作起点的示例 (mng.bz/6ZR6)。
与像 Hugo 或 Jekyll 这样的其他传统静态站点生成器(SSG)集成,需要采取不同的方法,因为它们都没有直接从 API 获取内容或生成页面的方法。然而,有一些工具可以提供将您的 Contentful 内容导入这些工具的方法。Contentful 提供了一个名为 jekyll-contentful-data-import (mng.bz/oaAv) 的 gem,可以用来将内容导入 Jekyll。对于 Hugo,有一个名为 contentful-hugo 的 npm 包 (mng.bz/nYA4),它将拉取您所有的 Contentful 内容并将其转换为 Markdown。
9.2.2 Sanity
在 Sanity 中构建内容模型的经验与 Contentful 完全不同。Contentful 主要依赖于其基于 Web 的 GUI 来构建内容模型,而 Sanity 的内容模型完全由 JavaScript 代码定义并通过 Sanity CLI 管理。(有关 Contentful 和 Sanity 内容类型的比较,请参阅表 9.1。)
表 9.1 比较 Contentful 和 Sanity 中可用的不同内容类型
| 内容类型描述 | Contentful 类型 | Sanity 类型(s) |
|---|---|---|
| 包含 HTML 格式化和嵌入内容的富文本 | 富文本 | 块 |
| 布尔值或真/假 | Boolean | Boolean |
| 原始文本、Markdown、URL、短名、JSON | 文本 JSON | 字符串文本 JSON 短名 URL |
| 数字,包括整数和小数 | 数字 | 数字 |
| 日期或带时间的日期 | 日期 | 日期日期时间 |
| 由纬度和经度确定的地点 | 位置 | 地理点 |
| 媒体,包括图片、视频和文件 | 媒体 | 文件图片 |
| CMS 中其他内容对象的引用 | 引用 | 引用数组 |
Sanity 认为在代码中定义内容模型可以让你更好地控制模型,同时还能让你添加自己的自定义验证。然而,这也意味着构建和维护内容模型需要开发资源,而使用可视化编辑器(例如 Contentful)的替代方案可能不需要。此外,Sanity 为每个项目部署了一个新的 Sanity Studio 实例,这是一个开源的、基于网络的 内容管理界面。由于你有权访问项目的管理站代码,你可以自由地按照自己的选择修改界面,但初始界面比 Contentful 界面要简单。
与 Contentful 的 9 种类型相比,Sanity 有 17 种类型,但它们在很大程度上与 Contentful 的类型相似:
|
- 数组
|
- 块
|
- 布尔值
|
|
- 日期
|
- 日期时间
|
- 文档
|
|
- 文件
|
- 地理点
|
- 图片
|
|
- 数字
|
- 对象
|
- 引用
|
|
- 短名
|
- 字符串
|
- 段落
|
|
- 文本
|
- URL
在许多情况下,当 Contentful 有子类型时,Sanity 有多种类型。例如,Contentful 有一个单一的引用类型,它指代内容项之间的一对一和一对多关系。在 Sanity 中,也有一个引用类型,但它只指代一对一关系。一对多关系指代数组类型。当 Contentful 有一个单一的媒体内容类型指代所有类型的媒体上传时,Sanity 有文件和图片类型用于不同类型的媒体上传。
Sanity 还提供了一种类似 Contentful 中丰富文本的结构化文本格式,称为块。Sanity 的块类型遵循 Portable Text 规范 (github.com/portabletext/portabletext),这意味着它由一个表示块内内容类型的子类型数组组成。这些可以是标准类型,如标题、列表或链接,也可以是自定义类型。例如,你的内容模型可能有一个员工类型,而我们的团队页面上的块元素允许你添加员工类型的条目。
Sanity 中的每个内容类型都有如是否只读或从管理 UI 中隐藏等属性。所有类型都共享相同的核心属性,尽管一些类型,如块和图像,有可以设置的附加属性。每个内容类型还有一个预定义的验证集,例如字段是否必需,或者在文本的情况下,是否有最小、最大或特定长度的要求。这些验证因类型而异。Sanity 还允许您为任何以 JavaScript 函数编写的类型指定自定义验证(mng.bz/voDr)。这意味着您可以创建复杂的验证,这些验证超出了内置验证类型的范围。
9.2.3 使用 Sanity 定义内容模型
由于它完全基于代码,并且编辑器 UI 可以在本地运行,因此使用 Sanity 构建内容模型的经验可以非常独特。让我们通过如何根据 Sanity 提供的样本项目创建内容模型,并探索样本架构代码来更好地了解 Sanity 的工作方式。
要创建 Sanity 项目,您需要一个 Sanity 账户,然后您将使用 Sanity CLI,它可以通过 npm 安装。然后,您将初始化一个新的项目:
npm install -g @sanity/cli
sanity init
如果您尚未通过 CLI 登录,CLI 将打开一个浏览器窗口以进行身份验证。之后,您将被要求选择一个现有项目以启动,如果您已经有了,或者创建一个新项目。您将为您的项目命名,然后选择是否要使用默认的数据集配置(默认情况下,生产数据集可以在未经身份验证的情况下查询)。接下来,您将选择一个输出路径,默认情况下将设置为当前项目文件夹。这是放置配置您的 Sanity 架构和 Sanity Studio 所需的所有本地文件的地方。最后,您可以选择一个空白架构或从现有的示例开始:
> Movie project (schema + sample data)
E-commerce (schema + sample data)
Blog (schema)
Clean project with no predefined schemas
电影项目架构是尝试 Sanity 的一个很好的入门示例,因为它既有简单的架构,又有填充它的数据。这为您提供了一个尝试工作室的机会,而无需手动用数据填充它。当被问及是否导入样本数据时,您应该回答“是”。一旦导入完成,从命令行运行 sanity start 以启动 Sanity Studio。默认情况下,它将在 http://localhost:3333 上本地运行。
构成内容模型的全部文件都位于项目中的 schemas 文件夹内。让我们快速探索列表 9.1 中定义在 person.js 文件中的 Person 类型,然后我们将深入了解它是如何工作的。
列表 9.1 定义 Sanity 中人员内容类型的架构文件
import UserIcon from 'part:@sanity/base/user-icon'
export default {
name: 'person',
title: 'Person',
type: 'document',
icon: UserIcon,
fields: [
{
name: 'name',
title: 'Name',
type: 'string',
description: 'Please use "Firstname Lastname" format',
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'name',
maxLength: 100,
},
},
{
name: 'image',
title: 'Image',
type: 'image',
options: {
hotspot: true,
},
},
],
preview: {
select: {title: 'name', media: 'image'},
},
}
名称是对象名称,而标题是将在工作室内的导航中显示的内容。文档类型是 Sanity 中类型的基石。内容类型可以是文档或对象。文档类型是你将在工作室中创建和编辑的类型(即,它们会出现在左侧菜单中),而对象可以用来构成文档的一部分(如在这个示例模式中的 castMember 对象类型,它包含一个具有可以添加到电影中的额外属性的人)。图标是在左侧导航中显示的图标,是创建内容类型的视觉提示(在这种情况下,用户图标代表一个人,如图 9.3 所示)。

图 9.3 默认的 Sanity Studio 编辑体验。Sanity Studio 是开源的,并且可以自定义。
字段是我们类型包含的不同属性。每个属性都分配了 17 种类型中的一种。可以在选项对象中定义的选项取决于类型。例如,slug 类型有一个源选项,它将确定哪个字段将用于自动填充默认的 slug 值(在这种情况下是人的名字)。
预览部分决定了 Sanity 在列出此类对象的 Sanity Studio 导航中会显示哪些字段。Sanity 将自动猜测哪些字段来显示,但你可以提供这些信息来自定义它显示的字段。
随意继续探索模式,了解它是如何组装的以及如何在 Sanity 中配置数据模型的多种选项。到目前为止,我们已经在本地上设置了模式和工作室。虽然 Sanity 的系统知道它,但我们还没有部署它,工作室对其他用户不可用。如果我们想将模式部署到 Sanity 并使工作室可用,我们将在命令行中运行 sanity deploy。
Sanity 提供了许多使用 SSGs(静态站点生成器)的 Jamstack 项目示例,例如 Next.js、Gatsby、Gridsome、Nuxt 和 Eleventy。这些提供了如何使用存储在 Sanity 项目中的内容生成网站和预览的示例。所有这些都可以在www.sanity.io/create找到。
9.3 使用 WordPress
WordPress 在关于 Jamstack 的书中做什么?如果你最近几年看到了许多关于 WordPress 与 Jamstack 的帖子讨论,你可能认为你必须选择其中一个。然而,Jamstack 对您选择的后端没有任何意见,而且,事实证明,WordPress 提供了一个 REST API,使其成为 Jamstack 站点的完美后端。
事实确实如此,典型的 WordPress 开发场景涉及紧密耦合的前端和后端,但 WordPress 还带来了高度精致的内容编辑体验,许多内容创作者已经习惯了,实际上可能已经在使用。而不是强迫他们迁移,有许多选项可以利用来构建与 WordPress 后端绑定的 Jamstack 前端。
虽然您可以将任何 Jamstack 前端与 Wordpress API 集成,但基于 React 的非常流行的 SSG Gatsby 已经使开发者能够轻松地将 Gatsby 连接到 WordPress。让我们看看如何将一个 Gatsby 网站连接到一个 WordPress 实例。如果您还没有设置 WordPress 后端,请不要担心;我们将逐步说明如何轻松地设置一个本地实例。
9.3.1 使用 Local 在本地安装 WordPress
首先,让我们设置一个 WordPress 实例。如果您已经安装了 WordPress,请随意使用它进行此演练并跳过设置(跳转到“设置 Gatsby 插件”)。您需要能够安装插件,这被一些主机限制。
设置 WordPress 本地实例的最简单方法之一是使用一个名为 Local (localwp.com/)的工具。虽然有一个付费的专业版,但您只需免费版即可进行此演练。安装后,打开 Local 并点击按钮添加新网站。您可以命名它任何您想要的名称。我将其命名为“gastby-sample”。然后您可以选择默认的“首选”环境。最后一步是为您的网站管理员选择用户名和密码。
本地环境将设置整个 WordPress 实例,包括 MySQL 数据库,并为您提供网站详情。您可以通过点击“打开网站”来查看您机器上本地运行的网站,或者您可以通过点击“管理”来访问您本地运行的网站的后端。目前,请打开管理界面。
9.3.2 安装 WordPress 的 Gatsby 插件
为了作为 WordPress 后端的前端,Gatsby 需要安装两个插件:
-
WPGraphQL (
github.com/wp-graphql/wp-graphql)——此插件的目的在于将 WordPress 的 REST API 转换为 GraphQL (graphql.org/)。Gatsby 的数据层,它为 Gatsby 提供用于生成网站的内容,完全基于 GraphQL。一旦启用,您的网站将有一个可在 https://[YOUR_SITE_NAME]/ graphql 处访问的 GraphQL API。例如,我的网站在 Local 上本地运行,其 GraphQL API URL 为 http://gatsbysample.local/graphql。 -
WPGatsby (
wordpress.org/plugins/wp-gatsby/)—此插件的主要目的是监控您 WordPress 站点的更改,以保持 Gatsby 前端与 WordPress 后端所做的更改同步。此插件监控诸如帖子或菜单之类的更改,并在您的 Gatsby 站点中调用一个 web 钩子以触发使用新信息的重建。它还使 WordPress 预览能够与 Gatsby 页面一起工作。在我们的教程中,它是可选的,因为它需要与您选择的部署平台(如 Netlify、Vercel 或 Gatsby Cloud)集成。
要安装这些插件,在 Local 中打开您的站点并点击管理按钮。这将打开您的 WordPress 站点管理面板。您需要使用在 Local 的 WordPress 设置过程中创建的凭据登录。登录后,在左侧菜单中点击插件,然后点击添加新按钮。在搜索框中输入插件的名称,WPGraphQL(图 9.4)和 WPGatsby,然后为每个点击安装。

图 9.4 展示了 WPGraphQL 插件将 WordPress 转换为与 Gatsby 一起工作。此卡片将在 WordPress 插件搜索 UI 中显示。
一旦安装了 WPGraphQL,您应该会在左侧菜单中看到添加的 GraphQL 菜单项。打开它以查看如图 9.5 所示的插件设置。

图 9.5 展示了在 WordPress 管理界面中 WPGraphQL 插件设置屏幕,您可以通过它设置端点位置并更改 GraphQL 端点的工作方式。
请注意,在 GraphQL 端点设置下方链接的 GraphQL 端点 URL。这是我们 Gatsby 前端将要连接到的端点。例如,我的端点是 http://gatsbysample.local/graphql。在测试过程中,您还可以向下滚动并勾选启用 GraphQL 调试模式复选框。这将使 GraphQL 查询在开发期间返回调试信息。
默认情况下,WPGraphQL 还为您添加了一个 GraphiQL IDE 查询编辑器到 WordPress 管理界面中,它可通过顶部导航栏上的 GraphiQL IDE 菜单项访问。点击它以打开查询编辑器并测试针对您的 WordPress 数据的 GraphQL 查询。
您可以查询的完整模式将在左侧的探索器中加载。它相当复杂,但探索器允许您点击并创建查询。您可以自由地自行查看更多,但现在让我们用一个类似于我们在简单示例 Gatsby 应用中使用的查询来测试它。
列表 9.2:一个从 WPGraphQL 获取 WordPress 内容的查询
query MyQuery {
posts {
edges {
node {
title
author {
node {
name
}
}
slug
excerpt
content
}
}
}
}
点击查询编辑器上方的播放按钮(即右箭头)来运行查询。
列表 9.3:列表 9.2 中查询返回的结果
{
"data": {
"posts": {
"edges": [
{
"node": {
"title": "Hello world!",
"author": {
"node": {
"name": "brian"
}
},
"slug": "hello-world",
"excerpt": "<p>Welcome to WordPress. This is your first post.
➥ Edit or delete it, then start writing!</p>\n",
"content": "\n<p>Welcome to WordPress. This is your first post.
➥ Edit or delete it, then start writing!</p>\n"
}
}
]
}
},
"extensions": {
"debug": []
}
}
图 9.6 显示了在 GraphiQL IDE 中的外观。

图 9.6 展示了在 WordPress 管理界面中的 GraphiQL 界面内查询由 WPGraphQL 生成的 GraphQL API。
设置 WordPress 所需做的所有事情就是这些。您可以随意用一些帖子填充网站,或者简单地使用 WordPress 提供的默认帖子继续操作。
9.3.3 设置 Gatsby
有许多 Gatsby 启动器是为与 WordPress 一起使用而设计的,并将加速您的项目。您可以通过访问 Gatsby 启动器页面 (www.gatsbyjs.com/starters) 并按“CMS: WordPress”进行筛选来找到它们。然而,为了更好地理解这一切是如何工作的,我们将编写我们自己的简单 Gatsby 登录页面,该页面将列出我们本地 WordPress 网站内的帖子。
在我们能够将 Gatsby 连接到 WordPress 之前,我们需要使用 CLI 初始化一个新的 Gatsby 项目。为此,打开您的项目目录并运行以下命令(我们将在设置过程中指定我们想要创建 Gatsby 项目的文件夹):
npm init gatsby
这将运行 create-gatsby,它将通过命令行提出一系列问题来配置您的新 Gatsby 网站。以下是您需要创建我们的示例项目所需的响应,如图 9.7 所示:
-
您想给您的站点起什么名字? Gatsby WordPress。
-
您想将您的站点创建在哪个文件夹中命名? gatsby-wordpress。
-
您将使用 CMS 吗? 使用您的箭头键选择 WordPress。
-
您想要安装一个样式系统吗? 不(或者我稍后添加)。
-
您想要使用其他插件安装附加功能吗? 使用箭头键向下滚动并选择完成。
-
配置 WordPress 插件。 这是我们在本地 WordPress 实例的 GraphQL 设置中记录的 GraphQL 端点。例如,我的地址是 http://gatsbysample.local/graphql。
-
我们应该这样做吗? 按下 Enter 键选择是。

图 9.7 Gatsby CLI 为新 Gatsby 站点提供了一个逐步设置过程。
这将开始生成我们 Gatsby 网站在 gatsby-wordpress 目录中的默认文件。一旦完成,将目录更改为项目文件夹并启动 Gatsby。请注意,由于创建我们的 Gatsby 应用程序过程中存在一个问题,我们可能需要在运行 Gatsby 之前安装 gatsby-plugin-sharp 和 gatsby-transformer-sharp 插件:
cd gatsby-wordpress
npm install gatsby-plugin-sharp gatsby-transformer-sharp
npm run develop
一旦构建完成,我们可以在 http://localhost:8000/ 上查看我们的网站,它应该看起来像图 9.8。

图 9.8 由 Gatsby CLI 生成的网站的默认主页
9.3.4 探索 Gatsby 的数据层
在我们开始编码之前,让我们探索 Gatsby 的数据层。Gatsby 倾向于从基于 GraphQL 的数据层获取您应用程序的所有数据。当我们的网站在本地运行时,我们可以通过访问 http://localhost:8000/graphql 来查看此数据层中可用的数据和查询。让我们在新的浏览器窗口中打开它。
由于我们已经探索了 WPGraphQL 提供的 GraphiQL 编辑器,所以这应该看起来有些熟悉。在这种情况下,探索面板中的查询包括 Gatsby 的所有默认数据查询以及一些 WordPress 查询。后者被添加到 Gatsby 的数据层,因为我们选择 WordPress 作为我们的 CMS 在创建网站的过程中。
随意探索可用的查询,但就目前而言,让我们测试我们将用于在主页上填充数据的查询。在查询编辑器中输入查询并点击播放按钮运行它。
列表 9.4 从 Gatsby 数据层检索 WordPress 内容的查询
{
allWpPost {
edges {
node {
title
author {
node {
name
}
}
slug
excerpt
content
}
}
}
}
这个查询与我们之前在 WordPress GraphiQL 编辑器中运行的查询非常相似,只是它使用了 Gatsby 数据层提供的 allWpPost 查询。
列表 9.5 列表 9.4 中查询返回的结果
{
"data": {
"allWpPost": {
"edges": [
{
"node": {
"title": "Hello world!",
"author": {
"node": {
"name": "brian"
}
},
"slug": "hello-world",
"excerpt": "<p>Welcome to WordPress. This is your first post.
➥ Edit or delete it, then start writing!</p>\n",
"content": "\n<p>Welcome to WordPress. This is your first post.
➥ Edit or delete it, then start writing!</p>\n"
}
}
]
}
},
"extensions": {}
}
完美!我们现在可以使用这些数据在我们的主页上了。
9.3.5 在 Gatsby 中消费 WordPress 内容
在您的代码编辑器中打开项目文件。根项目文件夹包含一个 gatsby-config.js 文件,该文件是在创建过程中为我们生成的。它包含我们指定的站点名称以及我们提供的 WordPress 插件配置和 GraphQL 端点 URL。如果我们部署我们的 WordPress 实例,这就是我们更新端点 URL 的地方:
module.exports = {
siteMetadata: {
title: "Gatsby Wordpress",
},
plugins: [
{
resolve: "gatsby-source-wordpress",
options: {
url: "http://gatsbysample.local/graphql",
},
},
],
};
我们站点的源代码位于 src 目录下。在该目录中,我们的站点页面位于 pages 目录中。我们只有一个主页(index.js)和一个 404 页面(404.js)。让我们打开 index.js 并进行一些修改。
页面使用在样式下方定义的 inline 链接数组中的数据,我们不需要它,所以让我们将其删除。同时,让我们也从标记中删除渲染链接列表的代码,以便我们的 IndexPage 标记看起来如下:
// markup
const IndexPage = () => {
return (
<main style={pageStyles}>
<title>Home Page</title>
<h1 style={headingStyles}>My Blog</h1>
<ul style={listStyles}></ul>
</main>
);
};
到目前为止,我们的页面除了显示带有文本“我的博客”的 h1 标题外,没有渲染任何内容。在我们能够渲染帖子列表之前,我们需要将数据提供给页面。让我们在 export default IndexPage 行下面直接创建一个查询,该查询将直接针对 Gatsby 的 GraphQL 数据层运行。这个查询看起来很熟悉,因为它与我们之前在探索 Gatsby 数据层时测试过的查询相同。
首先,我们需要在我们的 index.js 文件顶部添加一个导入:
import { graphql } from 'gatsby';
然后,我们可以添加查询:
export const pageQuery = graphql`
query IndexQuery {
allWpPost {
edges {
node {
title
author {
node {
name
}
}
slug
excerpt
content
}
}
}
}
`;
接下来,让我们使这个查询的数据对页面可用。首先,我们需要在 index.js 文件顶部添加另一个导入:
import PropTypes from 'prop-types';
然后,我们可以通过我们的 allWpPost 查询告诉我们的 Gatsby 页面我们提供的数据。PropTypes 库正在进行类型检查,以确保数据符合我们的预期。在 export default IndexPage 行之后放置以下代码:
IndexPage.propTypes = {
data: PropTypes.shape({
allWpPost: PropTypes.shape({
edges: PropTypes.array,
}),
}),
};
现在我们可以更新我们的标记来输出查询的结果。代码从我们的页面属性中解构数据,然后使用数据.allWpPost 中的项目数组,该数组包含查询的结果,以列表形式输出我们的帖子,并带有摘录。请注意,我们必须使用 dangerouslySetInnerHTML 来显示摘录内容,因为它以 HTML 的形式返回。这个属性之所以这样命名,是因为这样做会使 DOM 更改超出 React 虚拟 DOM 的作用域,这对于我们的目的来说是可行的,但通常应该谨慎行事。
列表 9.6 在 Gatsby 中输出 WordPress 博客帖子列表
// markup
const IndexPage = ({ data }) => {
return (
<main style={pageStyles}>
<title>Home Page</title>
<h1 style={headingStyles}>My Blog</h1>
<ul style={listStyles}>
{data.allWpPost.edges.map((post) => (
<li key={post.node.slug}>
<span>
<a style={linkStyle} href={post.node.slug}>
{post.node.title}
</a>
<p
dangerouslySetInnerHTML={{ __html: post.node.excerpt }}
style={descriptionStyle}
></p>
</span>
</li>
))}
</ul>
</main>
);
};
我们在浏览器中运行的更新后的主页应该看起来像图 9.9,显示我们默认 WordPress 安装中的一个帖子,以及帖子的摘录。

图 9.9 编辑后的博客主页输出我们 WordPress 实例中的博客文章和描述。默认情况下,WordPress 安装包含一个虚拟帖子。添加更多帖子以查看它们的出现。
9.3.6 使用 WordPress 作为无头 CMS 的下一步
我们正在使用 WordPress 作为 CMS 来填充我们的 Gatsby 站点。构建此应用程序的下一步是创建动态路由以在 Gatsby 中显示单个 WordPress 帖子。虽然我们在这里不会涉及这一点,但我鼓励你通过 Gatsby 的 WordPress 指南 (www.gatsbyjs.com/guides/wordpress/) 或 CSS-Tricks 上的这篇教程 (mng.bz/4jEv) 了解如何通过 Gatsby WordPress 源插件集成 Gatsby 和 WordPress。
重要的是要记住,WordPress REST API 可以被任何 Jamstack 前端消费,无论是 Gatsby 还是其他。实际上,WPGraphQL 插件提供的 GraphQL 端点也可以在 Gatsby 之外消费,例如使用 Eleventy 或 Next.js。虽然 Gatsby 为 WordPress 提供了一些内置连接,但 WordPress 也是几乎所有 Jamstack 站点的有效无头 CMS 后端,无论你选择了哪种 SSG。
9.4 使用网站构建器连接 CMS
到目前为止,我们讨论了你可以使用 SSG 或无头 CMS 提供商提供的启动器连接无头 CMS 的方法。我们还探讨了如何使用 WordPress 连接自己的 CMS。然而,我想要探索的另一条路径是使用像 WeWeb (www.weweb.io/)、Strattic (www.strattic.com/)或 Stackbit (www.stackbit.com/)这样的网站构建器。
这些网站构建器在提供启动器模板所提供的内容之外,还通过根据你的需求定制生成的网站以及提供基于 Web 的工具来编辑该网站(无论是技术资源还是非技术资源)来走得更远。它们还允许你通过流行的现有无头 CMS 提供商集成内容管理。
请记住,这些服务通常免费开始使用,但一旦达到使用上限或想要取消功能限制,就会向您收费。
9.4.1 WeWeb
WeWeb 是一个基于 Vue 的网站生成器,因此它不依赖于 SSG,可以生成一个可以直接在浏览器中完全编辑的网站。这意味着您不仅仅是编辑文本内容,还可以在其基于网页的 WYSIWYG 编辑器中添加和重新排列页面上的元素(图 9.10)。

图 9.10 WeWeb 管理 UI 允许您编辑页面上的文本以及影响网页设计外观和感觉的属性。
您可以根据 WeWeb 提供的预建模板构建和自定义网站,或者从空白网站开始。在两种情况下,您都将能够利用现有的网站组件来创建和修改您的网站。这些组件允许您添加常见的网站元素,如联系表单或富文本。您还可以创建和上传自己的自定义 WeWeb 组件以供使用。
WeWeb 内置了对多种无头 CMS 服务的连接,包括 Strapi、Ghost 或 WordPress。它提供的数据源还包括 Airtable、任何 REST API、Google Sheets、任何 RSS Feed、任何 GraphQL 端点、SQL 数据库或您可以使用 JavaScript 连接的任何端点。
9.4.2 Strattic
Strattic 在构建与 CMS 连接的 Jamstack 网站方面采取了非常不同的方法。本质上,Strattic 在其服务器上为您部署了一个完整的 WordPress 设置。您可以通过 WordPress 管理界面像平常一样管理内容。Strattic 甚至支持大多数标准 WordPress 主题和许多插件,但由于静态网站文件的性质,您可能需要首先检查与您最喜欢的插件的兼容性(mng.bz/QW6Q)。
当您准备好发布时,您可以使用已安装的 Strattic 插件将网站的 Jamstack 版本部署到 Strattic 的服务器上(图 9.11)。网站的外观和感觉就像一个 WordPress 网站,但它是由静态文件和无服务器 API 构建的。这些无服务器 API 使得网站搜索、评论、论坛和其他动态功能得以实现。

图 9.11 Strattic WordPress 插件为您的网站提供了一些发布选项。一旦生成并部署了静态网站代码,您将能够在 Strattic 的服务器上查看其运行情况。
请记住,尽管这是一个 Jamstack 网站,按照大多数人的定义,您无法访问前端文件。然而,Strattic 确实提供了对 WordPress PHP 文件的 SFTP 访问。Strattic 提供免费试用,但试用期过后需要付费账户。
9.4.3 Stackbit
与 WeWeb 相比,Stackbit 遵循更传统的 Jamstack 方法,它在网站构建器中利用 SSG。实际上,它支持多个 SSG 以及多个无头 CMS 选项。网站构建器免费使用,代码推送到你自己的 GitHub 账户并在你的 Netlify 账户上部署。然而,可选的网站编辑套件需要付费账户才能使用许多功能。
使用 Stackbit 构建 Jamstack 网站的第一步是选择一个主题(图 9.12)。截至本文撰写时,有 17 种不同的主题可供选择,基于多种原型,如博客或电子商务。你可以添加自定义主题,但这确实需要正确的 stackbit.yaml 配置文件,该文件告诉 Stackbit 如何将页面上的内容映射到 CMS 中的内容。

图 9.12 使用 Stackbit 生成 Jamstack 网站的第一步是选择其 17 种可用主题之一或上传你自己的自定义主题。
接下来,你将进入一个摘要屏幕,允许你修改其他默认网站构建选项,如图 9.13 所示。默认的 SSG 是 Next.js;然而,你也可以选择 Jekyll、Hugo 或 Gatsby。值得注意的是,并非所有可用的无头 CMS 目前都与 Stackbit 系统中的 Next.js 兼容。

图 9.13 一旦你选择了主题,你可以自定义 Stackbit 将使用的 SSG 和无头 CMS。所有代码都将推送到你连接的 GitHub 账户。
默认内容管理系统(CMS)是 Git。这是一个基于 Git 的纯 CMS,它直接管理连接的 Git 仓库中的内容。它通过维护一个单独的分支来处理 Stackbit 编辑器内的草稿和预览更改。虽然这个选项完全免费,但你可能需要考虑基于 Git 的 CMS 是否适合你的项目,如果你有多个内容贡献者/编辑者,你可能需要付费的 Stackbit 账户。
其他可用的无头选项包括 Sanity、Contentful、Netlify CMS、Dato CMS(基于 API 的 CMS)和 Forestry(基于 Git 的 CMS)。
为了创建网站,你需要将 Stackbit 连接到你的 GitHub 账户,但一旦完成,点击创建网站按钮将使用你选择的主题和 SSG 生成所有网站文件,同时配置你的无头 CMS 并填充一些默认内容。如果你已经连接了 Netlify 账户,它也会在 Netlify 上部署。当网站生成并部署完成后,你将被带到 Stackbit 的内容编辑器。编辑器对单个用户免费使用,但有一些功能限制,但值得注意的是,编辑器完全是可选的。你可以直接在 GitHub 中修改你的代码,在你的选择的 CMS 中直接修改你的内容。
9.5 接下来是什么?
我们已经探讨了在选择无头 CMS 时您可用的几种不同选项,甚至查看了一些用于集成这些选项的工具和资源。正如我们在本章开头讨论的那样,大约有 87 种不同的无头 CMS 选项,因此在做出明智选择时可能会感到不知所措。以下是一些问题,供您自我反思,以帮助您缩小值得评估的选项范围:
-
基于 Git 的无头 CMS 的基于文件的编辑是否适合我的项目,或者我是否需要基于 API 的无头 CMS 提供的额外灵活性? 我们在本章前面讨论了每个选项的优缺点,以帮助您决定哪个选项最适合您的项目。
-
解决方案是否需要开源? 如果是,那么像 Netlify CMS 或 Strapi 这样的解决方案值得调查。您可以通过访问 Jamstack 的无头 CMS 列表并按开源许可证进行筛选来找到其他开源选项。
-
我们是否有一个可以将其作为无头(例如,WordPress)利用的现有 CMS?如果是,我们是想继续自行维护和管理 CMS,还是考虑迁移到托管第三方服务? 在许多情况下,如果可能,保留现有的 CMS 将是最直接的方法,因为它不需要对所有内容贡献者进行新系统的再培训。这也可以使其他利益相关者更容易接受迁移到 Jamstack。
-
编辑体验是否满足我的内容编辑的需求?他们学习起来会有多难? 如果用户之前没有使用过 Markdown,那么这种过渡可能会很困难。如果用户习惯于基于页面的编辑(如 WordPress 中的编辑),那么转向由 API 基于的 CMS 所使用的模块化系统可能需要一些时间和培训。这些对内容贡献者的困难过渡可能是不可避免的,但您至少应该意识到它们并为此做好准备。
-
CMS 是否提供与我的 SSG 的集成,这将减少将我们的 Jamstack 前端连接到无头 CMS 的难度? 并非每个 CMS 都有针对每个 SSG 的模板,但许多都有。在从头开始构建之前,务必调查是否已经存在预构建的集成。
显然,将会有更多针对您项目和团队需求的具体问题,但即使只问这些问题,也希望能够帮助您在评估之前缩小选项范围。
要获取选择无头 CMS 时需要考虑的额外好列表,请参阅 Emmanuel Tissera 在Smashing Magazine上发表的“如何选择无头 CMS”(mng.bz/aD57)。
摘要
-
无头 CMS 主要有两种类型:基于 Git 的和基于 API 的。基于 Git 的 CMS 将内容存储在 Git 仓库中管理的文件中,并通过管理 UI 进行编辑。基于 API 的 CMS 在其系统中存储和编辑内容,并通过 API 将其提供给您的应用程序。
-
基于 Git 的无头 CMS 的主要优势是成本。基于 API 的无头 CMS 的主要优势是内容重用的便捷性。
-
Contentful 是一个流行的基于 API 的无头 CMS,它完全通过其管理 UI 进行配置和管理。Sanity 是另一个流行的基于 API 的无头 CMS,但它完全通过 JavaScript 进行配置,并通过开源的 Sanity Studio 进行编辑。
-
WordPress 提供了一个可以作为 Jamstack 网站后端的 API。Gatsby 提供了一系列工具,可以将 API 集成到 Gatsby 数据层中,该数据层为使用 Gatsby 生成网站提供所有数据和内容。我们使用这些工具演示了如何将本地 WordPress 网站连接到新的 Gatsby 网站。
-
网站构建器提供了一种简单(尽管通常不是免费的)的方式来生成可以连接到各种无头 CMS 的 Jamstack 网站。我们探索了三个网站构建器:WeWeb、Strattic 和 Stackbit。


浙公网安备 33010602011771号