Web App: 从 HTML 到 Jamstack
Web
技术
Web 领域已发展得十分庞杂,就知识规模来说已不是你我一个人可以穷尽。从 Web 前后端开发角度来看,前端当下正流行着 Next.js、Remix 等 SSR 方案、以及 React 后续继续开发的 Server Component 等等;后端方面则搞出了“云原生”相关的一整套技术生态,各路轮子百花齐放,一不留神就容易沉迷其中而难以自拔。
对于开发者来说,越来越重要的事情也是,放下“穷尽一切”的执念,寻找属于自己的一隅落脚之地。在学习精力、时间有限的背景下,关注点也需要开始区分优先级,关于哪些技术细节值得关注,哪些其实不必花太多时间。
在这寻找落脚之地的旅程中,对事物发展的脉络有一个大致的把握,也变得越来越重要。大致也是需要搞清楚,当前 Web 技术大概发展到了什么样的程度,当下有什么问题正在被解决,哪些技术又可能成为历史包袱被淘汰。
想起我自初中开始接触编程至今,冥冥中伴随着 HTML5 技术从萌芽到成熟的过程,当下 HTML5 已融入我们生活方方面面,乃至于已向客户端开发领域进一步扩展。也许可以就我的所见所想,简单理一理其中经历的一些比较关键的形态,也让我对接下来时间精力大概可以投向的方向有所感知。至于技术细节,网络上有着丰富的资料,这里就不再赘述。
1. HTML 文档与后端渲染
Web 诞生于 1991 年,在形态上,它最初被设计为一系列的 HTML 文档组成的资料库,在一个 HTML 文档中通过 URL 来描述与其他 HTML 文档的链接,再进一步产生了「网页」和「网站」的概念,一堆 HTML 文件和对应的图片等资源放在服务器某个文件夹里,在启动一个 Web 服务端程序,指向这个文件夹,一个网站就这么架起来了。
这也为开发者们开启了新的想象空间,开始尝试用程序去生成 HTML 网页,诞生了服务端处理 HTTP 请求的 CGI 标准,并且逐渐有了 Web App 的概念。为更好实现注册、登录等与后端交互的能力,大概从 1994 年开始(参考:HTTP cookie),Netscape 浏览器实现了 cookie 功能,支持在用户的浏览器与服务器的通信的 Header 带上一段特殊的字符串,从而让服务端可以以此比较方便地维护与客户端的状态,而不用将状态反复回传。
也是在这个时期,各类以 HTML 为“渲染层”的方案在逐步涌现,如 1994 年起步的 PHP,96年的 Java Servlet 及后来的 JSP,2002 年的 ASP.NET 等等,各个方案针对 Web App 这样的场景,设计了“MVC架构”,数据对应 Model 层,HTML 模版作为 View 层,用户的操作处理逻辑则是由 Controller 来承载。
在这个时期,Web App 的交互模式,基本上是“用户点击某个链接”、“服务器处理请求”、“生成新的 HTML 页面传回浏览器展示“、”用户在新的页面做点击操作“的循环。显然来回传输页面较为繁琐且会带来较大的流量消耗,但这也为后面更丰富的 Web App 形式打下了一个基础。
2. Ajax、Flash 与 RIA 的萌芽
大概在 1995 年开始,Netscape 的工程师推出了 JavaScript,网页开始具备初步的表单校验等交互能力。到了 1998 年,微软等厂商设计了 XMLHttpRequest API 以及 IE 浏览器的一些 ActiveX 控件,支持在网站上对服务器发送异步请求,然后收到响应后再直接更新需要更新的部分。
这也是 Ajax 的雏形,到了 2004 / 2005 年,谷歌在 Gmail 和谷歌地图大量应用了类似的异步请求的技术,Ajax 的概念也逐步推广开来,带来了大家常说的“Web 2.0 革命”。这个过程中,浏览器端做 UI 交互的能力也越来越强,虽然暂时还没有到取代客户端的程度,但实质上已成为一种独立的软件 UI 形式,各种网上银行、网上购物软件等等在大规模运用 Ajax 技术。
Web App 的另外一条支线,可谓和 Flash 息息相关,90 年代的 HTML / CSS / JavaScript 本身对于 UI 交互略显不足,做动画起家的 Macromedia 公司看到了这方面的机会,在 1996 年收购 FutureWave 后,开始推出 Macromedia Flash Player(2005 年被 Adobe 收购后改名为 Adobe Flash Player),并在 1999 年开始推出相关浏览器 Flash 插件,从此 Flash 开始席卷全球,Web 也开启了持续 20 余年的 Flash 插件嵌入时代。
这时候的 Web App,除了 Ajax 外,结合 Flash 插件技术,人们为此做过不少的探索,类似网页上的装饰动画挂件、音乐、视频播放器、网页小游戏、乃至后来流行的网络直播,基本都在依赖着 Flash 相关的技术。这样的实践,也围绕着一个概念 Rich Internet Application,简称 RIA,RIA 是 Macromedia 公司在 2002 年提出的,它关注点在于在插件的加持下,让 Web App 的交互体验尽可能地接近桌面软件。
这一领域的市场,并非只有 Flash 一家独大,还有 Microsoft Silverlight、Java applet、JavaFX 等,它们过去也曾试图分一杯羹,但不敌 Flash 的主流地位。当然,到了今天,Flash 也结束了最后一版更新,随着浏览器和 HTML5 的持续迭代,也逐渐消失在历史的长河。
RIA 由于强依赖浏览器插件,存在有一些缺陷:
- 搜索引擎收录问题,RIA 只关注 GUI 渲染的实现,搜索引擎索引、收录等方面存在障碍。
- 安全问题,Flash 的使用广泛,且因为平台、浏览器的复杂兼容逻辑等,导致其在信息安全上属于一个弱点。常常被黑客利用来绕过浏览器的沙盒机制、渗透进入用户电脑。因此 Flash 插件的安全问题频发,需要经常打补丁修复。
- 跨平台兼容的困难性,插件安装依赖操作系统层面的能力,在其他(特别是移动端)操作系统上(如 iOS、Android),会因为缺少合适版本的插件而带来网页兼容的问题。
3. 从 HTML5 到 SPA、PWA
鉴于 RIA 重度依赖插件的缺陷,在 Web 上承载更复杂交互的愿景,也驱使着开发者们在交互上做更多的尝试,这也是 HTML5 诞生的一个重要因素。结合 JavaScript 的能力,Web 在「文档」与「应用」两种形态的分化,也变得更加地明显。
在「文档」的视角,HTML5 强调语义化的表达,内容形态更加规范,可访问性与搜索引擎索引能力也随之增强。HTML5 标准也兼顾了历史的 HTML 的容错处理,起到一个承上启下作用。
在「应用」的视角,HTML5 在 CSS 和 JavaScript 方面的能力更加发达,并有了稳定的 DOM API,可供 JavaScript 更好地处理与 DOM 的交互,多媒体方面,提供了原生支持的 <video>
、<audio>
元素,以支持多媒体播放能力;在图形方面,有了更强大的 CSS3 动画、<canvas>
画布、SVG 矢量图渲染能力,让 Web 渲染复杂的图像、乃至于 3D 渲染、游戏等方面创造了可能。
自 2008 年 HTML5 标准发布开始,HTML5 在浏览器的迭代下功能越来越完善,再借助其高效代码分发优势,基于 HTML5 的 Web App 备受开发者们的青睐。实质上用户每一次打开页面,都伴随一次重新加载客户端代码的过程,相比于原生客户端是非常有优势的。无论浏览器还是 Web App 开发者,都有着强烈的动力让 Web 的体验向原生客户端靠近。
在浏览器之上的 Web App 开发,伴随着技术栈的不断迭代,上层 JavaScript 及生态也在发生着转变。在 Ajax 时期,人们应用开发的模式,是围绕着 HTML DOM 的交互,以及 Flash 插件所带来的更丰富功能。处理 DOM 交互方面一个代表的类库是 jQuery(2006年诞生),它的关注点也在于封装各平台 DOM API,辅助用户提高处理 DOM 节点的效率。逐渐地,Web App 承载的用户交互愈加复杂,单纯的 DOM 操作复杂度过高,非常容易出 Bug,为了改善这样的问题,社区慢慢地有了 Backbone (2010)、Angular.js (2010)、React (2013)、Vue (2014) 等等辅助处理 UI 的类库 / 框架,Web App 也借此迈上一条通往现代化 GUI 应用的道路,Web 开发者也逐渐从“页面仔”转向一个“工程师”的角色。
这一时期,也伴随着支持在浏览器外运行 JavaScript 的运行时 Node.js 诞生,Node.js 封装了 JavaScript 与操作系统交互相关的 API。这时 JavaScript 开始走向模块化封装代码的模式,诞生了 CommonJS 以及 AMD 乃至于后来的 ESM。进一步催生 Webpack、gulp 等针对 JS / CSS 的打包工具,在 CSS 领域有了 Sass、Less、PostCSS 等解决方案,“前端工程化”的概念逐渐明晰。慢慢地 Web 开发在角色分工上也更加明朗,逐渐实现“前后端分离”的分工模式。
以 React 为代表,Web App 开发开始拥抱“组件化”的开发模式,借此发展出了庞大的开发者生态,迄今为止,基本上你能想到的常见应用场景都有对应的 React / Vue / Angular 等实现(近年来主流正逐渐收敛到了 React / Vue,并进一步催生 Web Component 标准的迭代),慢慢地“单页面应用”(Single Page Application,简称 SPA)的概念也逐渐深入人心。
在浏览器厂商的推动下,HTML5 点交互能力在日益增强,几乎接近原生的 UI 交互,并且在几大浏览器厂商组成的 WHATWG 的推动下,更多操作系统的 API 在考虑以相对安全的方式开放给网页,逐渐形成 Web API 标准。在考虑向后兼容性的基础上,也提供缓存控制、Service Worker、通知等机制,让一个 SPA 可以“渐进式”地变成一个桌面 / 移动端的 App,常驻在用户的桌面上,这便是 Progressive Web App (简称 PWA)的概念。
4. 支线:向 GUI 开发延伸
当 HTML5 技术的交互体验与原生越来越接近,各互联网厂商也在探索把 Web 作为 GUI 的部分的可能性,这样的方案也叫 Hybrid App,在实现上其实就是一个在 WebView 承载的 SPA。Hybrid App 的优势在于,其内嵌的 SPA 每次都会加载最新版,通过 bridge 的方式,可以在 Web 实现 native 能力的调用,实现 SPA 和原生两者的优势互补。加之苹果等应用商店对 Web 并没有很多限制,这也意味着 WebView 是客户端实现业务热修复的一个绝佳的应用容器。
Hybrid App 在移动端会比较常见,尤其是微信等超级 App,实质上正在重度依赖这样的方案。微信内嵌浏览器上的 HTML5 页面,某种意义上也是一种 Hybrid App(微信的私有 API + WebView 上承载的 SPA)。
但由于历史包袱、兼容性等原因,Hybrid App 本身存在性能问题,且在微信上体验也不是非常丝滑流畅。为了更好的交互体验、以及商业生态建设的考虑,微信后来推出了小程序方案。一个小程序在 UI 层面,也是由几个 WebView 构成,它提供了页面栈管理以及各类与原生 API 交互的能力,并通过内部封装的特定语法,隔绝用户可能的危险操作,形成一个定制过的多 WebView 应用 App 开发解决方案。整体形态上,近似于一个可以快速分发的 Hybrid App。
在桌面端也存在 Hybrid App 的模式,不过与移动端不同的是,在桌面端开发者通常可以直接把所有的 UI 界面都画在一个 WebView 里,不用考虑太多多屏幕的交互。一个主流方案也是 Node.js 与 Chromium 的组合,前者负责对接 Native 相关 API,一个负责 HTML5 UI 的渲染、以及用户向的业务逻辑。在各种方案角逐之下,Electron 成为了最大的赢家,后来也催生出基于系统原生 WebView 的 tauri 等方案。具体应用方面,市面上已有不少有代表性的 Electron 应用,比如说 VSCode、Notion、飞书、Figma、Slack、XMind、重构后的 QQ 等等。
除了平台底层能力的适配,在其上层应用的设计思想的变迁,也在影响着开发者们。就拿 React 来说,它在 GUI 应用开发的理念也是十分超前,并且实现了 UI 逻辑与具体的平台解耦。除了 Web 外, React 也可以做 Native App(React Native),考虑 WebView 本身在性能和交互效果方面的缺陷,直接操作原生 API 实现的 UI 性能也更好,并且由于业务逻辑是纯 JavaScript,涉及到热更新、多端同构的场景,代码迁移也会非常方便。
即使在非 JavaScript 涉足的领域,也在被相似的设计思想所引领着,如跨平台的 Flutter、Android 的 Jetpack Compose、iOS 的 SwiftUI 等等,它们在 UI 表达的思想方面,也带着不少 React 的影子。另外,拿 Flutter 来说,它在 API 设计、布局与绘图引擎也和 Web 浏览器非常接近,甚至 Flutter 本身也提供了从 Dart 工程编译到 JavaScript / HTML / CSS 的能力。
5. 从 SSR 到 Jamstack
可以看到的是,客户端 GUI 开发只是其中属于「应用」的一个支线,再回到浏览器之上的 Web 发展主线来看,SPA 显然不是 Web 的最终形态。Web 更多强调的是开放与互联,但 SPA 只是借助了 HTML5 的渲染能力,WebView 对 SPA 更多是作为一个“应用容器”存在,开发者并不关心页面之间是如何通过链接建立的关联,在代表链接与传播属性的「文档」层面,仍带着一个比较大的缺口。
这也是 SPA 所面临的一个局限所在,首先它需要加载一段 HTML,这个 HTML 需要有一个 <script>
标签,加载一段 JS,然后所有业务逻辑都在这个 JS 中去承载,再通过操作 DOM 的方式去实现 UI 的展示。它的工作就像当年的 Flash 在页面嵌入一个 <object>
元素,然后再在这个 <object>
元素中通过插件加载 swf 文件一样,其实都是一个封闭的黑盒子,外部对于内部的内容会缺少一个索引和感知。
这样的一个首页 load 一段 js 问题,在上述说到的涉及到用户体验和 SEO 的场景,显然不是太能被接受,因此 Web 前端圈子也有了一个 “服务端渲染直出页面” 的概念(也叫 Service-Side Rendering,简称 SSR),通过服务器端预先加载好需要的数据,事先输出一部分 HTML,让用户更快看到界面,减少一些等待过程中的烦躁,让这个加载过程看起来更顺畅和丝滑;在 SEO 方面这份 HTML 也针对搜索引擎去做索引,并服务于 Web 站点的互相链接。
这里的 SSR 其实也和当年 PHP / JSP / ASP 等后端语言在做的事类似,只是说在编程语言层面都统一为了 JavaScript。与换一门后端语言写模版不同,这里的服务端渲染,更多是在复用了浏览器端 JS 渲染的组件代码逻辑,实现客户端与服务器端“代码同构”。
至此我们可以发现,此时的 Web 技术,无论是「文档」的视角还是「应用」的视角,在编程语言生态上,正在向最初的 HTML、CSS 和 JavaScript 这”三剑客“收拢,以及周边的各种语言和工具;在平台方面,这三剑客覆盖了浏览器之上的 GUI 交互、以及面向搜索引擎的 HTML 生成方面。
此时我们若将其视作一个共同体,就相当于某个业务系统的一部分,类似一个负责对接浏览器与搜索引擎的 “Web Service”,我们系统内的其他部分,可以约定好 API 与之相互通信。关于这一共同体的实现,我们可以将其收拢到 JavaScript 这一门编程语言,并通过 HTML / CSS 来渲染 UI,再利用浏览器的 fetch API 等 Ajax 能力以及 Node.js / Deno 等 JS 运行时,去对接其他模块的 API。
这样的共同体概念,业界将其称为 Jamstack,Jamstack 最早还叫 JAMstack,代表着 JavaScript / API / Markup 三者的结合,后来社群讨论影响下,开始改名为 Jamstack,意图让开发者把重心放在 JavaScript,以及 JavaScript 生成的 Markup(即 HTML),以及将 Web Page / App 的实现与其他系统解耦合,通过 API 去通信。
有了 Jamstack 这一概念对共同体的定义,也可以让开发者的关注点更加集中,把精力都聚焦于以 JavaScript 为中心的网站构建方案,也如前文提到的,通过一套代码去对接用户与搜索引擎,无论是内容类网页还是 Web App 都能轻松承载。
在服务端渲染领域,各种方案也围绕着服务端与客户端的紧密融合做了不少的工作,类似 Next.js / Remix / Nuxt.js / SvelteKit 等其实也都是为此而服务。一个例子是 React 的 Server Component 特性,他们在试图设计一个流式传输渲染结果的方案,进一步缩短页面打开在时间与流量的消耗。
另外,Jamstack 架构的 Web 站点 / App,可以直接部署在 边缘 CDN、Serverless 等平台,用户访问时,可以在最近用户的服务器上渲染页面并立刻返回,提升网站的响应速度。类似 Vercel、Netlify 等新形态的网站托管服务商,也是在做类似的服务支撑。
6. 总结
把这历史的潮流梳理下来,感觉 Web App 的开发,好像又重新走上了 20 年前 PHP 时代的轮回,隐隐约约类似一条这样的线索:后台直出 HTML 模版(PHP 等) -> Flash 插件 + Ajax + jQuery -> HTML5 SPA(React / Vue / Angular) -> SSR 改进 SPA -> Server Components(JS 实现的更强大的后台直出 HTML 模版)。
最近 Next.js Conf 介绍了他们开发的 Server Actions 特性,可以 在 React 组件内写 sql,这在网上引发一片舆论 哗然。不过在本文的视角来看,这些其实都在意料之中。从 Web App 开发逐渐收敛于 Jamstack 架构来看,这里更多的是一个“螺旋上升”。虽然在形式上好像又回到了类似过往 PHP 时代拼接输出 HTML 的模式,但不同的是,这里的输出更加灵活,Web 开发者可以更加专注,减少一些在不同框架间切换穿梭的倦怠感、以及 “老子学不动了” 的恐惧感。