学习如何避免在你的项目中出现这种情况(虽然它看起来有点酷😉)。图片由 GPT4 | 设计:Tomas Trajan (来自 Medium: Tomas Trajan)
嘿,大家伙儿!Angular 的复兴仍在继续进行,2024 年更是势头更足,Angular 团队带来了诸如基于信号的 input
/ output
/ viewChild
(ren) / contentChild
(ren) 之类的,新的控制流机制、@defer
用于组件的无缝懒加载功能,直到最近在 Angular 18 中发布的预览功能 无区变更检测机制!
这些新的 API 和语法非常令人兴奋,对开发体验、性能以及代码质量都有了明显的积极影响。
不过
与此同时,它们对我们设计应用程序和工作区的方式影响不大。而这是一件好事,因为……
专注于 Angular 架构的知识和技能基本上是持久的,非常值得花时间去掌握!
我们的 Angular 应用程序的结构和架构方式自 Angular 4 时代以来几乎没有什么变化。那时,带来了稳定的 Router
实现,支持按需加载的路由,这一特性是干净架构(不仅仅是性能)中最重要的构建模块之一,直到现在!
当然,API 随时间进行了一些调整,但这些调整主要影响了底层语法,而不是完全改变我们的做事方式。
最好的例子来说明这一点是独立组件和API的问世,这使得NgModules
变得可选。这一变化的影响在于,我们现在不是懒加载功能模块,而是懒加载路由配置(或根功能组件的配置),确保没有遗漏原文中的细节。
有啥不一样吗?当然!
这会改变更高层次的架构是否有所改变吗?一点儿都没有!
我希望这段介绍能定下基调,并且勾起你的好奇心,想更深入了解 Angular 的架构。这样的知识一直都很有用并且将继续保持相关性!这将有助于确保你的项目成功,并为你、你的团队以及所在组织创造价值!
居然完全没有架构(或计划)!在Angular应用架构的背景下,常见的架构错误是什么?
我们经常听到关于快速行动,打破东西的说法,这确实是一件好事,因为它使我们能够迅速响应不断变化的需求,帮助我们的应用或产品保持相关性!
不幸的是,往往被忽视的是,如果希望项目开始一年后仍然能保持快速进展,我们也需要确保项目不会变成一团过于纠缠的乱麻,或者用更专业的说法,不会形成一个充满循环依赖的复杂网络……
是的,这是一个真实项目中的依赖图,所有的线都是依赖关系(文件间的导入),你不想让你的代码基中出现类似的情况吗?想了解更多关于Angular企业级架构?
没有明确的架构概念就开始工作,往往会陷入上述情况。这改变了我们最初的陈述等等。
“快速行动,打破束缚”
更接近于...的某物
试一试改变这一点,看看会发生什么。
有点像这种情况 😅
判断是否遇到这种情况的一个好方法是,当你试图修复一个问题时,会有一种像被蜘蛛网缠住一样的感觉,这会让你陷入一段修改代码的旅程,一个接一个地添加条件,只是为了让它再运行一次。
总之,是的,我们需要考虑架构,以便在整个项目生命周期中“快速前进,打破一些小的孤立部分(而不影响大局)”。
没想过应用中即时和延迟部分的区别在 Angular(和一般的前端开发中),我们尽量减少初始加载的 JavaScript 量,这能显著提升用户的启动体验。
如今的问题不再是网络连接的快慢,而是低端设备的CPU性能不足,难以解析和执行下载的JavaScript代码……
所以很自然地,大多数现有的 Angular 应用会有一些概念,比如 核心模块(预加载)和 功能模块/页面/视图(延迟加载),来反映网页应用和一般网页开发的基本实际。
但是仅仅有概念及其对应的文件夹是不够的,我们还得确保不会在整个项目的生命周期中不小心打破这种隔离。
一个典型的例子是,我们有一个特定功能的服务来管理特定的状态,然后我们意识到能够在急切需要的核心系统的某个服务中也获取到这种状态会非常有用。
这样的话,只需在核心服务中导入并注入功能服务,就很容易忽略这种即时加载和延迟加载的边界问题(或者在PR审查过程中错过)。结果是,该服务(及其导入的所有内容)就会突然变成一个即时加载的JavaScript包的一部分!
这对性能和架构都不利,因为我们引入了本应保持独立的部分之间的依赖关系(或者说,它们之间只能有单向的依赖,例如feature可以依赖核心,但不能反过来)。
随着时间的推移,这再次导致了像第一次错误描述中那样的纠缠不清的依赖关系图情况。
所有功能都不采用懒加载另一个常见的问题是,尽管他们大多数已经接受了急加载/懒加载的划分,并且大多数逻辑已经被实现为懒加载的功能,即使这些项目架构较为合理,但还是有一些功能被忽略了……
在实际使用中最常见的问题有
- 注册 / 登录
- 404 错误页
- 首个功能,如主页或仪表板
最后一个例子是最常见的,也是最糟糕的,尤其是在实际操作中,很容易理解为什么这种情况会出现。
想想想这样一个场景,当我们正在开发一款新应用,并且在处理第一批需求时,我们需要展示一些数据。
我们还没有导航功能,所以我们直接开始创建组件,并在模板中递归使用它们,一直用到根组件AppComponent
为止。
当然,然后,新的需求来了,我们需要增加导航功能,并且新的特性将会作为一个按需加载的特性实现,但是通常没有足够的时间和预算(或意愿)去将原有的功能也改成按需加载的。
现在我们已经有这样的情况,至少有两种方法(一种是急切特性,另一种是惰性特性),甚至我们的隔离和性能也大受影响。
最好的解决方法是始终把所有功能,包括第一个(原始功能),都实现为按需加载的功能。
这笔费用非常小,很快我们就会庆幸自己这么做了!
export const routes: Routes = [
{
path: '',
pathMatch: 'full',
redirectTo: 'dashboard'
},
// 该应用具有单个功能,作为第一个按需加载的功能进行实现。
{
path: 'dashboard',
loadChildren: () => import('./features/dashboard/dashboard.routes.ts')
.then(m => m.routes)
}
]
使用不止一种方式来实现同一个目标
在此基础上,我们应该尽量减少工作中的不同方法。
我们以路由为例,现在至少有四种方法可以做这件事。
- 使用
component
将组件配置为急加载路由 - 使用
loadComponent
将组件配置为懒加载 - 使用
loadChildren
将模块配置为懒加载 - 使用
loadChildren
将基于功能的路由 (feature-x.routes.ts
) 配置为懒加载
在这种情形下,我们最好是选一个并坚持用。
我个人觉得最好总是用loadChildren
来定义懒加载路由,这样更现代且灵活的方式。如果懒加载功能有子导航,我们还可以用loadComponent
来加载额外的组件。
我们也应该这样做,特别是当我们的懒惰功能最初只有一个组件时,因为未来需求很可能发生变化。
提议的方法使我们能够无缝地增加到任何复杂程度,同时在整个项目中保持单一且统一的方式,从而减少认知负担,因为一切都是以同样的方式呈现和处理的。
// app.routes.ts
export const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./features/dashboard/dashboard.routes.ts')
.then(m => m.routes)
}
]
// dashboard.routes.ts (基于懒加载的路由)
export const routes: Routes = [
{
path: '',
loadComponent: () => import('./dashboard.component.ts')
.then(m => m.DashboardComponent)
},
// 未来扩展也很方便,例如
{
path: 'editor',
loadComponent: () => import('./dashboard-editor.component.ts')
.then(m => m.DashboardEditorComponent)
},
// 或者更大的子功能
{
path: 'forecast', // 稍后添加的预测子功能
loadChildren: () => import('./forecast/forecast.routes.ts')
.then(m => m.routes)
}
]
关注DRY而不是隔离:
隔离减少耦合,经常与软件工程中的另一个著名原则DRY背道而驰,DRY即“不要重复自己”(Don’t Repeat Yourself)。
DRY 原则旨在减少代码库中的代码重复,用抽象来代替重复的代码。其核心思想是每一项知识或逻辑都应在系统中只出现一次。
如果同样的信息或逻辑在多个地方重复出现,任何新的需求需要改变其工作方式时,我们都需要在这些地方一致地修改逻辑,这可能会导致错误和不一致的问题。
结果表明,在前端应用中,比起为了减少重复而增加耦合和引入额外的抽象,拥有更多的隔离要重要3到10倍的价值!
我们的观点是,在前端代码库中,适度的代码重复是有益的,因为这将允许它们独立演化以适应变化,随着需求的变化,需求确实会不断变化。
手动分析架构的方式而不是依靠工具在前端,也更常遇到即兴的需求,比如说为少数用户定制特定流程中的一个规则来说。
在前端开发中,遇到非常具体的需求更为常见。因此,这种隔离性和灵活性比单纯消除每一个重复的实例更有价值!
Angular CLI 并没有自带任何出色的工具来分析工作区的架构。
这可能就是这个话题几乎未被探索的主要原因,因此它很少出现在全球 Angular 开发者的在线讨论中。
另一方面,提供了一个很好的工具来分析架构和依赖关系图,通过
nx graph
命令,真的很棒!
手动验证架构的一个示例,比如验证懒加载特性之间的隔离性,是一个非常繁琐且容易出错的过程。
我们可能得……
- 使用我们编辑器中的搜索功能
- 选择一个特定的功能文件夹,例如功能A文件夹
- 然后搜索
../feature-B
(以及其他所有功能文件夹) - 检查功能A是否相对导入了其他兄弟功能,这表明存在一个问题,即兄弟懒加载功能应该相互独立
虽然这样做在技术上是可能的,但除非脑子有问题,否则谁会愿意经常做这样的事呢?
那么有哪些可用的替代选择呢?
麦琪Madge 是一个开发工具,用于生成您模块依赖关系的可视化图,查找循环依赖项,并提供其他有用的信息和建议
— npm Madge 文档
我最喜欢的工具之一,也是最好的,是 madge,它只需一个命令就能绘制出项目的依赖关系图,只需一个命令!
npx madge src/main.ts --ts-config tsconfig.json --image ./deps.png
运行此命令可以分析 src/main.ts
文件,并使用 tsconfig.json
配置文件生成 ./deps.png
图像。
就这样吧! *
请根据您的实际项目路径结构调整路径
它将爬取所有 .ts
文件(包括它们导入的文件等)并生成一个易懂的图表。
✅ 它从左往右看起来整齐有序吗
🔥 它看起来像是醉汉蜘蛛搞出来的一团糟
我们可以用它们来
- 检查代码库的健康状态
- 找出可以改进的地方
- 和不懂技术的同事交流
当你需要解释和沟通重构或清理技术欠债的好处时,这一点非常有用,这些努力能够帮助组织提高整体交付速度,但往往很难合理解释和有效地传达!
ESLint 插件限制eslint-plugin-boundaries
是保持 Angular 应用架构在整个项目生命周期中整洁的最简单有效的方法之一。
它允许我们用寥寥数行配置来定义类型和规则来描述架构,从而简洁地描述我们想要的架构。
这个插件仅依赖文件夹结构,这意味着没有额外的开销,也不需要对应用程序本身进行任何改动。
如上所述的架构设计可以用以下的架构配置来描述……
{
"overrides": [
{
"files": ["*.ts"],
"plugins": ["boundaries"],
"settings": {
"boundaries/elements": [
{
"type": "core",
"pattern": "core",
},
{
"type": "feature",
"pattern": "feature/*",
"capture": ["feature"]
}
]
}
}
]
}
// 说明:此 JSON 配置用于指定文件和插件的边界设置。
有了这些类型,我们就可以定义规则,来规定这种依赖关系图中允许的关系类型。
{
"overrides": [
{
"files": ["*.ts"],
// 这里省略了其他内容
"rules": {
"boundaries/element-types": [
"错误",
{
"default": "不允许",
"rules": [
{
"from": "core",
"allow": ["core"]
},
{
"from": "feature",
"allow": ["core"]
},
]
}
]
}
}
]
}
这样的规则集防止了功能模块之间的导入,这会破坏隔离性,同时也防止了从功能模块导入到核心,这会破坏 eager 和 lazy 之间的界限。
这真的很赞,因为它能自动验证每个合并请求或构建的架构,从而为我们提供了坚不可破的保证,确保整个项目的架构始终保持干净和整洁。
你想不想省时,直接跳过繁琐步骤,直达已被验证的可扩展自动化的Angular架构验证设置?
然后你可以看看我的电子书,里面不仅有各种架构类型的详尽解析及其关系,还有每种类型的详细实现说明。
它还附带了一个可以直接使用的示例仓库,这个仓库可以作为您下一个 Angular 项目的基础模板,或者作为在现有项目中实现这种架构的参考。
点击了解更多关于Angular企业架构电子书!
不考虑依赖图,就这么做.正如我们在之前的多个点中所看到的,清晰的架构和其他架构紧密相连,与我们代码库的基础依赖关系图密切相关。
尽管在单独编辑文件时可能看不出来,但我们应该始终记住幕后发生了什么情况,我们的更改对整个项目有什么影响!
一般来说,我们要确保以下3点总是考虑到,尽量保留。
- 我们希望保持依赖图的单向特性——这与在代码库中保持干净的急切/惰性界限相同,并且可以进一步扩展为惰性子功能可以从父惰性功能导入,但反之则不行。
- 我们希望保持依赖图中独立分支之间的隔离性——这与惰性功能之间完全隔离的概念一一对应着(在同一导航层级上)的同级功能。
- 在更细粒度的层面上,我们希望防止依赖图中出现任何循环——这些循环通常不仅会导致前面两点失效,这使得在我们希望重新利用或提取特定功能逻辑时更难拆分。
不清楚如何共享组件和逻辑代码
在许多代码库中,经常会遇到这样一种情况,通过下面的例子可以更好地解释这个问题……
我们已经实现了两个独立的懒惰特性,现在我们需要再加一个。
没想到,功能A里的一个组件在新功能C里可能挺管用的。
在这种情况下,不幸的是这种情况经常发生,我们直接将特性 A 的独立组件移到特性 C 里,就以为万事大吉了。
应用程序可以运行,但懒加载打包仍然大部分有效。我们引入了功能之间的隐形依赖,因此失去了这些功能之间期望的独立性及其大部分好处的丧失。
单一组件的问题并不是什么大不了的事,但这类情况往往会随时间累积。这再次导致了复杂的依赖关系图,并且无法在不影响功能 C 的前提下修改功能 A,从而降低了我们的效率,经常会引入回归。
那么我们还能做什么?!
在这种情况下,如果我们有一个像 ui
这样的明确定义的概念,用于通用复用组件,正确的方法是从功能 A 中提取所需的组件放入 ui
。
这实际上也意味着我们需要做的是。
- 清理特定功能的逻辑,使该组件变得完全通用(这通常是可以实现且理想的)
- 将组件移动到
ui/
文件夹中 - 分别在功能 A 和功能 C 中导入并集成该组件
之后我们就可以在两个功能中使用新提取的通用模块,而无需担心任何问题。
单向依赖关系图、隔离和因此而产生的干净的架构得到了完全保留!
还没摸透Angular中的两个主要部分及其工作原理在使用 Angular 进行开发时,一切都是由两个主要的底层系统主导的:和
- 模板上下文 — 在组件A的模板中我们可以用什么?
- 注入器层次结构 — 我们要将哪个可注入对象注入到组件A(或服务A)中?
类似于懒加载和JavaScript包的基础现实,这两种系统从Angular的角度来说代表了相同的基础,因此对我们的代码库,特别是架构,都有影响。
架构的影响在于我们如何实现组件和服务,使得它们可以被用在模板中或注入特性中,从而保持架构的整洁性。
一个很好的例子是,通过从 @Injectable()
装饰器中删除 providedIn: 'root'
选项,并改为在懒加载功能路由配置中提供该特定功能的服务,将该服务限定在特定功能上。
export const routes: Routes = [
{
path: '',
providers: [ProductService], // 将服务限定在按需加载的功能模块上
children: [
{
path: '',
loadComponent: () =>
import('./product-list/product-list.component').then(
(模块) => 模块.ProductListComponent)
},
],
},
];
这样,我们就可以避免功能 B 错误地使用原本只应由功能 A 使用的服务。
如果这种要求有效,我们就不得不以一种干净的方式去做,例如将服务从上一级父级懒加载特性中提取出来,甚至一直提取到核心。
不使用单独组件自 Angular 14 版本支持独立组件以来,现在已经超过两年了,NgModules 已不再是必需的。
独立组件,能带来最大的价值,并且无疑是最好的解决方案,尤其是在实现可重用/通用UI组件时,。这些组件仅通过input
和output
进行通信,而不依赖于任何特定的业务逻辑或数据源,而不绑定到任何特定的数据源。
用独立组件代替
NgModules
,这样我们的依赖关系图会更加细化,我们就能更清楚地了解各个部分是如何相互关联的,并能发现更多的问题。
除此之外,它允许懒加载特性仅加载实际所需的UI组件,而不是像以前那样依赖所有组件。当应用程序将这些组件分组并暴露在常用的 SharedModule
中时,这种情况以前很常见。
我希望你喜欢学习 Angular 架构中最常见的 10 个架构错误,并从中找到了至少几条有用的建议,可以在现有的和新的项目中实践,特别是新项目!
如果有任何问题,也不要犹豫与我联系,可以通过文章回复或在 Twitter 的私信与我交流,或者访问 angularexperts.io,我也会在那里等你。
别忘了,未来一定会很光明
显然,光明未来。(📸 图片由 Tomas Trajan 提供)
你喜欢提供的内容,并认为你的团队或组织可以从扩展的 Angular、NgRx、RxJs 和 NX 支持中获益吗?扩展的 Angular、NgRx、RxJs 和 NX 支持是否对您有帮助?我和我的同事,Angular GDE Kevin Kreuzer,我们一起在 AngularExperts.io 提供各种 Angular 咨询服务、工作坊和其他教育产品,您可以去看看。
为企业Angular开发提供高效专家支持 — angularexperts.io
让您的团队变得更强,用经过验证的实战经验让您的项目迅速启动并顺利运行!看看我们提供的各种服务,从研讨会、项目启动支持到按需咨询,解决您的紧急问题——立即联系我们,请与 Angular Experts 联系!
共同学习,写下你的评论
评论加载中...
作者其他优质文章