0%

下载源码

Angular的源码托管在GitHub上,通过git clone命令下载源码。

1
git clone https://github.com/angular/angular.git

创建Angular项目

参考这里创建Angular项目

VSCode或者WebStorm打开项目,找到src/main.ts文件,这是Angular应用的入口文件。可以看到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

此处通过调用platformBrowserDynamic().bootstrapModule(AppModule)来启动应用。那么bootstrapModule是什么呢?通过在Angular源码中搜索bootstrapModule,可以找到如下代码:

1
2
3
4
5
6
7
8
9
10
11
bootstrapModule<M>(
moduleType: Type<M>,
compilerOptions:
| (CompilerOptions & BootstrapOptions)
| Array<CompilerOptions & BootstrapOptions> = [],
): Promise<NgModuleRef<M>> {
const options = optionsReducer({}, compilerOptions);
return compileNgModuleFactory(this.injector, options, moduleType).then((moduleFactory) =>
this.bootstrapModuleFactory(moduleFactory, options),
);
}

快速定位源码的位置

我的习惯是,同时打开两个IDE,一个用来查看应用代码,一个用来查看Angular源码。这样可以快速定位源码的位置。
比如对于上面的bootstrapModule, 则可以复制一份,到Angular源码所在的IDE中(WebStorm), 连按两下Shift键,然后粘贴bootstrapModule,就可以找到源码。

IDE本身提供的Go to Definition/implementation(或者按住Ctrl点击方法名)功能对于源码来说并不好用,只能定位到*.d.ts类型文件,而不能定位到源码文件。

如何调试源码

阅读源码的最好方式就是单步调试,一步一步观察程序是如何运行的。

浏览器调试

Webpack

Angular App默认使用Webpack打包,Webpack打包后的代码是经过压缩和混淆的,不利于调试。但是Webpack提供了SourceMap功能,可以将打包后的代码映射到源码,方便调试。

运行Angular程序后,在浏览器地址栏输入localhost:4200,按键盘上的F12键打开开发者工具,切换到Source选项卡,可以看到项目的源码。在Webpack结点下找到src/main.ts文件,点击行号,可以在代码中打断点。然后按F5刷新页面,就可以看到断点被触发了。
browser-debug

但是当你step into源码时,会发现并没有进入到bootstrapModule方法中,而是停在了core.mjs文件的末尾,这时我们需要在core.mjs文件中找到bootstrapModule方法,然后在方法体内打断点。注意bootstrapModule会在core.mjs文件中出现多次,有些甚至是注释,可以搜索bootstrapModule(来定位。
angular-core

ESBuild

Angular 17以后也支持esbuild打包,而且打包后的代码调试更方便,如下图。
esbuild-debug

WebStorm调试

WebStorm调试十分简单,现在代码中打上断点,然后在Run Configuration列表中选择Angular Application,点击绿色的Debug按钮,就可以开始调试了。
webstorm-debug

VSCode调试

待补充

Anchor

Anchor is a way to jump to a specific location in a page.

  1. create anchor

    1
    <a name="anchor_name"></a>
  2. jump to anchor

    1
    [Link Text](#anchor_name)

Colored text

Markdown itself support a limited features of HTML, so you can use HTML to color text.

1
2
3
<span style="color:red">red</span>
<span style="color:blue">blue</span>
<span style="color:green">green</span>

Change Detection是如何触发的?

前端框架的主要任务就是根据数据的变化来更新UI,Angular也不例外,而变更检测(Change Detection)就是完成这一任务的核心机制。

change-detection-flow

React或者Vue这类框架中,触发UI更新的操作是由开发者手动完成的,比如调用setState或者$set
React中使用setState来触发变更检测。

1
this.setState({ count: this.state.count + 1 });

Vue中使用$set来触发变更检测。

1
this.$set(this.data, 'count', this.data.count + 1);

而在Angular中,变更检测是自动触发的,那么Angular是如何实现自动触发变更检测的呢?要解释清楚这个问题,大家首先要明白javascript这门语言的特殊性,JS的行为在运行时是可以被改写的,也就是说可以做Monkey Patch,而Angular正是利用了这一特性来实现自动变更检测的。

在Angular app启动时,Angular框架会对一些底层的异步事件进行Monkey Patch(在这些事件中加入额外的逻辑),这样就可以在异步事件执行时自动触发变更检测。Angular使用Zone.js来完成这一过程。

比如下面这个addEventListener的例子,Angular会在addEventListener的回调函数执行完后,自动触发变更检测。这就是Monkey Patch的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
function addEventListener(eventName, callback) {
// call the real addEventListener
callRealAddEventListener(eventName, function() {
// first call the original callback
callback(...);
// and then run Angular-specific functionality
var changed = angular.runChangeDetection();
if (changed) {
angular.reRenderUIPart();
}
});
}

在Angular程序启动时,会创建一个Zone,很多人刚开始对Zone比较迷惑,其实它很简单,就是一个执行上下文(Execution context)这个Zone会监控所有的异步操作,当异步操作执行时,Angular会运行变更检测。Zone.js会对浏览器中的所有异步操作进行monkey patch,这样Angular就能知道何时运行变更检测。

这些被monkey patch的异步操作包括

  • setTimeout, setInterval,
  • addEventListener,
  • XMLHttpRequest,
  • fetch,
  • Promise,
  • MutationObserver
  • requestAnimationFrame
  • async/await - 这个会降级到Promise,因为Zone.js无法直接监控async/await,所以会降级到Promise。

这种方式大大降低了开发人员写代码时的心智负担,但是这种方法也有一些弊端,那就是如果某些异步操作无法被Zone.js支持的话,那么Angular就无法对这些异步操作进行变更检测。比如IndexedDB的一些回调函数。

Change Detection是如何工作的?

每一个Angular Component都有一个与之关联的change detector, 这个change detector是在Angular App启动时创建的。当变更检测触发时,Angular会对Template中的每一个Expression进行检查,这个Expression一定是包含着Component中的某个或者某些变量,当该变量的当前值和之前的值不一致时,Angular就会使用新值渲染Template,这样就完成了UI的更新。

当然,这个操作不是一次一次触发的,Angular会对所有变化了的值进行标记,比如isChanged = true,待标记完成后,一次性进行更新。Angular对新旧值进行比较时用的时looseNotIdentical()算法,也就是===,但是对于NaN有个特殊处理,也就是说NaN === NaN返回true(注意,在JS中NaN === NaN返回false)

Angular会对整个Component tree进行检测,并遵循如下原则:(不考虑OnPush的情况下)

  1. 从整个组件树的根结点进行检测。
  2. 组件树中每个结点都被检测。
  3. 检测的方向是自顶向下。
  4. 组件树的遍历算法是深度优先遍历(DFS)算法。

每个被检测到的component都会执行以下操作:

  1. 运行Life Cycle方法
  2. 更新绑定 - Updating bindings.
  3. 刷新UI

一个常见的误解是:Angular在变更检测时会重新渲染整个组件树,这是不对的。Angular足够智能,它只会更新那些发生了变化的部分,而不是整个组件树。

如何关闭某个组件的Change Detection?

很简单,只需要在组件的constructor中detach change detector即可。

1
2
3
4
5
export class MyComponent {
constructor(private cd: ChangeDetectorRef) {
cd.detach();
}
}

这是一个很偏门的需求,使用的场景很少,但是也存在,比如如果后端通过websocket发送了大量的数据,而前端不可能这儿快的实时处理这些数据,所以就折中一下,每隔5秒钟更新一下UI,那么就可以用此方法。

我们现调用detach()方法,这样就可以关闭这个组件的变更检测了,然后再使用setInterval来定时手动触发变更检测,更新UI。

1
2
3
4
5
6
7
8
export class MyComponent {
constructor(private cdf: ChangeDetectorRef) {
this.cdf.detach();
setInterval(() => {
this.cdf.detectChanges();
}, 5000);
}
}

Take the DOM event as an example, when user click a button on page, Angular will run change detection to update the view. Angular will run change detection after the event handler is executed.

Here is the pseudo code, you can find the real code in packages/zone.js/lib/browser/event-target.ts in angular source code.(Note, initially, zone.js is a independent library, but it is moved to angular’s mono-repo.)

1
2
3
4
5
6
7
8
9
10
11
// Store a reference to the original addEventListener
const nativeAddEventListener = window.EventTarget.prototype.addEventListener;

// Replace the original addEventListener with a patched version
window.EventTarget.prototype.addEventListener = function(eventName, handler, options) {
// Call the native addEventListener with the provided arguments
nativeAddEventListener.call(this, eventName, handler, options);

// Additional functionality to track the state of asynchronous operations
// This could involve interacting with Zone.js's internal data structures
};

Change Detection steps

  1. An event occurs (user action, timer, AJAX response, etc.).
  2. Angular’s zone (NgZone) notifies Angular that an async operation has completed.
  3. Angular triggers change detection starting from the root component.
  4. For each component, it checks the template bindings against the current component state.
  5. If a binding has changed, Angular updates the DOM accordingly.
  6. Components using OnPush strategy only check if their inputs have changed or if marked explicitly.
  7. The process continues down the component tree until all components are checked.

What does change detection do in Angular?

  1. Update all bindings in templates.
  2. Update the view.

Angular中,每个Component都会关联一个Change Detector, 这个Change Detector是在Angular App启动时创建的(是所有Component共用一个Change Detector还是每个Component有自己的Change Detector?)。

这个Change Detector的工作就是检查每个Component的Template中的Expression是否发生了变化,如果发生了变化,就会更新View。

NgZone

Angular uses NgZone to detect changes in the application. NgZone is a class that provides a way to run code in the Angular zone. It is used to detect changes in the application.

You can easily see NgZone by logging it to the console.

1
2
3
4
5
6
export class AppComponent {
constructor(zone: NgZone) {
console.log((zone as any)._inner.name); // angular
console.log((zone as any)._outer.name); // root
}
}

NgZone is instantiated during the bootstrap phase.

How to determine when change detection is triggered in a component?

注意:下面这个结论是错误的,留在这里时刻提醒自己,不要再犯这种错误。详情请看这里

You can use the ngDoCheck lifecycle hook to determine when change detection is triggered in a component. ngDoCheck is called every time change detection is triggered in a component.

1
2
3
4
5
export class AppComponent {
ngDoCheck() {
console.log('Change detection triggered');
}
}

ngOnChanges

ngOnChanges is a lifecycle hook that is called when a component’s input properties change. It is used to detect changes in the component’s input properties.

What’s the scenarios to use ngOnChanges?

  1. When you want to detect changes in the component’s input properties.
  2. When you want to avoid function call in template. see here

References

  1. https://angular.love/the-latest-in-angular-change-detection-zoneless-signals - 这个写得太好了,仔细读之。

Install Nginx on Windows

  1. Download Nginx from Nginx official website.
  2. Extract the downloaded file to a folder, for example, C:\nginx.

Build your app

  1. Build your Angular app using the following command: This will generate the dist folder under your project root. for example, D:\codes\angular\angular-router.
    1
    ng build --prod

Setup Nginx

  1. Open a command prompt as an administrator.
  2. Navigate to the Nginx directory.
  3. Open the conf/nginx.conf file in a text editor.
  4. Find the http | server block and add the following line:
    1
    2
    3
    4
    5
    6
    7
    8
    server {
    listen 80;
    server_name localhost;
    location / {
    root D:/codes/angular/angular-router/dist;
    index index.html index.htm;
    }
    }

Start Nginx

  1. Open a command prompt as an administrator.
  2. Navigate to the Nginx directory.
  3. Start Nginx using the following command:
    1
    start nginx

Restart Nginx

1
nginx -s reload

Stop Nginx

1
nginx -s stop

Check Nginx configuration

1
nginx -t

If the configuration is valid, you will see a message like this:

1
2
nginx: the configuration file C:/nginx/conf/nginx.conf syntax is ok
nginx: configuration file C:/nginx/conf/nginx.conf test is successful

不知各位冰雪聪明的朋友们是否遇到过如下错误?

1
NG0100: Expression has changed after it was checked

大家在第一次遇到这个错误的时候是什么反应呢?反正我是一头雾水,今天我就带大家揭开这个错误的神秘面纱,我现在很困。。。非常不想写,但是我还是要写,因为我是一个有责任心的人。

1
为什么我的眼里常含泪水?因为我太困了。。。

言归正传!其实从这个错误的描述来看,聪明的你已经发现了,那就是在Angular刚刚进行完一次变更检测,你又更新了某个值。那么,有哪些场景会导致这个错误呢?我们依次来看。

先来看一个最简单的例子:

Update value in ngAfterViewInit

1
2
<!-- app.component.html -->
<div>{{ count }}</div>
1
2
3
4
// app.component.ts
ngAfterViewInit() {
this.count = 2;
}

这个例子中,我们在ngAfterViewInit生命周期钩子中更新了count的值,在ngAfterViewInit之前,Angular刚刚进行完一次变更检测,我们在ngAfterViewInit中更新了count的值,此时下一次变更检测还未开始,所以就产生了这个错误。

error

他来了,他来了,他迈着箭步走来了!
我们可以点击红框中的文件链接,查看到底哪个变量出了问题。

ng0100

既然错误已经发生了,那么该如何解决它呢?

  1. 不要在ngAfterViewInit中更新值。你可以在其他地方更新,比如用户点击了某个按钮,或者做了其他操作时。
  2. 使用ChangeDetectorRef手动触发变更检测。既然Angular不让我们在变更检测之后更新值,那么我们就在更新值以后手动触发一次变更检测,如下:
    1
    2
    3
    4
    5
    constructor(private cdr: ChangeDetectorRef) {}
    ngAfterViewInit() {
    this.count = 2;
    this.cdr.detectChanges();
    }
  3. 使用异步更新,比如用setTimeoutPromise等包裹一下更新值的操作。因为Angular内部使用zone.js对这些异步操作进行了hook,并自动加入了更新检测,所以这样做是安全的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    ngAfterViewInit() {
    setTimeout(() => {
    this.count = 2;
    });
    }

    // or

    ngAfterViewInit() {
    Promise.resolve().then(() => {
    this.count = 2;
    });
    }

Use random values in template bindings

1
2
<!-- app.component.html -->
<div>{{ randomValue }}</div>
1
2
3
4
// app.component.ts
get randomValue() {
return Math.random();
}

这个例子中,我们在模板中使用了一个随机值,每次变更检测都会重新计算这个值,所以就会产生这个错误。

Update parent component property from child component

We declare a name property in parent component and show it on html template.

1
2
3
4
5
// parent.component.ts
name: string;
constructor() {
this.name = 'Tom';
}
1
2
3
<!-- parent.component.html -->
<p>I'm Father, my name is: {{ name }}</p>
<app-child (onChangeName)="changeName($event)" [age]=30 [name]="'Philip'"></app-child>

Then we update the name property in child component.

1
2
3
4
5
<!-- child.component.html -->
@Output() onChangeName = new EventEmitter<string>();
ngOnInit(): void {
this.onChangeName.emit('Jerry');
}

Run the application, you will see the error in the console.

1
core.mjs:9171 ERROR Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'Tom'. Current value: 'Jerry'

Conclusion

There are several scenarios that produce this error.

  1. value in ngAfterViewInit.
  2. Use random values in template bindings
  3. Update parent component property from child component.

How to solve this problem

  1. Refactor ngAfterViewInit lifecycle hook.
  2. Use ChangeDetectorRef to manually trigger change detection.
  3. Make getters idempotent.
  4. Make the update async(last resort)

Reference

https://angular.io/errors/NG0100

后记

话说我现在实在太困了,睁不开眼睛,写这破玩意能涨工资吗?大概率不能!但是老话儿说的好,凡事求个明白!不扯了,睡觉去了。

你也许在Angular的文档或者某些博客中看到过这样一句话:
Every application has at least one Angular module, the root module, which must be present for bootstrapping the application on launch. By convention and by default, this NgModule is named AppModule

在Angular 14之前,这句话是正确的。但是从Angular 14开始,你可以不用AppModule,因为standalone component可以替代它。下面我们来看看,如何移除AppModule

创建一个Angular项目

1
ng new my-app

将app component设置为standalone

首先要将app.component.ts中的standalone设置为true。如果使用了router,还需要将RouterModule导入到component中。否则在html中使用router-outlet或者routerLink会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
import {Component} from '@angular/core';
import {RouterModule} from "@angular/router";

@Component({
standalone: true, // set standalone to true
selector: 'app-root',
imports: [RouterModule], // import RouterModule if you use router
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'angular-router';
}

创建routes文件

新建一个app-routes.ts文件,用来定义路由,就是将原本在app-routing.module.ts中定义的路由移到这个文件中。

1
2
3
4
5
6
7
8
9
10
11
import {Routes} from "@angular/router";
import {HomeComponent} from "./home/home.component";
import {AboutComponent} from "./about/about.component";
import {NotFoundComponent} from "./not-found/not-found.component";

export const routes: Routes = [
{path: 'home', component: HomeComponent},
{path: 'product', loadChildren: () => import('./product/product.module').then(m => m.ProductModule)},
{path: 'about', component: AboutComponent},
{path: '**', component: NotFoundComponent},
];

删除AppModule

经过上面的改造,我们已经将AppModule中的内容移到了AppComponent中,现在可以删除AppModule了。

  1. 删除app module文件:src/app/app.module.ts
  2. 删除route文件:src/app/app-routing.module.ts。(如果创建项目时选择了router)

修改main.ts

最后,我们还需要修改main.tsmain.ts是app的入口文件,它本来引入的是AppModule,我们需要修改它来引入standalone component

  1. bootstrapModule替换为bootstrapApplication
  2. AppModule替换为AppComponent
  3. 引入importProvidersFrom来加载路由。

修改前

1
2
3
4
5
6
7
8
9
10
11
12
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

修改后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {enableProdMode, importProvidersFrom} from '@angular/core';

import {environment} from './environments/environment';
import {RouterModule} from "@angular/router";
import {routes} from "./app/app.routes";
import {bootstrapApplication} from "@angular/platform-browser";
import {AppComponent} from "./app/app.component";

if (environment.production) {
enableProdMode();
}

bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(RouterModule.forRoot(routes))
]
})
.catch(err => console.error(err));

Angular 17+

从Angular 17开始,使用Angular CLI创建的app默认就是standalone component,不需要再做上面的修改。一个新的Angular 17+项目结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
my-app/
├─ node-modules/
├─ src/
│ ├─ app/
│ │ ├─ app.component.css
│ │ ├─ app.component.html
│ │ ├─ app.component.ts
│ │ ├─ app.config.ts
│ │ ├─ app.routes.ts
│ ├─ index.html
│ ├─ main.ts
├─ angular.json
├─ package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
title = 'my-app';
}

1
2
3
4
5
6
7
8
9
10
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)]
};

1
2
3
4
5
6
7
8
9
10
11
12
13
// app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
{
path: 'home',
component: HomeComponent,
},
{
path: 'product',
component: ProductComponent,
}
];
1
2
3
4
5
6
7
8
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));

To Do List

  1. typescript tag not found on github pages tags.

  2. Angular, AOT vs JIT and Ivy

  3. Angular, How ngc works?

  4. Angular, ngc vs ngcc.

  5. Angular change detection

    1. https://angularindepth.com/posts/1513/from-zone-js-to-zoneless-angular-and-back-how-it-all-works
    2. https://blog.angular-university.io/how-does-angular-2-change-detection-really-work/
    3. https://justangular.com/blog/a-new-era-for-angular-zoneless-change-detection?source=post_page-----babdeb6bc84e--------------------------------
  6. Angular zone

    1. https://dev.to/vivekdogra02/angular-zonejs-change-detection-understanding-the-core-concepts-16ek
    2. https://angularindepth.com/posts/1135/i-reverse-engineered-zones-zone-js-and-here-is-what-ive-found
    3. https://justangular.com/blog/a-change-detection-zone-js-zoneless-local-change-detection-and-signals-story
    4. https://justangular.com/blog/its-ok-to-use-function-calls-in-angular-templates
  7. Angular polyfill(under project root)

  8. Angular data binding.

  9. What’s new in Angular 15

  10. What’s new in Angular 16

  11. What’s new in Angular 17

  12. angular.json - projects - architect - build - options - tsConfig

  13. Angular router: resolver = https://medium.com/@singhkrishnapal0818/angular-resolver-with-example-cf23f6d38f7f

  14. Never use function call in angular template: https://medium.com/showpad-engineering/why-you-should-never-use-function-calls-in-angular-template-expressions-e1a50f9c0496

  15. https://medium.com/ngconf/accessing-route-params-in-angular-1f8e12770617

  16. Angular service scope providedIn: ‘root’, ‘platform’, ‘any’ - https://dev.to/christiankohler/improved-dependeny-injection-with-the-new-providedin-scopes-any-and-platform-30bb, https://angular.io/guide/providers#limiting-provider-scope-with-components

  17. Good angular post:

    1. https://justangular.com/blog
    2. https://blog.angular.io/
    3. https://angulararchitects.io/en/blog
  18. What is browserlist in Angular: https://github.com/browserslist/browserslist

  19. Angular, what is matrix parameters: https://stackoverflow.com/questions/2048121/url-matrix-parameters-vs-query-parameters

