0%

Angular builders负责构建,测试和运行Angular项目。Angular CLI使用builders来执行这些任务。以下是@angular-devkit/build-angular一些常见的builders:

Name Description
application Build an Angular application targeting a browser and server environment using esbuild.
app-shell Build an Angular App shell.
browser Build an Angular application targeting a browser environment using Webpack.
browser-esbuild Build an Angular application targeting a browser environment using esbuild.
dev-server A development server that provides live reloading.
extract-i18n Extract i18n messages from an Angular application.
karma Execute unit tests using Karma test runner.
ng-packagr Build and package an Angular library in Angular Package Format (APF) format using ng-packagr.
prerender Prerender pages of your application. Prerendering is the process where a dynamic page is processed at build time generating static HTML.
server Build an Angular application targeting a Node.js environment.
ssr-dev-server A development server which offers live reload during development, but uses server-side rendering.
protractor Deprecated - Run end-to-end tests using Protractor framework.

Builders的源码位于Angular CLI项目中。

  • application: packages/angular/build/src/builders/application/index.ts
  • browser: packages/angular_devkit/build_angular/src/builders/browser/index.ts
  • browser-esbuild: packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts
  • dev-server
    • ESBuild + Vite: packages/angular/build/src/builders/dev-server/index.ts
    • Webpack: packages/angular_devkit/build_angular/src/builders/dev-server/index.ts

Introduction

The angular.json file under the root of an Angular workspace provides workspace-wide and project-specific configuration defaults. It is used by the Angular CLI to build, test, and run Angular projects.

Path values given in the configuration are relative to the root of workspace directory.

Schema

You can find the schema file of angular.json from here: ./node_modules/@angular/cli/lib/config/schema.json

Configuration of angular.json

Turn on chunk named

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"projects": {
"my-project": {
"architect": {
"build": {
"options": {
"namedChunks": true
}
}
}
}
}
}

Named chunk is useful for lazy loading component, you can see which chunk was loaded from the network tab in Chrome.

Turn on source map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"projects": {
"my-project": {
"architect": {
"build": {
"configurations": {
"production": {
"sourceMap": true
}
}
}
}
}
}
}

Source map is useful for debugging, it will generate a source map file for each JavaScript file. For example, main.xxx.js will have a main.xxx.js.map file when source map was on.

Turn off optimization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"projects": {
"my-project": {
"architect": {
"build": {
"configurations": {
"production": {
"optimization": {
"scripts": true,
"styles": false,
"fonts": false
}
}
}
}
}
}
}
}

This option is useful when you want to debug the production build, with above option, it will not minify the scripts, but styles and fonts are still minified.

References

https://angular.dev/reference/configs/workspace-config

这篇文章讨论一下在 Angular/Nx 项目中 tsconfig.json 是如何工作的,本文使用的环境如下:

  • Node: 20.11.0(node -v)
  • NPM: 10.2.4(npm -v)
  • Angular CLI: 17.3.7(ng version)
  • Typescript: 5.4.2(in package.json —by project, or tsc -v —globally)
  • Nx: 15.2.1(nx --version)

1. 什么是 tsconfig.json

tsconfig.json 是 TypeScript 的配置文件,用于配置 TypeScript 编译器的行为。目前很多框架都使用Typescript,自然也就需要配置 tsconfig.json 文件。

2. Angular Single Project

Angular框架原生支持Typescript,所以在Angular项目中,使用Angular CLI创建Angular项目后,项目的文件结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
my-app/
├─ node_modules/
├─ src/
│ ├─ index.html
│ ├─ main.ts
│ ├─ ...
├─ angular.json
├─ package.json
├─ tsconfig.app.json
├─ tsconfig.json
├─ tsconfig.spec.json
├─ ...

与tsconfig相关的文件有如下三个:

  • tsconfig.json
  • tsconfig.app.json
  • tsconfig.spec.json

2.1 tsconfig.json

先看一下 tsconfig.json 文件,这是该项目总的配置文件,用来做一些通用配置,其他tsconfig文件可以继承这个文件。

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
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": [
"ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

2.2 tsconfig.app.json

