现代 web 技术体系
传统前端技术体系的问题

在传统的前端技术体系中,从下到上,代表抽象层从底层到顶层。最右边三个方块,都从最下面延伸到了最上面,代表它们都是端到端的解决方案,跟左边的体系,以及彼此之间,都是割裂的,包含大量重复。蓝色的方块都是代码层面的,绿色的方块都是平台层面的。

这套体系是从业务中自然发展出来的,但随着这种发展趋于停滞,这套技术栈正在同样自然的演变成历史遗留的「祖传技术栈」,其中每个部分都有比较大的瓶颈问题。

脚手架的问题

不管哪种形式的脚手架,本质都是复制粘贴一堆样板代码,组成新的项目。

前端脚手架跟建筑行业使用的脚手架一样,都是在搭建过程中使用,用完就放到一边,只留下搭完的项目。不一样的是,建筑用的脚手架拆掉之后留下的是一套不能动的钢筋混凝土建筑骨架。而脚手架生成的前端项目里,是基建与示例代码混杂在一起的样板代码,虽然有文件结构,但可以随意修改。而且经常是「必须改」样板文件的内容和结构,才能完成真实项目的完整搭建。

假设一个脚手架包含 A、B、C 三块功能,用这个脚手架创建出的三个项目,最初都是一样的,也都包含 A、B、C 功能。

接下来,三个项目必然需要在脚手架的生成结果上,做各种增删改,可能是因为业务需求,也可能是因为技术沉淀和工程改进。时间长了之后,三个项目之间会差别非常大。如果要做统一的改进,或者把一个项目的沉淀和改进,应用到其它项目里,都会很困难。有时甚至要推迟需求开发,先对这些项目做一轮统一的重构,但这也只能应付眼下,之后这些项目仍然会继续分裂和腐化。

另一方面,脚手架本身也在迭代改进,但因为脚手架是一次性的,一用即抛,这些改进不能对原先创建的项目带来好处,引入这些改进的成本,跟从其他项目里引入改进的成本差不多。

项目模板的问题

脚手架的必然结果,是需要各种项目模板。不但脚手架建设者需要提供多种模板,覆盖不同的需求,使用者也经常需要复制原有模板,修改成新的模板。不管产品的技术形态是 SPA 还是 MPA,都会产生不同的模板。

图上每个方块,都代表一个真实存在的模板,可以看到这些模板中有大量重复、但又不会完全相同的内容,升级维护模板、在模板之间同步技术沉淀,都有成本。很多模板会缺少更新,长期停滞,把成本留给搭建项目的人。

如果从项目场景的角度出发来设计和维护模板,也有相同的问题。图中的方块是一些最基础、最典型的场景,和场景中的技术需求,可以看到,不同场景之间的技术需求,重合度很高。

除了场景类型,一个项目还有很多类型维度,图中的每个方块,代表一种维度:

  • 业务方向
  • 场景类型
  • 组件库
  • 应用架构
  • 解决方案
  • 运行方式

比如从组件库的维度举例,不同项目之间仅仅因为「组件库」和「设计系统」不同,就要设计和建设不同的模板。而这些维度之间的排列组合

  • 要么会导致模板进一步分裂和数量爆炸,每种模板的维护成本更高,应用场景更小,ROI 因此变低,更加倾向于停滞
  • 要么会导致模板对很多维度中的需求,不做考虑,只能覆盖小部分需求,对项目开发的支持,局限在比较低的水平
Webpack 的问题

为了避免每个前端开发者都成为「Webpack 工程师」, 很多脚手架、工程化建设,都会对 Webpack 做下图这样的包装。

在最上层,提供围绕编译构建的两个命令 dev 和 build,搞出不同「规范」的配置文件和插件机制。

业务项目仍包含大量配置

这种包装的第一个问题,是抽象程度很有限,配置 API 的设计也五花八门,体现不同的个人偏好和业务经验,这种配置虽然被称作「规范」,但在真实业务项目中存在感不高,业务项目还是要直接靠 Webpack 来解决很多问题,项目中包含很多 webpack 配置,脚手架模板也包含大量相关的样板代码,避免不了前面说的问题。

If you, like me, are working on JavaScript tools, you’re a gatekeeper to the largest programming community in the world. This is scary.

这段话来自 Redux 作者 Dan 的一篇文章,讲 JS 工具的配置 API 的设计,这段话就是在讲这方面的抽象和设计能带来巨大的影响,有很高的门槛,需要非常严肃专业的对待,这种工作也需要高度的集中,而不是交给「Webpack 工程师」们搞各种各样的「规范」。

编译工具演进

JS 开发工具从去年开始又出现新一轮更新换代的征兆,有人把这种趋势称作 「JS 的第三纪元」,新范式的项目涌现,开始进入到日常业务的开发实践,很多场景下已经没有 Webpack。

如果业务项目深度依赖 Webpack、包含很多 Webpack 配置,还要面临编译工具升级的问题。

图中右侧的 esbuild 和 swc 这样的构建工具,把编译、构建、打包、压缩等在 Webpack 里属于不同环节的部分,合并在一起,加上非 JS 的系统编程语言的帮助,大幅提升构建速度。另一方面,也能支持 ESM 优先的、不需要打包的构建场景。

Snowpack、Vite 这样的工具,在此基础上实现了开发者体验(DX)优先的、不打包(Unbundled)的开发调试模式。

这些工具和模式跟 Webpack 的设计有一些本质矛盾,目前已经被用于公共库的构建、业务项目的开发调试等真实场景里。

dev 和 build 远远不够

基于 Webpack 包装的工程化建设,第三个问题是:对项目开发的工程支持只停留在比较低的水平,比如就像 dev 和 build 命令一样,局限于跟编译构建有关的环节。

而随着行业和业务的发展,随着前端技术的发展,一个前端工程的完整需求,就会像图中一样,会包含蓝色方块代表的整个研发链路的每个环节,以及每个环节下面,绿色方块代表的工程需求。这些都超出了 Webpack 的能力范围。

前端工程化的问题

刚才说的 webpack 的第三个问题,其实也是传统前端工程化建设本身的问题。

传统的工程化建设,只能实现「代码层面」的基础建设,在创建项目的时候,做的事情大部分都属于「代码初始化」。而前端工程需求不止「代码初始化」和「代码层面的基建」。

现代的 web 工程和前端工程,越来越多的包含「代码层面」之外的「平台层面」。

图中绿色方块代表的,是靠「代码层面」来实现工程需求,橙色方块代表的,是要靠「平台层面」才能更好实现的需求。

React 的问题

很多前端开发场景都在统一收敛到 React 技术栈,但 React 本身也是有局限的。

选择 React 的原因可以归纳为图上这四个,其中,符合技术趋势,设计演进更快,走在最前面,这两点让 React 在基础建设中,在有基础建设团队支持的业务场景中,都具备很大的优势。

React 庞大的生态红利或验证规模。从 npm 安装趋势图中可以看到,在比较贴近实际使用情况的依赖下载数据里,React 在绝对数量遥遥领先的情况下,增长势头也是更快的,React 生态下的 CRA、Next.js,单独拿出来都达到或接近其他非 React 技术的使用量。

这种生态和规模上的差距,在国内环境中也回避不了,国内 JS 开发者的数量差不多是全球(上千万)的十分之一,所以单靠国内开发者,影响不了整个行业和技术社区的生态和基建,导致在业务支持和工程建设中,React 目前都无法取代。

但在工程体系中只靠 React 自己是远远不够的,React 本身只解决视图层的问题,距离一个 Web 框架还缺很多东西,在框架层面上,React 是无偏见的,比如没有限制路由实现、组件类型、SSR 解决方案等,也没提供默认的配置和工具体系,跟一个真正框架的必备要素,是完全相反的。

