Hey , you !


  • Home

  • Archives

Angular is not that good before 2023

Posted on 2023-08-15 | In js

You might have often heard someone saying that

Angular is waiting for you in 3 years.

Angular is better for big or enterprise project

Angular provides an excellent updating experience

…

In this article, I’ll show you why I think Angular is not as good as you have heard before 2023 Angular 15, 16.

Of course, I can’t be 100% fair when comparing Angular with other frameworks/libs. Also, because I’m trying to demo that Angular before 2023 is not that good, so I’ll focus too much on the disadvantages. It’s unfair for Angular. After 2023, Angular is getting much better but I’ll cover those parts in other articles.

To be fair, I’m creating 2 projects, 1 for Angular, 1 for Vue in 2023/8/15.

For Angular, I’m creating with ng new my-app-angular with @angular/cli@16.2.0.
For Vue, I’m creating with npm create vue@latest with create-vue@3.7.2.

And I will demo it step by step.

Too simple template when creating a new project

When creating a new project with Angular CLI, I have 2 options

  • Need routing
  • CSS format

While with Vue CLI,

besides the 2 options, I also have more than 4 options

  • state-management lib
  • e2e test lib
  • ESLint
  • Prettier for code formatting
  • ……

To be honest, in my opinion, for a big or enterprise project configuring the above 4 options is important.

If developers are not a senior or professional front-end engineer, or just want to save some time, and the CLI doesn’t provide these options, they’re easily missing.

If they’re not set up at the beginning of a big project, it’s not easy to make all team members agree on one specific solution. Some people like this state management lib while others like another one. Some people like semi while others don’t…

Even luckily, developers set it up in the iteration, there’s still some legacy codes need to migrate.

By the way, Angular doesn’t have an official state-management solution until now. Angular added this feature to the backlog in 2022 because of this issue. Until now, the most starred state management solution for Angular is ngrx. And there’re also many people using their own designed state management lib with RxJs and DI.

Too simple default project folder structure

For Angular, I don’t even know how to code with the template before I find the CodingStyleGuide chapter.

  • Where to put router views?
  • Where to put shared code?
  • …

While for Vue, I think developers can code immediately.

Anyway, I’ll follow the Angular style guide to create a heroes feature module.

The demo code is copied from Angular homepage demo.

And here is the UI:

Default change detection strategy is a performance killer

Have you noticed how many times the function is called at the console when the page is loaded? 6*9=54 times! Here is the code

<h2>Heroes</h2>
<ul class="heroes">
<li
*ngFor="let hero of heroes$ | async"
[class.selected]="hero.id === selectedId"
[class.sensitive]="isSensitiveHeroName(hero)"
(mouseenter)="showTooltip()"
>
<a [routerLink]="['/hero', hero.id]">
<span class="badge">{{ hero.id }}</span>{{ hero.name }}
</a>
</li>
</ul>

<button type="button" routerLink="/sidekicks">Go to sidekicks</button>
@Component({
selector: "app-hero-list",
templateUrl: "./hero-list.component.html",
styleUrls: ["./hero-list.component.scss"],
})
export class HeroListComponent implements OnInit {
heroes$!: Observable<Hero[]>;
selectedId = 0;

constructor(private service: HeroService, private route: ActivatedRoute) {}

ngOnInit() {
this.heroes$ = this.route.paramMap.pipe(
switchMap((params) => {
this.selectedId = parseInt(params.get("id")!, 10);
return this.service.getHeroes();
})
);
}

isSensitiveHeroName(hero: Hero) {
console.log("isSensitiveHeroName");
return hero.name.toLowerCase().includes("sex");
}
showTooltip() {}
}

And if you move your mouse from top to bottom, the function will be triggered 2*9*9 times!

In this case, we can use the OnPush strategy.

@Component({
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
styleUrls: ['./hero-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush // +
})

isSensitiveHeroName will be triggered 9 times in the first time and 9*9 times for mouseenter event.

So the performance is improved by 1200% with OnPush strategy. In reality, if we want to change the Default strategy to OnPush, we need to apply more changes, not just one line in this demo.

Regarding this case, there’s a better solution for Angular.

export interface Hero {
id: number;
name: string;
isSensitive?: boolean; // +
}
this.heroes$ = this.route.paramMap.pipe(
switchMap((params) => {
this.selectedId = parseInt(params.get("id")!, 10);
return this.service.getHeroes().pipe(
map((heroes) => { // +
return heroes.map((hero) => {
return {
...hero,
isSensitive: this.isSensitiveHeroName(hero), // +
};
});
})
);
})
);

Now, we get better performance, maybe the best performance. That’s why you can often see this in the Angular community.

Never call functions in the template

Ok. Here are my concerns

Is it really good to avoid using functions in the template?

For better performance, we defined a derived state isSensitive. So, each time we change the name of the hero, we need to update isSensitive.

In real-word apps, there would be many derived states dependent on 2 or more other states. So we need to add more and more code to keep the current performance which will quickly bring bugs and maintenance issues.

There might be other ways to keep the performance without writing more code. But Here are my concern

How long time does Angular need to take developers to write high-performance and easy-maintenance code? 1 month or 1 year?

Luckily, in 2023 Angular launched Signals which is in developer preview now. Signals allows you to write high-performance and easy-maintenance code.

Complicated NgModule

Now, let’s assume I want to use HeroListComponent out of HeroesModule. I need to export it from HeroesModule, then import the HeroesModule to another module(let’s assume AppModule).

@NgModule({
declarations: [HeroListComponent],
imports: [CommonModule, HeroesRoutingModule],
exports: [
HeroListComponent, // +
],
})
export class HeroesModule {}
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
AppRoutingModule,
HeroesModule, // +
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}

I can see only 1 good point. If I want to use the components exported from HeroesModule, I don’t need to import the components to AppModule again.

However, I can see many drawbacks.

It’s not easy for developers to know how many things AppModule has imported from HeroesModule. Only Angular knows.

Because a component has to be declared in a module, it’s not easy for developers to know how many things the component is dependent on in the module. For example, is HeroListComponent dependent on CommonModule and HeroesRoutingModule? We need to check.

Thus it’s quite common that if you move a component from one module to another and it’s not working because you need to find out what dependencies the component needs and move the dependencies also. Because the dependencies are not declared in the component.

In conclusion, a component can not work by itself which is hard to think about if you’re coming from other frameworks.

Luckily, we got standalone components in Angular@15 by the end of 2022. Angular team even provided a tool for you to migrate from NgModule to standalone component.

Deep binding with RxJs

Many Angular APIs are exposed with Observable, even HttpClient. However, it’s not easy for beginners to write code with fewer bugs with RxJs.

Things to be cautious in RxJs declarative style

For example, the previous HeroListComponent is implemented with a declarative style. If we remove the heroes$ | async in the template, the service.getHeroes will never be called again. If you’re new to Angular or RxJs, it might be a shock to you.

<h2>Heroes</h2>
<ul class="heroes">
<!-- <li *ngFor="let hero of heroes$ | async" [class.selected]="hero.id === selectedId" [class.sensitive]="hero.isSensitive" (mouseenter)="showTooltip()">
<a [routerLink]="['/hero', hero.id]">
<span class="badge">{{ hero.id }}</span>{{ hero.name }}
</a>
</li> -->
</ul>

Also, if the service.getHeroes throws an error once, the function will not work anymore. That’s why you can often see catchError(() => EMPTY) in declarative code.

this.heroes$ = this.route.paramMap.pipe(
switchMap((params) => {
this.selectedId = parseInt(params.get("id")!, 10);
return this.service.getHeroes().pipe(
map((heroes) => {
return heroes.map((hero) => {
return {
...hero,
isSensitive: this.isSensitiveHeroName(hero),
};
});
}),
catchError(() => EMPTY) //+
);
})
);

Things to be cautious in RxJs imperative style

In reality, many developers are using imperative programming. In this case, HeroListComponent would be like

export class HeroListComponent implements OnInit {
heroes!: Hero[];
selectedId = 0;

constructor(private service: HeroService, private route: ActivatedRoute) {}

ngOnInit() {
this.route.paramMap.subscribe((params) => {
this.selectedId = parseInt(params.get("id")!, 10);
this.service.getHeroes().subscribe((heroes) => {
this.heroes = heroes.map((hero) => {
return {
...hero,
isSensitive: this.isSensitiveHeroName(hero),
};
});
});
});
}
//...
}

And in the template, heroes$ | async needs to be changed to heroes.

However, it has bugs. Just like we need removeEventListener after addEventListener, we also need to unsubscribe or use takeUntilDestroyed.

this.route.paramMap.pipe(takeUntilDestroyed()).subscribe((params) => {
this.selectedId = parseInt(params.get("id")!, 10);
this.service
.getHeroes()
.pipe(takeUntilDestroyed())
.subscribe((heroes) => {
this.heroes = heroes.map((hero) => {
return {
...hero,
isSensitive: this.isSensitiveHeroName(hero),
};
});
});
});