tsconfig.app.json 文件是 Angular app的编译配置文件,由第一行"extends": "./tsconfig.json"可知,它继承了 tsconfig.json 文件的配置,同时也可以覆盖 tsconfig.json 文件的配置。

  • files:
  • include: "src/**/*.d.ts" 表示包含所有位于src目录下的Typescript类型定义文件(*.d.ts)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts"
],
"include": [
"src/**/*.d.ts"
]
}

2.3 tsconfig.spec.json

tsconfig.spec.json 文件是 Angular app的测试配置文件,由第一行"extends": "./tsconfig.json"可知,它也继承了 tsconfig.json 文件。在include选项中,包含了所有的测试文件(.spec.ts)和类型定义文件(.d.ts)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

如果你尝试将src/**/*.spec.tsinclude选项中删除,再运行npm run test,将会遇到如下错误。

1
An unhandled exception occurred: error TS18003: No inputs were found in config file '.../my-app/tsconfig.spec.json'. Specified 'include' paths were '["src/**/*.d.ts"]' and 'exclude' paths were '["./out-tsc/spec"]'

由上面的分析可知,Angular项目中的 tsconfig.json 文件是一个全局配置文件,tsconfig.app.json掌管app的编译选项,tsconfig.spec.json负责测试文件的配置,这两者分别继承了 tsconfig.json 文件,同时可以做一些个性化的配置。

那么tsconfig.app.jsontsconfig.spec.json文件是如何被使用的呢?这就需要分析另一个文件:angular.json

2.4 angular.json

可以看到在projects | my-app | architect | build | options中,有一个tsConfig选项,这个选项指定了tsconfig.app.json文件的路径,这就是告诉Angular CLI在编译app时要使用tsconfig.app.json文件。

同时,在projects | my-app | architect | test | options中,有一个tsConfig选项,这个选项指定了tsconfig.spec.json文件的路径,这就是告诉Angular CLI在编译测试文件时要使用tsconfig.spec.json文件。

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
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"my-app": {
"projectType": "application",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"tsConfig": "tsconfig.app.json", // 1 <-- here
//...
}
},
// ...
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json", // 2 <-- here
// ...
}

总结一下:

  • tsconfig.json 是全局配置文件
  • tsconfig.app.json 是app的编译配置文件,继承了tsconfig.json文件。
  • tsconfig.spec.json 是测试文件的配置文件,继承了tsconfig.json文件。
  • angular.json 文件中的tsConfig选项指定了tsconfig.app.jsontsconfig.spec.json文件的路径

上图:
angular-single-project-tsconfig

3. Angular Multi Project

关于如何创建Angular多项目应用,可以参考这篇

多项目应用的文件结构如下,projects目录负责存放所有的子项目,这里有两个子项目:mfe1和shell。
每个子项目分别有自己的tsconfig.app.jsontsconfig.spec.json文件,这两个文件分别继承了全局的tsconfig.json文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
my-app/
├─ node_modules/
├─ projects/
│ ├─ mfe1/
│ │ ├─ ...
│ │ ├─ tsconfig.app.json
│ │ ├─ tsconfig.spec.json
│ ├─ shell/
│ │ ├─ ...
│ │ ├─ tsconfig.app.json
│ │ ├─ tsconfig.spec.json
├─ ...
├─ angular.json
├─ package.json
├─ tsconfig.json

angular.json文件中的tsConfig选项指定了每个子项目的tsconfig.app.jsontsconfig.spec.json文件的路径。

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
43
44
45
46
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"shell": { // project shell <-- here
"projectType": "application",
// ...
"architect": {
"build": {
"builder": "ngx-build-plus:browser",
"options": {
// ...
"tsConfig": "projects/shell/tsconfig.app.json", // 1 <-- here
},
},
// ...
"test": {
// ...
"tsConfig": "projects/shell/tsconfig.spec.json", // 2 <-- here
}
}
},
"mfe1": { // project mfe1 <-- here
"projectType": "application",
// ...
"architect": {
"build": {
"builder": "ngx-build-plus:browser",
"options": {
// ...
"tsConfig": "projects/mfe1/tsconfig.app.json", // 3 <-- here
},
},
// ...
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
// ...
"tsConfig": "projects/mfe1/tsconfig.spec.json", // 4 <-- here
}
}
}
}
}
}