Node.js 框架的问题

由于 Webpack、React 都不解决全链路的问题,缺乏框架级别的解决方案,很多前端项目会把目光转向发展时间更长、已经形成框架级别基建的服务器端框架领域,但这方面的 Node.js 框架,也有瓶颈问题。

NestJS 的作者,在文档首页的设计理念部分,就指出 React、Vue、各种开发工具,都没有解决「应用架构」的问题,而 NestJS 就是要提供开箱即用的应用架构。

传统的前端开发局限于视图层,跟完整的软件开发、产品开发相比,最缺的就是「应用架构」。

但是 Node.js 框架能提供的,只是「服务器端应用架构」,是以服务器端开发为中心的。

有些情况下,比如活动页面,客户端部分本来就很薄,谈不上「客户端应用架构」,但这种情况下的业务关注点仍然在客户端部分,如果基于有完整服务器端应用架构的 Node.js 框架去开发这种项目,对前端开发者来说有些偏离重点。

在另一些情况下,客户端部分比较厚,这种情况下需要的「客户端应用架构」,就像图中右边的蓝色部分,跟左边代表「服务器端应用架构」的橙色部分,是完全不同的,如果使用 Node.js 框架,蓝色部分仍然需要自己摸索和搭建。

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

不管什么软件架构,核心都是「分层」和「关注点分离」,完善的现代 Web 工程里,「服务器端应用架构」和「客户端应用架构」不会割裂,而是混为一体的。

除了架构,Node.js 框架也解决不了前面说的其他问题,同时还引入了图中右边的新问题:

业务逻辑既分散割裂,同时又重复,导致项目变得「低内聚高耦合」。在「前后端分离」之后从「后端项目」里独立出来的「前端项目」,使用 Node.js 框架之后,又混入了很重的后端业务逻辑和研发需求。

而 Node.js 框架随着发展,本质也越来越清楚:还是服务于专业服务器端开发的,降低不了前端开发者的服务器端开发门槛,这是一个最大的瓶颈。

从这张图可以看到,包含服务器端部分的「前端项目」,并不是真正的「全栈」,只是包含了服务器端最上面的、很薄的一层,解决 Web 和 BFF 需求(注意这里说的 Web 是指处理 HTML 请求,而 BFF 这个词在国内有很多误用,实际上是专指面向特定 UI 的 API 服务)

对于前端项目里的这些服务器端需求,用 Node.js 框架来开发,很多时候带来的问题比解决的问题更多。

https://blog.bitsrc.io/bff-pattern-backend-for-frontend-an-introduction-e4fa965128bf
IaaS 和 后端 PaaS

最后,看下最底层的 IaaS 和 PaaS 部分。

传统云计算中的 IaaS 和 PaaS,就像图中的绿色部分,从最底层到最上层,有这样几层,最上面这层,有不同的平台和服务形态,传统上,前端项目跟后端项目一样,直接部署这一层上,比如字节内部后端技术栈的项目,都部署在被称作 TCE 的 PaaS 上,而前端技术栈的项目,以前也只能这样部署。

* 《容器云在头条的落地和实践》:https://time.geekbang.org/dailylesson/detail/100016772

但是在这种 PaaS 上或直接在 IaaS 上做部署和运维,对前端开发者来说,不但复杂低效,而且在很多前端部署需求上,没有获得任何支持。

比如图中粉色部分中这些前端部署方面的通用需求。由于 IaaS 和 PaaS 都源自后端开发场景,是后端研发技术的平台化沉淀,设计和演进都天然的受到局限,对前端研发场景缺乏理解,就算理解,也无法在相同的基础设施里去兼顾这些前端部署需求。

也就是说,多数前端项目和底层的 IaaS 和 PaaS 之间,存在一大片空白,也就是图中粉色的区域。

在真实业务中,为了避免每个前端项目都直接跟 IaaS 和 PaaS 打交道,很多业务都会有意无意的做一些集中建设,填补这片空白。

