维护大型JavaScript应用程序经验教训 - MathiasSchäfer

19-01-17 banq
                   

这是我们从长期维护大型JavaScript应用程序中学到的经验教训。

我们的机构,客户项目通常持续几个月。从第一个客户联系和设计阶段到实施和首次启动,项目大致需要半年时间。但有时我们会在几年内开发和维护特定的软件。

例如,我们于2012年为贝塔斯曼基金会启动了GED VIZ,并于2013年发布,每隔几年就会增加新的功能和数据。2016年,我们将核心可视化转变为可重用的库,对其进行了重构。欧洲中央银行(ECB)今天仍在使用流数据可视化引擎。另一个长期存在的项目是OECD Data Portal前端:我们在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中,它们很难管理:构建继承链,对象组合,应用mixins,超级调用,处理实例属性,getter和setter,方法绑定,封装等.ECMAScript也没有为常见的OOP概念提供标准解决方案。社区是否就使用类的最佳做法达成一致。

如果具有一个定义的目的(banq:标签),则类是合适的。我学会了避免在类上增加更多的顾虑。例如,有状态的React组件通常声明为类。这对特定问题域有意义。它们有一个明确的目的:对属性,状态和两个功能进行分组。该类的核心在于render功能。

我停止用更多,松散相关的逻辑来丰富这些类。值得注意的是,React团队正逐渐从类转向有状态的函数组件

同样,Angular中的组件类是几个关注点的交集:使用@Component()装饰器应用的元数据字段。基于构造函数的依赖注入。将状态作为实例属性(输入,输出以及自定义公共和私有属性)。这些课程根本不是简单或单一目的。只要它们只包含所需的Angular特定逻辑,它们就是可管理的。

选择结构

多年来,我已经达到了这些指导方针:

  1. 使用最直接,最灵活和多函数的结构:函数。如果可能的话,让它成为一个纯粹的函数。
  2. 如果可能,避免在对象中混合数据和逻辑。
  3. 尽可能避免使用类。如果你使用它们,让他们做一件事。

大多数JavaScript框架都有自己的结构代码方式。在基于组件的UI框架(如React和Angular)中,组件通常是对象或类。选择组合比继承更容易:只需创建一个新的轻量级组件类来分离关注点。

这并不意味着需要坚持使用这些结构来模拟业务逻辑。最好将这个逻辑放入函数中,并将它们与UI框架分开。这允许分别发展框架代码和业务逻辑。

模块,很多

管理JavaScript文件和外部库之间的依赖关系曾经是一团糟。在9elements,我们是CommonJS或AMD模块的早期采用者。后来社区决定使用标准的ECMAScript 6模块

模块成为JavaScript中必不可少的代码结构。这取决于它们是否带来简单性或复杂性的用法。

我对模块的使用随着时间的推移而改变。我曾经用多个导出创建相当大的文件。或者,单个导出是将一堆常量和函数分组的巨大目标。今天我尝试用一​​个导出或只有几个导出来创建小而扁平的模块。这导致每个函数一个文件,每个类一个文件,依此类推。文件foo.js看起来像这样:

export default function foo(…) {…}

如果您更喜欢命名导出而不是默认导出:

export function foo(…) {…}

这使得单个函数更易于引用并且更易于重用。根据我的经验,许多小文件不会带来很大的成本。它们允许更容易地在代码中导航。此外,特定代码片段的依赖性更有效地声明。

避免创建无类型的对象

JavaScript的最佳功能之一是对象表示法object literal。它允许您使用任意属性快速创建对象。我们已经看到上面的一个例子:

const cat = {
  name: 'Maru',
  meow() {
    window.alert(`${this.name} says MEOW`);
  }
};

JavaScript对象表示法是如此简单和富有表现力,以至于它变成了一种无处不在的独立数据格式:JSON。但是在ECMAScript版本的过程中,对象表示法获得了越来越多超出其原始目的的功能。新的ECMAScript功能,如Object Rest / Spread,可以更自由地创建和混合对象。

在小代码库中,动态创建对象是一种生产力功能。然而,在大型代码库中,对象表示法成为一种负担。在我看来,具有任意属性的对象不应该存在于这样的项目中。

问题不在于对象表示法本身。问题是不符合中心类型定义的对象。它们通常是运行时错误的来源:属性可能存在与否,可能具有某种类型。该对象可能具有所有必需的属性,但也更多。通过阅读代码,您无法确定对象在运行时将具有哪些属性。

JavaScript没有类型定义,但有几种方法可以更加可控的方式创建对象。例如,函数可用于创建看起来相似的所有对象。该函数确保所需的属性存在且有效或具有默认值。另一种方法是使用一个创建死简单值对象

同样,函数可以在运行时检查参数是否可用。它可以明确地检查使用类型typeof,instanceof,Number.isNaN等或隐式使用duck typing

更彻底的解决方案是使用类型定义(如TypeScript或Flow)来丰富JavaScript。例如,在TypeScript中,首先定义重要数据模型的接口。函数声明其参数的类型和返回值。TypeScript编译器确保仅传递允许的类型 - 假定编译器可以访问所有调用。

强大的代码

这些指南涉及代码的整体结构。多年来,我在大型JavaScript项目中学到了更多的技术和实践。最常见的是影响JavaScript应用程序的健壮性:了解JavaScript程序如何失败以及如何防止它。我在免费的在线书籍中汇编了这些技巧:

强大的客户端JavaScript - 开发人员指南