项目

v2.9 到 v3.0 Angular UI 迁移指南

v3.0 有哪些变化?

Angular 10

新的 ABP Angular UI 基于 Angular 10 和 TypeScript 3.9,我们已停止对 Angular 8 的支持。不过,ABP 模块将继续与 Angular 9 兼容。因此,如果您的项目使用 Angular 9,则无需升级到 Angular 10。但通常来说,升级过程非常简单。

迁移时需要做什么?

在根文件夹打开终端并运行以下命令:

yarn ng update @angular/cli @angular/core --force

这将进行以下修改:

  • 更新您的 package.json 并安装新包
  • 修改 tsconfig.json 文件以创建 "Solution Style" 配置
  • browserslist 重命名为 .browserslistrc

另一方面,如果先单独使用 yarn ng update 命令检查需要更新哪些包会更好。Angular 会给出一个需要更新的包列表。

待更新包表格

当 Angular 报告以上包时,您的命令将如下所示:

yarn ng update @angular/cli @angular/core ng-zorro-antd --force

如果 Angular 提示您的仓库中有未提交的更改,您可以提交/存储这些更改,或者在命令中添加 --allow-dirty 参数。

配置模块

在 ABP v2.x 中,每个延迟加载模块都有一个可通过独立包使用的配置模块,模块配置如下:

import { AccountConfigModule } from '@abp/ng.account.config';

@NgModule({
  imports: [
    // 其他导入
    AccountConfigModule.forRoot({ redirectUrl: '/' }),
  ],
  // providers, declarations, 和 bootstrap
})
export class AppModule {}

... 以及在 app-routing.module.ts 中 ...

const routes: Routes = [
  // 其他路由配置
  {
    path: 'account',
    loadChildren: () => import(
      './lazy-libs/account-wrapper.module'
    ).then(m => m.AccountWrapperModule),
  },
];

这种方式虽然可行,但有一些缺点:

  • 每个模块都包含在两个独立的包中,但实际上这些包是相互依赖的。
  • 配置延迟加载模块需要一个包装器模块。
  • ABP 拥有可扩展系统,在根模块配置可扩展模块会增加包体积。

在 ABP v3.0 中,我们为每个配置模块引入了次要入口点,以及一种无需包装器即可配置延迟加载模块的新方法。现在,模块配置如下所示:

import { AccountConfigModule } from '@abp/ng.account/config';

@NgModule({
  imports: [
    // 其他导入
    AccountConfigModule.forRoot(),
  ],
  // providers, declarations, 和 bootstrap
})
export class AppModule {}

... 以及在 app-routing.module.ts 中 ...

const routes: Routes = [
  // 其他路由配置
  {
    path: 'account',
    loadChildren: () => import('@abp/ng.account')
      .then(m => m.AccountModule.forLazy({ redirectUrl: '/' })),
  },
];

此项更改帮助我们显著减少了包体积和构建时间。我们相信您会在您的应用程序中感受到差异。

一个更好的例子

AppModule:

import { AccountConfigModule } from '@abp/ng.account/config';
import { CoreModule } from '@abp/ng.core';
import { IdentityConfigModule } from '@abp/ng.identity/config';
import { SettingManagementConfigModule } from '@abp/ng.setting-management/config';
import { TenantManagementConfigModule } from '@abp/ng.tenant-management/config';
import { ThemeBasicModule } from '@abp/ng.theme.basic';
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgxsModule } from '@ngxs/store';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    AppRoutingModule,
    CoreModule.forRoot({
      environment,
      sendNullsAsQueryParam: false,
      skipGetAppConfiguration: false,
    }),
    ThemeSharedModule.forRoot(),
    AccountConfigModule.forRoot(),
    IdentityConfigModule.forRoot(),
    TenantManagementConfigModule.forRoot(),
    SettingManagementConfigModule.forRoot(),
    ThemeBasicModule.forRoot(),
    NgxsModule.forRoot(),
  ],
  // providers, declarations, 和 bootstrap
})
export class AppModule {}

