Lqs469.

也许你想要的只是一个静态应用

前言

记得刚开始学习做网站的时候,最先学会写出一个 HTML,再给他加上一些 CSS 样式作为修饰,也许最后再加上一丢丢 Javascript 去辅助出一些效果,duang ~ 一个最最基础的网站就完成了,然后我们把它丢到服务器上托管着,用户就可以访问到了。再此之后,我们又学会了用 Java、Python 做动态网站,学会用更复杂的 Javascript 去开发前端 MVC 应用、前后端分离、RESTful。。。

然鹅,最近几年静态网站似乎又不可思议的火了起来,类似于 Gatsby.js、Next.js 等带 SSG(Static Site Generator) 功能框架受到了一些人的欢迎,这是科技的退步,还是人心的复古,让我们一起走进今天的话题——也许你想要的只是一个静态网站。

关于 SSG、JAMstack 究竟是什么,这里就不做赘述了。这里给出一个我认为最简短的解释:

  • SSG - 生成静态网页的框架,基于你熟知的技术(React,Vue)
  • JAMStack - 由 JavaScript, APIs, Markup 构成的网站应用,Markup 这里指预渲染好的静态文件。

既然是预渲染好的,那么带来的问题也很明显,很多人担心静态网站不够“动态”,在业务过于“动态”化的今天,可能无法正常使用,比如业务需要频繁操作数据库,需要频繁用户交互等。而我在实际开发过几个 Gatsby 和 Next 项目之后,得出的结论是 SSG 其实可以做到最大程度的“静态化”,或者说,他可以在很多场景发挥出自己应有的作用,甚至比你想象的还多。也许在未来“静态驱动”型应用会占据前端开发的一片江山甚至有可能成为主流。

那么,何为“静态”

用传统“静态”网站的概念来形容现代的 Static Site 总觉得有点不太合适,那让我们从新认识一下现代前端技术中的 Static Site:Web 应用以数据装载完成且渲染好的 HTML 文件构成为基础,配合 CSS 和 Javascript 的丰富能力将结果直接呈现给用户,而不是在用户请求时由服务动态生成页面文件。

举个例子,你可以直接将静态应用放到 CDN 上,当用户请求网站,服务直接将以渲染完成的 HTML 返回,并不需要任何服务端的计算,也不会给服务造成过大的压力,服务的角色只是简单的托管。

静态的价值

首先我们来看看它的优势。当我们谈论静态应用,静态站点,极致的性能肯定是第一值得认可的地方,原因很好解释,服务端几乎为零的计算量,瞬间返回结果。除此之外,好处还很多:

稳定性

当你引入一项技术、一个工具,也顺便引入了一份风险,因为无论是数据库还是服务器,都有可能发生故障,更有可能因为流量的激增而堵塞,当服务因压力而挂起,用户只会白屏等待或者访问失败。这对于一个永不宕机的应用来说是灾难的,而从加强服务性能本身入手解决问题,无疑将是巨大的成本,而且真正遇到这种流量海啸的机率却是微乎其微,所以日常就是看着 CPU 使用率不到 1% 而花着冤枉钱。这种现象,我们俗称为“不够弹性”,当然你可以用云端弹性计算,Serverless 等方法来解决问题,但这无疑又增加了复杂度,可能又会带来新的问题。当你夜不能寐,也许会想起,我的服务还好吗。

但如果使用的是静态应用,问题就不存在了,无论流量如何变化,服务的计算量都是零;无论数据库是否正常工作,页面已经生成了,不会对线上造成任何影响;无论任何时间地点,CDN 边缘计算都可以用最快速度在离客户端最近的距离(用户本地缓存除外)把页面交付给用户。绝对可以让你睡个好觉。

SEO

相对于客户端渲染的 SPA 应用而言,静态应用天生支持 SEO;比起服务端渲染,无需实时计算生成页面,更直观有效;比起 prerending 方案,更稳定可靠。而且无论什么爬虫,即使技术最原始的爬虫也能保证爬到站点信息,因为静态应用提供的本身就是 HTML,而且每个页面的 SEO 信息都是预先可见的,非常直观且易于管理。

性能

