Angular 15 独立组件详解

前言

在 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
相关推荐
学不会•1 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
EasyNTS2 小时前
H.264/H.265播放器EasyPlayer.js视频流媒体播放器关于websocket1006的异常断连
javascript·h.265·h.264
活宝小娜3 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点3 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow3 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o3 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
开心工作室_kaic4 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
刚刚好ā4 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
沉默璇年6 小时前
react中useMemo的使用场景
前端·react.js·前端框架
yqcoder6 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript