0%

The npm install --production command is used to install only the dependencies listed under the dependencies section in the package.json file, excluding the devDependencies. This is typically used in production environments where you want to avoid installing unnecessary development tools and libraries.

Here is a brief explanation of when and why you might use npm install --production:

  1. Production Deployment: When deploying an application to a production environment, you often want to minimize the size of the deployment package and reduce the number of installed packages to only those necessary for running the application. This helps in improving performance and security.

  2. Server Environments: In server environments where the application is running, you generally do not need development tools like testing frameworks, linters, or build tools. Using npm install --production ensures that only the essential packages are installed.

  3. Docker Images: When building Docker images for your application, using npm install --production can help create smaller and more efficient images by excluding development dependencies.

Example usage:

1
npm install --production

This command will read the package.json file and install only the packages listed under dependencies, ignoring those under devDependencies.

Introduction

In RxJS, there is a timeout operator, it’s used to throw an error if the source observable does not emit a value within a specified timeout duration.

Use case

In Angular, the HttpClient service is used to make HTTP requests. Sometimes, we want to set a timeout for the request, if the request does not complete within the specified time, we want to cancel the request and show an error message to the user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { catchError, timeout } from 'rxjs/operators';
import { throwError } from 'rxjs';

@Injectable({
providedIn: 'root',
})
export class ApiService {
constructor(private http: HttpClient) {}

getData() {
return this.http.get('https://example.com/api/data').pipe(
timeout(10000), // 10 seconds
catchError((error) => {
// Handle timeout or other errors
console.error('Request timed out or failed', error);
return throwError(error);
})
);
}
}

But, wait, what if I want to add this timeout for all my requests? Do I need to add the timeout operator to every request? The answer is no, you can create an interceptor to add the timeout operator to all requests.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Injectable } from '@angular/core';
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { timeout } from 'rxjs/operators';

export const DEFAULT_TIMEOUT = new InjectionToken<number>('defaultTimeout');

@Injectable()
export class TimeoutInterceptor implements HttpInterceptor {
constructor(@Inject(DEFAULT_TIMEOUT) protected defaultTimeout: number) {
}

intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
return next.handle(req).pipe(timeout(this.defaultTimeout));
}
}

Don’t forget to add the interceptor to the providers array in the AppModule.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { TimeoutInterceptor } from './timeout.interceptor';

@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: TimeoutInterceptor,
multi: true,
},
{ provide: DEFAULT_TIMEOUT, useValue: 30000 }
],
})
export class AppModule {}

Now, all your HTTP requests will have a timeout of 10 seconds.

But, what if I want to set different timeout values for some specific requests? You can add a custom header to the request and check it in the interceptor, if custom header timeout exists, use the custom timeout value, otherwise use the default timeout value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Injectable } from '@angular/core';
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
} from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class TimeoutInterceptor implements HttpInterceptor {
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
const timeoutValue = req.headers.get('timeout') || this.defaultTimeout;
const timeoutValueNumeric = Number(timeoutValue);

return next.handle(req).pipe(timeout(timeoutValueNumeric));
}
}

Now, you can set the timeout value in the request headers.

1
2
3
4
import { HttpHeaders } from '@angular/common/http';

const headers = new HttpHeaders().set('timeout', '5000'); // 5 seconds
this.http.get('https://example.com/api/data', { headers });

References

  1. https://rxjs.dev/api/index/function/timeout
  2. https://stackoverflow.com/questions/45938931/default-and-specific-request-timeout

Introduction

There are lots of mockXXXOnce functions in Jest API, for example:

These functions are only mocked on the first call, all the subsequent calls will use the original implementations or return the original values.

Be careful when using such functions, it may not be what you want, take the following code as example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export class AppComponent {
title = 'jest-demo';
lastTime = Date.now();

getIdleTime() {
return Date.now() - this.lastTime;
}

getDuration() {
if (this.getIdleTime() > 5 * 60 * 60 * 1000) {
return '5+ hours';
} else if(this.getIdleTime() >= 60 * 60 * 1000) {
return '1 ~ 5 hours';
} else {
return 'in an hour'
}
}
}

Here are the test codes, the first one pass test, but the second one will failed. Do you know why?

1
2
3
4
5
6
7
8
9
10
11
12
13
it(`should return 5+ hours`, () => {
const fixture = TestBed.createComponent(AppComponent);
const component = fixture.componentInstance;
jest.spyOn(component, 'getIdleTime').mockImplementationOnce(() => 6 * 60 * 60 * 1000);
expect(component.getDuration()).toBe('5+ hours');
});

it(`should return 1 ~ 5 hours`, () => {
const fixture = TestBed.createComponent(AppComponent);
const component = fixture.componentInstance;
jest.spyOn(component, 'getIdleTime').mockImplementationOnce(() => 4 * 60 * 60 * 1000);
expect(component.getDuration()).toBe('1 ~ 5 hours');
});

Because the first test case only reach the first if branch in getDuration function, it only call getIdleTime once. So there is no problem.

But the second test case need to reach the second if branch in getDuration function, but the mock only take effect in the first if branch, the second branch will still use the original implementation of getIdleTime function, so the test failed.

To fix this issue, you can use mockImplementation instead of mockImplementationOnce in the second test case.

Nx Library Types by functionality

Feature library

此类library主要负责和业务相关的组件和页面等等。

UI library

此类Library主要是负责和UI相关的功能。

Data access library

此类library主要负责和数据相关的功能,比如和后端API交互,数据处理等。

Utility library

此类library主要负责工具和辅助功能,比如一些通用的函数,服务等。

Nx library types by buildable and publishable

Workspace library(Normal library)

Create without any options, it’s a normal library.

1
nx g @nx/angular:lib libName
  • No ng-packagr file generated.
  • No package.json file generated .
  • No targets/build section in project.json file.

This type of libraries is intended to be used within the monorepo. It was imported by apps or other libraries in the same monorepo. It can’t be builded or published independently.

Buildable library

Create by adding buildable option.

1
nx g @nx/angular:lib libName --buildable
  • Add ng-packagr file to root of the library.
  • Add package.json file to root of the library.
  • name property in package.json is the libName.
  • Add targets/build section in project.json file.
  • Executor of build is: "executor": "@nx/angular:ng-packagr-lite"

Buildable libraries are similar to “publishable libraries” described above. Their scope however is not to distribute or publish them to some external registry. Thus they might not be optimized for bundling and distribution.

Buildable libraries are mostly used for producing some pre-compiled output that can be directly referenced from an Nx workspace application without the need to again compile it. A typical scenario is to leverage Nx’s incremental building capabilities.

Publishable library

Create by adding publishable and importPath option. importPath is the path that the library will be imported from, will be used as name of the package.

1
nx g @nx/angular:lib libName --publishable --importPath=@myorg/libName
  • Add ng-packagr file to root of the library.
  • Add package.json file to root of the library.
  • name property in package.json is the importPath.
  • Add targets/build section in project.json file.
  • Executor of build is: "executor": "@nx/angular:package"

Publishable libraries is intended to be published outside of the monorepo, and can be imported by other projects.

References

Introduction

今天在做一个 Angular 项目的时候,遇到了一个问题:

1
ERROR in src/app/app.module.ts:1:1 - error TS6059: File 'xxx' is not under 'rootDir' 'yyy'. 'rootDir' is expected to contain all source files.

可以看到这个错误是关于rootDir的,那么rootDir是什么呢?看一下官网的解释:

Default: The longest common path of all non-declaration input files. If composite is set, the default is instead the directory containing the tsconfig.json file.

When TypeScript compiles files, it keeps the same directory structure in the output directory as exists in the input directory.

For example, let’s say you have some input files:

1
2
3
4
5
6
7
8
MyProj
├── tsconfig.json
├── core
│ ├── a.ts
│ ├── b.ts
│ ├── sub
│ │ ├── c.ts
├── types.d.ts

The inferred value for rootDir is the longest common path of all non-declaration input files, which in this case is core/.

If your outDir was dist, TypeScript would write this tree:

1
2
3
4
5
6
MyProj
├── dist
│ ├── a.js
│ ├── b.js
│ ├── sub
│ │ ├── c.js

Importantly, rootDir does not affect which files become part of the compilation. It has no interaction with the include, exclude, or files tsconfig.json settings.

Note that TypeScript will never write an output file to a directory outside of outDir, and will never skip emitting a file. For this reason, rootDir also enforces that all files which need to be emitted are underneath the rootDir path.

重点看这句:rootDir also enforces that all files which need to be emitted are underneath the rootDir path. 也就是说,所有需要被编译的文件都必须在rootDir的路径下,否则就会报错。

比如下面的代码结构,如果我们在tsconfig.json中指定了rootDircore,那么helpers.ts就不在rootDir的路径下,所以会报错。

1
2
3
4
5
6
MyProj
├── tsconfig.json
├── core
│ ├── a.ts
│ ├── b.ts
├── helpers.ts

Nx based mono-repos.

如果是基于Nx的但一代码仓库,有时候也会出现这个错误,原因是一个Publishable的lib引用了另一个Non-Publishable的lib,这时候就会报错。详情请看这里, 关于Nx buildable/publishable libraries, please see here

  1. ngc - Angular compiler
  2. ngcc - Angular compatible compiler
  3. . fems - Flattened ES Module, this is the angular file format for ES Module.
  4. AOT - Ahead of Time Compilation
  5. JIT - Just in Time Compilation
  6. ng add vs npm install - ng add is a schematic that can install packages and run additional code to set up the package. npm install is just installing the package.
  7. Angular HttpClient底层是基于HttpXMLRequest和Jsonp的,所以可以使用XMLHttpRequest的所有方法。
  8. HostBinding - Angular Decorator that declares a DOM property to bind to. is equivalent to [property]=”expression”.
  9. Angular如何处理scss文件的?package.json中并没有安装sass对应的包。难道是Angular CLI内置了sass的编译器,所以不需要安装sass包?
  10. :ng-deep和:host如何配合使用
  11. Angular中的ViewEncapsulation是如何实现的?有哪三种封装模式?
  12. Reload page on same url with different parameters
    1
    2
    3
    4
    5
    6
    7
    8
    export const appConfig: ApplicationConfig = {
    providers: [
    provideRouter(
    appRoutes,
    withRouterConfig({ onSameUrlNavigation: 'reload' })
    ),
    ],
    };
  13. 如何使用Native Federation?
  14. How to mock service in Unit test(Jest) in Angular? https://stackoverflow.com/questions/79071748/mock-provided-service-in-standalone-component-in-jest-unit-test
  15. 这种怎么测试?添加到Jest异步测试中。
    1
    2
    3
    4
    5
    getData() {
    this.dataService.fetchData().subscribe((data: any) => {
    this.products = data;
    });
    }
  16. Generate module based component after Angular 17.
    Start from Angular 17, Angular CLI use standalone component by default, however, you can still generate module based application with the following command.
