维护大型 JavaScript 应用程序
我们从长期维护大型 JavaScript 应用程序中吸取了教训。
在我们公司,一个客户项目通常持续几个月。从首次联系客户、设计阶段到实施和首次发布,一个项目大约需要半年时间。但有时,我们会花数年时间开发和维护某个特定的软件。
例如,我们于2012年为贝塔斯曼基金会启动了GED VIZ项目,并于2013年正式发布,之后每隔几年都会添加新的功能和数据。2016年,我们将核心可视化功能转化为可复用的库,并对其进行了重大重构。该流数据可视化引擎至今仍被欧洲中央银行(ECB)使用。另一个长期项目是经合组织数据门户前端:我们于2014年开始实施,目前仍在扩展代码库。
在主要开发阶段结束后,我们会修复问题并添加新功能。通常情况下,我们没有足够的预算进行大规模重构甚至重写。因此,在一些项目中,我只能使用 4-6 年前编写的代码和当时流行的库堆栈。
小改进,而不是大改
提到的两个项目都是规模可观的客户端 JavaScript 应用程序。如今,你很少能找到关于如何长期维护现有 JavaScript 代码库的博客文章。不过,你会发现很多文章都是关于使用当下流行的 JavaScript 框架重写前端的。
迁移到一套新的库和工具是一项巨大的投资,但可能很快就会获得回报。它可以简化维护,降低变更成本,加快迭代速度,并更快地实现新功能。它可以减少错误,提高健壮性和性能。最终,这样的投资可能会降低总体拥有成本。
但是当客户无法进行这项投资时,我们会寻找逐步改进现有代码库的方法。
从长期项目中学习
对于一些 Web 开发者来说,被困在现有的代码库里简直是一场噩梦。他们会用“遗留”这个词来贬义地描述那些他们最近没有写过的代码。
对我来说,情况恰恰相反。维护一个项目代码几年,比起几个短命的、一劳永逸的项目,让我学到了更多软件开发的知识。
最重要的是,它让我面对着我多年前写的代码。我多年前做的决定会对今天的整个系统产生影响。而我今天做的决定将决定系统的长远命运。
我经常会想:今天我会做什么不一样的事情?哪些方面需要改进?就像所有开发者一样,我有时也会有想要彻底颠覆一切,从头开始构建的冲动。
但大多数时候,我现有代码中的问题都比较微妙:今天,我会用不同的结构编写相同的逻辑。让我向你展示我在 JavaScript 代码中发现的主要结构问题。
避免复杂的结构
我所说的“复杂”并非仅仅指规模庞大。每个重要的项目都包含大量的逻辑,需要考虑和测试各种情况,处理各种不同的数据。
复杂性源于不同关注点的交织。我们无法完全避免这种情况,但我已经学会了先将关注点分离,然后再以可控的方式将它们重新组合起来。
让我们看看 JavaScript 中的简单结构和复杂结构。
功能
最简单的可复用 JavaScript 代码就是函数。具体来说,一个纯函数,它接收一些输入并产生一个结果(返回值)。该函数将所有必需的数据显式地作为参数获取。它不会更改输入数据或其他上下文数据。这样的函数易于编写、易于测试、易于文档化,并且易于推理。
编写优秀的 JavaScript 代码并不一定需要高级的设计模式。首先,也是最重要的,它需要你能够巧妙有效地运用最基本的技术:用只做一件事的函数来构建你的程序。然后将低级函数组合成高级函数。
JavaScript 中的函数是完全成熟的值,也称为一等对象。作为一门多范式语言,JavaScript 支持强大的函数式编程模式。在我的职业生涯中,我对 JavaScript 函数式编程还只是略知皮毛,但理解其基础知识已经有助于编写更简单的程序。
对象
下一个复杂的结构是对象。最简单的形式是,对象将字符串映射到任意值,没有任何逻辑。但它也可以包含逻辑:函数附加到对象后就变成了方法。
const cat = {
name: 'Maru',
meow() {
window.alert(`${this.name} says MEOW`);
}
};
cat.meow();
JavaScript 中的对象无处不在,用途广泛。一个对象可以用作一个包含多个处理函数的参数包。一个对象不仅可以对关联值进行分组,还可以构建程序结构。例如,你可以将几个类似的函数放在一个对象上,并让它们对相同的数据进行操作。
课程
JavaScript 中最复杂的结构是类。它是对象的蓝图,同时也是生产此类对象的工厂。它将原型继承与对象的创建相结合。它将逻辑(函数)与数据(实例属性)交织在一起。有时,构造函数中会有一些属性,称为“静态”属性。像“单例”这样的模式会用更多的逻辑来重载类。
类是面向对象语言中常见的工具,但它们需要设计模式知识和对象建模经验。尤其是在 JavaScript 中,类的管理非常困难:构建继承链、对象组合、应用混合宏、调用父类、处理实例属性、getter 和 setter、方法绑定、封装等等。ECMAScript 既没有为常见的面向对象编程 (OOP) 概念提供标准解决方案,社区也没有就类的使用达成一致的最佳实践。
如果类具有明确的用途,那么使用类是合适的。我学会了避免给类添加更多关注点。例如,有状态的 React 组件通常被声明为类。这对于特定的问题领域来说很有意义。它们有一个明确的用途:将 props、state 和几个对它们进行操作的函数分组。render
函数位于类的中心。
我不再用更多松散关联的逻辑来丰富这些类。值得注意的是,React 团队正在慢慢地从类转向有状态的函数组件。
同样,Angular 中的组件类是多个关注点的交集:使用@Component()
装饰器应用的元数据字段;基于构造函数的依赖注入;以及作为实例属性的状态(输入、输出以及自定义的公有和私有属性)。这样的类并非简单或单一用途。只要它们只包含必要的 Angular 特定逻辑,它们就易于管理。
选择结构
多年来,我总结了以下指导原则:
- 使用最直接、最灵活、用途最广泛的结构:函数。如果可能的话,最好使用纯函数。
- 如果可能的话,避免在对象中混合数据和逻辑。
- 尽量避免使用类。如果要用,就让它们只做一件事。
大多数 JavaScript 框架都有自己的代码结构。在 React 和 Angular 等基于组件的 UI 框架中,组件通常是对象或类。选择组合而不是继承很容易:只需创建一个新的轻量级组件类来分离关注点即可。
这并不意味着需要拘泥于这些结构来建模业务逻辑。最好将这些逻辑放入函数中,并将其与 UI 框架分离。这样可以分别开发框架代码和业务逻辑。
模块,很多
管理 JavaScript 文件和外部库之间的依赖关系曾经非常混乱。在 9elements,我们早期采用的是 CommonJS 或 AMD 模块。后来,社区最终采用了标准ECMAScript 6 模块。
模块已成为 JavaScript 中必不可少的代码结构。它们带来的是简单性还是复杂性取决于用途。
我对模块的使用方式一直在变化。我以前会创建很大的文件,并进行多次导出。或者,一次导出就是一个巨大的对象,里面包含一堆常量和函数。现在,我尝试创建小型、扁平的模块,只导出一个或几个文件。这样,每个函数一个文件,每个类一个文件,等等。一个文件foo.js
看起来会像这样:
export default function foo(…) {…}
如果您更喜欢命名导出而不是默认导出:
export function foo(…) {…}
这使得单个函数更容易引用和复用。根据我的经验,大量的小文件并不会造成太大的成本。它们可以更轻松地在代码中导航。此外,特定代码段的依赖关系声明也更加高效。
避免创建无类型对象
JavaScript 最棒的特性之一就是对象字面量。它允许你快速创建一个具有任意属性的对象。上面我们已经看到了一个例子:
const cat = {
name: 'Maru',
meow() {
window.alert(`${this.name} says MEOW`);
}
};
JavaScript 对象表示法非常简单且富有表现力,以至于它被转化成如今无处不在的独立数据格式:JSON。但随着 ECMAScript 版本的演变,对象字面量获得了越来越多的超越其初衷的功能。诸如Object Rest/Spread 之类的 ECMAScript 新功能允许更自由地创建和混合对象。
在小型代码库中,动态创建对象是一种高效的特性。然而,在大型代码库中,对象字面量就成了负担。在我看来,在这样的项目中,具有任意属性的对象不应该存在。
问题不在于对象字面量本身,而在于那些不遵循中心类型定义的对象。它们通常是运行时错误的根源:属性可能存在也可能不存在,可能属于特定类型也可能不属于特定类型。对象可能拥有所有必需的属性,甚至更多。通过阅读代码,你无法判断对象在运行时会拥有哪些属性。
JavaScript 没有类型定义,但有几种方法可以更控制地创建对象。例如,可以使用一个函数来创建所有相似的对象。该函数确保所需的属性存在且有效,或者具有默认值。另一种方法是使用一个创建简单值对象的类。
同样,函数可以在运行时检查参数是否可用。它可以使用typeof
、instanceof
等显式地检查类型Number.isNaN
,也可以使用鸭子类型隐式地检查类型。
更彻底的解决方案是使用类型定义来丰富 JavaScript,例如 TypeScript 或 Flow。例如,在 TypeScript 中,首先要为重要的数据模型定义接口。函数声明其参数和返回值的类型。TypeScript 编译器确保只传递允许的类型——前提是所有调用都对编译器可用。
健壮的代码
这些准则涉及代码的整体结构。多年来,我在大型 JavaScript 项目上积累了许多其他技术和实践经验。其中,大多数都影响着 JavaScript 应用程序的健壮性:理解 JavaScript 程序可能出现故障的原因以及如何避免故障。我将这些技术汇编成一本免费的在线书籍:
致谢
感谢9elements 设计师Susanne Nähler创作了预告插图。
学到了什么?与他人分享这篇文章,或
订阅我们的新闻通讯。
最初于 2019 年 1 月 15 日在9elements.com上发布。
文章来源:https://dev.to/molily/maintaining-large-javascript-applications-4aoj