比如字节的一些 web app 项目,会由后端负责的 go server 来运行。

很多有 SSR 需求的业务,会搞一个统一的 SSR server 项目,在开发各种前端仓库的时候,需要本地有 SSR server 的仓库,才能运行和调试这些前端项目。

像这样的集中建设,要么把前端项目中的正常组成部分,放到了前端开发者无法掌控的地方;要么反复重复的建设出被业务需求扭曲的部署方案,难以沉淀和演进,最大化的提效。

所以图中粉色这片区域的空缺,该由什么来填补,是传统前端技术栈里的一个大问题。

Web 开发的范式转移

回顾了「祖传技术栈」的这些问题,接下来我们看下在这些问题的驱动下,业界和技术社区里已经形成的趋势,这些趋势带来的发展和优势,也反过来,让前面说的这些问题变得更明显,更急需解决。

这种趋势可以归纳为「传统 Web 开发」范式到「现代 Web 开发」范式的转变。

这种转变,可以称得上是一场「范式转移」,原有的理解和习以为常的假设被打破,在原有的东西上小修小补解决不了问题,需要建一套新的东西,做出根本性的变化。

「现代 Web 开发」范式的起源、背景和发展状态:

传统

图中左边蓝色部分是最能代表「传统 Web 开发」的一些技术栈和规范,右边橙色部分是在「现代 Web 开发」趋势下出现的技术栈和技术形态,接下来把它们展开看看包含哪些要素,就会看到左边的东西跟右边的东西,差别非常大。

Ruby on Rails 是「传统 Web 开发」范式的一个典型代表, 本质上是以服务器端开发为中心的、MVC 架构的「服务器端 Web 框架」,就像图上右边这样,产出网页和 API。

网页部分需要的前端代码和工程化,一般包含在同一个仓库里,框架本身也会内置一些前端相关的工程化,经常会需要被专业全面的前端工程化替代掉。

「十二要素应用宣言」本质上是一种工程标准,是进入云计算时代之后,服务器端应用和工程项目中形成的规范,这套标准可以保证应用能很好的运行在「后端 PaaS」或其他的传统云计算平台上。

在 Node.js 帮助下形成的前端工程规范,不管有没有服务器端代码,也都受到了这个规范很大影响。

MERN 技术栈,是前端代码和工程,从其他技术栈的服务器端框架中「分离」出来之后,又融合相同技术栈的服务器端框架,形成的「全栈」开发方案。

组成 MERN 的 4 个首字母,就能体现这套技术栈的本质,整体上还是以服务器端的研发方式、应用架构、平台基建为中心的。

JAMstack / SHAMstack

在「现代 Web 开发」趋势下,在国外被称作「JAMstack」的技术栈,变得越来越清晰和主流。它还有另一种命名建议是图中第二行的「SHAMstack」

这两个缩写的首字母组合虽然不同,但就像图中的推导结果,本质是一样的,都是由 4 个要素组成。

跟刚才说的 MERN 技术栈比较,Serverless 模式和基建取代了 M 代表的传统后端基建,前端应用的构建产物 、 服务器端渲染、静态网站生成、前端程序中的 BFF、等等,取代了 E 代表的服务器端 Web 框架和服务器端代码。

这种技术栈的项目仍然是「全栈」的 Web 项目,但变成以客户端研发方式、客户端应用架构为中心,看上去很像是纯静态的、纯前端的项目。

STAR 技术栈

还有人归纳了这种叫「STAR」的技术栈,本质上是图中右边这4个要素组成。

同一个现代 Web 项目,可以同时符合 JAMstack 和 STAR:

  • JAMstack 强调的是前后端架构和开发范式
  • STAR 强调的是专业的编程方式

特别要注意的是,STAR 其实体现了一个现代 Web 项目中的「前端工程化」,除了编译环节,也需要运行时环节和代码编写环节的支持,除了视图层,也需要完整的应用架构和 Model 层的支持。