However, takeUntilDestroyed is in the developer preview until now. Before 2023, we need to add more code. And one more thing, this way is unfriendly for OnPush strategy.

Short conclusion

As you can see, deep binding with RxJs makes it easier for developers to make mistakes or write less performant code.

I do think RxJs is powerful and especially good for edge cases. However, having a powerful tool doesn’t mean that we need to use it in all cases. Lots of frameworks/libs/projects without RxJs are working quite well.

Also, I didn’t mention the things developers need to know from RxJs and the very intrusive code style it brings.

Current status of Angular

As you can see, Angular has brought many new solutions. It’s a good thing, but it could be a bad thing if they don’t point out recommended solutions in time. The community could get much more split than before.

  • declarative or imperative programming
  • less or more RxJs
  • Default or OnPush
  • NgModule or standalone
  • zone.js or Singals
  • …

Choses between those will result in different styles, which also make code hard to maintain.

The first 2 choices have already made the community split. Now we have more.

In my opinion,

  • standalone + Singals are the future of Angular.
  • RxJs will be optional for Angular.
  • Official state management solution will be provided.
  • Angular will be more like other frameworks/libs.

Angular has made great choices like choosing typescript, but choosing NgModule and zone.js might have been proven not that successful. Even built-in RxJs APIs is also probably not a good solution.

Angular is not waiting for other frameworks/libs in 3 years.

He is making progress and choices. Some solutions that many frameworks and developers are not choosing often mean that they might not be that suitable for front end developing. In those cases, Angular is also learning from other frameworks/libs instead of waiting and insisting that he’s in the right direction.

Actually, frameworks/libs are both learning from each other. Learning and improving themselves is much better than thinking I’m the best one.

Issue

Source

Reference

Several Ways of Configuring Environment Variables in Web FrontEnd Project

Posted on 2023-08-13 | In js

It’s quite often that we need to define and use environment variables. May be in building process or runtime. There’re many solutions, I’ll show some of them.

Production / CI build phase

Basically, we don’t need to care about them, just follow the rules provided by the platform because the env variables are often set manually in the platform. For example, in the vercel

So, they will not appear in our projects’ code.

Development phase

If the framework you’re currently using has provided such configurations, normally we don’t need to care also, just follow the rules provided by the platform.

If not, we can consider some available solutions in the market.

dotnet

Just put .env file in the root of your project:

S3_BUCKET="my bucket"
SECRET_KEY="my secret key"

Then as early as possible in your application, import and configure dotenv:

require('dotenv').config()
console.log(process.env) // remove this after you've confirmed it is working

If you don’t want to add the require code to the source code, you can

node -r dotenv/config your_script.js

It works now.

Also, for the reason of security or interruptions between team members’ different configurations, I recommend putting .env file in the .gitignore.

For better instruction for the new developers, you can add another .env.example file not in the .gitignore with content like

# You need to create a .env file in the root with the below env variables to run this project.
S3_BUCKET="your bucket"
SECRET_KEY="your secret key"

cross-env

In the past, we had another popular package cross-env. But now, it has been archived.

I think it’s really straightforward for new users.

{
"scripts": {
"build": "cross-env S3_BUCKET=\"my bucket\" npm run start"
}
}

Just with some issues:

  • It’ll become unfriendly if I have many env variables.
  • I need to remove the variables before committing if I don’t want to share them with others.

Conclusion

For me, I would prefer dotenv at this moment.

Issue

Source

Reference

The Impact of Styles and Scripts Position in index.html

Posted on 2020-03-17 | In browser

Preface

For a long time, I was thinking if the .css files would block DOM parsing and rendering. It seems I got different results sometimes. So, I decided to test and summarize the behaviors carefully with chrome.

Let’s begin.

Put .css Files in the <head>

Normally, we would put the .css files in the head and .js files at the bottom of <body>. For example,

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./static/header.css" />
<link rel="stylesheet" href="./static/main.css" />
</head>
<body>
<div>MyApp</div>
<header>
Header
</header>
<main>
Main
</main>
<footer>Footer</footer>
<script src="./static/app.js"></script>
</body>
</html>

Say we blocked the .css files request by fiddler. And here are the questions,

  • Would header.css block DOM parsing and rendering?
  • Would header.css block main.css, app.js and other resources fetching?

And here are the results.

  1. header.css won’t block DOM parsing.
  2. header.css, main.css all blocked rendering which means the page would be always blank until all the .css files request in the <head> finished.
  3. header.css won’t block main.css, app.js and other resources fetching. Though the codes in the resources won’t be executed in advance!
  4. After header.css, main.css loaded and parsed, we will get the first paint.
  5. Then app.js was fetched, parsed and executed with DOM parsing and rendering blocked.
  6. After app.js was executed, DOM parsing and rendering keep working.
  7. DOM parsing complete.
  8. Rendering complete.

