0%

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

What’s Fixture?

先解释一下fixture这个单词,它的意思是固定设施,比如室内的浴缸或抽水马桶。当然这里指的是测试中的固定设施,也就是我们要测试的组件。在Angular中,我们可以通过TestBed来创建一个组件的fixture,然后对这个fixture进行测试。

通常来说,fixture都是在beforeEach中创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
describe('ProductComponent', () => {
let component: ProductComponent;
let fixture: ComponentFixture<ProductComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProductComponent] // import standalone component
})
.compileComponents();

fixture = TestBed.createComponent(ProductComponent); // create fixture
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

fixture.detectChanges()

fixture.detectChanges()是用来触发组件的变更检测的,也就是说,当我们对组件的状态进行了修改之后,我们需要调用fixture.detectChanges()来通知Angular进行变更检测,以便更新视图。
如果你的测试中包含对UI的检测,那么你就需要调用fixture.detectChanges()。否则不需要。
来,举个例子!

We have a product component which just display the product name as ‘Computer’, and when user click the Change product name button, we’ll update the product name to ‘Phone’.

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

@Component({
selector: 'app-product',
standalone: true,
imports: [],
templateUrl: './product.component.html',
styleUrl: './product.component.css'
})
export class ProductComponent {
name = 'Computer';

changeName() {
this.name = 'Phone';
}
}
1
2
<p id="product-name">Product name: {{name}}</p>
<button (click)="changeName()">Change product name</button>

Does this test case work?

1
2
3
4
5
it('should have name as Computer', () => {
component.changeName();
fixture.detectChanges();
expect(component.name).toEqual('Phone');
});

Yes, it works, but, we don’t need to call fixture.detectChanges() here, because we are testing the component’s property name change directly, not the UI changes.

Does the following test case work?

1
2
3
4
5
it('should change name', () => {
component.changeName();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('#product-name').textContent).toContain('Phone');
});

No, it doesn’t work, because, you call component.changeName() to change the product name, but you didn’t call fixture.detectChanges() to trigger the change detection and update the view. so the product name on page is still ‘Computer’.

We can call fixture.detectChanges() after component.changeName() to fix this issue.

1
2
3
4
5
6
it('should change name', () => {
component.changeName();
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('#product-name').textContent).toContain('Phone');
});

fixture.whenStable()

fixture.whenStable()是用来等待异步任务完成的,为了使用这个函数,我们给product component添加一个异步任务,比如通过setTimeout来模拟一个异步任务。

1
2
3
4
5
6
7
8
9
10
11
getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Keyboard');
}, 1000);
});
}

async updateName() {
this.name = await this.getData() as unknown as string;
}
1
<button (click)="updateName()">Update name</button>

接下来,我们测试一下updateName方法。下面这个test case,没有调用fixture.detectChanges(),但是仍然可以通过测试,为什么呢?

1
2
3
4
5
6
7
8
it('should update name', async () => {
component.updateName();
fixture.whenStable().then(() => {
expect(component.name).toEqual('Keyboard');
const compiled = fixture.nativeElement;
expect(compiled.querySelector('#product-name').textContent).toContain('Keyboard');
});
});

其实,这个test case是有问题的,它根本没有通过测试,虽然我们使用async标记了测试方法,但是async要配合await使用,而不是then,实际上,这里的then根本没有执行!

为了修复这个test case,我们可以将async删除。(这里有待验证!!!

1
2
3
4
5
6
7
8
9
it('should update name', async () => {
await component.updateName();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(component.name).toEqual('Keyboard');
const compiled = fixture.nativeElement;
expect(compiled.querySelector('#product-name').textContent).toContain('Keyboard');
});
});

其实,这里根本就不需要fixture.whenStable(),因为我们使用了await,它会等待异步任务完成,所以我们可以直接这样写:

1
2
3
4
5
6
7
it('should update name', async () => {
await component.updateName();
fixture.detectChanges();
expect(component.name).toEqual('Keyboard');
const compiled = fixture.nativeElement;
expect(compiled.querySelector('#product-name').textContent).toContain('Keyboard');
});

那到底什么时候需要使用fixture.whenStable()呢?当你测试的方法里面包含异步操作,但是这个方法又不返回Promise的时候,你就需要使用fixture.whenStable()来等待异步任务完成。

思考题:
下面的代码能通过测试吗?

1
2
3
4
5
it('should update name', () => {
fixture.whenStable().then(() => {
expect(false).toBe(true);
});
});

答案是:能!因为fixture.whenStable()里面的代码根本就没有执行,如何修复?

1
2
3
4
it('should update name', async () => {
await fixture.whenStable();
expect(false).toBe(true);
});

还可以在fixture.whenStable()前面加上return,因为Jest中如果返回一个Promise,Jest会等待这个Promise执行完成。详情看这里