SSR in Angular

多页应用
http://www.guoxue.com/shibu/24shi/hansu/hsu_002.htm

  1. https://medium.com/@nishtha.viitorcloud/ssr-server-side-rendering-in-angular-a68bee12cb0d
  2. https://medium.com/@mbleigh/when-should-i-server-side-render-c2a383ff2d0f
  3. https://solutionshub.epam.com/blog/post/what-is-server-side-rendering

Module Federation

  1. code example: https://github.com/module-federation/module-federation-examples
  2. https://medium.com/swlh/webpack-5-module-federation-a-game-changer-to-javascript-architecture-bcdd30e02669 - need pay
  3. https://medium.com/@scriptedalchemy/understanding-webpack-module-federation-a-deep-dive-efe5c55bf366
  4. https://www.angulararchitects.io/en/blog/micro-frontends-with-modern-angular-part-1-standalone-and-esbuild/
  5. https://module-federation.io/
  6. https://www.youtube.com/watch?v=-ei6RqZilYI
  7. https://vugar-005.medium.com/module-federation-series-part-1-a-little-in-depth-258f331bc11e

Native module federation

https://github.com/angular-architects/module-federation-plugin/blob/main/libs/native-federation/README.md

Angular Elements

Angular elements are Angular components packaged as custom elements (also called Web Components), a web standard for defining new HTML elements in a framework-agnostic way

Pure ESM package

https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c

Angular CLI

https://angular.dev/cli

dynamic import

https://medium.com/unalai/%E8%AA%8D%E8%AD%98-dynamic-import-3a6f75da2fc9
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import

Angular lazy loading

https://angular.dev/guide/ngmodules/lazy-loading

Write tools

ASCII tree
markdown table generator, convert table cell contains multiple lines of code to markdown text, see What's new in angular 17 for details

本文讲解一下如何在Angular项目中使用router

