@levinzhang
2022-12-24T22:34:02.000000Z
字数 6926
阅读 407
近日,Angular v15正式发布,其中最引入关注的特性是独立组件,它能够使开发人员脱离NgModules构建组件,除此之外,该版本还带来了众多新特性和开发工具的升级。
本文最初发表于Angular的官方博客,经原作者Minko Gechev授权由InfoQ中文站翻译分享。
在过去的一年间,我们移除了Angular的遗留编译器和渲染流水线,这使得我们能够在过去的几个月中开发了一系列针对开发人员体验的改善。Angular v15是这项工作的高潮,它有几十项改进,可以带来更好的开发人员体验和性能。
在v14中,我们引入了新的独立(standalone)API,它能够让开发人员在不使用NgModule的情况下构建应用。我们很高兴地向大家宣布,这些API已经从开发人员预览阶段毕业,现在成为了稳定API的一部分。从现在开始,我们将会按照语义化版本的方式逐步演进它们。
为了确保独立API能够毕业,我们的一部分工作就是保证独立组件能够在整个Angular中运行,它们现在已经完全可以在HttpClient
、Angular元素、路由器中运行了。
独立API允许我们使用单个组件来引导应用:
import {bootstrapApplication} from '@angular/platform-browser';
import {ImageGridComponent} from'./image-grid';
@Component({
standalone: true,
selector: 'photo-gallery',
imports: [ImageGridComponent],
template: `
… <image-grid [images]="imageList"></image-grid>
`,
})
export class PhotoGalleryComponent {
// component logic
}
bootstrapApplication(PhotoGalleryComponent);
我们可以使用新的路由器独立API构建多路由的应用。为了声明根路由,我们可以采取以下的方式:
export const appRoutes: Routes = [{
path: 'lazy',
loadChildren: () => import('./lazy/lazy.routes')
.then(routes => routes.lazyRoutes)
}];
其中,lazyRoutes
的声明如下:
import {Routes} from '@angular/router';
import {LazyComponent} from './lazy.component';
export const lazyRoutes: Routes = [{path: '', component: LazyComponent}];
最后,在bootstrapApplication
调用中注册appRoutes
:
bootstrapApplication(AppComponent, {
providers: [
provideRouter(appRoutes)
]
});
provideRouter
API的另外一个好处在于它是支持可摇树(tree-shakable)的。打包器可以在构建时移除路由器未使用的特性。在使用新API进行的测试中,我们发现从包(bundle)中移除这些未使用的特性后,应用包中路由器代码的大小减少了11%。
指令组合API将代码的重用提升到了一个新的层次。该特性来源于GitHub上最受欢迎的一个特性请求,它要求提供向宿主(host)元素添加指令的功能。
指令组合API使开发人员能够使用指令来增强宿主元素,并为Angular提供了强大的代码重用策略,这一点要归功于我们的编译器。指令组合API仅适用于独立指令。
我们快速看一个样例:
@Component({
selector: 'mat-menu',
hostDirectives: [HasColor, {
directive: CdkMenu,
inputs: ['cdkMenuDisabled: disabled'],
outputs: ['cdkMenuClosed: closed']
}]
})
class MatMenu {}
在上面的代码片段中,我们使用两个指令HasColor
和CdkMenu
对MatMenu
进行了增强。MatMenu
重用了HasColor
的所有输入、输出和相关的逻辑,以及CdkMenu
的逻辑和选中的输入。
这项技术可能会让你想起其他编程语言中的多重继承或trait,与之不同的是,我们有一个解决名称冲突的机制,而且适用于用户界面的基础元素。
在v14.2中,我们曾经宣布与Chrome Aurora合作开发的Angular图像指令的开发人员预览版。
优化前后的示例应用
我们很开心的宣布,图像指令的功能已经稳定。Land's End对这一功能进行了实验,在lighthouse lab测试中观察到LCP有75%的改善。
v15还包含了图像指令的一些新特性:
srcset
:该指令会为我们生成一个srcset
属性,确保请求一个大小适当的图像。这可以减少图像的下载时间。在组件或NgModule中,你可以直接使用独立的NgOptimizedImage指令:
import { NgOptimizedImage } from '@angular/common';
// Include it into the necessary NgModule
@NgModule({
imports: [NgOptimizedImage],
})
class AppModule {}
// ... or a standalone Component
@Component({
standalone: true
imports: [NgOptimizedImage],
})
class MyStandaloneComponent {}
要在组件中使用它的话,只需将图像的scr
属性替换为ngSrc
,并确保为LCP图像声明了priority
属性。
你可以在我们的文档中获取更多的信息。
与可摇树独立路由器API一起,我们致力于减少守卫中的样板式代码。我们看一个样例,它是验证用户是否登录的守卫:
@Injectable({ providedIn: 'root' })
export class MyGuardWithDependency implements CanActivate {
constructor(private loginService: LoginService) {}
canActivate() {
return this.loginService.isLoggedIn();
}
}
const route = {
path: 'somePath',
canActivate: [MyGuardWithDependency]
};
LoginService
实现了大多数的逻辑,在守卫中,我们只是调用了isLoggedIn()
。即便守卫非常简单,我们依然有很多样板式的代码。
借助新的函数式路由守卫,我们可以将代码重构为如下的形式:
const route = {
path: 'admin',
canActivate: [() => inject(LoginService).isLoggedIn()]
};
我们在守卫声明中表述了整个守卫的内容。函数式守卫是可组合的,我们可以创建类似工厂的函数,它接受一个配置并返回一个守卫或解析器函数。你可以在GitHub上找到一个连续运行路由守卫的样例。
为了使路由器更加简洁,并进一步减少样板代码,路由器现在能够在懒加载时自动解包默认的导出。
假设我们有如下的LazyComponent
:
@Component({
standalone: true,
template: '...'
})
export default class LazyComponent { ... }
在这项变化之前,要懒加载一个独立组件,我们需要这样做:
{
path: 'lazy',
loadComponent: () => import('./lazy-file').then(m => m.LazyComponent),
}
现在,路由器会寻找一个默认的导出,如果能够找到的话,将会自动使用它,这会简化路由的声明:
{
path: 'lazy',
loadComponent: () => import('./lazy-file'),
}
我们从每年的开发者调查中得到了很多的启示,所以我们要感谢你花时间来分享你的想法。深入研究开发人员在调试体验所面临的斗争之后,我们发现错误信息可以进行一些改进。
关于调试所面临挑战的反馈
我们与Chrome DevTools协作来解决这个问题。我们看一个在Angular应用中可能得到的堆栈跟踪样例。
ERROR Error: Uncaught (in promise): Error
Error
at app.component.ts:18:11
at Generator.next (<anonymous>)
at asyncGeneratorStep (asyncToGenerator.js:3:1)
at _next (asyncToGenerator.js:25:1)
at _ZoneDelegate.invoke (zone.js:372:26)
at Object.onInvoke (core.mjs:26378:33)
at _ZoneDelegate.invoke (zone.js:371:52)
at Zone.run (zone.js:134:43)
at zone.js:1275:36
at _ZoneDelegate.invokeTask (zone.js:406:31)
at resolvePromise (zone.js:1211:31)
at zone.js:1118:17
at zone.js:1134:33
这个片段有两个主要的问题:
Chrome DevTools团队创建了一种机制,通过Angular CLI标注源码映射(source map)来忽略来自node_modules
的脚本。我们还合作开发了一个异步堆栈标签API,它允许我们将独立的、调度的异步任务串联成一个堆栈跟踪。Jia Li将Zone.js与异步堆栈标签API进行了集成,这使得我们能够提供链接在一起的堆栈跟踪信息。
这两个变更极大地改善了开发人员在Chrome DevTools中看到的堆栈跟踪信息:
ERROR Error: Uncaught (in promise): Error
Error
at app.component.ts:18:11
at fetch (async)
at (anonymous) (app.component.ts:4)
at request (app.component.ts:4)
at (anonymous) (app.component.ts:17)
at submit (app.component.ts:15)
at AppComponent_click_3_listener (app.component.html:4)
在这里,我们可以跟踪从AppComponent
中的按钮按下到出错的整个执行过程。你可以在这里阅读关于这些改进的更多信息。
我们很高兴地宣布,基于Material Design Components for Web(MDC)的Angular material组件的重构已经完成。这个变化使Angular更加符合Material Design规范,并使我们能够在最终确定style token后立即采用Material 3。
对于许多组件,我们更新了样式和DOM结构,还从头重写了一些组件。我们为新组件保留了大多数TypeScript API和组件/指令选择器,使其与旧的实现方式完全相同。
我们迁移了数以千计的谷歌项目,这使得我们确保外部迁移路径能够顺利进行,并且记录了所有组件的变更清单。
由于使用了新的DOM和CSS,你可能会发现应用中的一些样式需要调整,尤其是如果你的CSS覆盖了已迁移组件的内部元素的样式的话。
每个新组件的旧实现均已被废弃,但是依然可以通过“legacy”导入获取它们。比如,通过导入遗留的按钮模块,你可以导入旧的mat-button
实现。
import {MatLegacyButtonModule} from '@angular/material/legacy-button';
请访问迁移指南获取更多信息。
在幕后,我们将许多组件修改为使用design token和CSS变量,这为采用Material 3组件风格的应用提供了更平滑的迁移路径。
我们解决了投票第四多的问题,即滑块中的范围选择。
要获取范围输入,我们可以:
<mat-slider>
<input matSliderStartThumb>
<input matSliderEndThumb>
</mat-slider>
除此之外,所有的组件现在都有一个API来自定义密度,这解决了另一个很常见GitHub的问题。
我们现在可以通过自定义主题来声明所有组件的默认密度:
@use '@angular/material' as mat;
$theme: mat.define-light-theme((
color: (
primary: mat.define-palette(mat.$red-palette),
accent: mat.define-palette(mat.$blue-palette),
),
typography: mat.define-typography-config(),
density: -2,
));
@include mat.all-component-themes($theme);
新版本的组件包括广泛的可访问性改进,包括更好的对比度、增加触摸目标尺寸,以及完善的ARIA语义。
在v14中,我们宣布在ng build
中对esbuild的实验性支持,以实现更快的构建时间并简化我们的流水线。
在v15中,我们现在有了实验性的Sass、SVG模板、文件替换和ng build --watch
支持! 请通过更新你的构建器angular.json
来尝试esbuild,将其从
"builder": "@angular-devkit/build-angular:browser"
更改为:
"builder": "@angular-devkit/build-angular:browser-esbuild"
如果你在生产环境构建中遇到任何问题,都可以在GitHub上提交issue。
在Angular CLI中,我们引入了对稳定的独立API的支持。现在可以通过ng g component --standalone
生成一个新的独立组件。
我们还在简化ng new
输出。作为第一步,我们通过删除test.ts
、polyfills.ts
和environments
来减少配置。现在你可以直接在angular.json
中的polyfills
区域指定你的polyfills:
"polyfills": [
"zone.js"
]
为了进一步减少配置开销,我们现在使用.browserlist
让你定义目标ECMAScript版本。
主发布版本使我们能够使框架朝着简单化、更好的开发者体验以及与Web平台一致的方向发展。
在分析了谷歌的数千个项目后,我们发现了一些很少使用的模式,这些模式在大多数情况下会被滥用。因此,我们废除的providedIn: 'any'
就是这种情况,除了框架内部的一些晦涩情况外,它的用途非常有限。
我们也将废弃providedIn: NgModule
。它的用途并不广泛,而且在大多数情况下会导致使用不当,在这种情况下你应该选择providedIn: 'root'
。如果你真的需要将提供者的范围扩大到特定的NgModule
,请使用NgModule.providers
。
随着CSS中布局的不断发展,团队将停止发布@angular/flex-layout
的新版本。我们会在明年继续提供安全性和浏览器兼容性方面的修复。你可以在我们的 “现代CSS”系列的第一篇博文中了解更多信息。
在2020年推出的Ivy实现了很多全面性的改进,你可以发现这些改进已经开始浮现。可选的NgModules就是一个很好的样例。它有助于减少初学者需要处理的概念,它同时能够支持高级功能,如通过独立指令实现组合API。
下一步,我们将处理服务器端渲染流水线和反应性的改进,同时带来全面的改进与增强。