0%

This is a very common error when you are working with Angular, today, I will give a special case of this error.

I have the following code in my app.

  1. ProductHomeModule -> ProductHomeComponent
  2. ProductDetailModule -> ProductDetailComponent
  3. App router contains the following routes:
    1
    2
    3
    4
    // app-routing.module.ts
    const routes: Routes = [
    { path: 'product', component: ProductHomeComponent },
    ]
    When I build my app, I got the following error:
1
ERROR in src/app/product-home/product-home.component.html:1:1 - error NG8001: 'app-product-detail' is not a known element:

But If I remove the router for product, the error is gone, why?
Because, I use ProductHomeComponent in app-routing.module.ts which in turn was used in file app.module.ts, But, when Angular compile template for ProductHomeComponent, it didn’t know where to find ProductDetailComponent. to solve this, import ProductHomeModule in app.module.ts like this:

1
2
3
imports: [
ProductHomeModule
]

Then the error is gone. Because, ProductHomeModule has imports ProductDetailModule, so Angular knows where to find ProductDetailComponent.

Conclusion:
If you wan to use a component from another module, you need to import the module which contains the component in the module where you use the component. You may use the component by selector in templates or in the router, both need to import the module which contains the component.

Angular Server Side Rendering For New Project

Step by step guide

Start from Angular 17, SSR was enabled by default when you create a new Angular project. To create a new Angular project, run the following command.

1
ng new angular-ssr

Press Y on keyboard when prompted with the following question, then Angular will generate the boilerplate code for you.

1
Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? (y/N)

Compared with the traditional pure client-side rendering project, the Angular SSR project has the following differences:

Project structure changes for SSR

The following new files were added to the project:

  1. The server.ts file is added to the project root directory.
  2. The src/main.server.ts file is added to the project source directory.
  3. The src/app/app.config.server.ts file is added to the project source directory.

The following changes were made to the existing files:

  1. The package.json file has a new server script.
    1
    2
    3
    4
    "scripts": {
    // ...
    "serve:ssr:angular-ssr": "node dist/angular-ssr/server/server.mjs" // <--- new
    },
  2. The angular.json file has a new server configuration under the architect | build | options section.
    1
    2
    3
    4
    5
    6
    7
    8
    "options": {
    // ...
    "server": "src/main.server.ts",
    "prerender": true,
    "ssr": {
    "entry": "server.ts"
    }
    },
  3. The tsconfig.app.json file file section is updated to include the src/main.server.ts file.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    // ...
    "files": [
    "src/main.ts",
    "src/main.server.ts", // <--- new
    "server.ts" // <--- new
    ],
    // ...
    }
  4. The src/app/app.config.ts file add provideClientHydration in providers.
    1
    2
    3
    export const appConfig: ApplicationConfig = {
    providers: [provideRouter(routes), provideClientHydration()]
    };

Run app

Deployment

How to check if page is rendered by server or client

  1. Open the page in Chrome browser.
  2. Right-click on the page and select View page source.
  3. Check the body tag, if the body tag is empty, it means the page is rendered in client side, the following page is a CSR page, since the body tag only contains <app-root></app-root>. that’s for the client side rendering.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!doctype html>
    <html lang="en">
    <head>
    <script type="module" src="/@vite/client"></script>

    <meta charset="utf-8">
    <title>Angular SSR In Depth</title>
    <base href="/">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/x-icon" href="favicon.ico">
    <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&amp; display=swap" rel="stylesheet">
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    <link rel="stylesheet" href="styles.css"></head>
    <body class="mat-typography">
    <app-root></app-root>
    <script src="polyfills.js" type="module"></script><script src="main.js" type="module"></ script></body>
    </html>

Angular SSR for existing project

If you have an existing project and want to enable SSR, you can follow the steps below.

1
ng add @angular/ssr

This command will add the necessary files and configurations to your project to enable SSR.

  1. Create src/main.server.ts file.

    1
    2
    3
    4
    5
    6
    7
    import { bootstrapApplication } from '@angular/platform-browser';
    import { AppComponent } from './app/app.component';
    import { config } from './app/app.config.server';

    const bootstrap = () => bootstrapApplication(AppComponent, config);

    export default bootstrap;
  2. Create src/app/app.config.server.ts file.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
    import { provideServerRendering } from '@angular/platform-server';
    import { appConfig } from './app.config';

    const serverConfig: ApplicationConfig = {
    providers: [
    provideServerRendering()
    ]
    };

    export const config = mergeApplicationConfig(appConfig, serverConfig);
  3. Create server.ts file.

    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
    47
    48
    49
    50
    51
    52
    53
    54
    import { APP_BASE_HREF } from '@angular/common';
    import { CommonEngine } from '@angular/ssr';
    import express from 'express';
    import { fileURLToPath } from 'node:url';
    import { dirname, join, resolve } from 'node:path';
    import bootstrap from './src/main.server';

    // The Express app is exported so that it can be used by serverless Functions.
    export function app(): express.Express {
    const server = express();
    const serverDistFolder = dirname(fileURLToPath(import.meta.url));
    const browserDistFolder = resolve(serverDistFolder, '../browser');
    const indexHtml = join(serverDistFolder, 'index.server.html');
    const commonEngine = new CommonEngine();

    server.set('view engine', 'html');
    server.set('views', browserDistFolder);

    // Example Express Rest API endpoints
    // server.get('/api/**', (req, res) => { });
    // Serve static files from /browser
    server.get('**', express.static(browserDistFolder, {
    maxAge: '1y',
    index: 'index.html',
    }));

    // All regular routes use the Angular engine
    server.get('**', (req, res, next) => {
    const { protocol, originalUrl, baseUrl, headers } = req;
    commonEngine
    .render({
    bootstrap,
    documentFilePath: indexHtml,
    url: `${protocol}://${headers.host}${originalUrl}`,
    publicPath: browserDistFolder,
    providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
    })
    .then((html) => res.send(html))
    .catch((err) => next(err));
    });

    return server;
    }

    function run(): void {
    const port = process.env['PORT'] || 4000;
    // Start up the Node server
    const server = app();
    server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
    });
    }

    run();
  4. Update angular.json file.

    Add the following in architect | build | options section.

    1
    2
    3
    4
    5
    "server": "src/main.server.ts",
    "prerender": true,
    "ssr": {
    "entry": "server.ts"
    }
  5. Update tsconfig.app.json file.

  6. Update src/app/app.config.ts file.

  7. Update package.json file.

References

  1. https://www.youtube.com/watch?v=U1MP4uCuUVI

There are many ways to define a dependency provider in Angular. In this article, we will discuss the following ways:

useClass

useClass是依赖注入中提供provider最常见的方式,我们项目中90%用的都是这种方式,下面来看看怎么使用这种方式,假设我们有一个ProductComponent组件,它依赖一个OrderService服务,我们可以像下面这样提供OrderService:

1
2
3
4
5
6
7
8
9
@Component({
selector: 'app-product',
standalone: true,
providers: [OrderService], // <-- Provide OrderService
templateUrl: './product.component.html',
styleUrl: './product.component.scss'
})
export class ProductComponent {
}

上面的例子其实是简写形式,它等价于下面的完整形式:也就是当provide对应的值和useClass对应的值相同时,我们可以采用这种简写形式。

1
providers: [{ provide: OrderService, useClass: OrderService }],

还有一点需要注意的是:如果OrderServiceprovidedIn: 'root'的话,那么我们就不需要在providers中再次提供OrderService了,直接在构造函数中注入即可。

1
2
3
export class ProductComponent {
constructor(private orderService: OrderService) {}
}

useClass在单元测试中特别有用,如果要测试的Service很复杂,我们可以提供一个mockService来代替他,这个mockService只要满足最小测试范围内即可,这样就可以保证测试的独立性。

1
providers: [{ provide: UserService, useClass: mockUserService}]

useExisting

useExisting通常用于提供一个别名,也就是说,useExisting提供了多种访问同一Service的方式,下面来看一个例子:
假设我们的Order分为线下订单和线上订单,那么线上订单我们可以单独定义一个Service,叫做OnlineOrderService,这个Service需要的功能OrderService中都有,所以我们可以如下使用它。

1
2
3
4
5
6
7
8
9
@Component({
selector: 'app-product',
standalone: true,
providers: [OrderService, {provide: OnlineOrderService, useExisting: OrderService}],
templateUrl: './product.component.html',
styleUrl: './product.component.scss'
})
export class ProductComponent {
}

这里:OnlineOrderServiceOrderService的别名,我们可以通过OnlineOrderService来访问OrderService的实例。

这种方式通常发生在旧系统到新系统的迁移时,比如迁移的时候,我们可能会更改一些Service的名字,但是为了兼容旧系统,我们可以通过useExisting来提供别名。

以Logger为例,假设旧系统使用的是OldLoggerService, 新系统使用的是NewLoggerService,那么我们可以这样定义:

1
providers: [{provide: OldLoggerService, useExisting: NewLoggerService}]

