48

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

 6 years ago
source link: http://www.infoq.com/cn/articles/angular-application-generator?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文要点

  • 生成工具开始时必须有一个定义好的范围,而且能够为利益相关者带来真正的价值;
  • 有时候,单是一个基于模板或脚手架的生成器是不够的;
  • 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据此可以知道如何处理它们。所有的逻辑服务和组件都封装到模块中。

6491-1528819173374.png

图1、Angular的架构

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

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

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

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

4982-1528819178550.png

图2、webpack bundle中的Angular依赖注入

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

  • __decorate()函数负责封装整个Angular组件;
  • Component()函数处理HTML模板和CSS。它是作为根/其他模块的占位符,也就是架构中的“选择器(selector)”;
  • __metadata(“design:paramtypes”, …)函数会把一些依赖项注入到相应的构造器参数。

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

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

定义一个源代码生成平台

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

Angular模板

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

4053-1528819174921.png

图3、Cheerio操作样例代码

Angular组件

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

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

3384-1528819174294.png

图4、Recast parse和print高级用法

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

2855-1528819175927.png

图5、AST-Types使用访问者模式遍历一个函数的返回语句

模板处理

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

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

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

遵照最佳实践编写DSL代码

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

2566-1528819175612.png

图6、使用Recast以及AST-Types处理的组件模板

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

2087-1528819173951.png

图7、用于构造函数声明的AST-Type访问者

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

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

1698-1528819178878.png

图7、Angular源代码架构

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

Angular应用程序生成器

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

  • 元数据XML/JSON输入——描述系统模块高级信息的文件;
  • 应用程序规则转换器——负责读取具有有意义的应用程序规则的XML元数据,并在Angular模块的内存数据库中对它们进行转换;
  • 模块数据——包含模块的内存数据库。这些模块是系统模块对象,描述了它们的表单域及其组件和依赖。

Angular Tree Domain(ATD)

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

Angular模块编排器

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

  • Angular模板——负责生成HTML源代码的对象Fluent API;
  • Angular组件——负责生成JavaScript源代码的对象Fluent API。

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

DSL构建器

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

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

1569-1528819177217.png

图8、Angular组件构建器

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

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

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

10710-1528819176540.png

图9、类构建器

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

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

9911-1528819174611.png

图10、语法树抽象类

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

10312-1528819178252.png

图11、getAst()实现,添加一个REST路径参数

9213-1528819176832.png

图12、RequireSyntax类——getAst()返回树表达式

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

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

7214-1528819175315.png

图13、两个由它们自己和SyntaxTree类共享的函数

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

6015-1528819176220.png

图14、函数“createVariableRequire”的简单测试用例

为什么要自动化?

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

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

关于作者

1jonatas-1526414553460.png Jonatas Wingeter Rodrigues 是小型IT咨询公司IS Tecnologia的一名高级软件顾问。作为一名现场顾问,Jonatas为巴西南部一个大型商场服务。他从十几岁就开始接触编程。自2002年开始,他大部分时间都在从事软件开发、架构定义、团队指导及领导小型团队,有国内项目,也有国际项目。在业余时间,他喜欢和家人一起亲近自然,学习新语言,如德语和法语。感兴趣的读者可以在 Linkedin 上和他联系。

1luciano-1526414553255.png Luciano Augusto Yamane 是IS Tecnologia的一名高级软件工程师。作为一名现场顾问,他和他的同事Jonatas在不同的技术领域展开合作,如Android、JavaEE、Angular和NodeJS。他有不少于10年的软件开发经验。在业余时间,他喜欢打网球。感兴趣的读者可以在 Linkedin 上和他联系

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK