在 2018 年,我们推出了 Firefox 扩展工作坊,一个针对 Firefox 特定扩展开发文档的网站。该网站最初使用基于 Ruby 的静态网站生成器 Jekyll 构建。我们最初选择 Jekyll 用于此项目是因为我们希望让编辑能够轻松地使用 Markdown(一种轻量级标记语言)更新网站。
一旦网站创建完毕,并添加了更多文档,构建时间就开始增长。每次我们对网站进行更改并希望在本地测试时,网站的构建都需要 10 分钟或更长时间。构建花费的时间太长,以至于我们需要增加我们持续集成和持续交付服务 CircleCI 的默认时间限制,因为构建在没有输出的情况下运行超过 10 分钟就会失败。
使用分析调查这些缓慢构建表明,大部分时间都花在了 Liquid 模板渲染调用上。
除了构建时间问题之外,还有本地开发中缓存清除的问题。这意味着像图像更改这样的操作不会在本地缓存清除后完全重建的情况下显示在网站上。
当我们讨论如何最好地前进时,Jekyll 4 发布了,其功能预计会提高构建性能。但是,早期测试将移植到这个版本实际上比以前的版本表现更差。 然后我们意识到我们需要找到一个替代方案并移植网站。
更新:2020 年 10 月 5 日:一位 Jekyll 开发人员联系了我们,他调查了缓慢的构建。调查结果表明 Jekyll 本身并不慢,我们案例中很高的构建时间主要是由第三方插件造成的。
查看替代方案
从 Jekyll 迁移的基本要求如下
- 构建性能需要优于在本地构建所需的 10 分钟(600 秒以上)。
- 本地更改应尽快显示。
- 理想情况下,解决方案应该是基于 JavaScript 的,因为附加组件工程团队拥有丰富的 JavaScript 集体经验,而基于 JavaScript 的基础架构将使其更容易扩展。
- 它需要足够灵活,以满足未来添加更多文档的需求。
Hugo(用 Go 编写)、Gatsby(JS)和 Eleventy(11ty)(JS)都被视为选项。
最终,选择 Eleventy 的主要原因是:它提供了与 Jekyll 足够的相似之处,使我们能够在没有重大重写的情况下迁移网站。相比之下,Hugo 和 Gatsby 都需要进行重大重构。移植网站也意味着前期更改更少,这使我们能够专注于保持与已在生产中运行的网站的一致性。
由于 Eleventy 提供了通过 LiquidJS 使用 Liquid 模板的选项,这意味着模板只需要相对较小的更改即可正常工作。
当前架构
Jekyll 网站中有四个主要构建块:Liquid 模板、Markdown 文档、Sass 用于 CSS 以及 JQuery 用于行为。
为了迁移到 Eleventy,我们计划尽量减少对网站工作方式的更改,并专注于移植所有现有文档,而不更改 CSS 或 JavaScript。
开始移植
将 Jekyll 升级到 Eleventy 的博客文章,由 Paul Lloyd 撰写,对描述在 Eleventy 下让网站正常工作需要完成的操作有很大帮助。
第一步是根据旧的 Jekyll 配置文件创建一个 Eleventy 配置文件。
数据文件从 YAML 移动到 JSON。这是通过 YAML 到 JSON 转换 完成的。
接下来,更新了模板以修复变量和包含项的差异。jekyll-assets 插件语法已删除,因此资产直接从 assets 目录提供。
使用静态文件运行
为了替换 Jekyll 插件以用于 CSS 和 JS 的最小修复,CSS(Sass)和 JS 是使用添加到 package.json 中的命令行界面 (CLI) 脚本构建的,使用 Uglify 和 SASS。
对于 Sass,这需要通过 CLI 加载路径,然后只传递主样式表
sass
--style=compressed
--load-path=node_modules/foundation-sites/scss/
--load-path=node_modules/slick-carousel/slick/
_assets/css/styles.scss _assets/css/styles.css
对于 JS,每个脚本都按顺序传递给 uglify
uglifyjs
node_modules/jquery/dist/jquery.js
node_modules/dompurify/dist/purify.js
node_modules/velocity-animate/velocity.js
node_modules/velocity-ui-pack/velocity.ui.js
node_modules/slick-carousel/slick/slick.js
_assets/js/tinypubsub.js
_assets/js/breakpoints.js
_assets/js/parallax.js
_assets/js/parallaxFG.js
_assets/js/inview.js
_assets/js/youtubeplayer.js
_assets/js/main.js
-o _assets/js/bundle.js
这显然很笨拙,但它让 JS 和 CSS 在开发中正常工作,尽管需要手动运行脚本。但是,由于 CSS 和 JS 包正常工作,这使得能够专注于让主页正常运行,而不必担心任何更复杂的内容来开始。
经过一些进一步的调整,主页成功构建
使用 Eleventy 构建的扩展工作坊主页截图
让整个网站构建
主页看起来应该的样子后,下一个任务是修复其余的语法。这是一个相当费力的过程,需要更新所有模板,并删除或替换 Eleventy 没有的插件和过滤器。
经过一些修复文件的努力,终于可以在没有错误的情况下构建整个网站!🎉
立即显而易见的是 Eleventy 的速度有多快。整个网站在不到 3 秒内构建完成。这不是打错字;是 3 秒而不是分钟。

当 Doc 第一次意识到 Eleventy 的速度有多快时,《回到未来》中的他:”不到 3 秒……太棒了!“
改进静态构建
构建 JS 和 CSS 不是 Eleventy 本身的一部分。这意味着如何处理静态文件由您决定。
目标如下
- 保持速度。
- 保持简单(特别是对于内容作者)
- 让更改尽快反映出来。
第一个方法将 CSS 和 JS 移动到节点脚本。这些脚本使用 API 复制了粗糙的 CLI 脚本。
最终,我们决定将资产构建完全从 Eleventy 中分离出来。这意味着 Eleventy 可以专注于构建内容,而另一个进程将处理静态资产。这也意味着静态资产脚本可以直接写入构建文件夹。
这使得能够与 Eleventy 构建并行构建静态资产。缺点是 Eleventy 无法告诉 browserSync 实例(Eleventy 在开发中使用的服务器)更新,因为它没有参与此过程。这也意味着需要使用单独的监控配置来监控 JS 和 SASS 源文件,在本例中,这是通过 chokidar-cli 完成的。以下是 package.json 脚本中用于 CSS 构建的命令
chokidar 'src/assets/css/*.scss' -c 'npm run sass:build'
sass:build
脚本运行 bin/build-styles
告诉 BrowserSync 更新 JS 和 CSS
由于 JS 和 CSS 构建正确,我们需要更新 browserSync 实例以告知它 JS 和 CSS 已更改。这样,当您进行 CSS 更改时,页面将在没有刷新情况下重新加载。快速更新为迭代更改提供了理想的短期反馈循环。
幸运的是,browserSync 有一个 Web API。我们可以使用它来告知 browserSync 每次在开发中构建 CSS 或 JS 时更新。
对于样式包,调用的 URL 是 http://localhost:8081/__browser_sync__?method=reload&args=styles.css
为了处理这个问题,构建脚本会在每次构建新 CSS 时获取此 URL。
清理
接下来,我们需要清理所有内容并确保网站看起来正确。需要替换丢失的插件功能,并且文档和模板需要一些调整。
以下是所需任务列表
- 构建模板和 计算数据 中 Jekyll SEO 插件的替代方案。
- 清理语法高亮显示并移至“代码围栏”。
- 使用 11ty 的“在 GitHub 上编辑此页面”配方。(一个很棒的功能,可以更轻松地接受对文档改进的贡献)。
- 清理模板以修复一些错误的标记。这主要是关于查看 Liquid 中的空格控制,因为在某些情况下,它与 Markdown 有些不良交互,导致出现额外的多余元素。
- 重新创建页面 API 插件。在 Jekyll 下,搜索使用此插件,因此我们需要重新创建它以保持一致性,以避免从头开始重新实现搜索。
- 构建标签页面,而不是使用搜索功能。这样做也是为了SEO优化。
生产环境构建
网站看起来已经符合预期,并且许多小问题已经整理完毕,最后一步就是研究如何处理生产环境的静态资源构建。
为了获得最佳性能,如果静态资源(JS、CSS、图像、字体等)存在于浏览器的缓存中,则不应该让浏览器重新获取这些资源。为此,可以将资源的失效时间设置为未来的某个时间点(通常是一年)。这意味着如果 foo.js 已经被缓存,那么当浏览器请求该资源时,它不会重新获取资源,除非缓存被清除或缓存的资源比原始响应中失效时间头指定的日期更早。
使用这种方式缓存资源后,需要更改资源的 URL 以“清除缓存”,否则浏览器就不会发出请求,因为它认为缓存的资源是“新鲜”的。
清除缓存策略
以下是用于清除缓存 URL 的一些标准方法:
整个网站版本字符串
可以使用整个网站的 Git 版本号作为 URL 的一部分。这意味着每次添加一个新版本并发布网站时,所有资源都会被视为新鲜资源。缺点是,即使资源自上次发布网站以来没有更新,客户也会下载所有资源。
查询参数
使用这种方法,会在 URL 后面添加一个查询字符串,其中包含内容的哈希值或其他唯一字符串,例如:
foo.css?v=1
foo.css?v=4ab8sbc7
这种方法的缺点是,在某些情况下,缓存代理不会考虑查询字符串。这可能导致提供陈旧的资源。尽管如此,这种方法还是非常常见的,缓存代理通常不会默认忽略查询参数。
带有服务器端重写的 Content Hash
在这种情况下,需要将资源引用更改为指向包含哈希值的 URL 的文件。服务器被配置为在内部重写这些资源以忽略哈希值。
例如,如果 HTML 文件引用 foo.4ab8sbc7.css,则服务器会提供 foo.css。这意味着不需要将 foo.css 的实际文件名更新为 foo.4ab8sbc7.css。
这需要服务器配置才能工作,但它是一个非常简洁的方法。
内置文件中的 Content Hash
在这种方法中,一旦知道内容的哈希值,就需要更新对该文件的引用,然后就可以将包含哈希值的文件名输出。
这种方法的优点是,一旦静态网站以这种方式构建,就可以在任何地方提供服务,而不需要像上一个例子那样进行任何额外的服务器配置。
我们决定使用这种策略。
构建资源管道
Eleventy 没有资源管道,但这是一个正在考虑用于未来的功能。
为了部署 Eleventy 端口,需要进行清除缓存,以便我们可以继续将资源部署到 S3,并将失效时间设置为未来的某个时间点。
使用 Jekyll 资源插件,可以使用 Liquid 模板控制资源构建。理想情况下,我希望避免内容作者需要了解清除缓存。
为了实现这一点,所有资源引用都需要以“/assets/”开头,并且不能使用变量构建。考虑到网站的简单性,这是一个合理的限制。
由于资源引用很容易找到,因此需要找到一个解决方案来实现清除缓存。
第一次尝试:使用 Webpack 作为资源管道
第一次尝试使用了 Webpack。我们几乎使用 eleventy-webpack-boilerplate 作为指南实现了它,但是这开始引入开发和生产环境的 webpack 配置之间的差异,因为它实际上将构建后的 HTML 作为入口点。这是因为清除缓存和优化不应该是开发过程的一部分,以使本地开发构建尽可能快。让它工作变得越来越复杂,需要对加载器进行特殊的修补分叉,因为 HTML 提取方式存在限制。
Webpack 也不适合此项目,因为它期望了解 JS 模块之间的关系,以便构建一个包。此网站的 JS 是使用一种旧的风格编写的,其中脚本按特定顺序连接在一起,没有任何对依赖项的特殊考虑(除了脚本顺序之外)。这本身就需要大量的变通方法才能在 Webpack 中工作。
第二次尝试:AssetGraph
在纸面上,AssetGraph 看起来像是完美的解决方案。
AssetGraph 是一个可扩展的,基于 node.js 的框架,用于操作和优化网页和 Web 应用程序。核心是一个整个网站的依赖关系图模型,其中所有资源都被视为一等公民。它可以根据声明性代码自动发现资源,将配置需求降到最低。
主要概念是,HTML 文档和其他资源之间的关系本质上是一个图问题。
AssetGraph-builder 使用 AssetGraph,并以 HTML 作为起点,计算出所有关系并优化所有资源。
这听起来很理想。但是,当我在网站的构建内容上运行它时,节点内存不足。由于无法控制它的行为,而且关于它卡在哪里几乎没有反馈,所以这种尝试被搁置了。
也就是说,AssetGraph 项目的总体目标非常好,它看起来是值得关注的未来方向。
第三次尝试:构建一个管道脚本
最终,最有效的解决方案是构建一个脚本,该脚本会在 Eleventy 完成网站构建后处理资源。
它的工作原理如下:
- 处理所有二进制资源(图像、字体等),记录它们的 Content Hash。
- 处理 SVG,记录它们的 Content Hash。
- 处理 CSS,重写其中对资源的引用,压缩 CSS,记录它们的 Content Hash。
- 处理 JS,重写其中对资源的引用,压缩 JS,记录它们的 Content Hash。
- 处理 HTML,重写其中对资源的引用。
- 将所有未写入新目录的文件写入新目录。
注意:这里可能存在一些未涵盖的边缘情况。例如,如果一个 JS 文件引用了另一个 JS 文件,那么这可能会根据处理顺序而中断。可以更新脚本来处理这种情况,但这意味着需要重新处理已更改的文件,以及引用它们的任何内容都需要更新,等等。由于这对于当前网站来说不是问题,所以为了简单起见,它被省略了。此外,也没有循环依赖检测。同样,对于此网站和大多数其他基本网站来说,这不会是一个问题。
优化和清除缓存不属于开发构建的原因是有道理的。这种分离有助于确保网站在本地进行更改时仍然可以快速构建。
这是一个权衡,但只要在发布前检查生产构建,它就是一个合理的折衷方案。在我们的案例中,我们有一个从 master 分支构建的开发网站,一个从标签构建的包含 -stage 后缀的预发布网站,以及一个生产网站。所有这些部署都运行生产部署流程,因此有很多机会捕获完整构建中的问题。
结论
迁移到 Eleventy 是一项积极的改变。虽然实现它确实需要很多步骤,但这是值得的。
过去,Jekyll 的构建时间很长,这使得贡献者和文档作者使用此网站非常痛苦。由于降低了门槛,我们已经开始看到一些额外的贡献。
随着此次迁移的完成,现在开始变得清楚,下一步应该采取哪些措施来最大程度地减少文档作者的样板代码,使网站更易于使用和贡献。
如果您有一个运行在 Jekyll 上的网站,或者正在考虑使用现代静态网站生成器,那么强烈建议您试一下 Eleventy。它快速、灵活、文档齐全,而且使用起来很愉快。✨
关于 Stuart Colville
Stuart 是 Firefox 扩展的工程经理。
一条评论