1
ng g app my-app --standalone=false
  1. Why Angular use Decorator instead of Abstract Class for Component and Service?
  2. Android No matching client found for package name ‘com.jiyuzhai.caoquanbei’, update package name in file app/src/google-services.json to match your application id.
  3. 一个Angular文件内可以定义多个组件。
  4. Event loop and browser rendering是如何交互的,也就是Browser rendering是在Event loop哪个阶段进行的?
  5. NW.js和Electron类似,都是用来开发跨平台桌面应用的框架。
  6. Traceur和Babel都是用来将ES6+代码转换为ES5代码的工具。Traceur是Google开发的,Babel是社区开发的。
  7. RxJS 社区中常见的约定:以 $ 结尾的变量通常表示它是一个 Observable(可观察对象)。
  8. Virtual Dom的好处是什么?为什么要使用Virtual Dom?
    1. performance - 批量更新dom,而不是每次更新都操作dom。
    2. 可以跨平台 - 这点很重要,多数人回到虚拟DOM,都说性能,但是跨平台也很重要,虚拟DOM相当于一个中间层,可以对接不同平台,比如Android,IOS,Desktop,Web等。
    3. 为什么有些框架不用虚拟DOM还更快?这是因为React/Vue等框架的最小单元就是组件,无法做到控件级别的更新,所以只能更新整个组件,这样就会有性能问题。而有些框架是以控件为最小单元,所以可以做到控件级别的更新,这样就会更快。
  9. Angular router可以传参数,用navigationExtras对象中的state.
    1. 在导航时传递参数
    1
    this.router.navigate(['/detail'], { state: { id: 1 } });
    1. 在接收参数的组件中获取参数
    1
    const id = history.state.id; // 不对吧?history 哪来的?
  10. GraphQL playground.
  11. You should never use function in angular template. - url
  12. Directive composition API - search on angular.dev, this is a new feature in angular 15, why this is useful?
  13. javascript, generator functions
  14. default import syntax in ES6.
  15. WeakMap is not iterable, why? TypedArray is not array.
  16. Angular BehaviorSubject vs Subject - BehaviorSubject needs an initial value, Subject does not.
  17. Angular aot can be enabled/disabled in angular.json file.
  18. it.each in Jest - This need real example from project.
  19. Why Jest need transformer? Because Jest only understand JavaScript, so it needs transformer to transform other file types to JavaScript. for example ts -> js, tsx/jsx -> js vue/angular -> js, by default Jest use babel-jest as transformer, you can also use angular-jest-preset for Angular project.
  20. Proxy.revocable(target, handler).
  21. What is cross-site request forgery (CSRF) attack? How to prevent it?
  22. Website defacement - what’s this?
  23. steps to use web component.
  24. Grid layout.
  25. HTML page will always show even it contains errors, the console won’t display html error, it only show javascript errors.
  26. Html中标签的样式按照LVHA的顺序来写,这样可以避免样式覆盖的问题。
    1. :link - 未访问的链接
    2. :visited - 已访问的链接
    3. :hover - 鼠标悬停
    4. :active - 激活状态
  27. css function env - env() 函数可以用来获取环境变量的值,比如获取视口的宽度。
  28. css @support operator - @supports 操作符用来检测浏览器是否支持某个CSS属性。
  29. video tag: source vs src - What’s the differences?
  30. CSS attributes selectors - ^, $, *, ~ - What’s the differences? [attr*=value] vs [attr~=value].
  31. compare in javascript:
    1. ==
    2. ===
    3. Object.is
    4. SameValueZero - MDN
  32. chunk load error: https://rollbar.com/blog/javascript-chunk-load-error/
  33. drag and drop:
    1. https://material.angular.io/cdk/drag-drop/overview
    2. https://github.com/swimlane/ngx-dnd
    3. https://packery.metafizzy.co/
  34. What’s the differences between markForCheck() and detectChanges() in Angular? - stackoverflow
  35. SASS vs SCSS
    1. SASS - Syntactically Awesome Style Sheets
    2. SCSS - Sassy CSS
    3. SASS is the older syntax, it use indentation to define blocks, SCSS is the newer syntax, it use curl braces to define
  36. 如何查看JS编译后的代码?比如变量提升后的代码,Google的V8引擎可以实现这个功能。而Node底层也是V8,所以用Node命令可以查看,但是输出的代码不是很直观。需要进一步研究,参考资料:https://medium.com/@drag13dev/https-medium-com-drag13dev-how-to-get-javascript-bytecode-from-nodejs-7bd396805d30
  37. JavaScript中有些时候使用new和不使用new结果完全不同,比如:
    1. Date() vs new Date(): Date()返回的是字符串,new Date()返回的是Date对象。
    2. String() vs new String(): String()返回的是字符串,new String()返回的是String对象。- 在这里, 不加new其实相当于类型转换
    3. Number() vs new Number(): Number()返回的是数字,new Number()返回的是Number对象。- 同上
    4. 注意:对于Array,加不加new,结果一样,都是数组。
  38. Harmony Import/Export - 其实就是ES6中的Import/Export,这是ES6的模块化规范,可以用来导入导出模块。参考这里:https://en.wikipedia.org/wiki/ECMAScript_version_history#4th_Edition_(abandoned) 和这里:https://stackoverflow.com/questions/52871611/what-is-harmony-and-what-are-harmony-exports
  39. Immutable.js - 一个用来处理不可变数据的库,可以用来提高性能。参考这里:https://immutable-js.com/
  40. 性能优化好文 - https://zhuanlan.zhihu.com/p/41292532
  41. html中的checkbox/radio button默认的样式由浏览器决定,不是十分好定制化,如果只是更改颜色的话,可以使用accent-color属性,如果要更改大小的话,可能就需要其他手段了。比如这里:https://stackoverflow.com/questions/4148499/how-to-style-a-checkbox-using-css/69164710#69164710
  42. 有哪些样式是CSS改变不了的?
    1. select控件中option的高亮色,就是鼠标划过每个选项时的高亮色,这个一般是蓝色,且不能更改。如果想改,那么需要使用ul/li模拟select控件。
  43. 浏览器如何处理双击,双击的时候,浏览器会选取最近的能选到的文本,所以随便打开一个网站,找个空白处双击一下,你会发现有文本被选中了。如何禁止这种行为呢?
    以下方法会禁止所有选中文本的行为,包括用户主动选择的,要找到最近的node,手动设置一下。
