过去几年对 Angular 来说是变革的时期,我们推出了许多重大进展,如使用 Signals 的响应式编程和 Zoneless 应用的强大功能。
我们希望这些功能能帮助 Angular 社区构建下一代网络应用程序,使其快速上市并具有强大的性能。
我们才刚刚开始!Angular v20 是我们最新发布的版本,我们花费了无数的时间打磨一些正在开发中的功能,为您提供坚如磐石的开发体验。
亮点包括:
- 稳定 API,如
effect
、linkedSignal
、toSignal
、增量水合、路由级渲染模式配置以及将 zoneless 提升至开发者预览版 - 通过 Angular DevTools 改进调试功能,并与 Chrome 合作,在 Chrome DevTools 中直接进行自定义 Angular 报告
- 通过编码风格指南更新、类型检查和 HostBindings 的语言服务支持、模板中支持 untagged 模板字面量表达式、默认模板热模块替换等,提升开发者体验。
- GenAI 开发方面的进步,包括
llms.txt
和 angular.dev 指南和视频,用于构建生成式 AI 应用。 - 启动对 Angular 官方吉祥物的评论请求。
将响应式特性提升至稳定版
过去三年,我们重新思考了 Angular 的反应性模型,使其更加健壮和面向未来。在 Angular v16 中,我们发布了 Angular Signals 的开发者预览版,自那以后,它们在谷歌内外得到了广泛采用。
YouTube 在舞台上分享了如何利用 Angular Signals 和 Wiz 将Living Room的输入延迟改善了 35%。与此同时,TC39 启动了一项调查,通过基于 Angular Signals 的参考实现,将 Signals 引入 JavaScript 语言。
在收集了 RFC 反馈并迭代实现后,我们将 signal
、computed
、input
和 view queries API 推升至稳定版本。今天,我们宣布 effect
、linkedSignal
和 toSignal
也成为稳定版本。
新的实验性 API
为了解决使用 Angular 管理异步状态的问题,在 v19 中我们开发了 resource API。自那以后,我们引入了资源流,并创建了一个名为 httpResource
的新 API,它允许你使用基于 Signal 的响应式 API 发起 HTTP 请求。这两个 API 都作为 v20 的一部分提供实验性支持。
resource API
允许你在 Signal 变化时发起异步操作,并将该操作的结果作为 Signal 暴露出去:
const userId: Signal<string> = getUserId();
const userResource = resource({
params: () => ({id: userId()}),
loader: ({request, abortSignal}): Promise<User> => {
// 当给定的 `AbortSignal` 指示请求已中止时,
// fetch 会取消任何未完成的 HTTP 请求。
return fetch(`users/${request.id}`, {signal: abortSignal});
},
});
上面的代码将在 userId
signal 变化时获取具有特定标识符的用户。
现在假设我们从 WebSocket 获取数据。为此,我们可以使用流式资源:
@Component({
template: `{{ dataStream.value() }}`
})
export class App {
// WebSocket 初始化逻辑将在此处执行……
// ...
// 流媒体资源的初始化
dataStream = resource({
stream: () => {
return new Promise<Signal<ResourceStreamItem<string[]>>>((resolve) => {
const resourceResult = signal<{ value: string[] }>({
value: [],
});
this.socket.onmessage = event => {
resourceResult.update(current => ({
value: [...current.value, event.data]
});
};
resolve(resourceResult);
});
},
});
}
在这个最小示例中,我们声明了一个新的流式 resource,它返回一个 Signal 的 Promise。该 signal 的值类型为 ResourceStreamItem<string[]>
,这意味着如果我们想返回错误,signal 可以持有 { value: string[] }
或 {error: … }
的值。
我们通过 resourceResult
Signal 发出从 WebSocket 接收到的值。
基于这一模式,我们还发布了实验性功能 httpResource
:
@Component({
template: `{{ userResource.value() | json }}`
})
class UserProfile {
userId = signal(1);
userResource = httpResource<User>(() =>
`https://example.com/v1/users/${this.userId()}`
});
}
上述代码片段会在 userId
变化时,向我们指定的 URL 发送 HTTP GET 请求。 httpResource
返回 HttpResourceRef
,它具有一个类型为 signal 的 value
属性,我们可以在模板中直接访问。 userResource
还包含其他值,例如 isLoading
、headers
等。
在底层,httpResource
使用 HttpClient
,因此你可以在 HttpClient
provider 中指定拦截器:
bootstrapApplication(AppComponent, {providers: [
provideHttpClient(
withInterceptors([loggingInterceptor, cachingInterceptor]),
)
]});
将 Zoneless 提升至开发者预览版
在过去六个月里,我们在 zoneless 的方面取得了很大进展,特别是在服务器端渲染和错误处理方面。
许多开发人员甚至在不知不觉中使用 Zone.js 来捕捉应用程序中的错误。Zone.js 还能让框架知道我们何时准备好将服务器端渲染的应用程序刷新到客户端。
在 Zoneless 的世界里,我们必须为这些问题找到可靠的解决方案。
在 v20 中,我们现在在 Node.js 的 SSR 期间为 unhandledRejection
和 uncaughtException
提供了默认处理器,以防止在出现错误时 Node 服务器崩溃。
在客户端,你可以在你的 providers 中包含 provideBrowserGlobalErrorListeners
。你可以通过更新你的 providers 来开始使用 zoneless:
bootstrapApplication(AppComponent, {providers: [
provideZonelessChangeDetection(),
provideBrowserGlobalErrorListeners()
]});
此外,请确保从你的 angular.json
中移除 zone.js
依赖。了解更多关于 zoneless 的优势以及如何迁移你的项目,请参阅我们的文档。
如果你正在创建一个新的 Angular 项目,可以使用 CLI 从一开始就将其设置为 zoneless:
在服务器上巩固 Angular
在 v20 版本中,我们还专注于优化我们的旗舰服务器端渲染功能 —— 增量水合和路由级渲染模式配置。今天,我们很高兴地将两者都提升至稳定版本!
提醒一下,增量水合通过在特定触发条件下下载并水合页面的一部分,从而让你的应用运行更快。
这样,您的用户无需下载特定页面的所有 JavaScript,而是可以逐步下载他们需要的部分。
要开始使用增量水合,请通过指定 withIncrementalHydration
来配置水合:
import { provideClientHydration, withIncrementalHydration } from '@angular/platform-browser';
// ...
provideClientHydration(withIncrementalHydration());
现在,在您的组件模板中,您可以使用可延迟视图:
@defer (hydrate on viewport) {
<shopping-cart/>
}
这样,Angular 将下载购物车组件及其传递依赖项,并在该部分 UI 进入视口时才进行水合。
此外,现在您可以使用路由级别的渲染模式配置作为稳定的 API!如果您的应用中不同路由有不同的渲染需求,您可以在服务器路由配置中进行设置:
export const routeConfig: ServerRoute = [
{ path: '/login', mode: RenderMode.Server },
{ path: '/dashboard', mode: RenderMode.Client },
{
path: '/product/:id',
mode: RenderMode.Prerender,
async getPrerenderParams() {
const dataService = inject(ProductService);
const ids = await dataService.getIds(); // ["1", "2", "3"]
// 在路由路径中,使用 `id` 代替 `:id`。
return ids.map(id => ({ id }));
}
}
];
在上述代码片段中,我们配置了在服务器上渲染登录页面,在客户端渲染仪表板页面,并预渲染产品页面。
请注意,产品页面需要一个 id
参数。为了解析每个产品的标识符,我们可以使用异步函数 getPrerenderParams
。它返回一个对象,其中的键映射到路由参数。对于 /product/:id
页面,我们返回一个具有 id
属性的对象。
您可以将服务器端渲染的应用程序托管在大多数云服务提供商上。我们与 Firebase App Hosting 紧密合作,提供无缝的部署体验,支持混合渲染(SSR、SSG 和 CSR),并提供 Google Cloud 的安全性和可扩展性。
优化开发者体验
我们在开发 v20 的过程中投入了大量时间,致力于工程卓越性 —— 完善现有 API 以提升开发者体验。我们全面进行了这项工作 —— 框架、路由器、表单、HTTP 等。让我在这里分享更多我们在此处所做的工作!
Chrome DevTools 中的性能洞察
为了进一步增强开发者体验,并深入了解应用程序性能,我们与 Chrome DevTools 团队合作,将 Angular 特定的分析数据直接集成到性能面板中。
此前,开发者经常需要在框架特定的分析器和浏览器 DevTools 之间切换,这使得关联信息并定位瓶颈变得具有挑战性,尤其是在处理压缩的生产代码时。
这项新集成旨在通过在与其他浏览器性能指标相同的时序轴上显示 Angular 运行时数据(如组件渲染、变更检测周期和事件监听器执行)来解决这个问题。
这项直接集成从 Angular v20 开始提供,利用了性能面板扩展性 API,特别是使用 console.timeStamp
API 以其低开销,确保分析不会对应用程序性能产生负面影响。
开发者现在可以更深入地了解 Angular 的内部运作,通过彩色编码的条目来区分开发者编写的 TypeScript 代码和 Angular 编译器生成的代码。要启用此功能,只需在应用程序中或 DevTools 控制台中运行全局工具 ng.enableProfiling()
。这一进步提供了更直观和全面的性能分析体验,使开发者能够构建性能更优的 Angular 应用程序。
在上面的截图中,你可以看到这项功能的效果。注意在性能时序轴的底部有一个专门用于 Angular 的轨道。
通过彩色条形图,你可以预览组件实例化、运行变更检测等。Angular DevTools 和 Chrome 性能时间线中的 Angular 跟踪都使用相同的钩子,不同之处在于 Chrome 的性能时间线可以将你的应用生命周期置于框架外部的其他 JavaScript 调用上下文中。
此外,Chrome 性能时间线中的 Angular 跟踪显示了一些目前在 Angular DevTools 中不存在的数据,例如组件和提供者的实例化。
框架的添加和改进
要动态创建一个 Angular 组件,你可以使用 createComponent
函数。在 v20 中,我们引入了新功能,允许你将指令应用于动态创建的组件并指定绑定:
import {createComponent, signal, inputBinding, outputBinding} from '@angular/core';
const canClose = signal(false);
const title = signal('My dialog title');
// 创建 MyDialog
createComponent(MyDialog, {
bindings: [
// 绑定一个 signal 到 `canClose` 输入
inputBinding('canClose', canClose),
// 专门监听 MyDialog 上的 `onClose` 事件。
outputBinding<Result>('onClose', result => console.log(result)),
// 在 title 属性上创建双向绑定
twoWayBinding('title', title),
],
directives: [
// 将 `FocusTrap` 指令应用于 `MyDialog`,无需任何绑定。
FocusTrap,
// 将 `HasColor` 指令应用于 `MyDialog` 并将 `red` 值绑定到其 `color` 输入。
// 每次检测到变化时都会调用 `inputBinding` 的回调。
{
type: HasColor,
bindings: [inputBinding('color', () => 'red')]
}
]
});
我们在上面创建了一个对话框组件,并指定:
canClose
输入绑定,将canClose
signal 作为值传递- 将输出
onClose
设置为回调函数,该函数记录发出的结果 title
属性与title
signal 之间的双向绑定
此外,我们向组件中添加了 FocusTrap
和 HasColor
指令。请注意,我们还可以为应用于 MyDialog
的 HasColor
指令指定输入绑定。
扩展模板表达式语法
我们一直在弥合 Angular 模板表达式与完整 JavaScript 语法之间的差距,以实现更高的表达能力和更好的开发者体验。今天,我们引入了对幂运算符 **
和 in
运算符的支持:
<!-- 幂运算 -->
{{ n ** 2 }}
<!-- 检查 person 对象是否包含 name 属性 -->
{{ name in person }}
在 v20 中,我们还允许您直接在表达式中使用 untagged 的模板字面量:
<div [class]="`layout col-${colWidth}`"></div>
扩展诊断
为了防止常见错误,我们引入了静态检查,用于检测无效的空值合并、检测结构化指令的缺失导入,并在您未调用传递给 @for
的 track
函数时发出警告:
@Component({
template: `
@for (user of users; track trackFn) {
<!-- ... ->
}
`
})
class UserList {
users = getUsers();
trackFn() {
// ... body
}
}
Angular 模板中的 @for
循环接受一个跟踪表达式。实际上,trackFn
本身是一个返回 trackFn
函数的表达式,这是一个有效的值。同时,我们很可能想要调用 trackFn
,新的诊断功能使捕获此类错误更加容易。
风格指南更新
在看到过去十年中成千上万的应用如何使用 Angular 后,我们决定更新我们的风格指南。我们的主要目标是使其现代化并去除不必要的复杂性。
在收集了 RFC 的反馈后,我们引入了一系列简化措施 —— 从风格指南中移除非 Angular 特定的代码健康实践,并将与编码风格无关的 Angular 最佳实践移至文档中。
我们还使文件名和类名后缀可选,以鼓励对抽象进行更有目的性的命名,从而减少样板代码。
从 Angular v20 开始,Angular CLI 默认将不会为您的组件、指令、服务和管道生成后缀。对于现有项目,通过 ng update
您的 angular.json
将启用后缀生成。要在新项目中启用后缀生成,请使用以下 schematic
配置:
{
"projects": {
"app": {
...
"schematics": {
"@schematics/angular:component": { "type": "component" },
"@schematics/angular:directive": { "type": "directive" },
"@schematics/angular:service": { "type": "service" },
"@schematics/angular:guard": { "typeSeparator": "." },
"@schematics/angular:interceptor": { "typeSeparator": "." },
"@schematics/angular:module": { "typeSeparator": "." },
"@schematics/angular:pipe": { "typeSeparator": "." },
"@schematics/angular:resolver": { "typeSeparator": "." }
},
...
}
多年来,Angular 经历了很大的发展,我们希望在其风格指南中也能反映这种发展。因此,我们移除了与 NgModules 相关的多数指导,并重新审视了 @HostBinding
和 @HostListener
的使用,转而使用指令元数据中的 host
对象。为确保新指南不会降低开发者体验,我们还解决了宿主绑定支持中的一些不足之处。
改进的宿主绑定
历史上我们推荐 @HostBinding
和 @HostListener
的原因之一是,它们在组件元数据中的 host
对象相比,具有略微更好的编辑器支持,因为你可以直接在特定的绑定或方法上使用它们。但同时,它们难以被发现,需要使用装饰器,并可能导致更繁琐的代码。
在 Angular v20 中,我们引入了宿主绑定和监听表达式的类型检查和语言支持。
在下面的截图中你可以看到这个功能的效果。我们首先得到一个错误,因为我们调用了名为 getAppTile
的函数而不是 getAppTitle
。一旦我们修复这个问题,语言服务会检测到程序没有通过类型检查,因为我们向一个不期望任何参数的函数传递了参数。
要启用此功能,请将 tsconfig.json
中 angularCompilerOptions
下的 typeCheckHostBindings
属性设置为 true
。我们将在 Angular v21 中默认启用此功能。
Angular DevTools 中的增量水合
为了简化增量水合和可延迟视图的调试,你现在可以直接在 Angular DevTools 中预览它们!
下面的屏幕截图展示了如何检查 @defer
及其后续加载的内容。
在使用 @defer
块进行增量水合时,你还会看到图标指示 Angular 是否已经水合了当前组件。
对 vitest 的实验性支持
随着 Karma 的弃用,我们与测试框架作者合作,寻找一个维护良好的替代方案以支持浏览器测试。我们实现了一个拉取请求,创建了一个实验性游乐场,供我们尝试不同的测试运行器。
在 v20 版本中,Angular CLI 带来了实验性的 vitest 支持,支持监视模式和浏览器测试!
要在 node 环境中尝试 vitest,请在项目中运行:
npm i vitest jsdom --save-dev
之后,将 angular.json
中的测试配置更新为:
"test": {
"builder": "@angular/build:unit-test",
"options": {
"tsConfig": "tsconfig.spec.json",
"buildTarget": "::development",
"runner": "vitest"
}
}
接下来,你可能需要更新你的单元测试文件以包含正确的导入:
...
import { describe, beforeEach, it, expect } from 'vitest';
...
最后,使用 vitest 运行你的单元测试:ng test
。
Angular Material 的改进
在此版本中,我们进一步优化了按钮组件,使其更好地符合 M3 规范。
一些变更:
- 实现了 Tonal 按钮
- 术语与 M3 规范对齐
- 添加了设置按钮默认外观的功能
- 为图标按钮添加了
matIconButton
选择器以保持一致性
我们实施的一些提升用户体验的改进包括:
- 为对话框添加了新的
closePredicate
,该功能可以关闭请求,获赞 108 👍 - 新增了用于更好树摇动的覆盖层 API
- 现在自动处理
prefers-reduced-motion
- 新增用于禁用动画的 DI 令牌
MatButton
和MatAnchor
被合并,用户无需导入两者。
支持使用 GenAI 的开发者
为了让 LLMs 生成现代 Angular 代码,并使您能够使用 GenAI 构建应用,我们启动了两个项目
- 维护一个
llms.txt
文件(见 GitHub 上的 pull 请求),帮助大型语言模型发现最新的 Angular 文档和代码示例 - 为使用 GenAI 构建应用的开发者提供起点
一些语言模型仍然使用结构化指令而不是最新的控制流生成旧的 Angular 语法,或者使用 NgModules 而不是独立的组件、指令和管道。解决这个问题是一个多步骤的过程,我们首先通过创建一个 llms.txt
文件开始。未来,我们将继续提供使用最新 Angular 语法的代码示例,并探索开发系统提示,提示 LLMs 使用正确的 API。
我们启动的第二项工作是提供针对使用 AI 功能的 API 开发者的指南。我们进行了多次直播,展示了如何在 Angular 应用中利用 Genkit 和 Vertex AI。我们开源了示例应用,并在 angular.dev/ai 上列出了我们发现的一些最佳实践。
这只是让 Angular 成为您智能 AI 应用解决方案的起点。
NgIf、NgFor 和 NgSwitch 的弃用
我们在 v17 版本中引入了 Angular 内置的控制流,以带来一系列改进:
- 更直观的语法,更接近 JavaScript
- 更简单的使用方式,无需导入其他模块或单个指令
- 通过更新 diffing 算法提升性能
- 通过类型缩小改进类型检查
我们还发布了一个 schematic
,只需一行代码即可让您的项目从结构化指令迁移到内置控制流:
ng generate @angular/core:control-flow
目前,HTTP Archive 公共数据集中的超过一半 Angular v17+ 应用已使用新语法!
根据社区反馈和采用指标,我们将逐步弃用 *ngIf
、*ngFor
和 *ngSwitch
,并鼓励大家使用最新的内置控制流。这意味着根据我们的弃用政策,我们可以在一年后的 v22 版本中移除基于结构化指令的控制流指令(NgIf、NgFor 和 NgSwitch)。
官方 Angular 吉祥物
随着 Angular 的持续发展和演进,我们很高兴宣布一项新举措,将进一步提升我们卓越的社区:创建官方 Angular 吉祥物!
虽然 Angular 是一个备受认可且广泛采用的框架,但它一直缺少许多其他成功开源项目所拥有的有趣、可视化的代表。
我们收到了你们希望拥有一些实体物品来连接的请求,比如毛绒玩具或钥匙链,我们非常高兴能与大家一起踏上这段创意之旅。
我们的团队与 Dart 和 Firebase 吉祥物的设计师合作,分享了 Angular 的核心优势与社区精神。这个过程导致了三个初步的吉祥物提案,我们迫不及待地要与大家分享。
现在轮到你们,Angular 社区,发挥作用了!遵循 Angular 包容性和社区主导决策的价值观,我们向所有人开放了贡献的过程。在 goo.gle/angular-mas… 找到官方的 Angular 吉祥物 RFC。
这是初步概念的预览:
“Angular 塑造角色”灵感来源于我们的标志
智慧而坚韧的“灯笼鱼”(比现实中的同类可爱多了!)
灯笼鱼的一个变体。
我们邀请您查看完整的 RFC,为其中的一项提案投票,提出改进建议,甚至提出名称。您的反馈对塑造 Angular 的功能至关重要,我们很高兴看到同样的协作精神将定义我们的未来吉祥物。
让我们一起创造一些真正特别的东西!
感谢所有贡献!
在全球范围内,有数千名库作者、会议和聚会组织者、教育工作者以及其他通过 Angular 推动网页发展的人!没有你们,我们永远无法到达这里。
自 v19 发布以来,我们从框架、组件和 CLI 收到了来自 225 多名开发者的提交并进行了合并!你做出的每一个改变都帮助我们让 Angular 变得更好。我想突出一些来自社区成员的功能:
- Domenico Gemoli 为 AbstractControl 添加了 markAllAsDirty 功能,现在可将特定组件及其所有子组件设为脏组件
- Enea Jahollari 实现了 @for 上未调用 track 函数的扩展诊断功能
- Enea Jahollari 还添加了一个迁移,用于将模板转换为使用自闭合标签以保持代码一致性
- Jeri Peier 使使用集合与表单验证器成为可能
- Kevin Brey 专注于扩展诊断功能,以帮助您检测缺失的结构性指令导入
- Lukas Spirig 为 RouterLink 引入了自定义元素支持
- Meddah Abdallah 为路由器引入了异步重定向
- Younes Jaaidi 使 Jest 和 Karma 中的测试能够在编译时运行
感谢大家 🙏
期待未来!
作为 v20 的一部分,我们为过去几年启动的重大工作(如响应式、zoneless、增量水合、框架和表单 API)发布了许多润色改进。
我们还发布了即将到来的重大进展的初步高级草图,如 selectorless、signal 表单、单元测试和官方 Angular 吉祥物!
我们正在为你们构建 Angular,而你们的意见对于我们如何推进这些计划至关重要。随着这些重大项目的高级计划逐渐成形,我们将分享更新和意见征集。
到目前为止,请确保分享你们对 Angular 未来吉祥物的想法!我们非常期待找到一个能象征 Angular 产品和价值观的标识。让我们知道你们对 RFC 中的概念有什么看法!
直到下次再见,感谢您成为 Angular 的一部分 🙏