路由配置(NgModule based apps)

创建一个Angular项目

1
ng new angular-router

在创建过程中,Angular CLI会询问是否使用router,选择Yes

创建三个组件。

使用WebStorm或者VSCode打开上面创建的项目,呼出Terminal,执行以下命令。

1
2
3
ng generate component home
ng generate component product
ng generate component about

然后打开app-routing.module.ts文件,创建路由。注意,path不能以/开头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// app-routing.module.ts
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {HomeComponent} from "./home/home.component";
import {ProductComponent} from "./product/product.component";
import {AboutComponent} from "./about/about.component";

// Add Routes here
const routes: Routes = [
{path: '', redirectTo: 'home', pathMatch: 'full'},
{path: 'home', component: HomeComponent},
{path: 'product', component: ProductComponent},
{path: 'about', component: AboutComponent}
];

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

添加导航链接

打开app.component.html文件,添加导航链接。注意,routerLink的值要和path一致。如果是根路由,可以使用/。文档末尾的router-outlet是用来显示路由对应的组件的。当点击某一个链接的时候,Angular会找到对应的组件,并将其加载到router-outlet中。所以router-outlet相当于一个占位符,与html中的slot类似。

1
2
3
4
5
6
7
8
9
<h1>Angular Router Demo</h1>
<nav>
<ul>
<li><a routerLink="/home">Home</a></li>
<li><a routerLink="/product">Product</a></li>
<li><a routerLink="/about">About</a></li>
</ul>
</nav>
<router-outlet></router-outlet>

运行项目

Terminal中执行以下命令。打开浏览器,输入http://localhost:4200,就可以看到导航链接了。依次点击页面上的Home,Product,About链接,可以看到页面跳转了,并且浏览器中的地址也发生了变化。

1
ng serve

基于ROUTES的路由配置

ROUTES是Angular内置的一个Injection Token,在一些比较老的Angular app中,也有使用ROUTES来配置路由的,现举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@NgModule({
declarations: [
AppComponent,
ProductComponent,
ParentComponent,
ChildComponent,
HomeComponent,
AboutComponent,
],
imports: [
BrowserModule,
RouterModule.forRoot([]), // This line is must!
],
providers: [
{
provide: ROUTES,
useFactory: () => [
{path: 'home', component: HomeComponent},
{path: 'product', component: ProductComponent},
{path: 'about', component: AboutComponent}
],
multi: true, // This line is must!
},
],
bootstrap: [AppComponent]
})
export class AppModule {
}

useFactory后面跟的是一个工厂函数,这个函数我们直接返回一个路由数组。
使用这种方法后,我们就不需要路由文件app-routing.module.ts了,可以将它删除掉。

路由配置(Standalone apps)

基于standalone组件的app,路由配置稍有不同。

  1. main.ts文件中启动app时会传递appConfig进去,我们的路由配置就在这个appConfig中。
1
2
3
4
5
6
7
// main.ts
import {bootstrapApplication} from '@angular/platform-browser';
import {appConfig} from './app/app.config';
import {AppComponent} from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
  1. app.config.ts文件中通过provideRouter函数提供路由。
1
2
3
4
5
6
7
8
9
10
// app.config.ts
import {provideRouter} from '@angular/router';
import {routes} from './app.routes';

export const appConfig: ApplicationConfig = {
providers: [
// ...
provideRouter(routes),
]
};
  1. app.routes.ts文件中定义路由。
1
2
3
4
5
6
// app.routes.ts
export const routes: Routes = [
{path: 'home', component: HomeComponent},
{path: 'product', component: ProductComponent},
{path: 'about', component: AboutComponent}
];

可见,路由的结构是十分清晰的。

添加404页面

如果我们在浏览器中随意输入一个不存在的路径,比如http://localhost:4200/abc,会发现页面是空白的。这是因为我们没有处理404页面。我们可以在app-routing.module.ts中添加一个**路径,用来匹配所有路径,然后将其重定向到一个404组件。

创建404页面组件

1
ng generate component not-found

修改404页面内容

打开not-found.component.html,输入以下内容。

1
<p>404: Not Found!</p>

添加404路由

1
2
3
4
5
6
7
// app-routing.module.ts
const routes: Routes = [
{path: 'home', component: HomeComponent},
{path: 'product', component: ProductComponent},
{path: 'about', component: AboutComponent},
{path: '**', component: NotFoundComponent}
];

此时,如果在浏览器里面输入一个不存在的路径,比如http://localhost:4200/abc,则会显示404: Not Found!

嵌套路由

如果我们想要在product组件中添加两个子路由,product-aproduct-a,可以在app-routing.module.ts中添加如下代码。

新建两个组件

1
2
ng generate component product-a
ng generate component product-b

在product页面添加router-outlet

这个router-outlet用来显示ProductA和ProductB对应的的组件。

1
2
3
<!-- product.component.html -->
<p>product works!</p>
<router-outlet></router-outlet>

添加子路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const routes: Routes = [
{path: 'home', component: HomeComponent},
{
path: 'product',
// pathMatch: 'full', 注意这一句不能写!否则子路由无法匹配,会导致404!
component: ProductComponent,
children: [
{path: 'a', component: ProductAComponent},
{path: 'b', component: ProductBComponent}
]
},
{path: 'about', component: AboutComponent},
{path: 'about-me', component: AboutmeComponent},
{path: '**', component: NotFoundComponent}
];

此时,如果在浏览器中输入http://localhost:4200/product/a,会显示ProductA works!,输入http://localhost:4200/product/b,会显示ProductB works!。同时也会显示product works!,因为product.component.html中除了有<router-outlet></router-outlet>。还有<p>product works!</p>

严格匹配

上面的例子中,product的path不能使用严格匹配选项pathMath: 'full',否则子路由无法匹配,会导致404。这是为什么呢?
因为匹配子路由的时候,必须先匹配父级路由,而由于父级路由使用了pathMatch: 'full',所以无法匹配,父级路由无法匹配导致子路由的匹配无法进行,所以导致了404!

如果你非要使用pathMatch: 'full',可以如下定义路由表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const routes: Routes = [
{path: 'home', component: HomeComponent},
{
path: 'product',
pathMatch: 'full',
component: ProductComponent,
},
{
path: 'product/a', component: ProductAComponent
},
{path: 'product/b', component: ProductBComponent},
{path: 'about', component: AboutComponent},
{path: 'about-me', component: AboutmeComponent},
{path: '**', component: NotFoundComponent}
];

这样就可以使用pathMatch: 'full'了。但是这么做也有一个弊端,就是a和b从逻辑上讲不再是product子路由了。而是平级路由,只不过路径中包含了字符串product而已。这会导致product页面的内容无法显示。通常父级路由都会显示一些公共信息,差异化的信息放到子路由中。如果你使用了pathMatch: 'full',则会导致父级页面内容无法显示。请酌情使用此方法。

注意,如果你使用了嵌套路由,或者lazy load, 请谨慎使用pathMatch: 'full'。详情请看这里:detail

路由拆分

如果product中的子路由越来越多,都放到一个文件中会显得很臃肿。这时候我们可以将子路由拆分到单独的文件中。将子路由定义到product组件中。

基于NgModule的路由拆分

进入product文件夹,执行以下命令。

1
2
ng generate module product-routing --flat
ng generate module product --flat

--flat选项表示不创建文件夹。

product-routing.module.ts中添加如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// product-routing.module.ts
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ProductBComponent} from "../product-b/product-b.component";
import {ProductAComponent} from "../product-a/product-a.component";
import {ProductComponent} from "./product.component";
import {RouterModule, Routes} from "@angular/router";

const routes: Routes = [
{
path: '', // 注意这里不能写product,因为`app-routing.module.ts`中已经定义了product路由, 并且指定了path: 'product'
component: ProductComponent,
children: [
{path: 'a', component: ProductAComponent},
{path: 'b', component: ProductBComponent}
]
}
];

@NgModule({
imports: [
CommonModule,
RouterModule.forChild(routes),
],
exports: [RouterModule]
})
export class ProductRoutingModule {
}

product.module.ts文件中添加如下代码,导入ProductRoutingModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ProductComponent} from './product.component';
import {ProductRoutingModule} from './product-routing.module';
import {ProductAComponent} from '../product-a/product-a.component';
import {ProductBComponent} from '../product-b/product-b.component';

@NgModule({
declarations: [
ProductComponent,
ProductAComponent,
ProductBComponent
],
imports: [
CommonModule,
ProductRoutingModule
]
})
export class ProductModule {
}

修改app-routing.module.ts
注意,这里必须使用loadChildren, 否则子路由无法显示。

1
2
3
4
5
// app-routing.module.ts
const routes: Routes = [
{path: 'product', loadChildren: () => import('./product/product.module').then(m => m.ProductModule)},
// ...
];

这样就可以将product中的子路由拆分到单独的文件中了。

基于Standalone组件的路由拆分

如果你的组件是standalone的,没有对应的module文件,那么可以使用如下方法拆分路由。注意loadChildren中加载的是路由文件,而不是module文件,也不是component文件。

1
2
// app.routers.ts
{path: 'product', loadChildren: () => import('./product/product.routers').then((routes) => routes.productRouters)},

在Product组件文件夹下新建路由文件product.routers.ts,添加如下代码。

1
2
3
4
5
6
7
8
9
// product.routers.ts
import {Routes} from "@angular/router";
import {ProductComponent} from "./product.component";
import {ProductDetailComponent} from "../product-detail/product-detail.component";

export const productRouters: Routes = [
{path: '', component: ProductComponent},
{path: 'product-detail', component: ProductDetailComponent},
]

注意这种方式下,点击product-detail链接时,会显示product-detail组件。product组件的内容不会显示。

如果要显示product组件的内容,需要将product-detail组件定义为product组件的子路由。如下所示。这种方式在导航时很方便,比如左侧是导航菜单,点击不同菜单显示不同的组件。对于整个页面来说,导航菜单一直显示,而右侧的内容会随着导航的变化而变化。

1
2
3
4
5
6
7
8
9
10
11
12
// product.routers.ts
import {Routes} from "@angular/router";
import {ProductComponent} from "./product.component";
import {ProductDetailComponent} from "../product-detail/product-detail.component";

export const productRouters: Routes = [
{
path: '', component: ProductComponent, children: [
{path: 'product-detail', component: ProductDetailComponent},
]
},
]

监听路由变化

如果我们想要在路由变化的时候做一些事情,比如打印路由信息,可以在app.component.ts中添加如下代码。此时会监听整个app所有路由的变化,如果只想监控某个组件的路由变化,那么可以在该组件中添监听代码。

1
2
3
4
5
6
7
// app.component.ts
constructor(private router: Router) {}
ngOnInit() {
this.router.events.subscribe((event) => {
console.log('Route:', event);
});
}

路由变化时会依次触发以下事件:

  • NavigationStart
  • RoutesRecognized
  • GuardsCheckStart
  • ChildActivationStart
  • ActivationStart
  • GuardsCheckEnd
  • ResolveStart
  • ResolveEnd
  • ActivationEnd
  • ChildActivationEnd
  • NavigationEnd

我们一般在NavigationEnd事件中做一些事情。

1
2
3
4
5
router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
// do something
}
});

注意

即使使用了skipLocationChange选项,也可以通过以上方法获取完整的路由信息,因为skipLocationChange只是不改变浏览器地址栏的URL,但是路由信息还是会变化的。

获取路由信息

如果想要获取路由对应的url,路由中的参数等信息,可以在组件中注入ActivatedRoute服务。

使用ActivatedRoute.snapshot获取路由信息