1
2
3
function disableTextSelection() {
document.addEventListener('selectstart', (e) => e.preventDefault());
}

Install TypeScript

This directive will install TypeScript globally.

1
npm install -g typescript

Init TypeScript project

This step will create a tsconfig.json file in under typescript-tsc-guide folder.

1
2
3
mkdir typescript-tsc-guide
cd typescript-tsc-guide
tsc --init # create tsconfig.json file

Create typescript files

Create a folder src under current folder. Then create a file index.ts under src folder.

1
2
mkdir src
touch src/index.ts

input the following code to src/index.ts file.

1
const add = (a, b) => a + b;

Compile TypeScript files

This step will compile the src/index.ts file to src/index.js file.

1
2
cd typescript-tsc-guide
tsc

Compile options

outDir

Usually, we put the emitted files under dist folder. To do this, we need to modify the tsconfig.json file.

1
2
3
4
5
{
"compilerOptions": {
"outDir": "./dist" // output directory for the emitted files
}
}

Run tsc again, the emitted files will be under dist folder.

rootDir

We can also specify the source folder by using rootDir option, in this way, only files under the source folder will be compiled.

1
2
3
4
5
6
{
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src" // specify the source folder
}
}

If you specify the rootDir option, you can’t put the source files outside the source folder. otherwise you’ll got the following error:

1
error TS6059: File '/typescript-tsc-guide/xxx.ts' is not under 'rootDir' '/typescript-tsc-guide/src'. 'rootDir' is expected to contain all source files.

To test this, create a file test.ts under project root(same location as tsconfig.json) and run tsc command, you’ll get the error.

Angular 隐藏了项目构建的细节,其构建过程通过各种Builder来实现,底层可以使用Webpack或者ESBuild(从Angular17开始,默认使用ESBuild),那么我们需要修改Webpack配置怎么办呢?其实只有一个办法,那就是通过第三方npm package,这里介绍目前流行的两个。

这两者的实现原理也比较简单,都是通过继承@angular-devkit/build-angular来实现的。

下面我们分别看看,这两个package的使用方法。

ngx-build-plus

Installation

1
npm install ngx-build-plus --save-dev

Usage

angular.json(for Angular project) or project.json(For Nx based mono repo)

1
2
3
4
5
6
7
8
9
10
{
"architect": {
"build": {
"builder": "ngx-build-plus:browser", // change builder
"options": {
"extraWebpackConfig": "extra-webpack.config.js" // supply a path to the custom webpack config
}
}
}
}

@angular-builders/custom-webpack

Installation

1
npm install @angular-builders/custom-webpack --save-dev

Usage

angular.json(for Angular project) or project.json(For Nx based mono repo)

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"architect": {
"build": {
"builder": "@angular-builders/custom-webpack:browser", // change builder
"options": {
"customWebpackConfig": {
"path": "./extra-webpack.config.js", // supply a path to the custom webpack config
"mergeStrategies": { "externals": "replace" }
}
}
}
}
}

How to choose?

  1. If you are using Angular Module Federation, you should use ngx-build-plus, and it is installed automatically when you create a new Angular project with Angular Module Federation. This package has not been upgraded for a long time, but it is still working. ngx-build-plus support hooks.
  2. If you are not using Angular Module Federation, you can choose either of them, but @angular-builders/custom-webpack is more popular. This package is upgraded frequently.

Angular的编译过程是怎样的?
通常我么使用ng build命令来编译Angular项目,但是这个命令背后的逻辑是怎样的呢?

  • ng build命令底层是用WebPack还是ESBuild
  • ng build命令是如何处理TypeScript文件的?
  • ng build命令是如何处理HTML文件的?