1
2
3
4
5
it('should update name', async () => {
return fixture.whenStable().then(() => {
expect(false).toBe(true);
});
});

Conclusion

fixture.detectChanges()

  1. This function will trigger change detection.
  2. Change detection will updates the view.
  3. If you test UI changes, you need to call fixture.detectChanges().

fixture.whenStable()

  1. This function will wait for the asynchronous tasks to complete.
  2. You should use this function in async function.

1. TypeError: configSet.processWithEsbuild is not a function

Solution: Update jest.config.js to use jest-angular-preset instead of ts-preset.

1
2
3
4
5
module.exports = {
transform: {
'^.+\\.(ts|js|html)$': 'ts-jest', // <-- update this line
},
};
1
2
3
4
5
module.exports = {
transform: {
'^.+\\.(ts|js|html)$': 'jest-preset-angular', // <-- update this line
},
};

2. SyntaxError: xxx.spec.ts: Missing semicolon. (26:42)

Look into the error message, the error occurs on this line.

1
const compiled = fixture.nativeElement as HTMLElement;

The reason is because as, it’s a keyword in TypeScript, but it’s not a keyword in JavaScript. So the root cause is Jest cannot understand the TypeScript syntax.
We need a preset to help Jest to understand TypeScript syntax. The ts-jest is the most popular one.

  • If you project is a pure TypeScript project, see here on how to config ts-jest.
  • If you project is an Angular project, Please use jest-preset-angular, see here for details.

3. jest: failed to cache transform results in: xxx/jest/jest-transform-cache.map, Failure message: ENOSPC: no space left on device, write

This is because Jest is trying to transform itself, so add the following to your jest.config.js file will resolve this issue. see here for details.

1
2
3
4
transformIgnorePatterns: [
'<rootDir>/node_modules/@babel',
'<rootDir>/node_modules/@jest',
],

Firstly, node can’t run typescript files directly. You need to compile the typescript files to javascript files and then run them with node.

The following command won’t work!

1
node ./src/main.ts

You’ll got error like this:

1
2
3
(node:13596) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.

SyntaxError: Cannot use import statement outside a module

How to solve this problem?

  1. Use ts-node to run typescript files directly.
    1. npm install -g ts-node
    2. ts-node ./src/main.ts
  2. Use tsc to compile typescript files to javascript files and then run them with node.
    1. npm install -g typescript
    2. tsc ./src/main.ts
    3. node ./src/main.js

Happy coding!

This article discuss how to debug Angular/Node.js application in WebStorm and VSCode.

Debug Angular app

WebStorm

  1. Start angular application
    1
    npm start // or ng serve
  2. Set breakpoints in the source code in WebStorm Editor.
  3. Press Ctrl + Shift + Click on the URL in the terminal to open the browser.
    debug angular WebStorm
  4. See here for more details.

VSCode

Launch with task file

  1. Create file launch.json under .vscode folder in project root.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
    {
    "name": "debug angular app",
    "type": "chrome",
    "request": "launch",
    "preLaunchTask": "npm: start",
    "url": "http://localhost:4200/"
    },
    ]
    }
  2. Create file task.json under .vscode folder in project root.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    {
    // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
    "version": "2.0.0",
    "tasks": [
    {
    "type": "npm",
    "script": "start",
    "isBackground": true,
    "problemMatcher": {
    "owner": "typescript",
    "pattern": "$tsc",
    "background": {
    "activeOnStart": true,
    "beginsPattern": {
    "regexp": "(.*?)"
    },
    "endsPattern": {
    "regexp": "bundle generation complete"
    }
    }
    }
    },
    ]
    }
  3. Click Run and Debug in VSCode’s left side menu. // number 1 on picture below
  4. Select debug angular app in the dropdown list. // number 2 on picture below
  5. Click the Debug icon or Press F5 to start debugging. // number 3 on picture below
    debug angular VSCode
    In this way, VSCode will open a new Chrome window and you can debug the Angular app in VSCode.

Launch without task file

If you don’t want to use task.json file, you can remove it and delete preLaunchTask in launch.json file. Then

  1. Manually start the Angular app by npm start in terminal.
  2. Click the Debug icon or Press F5 to start debugging in VSCode.
    In this way you need to manually open the browser and navigate to http://localhost:4200/ to debug the Angular app.

Browser

No matter you use WebStorm or VSCode, you can also debug Angular app in browser.

  1. Run your angular app by npm run start or ng serve
  2. Open the browser and navigate to http://localhost:4200/.
  3. Press F12 to open the developer tools and switch to Source tab.
  4. Set breakpoint in your source code in browser. see here for more details.
    1. If you use webpack, the source file was in webpack://src folder.
    2. If you use ESBuild + Vite, the source files were in src folder.
  5. Refresh the page to trigger the breakpoints.