useFactory

useFactory,顾名思义,这种方式使用一个工厂方法来提供依赖的实例,通常用在需要参数的Service中,上面的例子中,我们提供的Service都是无参数的,下面我们来看一个需要参数的Service。

下面这个HeroService需要一个Logger和一个isAuthorized参数,无论是useClass,还是useExisting都无法满足这个需求,这时我们就可以使用useFactory来提供这个Service。

1
2
3
4
5
6
7
8
9
10
11
class HeroService {
constructor(
private logger: Logger,
private isAuthorized: boolean) { }

getHeroes() {
const auth = this.isAuthorized ? 'authorized' : 'unauthorized';
this.logger.log(`Getting heroes for ${auth} user.`);
return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}
}

useFactory后面跟的是一个函数,所以我们先定义这个函数。(注意Factory函数是一个injection context,在这个函数内部可以使用inject()方法来注入其他依赖。)

1
2
const heroServiceFactory = (logger: Logger, userService: UserService) =>
new HeroService(logger, userService.user.isAuthorized);

然后使用useFactory来提供HeroService

1
2
3
4
5
6
7
8
9
providers: [
Logger,
UserService,
{
provide: HeroService,
useFactory: heroServiceFactory,
deps: [Logger, UserService] // deps是用来给工厂方法传递参数的
}
]

注意观察,上面的工厂函数heroServiceFactory中的参数loggeruserService是通过deps传递进来的。

useValue

useValue允许你绑定一个静态值到一个token上,这种方式通常用于提供一些常量,比如配置信息等。如果我们需要运行时动态取值,就可以使用这种方式了。

useValue的使用步骤如下:

  1. 定义一个token
  2. 使用useValue来提供这个token
  3. 在构造函数中注入这个token

token是简单值

定义一个token

首先新建一个文件src/app/config/AppConfig.ts用来存储token。这里我们创建一个token用来存储主题信息。

1
2
3
import {InjectionToken} from "@angular/core";

export const AppTheme = new InjectionToken<string>('appTheme');

使用useValue来提供这个token

由于我们这个token比较简单,就是字符串类型的,所以直接用字符串’light’来提供这个token。

1
providers: [{provide: AppTheme, useValue: 'light'}],

在构造函数中注入这个token

1
2
3
constructor(@Inject(AppTheme) private theme: string) {
console.log(theme);
}

token是对象

对于复杂的token,我们将其封装到一个JS对象中,如果一个app中有多个配置项,我们可以将这些配置项封装到一个对象AppConfig中,然后使用useValue来提供这个对象。

定义一个token

首先定义token对应的对象类型,这里使用typescript中的interface来定义。

1
2
3
4
5
6
7
8
// src/app/config/AppConfig.ts
export interface AppConfig {
theme: string;
version: string;
production: boolean;
baseUrl: string;
apiEndpoint: string;
}

然后定义该类型对应的token实例。

1
2
// src/app/config/AppConfig.ts
export const APP_CONFIG = new InjectionToken<AppConfig>('app config');

使用useValue来提供这个token

先创建一个AppConfig对象,然后使用useValue来提供这个对象。

1
2
3
4
5
6
7
8
9
10
// app.component.ts
const currentAppConfig: AppConfig = {
theme: 'light',
version: '1.0.0',
production: true,
baseUrl: 'https://example.com',
apiEndpoint: 'https://example.com/api',
}

providers: [{provide: APP_CONFIG, useValue: currentAppConfig}],

在构造函数中注入这个token

1
2
3
constructor(@Inject(APP_CONFIG) private config: AppConfig) {
console.log(config);
}

这样在Component中,就可以使用this.config来访问这个配置对象了。

References

  1. https://angular.dev/guide/di/dependency-injection-providers
  2. https://medium.com/@matsal.dev/angular-usevalue-useclass-useexisting-and-usefactory-in-a-nutshell-97db8d206084

:last-child

:last-child是一个CSS伪类,它表示一组元素中的最后一个。

:last-of-type

:last-of-type也是一个CSS伪类,它表示一组同类元素中的最后一个。注意这里多了一个限制:同类

那么这两者有什么区别呢?我们来看一个例子。
给定如下html代码,现在要求你写一段CSS,设置最后一个li元素的背景颜色为绿色。

1
2
3
4
5
<ul>
<li>Angular</li>
<li>React</li>
<li>Vue</li>
</ul>

以下两种写法都可以实现这个要求。

1
2
3
li:last-child {
background-color: green;
}
1
2
3
li:last-of-type {
background-color: green;
}

