如何逃离框架孤井?

前言

前面我发过一篇文章,脱离了Spring询问大家能不能继续开发,结果文章下面的评论和回复都告诉我大家伙的基础打得很牢固,该咋写还是咋写。看得我在这内卷的时代瞬间躺平。

如何逃离框架孤井?_第1张图片
那么今天挑战升级,不用任何框架开发 Web 应用程序,你能做到么?

首先,我们要思考一个问题:

不使用框架等同于重复造轮子吗?

过去流行的是 Angular,然后是 React,现在是 Vue.js……其他的像 Ember、Backbone 或 Knockout 什么的几乎都快消失了。一些标准,例如 Web Components,则很少被使用。似乎每年都会发布一些新框架,比如 Svelte、Aurelia,而且每个框架在服务器端都有对应的对象(开头那些框架对应的 NestJS、NextJS 或 Nuxt,Svelte 对应的 Sapper,等等)。非 JavaScript Web 框架(如 Django、Spring、Laravel、Rails 等)就更不用说了。甚至还有框架之上的框架(Quasar、SolidJS)、为框架生成组件代码的框架(Stencil、Mitosis),以及 NCDP(无代码开发平台,No-Code Development Platform)。

这种多样性让想知道哪种技术值得学习的开发人员和技术选型决策者感到困惑。

网络上经常会出现一些比较这些框架的文章,好像是在帮助我们解开这种困惑。但大多数作者通常是带有偏见的,因为他们可能“用过这个框架”,但“只尝试了一些其他的框架”。偏见程度较低的作者总是得出“这取决于具体情况”的结论(取决于性能、工具、支持、社区等),这实际上是一种非结论性的结论。

即使一些基准测试基于同一个应用程序对不同的框架进行了比较,也很难获得真实的结果,因为这种基准测试受限于被测试的应用程序(比如待办事项应用程序)。

框架看起来就像是宗教(或者说是政治):每一个框架都假装自己为开发者提供了解决方案,但每一个又都不一样。它们每一个都声称可以为应用程序提供最好的前景,但关于哪一个真正名副其实的争论又不绝于耳。每一个框架都要求你遵循特定的规则,它们之间可能有相似之处,但要从一个框架转换到另一个框架总是很难。所以没有什么一说:Angular天下第一;Vue是天。ヾ(=・ω・=)o

现在,让我们来看看框架的“无神论”方法:不使用框架

为什么不使用框架?

实际上,这个想法还很新。早在 2017 年,Django Web 框架联合创始人 Adrian Holovaty 就谈到了他的框架“疲劳”,以及他为什么离开 Django 去构建自己的纯 JS 项目。

有人可能会问,为什么会有人想要在不使用框架的情况下开发 Web 应用程序?为什么不在其他人花了数年时间和精力的成果的基础上做开发?或者是因为 NIH(Not Invented Here)综合症导致人人都想构建定制的框架?

开发人员并不会比一般人更倾向于自找麻烦,实际上,他们可能比任何人都懒:他们只会想写更少的代码(这样他们就可以更少犯错),想要自动化(以避免人为错误)……

能摆谁不摆呢?
如何逃离框架孤井?_第2张图片

但他们又想要敏捷,也就是能够轻松、快速地解决问题。

虽然“快速”似乎是框架承诺的东西(为你搭建脚手架,并增加可靠性),但它不是免费的:它们想让你签署合同,同意支付“税”费,并将你的代码放入“孤井”(“税和孤井”的说法来自 IBM Carbon 系统设计团队负责人 Akira Sud)。

框架税