极致的快,得益于服务的无计算过程,也得益于资源可以非常方便的缓存。如果你把应用放到 nginx 托管,可以使用 nginx 缓存;如果放到 CDN,可以直接缓存在云端边缘。除此之外页面还会缓存在浏览器中,而这一切几乎不用写一行代码就可以做到。或者你也可以再写一点 PWA 来管理浏览器端缓存,一切都会变得更美妙。

“动态”的数据源

上面的优点看起来还不错,但你一定有了疑问:数据是预取的,内容都已经写死了,如果业务有些许“动态”的数据,这下应用该怎么办?别着急,我们慢慢解释。

无论数据来源是数据库、CMS、还是文件。我们的应用始终依赖该数据之上,我们将数据源统一抽象为原子数据,以取出某个原子数据为例,在传统的服务端渲染应用中,当用户请求页面,会发生以下事件:

  1. 客户端发出请求 /XXX,服务端接到请求,作出反应
  2. 根据请求信息查询数据库或者 CMS 找到结果
  3. 服务端将找到的数据和 HTML 模版进行字符串计算,最后经过一炷香的时间渲染出新的 HTML 文件
  4. 服务端返回渲染好的 HTML 文件给客户端

那么在静态应用这里的情况呢?其实我们可以有很多种设计,最容易的实现大致是静态生成初始页面,页面带有一些 Loading 态或者用户引导,并且在此之上通过 API 和服务端交换数据,完成页面更新:

  1. 客户端发出请求 /XXX,服务端接到请求,作出反应
  2. 服务端马上返回初始化 HTML 页面,页面并不包含任何动态数据
  3. 在浏览器解析页面之后,页面进入等待,JS 向服务端发出异步请求
  4. 服务端接到请求,根据请求信息查询数据库或者 CMS 找到结果并返回 JSON
  5. 客户端根据返回的 JSON 更新页面

本质上这样的静态应用跟一个单页应用并没什么很大的区别,在初始页面下发之后,执行同样的客户端渲染逻辑,在性能上也只是比单页应用快了一个首页加载而已。其实也可以理解为这是一个折中方案,一方面加速了首屏渲染速度,另一方面数据能够实时动态获取。

那么,我们换一个思路,假设我们不考虑折中方案,如果一开始就拿到的是完整预渲染完的页面呢?

换个思路

我们以往的渲染思路普遍都是前端渲染或者服务端渲染,数据实时计算并通过异步或者同步返回给请求方。如果我们在编译时就把数据准备好呢,将已经与渲染好的 HTML 文件直接返回客户端,这可行吗?首先如果这样做,上面的请求过程将变为:

  1. 客户端发出请求 /XXX,服务端接到请求,作出反应
  2. 服务器立即将完整的 HTML 文件返回给客户端

没有数据库查询,没有即时计算,返回一个 web 应用跟返回一个“Hello world”字符串的原理一样。而且因为是纯 HTML 文件,浏览器可以缓存在客户侧,一些运转起来都是极致的快。

编译时数据预取

上面的场景很美好,但现实很骨感。我们想要完成预渲染就得完成编译时数据预取的工作,而且是“全量”的数据,数据依然来自于 CMS,数据库或者数据文件。当你使用命令 datsby build 或者 next build && next export 去生成静态应用时,编译程序将请求所有应用依赖到的数据源,他们可以是 API,可以是连接池,可以是 JSON 文件,然后按你希望的方式拼接进字符串中,然后使用 React 提供的 server-rending 相关方法生成一个个完整的 HTML 文件。

在 Gatsby 中,社区设计了丰富的插件,用于请求不同类型的数据源,并且互相兼容不同的页面和组件,这里可以瞥一眼:

在 Next.js 中并没有像 Gatsby 这么的插件化,不过也可以在下面生命周期内自由引入:

以上过程都会自动搜集数据,自动执行编译,这样很容易就可以跟 CI/CD 结合,工程化和扩展性都很棒。

真的是 所有数据?

所有数据都下载一遍,生成静态文件得有多少个,这个想法听起来就有点滑稽,这真的可行吗?如果是一个博客网站还好说,也就是每篇文章生成一个页面;但如果是一个电商网站,是不是要生成所有商品的页面,假设商品数以万计,这不是光生成页面就得半天?如果是一个实时监控的控制台呢......

而我认为这个问题的答案并不是非黑即白,就是说这一切技术架构都得根据真实的业务和应用规模和类型来合理使用,什么意思呢。博客非常适合静态应用,文档也非常适合静态应用,是因为他们本身数据不会经常变动,这个“经常”的频率是不会每隔十分钟都发生变化,而这个不变的数据间隔跟我们重新编译生成静态文件相比,是远远大于需要构建的时间的。而控制台一类的中后台监控页面呢,首先数据是实时的,秒级别或者分钟级别的变化频率远远小于我们编译做需要的时间,所以选择静态应用是个非常消耗且不满足需求的方案。那么问题就很明显了,如何找到合适变化的度就成为了判断是否选择静态应用方案的关键。

那么究竟这个变化的阈值该如何掌握呢?让我们深入 SSG 技术的前沿领域,Gatsby 社区一方面努力于构建大型应用的性能提升,另一方面也正在努力于“增量构建”—— incremental buildsincremental builds?Gatsby Incremental Builds Private beta),同样 Next.js 也有类似的 RFC RFC: Incremental Static Generation。增量构建允许我们在每一次构建中只会重新编译发生了改变的那部分页面和组件,而不是全部重来一遍。这会起到多大的作用呢?让我们用上面没有提到的商场来举例。

假设一个电商网站有一万件商品,需要生成一万个左右的静态页面,这个是各自商品介绍页面,不包括首页,推荐页,用户页面等定制化页面。那么在第一次耗时较长的构建之后(这个过程可能会比你想象的要少,以我个人的经验来说 1000 张页面大概耗时 500ms,根据实际内容而定所以并不一定准确),静态应用放到 CDN 完成上线,以边缘计算的绝对速度优势服务于全世界的用户,用户惊叹与真特么的快。然鹅这时候有个商品的厂家通过 CMS 改变了售价,于是我们需要对某件商品改价,构建机器的钩子接到推送,启动增量编译,将被修改的某个商品页面单独重新编译后生成 HTML 文件,因为有变化页面非常小,这个过程甚至可以瞬间发生并结束,然后就是在 CDN 上更换该单独页面的静态文件,剩下的事情也可以在瞬间发生并完成。而代码的变化也是同理,增量构建,增量更新,增量替换。只要保障需要重新构建的部分在一定规模内,用户体验几乎是跟动态网站是差不多的。

而在未来,而随着增加构建、构建加速等特性的不断升级,或许这项技术会成为静态应用方案的一个关键突破点,甚至成为改变生态的胜负手。想一想,如果既有静态应用的优点:稳定、极致性能、可扩展、低功耗,又没有不够“动态”的缺点,简直美滋滋!

目前仍然存在的限制

然鹅,因为编译速度还不够快,所以现阶段静态应用的依旧受限,上面我也表达出了,并不是所有应用都适合静态化。实时控制台类型的应用就不一定适合,但其实他也可以最大程度的“静态化”,除去数据呈现部分,控制台的骨架、Header、首屏、甚至载入态都是可以静态化的,在此之上再加上异步请求的数据去更新页面。也许你觉得这样做的意义并不大,的确如此,但是如果只是为了首屏加载或者 SEO 目的,其实你已经一定程度上达到了,剩下追求用户体验的目标就可以更多放到 REST API (异步请求)的优化上了。

同理,此类型的应用也可以是 Feed 流类型的应用,比如微博推特,核心内容是不断更新的,甚至用户个性化数据,但是页面骨架不会变,路由不会变,静态应用还是可以发挥作用,加速首屏渲染速度,简化应用模型,一定程度上减少一些服务端压力。

总结

静态应用以及 SSG 的技术栈给前端生态带来一些新的解决思路,当我们熟悉了要么 SPA,要么 SSR 之后,发现原来还可以这么玩,老瓶装新酒焕发生机,这无疑给我们一阵头脑风暴。但不论怎么变化,客户端交付给用户的东西是不变的,围绕这一点,前端工程师或者端工程师可以发挥的空间还有很多。而这项技术本身,随着增量构建和构建本身的不断优化加速、解放潜力,也许 SSG 在未来会成为一个不错的解决方案,甚至成为主流也说不定。