1
2
// app.routers.ts
{ path: 'product/:id', component: ProductDetailComponent },
1
2
<!-- product.component.html -->
<p>This is product: {{id}}</p>
1
2
3
4
5
6
7
8
9
10
11
12
// product-detail.component.ts
export class ProductDetailComponent implements OnInit {
id: number = -1;
constructor(private activatedRoute: ActivatedRoute) {}

ngOnInit() {
const id = this.activatedRoute.snapshot.paramMap.get('id');
if (id) {
this.id = parseInt(id);
}
}
}

使用ActivatedRoute.params订阅路由变化

1
2
3
4
5
6
7
8
9
10
11
// product-detail.component.ts
export class ProductDetailComponent implements OnInit {
id: number = -1;
constructor(private activatedRoute: ActivatedRoute) {}

ngOnInit() {
this.activatedRoute.params.subscribe((params) => {
this.id = parseInt(params.id);
});
}
}

snapshop和params的区别:

  • snapshot只会获取一次路由信息,它是静态的,获取之后就不会再变化了。
  • params会订阅路由变化,当路由变化时,会重新获取路由信息。

params和queryParams的区别:

  • params: 获取路由中的参数,比如/product/1中的1
  • queryParams: 获取路由中的查询参数,比如/product?id=1&name=zdd中的id, name
  • matrixParameters: 获取路由中的矩阵参数,比如/product;id=1;name=zdd中的id, name

queryParams和matrixParameters的区别

  • queryParams: 使用?开头,使用&分隔每组参数,比如/product?id=1&name=zdd
  • matrixParameters: 使用;开头,使用;分隔每组参数,比如/product;id=1;name=zdd
  • 两者都以=分隔键值对。

路由守卫

待整理

路由传递数据

定义Router时发送数据

1
2
3
4
5
6
// app.routers.ts
{
path: 'product',
component: ProductComponent,
data: {category: 'digital'} // send data to component
},

组件中接收数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// product.component.ts
export class ProductComponent implements OnInit {
constructor(private activatedRoute: ActivatedRoute) {}
ngOnInit() {
const {category} = this.activatedRoute.snapshot.data;
console.log(category);

// or

this.activatedRoute.data.subscribe((data) => {
const {category} = data;
console.log(category);
})
}
}
1
this.router.navigate(['/product'], {state: {category: 'digital'}});

组件中接收数据

After Angular 16, must call this on constructor or you will get null, see here for detail

1
2
3
4
5
6
7
8
// product.component.ts
export class ProductComponent implements OnInit {
constructor(private router: Router) {
const state = this.router.getCurrentNavigation()?.extras.state;
const {category} = state || {category: 'none'};
console.log(category);
}
}

定义routerLink时发送数据

1
<a [routerLink]="['/product']" [state]="{category: 'digital'}">Product</a>

组件中接收数据

1
2
3
4
5
6
7
8
// product.component.ts
export class ProductComponent implements OnInit {
constructor(private router: Router) {
const state = this.router.getCurrentNavigation()?.extras.state;
const {category} = state || {category: 'none'};
console.log(category);
}
}

保持Fragment不丢失

Url中#后面紧跟的内容为Fragment,比如http://localhost:4200/book#b123中的b123就是Fragment

首先在Component中定义Fragment变量

1
2
3
export class AppComponent implements OnInit {
bookId = 'b123';
}

在routerLink中添加Fragment

1
<a [routerLink]="['book', {}]" [fragment]="bookId">Book</a>

在navigate中添加Fragment

1
this.router.navigate(['book'], {fragment: this.bookId});

激活后路由为:http://localhost:4200/book#b123

保持Fragment不变

当我们从一个带Fragment的路由跳转到其他路由时(比如辅助路由),Fragment会丢失。如果我们想要保持Fragment不变,可以在navigate中添加preserveFragment选项。

1
this.router.navigate([{outlets: {primary: 'book/book1/book2', detail: 'book-detail'}}, {preserveFragment: true}]);

此时路由变为:http://localhost:4200/book(detail:book-detail//list:book-list)#b123 - 注意Fragment在url末尾。

注意关闭辅助路由时,也要添加此选项,否则Fragment仍会丢失。

1
this.router.navigate([{outlets: {list: null}}], {preserveFragment: true}); 

此时路由变为:http://localhost:4200/book(detail:book-detail)#b123

1
this.router.navigate([{outlets: {detail: null}}], {preserveFragment: true});

此时路由变为:http://localhost:4200/book#b123

路由三要素

  1. 路由对应的路径及组件 - Routes配置文件
  2. 路由显示的位置 - Outlet
  3. 路由的触发方式
    • 点击触发,比如routerLink, button等。
    • 编程触发,比如navigatenavigateByUrl等。
    • 动态加载,比如动态创建组件并加载。

待商榷:

  1. 没有module时如何拆分router文件? - 可以直接使用路由module文件,不需要组件对应的module文件。目前standalone组件无法做到路由文件的拆分,必须有module支持。

https://www.angulararchitects.io/en/blog/routing-and-lazy-loading-with-standalone-components/

Angular中如何实现嵌套路由。

需求,一个Angular App,根路由包含三个组件,home, product, aboutproduct组件下有两个子路由product-listproduct-detail。如何实现这个需求呢?

首先定义路由文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { HelpComponent } from './help/help.component';
import { AbcComponent } from './home/abc/abc.component';
import { DefComponent } from './home/def/def.component';

const routes: Routes = [
{
path: 'home',
component: HomeComponent,

},
{
path: 'about',
component: AboutComponent,
},
{
path: 'product',
component: HelpComponent,
children: [
{
path: 'product-list',
component: AbcComponent,
},
{
path: 'product-detail',
component: DefComponent,
},
],
},
];

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

然后在template文件中定义导航链接:

1
2
3
4
5
6
7
<!-- app.component.html -->
<nav>
<a routerLink="/home">Home</a>
<a routerLink="/about">About</a>
<a routerLink="/product">Product</a>
</nav>
<router-outlet></router-outlet>