STAR Apps: A New Generation of Front-End Tooling for Development Workflow

meta-framework

在「现代 Web 开发」趋势下,国外技术社区里越来越多的提到 meta-framework,本质上是把 JAMstack 和 STAR 强调的部分加起来,用以客户端为中心的、包含更上层抽象的、通用的 Web 框架的形式,整体地系统地满足这些需求,抽象、简化这里面的各种模式。

在「现代 Web 开发」里,这种 meta-framework 是更合适的基础抽象层,取代了以脚手架和构建工具为代表的更原始的基础。

「S 型曲线」是一种事物「发展成长规律」的表示方法,这张 JS 框架的 S 型曲线图,很好的表达了 meta-framework 在 JS 技术发展中的意义。

  • 在图中左侧的早期阶段,各种 UI 框架在行业和技术社区里不断涌现,包含各种各样的尝试和破坏性创新。
  • 随着发展,UI 框架不再是快速发展的前沿了,选择也不再那么多,技术发展逐渐稳定和收敛成了基于 React 做上层的整合和框架建设。
  • 这种框架变成了新的发展前沿,也产生了大量选择,同时作为更成熟的技术,以更快的速度被采纳使用,之后又逐渐稳定和收敛,形成主流技术。
  • React Distros and The Deployment Age of JavaScript Frameworks
归纳定义

比较了「传统 Web 开发」和「现代 Web 开发」中的不同现象,最后我们来归纳和定义一下这两种范式的本质特征和组成要素。

框架、UI

从框架和 UI 的角度来看:

  • 「传统 Web 开发」是以服务器端框架为中心来做全栈开发,客户端是围绕 HTML 和对象树的编程范式。
  • 「现代 Web 开发」是以新一代客户端框架为中心来做全栈开发,客户端同样围绕专业、通用的软件开发语言和编程范式。
架构

从架构的角度来看:

  • 「传统 Web 开发」中存在两套比较独立和欠缺融合的程序,「前后端分离」改善了分工协作,但对于分离出来的「前端技术栈的 Web 项目」,在架构和开发范式上没有真正实现「分离」,前端开发者需要跟服务器代码打交道。
  • 「现代 Web 开发」实现了在同一套程序里一体化开发,在开发、调试、运行、部署等环节都能做到「无服务器化」,让前端技术栈的开发者更容易成为真正的「产品开发者」。
抽象

从抽象的角度来看:

  • 「传统 Web 开发」中前端部分的基础抽象是原始、初级的,比如脚手架、样板代码、基本的 Webpack 包装和 CLI 工具、React 和 JS 生态中的海量选择,很有「前端特色」,跟成熟的软件开发有显著差距。图中的 DX 是指开发者自身的效率,UX 是指开发出的应用的能力和质量,传统范式中的抽象太原始,导致 DX 和 UX 没法同时提升,提升其中一个,就会损害另一个。
  • 「现代 Web 开发」中有了一体化的、更成熟的、框架级别的基础抽象,能同时追求和确保 DX 和 UX 的最大化。
部署

从部署的角度来看:

  • 「传统 Web 开发」的项目,要么纯静态托管,要么作为服务器端应用来部署和运维。不同部署方式,会导致整个工程都不同,需要不同的脚手架模板。
  • 「现代 Web 开发」的项目有更多样强大的运行和部署方式,但在 meta-framework 的支持下,项目模板的变化变少了,甚至有机会完全融合、收敛成一个模板,成为「Universal App」。
平台

最后,从平台角度来看:

  • 「传统 Web 开发」基于后端的云基础设施,由于代码层面缺乏抽象,客户端代码的复杂性容易停留在很基础的程度,有些时候会干脆直接改成 JSON 之类的配置。
  • 「现代 Web 开发」基于新一代 Serverless 平台。在 Serverless 的支持下,能实现以前难以实用化的云研发。由于代码层面有统一的、上层的抽象,在开发中能引入更多低代码模式。