现在我们稍稍改动一下html代码,增加一个span元素作为最后一个子元素。

1
2
3
4
5
6
<ul>
<li>Angular</li>
<li>React</li>
<li>Vue</li>
<span>JavaScript</span>
</ul>

这时候下面的方法就不好使了,为什么呢?实际上li:last-child不会选中任何元素,因为span是这组元素的最后一个元素,所以要选中这组元素的最后一个li元素是找不到的。如果改成span: last-child,那么就可以选中span元素了。

1
2
3
li:last-child {
background-color: green;
}

但是下面这种方法仍然好用,因为我们指定了type为li,所以它会选中最后一个li元素。

1
2
3
li:last-of-type {
background-color: green;
}

参考

Table of content

Feature Description
provideIn A new way to declare service scope

provideIn


In Angular 6, you can use the provideIn property in the @Injectable decorator to declare the scope of the service.

Introduction

The get syntax binds an object property to a function that will be called when that property is looked up. It can also be used in classes.

Here is an example:

1

Case

When you use getter, you would better provide setter for the property too. If you only provide getter, you might got errors, here is a real case.

We have a child component with a name property, and we want to format the name to uppercase in the child component. the name property is passed from the parent component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// child.component.ts
@Component({
standalone: true,
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent {
private _name = '';

@Input() set name(name: string) {
this._name = name
}

formatName(name: string) {
return name.toUpperCase();
}
}
1
2
<!-- child.component.html -->
<div>Uppercase name: {{ formatName(name) }}</div>

And in the parent component’s template, we pass the name property to the child component.

1
2
<!-- parent.component.html -->
<app-child name="Philip"></app-child>

When you run the code, you will get the following error:

1
TypeError: Cannot read properties of undefined (reading 'toUpperCase')

The error is caused by the formatName function in the child component, here name is undefined, we can’t call toUpperCase on undefined, but why?

1
return name.toUpperCase();

When we read name in the formatName function, it will trigger the getter of the name property, but we only provide the setter for the name property, so name is undefined.

To fix this error, we need to provide the getter for the name property.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// child.component.ts
@Component({
standalone: true,
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent {
private _name = '';

@Input() set name(name: string) {
this._name = name
}

get name() {
return this._name;
}

formatName(name: string) {
return name.toUpperCase();
}
}

References:

  1. get in object

Introduction

URL fragment is the part of the URL that comes after the # symbol.

url

1. A fragment in a url specify the location within the page.

How to go to the specific location in the page?

  1. Create a empty <a> tag with id attribute set to the fragment part in url.
1
2
3
4
5
6
<a id="fragment1">
content of fragment 1
</a>
<a id="fragment2">
content of fragment 2
</a>

input the following url in your browser http://example.com/page.html#fragment1, the browser will scroll to the location of the fragment1 in the page.

If you want to jump to the specific location in the page by clicking a link in the page, you can create a link with its href set to the id part of the section.

1
2
<a href="#fragment1">Go to fragment 1</a>
<a href="#fragment2">Go to fragment 2</a>

2. Fragment never send to server in http request.

The fragment part of the URL is never sent to the server. It is only used by the browser. Take the following URL as an example, when you input it in the browser, observe the network tab in the developer tools, you will find that the fragment part is not sent to the server. the server only receives the request for the URL http://example.com/page.html.

1
https://localhost:3000/page1#fragment

3. Anything after the first # is a fragment identifier.

The url below want to set the color by the fragment part of the URL, but it won’t work because browser will parse the fragment part as #ffff&shape=circle instead of #ffff.

1
http://example.com/?color=#ffff&shape=circle

4. Changing fragment id doesn’t reload a page but create history.

When you change the fragment part of the URL, the page will not reload, but it will create a new entry in the browser history. You can use the forward and back button in the browser to navigate between the history entries.

## 5. Googlebot ignores the fragment part of the URL.

# References:
1. https://blog.httpwatch.com/2011/03/01/6-things-you-should-know-about-fragment-urls/
2. https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Identifying_resources_on_the_Web#Fragment

Export * from …

This syntax is used to re-export all named exports from another module. It does not work for default exports.

1
2
3
4
5
6
7
// math.mjs
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
const multiply = (a, b) => a * b;

export { add, subtract };
export default multiply; // default export

File index.mj re-exports all named exports from math.mjs, in this case, add and subtract, but not multiply, since it is a default export.

1
2
// index.mjs
export * from './math.mjs';

Now you can import all named exports from index.mjs using the following syntax:

1
2
3
4
5
6
7
8
9
// main.mjs
import * as mathfunc from './index.mjs';

console.log(mathfunc.add(1, 2)); // 3
console.log(mathfunc.subtract(2, 1)); // 1

// This one does not work, will cause an error:
/// mathfunc.multiply is not a function
console.log(mathfunc.multiply(2, 3)); // 6

最近项目中出现一个问题,后端监测系统发现,同样一个请求在短时间内一次性发起了四次,这是极大的浪费,于是考虑使用缓存策略来解决。

首先模拟一个获取后端数据的方法,这里使用setTimeout模拟一个异步请求。这个方法在延时一秒钟后返回一个结构化数据。{id: 1, name: 'Data 1'}

1
2
3
4
5
6
7
8
9
10
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
id: 1,
name: 'Data 1',
});
}, 1000);
});
}

