发布于:2021-02-05 14:50:20
0
242
0
我们从长期维护大型JavaScript应用程序中学到的经验教训。
在我们公司,客户项目通常持续几个月。从第一次与客户接触到设计阶段,再到实施和初始启动,一个项目大约需要半年的时间。但有时我们在几年的时间里开发和维护一个特定的软件。
例如,我们在2012年为基金会启动了GED-VIZ,2013年发布了GED-VIZ,并每隔几年添加新的功能和数据。2016年,我们将核心可视化转变为可重用库,对其进行了重大重构。如今,央行仍在使用流量数据可视化引擎。另一个长期项目是OECD数据门户前端:我们于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中最复杂的结构是类。它是对象的蓝图,同时也是这些对象的工厂。它将原型继承与对象的创建相结合。它将逻辑(函数)与数据(实例属性)交织在一起。有时构造函数上有一些属性,称为“静态”属性。像“singleton”这样的模式用更多的逻辑重载了一个类。
类是面向对象语言中的常见工具,但它们需要设计模式的知识和对象建模的经验。尤其是在JavaScript中,它们很难管理:构建继承链、对象组合、应用mixin、超级调用、处理实例属性、getter和setter、方法绑定、封装,ECMAScript既没有为常见的OOP概念提供标准的解决方案,也没有为社区提供关于类使用的最佳实践。
如果类有一个明确的目的,那么它们是合适的。我学会了避免在课堂上增加更多的关注点。例如,有状态的React组件通常被声明为类。这对于特定的问题域是有意义的。它们有一个明确的目的:将道具、状态和对两者都起作用的几个函数分组。类的中心是render
函数。
我不再用更多松散相关的逻辑来丰富这些类。值得注意的是,React团队正在慢慢地从类转向有状态的功能组件。
同样,Angular中的组件类是几个关注点的交叉点:使用@Component()
装饰器应用的元数据字段。基于构造函数的依赖注入。状态为实例属性(输入、输出以及自定义公共和私有属性)。这类课程根本不是简单或单一的目的。它们是可管理的,只要它们只包含所需的特定于角度的逻辑。
选择结构
多年来,我一直遵循以下准则:
使用最直接、最灵活、最通用的结构:函数。如果可能,让它成为纯函数。
如果可能,避免在对象中混合数据和逻辑。
如果可能,避免使用类。如果你使用它们,让它们做一件事。
大多数JavaScript框架都有自己的代码结构。在基于组件的UI框架(如React和Angular)中,组件通常是对象或类。选择组合而不是继承很容易:只需创建一个新的轻量级组件类来分离关注点。
这并不意味着需要坚持这些结构来建模业务逻辑。最好将这个逻辑放到函数中,并将它们从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版本的过程中,对象文本获得了越来越多超出其最初用途的特性。新的ECMAScript特性,比如objectrest/Spread,允许更自由地创建和混合对象。
在一个小的代码库中,动态创建对象是一个高效的特性。不过,在大型代码库中,对象文字成为一种负担。在我看来,具有任意属性的对象不应该存在于这样的项目中。
问题不在于对象本身。问题是不符合中心类型定义的对象。它们通常是运行时错误的来源:属性可能存在或不存在,可能有某种类型或没有。对象可以具有所有必需的属性,但也可以具有更多属性。通过阅读代码,您无法判断对象在运行时将具有哪些属性。
JavaScript没有类型定义,但是有几种方法可以以更受控制的方式创建对象。例如,一个函数可以用来创建所有看起来相似的对象。该函数确保所需的属性存在且有效,或者具有默认值。另一种方法是使用创建死简单值对象的类。
同样,函数可以在运行时检查参数是否可用。它可以使用typeof
、instanceof
、Number.isNaN
等显式检查类型,也可以隐式使用duck类型。
一个更彻底的解决方案是用类型定义来丰富JavaScript,比如TypeScript或Flow。例如,在TypeScript中,首先定义重要数据模型的接口。函数声明其参数的类型和返回值。TypeScript编译器确保只传递允许的类型,因为编译器可以访问所有调用。
作者介绍