重新思考 Web (RTW) - Web 颠覆
将工作放在工作者身上
引言/回顾
Web 开发比实际需要更复杂。一个原因是命名冲突的规避。另一个是管理 CSS 级联的复杂性。造成这些和其他因素的原因是“东西太多”因素。
我们使用的很多实践和方法都是在 Web 早期开发的。当时 Web 速度慢且不可靠,工具薄弱,许多标准的功能支持有限甚至没有。页面是“一次性加载”(所有内容预先加载)。链接的资源一起加载,都在同一个命名空间中。你了解这些问题和解决方法。每次技术进步似乎都增加了问题——CSS 文件越来越大,脚本也越来越大。更多的脚本,更多的文件…东西太多了!
最简单的形式,浏览器只是一个查看器。任何当前不在视图中的东西都是多余的包袱。与早期相比,技术格局已经发生了根本性的变化,但我们的方法没有。我们仍然使用一个大型文档、以文档为中心的方法,里面充满了多余的内容。我称之为“以防万一”的方法:提前加载你需要的所有东西,甚至更多(以防万一)。但是,借助现代技术,可以轻松地“按需加载”,从而实现更快、更动态、更易于管理的 Web 应用程序。
代码单元
在深入探讨“东西太多”之前,我想简要介绍一下“代码单元”。
最简单的形式,代码单元 (UOC) 是一系列指令,它们将执行完毕而不会延迟或中断,并表达一个单一的、可识别的目的或“想法”。
简化来说:连续的、相关的代码行,不进行分支(包括调用和 new
)或阻塞,我们可以给它们命名。**片段** 和 **指令** 与 UOC 相关。
UOC 是一个“基本粒子”——所有代码都可以根据 UOC 的分组和层级来表达。一个“最简单”的 UOC 价值有限——无法创建任何东西或调用任何东西的代码没有多少令人信服的用例。如果你还记得那个**机器**的东西,我们可以这样看待——**机器**(抽象),**工作区**(下方),**指令**(UOC 步骤)。一个 UOC 机器分三个步骤运行:
- **初始化** - 初始化设置环境。它设置变量,并且可以从外部获取值。它可以获取对象,但在 UOC 中使用对象(读取其值)需要某种中断/调用。必需的值必须提取到本地值类型中。
这就是**工作区**。 - **运行** - UOC 运行,改变本地值类型。这是“不间断运行”的部分。
重新审视工作:工作是消耗能量的资源,用于执行指令,以实现期望的**变化**。 - **改变资源** - 这是“**期望的变化**”。为了有用,我们需要改变外部的某些东西。(技术上来说,我们告诉资源去改变。我们无法改变我们不拥有的东西。)
这里的工作/能量包括经典物理学中的相同工作/能量概念。大多数标准方程都适用。方程用值|度量集(4 | 米)表示。创建 UOC 的一个目的是提供一个“软件标准单位”,用于开发可度量的、用于这些和其他方程的工作/代码的维度。这对这里来说不是非常重要,但对规划和管理至关重要。许多方法将管理/剖析指标“置于”方法论之上。在这里,它从根本上就融入了其中。
所有软件都是 UOC 的组合,(每个)都有定义的、标准的、可度量的方面。
如果所有“部分”都是可度量的,那么“整体”应该是可度量的,并且任何部分也应该是可度量的。UOC 提供的指标比“代码行数”更有用。
一个 UOC 机器可以(部分地)用下图表示。“代码单元”被替换为名称/标识符。数字(此处为 1)定义了层级中的位置。层级的两种格式是点式(1.1.2.3)或斜杠式(/1/1/2/3/)。
这个最后的 UOC 部分只是信息性的,但值得介绍。
我们可以从两个角度定义一个 UOC:
- **方法**角度——“用这些东西做好你的工作,然后给我结果”。
- **变化**角度——“以这种方式改变 X、Y 项目”。
“洗碗并把它们放好。” - 方法
“将水槽里的脏盘子变成架子上的干净盘子。” - 变化
让我们来看看这些盘子的 UOC 视角。
方法视角
方法视角通过**动作**来指定。
动作由**签名**识别。签名包含一个应该描述其功能的名称、输入列表和一个(可能是复合的)返回项;
方法视角查看器知道两个“动作”——洗碗…、放好…。
它启动动作并处理结果。这是一种**推送**方法。
要知道比公共接口更多的信息,查看器必须查看内部或事先了解。
签名没有说明它改变了什么。
**函数式分析**有一个**纯函数**的概念——一个只返回值而不改变任何东西的函数。它“没有**副作用**”。
使用方法视角
Dishrack.Dishes = WashDishes(Sink.Dishes);
Shelf.Dishes = PutAway(Dishrack.Dishes);
变化视角
变化视角通过**变更**来指定。
它只能改变现有资源,从不创建。
变更也由签名识别。
签名定义了将被读取和/或更改的项目以及任何返回值。
变更也有一个**纯变更**,它只改变而不返回任何东西。
CP 查看器看到“数据端口”。它响应数据端口上的变化。
它看不到(或无法)启动过程。
变更具有响应性,并且可以被样式化为**拉取**过程。期望的更改由使用它的代码/进程的“期望”指定。
使用变化视角
result/void = DoDishes(in Sink.Dishes, out Shelf.Dishes);
融合
这两种方法都不能提供工作的全面视图。但是,非纯版本比纯版本隐藏了更多信息。UOC 是纯版本的组合。
方法方面:仅用于流程控制。不返回业务数据,仅返回状态信息。
变更方面:仅改变资源。
还有更多内容,但对于本文而言,重要的是我们可以从代码单元和“分组”的 UOC 中组合越来越复杂的软件结构/组件。
UOC 在此及其他文章中稍后出现。
求逆
现在,关于那些东西…
Web 颠倒是一个颠覆 Web 架构某些方面的概念。本节将介绍颠倒的概念。
Current
一个基本的 Web 应用程序看起来是这样的:
浏览器对 Web 服务器发出 GET
请求,服务器返回一个**HTML 文档**。文档包含大量内容。但显然不够。链接会拉取更多内容。除非你拥有链接,否则链接是一件冒险的事情。在过去几年里,有几起关于链接中断导致数千个网站瘫痪的事件曾登上世界新闻。我认为最能说明当前 Web 开发状况的是,当一位开发人员撤回他的“库”代码时,成千上万的网站(包括一些大公司的网站)瘫痪了。他**一行代码**的库。复制粘贴别人代码的开发人员已经够糟糕了,但至少他们看到了他们将什么放入了他们的应用程序,并“使其成为自己的”。盲目链接很糟糕,但链接到一行外部代码是不可原谅的。
这有很多变种。为一个单一功能链接一个大型库,或者在 C# 中 using
整个命名空间来使用一个小的、解耦的类——如果是开源的,请复制并注明来源!让代码属于你自己。你摆脱了强制性更改及其影响。评估源文件的更改并相应地进行整合。
我们在浏览器中放置的内容有不同的影响。有些只是占用更多内存和带宽。有些会影响后台处理。有些会窃取主线程的资源。事实上,几乎所有东西都会窃取主线程的某些东西。
随着 GUI 变得越来越动画化和交互化,主线程的负担越来越重。客户找到你,说:“给我做一个漂亮的动画应用程序,实现这些业务功能。哦,而且要以每次运行约 10 毫秒的代码块形式交付。”这个限制听起来是否熟悉?随着 CSS 添加越来越多的复杂“随时间过渡”,浏览器除了其他功能外,还在变成视频播放器。它们内置支持,一直到帧定时时钟。为了防止视频卡顿,你的业务逻辑只能在浏览器不需要线程的时间里使用它,在每一帧——几毫秒。
我们需要将东西移出主线程。可以做到这两点是:
- 删除我们当前不需要的东西。
- 转移处理。
我们可以一起做到这两点。
中级
有几种方法可以移除代码,也有几个地方可以移动代码。代码的移动有优先级——减小 DOM 大小提供了部分最佳收益。
一种将此从 main
移出的方法是使用**Web Workers**。Worker 通常使用**远程过程调用**方法。但是创建 Worker 并不便宜——你不会将每个函数都移到一个单独的 Worker 中。有些代码比其他代码移动得更好。Worker 不能使用 Window DOM。这是有道理的。但是 Worker 没有 HTML 甚至 XML 的概念。没有 XML 支持是没有道理的——并非所有层级都是 HTML 文档,也并非所有 HTML 文档都是“实时”的。
代码可以移出 DOM,但仍然保留在 Window/EC 中。我们可以创建由服务器进程支持的 WebAPI,而不是浏览器执行进程,来提供帮助。
Web 内容分为三类:HTML、JavaScript、CSS。
所有这些都有主要的子类别。
- 标记 (HTML)
- 自定义元素 (HTML/JS)
- 脚本 (JS)
- 类 (JS)
- 骨架 (CSS)
- 皮肤 (CSS)
我避免将影响盒模型布局的 CSS(骨架)与样式/主题(皮肤)CSS 混合。尤其是在使用 CSSOM 和动态样式时。
我们可以围绕这些项目创建 WebAPI。我使用**元素**、**类**、**样式表**和某种**控制器** API。但没有标准或框架。我们可以将代码移入 API 实例。空闲代码仍然在浏览器中,但不在关键路径上。通过 API,我们可以将其从浏览器移到服务器,按需发送到浏览器。一些代码可以被**移动**并在服务器上**执行**,通过实时连接同步。Web 服务器可以托管逻辑(显示业务服务),或者它可以移到 Web 进程外的服务。
整个文件的内容可以被移动,但我们也可以只发送**“仅需要的东西,仅在需要的时候”**。服务器通过实时地将动态构建的“片段”移入和移出 API 实现来平衡浏览器的负载。要做到这一点,我们需要一种好的方法来“管理这些片段”。
软件制造
我在制造环境中花费了大量时间。毫无疑问——**软件开发是一个制造过程**。不是艺术或科学!(好的,它是混合的。但它不是“手艺”,除非你是一个前工业时代的“代码鞋匠”)。在其他文章中,我将展示**精益制造**(又名丰田)方法如何比**阶段门控**(又名**瀑布**)或**敏捷**(又名(对我而言)**自由落体**)方法更适合现代(尤其是业务)软件。你可能见过“**看板**”这个词与敏捷方法相关。看板直接源于精益制造,尽管略有误用。
题外话
“瀑布”一词是阶段性方法的贬义词。阶段性方法是迭代的,但覆盖整个阶段——如果下一阶段不接受你的工作,你就要重新做。瀑布一词描述了那些只是“把工作扔下瀑布”的组织。下一阶段无法“爬上瀑布”让你重做。瀑布不是一个“失败的方法”。它是关于失败的组织。
我创造了“自由落体”一词作为敏捷的等价物。宣言明确地将设计视为“真实的事物”,即正式的设计文档,只是轻量级的。这些作为锚点/参考。在每次迭代中,你都会更新设计文档以反映当前代码(创建当前状态),计划下一个冲刺,并更新一个未来状态规范。然后,你按照增量的未来状态进行构建。重复。当你听到有人说,“软件就是设计”时,这个组织就失败了,自由落体也随之而来。你永远无法达到一个不确定且移动的目标。当没有什么可以“抓住”时(没有参考设计)——你就处于自由落体状态。
失败组织的另外一个迹象是那些使用“敏捷项目管理”之类的术语的人。想想看。
阶段性和敏捷性都有其优点,但服务于不同(且互补)的目的——阶段性是宏观的,而敏捷性在阶段内的更细粒度的工作中效果很好。
零件
Web 服务器正在向浏览器发送“部分”。代码单元代表最小的有意义的“可执行”文件(不是 .exe)。代码单元是我们拥有的最小**零件**。它有一个**零件编号**。代码可以指定为分层的**物料清单 (BOM)**,其中物料是代码单元和由 UOC 组成的组件。UOC 可以被“混合进入”——有序地放置在容器(例如,方法)内的 UOC,或用作容器的整个主体。
一个**抽象语法树 (AST)** 就是一个 BOM。如果“代码结果”是一个 BOM,那么早期就开始处理 BOM 不是很有意义吗?
整个树可以实时地从下往上构建,但这效率不高也不必要。在每个节点,节点 UOC 和子节点被组装成一个缓存的零件。
例如,**片段**(由**指令**标识)就是一个 UOC。我们可以使用更好的指令。
//::Class:Context
class Context {
static raw = "";
static state = new Map();
static get State() { return this.raw; }
}
//::Element:Niche
class Niche extends HTMLElement {...}
我们可以发送每个小零件,但这可能会有很多发送。更好的方法是“缓存”。一个**BOM 节点**保存 BOM 和组件。
节点 BOM
<part name="assem" pn="xxxx" rev="current">
<part name="Context" pn="xxxx" rev="current"/>
<part name="Niche" pn="xxxx" rev="54.2.1"/>
</part>
节点组件
class Context {
static raw = "";
static state = new Map();
static get State() { return this.raw; }
}
class Niche extends HTMLElement {...}
一旦构建完成,一个组件只需要在它的一个零件发生变化时才需要重新构建。当一个零件发生变化时,它会贯穿整个代码。精益原则包括:“内置质量”。每个节点都有内置的测试,涵盖(仅)其行为。如果一个节点检测到 BOM 更改,它会重新组装并运行测试(根据需要更新)。
组件中每个更新并通过测试的节点都会通知使用它的节点。这些节点也做同样的事情。变化会逐级传播,并在每个级别进行测试。很容易识别故障点。这一切都应该是自动化的,因此传播几乎是即时的。如果我们不需要特定版本的零件或特定的零件编号,我们可以使用“current
”。我们可以构建代码的当前版本或任何历史版本。没有“单一版本”,而是版本化零件的 BOM。每个节点都可以从其零件的“current
”版本、任何编号版本,甚至是一次性定制版本中提取。
这种方法适用于任何开发,不仅仅是 JIT 场景。有简单的方法来管理所有这些,包括一个新的 IDE/扩展,可以在模拟/测试模式和开发模式之间进行实时切换。
以这种方式管理脚本很简单,但编译型语言更复杂。我已经确定了一些我认为可以显著改进这一点的地方。
颠倒
在完全颠倒的模型中,Window 只是一个显示和事件源。
BIOS 立即创建一个 WebWorker
并将控制权交给 Worker。Worker 与服务器通信并运行所有业务逻辑。它更新 Window。Web 服务器被降级为一个简单的桥梁,连接服务器逻辑和 Worker 客户端逻辑——一个单一进程的两个部分,具有“水平”对齐,能够同步或异步操作。
Worker 通过**DOM 驱动程序**(通常是 JIT)更新 Window。你无法将所有内容都移出 main
线程(但接近),并且有些东西确实属于 Window。业务代码和显示代码各自有自己的线程。Worker 可以生成其他 Worker,因此这可以很好地扩展和多线程。
错误的东西
在完成所有这些工作并试图让各个部分契合时,我意识到了一件事。
我们管理错误的东西
编码和建模如今是模糊的。我们对模型的利用比我们意识到的要多。
模型有多种形式。模拟和天气模型是基于代码的模型。TypeScript 是 JS 的一个模型,它被渲染成 JS。当你从模板开始一个项目或填充一个模板时,你就是在使用模型。一旦你开始观察,模型无处不在。建模甚至通过**源生成器**集成到构建管道中。生成器是一个在其他代码被处理之前被渲染的模型。
如果我解释清楚了,你应该能看到,有了 BOM,我们**不需要**像**命名空间**或**源文件**这样的东西。我们当然需要**编译单元**,但它们需要是静态文件吗?它们需要有多“人类可读”?
一个**物料主表**是所有零件的扁平列表,一直到 UOC,以零件编号为键。这才是我们真正需要的。其余的是配置管理。我们的源文件可以是虚拟的(**虚拟源文件**),由存储在数据库(或类似)中的零件组成,并在 IDE 中即时构建。当我们不需要过多担心命名和命名空间时,许多结构/组织决策会变得更容易。它们不会消失,但它们会采取不同的形式和目的。
我们不应该管理静态源文件。
**我们应该管理(我们)虚拟源文件的来源**。
供思考
如果我们接受,要得到一个源文件,很大一部分是建模,那么管理静态源文件有点像渲染动画的第一帧,然后手动完成其余部分。将这个想法进一步推进一步(并在另一篇文章中探讨)是让一切都“建模”。
而不是数据库或源文件,所有零件/UOC 都可以作为模型项存储——好的工具支持完整的版本控制、后端数据库、模拟、分析等。要实现难以捉摸的**可视化编码**,只是一步之遥。
这(大部分)消除了源代码的“人类可读因素”,使其可能更有效率。
历史
- 2022 年 9 月 22 日:原始版本