[关闭]
@lsmn 2018-06-12T02:35:25.000000Z 字数 6767 阅读 2530

Angular应用程序生成器架构概述

语言 JavaScript Angular


摘要

在做决定之前,要记住,代码生成既有优点,也有不足,但是,使用什么方法生成Angular的源代码最好:模板,还是AST操作?本文将深入研究技术,基于一种DSL机制实现所生成代码的一致性和可维护性,把Angular源代码生成带到一个新的水平。

正文

本文要点

  • 生成工具开始时必须有一个定义好的范围,而且能够为利益相关者带来真正的价值;
  • 有时候,单是一个基于模板或脚手架的生成器是不够的;
  • Angular平台的架构至少必须提供模板创建(HTML)、组件解析(JavaScript)、Angular元数据及应用程序构建工具的解决方案;
  • 其实现可以结合源代码生成模板解析;
  • Javascript解析可以划分为不同的粒度级别,从而隔离复杂性。

软件自动化是一个有意思的软件开发主题。我第一次接触这类应用程序是在2004年见到Middlegen:这是一款数据库驱动的通用的免费代码生成工具。我记得,我用它完成过下至CMP 2.0层生成、上至JSP/Struts Web页面构建的工作。我那会还没意识到,有些生成工具可以在一眨眼间完成大量的工作。

从那时开始,过了几年,“软件自动化”的主题依然存在于社区中,专家、开发人员、架构师对其有效性持有不同的看法。一方面,它减少了软件构建的时间。所有重复性的工作都可以快速地交付,与此同时,团队可以专注于业务需求的开发。另一方面,如果没有定义好的范围就决定编写一个软件生成工具,那会很困难,而且有危险。

实际上上,在做决定之前,要记住,代码生成显然既有优点,也有不足,但Angular呢?使用什么方法生成Angular的源代码最好:模板还是AST处理?

本文将深入研究技术,基于一种DSL机制实现所生成代码的一致性和可维护性,把Angular源代码生成带到一个新的水平。

为什么要自动化?

乍一看,软件自动化意味着你可以通过标准化重复性的工作(例如CRUD)来节省时间。但是,这里有一个有趣的问题:我们为什么要使用一个通用的软件构建器?

这是没有必要的。我们经常会设法把事情做得更具一般性,因为逻辑、抽象和模式实际上是人类的概念,而缺少经验会导致你做出一些错误的决定。例如,在预见未阐明的决定、未知的问题甚或是解决现在还不存在的未来问题时,有些开发人员通常会选择一种通用的方法

其他人会认为,设法抽象需求,编写几个类的通用代码应该比从软件生成工具的角度来思考要简单;实际上,就是强烈反对软件自动化。但是,事实上,如果他们已经知道应用程序的范围,并且对需求有一个深入的了解,有经验的开发人员就会充分利用软件自动化。当然,我们没有说那是一项简单的工作,但我们非常确定,那是可行的。例如,有个团队正在把一个遗留应用程序迁移到一项新技术,他们可能会有扎实的知识和经验来判断软件自动化是否合适。

为了利用软件生成工具,必须要明确可以自动化的标准,和利益相关者一起确定范围界限,并把它们转换成真正的产品价值。从根本上讲,敏捷思维是构建软件自动化的关键因素。

为什么不能仅仅使用模板?

要回答这个问题,我们要反问:为什么不使用源代码生成?在代码模板和代码生成之间做选择时涉及几个步骤,这可以让我们明白哪条路才是正确的:模板、生成,还是二者兼而有之。

大多数时候,如果我们决定简化源代码生成,遵循一个非常严格的标准,使用一种实用的方式开发一个应用程序,那么,像Angular CLI和Yeoman这样的工具就非常有用。Angular CLI在做脚手架时非常有用。同时,Yeoman生成器可以为Angular提供非常有价值的生成器,并且提供了很大的灵活性,因为你可以自己编写生成器来满足你的需求。不管是Yeoman,还是Angular CLI,都通过它们的生态系统丰富了软件开发:提供大量定义好的模板,用于构建最初的软件框架。

另一方面,仅仅使用模板会很困难。有时候,标准化原则可以转换成几个模板,可以用于许多场景。但是,当要设法自动化有许多变化、布局和字段差别很大的表单时,那就不适用了,这种情况下,会产生无数的模板组合。这只会让人头疼,并引入长期的问题,因为它降低了可维护性,导致了技术债务。有理由认为,软件开发应该以良好的原则为基础,如KISS、YAGNI、模式等。

如果可以混合模板和源代码生成,而不是根据开发人员的偏好选择一种片面的模型,那会很棒。

理解Angular的基本架构

首先,我们必须理解架构的工作原理,抽取出概念,归类到两个关键部分:模板代码和生成代码。

Angular采用了基于组件的架构,其基本结构是HTML模板和TypeScript/JavaScript组件。HTML模板是使用标记语言设计的,有属性,有事件,而组件负责处理这些事件。这些组件是通过元数据管理的,Angular据此可以知道如何处理它们。所有的逻辑服务和组件都封装到模块中。

此处输入图片的描述
图1、Angular的架构

当决定抽象化系统并生成代码时,有必要确定下Angular架构中哪些部分最重要。这里列举下三个关键的因素。

首先,模板是HTML结构的数据,既可以作为HTML实体解析、操作和渲染,也可以作为基于占位符的模板化文件来处理。组件的处理方式类似:解析抽象树或者处理JavaScript模板文件。

其次,对于TypeScript代码,有几个问题:为什么我们不能遍历TypeScript抽象树来生成源代码?我们可以,但是,那会消耗额外的内存,需要更多的处理,因为那总是需要把TypeScript编译成JavaScript代码。另外一个大问题是抽象树处理。可以遍历TypeScript树,但我们的项目期限要求我们用一个强大的库/API通过一种简单的方式遍历和构建JavaScript树。

最后,可能还有一些类似元数据、指令和变量注入器这样的更为细化的事项。时不时地模板化这些东西会很痛苦,而且仅通过模板来维护这些逻辑代码会很困难。

此处输入图片的描述
图2、webpack bundle中的Angular依赖注入

如图2所示,这是一个典型的Angular 5应用程序。首先,TypeScript代码被编译,然后生成的JavaScript代码被打包进一个webpack文件。Angular提供了一组函数,可以将TypeScript元数据转换成有意义的JavaScript代码:

上述三点非常重要,让你在生成源代码时可以通过处理JavaScript抽象树避免一些麻烦。在继续之前,可以通过下面的方法做决定:

变量、参数、逻辑控制越多,就越适合通过树处理。否则,通过模板。

定义一个源代码生成平台

为了创建一个可靠的Angular模板和组件源代码生成架构,合理的做法是选择社区支持并且在不断发展的工具。为了从Angular组件生成代码,需要完成HTML、JavaScript树和模板处理。因此,我们选择了几个库来解决我们的代码生成。

Angular模板

为了处理HTML源代码操作,最好是使用一个可以遍历DOM树并能生成简洁、安全的函数代码的库。后来,我们选择了Cheerio库。Cheerio是一个基于JQuery、用于HTML操作的库。这是一个长期项目,有一个有帮助的开发者及贡献者社区。

此处输入图片的描述
图3、Cheerio操作样例代码

Angular组件

操作抽象JavaScript树是Angular架构的核心。为了实现这项功能而又不引入许多依赖,避免复杂度的提升,保持代码的健壮性,我们选择了Recast以及AST-Types。

Recast是一种读取和写入JavaScript代码的高级API,而AST-Types是一种解析和构建JavaScript抽象树的底层API。

此处输入图片的描述
图4、Recast parse和print高级用法

在图4中我们可以看到,从代码构建树非常简单,反之亦然;AST-Types可以读取JavaScript树的特定节点。Recast可以可以辅助读/写整个应用程序,而AST-Types可以用于操作小部分代码。

此处输入图片的描述
图5、AST-Types使用访问者模式遍历一个函数的返回语句

模板处理

至于模板处理,我们选择了Yeoman,因为它简单。该工具会自动化构建过程以及它们的依赖关系,而且主要是面向Web应用程序。该工具为项目静态部分和生成部分的整合提供了便利,我们可以扩展项目构建过程而不增加复杂度。

选择一个Angular入门工具包对模板加以利用

我们选择了著名的Angular Webpack Starter作为我们的模板样板。该项目的创建者做了一项了不起的工作,Angular Webpack Starter有一个初始设置,整合了最好的库。我们的生成器的基础应用模板就是使用这个入门工具包构建的,那让我们的工作变得更容易,让我们可以把更多的时间花在更复杂的问题上。

遵照最佳实践编写DSL代码

最初编写应用生成器代码时并不简单。在我们最初的场景中,最小可行产品(MVP)包含几个JavaScript模板类,这些类是通过Yeoman模板编排的。这些JavaScript模板类是通过领域类来处理的,为了找出抽象树中的引用并插入代码片段,它们实现了一些访问者函数。

此处输入图片的描述
图6、使用Recast以及AST-Types处理的组件模板

例如,在图6中,构造函数领域类通过一个具体的访问者查找模板的默认构造函数,然后插入功能代码(变量声明、初始化、引用,等等)。下面的例子中有一个访问者函数。

此处输入图片的描述
图7、用于构造函数声明的AST-Type访问者

可以想象,一个有许多模板和组件的大型应用程序会导致性能衰退。那些问题会使Node JS虚拟机退化,因为抽象树遍历的处理成本和内存利用率很高。“自然演进(natural evolution)”会使用一种更优雅更流行的Angular Tree Domain(ATD)替换访问者函数。

从根本上讲,ATD是一个架构概念,是为了隔离复杂性,使Angular组件(包括HTML模板和JS组件)Fluent的功能对生成器透明,如下图所示。

此处输入图片的描述
图7、Angular源代码架构

图7展示了Angular应用程序生成器的总体架构。

Angular应用程序生成器

这是应用程序入口,负责读取元数据并转换成技术数据供ATD使用。我们不会详细介绍应用程序的这个部分,而只是大概地介绍一下。它包含如下组件:

Angular Tree Domain(ATD)

这是架构中最重要的组件。ATD是系统域,包含Angular应用程序构造的核心DSL构建器。这些DSL由Angular模块编排器处理和编排。

Angular模块编排器

这个模块是一个简单的Yeoman生成器,负责连接不同的组件,并编排它们的执行。有一组排好序的“处理器(Processor)”会在一个责任链处理器实现中执行。这些处理器是一些任务,负责处理应用程序的每个部分,如Angular组件和模板、模型、SASS文件和菜单系统更新。我们不会详细介绍这个模块,但是,我们会介绍处理器使用的两个最重要的组件:

Fluent Angular Component API分成三个基本组成部分,下面我们会详细介绍。

DSL构建器

DSL构建器是一组高级构建器(以DSL模式为基础),处理JavaScript组件构造的每一个重要部分。从这个角度讲,大多数Angular开发人员都应该使用这种高级实现进行开发,对于生成器而言,这有助于创建新的组件。

构建器的粒度会随着其在树解析中的职责而增加。例如,Angular组件构建器是根粒度,因为它是入口,通过构建器组合实现整个代码。类构建器的粒度就低一些,它不知道Angular组件构建器的存在,因为后者在树顶。

此处输入图片的描述
图8、Angular组件构建器