ts/js编译

Webpack的配置文件在这里:packages/angular_devkit/build_angular/src/tools/webpack/configs/common.ts

这个文件里我们可以看到如下rule配置:

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
{
test: /\.[cm]?[tj]sx?$/,
// The below is needed due to a bug in `@babel/runtime`. See: https://github.com/babel/babel/issues/12824
resolve: { fullySpecified: false },
exclude: [
/[\\/]node_modules[/\\](?:core-js|@babel|tslib|web-animations-js|web-streams-polyfill|whatwg-url)[/\\]/,
],
use: [
{
loader: require.resolve('../../babel/webpack-loader'),
options: {
cacheDirectory: (cache.enabled && path.join(cache.path, 'babel-webpack')) || false,
aot: buildOptions.aot,
optimize: buildOptions.buildOptimizer,
supportedBrowsers: buildOptions.supportedBrowsers,
instrumentCode: codeCoverage
? {
includedBasePath: sourceRoot ?? projectRoot,
excludedPaths: getInstrumentationExcludedPaths(root, codeCoverageExclude),
}
: undefined,
} as AngularBabelLoaderOptions,
},
],
},

可以看到这个正则表达式匹配的是:

  • .ts.tsx
  • .js.jsx
  • .cjs.mjs

这些文件都会被require.resolve('../../babel/webpack-loader')这个loader处理。而这个loader的代码在这里:packages/angular_devkit/build_angular/src/tools/babel/webpack-loader.ts

这个文件里面又调用了babelLoader。所以可知,ng build命令底层是用Babel来处理TypeScript文件的。

这里可以画一个流程图:ng build -> Webpack -> Babel -> TypeScript

scss/sass/less/css编译

Webpack通过各种loader来处理不同类型的文件,比如css-loadersass-loaderless-loader等。那么什么是loader呢?loader是一个转换器,负责把一种文件格式转换为另一种文件格式。

1
output_format = loader(input_format)

loader是可以链式调用的,上一个loader的输出可以作为下一个loader的输入,比如scss文件可以先经过sass-loader处理,然后再经过css-loader处理,最后再经过style-loader处理。

1
output_format = style-loader(css-loader(sass-loader(input_format)))

Angular默认采用scss作为样式文件的扩展名,所以我们可以看到scss文件是如何被处理的:Webpack中关于样式文件的配置在这里:packages/angular_devkit/build_angular/src/tools/webpack/configs/styles.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
extensions: ['scss'],
use: [
{
loader: require.resolve('resolve-url-loader'),
options: {
sourceMap: cssSourceMap,
},
},
{
loader: require.resolve('sass-loader'),
options: getSassLoaderOptions(
root,
sassImplementation,
includePaths,
false,
!!buildOptions.verbose,
!!buildOptions.preserveSymlinks,
),
},
],
},

html编译

Introduction

Angular的CLI(Command Line Interface)系统非常强大,它提供了丰富的命令用来构建,测试,运行Angular程序,今天我们就从源码的角度来看看cli中的ng serve命令是如何工作的。

ng serve是我们日常频繁使用的命令,它用来本地启动Angular项目,检测代码更新以做出响应,但是用了这么久的命令,你了解它的底层逻辑吗?下面的几个问题你是否思考过?

  1. ng serve命令为什么没有输出文件到dist目录?
  2. ng serve命令是如何启动一个web server的?
  3. ng serve命令是如何监控文件变化的?
  4. ng serve命令是如何实现热更新的?

下面就让我们开始这段愉快而又漫长的探索之旅吧!

准备工作

为了调试ng serve命令,我们需要准备一个Angular项目,为了方便查看源码,我们将angluar-cli的源码也下载到本地,然后打开两个IDE,一个查看待调试的项目代码,一个查看angular-cli的源码。对照起来看,简直不要太爽!

从命令行开始

如果你直接查看ng serve命令的源码,可能得不到什么有用的信息,serve命令对应的源码在这里:packages/angular/cli/src/commands/serve/cli.ts, 从以下代码可知,serve命令是继承自ArchitectCommandModule的,同时也实现了CommandModuleImplementation接口。而该文件本身只是做了一个简单的配置,具体的实现逻辑都在ArchitectCommandModuleCommandModuleImplementation中。