总结一下:
多项目Angular应用的tsconfig文件使用情况

  • tsconfig.json 是全局配置文件,位于workspace根目录下
  • 每个子项目有自己的tsconfig.app.jsontsconfig.spec.json文件,分别继承了全局的tsconfig.json文件
  • angular.json 文件中的tsConfig选项指定了每个子项目的tsconfig.app.jsontsconfig.spec.json文件的路径

上图:
angular-multiple-projects-tsconfig

4. Nx Mono Repo

使用Nx创建的Mono Repo项目,Nx会自动创建一个tsconfig.base.json文件,这个文件是全局的配置文件,所有的子项目都会继承这个文件。并且没有angular.json文件,每个子项目有自己的project.json文件,负责引用子项目中的tsconfig文件。

假设我们的mono repo包含一个app,名为todo, 一个library, 名为data-access,则文件结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Nx mono repo/
├─ apps/
│ ├─ todo/
│ │ ├─ ...
│ │ ├─ project.json
│ │ ├─ tsconfig.app.json
│ │ ├─ tsconfig.json
│ │ ├─ tsconfig.spec.json
├─ libs/
│ ├─ data-access/
│ │ ├─ ...
│ │ ├─ project.json
│ │ ├─ tsconfig.json
│ │ ├─ tsconfig.lib.json
│ │ ├─ tsconfig.spec.json
├─ node_modules/
├─ package.json
├─ tsconfig.base.json
├─ ...

总结一下:

  • tsconfig.base.json 是全局配置文件,位于workspace根目录下
  • app项目有自己的tsconfig.app.jsontsconfig.spec.json文件以及tsconfig.json,继承关系如下:
    • tsconfig.app.json继承tsconfig.json
    • tsconfig.spec.json继承tsconfig.json
    • tsconfig.json继承tsconfig.base.json
  • library项目有自己的tsconfig.lib.jsontsconfig.spec.json文件以及tsconfig.json,继承关系如下:
    • tsconfig.lib.json继承tsconfig.json
    • tsconfig.spec.json继承tsconfig.json
    • tsconfig.json继承tsconfig.base.json
  • 每个子项目的project.json文件负责引用子项目中的tsconfig文件

一图胜千言
nx-mono-repo-tsconfig

今天做Code Review的时候看到如下一行代码,之前没有见过这种写法,查了一下资料,发现这是TypeScript中的类型断言。

1
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

其实类型断言我也之前也用过,不过用的是as关键字,如下:

1
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

这两者效果一样。TypeScript中的类型断言有两种写法,一种是<Type>,另一种是as Type。这两种写法是等价的,但是在React中,<Type>会和JSX的语法冲突,所以推荐使用as Type的写法。

Introduction

今天我们来介绍一下Angular中的NavigationExtras接口,我们在使用Router进行导航时,可以通过NavigationExtras接口来传递额外的导航参数。

NavigationExtras接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
interface NavigationExtras {
override relativeTo?: ActivatedRoute;
override queryParams?: Params;
override fragment?: string;
override queryParamsHandling?: QueryParamsHandling;
override preserveFragment?: boolean;
override onSameUrlNavigation?: OnSameUrlNavigation;
override skipLocationChange?: boolean;
override replaceUrl?: boolean;
override state?: { [k: string]: any; };
readonly override info?: unknown;
}

relativeTo

relativeTo属性是一个ActivatedRoute对象,用来指定导航的相对路径。假设有如下路由配置,ParentComponent下有两个子路由ListComponentChildComponent.

1
2
3
4
5
6
7
8
9
10
11
[{
path: 'parent',
component: ParentComponent,
children: [{
path: 'list',
component: ListComponent
},{
path: 'child',
component: ChildComponent
}]
}]

