在 Angular v13.2.0 中,我们发布了扩展诊断,这是 Angular 编译器中的一项新功能,可以让您更深入地了解模板以及如何改进它们。这些诊断为您的模板提供编译时警告和精确、可操作的建议,在您发现错误之前捕获它们。
以下面的示例为例,它使用双向绑定来显示消息“我最喜欢的水果是:香蕉”。
@Component({
selector: 'app-invalid-banana-in-box',
template: `
<h2>Invalid banana in box</h2>
<app-favorite-fruit ([fruit])="favorite"></app-favorite-fruit>
`,
})
export class InvalidBananaInBoxComponent {
favorite = 'banana';
}
@Component({
selector: 'app-favorite-fruit',
template: `<div>My favorite fruit is: {{fruit}}</div>`,
})
export class FavoriteFruitComponent {
@Input() fruit: string = '';
@Output() fruitChange = new EventEmitter<string>();
}
您可能会惊讶地发现这不起作用。 “Banana”被丢弃并且不显示。
发生了什么?代码看起来非常合理。然而,使用 v13.2.0 构建它现在会显示编译器警告:
Warning: src/app/app.component.ts:5:25 - warning NG8101: In the two-way binding syntax the parentheses should be inside the brackets, ex. '[(fruit)]="favorite"'.
Find more at https://angular.io/guide/two-way-binding
5 <app-favorite-fruit ([fruit])="favorite"></app-favorite-fruit>
~~~~~~~~~~~~~~~~~~~~
此警告指出了确切的错误:双向绑定语法是向后的。事实上,这实际上是一个事件绑定,恰好被命名为[fruit]
。这在技术上是有效的,并且可以使用该方案来命名事件;然而,这几乎肯定不是这里想要的。扩展诊断旨在识别这样的模式,这些模式实际上是有效的,但几乎肯定不是开发人员想要的,并且可能会导致错误。
此警告还建议修复。对于双向绑定,香蕉(括号)应该放在盒子(方括号)内,并将其写为[(fruit)]="favorite"
按最初的预期工作。向后使用此语法是一个常见的错误,并且在更复杂的应用程序中使用它很容易花费几个小时的调试时间。这种扩展的诊断可以在浪费宝贵的调试时间之前发现错误。
Angular v13.2.0 还包括编译器在遇到具有不可为空输入的空合并运算符 ( ??
) 时发出的另一个警告。在这种情况下不需要操作员,因为用户永远不会看到右侧。这可能不是开发人员的本意,但却是代码清理的绝佳机会。编译器利用其对程序类型的理解将输入识别为不可空(类型不包括null
或undefined
)。
查看文档以获取有关这些诊断的更多信息。
扩展诊断通过ng build
和ng serve
以及Angular Language Service显示在编译器中。默认情况下,它们会作为警告发出,以便在不需要时不会中断集中的编辑/刷新循环。由于??
导致您的构建被破坏可能会非常烦人。不喜欢你删除了一个null
并且还没有抽出时间来修复它。扩展诊断的目的是在不破坏此流程的情况下及早发现开发人员的错误,而警告是一个有用的中间立场。
也就是说,开发人员可能希望自定义扩展诊断,因此 Angular 支持通过tsconfig.json
文件进行配置。您可以为每个特定诊断选择类别( warning
、 error
或suppress
),并为所有其他诊断选择默认类别。在这种情况下,我们抑制无效合并检查以完全忽略它,同时使所有其他诊断(框中无效的香蕉)成为阻止构建的硬错误,因为该错误非常常见。
// tsconfig.json
{
"angularCompilerOptions": {
"extendedDiagnostics": {
// The categories to use for specific diagnostics.
"checks": {
// Maps check name to its category.
"nullishCoalescingNotNullable": "suppress"
},
// The category to use for any diagnostics not listed in `checks` above.
"defaultCategory": "error"
},
// Other Angular options...
},
// Other TypeScript options...
}
有关配置诊断的更多详细信息以及检查名称的完整列表,请参阅我们的文档。
一种常见的模式是将某些诊断视为本地警告,以保持开发人员流程,同时将其视为持续集成 (CI) 系统中的错误。忽略不需要的可以吗??
操作员正在调试另一个问题,但项目最好在合并代码之前修复它。
Angular CLI 使这种模式的设置变得很容易。它支持单个构建器的多种配置,这通常是用户管理开发与生产构建的方式。我们可以为 CI 构建添加新的配置,并进行更严格的类型检查。所需要做的就是更新angular.json
文件:
{
"projects": {
"my-project": {
"architect": {
// `ng build` options.
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
// Use `tsconfig.app.json` by default.
"tsConfig": "tsconfig.app.json",
// Other options...
},
// Configurations we can build with the `-c` flag.
"configurations": {
"production": {
// Production build options.
},
"development": {
// Development build options.
},
+ "ci": {
+ // CI build options. Use a different `tsconfig.json`.
+ "tsConfig": "tsconfig.ci.json"
+ }
}
}
}
}
}
}
这意味着ng build -c ci
将使用tsconfig.ci.json
进行类型检查。我们需要做的就是使用更严格的诊断设置来创建此文件:
{
// Reuse all the settings in `tsconfig.app.json`.
"extends": "./tsconfig.app.json",
// Override just the one option we care about for CI.
"angularCompilerOptions": {
"extendedDiagnostics": {
// Make all extended diagnostics emit hard errors.
"defaultCategory": "error"
}
}
}
现在,如果发出任何扩展诊断, ng build -c ci
将失败并出现硬错误,但ng serve
和ng build -c development
会将它们视为警告,以避免阻碍本地开发工作流程。更新您的 CI 系统以使用ng build -c ci
,您可以确信所有合并的 PR 都遵循这些最佳实践。
虽然这是一个强大的工具,但请记住,Angular 团队可能会在次要版本中添加新的扩展诊断,并且defaultCategory
将适用于它们。将这些诊断视为错误可能会在次要版本更新中引入错误,因此在使用defaultCategory: "error"
之前请仔细考虑。请参阅文档以获取更多信息。
我们从两项检查开始,但希望将来添加更多检查。扩展诊断系统的设计易于扩展,使未来的检查可以轻松编写。诊断使用一致的界面来检查给定的 Angular 模板,如下所示(为了清晰起见,简化了代码片段并进行了注释):
// template_check.ts
/**
* Interface for a check which processes an Angular template and
* generates an array of diagnostics found in it.
*/
export interface TemplateCheck<Code extends ErrorCode> {
/** Error code the diagnostic will emit. */
code: Code;
/**
* Processes the given Angular component and its template's
* abstract syntax tree, returning any diagnostics found.
*/
run(ctx: TemplateContext<Code>, component: ts.ClassDeclaration,
template: TmplAstNode[]): NgTemplateDiagnostic<Code>[];
}
查看无效香蕉盒诊断的源代码:
// invalid_banana_in_box.ts
/**
* An implementation of the above interface for an invalid banana in
* box. It extends a base `TemplateCheckWithVisitor` class which is
* responsible for visiting every node in the template's abstract
* syntax tree and calling `visitNode()` to check it.
*/
class InvalidBananaInBoxCheck extends TemplateCheckWithVisitor<ErrorCode.INVALID_BANANA_IN_BOX> {
// The error code this check will emit.
override code = ErrorCode.INVALID_BANANA_IN_BOX as const;
/**
* Called for every node in the template's abstract syntax tree.
* Returns an invalid banana in box diagnostic if found.
*/
override visitNode(
ctx: TemplateContext<ErrorCode.INVALID_BANANA_IN_BOX>,
component: ts.ClassDeclaration,
node: TmplAstNode|AST,
): NgTemplateDiagnostic<ErrorCode.INVALID_BANANA_IN_BOX>[] {
// Remember: The mistake occurs from `([foo])="bar"`, which is
// interpreted as an event binding with the name "[foo]". We only
// care about event bindings nodes, since that is the only way
// this error can happen.
if (!(node instanceof TmplAstBoundEvent)) return [];
// Look for an event binding with a name like `[foo]`.
const name = node.name;
if (!name.startsWith('[') || !name.endsWith(']')) return [];
// Found an event binding with a `[foo]` name! That's probably a
// mistake, generate a useful error message and suggest a fix.
const boundSyntax = node.sourceSpan.toString();
const expectedBoundSyntax = boundSyntax.replace(`(${name})`, `[(${name.slice(1, -1)})]`);
const diagnostic = ctx.makeTemplateDiagnostic(
node.sourceSpan,
`In the two-way binding syntax the parentheses should be inside the brackets, ex. '${expectedBoundSyntax}'.
Find more at https://angular.io/guide/two-way-binding`);
return [diagnostic];
}
}
由于该系统旨在提供一种无摩擦的方式来添加新诊断,因此核心逻辑被简化为“给定模板抽象语法树中的节点,如果合适,则为其返回诊断”。大部分工作由TemplateCheckWithVisitor
处理,它是一个实用程序,用于设置访问和检查树中每个节点所需的访问者模式。我们甚至可以向类型系统询问有关模板的信息,以更好地理解模板表达式,这就是我们如何识别具有不可空输入的空合并运算符。
编译器本身保留所有扩展诊断的列表,管理其配置,执行它们并收集所有结果。这是编译器类型检查阶段的一部分,请参阅此细分以更全面地了解编译器的工作原理。
该团队迫不及待地想通过更多检查来扩展系统,为开发人员提供额外的安全保障。扩展诊断是一个非常有用的工具,可以在开发人员花费大量时间进行调试之前检测 Angular 模板中的反模式或错误。因此,我们将该系统的重点放在诊断上,以识别正确性问题、性能回归或可维护性挑战。
我们仍处于此过程的早期阶段,但一些初步想法包括:
===
和!==
(而不是==
和!=
)。i18 n
属性,每条消息都有描述等)。a lt
属性,所有路线都有标题等)。< style />
标签,因为这些标签会破坏 CSS 内联和视图封装。r outerLink
属性的相对锚标记,以防止不必要的页面刷新。?.
操作员。如果其中任何一个听起来有趣,请考虑投票并关注这些问题。我们利用这些反馈来确定对社区最有价值的功能,并确定我们下一步工作的优先顺序。
其中一些诊断可能听起来像是属于 linter、格式化程序或其他静态分析工具,但我们认为扩展诊断是一个独特的系统。 Linters 通常会识别代码库中的各种潜在问题,其严重性和置信度也各不相同。低置信度(不安全的 API)或低严重性(风格)检查对于代码健康和质量很重要,但通常不需要中断开发人员的工作流程。这些检查通常设置为作为 CI 的一部分运行,而不是在本地开发中运行。相比之下,扩展诊断旨在作为开发人员编辑/刷新循环的一部分运行,提供有关代码中可能阻止广泛调试的潜在问题的即时反馈。
一些检查非常适合作为扩展诊断,而另一些检查则更适合作为 lint 检查或通过其他一些静态分析工具来实现。特定检查的确切归属将取决于它试图捕获的错误、它的可信度、问题的可操作性以及它对于积极编写代码的开发人员的重要性。
如果您有此处未列出的扩展诊断想法,我们很乐意听到!以下是理想诊断的一些指南:
如果您认为您的想法适合进行扩展诊断,请考虑提交功能请求。
除了新的诊断之外,我们还希望扩展系统的整体功能。这包括生成“快速修复”,这是来自 Angular 语言服务的建议,只需单击一下即可自动修复错误并更正代码。从长远来看,我们还希望支持社区编写的第三方诊断。理想情况下,您可以ng add my-super-cool-library
并自动获取扩展诊断,以捕获该库的常见错误。我们仍在考虑如何以安全且可维护的方式做到这一点,因此请密切关注未来的公告。
最后,向我们的暑期实习生 Daniel Treviño表示衷心的感谢,他为扩展诊断的大部分设计和实现做出了贡献。太棒了!
原文链接:https://blog.angular.dev/angular-extended-diagnostics-53e2fa19ece9