Coding.blog JAMStack博客是如何毫不费力地变得快速的

2020-06-26 02:09:41

通过Internet提供内容的基础相当简单:浏览器将向您的服务器发送请求,而您的服务器使用一些可能链接到某些JS和CSS的HTML进行响应。

但是,如何以及何时生成这些HTML、CSS和JS内容则是完全不同的事情。您可能想要预先准备好它们,让您的服务器只为它们提供服务(预呈现),您可以让您的服务器动态生成它们(SSR),或者您可以让您的服务器将一些代码发送到浏览器,然后浏览器将生成它们(客户端呈现,SPA)。您甚至可以在服务器上生成请求的内容,并将其与在浏览器上生成后续请求内容的代码一起发布(同构)。

有一种特别的方法非常适合博客:您事先准备好大部分内容,然后将其与代码一起提供,这些代码将嵌入交互部分并生成完全动态的组件。这称为JAMStack,这是coding.blog和CODEDOC用来生成内容的方法。在这篇文章中,我们详细介绍了我们选择这种方法的原因,以及我们是如何实施它的。

为了更好地理解为什么我们选择JAMStack作为coding.blog,以及我们如何设定进一步的设计目标,最好先对Web内容生成/交付方法有一个总体概述:

预渲染只是指事先(即在构建阶段)准备好您的内容。这是最快的交付方式,允许您简单地使用CDN,而不是编写服务器来为您的内容提供服务。

您可以在服务器上即时生成内容,以响应每个请求,而不是准备内容。当您的内容需要根据传入的请求进行更改,并且您可能需要从某个API获取一些数据才能创建内容时,这可能会很有用。但是,这种方法显然比预渲染慢得多。

高度交互和动态的内容意味着您需要在客户端生成内容。为了避免将内容生成代码放在多个地方(这很难维护/扩展),您可以在客户端浏览器上执行所有内容生成,让您的服务器只发布内容生成代码。

为了避免为站点的每个页面提供内容生成代码,您还可以控制客户端的导航,这将导致单页面应用程序(Single Page Application,简称SPA)。这是所有现代前端框架的基础。

客户端呈现意味着用户应该等待额外的时间才能看到内容,因为通常服务器生成内容的速度更快。它还会扰乱SEO,因为爬行器可能无法获取完整形式的内容(因为它们不是浏览器),因此可能无法正确地对其进行索引。

为了在不将内容生成代码分散到多个地方的情况下克服这些问题,引入了同构应用的概念。我们的想法是,基本上在服务器和客户端运行相同的代码,同时将代码本身与服务器上呈现的内容一起发送到客户端。

同构应用程序的复杂性不可避免地带来了额外的约束和管理费用。例如,您需要在客户端重新消重内容,这意味着您在如何操作内容方面受到限制(例如,对于Reaction SSR,Reaction需要保持对DOM树的完全控制)。这些复杂性也使优化性能变得更加困难,因为有更多的组件及其交互在影响性能。

另一种方法是预先呈现所有静态内容,然后将填充动态/交互部分的代码发送给客户端。这将允许在不牺牲内容交互性的情况下实现与预渲染相同的传送速度。它还意味着代码库与同构应用程序之间存在明显的分离:有预先呈现内容的代码,也有转到客户端并使内容具有交互性的代码。

JAMStack架构特别适合于大多数静态内容,这使得它成为CODEDOC(用于代码文档/指南)和coding.blog(用于关于编码/编程的博客)等工具的完美选择。工作流程的简单性允许轻松优化以及高度的互操作性和可扩展性。

虽然JAMStack架构最适合于CODEDOC和coding.blog,但它要求将内容生成代码拆分成在构建阶段执行的位和传送到客户端的位。这会迅速增加任何不断增长的项目的复杂性/开销,因此我们必须找到解决此特定问题的解决方案。

此外,我们需要最小的工具链和堆栈,具有最大的可扩展性和互操作性。通常,我们希望HTML/JS/CSS的知识足以进行严格的定制,这使得基于JSX的语法成为我们组件系统的最佳选择(特别是因为TypeScript支持它的开箱即用)。

