前言
在 Angular 15 中,独立 APIs 正式稳定,开发人员可以使用独立组件开发各种组件、指令、管道和构建应用程序。独立 APIs 减少了在开发时对 ngModule 的依赖,提升了开发体验。
目前,组件库 ngx-tethys 紧跟 Angular 脚步,现在已支持独立组件。
什么是独立组件
独立组件提供了一种简化的方式来构建 Angular 应用程序。独立组件、指令和管道旨在通过减少对 NgModule 的需求来简化创作体验。现有应用程序可以选择性地以增量方式采用新的独立风格,而无需任何重大更改。
如何创建独立组件
NgModule 创建组件
回顾下在独立 APIs 之前,创建组件、指令、管道需要完成哪些:
创建组件、指令、管道
创建 ngModule ,在此 ngModule 中需要:
通过 declarations 声明新创建的组件、指令和管道
通过 imports 导入在该模块中使用到的其他组件、指令、管道所属的模块
通过 exports 导出可供其他模块使用的组件、指令和管道
通过 providers 设置一些供组件使用的服务
比如创建一个按钮组件:
创建组件
@Component({
selector: 'button',
templateUrl: './button.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ButtonComponent {}
定义 ButtonModule
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ButtonComponent } from './button.component';
@NgModule({
declarations: [ButtonComponent,],
imports: [CommonModule],
exports: [ButtonComponent,],
providers: []
})
export class ButtonModule {}
每次新增、修改组件时,都需要在 ngModule 中进行更改,过程比较繁琐。
独立 APIs 创建组件
使用独立 APIs 创建组件、指令、管道:
创建组件、指令、管道时,在元数据中配置 standalone: true
在元数据中 imports 使用的其他独立组件、模块
比如创建按钮组件:其中 NgIf 、 NgClass 为 Angular 提供的独立组件
@Component({
selector: 'button',
templateUrl: './button.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgIf, NgClass]
})
export class ButtonComponent {
}
回顾之前使用 NgModule 创建组件时,在模块定义中还会导出可用于其他模块/组件使用的组件列表,如果使用模块中多个组件,直接导入模块就行。使用独立组件如何设置一系列组件供其他模块/组件使用呢?
方式一:
定义一个 ngModule 作为多个独立组件的集合。此处需要注意的是:独立组件不可以再次在 ngModule 中声明。
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ThyButtonComponent } from './button.component';
@NgModule({
imports: [CommonModule, ThyButtonComponent],
exports: [ButtonComponent]
})
export class ButtonModule {}
方式二:
定义一个常量数组,将多个独立组件导出。此处需要注意的是: 常量需要 as const ,为 Angular 编译器提供了正确编译所需的额外信息。
export const BUTTON_COMPONENTS = [ThyButtonComponent] as const;
指令组合
如何使用
在指令支持独立指令后,独立指令还可以直接用于其他组件、指令,复用独立指令的逻辑。在指令或组件上使用 hostDirectives:[] ,将独立指令应用于组件。
如下示例, ThyFlex 为独立指令,指令有输入参数 thyDirection :
@Directive({
selector: '[flex]',
standalone: true,
host: {
class: 'd-flex'
}
})
export class Flex implements OnInit, OnChanges {
@Input() direction: FlexDirection;
constructor() {}
}
组件 flex :
@Component({
selector: 'flex',
template: `<ng-content></ng-content>`,
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [
{
directive: Flex,
inputs: ['thyDirection: direction']
}
],
imports: [Flex]
})
export class FlexComponent {}
需要注意的是: 默认情况下,指令Flex 的输入、输出参数不会作为组件ThyFlexComponent 的公开 API,需要如上显示的定义。
使用:
javascript
<flex thyDirection="xxx"></flex>
执行顺序
使用组合指令后,组件和宿主指令执行顺序:
宿主指令和直接在模板中使用的组件、指令会经历相同的生命周期。但是,宿主指令总是会在应用它们的组件或指令之前执行它们的构造函数、生命周期钩子和绑定。因此顺序为:
指令 Flex 实例化
组件 FlexComponent 实例化
指令 Flex 接收输入,执行 ngOninit
组件 FlexComponent 接收输入,执行 ngOninit
指令 Flex 应用宿主绑定
组件 FlexComponent 应用宿主绑定
依赖注入
使用了 hostDirectives 的组件或指令可以注入这些宿主指令的实例。
javascript
@Component({
selector: 'flex',
template: `<ng-content></ng-content>`,
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [
{
directive: Flex,
inputs: ['direction']
}
],
imports: [Flex]
})
export class FlexComponent implements OnInit {
private flexDirective = inject(Flex);
constructor() {
console.log('FlexComponent constructor');
}
ngOnInit(): void {
console.log('FlexComponent ngOnInit');
}
}
当把宿主指令应用于组件时,组件和宿主指令都可以定义提供者。如果带有 hostDirectives 的组件或指令以及这些宿主指令都提供相同的注入令牌,则带有 hostDirectives 的类定义的提供者会优先于宿主指令定义的提供者。
优势:
增强使用宿主指令的组件的功能;
增强代码的复用性
缺点:
过度使用宿主指令会影响应用程序的内存使用。
应用中使用
NgModule 启动应用
通过根模块启动应用,通过 bootstrapModule 启动,程序入口文件 main.ts 如下:
javascript
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
AppModule 中,除了上边提到的 ngModule 中配置,还会通过 providers 设置全局的依赖,通过 bootstrap: [ AppComponent ] 设置根组件。启动后,将根组件插入到 index.html 页面中,进而构建组件树( bootstrap 中支持配置多个组件,一般情况跟组件只会设置一个组件)。
javascript
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
独立应用启动
通过使用独立组件作为应用程序的根组件,可以在没有任何 NgModule 的情况下引导 Angular 应用程序。使用 bootstrapApplication 启动应用,配置根组件(根组件为独立组件):
main.ts:
javascript
bootstrapApplication(AppComponent).catch(err => console.error(err));
AppComponent
javascript
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, TopBarComponent],
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'demo-standalone';
}
回顾使用 ngModule 启动应用时,除了指定启动模块、启动组件,还需要配置全局服务和路由。在使用独立 APIs 启动应用如何配置呢?
依赖
可以在启动应用程序时,配置全局依赖。
javascript
bootstrapApplication(AppComponent, {
providers: [
ProjectStore
]
})
.catch((err) => console.error(err));
如果第三方库仅支持 ngModule 模式配置依赖,可以使用 importProvidersFrom 配置。
javascript
import {LibraryModule} from 'ngmodule-based-library';
bootstrapApplication(PhotoAppComponent, {
providers: [
importProvidersFrom(
RouterModule.forRoot(routes)
)
]
});
此外,Angular 其他模块或者第三方库也支持了以 provide-* 的方法配置依赖。
provideRouter() // 可用于配置路由
provideHttpClient() // 可用于配置 HttpClient 服务
provideZoneChangeDetection() // 可用于配置 ngZone
provideAnimations() // 需要动画时,使用该方法
provideNoopAnimations()
provideServiceWorker()
provideServerRendering()
provideClientHydration()
...
在之前 NgModule 应用中,会根据在 @ngModule.providers 或者 @({providedIn: "..."}) 中配置依赖,根据配置创建模块注入器,在独立应用中,没有模块的概念,Angular 会创建环境注入器 environment injectors 。
一下场景会创建环境注入器:
@NgModule.providers ,通过 NgModule 引导的应用程序时
@({provideIn: "..."})
bootstrapApplication 独立应用启动时配置的 providers
Route 配置的 providers
除了在以上使用中 Angular 创建环境注入器,Angular 还支持 createEnvironmentInjector 创建。
路由
Angular 为支持独立应用,提供了 provideRouter 方法用于配置独立应用的路由。
javascript
export const routes: Routes = [
{ path: 'products', component: ProductListComponent }
];
javascript
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
// importProvidersFrom(
// RouterModule.forRoot(routes)
// ),
]
})
.catch((err) => console.error(err));
惰性加载
之前,要惰性加载模块,Angular 提供了 loadChildren 方法,加载一组路由,使用如下:
javascript
const routes: Routes = [
{
path: 'items',
loadChildren: () => import('./items/items.module').then(m => m.ItemsModule)
}
];
在独立应用中,同样支持通过 loadChildren 惰性加载一组路由
javascript
// In the main application:
export const ROUTES: Route[] = [
{path: 'admin', loadChildren: () => import('./admin/routes').then(mod => mod.ADMIN_ROUTES)},
// ...
];
javascript
// In admin/routes.ts:
export const ADMIN_ROUTES: Route[] = [
{path: 'home', component: AdminHomeComponent},
{path: 'users', component: AdminUsersComponent},
// ...
];
此外还支持 loadComponent 加载独立组件。
javascript
export const ROUTES: Route[] = [
{path: 'admin', loadComponent: () => import('./admin/panel.component').then(mod => mod.AdminPanelComponent)},
];
简化使用:路由器会理解并使用 default 导出来的路由或者组件。
javascript
// In the main application:
export const ROUTES: Route[] = [
{path: 'admin', loadChildren: () => import('./admin/routes')},
// ...
];
javascript
// In admin/routes.ts:
export default [
{path: 'home', component: AdminHomeComponent},
{path: 'users', component: AdminUsersComponent},
// ...
] as Route[];
工具
原有升级应用或类库升级使用独立组件
Angular (版本大于 15.2.0) 提供了完善的 Schematic 帮助原有应用升级转换为独立组件模式。
ng generate @angular/core:standalone
按照下面列出的顺序运行迁移:
运行 ng g @angular/core:standalone 并选择 "Convert all components, directives and pipes to standalone"
运行 ng g @angular/core:standalone 并选择 "Remove unnecessary NgModule classes"
运行 ng g @angular/core:standalone 并选择 "Bootstrap the project using standalone APIs"
运行任何静态分析(lint)和格式检查,修复任何故障,并提交结果
生成独立启动应用
ng new demo-standalone --standalone
生成独立组件
ng g c button --standalone