Put .css Files in the <body>

It is not very common to see this usage recently. However, test is still needed if we want to know more. Consider the following code,

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div>MyApp</div>
<link rel="stylesheet" href="./static/header.css" />
<header>
Header
</header>
<link rel="stylesheet" href="./static/main.css" />
<main>
Main
</main>
<footer>Footer</footer>
<script src="./static/app.js"></script>
</body>
</html>

Say I still block all the .css files with fiddler. What would the results be?

As you can see,

  1. The DOM parsing and rendering would work before they met header.css in the <body> which would get an earlier first paint. User can see something(usually there is a loading bar) before the styles in <body> loaded.

  2. header.css blocked DOM parsing, hence blocked rendering.

  3. header.css didn’t block other files fetching and app.js is still not executed in advance!

  4. After header.css was loaded, the DOM parsing and rendering keeps working. Then blocked by main.css again. However, user can see the header now!

  5. After main.css was loaded, app.js is executed!

  6. DOM parsing complete

  7. Rendering complete.

Compared with the first example, we can find the difference is that

  • .css files in the <body> would block DOM parsing while .css files in the <head> wouldn’t though they both blocked rendering.

Hence, if we put the styles in the <head>, we can make use of the time of styles downloading and parsing to parse more DOM.

However, .css files in the <body> would get a faster first paint and is closer to a progressive loading webpage . That’s because it doesn’t need to load all the styles in the <head> and allow content above the <link> to render.

Assuming that the requests of the styles need 1 second, let’s see the following demo.

<!-- demo1 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./static/app.css" />
</head>
<body>
<link rel="stylesheet" href="./static/header.css" />
<header>
Header
</header>
<link rel="stylesheet" href="./static/main.css" />
<main>
Main
</main>
<link rel="stylesheet" href="./static/footer.css" />
<footer>Footer</footer>
</body>
</html>
<!-- demo2 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./static/app.css" />
<link rel="stylesheet" href="./static/header.css" />
<link rel="stylesheet" href="./static/main.css" />
<link rel="stylesheet" href="./static/footer.css" />
</head>
<body>
<header>
Header
</header>
<main>
Main
</main>
<footer>Footer</footer>
</body>
</html>

Demo2 would render the page after all the styles loaded while demo1 would load and render step by step. Assuming that there is a 1 second delay in main.css, the results would be:

styles in head

styles in head

styles in body

styles in body

As you can see, the result of styles in body might be more acceptable and expected. However, with that way we might need more code because we haven’t taken consideration of script. Things can be more complicated in production.

Put .js Files in the <head> or <body>

Actually, it works like styles in <body> where we put it in the <head> or <body>. The script without async and deferred would block DOM parsing and rendering. Actually, it is specified in W3C specifications.

For classic scripts, if the async attribute is present, then the classic script will be fetched in parallel to parsing and evaluated as soon as it is available (potentially before parsing completes).
If the async attribute is not present but the defer attribute is present, then the classic script will be fetched in parallel and evaluated when the page has finished parsing.
If neither attribute is present, then the script is fetched and evaluated immediately, blocking parsing until these are both complete.

Hence for better experience we can add a loading before the script. So it’s common to put the scripts at the bottom of <body> to let the loading dom be parsed and rendering first.

Inline Scripts Blocks Rendering

It’s also worth mentioning that if we put an inline script at the bottom of the <body> the nearest or even all DOM and styles rendering before the script would be blocked until the script was executed. You can use code below to test

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./static/app.css" />
</head>
<body>
<link rel="stylesheet" href="./static/header.css" />
<header>
Header
</header>
<link rel="stylesheet" href="./static/main.css" />
<main>
Main
</main>
<link rel="stylesheet" href="./static/footer.css" />
<footer>Footer</footer>
<script>
let startTime = Date.now();
let interval = 0;
while ((interval = Date.now() - startTime) < 10000) {
if (interval % 1000 === 0) {
console.log(interval / 1000 + " second passed");
}
}
</script>
</body>
</html>

In that case, users might not see the loading bar. So take care of the inline scripts.

End

The above tests are tested in windows7 Chrome 80.0.3987.106.

References

The future of loading CSS

Source

123…18

xianshenglu

54 posts
14 categories
142 tags
© 2023 xianshenglu
Powered by Hexo
|
Theme — NexT.Muse v5.1.4