最后在根模块中引入路由模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// app.component.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
import { ProductComponent } from './product.component';

@NgModule({
declarations: [
AppComponent,
AboutComponent,
ProductComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}

product组件的模板文件中定义子路由导航:

1
2
3
4
5
6
<!-- product.component.html -->
<nav>
<a routerLink="/product/product-list">Product List</a>
<a routerLink="/product/product-detail">Product Detail</a>
</nav>
<router-outlet></router-outlet>

策略模式

策略模式是一种行为设计模式,它定义了一系列算法,将每个算法封装到一个类中,并使它们可以相互替换。策略模式让算法独立于使用它的客户端。

现实中的例子

假设我们经营一家电影院,目前是销售淡季,我们推出了各种折扣,学生票八折,儿童票九折,VIP会员半价。我们要设计一个系统,根据不同的折扣算法计算电影票的价格。

不假思索的你可能写下了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
enum UserType {
Regular,
Student,
Child,
VIP,
}

class MovieTicket {
private ticketPrice = 100;
private userType?: UserType;

setUserType(userType: UserType) {
this.userType = userType;
}

getPrice() {
switch (this.userType) {
case UserType.Student:
return this.ticketPrice * 0.8;
case UserType.Child:
return this.ticketPrice * 0.9;
case UserType.VIP:
return this.ticketPrice * 0.5;
default:
return this.ticketPrice;
}
}
}

const ticket = new MovieTicket();
ticket.setUserType(UserType.Student);
console.log(ticket.getPrice());

我们来看看这段代码有什么问题:

  1. 违反了开闭原则。如果我们要添加新的折扣类型,比如老年人票,就需要修改MovieTicket类,违反了开闭原则。
  2. 违反了单一职责原则。MovieTicket类不仅负责计算价格,还负责判断折扣类型,违反了单一职责原则。
  3. 耦合性太高,不易维护。如果折扣算法发生变化,需要修改MovieTicket类,不易维护。
  4. 其他类如果想要使用折扣算法的话,只能复制粘贴一份,导致代码重复。

下面我们通过策略模式来重构这段代码,策略模式的精髓就是将算法和使用算法的类解耦,使得算法可以独立于使用它的类进行变化。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
interface Discount {
getDiscount(): number;
}

class StudentDiscount implements Discount {
getDiscount(): number {
return 0.8;
}
}

class ChildrenDiscount implements Discount {
getDiscount(): number {
return 0.9;
}
}

class VIPDiscount implements Discount {
getDiscount(): number {
return 0.5;
}
}

class MovieTicket {
private ticketPrice = 100;
private discount?: Discount;

setDiscount(discount: Discount) {
this.discount = discount;
}

getPrice() {
if (this.discount) {
return this.ticketPrice * this.discount.getDiscount();
}

throw new Error("No discount set");
}
}

const ticket = new MovieTicket();
ticket.setDiscount(new StudentDiscount());
console.log(ticket.getPrice());

代码解析:

  1. 首先定义一个接口Discount,它有一个getDiscount方法,返回折扣值。所有具体的折扣类都要实现这个接口。
  2. 然后定义三个具体的折扣类:StudentDiscountChildrenDiscountVIPDiscount。在每个折扣类中,我们实现了getDiscount方法,返回不同的折扣值。
  3. 最后定义一个MovieTicket类,它有一个ticketPrice属性,表示电影票的价格,还有一个discount属性,表示折扣。我们定义了一个setDiscount方法,用来设置折扣。getPrice方法用来计算折扣后的价格。

这种实现使得折扣算法和使用折扣的类解耦,使得折扣算法可以独立于使用它的类进行变化。好处如下:

  1. 便于扩展,可以很容易地添加新的折扣类。只要这个类实现了Discount接口,就可以被MovieTicket类使用。
  2. 由于折扣算法和使用折扣的类解耦,所以折扣算法可以独立于使用它的类进行变化。如果折扣算法发生变化,只需要修改折扣类即可,不需要修改使用折扣的类, 比如儿童票从9折改成8折,只需要修改ChildrenDiscount类使其返回0.8即可。
  3. 如果有其他类想使用折扣算法的话,也可以直接使用。

以上代码其实使用了依赖注入的方式,MovieTicket类依赖于Discount接口,我们通过Setter的方式将具体的折扣类注入到MovieTicket类中。

注意这里discount属性是可选的,因为我们在getPrice方法中判断了discount是否存在,如果不存在就抛出异常。这样做是为了防止忘记设置折扣而导致的错误。如果不设置为可选的话会报错:Property 'discount' has no initializer and is not definitely assigned in the constructor. 由于TypeScript配置文件默认开启了strictPropertyInitialization选项导致的。

策略模式UML图

image

  • Context:消费策略的类相当于上例中的MovieTicket类,内涵一个抽象策略类,相当于上例中的Discount接口。
  • Strategy:抽象策略类,相当于上例中的Discount接口。
  • ConcreteStrategy:具体策略类,相当于上例中的StudentDiscountChildrenDiscountVIPDiscount类。
  • execute:执行策略的方法,相当于上例中的getDiscount方法。

策略模式的应用场景

  1. Sorting Algorithms: If you have a collection of data that can be sorted in several ways, you can use the Strategy pattern to switch between sorting algorithms (like quicksort, merge sort, heap sort) at runtime.
  2. Payment Methods: In an e-commerce application, you can use the Strategy pattern to switch between different payment methods (like credit card, PayPal, Bitcoin) at runtime.
  3. Compression Algorithms: If you have a file that can be compressed using different algorithms (like zip, rar, 7z), you can use the Strategy pattern to choose the compression algorithm at runtime.
  4. Travel Planning: If you have a travel planning application, you can use the Strategy pattern to switch between different travel strategies (like by car, by train, by plane) at runtime.