然而,我们不能使用像React这样的库(或任何基于VirtualDOM的解决方案),因为它对外部对DOM树的更改非常敏感,这意味着扩展如何处理DOM受到限制,无论是在预呈现期间还是在客户端。此外,我们需要内容尽可能轻量化,这只是简单地禁止了相对较重的操作,如VirtualDOM差异。

为了满足我们的设计目标,我们创建了一个直接位于DOMAPI之上的基于JSX的呈现工具。这称为连接性HTML,在基本级别上,它只是一个允许通过JSX使用它们的DOMAPI的包装器:

当然,这意味着对于交互组件,需要熟悉RxJS,但是,您通常需要熟悉一些反应式状态管理库才能正确创建交互组件。对于人们来说,RxJS可能不是最容易使用的库,但是考虑到它的广泛使用和整体性能提升,我们觉得这是一个非常值得的妥协。

有趣的事实:历史上具有连接性的HTML最先被开发出来,CODEDOC作为记录它的工具。然而,由于CODEDOC和随后的Coding.blog的流行,我还没有找到时间将它用于最初的目的。

为了实现其余的设计目标,我们创建了一个名为Connective SDH的工具。SDH代表静态/动态HTML,这意味着这个库允许我们无缝地创建静态(预先呈现)和动态(在客户端呈现)HTML内容。

对于预呈现(甚至是SSR),连接HTML非常薄这一事实意味着我们可以简单地将其与JSDOM结合,并添加一些用于存储结果的很好的函数:

对于动态组件,即要在客户端呈现的组件,我们需要:

在本例中,计数器是需要在客户端呈现的组件,即需要传输到客户端。这是通过以下代码行完成的:

Transport()函数简单地计算出计数器的导入路径,然后创建一个PlaceholderComponent,即$Counter,该组件也标记有该信息。如果在客户端执行,它将返回COUNTER本身,因为不需要传输任何东西。

因此,我们现在可以在任何上下文中使用$COUNTER。如果在预渲染过程中使用,那么它将是一个占位符,在客户端将替换为Counter,如果在客户端使用,则与Counter相同:

SDH提供的Bundle类允许我们轻松地管理客户端捆绑包。我们希望遵循SDH的非魔术方法,因此我们不想将捆绑包管理隐藏在某个模糊的序列化过程中:

如前所述,我们需要收集所有客户端组件并捆绑它们的代码。捆绑包对象专门为此目的提供了.Collect()方法:

这将导致捆绑包扫描生成的DOM树并搜索所有传输占位符(例如$COUNTER)。由于这些占位符是用其原始组件的导入路径标记的,因此捆绑包对象可以收集创建客户端捆绑包所需的所有导入路径。

SDH将首先为客户端捆绑包创建一个入口点,在其中导入所有必要的组件。它还添加了一些必要的代码,以允许传输占位符在到达客户端时用其实际组件替换它们自己。

然后,SDH使用webpack从该条目文件中创建捆绑包(捆绑器及其配置自然也可以被覆盖)。webpack的树抖动,加上这样一个事实,即只有在扫描的DOM树中使用的组件,

让我们再看一下负责创建COUNTER的传输占位符的行:

如前所述,传输占位符$Counter需要知道其原始组件,即计数器,以便在客户端替换其自身。这也意味着它必须知道COUNTER的确切导入路径,以便它能够将其传递给包,以便它可以导入它。

由于SDH的部分目标是避免不必要的复杂性,我们也不想让这个过程变得超级复杂。因此,我们添加了一条简单的规则:必须在从中导出原始组件的文件中调用Transport()函数。这将允许Transport()函数查看它自己的调用堆栈和传递给它的组件的名称,并构造它的导入路径。

首先,也是最重要的是,这种方法会带来极好的性能。大多数内容都是预先渲染的,可以通过CDNS进行缓存和高效交付,这意味着浏览器可以很快得到第一个响应,然后立即能够显示内容。当然,浏览器确实倾向于等待一些额外的资源,如脚本和样式表,但是脚本也可以推迟,因为它们不是使用初始内容所必需的(并且样式通常非常单薄)。