如果我们当前位于ChildComponent,想要导航到ListComponent,可以使用relativeTo属性来指定相对路径,其中..表示上一级路径,也就是ParentComponent../list表示导航到parent/list路径,也就是ParentComponent下的ListComponent`。

1
2
3
4
5
6
7
@Component({...})
class ChildComponent {
constructor(private router: Router, private route: ActivatedRoute) {}
go() {
router.navigate(['../list'], { relativeTo: this.route });
}
}

queryParams

queryParams属性是一个Params对象,用来指定导航时的查询参数。比如我们要导航到/list路径,并且带上查询参数page=1,可以使用queryParams属性。

1
2
3
4
5
// Navigate to /results?page=1
router.navigate(['/results'], { queryParams: { page: 1 } });

// Navigate to /results?page=1&sort=desc
router.navigate(['/results'], { queryParams: { page: 1, sort: 'desc' } });

fragment

fragment属性是一个字符串,用来指定导航时的fragmentfragment是以#号开头的标识符。比如我们要导航到/list路径,并且带上片段标识符top,可以使用fragment属性。

1
2
// Navigate to /results#top
router.navigate(['/results'], { fragment: 'top' });

queryParamsHandling

queryParamsHandling属性是一个枚举类型,用来指定导航时如何处理查询参数。有以下几种取值:

  • preserve : Preserve parameters in original url and discard new parameters in target url
  • merge : Merge target url query params with original url parameters.

在下面的代码中使用了preserve参数,表示保留原始url中的查询参数。而目标url中的查询参数{page: 2}将被忽略。导航结束后,url为/view2?page=1

1
2
3
// from /view1?page=1 to /view2?page=1
router.navigate(['/view2'], { queryParams: { page: 2 }, queryParamsHandling: "preserve"
});

在下面的代码中使用了merge参数,表示将目标url中的查询参数与原始url中的查询参数合并。导航结束后,url为/view2?page=1&otherKey=2

1
2
3
// from /view1?page=1 to /view2?page=1&otherKey=2
router.navigate(['/view2'], { queryParams: { otherKey: 2 }, queryParamsHandling: "merge"
});

需要注意,如果源url和目标url中都有相同的查询参数,那么目标url中的查询参数会覆盖源url中的查询参数。下面的例子中,导航后,url为/view2?page=2&id=2page参数的值被目标url中的值覆盖了。

1
2
// from /view1?page=1 to /view2?page=2&id=2
router.navigate(['/view2'], { queryParams: { page: 2, id: 2 }, queryParamsHandling: "merge"});

preserveFragment

preserveFragment属性是一个布尔值,用来指定导航时是否保留原始url中的fragment标识符。如果设置为true,则保留原始url中的片段标识符,否则忽略。默认值为false

1
2
// from /view1#top to /view2#top
router.navigate(['/view2'], { preserveFragment: true });
1
2
3
4
// from /view1#top to /view2
router.navigate(['/view2']);
// or
router.navigate(['/view2'], { preserveFragment: false });

onSameUrlNavigation

onSameUrlNavigation属性是一个枚举类型,用来指定当导航到相同url时的行为。有以下几种取值:

  • reload : Reload the page
  • ignore : Ignore the navigation
1
2
// from /view1 to /view1
router.navigate(['/view1'], { onSameUrlNavigation: 'reload' });

skipLocationChange

skipLocationChange属性是一个布尔值,用来指定是否在url中添加一个新的历史记录。如果设置为true,则不会添加新的历史记录,导航结束后,浏览器的历史记录不会改变。默认值为false

假设有如下导航:view1 -> view2 -> view3, 如果在view2 -> view3的导航中设置skipLocationChangetrue,则导航结束后,浏览器的历史记录为view1 -> view2,而不是view1 -> view2 -> view3。导航结束后,浏览器的url为/view2,点击浏览器的回退按钮,会回到/view1

注意以上只是本质行为,还有一个视觉行为,如果设置为true,则导航结束后,浏览器的url不会改变,但是页面内容会改变。这样用户在浏览器的历史记录中看不到导航记录。

以下代码从/view1导航到/view2,导航结束后,浏览器的url不会改变,仍然是/view1,但是页面内容会改变, 显示/view2的内容。

1
2
// from /view1 to /view2
router.navigate(['/view2'], { skipLocationChange: true });

replaceUrl

replaceUrl属性是一个布尔值,用来指定导航时是否替换当前的历史记录。如果设置为true,则替换当前的历史记录,默认值为false

假设有如下导航,view1 -> view2 -> view3,如果在view2 -> view3的导航中设置replaceUrltrue,则导航结束后,浏览器url为/view3 浏览器的历史记录为view1 -> view3,而不是view1 -> view2 -> view3(view2view3替换掉)。导航结束后,浏览器的url为/view3,点击浏览器的回退按钮,会回到/view1

1
2
// from /view2 to /view3
router.navigate(['/view3'], { replaceUrl: true });

state

待续

info

待续

References

写好代码不是一件容易的事,代码能够按照预期效果工作只是最低要求,在这个基础上将代码写得简洁,可维护性高,可以说是一门艺术,今天读了一本书:《The art of readable code》,感触颇深,现摘取一例,与大家分享,为了便于阅读,我将代码简化并用JS重写了一下,先看原始代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function reply(userResult, permissionResult) {
if (userResult === "SUCCESS") {
if (permissionResult !== "SUCCESS") {
console.log("error reading permissions");
console.log("done");
return;
}
console.log("success");
} else {
console.log(userResult);
}
console.log("done");
}

很简单的一段代码,就是根据输入参数的不同取值,打印不同的log,但是代码里一个嵌套的if语句,导致可读性下降,即使经验再丰富的程序员,也要停留思考一下。罗马不是一天建成的,可读性差的代码也不是一天写成的,通常随着功能的增加,维护人员的增多,代码的可读性会越来越差,其实这段代码一开始逻辑很简单。就是根据userResult的值不同,做不同的处理。

1
2
3
4
5
6
7
8
function reply(userResult) {
if (userResult === "SUCCESS") {
console.log("success");
} else {
console.log(userResult);
}
console.log("done");
}

然后突然有一天增加了一个需求,需要根据permissionResult的值来做进一步的处理,于是该函数多了一个参数,处理逻辑也跟着复杂起来,这便有了文章开头那段代码.

那么如何优化这段代码呢?可以看到外层的if有点头重脚轻的感觉,我们可以采用优先处理negative case的方式,这样可以减少嵌套,提高代码的可读性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function reply(userResult, permissionResult) {
if (userResult !== "SUCCESS") {
console.log(userResult);
console.log("done");
return;
}

if (permissionResult !== "SUCCESS") {
console.log("error reading permissions");
console.log("done");
return;
}

console.log("success");
console.log("done");
}

至此,我们就移出了嵌套的if,使这两个if变为平级结构,可读性好多了。需要注意的是,因为我们在两个if中都使用了return(return early principle),所以需要将末尾的console.log("done")上提到每个if中,否则会导致逻辑丢失。

原书写到这里就结束了,但是我想再进一步,可以改进后的代码里仍然有多个重复的console.log("done"),我们可以将其提取出来,别忘了那句名言:Don't repeat yourself

在改动之前,先讲一个原理。

if{..., return}结构转换为if-else结构

如果一个函数中有多个if,每个if中都有return,可以考虑将其转换为if-else结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
if (condition1) { // 1
// code block
return;
}

if (condition2) { // 2
// code block
return;
}

// code block
}

等同于:

1
2
3
4
5
6
7
8
9
function foo() {
if (condition1) { // 1
// code block
} else if (condition2) { // 2
// code block
} else {
// code block
}
}

所以之前的代码可以继续优化为下面的形式

1
2
3
4
5
6
7
8
9
10
11
12
function reply(userResult, permissionResult) {
if (userResult !== "SUCCESS") {
console.log(userResult);
console.log("done");
} else if (permissionResult !== "SUCCESS") {
console.log("error reading permissions");
console.log("done");
} else {
console.log("success");
console.log("done");
}
}

最后把重复的console.log("done")提取出来即可。

1
2
3
4
5
6
7
8
9
10
function reply(userResult, permissionResult) {
if (userResult !== "SUCCESS") {
console.log(userResult);
} else if (permissionResult !== "SUCCESS") {
console.log("error reading permissions");
} else {
console.log("success");
}
console.log("done");
}

最后需要注意的是,这种逻辑性的改动,必须经过严密的单元测试才可以。因为只有两个参数,两两组合,所以测试用例有4个:

1
2
3
4
userResult = "SUCCESS", permissionResult = "SUCCESS";
userResult = "SUCCESS", permissionResult = "ERROR";
userResult = "ERROR", permissionResult = "SUCCESS";
userResult = "ERROR", permissionResult = "ERROR";

Generate a new Angular project

1
ng new my-app

Generate a new empty workspace

1
ng new my-workspace --create-application=false

Generate a new application in the workspace

1
2
cd my-workspace
ng generate application shell

Install package for a specific project

When you working on a workspace with multiple projects, you can install a package for a specific project.

1
ng add @angular-architects/module-federation --project shell

References:

https://angular.dev/cli

Introduction

This article will introduce Angular Module Federation, a new feature in Webpack 5 that allows you to share code between Angular applications.

Module Federation is a new feature in Webpack 5 that allows you to share code between applications. It enables your application to load remote modules at runtime from another application and share dependencies between applications. This feature is particularly useful for micro-frontends, where you have multiple applications that need to share code.

Steps

Create an empty angular workspace

Option --no-create-application is used to create an empty workspace without an initial application.

1
ng new angular-module-federation --no-create-application

Generate shell application shell

1
2
cd angular-module-federation
ng generate application shell

Generate remote application mfe1

1
ng generate application mfe1

Add plugin @angular-architects/module-federation to shell app

1
ng add @angular-architects/module-federation --project shell --port 4200 --type host

This command will do the following:

  1. Add @angular-architects/module-federation and ngx-build-plus to file package.json under workspace root.

  2. Add command "run:all": "node node_modules/@angular-architects/module-federation/src/server/mf-dev-server.js" to scripts section in file package.json. This command will start the module federation development server, which will run app shell and app mfe1 at the same time.

  3. Add file bootstrap.ts under projects/shell/src folder, its content was copied from main.ts

    1
    2
    3
    4
    5
    6
    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));
  4. Update file projects/shell/src/main.ts to import bootstrap.ts(Note, when a file was imported, it was executed automatically)

    1
    import './bootstrap';
  5. Add file webpack.config.js under projects/shell folder,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
    module.exports = withModuleFederationPlugin({
    remotes: {
    "mfe1": "http://localhost:4200/remoteEntry.js",
    },
    shared: {
    ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
    },
    });
  6. Add file webpack.prod.config.js under projects/shell folder,

    1
    module.exports = require('./webpack.config');
  7. Update file angular.json under workspace root.

    1. projects/shell/architect/build/builder: "@angular-devkit/build-angular:application" —> ngx-build-plus:browser - The reason for this change is that module federation is a feature provided by WebPack, but "@angular-devkit/build-angular:application" is for ESBuild, so we need to change it to ngx-build-plus:browser which is for WebPack. see here for details.
    2. projects/shell/architect/build/builder/extraWebpackConfig: "projects/shell/webpack.config.js"
    3. projects/shell/architect/build/configurations/production/extraWebpackConfig: "projects/shell/webpack.prod.config.js"
    4. projects/shell/architect/serve/configurations/extraWebpackConfig: "projects/shell/webpack.prod.config.js"
    5. projects/shell/architect/serve/options:
      1
      2
      3
      4
      5
      "options": {
      "port": 4200,
      "publicHost": "http://localhost:4200",
      "extraWebpackConfig": "projects/shell/webpack.config.js"
      }

Note that: extraWebpackConfig is an option from ngx-build-plus, it allows you to add additional webpack configuration to the project.

Add plugin @angular-architects/module-federation to mfe1 app

1
ng add @angular-architects/module-federation --project mfe1 --port 4201 --type remote

This command will do the same thing as the previous command, but for the mfe1 app with a little difference.

  1. webpack.config.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');

    module.exports = withModuleFederationPlugin({
    name: 'mfe1',
    exposes: {
    './Component': './projects/mfe1/src/app/app.component.ts',
    },
    shared: {
    ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
    },
    });
  2. Serve options in angular.json
    1
    2
    3
    4
    5
    "options": {
    "port": 4201,
    "publicHost": "http://localhost:4201",
    "extraWebpackConfig": "projects/mfe1/webpack.config.js"
    }

Add home component to shell app

1
ng generate component home --project=shell

Add product component to mfe1 app

app mfe1 will expose a component product to shell app.

1
ng generate component product --project=mfe1

Update webpack.config.js in app mfe1

We’ll expose the product component to the shell app.

1
2
3
4
5
6
7
8
9
10
11
const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');

module.exports = withModuleFederationPlugin({
name: 'mfe1',
exposes: {
'./Component': './projects/mfe1/src/app/product/product.component.ts',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});

Update app.routers.ts in app mfe1

1
2
3
4
5
6
export const routes: Routes = [
{
path: 'product',
component: ProductComponent,
}
];

Update webpack.config.js in app shell

Note the generated port in webpack.config.js, its value is 4200, we should change it to 4201 since mfe1 runs on this port.

1
2
3
4
5
6
7
8
9
const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
remotes: {
"mfe1": "http://localhost:4201/remoteEntry.js", // <--- 4201
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});

Update app.routes.ts in app shell

Add a route path product to load the product component remotely from mfe1.

1
2
3
4
5
6
7
8
9
10
export const routes: Routes = [
{
path: 'home',
component: HomeComponent,
},
{
path: 'product',
loadChildren: () => import('mfe1/Component').then(m => m.ProductComponent),
}
];

Add typescript declaration file decl.d.ts under projects/shell/src

If you don’t add this file, you will get an error when you import the remote module mfe1 in the shell app.

1
declare module 'mfe1/Component';

Workspace structure

Here is the file structure of the workspace till now.

1
2
3
4
5
6
7
8
9
10
11
12
angular-module-federation/
├─ projects/
│ ├─ mfe1/
│ │ ├─ src/
│ │ ├─ webpack.config.js
│ │ ├─ webpack.prod.config.js
│ ├─ shell/
│ │ ├─ src/
│ │ ├─ webpack.config.js
│ │ ├─ webpack.prod.config.js
├─ angular.json
├─ package.json

Start the development server

You can run shell and mef1 app separately by running the following command.

1
2
ng serve shell - o
ng serve mfe1 - o

Or run them at the same time by running the following command.

1
npm run run:all

Change the shell app url in browser to localhost:4200/product, you will see the product component from mfe1 app.

Remote entry file

If you look into the webpack.config.js file in the shell app, you will see the following configuration.

1
2
3
remotes: {
"mfe1": "http://localhost:4200/remoteEntry.js",
},

Here we use remoteEntry.js as the entry point of the remote expose module, this is the default file name, However, if you want to change it, you can update the configuration in the webpack.config.js file in the mfe1 app.

1
2
3
4
module.exports = withModuleFederationPlugin({
// ...
filename: 'myEntryName.js',
});

and then update the configuration in the webpack.config.js file in the shell app.

1
2
3
4
5
6
module.exports = withModuleFederationPlugin({
// ...
remotes: {
"mfe1": "http://localhost:4200/myEntryName.js",
},
});

namedChunk

When you run the shell app, switch to network tab in the browser’s developer tool, you will see the response of the javascript file name as a random string, that’s because Webpack generates a random name for the chunk file, you can change it to a named chunk by adding the following configuration in the angular.json file in the mfe1 app.(under architect | build | configuration | development)

1
2
3
4
5
module.exports = withModuleFederationPlugin({
// ...
filename: 'myEntryName.js',
namedChunk: true,
});

Dynamic loading

You can remove the remotes part in app shell’s webpack.config.js file, and update the app.routes.ts file in the shell app to load the remote module dynamically.

1
2
3
4
5
6
7
8
9
{
path: 'product',
loadComponent: () =>
loadRemoteModule({
type: 'module',
remoteEntry: 'http://localhost:4201/remoteEntry.js',
exposedModule: './Component'
}).then(m => m.ProductComponent)
}

You can found the options of withModuleFederationPlugin in WebPack’s official documentation here, unfortunately, some property was even not mentioned in this document, you can found them in the source code of the plugin(schema file) from here

loadRemoteEntry vs loadRemoteModule

loadRemoteEntry is used to load the remote entry file, and loadRemoteModule is used to load the remote module.

loadRemoteEntry is not required, but nice to have, here is the description from github
If somehow possible, load the remoteEntry upfront. This allows Module Federation to take the remote’s metadata in consideration when negotiating the versions of the shared libraries.

For this, you could call loadRemoteEntry BEFORE bootstrapping Angular:

1
2
3
4
5
6
7
8
9
10
11
12
// main.ts
import { loadRemoteEntry } from '@angular-architects/module-federation';

Promise.all([
loadRemoteEntry({
type: 'module',
remoteEntry: 'http://localhost:3000/remoteEntry.js',
}),
])
.catch((err) => console.error('Error loading remote entries', err))
.then(() => import('./bootstrap'))
.catch((err) => console.error(err));

References

  1. Mono repo with Angular CLI: https://angular.dev/reference/configs/file-structure#multiple-projects
  2. Module federation with Angular: https://module-federation.io/practice/frameworks/angular/angular-cli.html
  3. https://github.com/manfredsteyer/ngx-build-plus/

Introduction

paths is a TypeScript configuration option that allows you to map a module name to a path. This is useful when you have a complex project structure and you want to avoid long relative paths.

Config

Add the following configuration to your tsconfig.json file.

1
2
3
4
5
6
7
8
9
10
{
"compilerOptions": {
"baseUrl": ".", // This must be specified.
"paths": {
"@app/*": ["src/app/*"],
"@shared/*": ["src/shared/*"],
"@utils/*": ["src/app/service/others/utils/*"],
}
}
}

baseUrl

The baseUrl option specifies the base directory to resolve non-relative module names. This must be specified when using absolute path. In above example, baseUrl is set to .(this is the same directory with tsconfig.json) which means the base directory is the root directory of the project. take “@app/*” as an example, when you import a module like import {AppComponent} from '@app/app.component', TypeScript will look for the module in the src/app directory.

if you don’t specify baseUrl, You must use relative paths to import modules. For example:

1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
"paths": {
"@app/*": ["./src/app/*"],
"@shared/*": ["./src/shared/*"],
"@utils/*": ["./src/app/service/others/utils/*"],
}
}
}

Usage

Suppose you have a file stringUtils.ts in the src/app/service/others/utils directory. You can import it like this:

1
import {stringUtils} from '@utils/stringUtils';

You don’t need to write the long relative path. TypeScript will automatically resolve the path based on the configuration in tsconfig.json.

Nx mono repo

When using Nx mono repo to create multiple app/libs, be careful when you use paths in tsconfig.json. You need to add the paths configuration to the tsconfig.base.json file in the root directory of the Nx workspace. and every tsconfig.json in each app/lib should extend tsconfig.base.json.

1
2
3
4
5
6
7
8
9
// tsconfig.json in app or lib
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": []
},
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]
}
1
2
3
4
5
6
7
8
9
10
11
// tsconfig.base.json
{
"compilerOptions": {
"baseUrl": ".", // This must be specified.
"paths": {
"@app/*": ["apps/*"],
"@shared/*": ["libs/shared/*"],
"@utils/*": ["libs/shared/utils/*"],
}
}
}

Note that tsconfig.json in app/lib has higher priority than tsconfig.base.json. If you have same configuration in both files, the configuration in tsconfig.json will override the configuration in tsconfig.base.json. this will cause some path not found sometimes.

So,

  • Only add paths configuration to tsconfig.base.json file in the root directory of Nx workspace.
  • Do not add paths configuration to any other tsconfig.json file in the app/lib.

References

https://www.typescriptlang.org/tsconfig/#paths

What is angular ngc?

ngc stands fore Angular Compiler. It is a tool that compiles Angular applications. ngc was built on tsc(TypeScript Compiler) and extends it to support Angular.

Angular compile options

You can see the ngc compile options in the tsconfig.json file under your Angular project root.

We can see that the output directory is set to dist/out-tsc. and there is a special section angularCompilerOptions used for angular.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
...
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

Run ngc manually

You can run ngc manually by using the command ngc in the terminal. and then found the compiled files in the dist/out-tsc directory.

References

https://blog.angular.io/how-the-angular-compiler-works-42111f9d2549