如图8所示,Angular组件构造函数调用每个fluent方法构建组件的每个部分。Angular模块编排处理器会向Angular组件构建器的入口发送一条命令。如上图所示,在AngularComponentBuilder的构造函数中是一个有顺序的构建器调用序列。每个构建器各负其责,保证高内聚和低耦合。

因此,每个构建器都有自己的抽象树片段,主AST模型可以在任何时间通过根构建器构建。最终的AST成为语义模型。这种表示法意味着AST模型结果是逐步构建起来的。

稍后,我们还会稍微详细地介绍下,以便更好地理解这个概念,但是现在,我们深入介绍Angular组件的构建,并逐语句看一下ClassBuilder。

此处输入图片的描述
图9、类构建器

在图9中,ClassBuilder引用了一个负责调用AST函数的类,用于创建和解析抽象树。借助桥接模式和组合模式,架构中的所有构建器都把底层实现委托给了语法树类,保证API fluent。

让我们看下addRequire()方法,显然:它创建了一个RequireSyntaxTree类引用,并添加到ClassSyntaxTree引用。然后,它返回构建器的自引用,保证它fluent。任何时候,就像前面提到的那样,AST模型都可以还原,因为所有构建器都持有到其SyntaxTree类的引用。

Figure 10. Syntax Tree abstract class
图10、语法树抽象类

所有SyntaxTree类负责处理树解析的底层代码。随着项目的不断重构,大部分树节点解析都委托给了公共类(如图11所示)。它帮助这些类保证代码的简洁和功能的强大及有意义。下面的代码就展示了这种情况。

此处输入图片的描述
图11、getAst()实现,添加一个REST路径参数

此处输入图片的描述
图12、RequireSyntax类——getAst()返回树表达式

如图12所示,组合这些帮助解析和生成树的功能非常有用。在这个例子中,类“utilsCommon”有一些小功能用于创建属性、变量和数组。

至于类,我们可以把它们描述为AST-Type共享小函数的底层实现,如图13所示:

此处输入图片的描述
图13、两个由它们自己和SyntaxTree类共享的函数

把Fluent API分割到不同的层,实现低耦合,有助于我们进行无数的单元测试,保证整个ATD的一致性。当然,所有的构建器、SyntaxTree、公共/工具类都有单元测试。对于任何类型的JavaScript应用程序而言,像mocha、expect.js和assert这样的库和工具都是一个可靠的组合。在本文的末尾,我们将在图14中展示一个简单的单元测试场景,测试“utilsCommon”函数。

此处输入图片的描述
图14、函数“createVariableRequire”的简单测试用例

为什么要自动化?

最后,我们再回到本文开头提出的问题:为什么要自动化?

实际上,这不是个容易做出的决定。对于许多开发人员而言,“代码生成”这个主题看上去让人兴奋,但对于管理人员和CTO,我们认为情况并非如此。我们始终要记住两点:这种方法的真正好处是什么以及如何汇聚成最终的产品价值?在决定生成源代码时,我们必须思考和争论其优缺点,但是有一点很清楚:团队的成熟度和专业知识有所不同。

关于作者

Jonatas Wingeter Rodrigues是小型IT咨询公司IS Tecnologia的一名高级软件顾问。作为一名现场顾问,Jonatas为巴西南部一个大型商场服务。他从十几岁就开始接触编程。自2002年开始,他大部分时间都在从事软件开发、架构定义、团队指导及领导小型团队,有国内项目,也有国际项目。在业余时间,他喜欢和家人一起亲近自然,学习新语言,如德语和法语。感兴趣的读者可以在Linkedin上和他联系。
Luciano Augusto Yamane是IS Tecnologia的一名高级软件工程师。作为一名现场顾问,他和他的同事Jonatas在不同的技术领域展开合作,如Android、JavaEE、Angular和NodeJS。他有不少于10年的软件开发经验。在业余时间,他喜欢打网球。感兴趣的读者可以在Linkedin上和他联系

查看英文原文:Angular Application Generator - an Architecture Overview

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注