使用框架是需要付出成本的:

  • 遵循它们的 API 规则,这样它们就可以向你提供服务。这就是框架的工作方式:你的代码必须遵守某些规则,包括或多或少的样板代码。你每天要想的不是“如何做这件事”,而是“如何让框架做(或不做)这件事”。如果你规避了这些约束,风险就由你自己承担:如果你通过直接调用底层 API 来绕过框架,就不要指望它们能理解你的意图,也不要指望它们的行为能保持一致。所以,框架会让你“专注于业务”是一个虚假的承诺:实际上,框架的事情你也没少操心。
  • 如果你想要以下这些东西,就不得不强制进行升级:

    1) 想要一个新功能(即使你不需要所有功能,也必须升级所有东西);

    2) 想要修复一个 bug;

    3) 不想失去框架的支持(随着新版本的发布,你的应用程序所依赖的版本将会被弃用)。

  • 如果框架出现了 bug,但没有明确的计划修复日期,这会让你感到非常沮丧(可能还会让项目面临风险)。第三方提供的框架库(如小部件)或插件也不例外,如果你一直使用旧版本,它们与你的应用程序的兼容性会越来越差。对于框架维护者来说,维护向后兼容性已经成为一件非常麻烦的事情。他们发现,开发自动升级代码的工具(Angular 的 ng-update、React 的原生升级助手、Facebook 的 jscodesshift 等)会更有利可图。
  • 需要学习如何使用它们(它们能做或不能做什么、它们的概念、API、生态系统、工具),包括了解在新版本中可能发生的变化。如果你选择的是当前最流行的框架,这可能会容易些,但你不可能了解一个框架的方方面面。此外,炒作也从来不会消停:如果你决定在一个新应用程序中使用另一个框架(或者更糟的是,从一个框架迁移到另一个框架),那么你在旧框架上所有的投入都将归零。这就是为什么很多企业项目会缺乏活力,即使每个项目都可能与前一个项目不一样。已故的 David Wheeler 曾经说过:“保持兼容性意味着有意重复别人的错误”。
  • 将控制权委托给框架,这是对框架缺陷的妥协:你可能无法做任何你想做的事(或防止框架做你不希望它们做的事情)或者你也许不能获得你想要的性能(因为额外的分层、普适性、更大的代码体积或向后兼容性需求)。
  • 技能零散化。很多开发人员要么不太了解底层 API(因为他们总是使用框架提供的东西),要么活在过去(只知道过时的知识,不知道最新的改进和功能)。“工具法则”常常导致过度设计,为简单的问题构建复杂的解决方案,而构建简单解决方案的知识逐渐零散化。在指南的指导下,我们失去了(或者没有获得)好的软件设计(原则、模式)文化,并失去(或者没有获得)构建重要工程的经验。就像 CSS 框架(Bootstrap、Tailwind 等)的用户缺乏 CSS 技能一样,Web 框架的用户也注定缺乏现代 Web API 和软件设计经验。

如何逃离框架孤井?_第3张图片

一旦你把钱放入框架,就很难把它拿出来。要么砸碎他,当然可能最好的方法就是一开始就不塞进去。

框架孤井

除了必须支付“税”费来获得框架的好处之外,如果框架没有标准化,它们还会带来另一个问题。

因为它们强制要求你遵循框架规则——而且每一条规则都不一样——这意味着你的应用程序将与一个专有的生态系统绑定在一起,也就是用专有 API(及其升级过程)锁定你的应用程序代码。这对于你的项目来说是一个冒险的赌注,正如它们所暗示的那样:

  • 没有可移植性:将代码迁移到另一个框架(或者一个有重大变化的新版本,甚至是不使用框架)将是非常昂贵的,包括可能需要进行重新培训的成本;
  • 你的代码与其他框架运行时或你想要使用的其他框架组件库没有互操作性:由于规则不同,大多数框架彼此之间很难实现互操作。

当然,在项目刚开始时,你可以选择最流行的框架。对于一个短期的项目来说,这可能是可以接受的,但对于长期项目来说则不然。

如何逃离框架孤井?_第4张图片

框架来来去去。从 2018 年开始,每年都有 1 到 3 个新框架取代旧框架。

不过,标准框架并不存在孤井。在 Web 平台(即浏览器框架)上,使用标准 Web API 可以降低你的投入风险,因为它们可以在大多数浏览器上运行。即使不是所有的浏览器都支持,仍然可以通过 polyfill 来弥补。

例如,现在的 Web 组件既可移植(几乎可以在所有浏览器中使用),又可互操作(可以被任何代码使用,包括专有框架),因为它们可以被封装成任意的 HTML 元素。不仅具备更好的性能,它们的运行时(自定义元素、阴影 DOM、HTML 模板)还作为浏览器的一部分运行,所以它们已经在那里(不需要下载),并且是原生的。

如何逃离框架孤井?_第5张图片

但很少会有开发者试图逃离框架孤井。

那么框架本质上就是不好的吗?

如果是为实现应用程序逻辑而创建自己的框架,那就不能说框架是不好的:任何应用程序都需要实现自己的业务规则。

如果符合以下这些情况,框架就是好的:

  • 是应用程序特有的:任何应用程序最终都会设计自己的“业务”框架。
  • 成为标准,例如,Web 平台就是一个标准的 Web 框架,而 Web 组件框架(lit、stencil、skatejs 等)最终构建的组件都符合这个标准。
  • 添加一些其他解决方案(包括其他框架)所缺少的独特价值。对于这种情况,你几乎没有选择,这些附加价值证明了隐含的锁定成本是合理的。例如,一个特定于操作系统的框架遵循了操作系统的标准,除此之外没有其他方式可以获得能够满足需求的应用程序或扩展。
  • 用于构建非关键(短期、低质量预期,并且可以接受“税费”和“孤井”)应用程序。例如,使用 Bootstrap 构建原型、MVP 或内部工具。

去框架化的目标