接下来我们模拟短时间内的四次请求,可以使用for循环来依次发送四次请求。

1
2
3
4
5
for (let i = 0; i < 4; i++) {
fetchData().then(data => {
console.log(data);
});
}

我们使用async/await语法封装一下上面的for循环。

1
2
3
4
5
6
async function fetchDataFourTimes() {
for (let i = 0; i < 4; i++) {
const data = await fetchData();
console.log(data);
}
}

最后,我们调用一下fetchDataFourTimes方法,查看控制台输出。

1
fetchDataFourTimes();

控制台输出如下:

1
2
3
4
{ id: 1, name: 'Data 1' }
{ id: 1, name: 'Data 1' }
{ id: 1, name: 'Data 1' }
{ id: 1, name: 'Data 1' }

注意,这里有一个问题,我们很难区分这四次请求是不是真的发送了四次,因为返回的数据都是一样的。为了验证后续的cache策略确实能工作,我们可以在fetchData方法中加入一个console.log来验证。

1
2
3
4
5
6
7
8
9
10
11
function fetchData() {
console.log('fetchData');
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
id: 1,
name: 'Data 1',
});
}, 1000);
});
}

再次调用fetchDataFourTimes方法,查看控制台输出。

1
2
3
4
5
6
7
8
fetchData
{ id: 1, name: 'Data 1' }
fetchData
{ id: 1, name: 'Data 1' }
fetchData
{ id: 1, name: 'Data 1' }
fetchData
{ id: 1, name: 'Data 1' }

接下来,使用缓存策略来解决这个问题。

我们可以改造一下fetchDataFourTimes,使用一个全局变量来存储请求的数据,然后在下次请求时,先判断是否有缓存数据,如果有则直接返回缓存数据,否则发送请求。

1
2
3
4
5
6
7
8
9
10
11
12
const cacheRequest = {};

async function fetchDataFourTimesWithCache() {
for (let i = 0; i < 4; i++) {
if (!cacheRequest['fetchData']) {
cacheRequest['fetchData'] = await fetchData();
} else {
console.log('Hit cache');
console.log(cacheRequest['fetchData']);
}
}
}

然后运行fetchDataFourTimesWithCache方法,查看控制台输出。

1
2
3
4
5
6
7
8
fetchData
{ id: 1, name: 'Data 1' }
Hit cache
{ id: 1, name: 'Data 1' }
Hit cache
{ id: 1, name: 'Data 1' }
Hit cache
{ id: 1, name: 'Data 1' }

可以看到,第一次请求了数据,后面三次都是直接从缓存中获取的,这样就避免了多次请求的问题。

这里有一点需要注意,那就是缓存用的cacheRequest必须是位于缓存函数外部,如果直接放在缓存函数内部,那么每次调用缓存函数都会重新初始化cacheRequest并将其设置为{},导致缓存失效。

我们也可以使用闭包的特性,将cacheRequest封装到函数内部,这样的话,cacheRequest就成了fetchDataFourTimesWithCache函数的内部变量,可以避免全局变量的污染。在fetchDataFourTimesWithCache中,我们返回了一个匿名异步函数,而cacheRequest相对于这个匿名函数是一个外部变量,这样就形成了一个闭包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function fetchDataFourTimesWithCache() {
const cacheRequest = {};

return async () => {
for (let i = 0; i < 4; i++) {
if (!cacheRequest['fetchData']) {
cacheRequest['fetchData'] = await fetchData();
} else {
console.log('Hit cache');
console.log(cacheRequest['fetchData']);
}
}
};
}

const fetchDataFourTimesWithCacheFn = fetchDataFourTimesWithCache();
fetchDataFourTimesWithCacheFn();