AppRoutingModule:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    loadChildren: () => import('./home/home.module').then(m => m.HomeModule),
  },
  {
    path: 'account',
    loadChildren: () =>
      import('@abp/ng.account').then(m => m.AccountModule.forLazy({ redirectUrl: '/' })),
  },
  {
    path: 'identity',
    loadChildren: () => import('@abp/ng.identity').then(m => m.IdentityModule.forLazy()),
  },
  {
    path: 'tenant-management',
    loadChildren: () =>
      import('@abp/ng.tenant-management').then(m => m.TenantManagementModule.forLazy()),
  },
  {
    path: 'setting-management',
    loadChildren: () =>
      import('@abp/ng.setting-management').then(m => m.SettingManagementModule.forLazy()),
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

AppComponent:

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <abp-loader-bar></abp-loader-bar>
    <abp-dynamic-layout></abp-dynamic-layout>
  `,
})
export class AppComponent {}

您可能已经注意到,我们在 AppComponent 模板中使用了 <abp-dynamic-layout> 而不是 <router-outlet>。我们进行此更改是为了避免不必要的渲染和闪烁。这不是强制性的,但我们建议在您的 AppComponent 中执行相同的操作。

迁移时需要做什么?

  • 使用 yarn remove 从项目中移除配置包。
  • 从次要入口点导入配置模块(例如 @abp/ng.identity/config)。
  • 调用所有新配置模块的静态 forRoot 方法,即使不传递配置。
  • 调用 ThemeBasicModule(如果是商业版则调用 ThemeLeptonModule)的静态 forRoot 方法,并从 imports 中移除 SharedModule(除非您在其中添加了根模块必需的内容)。
  • 在应用路由模块中直接导入延迟加载的 ABP 模块(例如 () => import('@abp/ng.identity').then(...))。
  • then 内部调用所有延迟加载模块的静态 forLazy 方法,即使不传递配置。
  • [可选] 在 AppComponent 模板中添加 <abp-dynamic-layout></abp-dynamic-layout> 并移除 <router-outlet></router-outlet> 以获得更好的性能和用户体验。

RoutesService

在 ABP v2.x 中,向菜单添加路由有两种方式:

从 v3.0 开始,我们改变了添加和修改路由的方式。我们不再将路由存储在 ConfigState 中(破坏性变更)。取而代之的是一个名为 RoutesService 的新服务,用于添加、修补或删除菜单项。请查看文档了解详情。

迁移时需要做什么?

  • 检查您是否曾经使用 ConfigStateConfigStateService 来添加任何路由。如果是,请将其替换为 RoutesServiceadd 方法。
  • 检查您是否曾经修补过路由。如果是,请将其转换为 RoutesServicepatch 方法。
  • 仔细检查在 addpatch 方法调用中,是否为子菜单项使用了绝对路径并提供了 parentName 而不是 children 属性。

NavItemsService

在 ABP v2.x 中,添加导航元素是通过 LayoutStateService 完成的。

从 v3.0 开始,我们改变了添加和修改导航项的方式,之前的方法不再可用(破坏性变更)。请查看文档了解详情。

迁移时需要做什么?

  • 将所有 dispatchAddNavigationElement 调用替换为 NavItemsServiceaddItems 方法。

ngx-datatable

直到 v3 版本之前,我们一直使用一个自定义组件 abp-table 作为默认表格。然而,数据网格是复杂的组件,实现一个功能齐全的组件需要大量精力,而我们计划将这些精力投入到其他功能和问题上。

从 ABP v3 开始,我们切换到了一个经过实战检验、实现良好的数据网格:ngx-datatable。所有 ABP 模块都将附带已经实现好的 ngx-datatable。ThemeSharedModule 已经导出了 NgxDatatableModule。因此,如果您通过在终端中运行 yarn add @swimlane/ngx-datatable 来安装该包,它将在您应用的所有模块中可用。

为了正确的样式,您需要在 angular.json 文件的 styles 部分添加以下内容(在所有其他样式之上):

"styles": [
  {
    "input": "node_modules/@swimlane/ngx-datatable/index.css",
    "inject": true,
    "bundleName": "ngx-datatable-index"
  },
  {
    "input": "node_modules/@swimlane/ngx-datatable/assets/icons.css",
    "inject": true,
    "bundleName": "ngx-datatable-icons"
  },
  {
    "input": "node_modules/@swimlane/ngx-datatable/themes/material.css",
    "inject": true,
    "bundleName": "ngx-datatable-material"
  },
  // 其他样式
]

由于 abp-table 尚未被弃用,之前由 ABP v2.x 构建的模块不会突然失去所有表格。然而,它们的外观和感觉将与内置的 ABP v3 模块不同。因此,您可能希望将这些模块中的表格转换为 ngx-datatable。为了减少将 abp-table 转换为 ngx-datatable 所需的工作量,我们修改了 ListService 以更好地与 ngx-datatable 配合工作,并引入了两个新指令:NgxDatatableListDirectiveNgxDatatableDefaultDirective

这些指令的使用相当简单:

@Component({
  providers: [ListService],
})
export class SomeComponent {
  data$ = this.list.hookToQuery(
    query => this.dataService.get(query)
  );

  constructor(
    public readonly list: ListService,
    public readonly dataService: SomeDataService,
  ) {}
}

... 以及在组件模板中 ...

<ngx-datatable
  [rows]="(data$ | async)?.items || []"
  [count]="(data$ | async)?.totalCount || 0"
  [list]="list"
  default
>
  <!-- 列模板放在这里 -->
</ngx-datatable>

一旦您通过 NgxDatatableListDirective 绑定了注入的 ListService 实例,您就不再需要担心分页或排序。同样,NgxDatatableDefaultDirective 消除了多个属性绑定,使 ngx-datatable 适应我们的样式。

一个更好的例子

<ngx-datatable
  [rows]="items"
  [count]="count"
  [list]="list"
  default
>
  <!-- 网格操作列 -->
  <ngx-datatable-column
    name=""
    [maxWidth]="150"
    [width]="150"
    [sortable]="false"
  >
    <ng-template
      ngx-datatable-cell-template
      let-row="row"
      let-i="rowIndex"
    >
      <abp-grid-actions
        [index]="i"
        [record]="row"
        text="AbpUi::Actions"
      ></abp-grid-actions>
    </ng-template>
  </ngx-datatable-column>

  <!-- 基础列 -->
  <ngx-datatable-column
    prop="someProp"
    [name]="'::SomeProp' | abpLocalization"
    [width]="200"
  ></ngx-datatable-column>

  <!-- 具有自定义模板的列 -->
  <ngx-datatable-column
    prop="someOtherProp"
    [name]="'::SomeOtherProp' | abpLocalization"
    [width]="250"
  >
    <ng-template
      ngx-datatable-cell-template
      let-row="row"
      let-i="index"
    >
      <div abpEllipsis>{{ row.someOtherProp }}</div>
    </ng-template>
  </ngx-datatable-column>
</ngx-datatable>

迁移时需要做什么?

  • 安装 @swimlane/ngx-datatable 包。
  • 在 angular.json 文件中添加 ngx-datatable 样式。
  • 如果可能,请根据上面的示例更新您的模块。
  • 如果您必须稍后再进行此操作,并计划暂时保留 abp-table,请确保根据此处描述的破坏性变更更新您的分页逻辑。

重要提示: abp-table 尚未被移除,但已被弃用,并将在未来移除。请考虑切换到 ngx-datatable。

扩展系统 [商业版]

扩展系统现已开源,公开可从 @abp/ng.theme.shared/extensions 包获取,而不是 @volo/abp.commercial.ng.ui。此外,根据配置包的新结构,配置是通过上述的 forLazy 静态方法提供的。

迁移时需要做什么?

如果您以前从未使用过扩展系统,则无需执行任何操作。如果您使用过,请再次查看文档以了解发生了哪些变化。扩展系统本身的工作方式与以前相同。唯一的变化是导入的包,以及传递贡献者的静态方法和模块。

Lepton 主题徽标 [商业版]

在 ABP v2.x 中,Lepton 每个颜色主题都有一个浅色和一个深色徽标。我们意识到我们可以让它只使用一个浅色和一个深色徽标。因此,我们改变了 Lepton 查找徽标图像的方式,现在您只需要在项目中有一个 logo-light.png 和一个 logo-dark.png

迁移时需要做什么?

如果您之前切换过模板徽标 PNG,更改很简单:

  • 转到 /assets/images/logo 文件夹。
  • theme1.png 重命名为 logo-light.png,将 theme1-reverse.png 重命名为 logo-dark.png
  • 删除所有其他的 theme*.png 文件。

如果您替换了徽标组件,更改略有不同,但仍然很简单。LayoutStateService 有两个新成员:primaryLogoColorsecondaryLogoColor。它们具有值为 'light''dark' 字符串的可观察流。您可以在自定义徽标组件模板中使用 async 管道使用它们的值。这是一个涵盖主布局和次要(账户)布局徽标的完整示例。

import { AddReplaceableComponent } from '@abp/ng.core';
import { CommonModule } from '@angular/common';
import { APP_INITIALIZER, Component, Injector, NgModule } from '@angular/core';
import { Store } from '@ngxs/store';
import { eAccountComponents } from '@volo/abp.ng.account';
import {
  AccountLayoutComponent,
  eThemeLeptonComponents,
  LayoutStateService,
} from '@volo/abp.ng.theme.lepton';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Component({
  template: `
    <div class="account-brand p-4 text-center mb-1" *ngIf="isAccount; else link">
      <ng-template [ngTemplateOutlet]="link"></ng-template>
    </div>

    <ng-template #link>
      <a [style.background-image]="logoUrl | async" class="navbar-brand" routerLink="/"></a>
    </ng-template>
  `,
})
export class LogoComponent {
  isAccount: boolean;

  logoColor: Observable<'dark' | 'light'>;

  get logoUrl() {
    return this.logoColor.pipe(map(color => `url(/assets/images/logo/logo-${color}.png)`));
  }

  constructor(injector: Injector) {
    const layout = injector.get(LayoutStateService);
    this.isAccount = Boolean(injector.get(AccountLayoutComponent, false));
    this.logoColor = this.isAccount ? layout.secondaryLogoColor : layout.primaryLogoColor;
  }
}

@NgModule({
  imports: [CommonModule],
  declarations: [LogoComponent],
  exports: [LogoComponent],
})
export class LogoModule {}

export const APP_LOGO_PROVIDER = [
  { provide: APP_INITIALIZER, useFactory: switchLogos, multi: true, deps: [Store] },
];

export function switchLogos(store: Store) {
  return () => {
    store.dispatch(
      new AddReplaceableComponent({
        component: LogoComponent,
        key: eThemeLeptonComponents.Logo,
      }),
    );

    store.dispatch(
      new AddReplaceableComponent({
        component: LogoComponent,
        key: eAccountComponents.Logo,
      }),
    );
  };
}

只需将 APP_LOGO_PROVIDER 添加到根模块(通常是 AppModule)的 providers 中,您就会拥有一个能适应主题颜色的自定义徽标组件。

已弃用的接口

一些接口早已被标记为弃用,现在它们已被移除。

迁移时需要做什么?

在本文档中