简单地说,避免使用框架来构建应用程序的目标是:

  • 通过避免框架的“一刀切”约束来最大化灵活性。此外,去掉规则的约束,提升应用程序的创造力。大多数使用 Bootstrap 开发的 Web 应用程序都属于此类,因为它们很难摆脱预定义组件和样式,最终将很难从其他角度思考问题。
  • 尽量减少对炒作过度的框架的依赖。不被框架锁定,才能够避免可移植性和互操作性方面的问题。
  • 只在需要时进行最细粒度的操作(例如,不依赖框架的刷新周期),并减少依赖项,只使用一些必需的轻量级库,以此来最大化性能。

当然,我们的目标也不能是“重新发明轮子”。我们来看看该怎么做。

框架之外的选择

那么,如何在没有框架的情况下开发应用程序呢?

首先,我们必须明确一个反目标:不要将“不使用框架构建应用程序”与“取代框架”混淆起来了。框架是一种用于托管任意应用程序的通用技术解决方案,所以它们的目标并非你的应用程序,而是所有的应用程序。相反,脱离框架才有可能让你更专注于你的应用程序。

如何逃离框架孤井?_第6张图片

不使用框架开发应用程序并不意味着要重新实现框架。

要评估在不使用框架的情况下构建应用程序的难度,我们要明白:它不像构建框架那么困难,因为以下这些不是我们的目标:

  • 构建专有的组件模型(实现特定组件生命周期的容器);
  • 构建专有的插件或扩展系统;
  • 构建一个奇特的模板语法(JSX、Angular HTML 等);
  • 实现通用的优化(变更检测、虚拟 DOM);
  • 特定于框架的工具(调试扩展、UI 构建器、版本迁移工具)。

因此,构建一个普通的应用程序并不是一项艰巨的“重新发明轮子”的任务,因为这个“轮子”主要是关于 API/ 合约、实现、通用引擎和相关的优化、调试能力等。放弃通用目标,专注于应用程序的目标,这意味着你可以摆脱大部分目标,而这才是真正的“专注于你的应用程序”。

那么,我们该如何设计和实现一个普通的应用程序?因为大多数应用程序都是使用框架构建的,所以如果没有这些熟悉的工具,确实很难设计出一种方法来实现类似的结果。你必须:

  • 改变你的想法:不要使用特定于框架的服务。对于一个普通的应用程序来说,你可能不需要这些服务。不需要变更检测,直接更新 DOM 即可……
  • 用其他技术替代方案来执行原先使用框架执行的常见任务(更新 DOM、延迟加载等)。

一些作者,如 Jeremy Likness 或 Chris Ferdinandi(被称为“JS 极客”)也提到过这个话题。但是,根据定义,任何一个普通的应用程序都可以选择(或不选择)使用其中的一种技术,具体视需求而定。例如,MeetSpace 的作者只需要使用标准 API 就足以。

接下来,让我们来看看一些常见的“解法”。

标准

标准 API 属于“好的框架”,因为它们:

  • 具备可移植性:它们在任何地方都可用,如果不可用,可以通过 polyfill 的方式实现。
  • 具备互操作性:它们可以与其他标准交互,并被用在专有代码中。
  • 长期存在:由多个行业参与者设计,而不只是一个。它们被设计得很好,一旦发布就会一直存在,使用它们的风险较小。
  • 在大多数情况下在浏览器中都是立即可用的,避免了下载过程。在某些情况下,你可能需要下载 polyfill。但是,与专有框架(注定会越来越不流行)不一样的是,它们的可用性会越来越高(逐渐降低下载的必要性)。

在选择编程语言时,我们要着重考虑标准。JavaScript 经过多年的发展,现在也包含了在其他编程语言中出现的特性,比如 class 关键字和通过 JSDoc 注释(如 @type)提供有限的类型检查支持。

很多编程语言可以被编译成 JavaScript:TypeScript、CoffeeScript、Elm、Kotlin、Scala.js、Haxe、Dart、Rust、Flow 等。它们都为你的代码添加了不同的价值(类型检查、额外的抽象、语法糖)。普通的应用出现应该使用它们吗?为了回答这个问题,让我们来看看它们是否隐含了与框架相同的缺点:

  • 遵循语法:大多数编程语言都强制要求这么做(CoffeeScript、Elm、Kotlin 等)。但需要注意的是,它们是 JavaScript 的超集(TypeScript、Flow),你仍然可以用纯 JavaScript 编写你选择的某些部分。
  • 如果你使用的是非常旧的编程语言(包括 JavaScript)版本,就需要升级,但升级频率比框架低很多。
  • 需要学习它们的语法。不过,你可以循序渐进地学习超集编程语言,因为你的代码的某些部分可以继续使用传统 JS。
  • 对于非超集编程语言来说,离散化技能确实是一个风险。因为它们的编译具有普适性,可能不是最优的,而你可能没有意识到这一点。也许你可以使用更简单和高效的 JS 代码来完成同样的操作。
  • 需要对缺点做出妥协,因为我们无法改变转译成 JS(或者使用 tsconfig.json 做一点定制)或编译成 WebAssembly 的过程。有些语言可能还会忽略 JS 的一些概念。
  • 具备可移植性,因为通常代码可以转译到 ES5(但有时你不得不妥协,即使你想要转译到 ES6)。WebAssembly 很新,所有现代浏览器都支持它。
  • 提供与其他 JS 代码的互操作性。例如,Typescript 可以被配置为支持 JS。

在一个普通的应用程序中,我们要小心谨慎地使用非超集语言,因为它们或多或少都隐含了一些约束。超集语言(TypeScript、Flow)通过避免“要么全有要么全无”来最小化这些约束,我们应该在它们可以带来价值的地方使用它们。

需要注意的是,在 JavaScript 之上构建的语言层意味着我们的工具链中又增加了一层复杂性,可能会因为某些原因招致失败(见下文)。此外,在经过编译或转译之后,开发阶段的好处也会消失(通常在运行时不会强制执行类型或可见性约束检查)。

开发库

基于不“重写框架”的假设,就会得出普通的 JS 应用程序不应该使用开发库的结论。这是完全错误的。“重新发明轮子”,即从头开始重写一切,并不是一个明智的目标。我们的目标是消除框架(而不是开发库)中隐含的约束,请不要将其与“自己编写一切”的教条混淆在一起。

因此,如果你自己不能编写某些代码(可能是因为没有时间,或者因为需要太多的专业知识),使用开发库并没有什么错。你只需要关心:

  • 模块化:如果你只需要一小部分功能,就要避免依赖整个大开发库;
  • 避免冗余:在没有标准的情况下才使用开发库,并优先选择实现了标准的开发库;
  • 避免锁定:不要直接使用开发库的 API,而是把它们包装在应用程序 API 中。

需要注意的是,不要被那些声称它们不是框架的文档或文章所迷惑(因为它们“没有被明确定义”成框架,或者没有定义一个“完整的应用程序”):只要隐含了约束,它们就是框架。

模式

Holovaty 说,只是应用模式(不使用框架)来构建软件是不够的。

模式是众所周知的东西,不特定于某种开发过程。它们本身是自我文档化的,因为它们可以被有经验的开发人员快速识别出来。

这里仅举几个例子:

  • 模型、视图和控制器模式(MVC);
  • 根据配置创建对象的工厂模式;
  • 简化反应式编程的观察者模式;
  • 用于遍历集合的迭代器模式;
  • 用于延迟加载、安全检查的代理模式;
  • 用于封装操作(可能基于上下文被触发)的命令模式。

这样的模式有很多:你可以自由地用它们来满足你的需求。如果一个模式为你的应用程序的一个典型问题提供了典型的解决方案,你一定要用它。更宽泛地说,任何符合 SOLID 原则和具有良好内聚力的东西都有利于应用程序的灵活性和可维护性。

更新视图

在面试开发者时,当被问及在构建一个普通应用程序时他们主要会担心哪些东西时,他们大多数会回答:实现复杂的模型变化检测和后续的“视图”更新。这是典型的“工具法则”效应,它会让你按照框架的思路思考问题,但实际上你的一些简单的需求根本不需要用到框架:

  • “视图”只是 DOM 元素。你当然可以对它们进行抽象(你也应该这样做),但最终它们也只是抽象而已。
  • 更新它们只是调用 viewElement.replaceChild(newContent) 的问题,不需要更新更大范围的 DOM,也不需要重画或滚动。更新 DOM 的方法有好多种,可以插入文本,也可以操作实际的 DOM 对象,只要选一个适合你的就行了。
  • 在普通应用程序中,“检测”什么时候需要更新视图通常是没有必要的。因为在大多数情况下,你只知道在一个事件之后需要更新什么,然后你直接执行这个命令就可以了。当然,在某些情况下,你可能需要通过反转依赖和通知观察者(见下文)来进行一般性的更新。

模板

开发人员不希望缺失的另一个特性是编写带有动态部分或监听器的 HTML 片段。

首先,DOM API(如 document.createElement("button"))并不是那么难,而且实际上比任何模板语言都更强大,因为你可以全面访问这些 API。编写很长的 HTML 片段可能很乏味,如果它们真的很长,可以将它们拆分成更细粒度的组件。

不过,将这些元素视为模板确实可以提高可读性。那么该如何管理它们呢?这里有多种方法:

  • 现在可以在浏览器中使用HTML模板了(实际上从2017年就可以了)。它们提供了构建可重用的HTML