1
2
3
4
5
6
7
8
9
10
export default class ServeCommandModule
extends ArchitectCommandModule
implements CommandModuleImplementation
{
multiTarget = false;
command = 'serve [project]';
aliases = RootCommands['serve'].aliases;
describe = 'Builds and serves your application, rebuilding on file changes.';
longDescriptionPath?: string | undefined;
}

还是调试吧,我们打开Angular项目,然后打开项目根目录下的package.json文件,找到scripts字段,你会看到如下内容:

1
2
3
4
5
6
7
8
9
10
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"compile": "ngc",
"test": "jest --verbose",
"test:watch": "jest --watch",
"prepare": "husky install"
},

想要深入理解ng serve命令,我们需要先了解start命令是如何工作的。我们通过调式该命令的方式来了解ng serve的底层逻辑。不同的IDE对应不同的调试方式:

  • WebStorm: 点击start命令左侧的绿色三角按钮,然后选择Debug 'start',这样就可以进入ng serve的源码了。
  • VSCode: 鼠标悬停到start命令上,点击Debug按钮,这样就可以进入ng serve的源码了。

使用这种方法可以进入调试状态,但是我们要在哪里设置断点呢?

对于package.json中的script区块中的命令,其实他们都对应node_modules/.bin下面的一个文件。以ng serve为例,因为它的命令由ng来引导,那么我们首先到node_modules/.bin目录下找到ng文件。

可是我们找到了三个ng文件,分别是ng, ng.cmd, ng.ps1,那么我们应该选择哪一个呢?其实通过扩展名就能看出来,我这里使用的是Windows系统,所以我们选择ng.cmd文件。

  • ng - Unix shell script
  • ng.cmd - Windows batch file
  • ng.ps1 - Windows PowerShell script

用记事本打开这个文件看看它的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0

IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)

endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\@angular\cli\bin\ng.js" %*

我们不必纠结于每一行代码的含义,简单分析下来,我们可以推断出这个脚本的作用就是用node.exe来调用@angular/cli/bin/ng.js文件。

好了,我们再去看一下这个ng.js文件,这个文件就是Angular CLI的入口文件。ng.js调用了同一目录下的bootstrap.js文件,bootstrap.js文件调用了lib/init.ts文件。init.ts又调用了lib/cli/index.ts文件,后续又有一大堆的调用。

bin/ng.js –> bin/bootstrap.js –> lib/init.ts –> lib/cli/index.ts –> …

通过不断的设置断点并调试得知,最终在文件packages/angular/cli/src/command-builder/architect-base-command-module.ts中通过解析项目的配置文件angular.json,找到serve命令对应的builder,然后通过调用那个builder来启动一个web server。

architect-base-command-module.ts对应的代码如下:

1
2
3
4
5
6
7
8
9
10
11
protected async runSingleTarget(target: Target, options: OtherOptions): Promise<number> {
const architectHost = this.getArchitectHost();
let builderName: string;
try {
builderName = await architectHost.getBuilderNameForTarget(target); // 获取builder名称
} catch (e) {
assertIsError(e);
return this.onMissingTarget(e.message);
}
//...
}

Webpack配置在哪里?

我们都知道,ng serve命令启动的是一个web server,而这是通过webpack-dev-server来实现的,那么这个webpack-dev-server的配置在哪里呢?我么可以在源码中找到如下目录:
packages/angular_devkit/build_angular/src/tools/webpack/configs/common.ts,这个目录下有四个文件:

  • common.ts - 通用配置,主要是打包配置
  • dev-server.ts - 开发服务器配置 - 我们要找的配置就在这里。
  • styles.ts - 样式配置,用来处理各种样式文件, 如css, scss/sass, less等
  • index.ts - 导出上面三个文件

总结

Angular的CLI命令对应的逻辑源码并不在CLI命令本身中,而是在对应的builder中,builder源码在这个目录下:

  • packages/angular/build/src/builders - application builder
  • packages/angular_devkit/build_angular/src/builders - other builders

关于builder的详细介绍,可以参考之前的一篇博文:https://zdd.github.io/2024/06/06/angular-builders/, 也可以参考Angular官方文档:https://angular.dev/tools/cli/cli-builder/

  • ng server works in memory and never generate files to dist folder