显然,在性能方面仍有改进的空间,例如现在字体正在不必要地限制页面。我仍然不确定内联样式还是独立加载它们,因为前者会产生更快的页面加载,但后者意味着更多的内容可以缓存。

除了性能之外,SDH呈现方法还使得维护预先呈现的组件和客户端组件非常方便。您可以轻松地将任何预先呈现的组件转换为客户端组件(反之亦然),捆绑包将自动适应这些更改。为了避免错误地预呈现客户端组件,SDH在预呈现期间检测到试图绑定到浏览器事件的组件时也会抛出错误。

不过,更重要的是,所使用的所有工具的简单性和简单性提供了与DOMAPI和外部工具的极端互操作性。在预呈现期间和客户端,CODEDOC及其插件经常使用诸如Document.querySelector()这样的API来查找和手动替换/修改DOM元素。因为没有需要与实际DOM保持同步的虚拟DOM,所以对如何操作类别生成的内容没有限制。

SDH呈现方法也不是没有它自己的警告。以下是我们在使用SDH进行CODEDOC和编码时面临的最重要的注意事项。博客:

SDH使得在预先呈现的组件和客户端组件之间无缝共享代码。然而,这并不总是一件好事,因为很多包和代码都不是为在客户端使用而设计的。

在非常糟糕的情况下,webpack甚至无法创建捆绑包。更糟糕的情况是,它确实创建了包,但无法正确地对其进行树抖动,包括包中大量未使用的代码。

尽管CODEDOC(和coding.blog)页面非常轻量级,共享资源很小且非常可缓存,但是SPA风格的加载仍然提供了更好的用户体验。

为此,我们甚至为CODEDOC和coding.blog开发了一种特定的平滑加载机制,它只获取请求的HTML并替换DOM的指定部分。这个策略非常有效,事实上,我怀疑可以简单地将其解压到一个单独的包中,该包可以为任何JAMStack应用程序提供SPA风格的导航。

然而,我们的流畅加载策略取决于这样一个事实,即主要资源(如字体、脚本和样式)在不同的内容页面之间共享。SDH确实支持捆绑包拆分,因为任何Bundle实例都会跟踪另一个Bundle实例是否已经处理了特定的客户端组件。然而,如果天真地使用这个特性,仍然会留下一些共享代码(主要是RxJS和连接性HTML的代码)跨多个捆绑包进行复制。也可以将这些依赖项拆分到单独的捆绑包中,但目前这需要手动配置webpack(或任何使用的捆绑包),并且还需要在实践中进行测试。

附注:如果你不知道coding.blog是什么,它是一个开源博客平台,被设计成一个高质量的编程文章的地方,而不是广告,付费墙等。这不仅仅是一个口头禅:我们想要设计一个通过透明的价格管理服务和平台本身的开放性来系统地保持高内容质量的平台。

如果你感兴趣,你可以在这里阅读更多关于它的信息。如果你是一个写编程博客的人,并且正在寻找一个新鲜的,方便的,非常可定制的开源博客,在像https://your.coding.blog,这样的酷域名上,那么签下这篇文章,如果你选择加入我们,就加入我们的潜在创建者列表。

为什么我们选择JAMStack作为coding.blog,而不是做SSR、制作单页应用程序、设置同构的Javascript项目等等,还详细介绍了我们是如何使用SDH呈现来实现它的,以及它是如何给我们带来令人难以置信的性能、灵活性和开发人员体验的。

为什么我们选择JAMStack作为coding.blog,而不是做SSR、制作单页应用程序、设置同构的Javascript项目等等,还详细介绍了我们是如何使用SDH呈现来实现它的,以及它是如何给我们带来令人难以置信的性能、灵活性和开发人员体验的。

为什么我们选择JAMStack作为coding.blog,而不是做SSR、制作单页应用程序、设置同构的Javascript项目等等,还详细介绍了我们是如何使用SDH呈现来实现它的,以及它是如何给我们带来令人难以置信的性能、灵活性和开发人员体验的。