Angular 18+ 高级教程 – Angular 的局限和 Github Issues
前言
Angular 绝对有很多缺陷,Issue 非常多,workaround 非常多。
我以前至少有 subscribe 超过 20 个 Issues,几年都没有 right way 处理的。
Angular 不支持 Custom @Decorator
Angular 自己是有在用 Decorator (旧版,不是 TypeScript 5.0 后的版本) 的,但是我们可用不了。
相关 Github Issues:
Class decorator stopped working on angular@15
Custom decorators not working using AoT
Angular 不支持 PostCSS Configuration
Angular CLI 自己是有在用 PostCSS 来支持 Tailwind CSS 的。但是!它不允许我们设置。
连 postcss-preset-env 我们都设置不了。
像下图这样,办不到😡
相关 Github Issues:
Add postcss-preset-env to Angular build process / allow for configuring custom PostCSS plugins
而且短期没有任何计划要支持😡
Angular 不支持 Static Image Hash
Template
<div class="bg-img"></div>
Styles
.bg-img { background-image: url('../../public/nana.jpg'); background-size: cover; width: 360px; aspect-ratio: 16 / 9; }
ng build
两个知识点
-
path 换了
原本是 '../../public/nana.jpg' (我们写的时候是依据开发时的文件路径)
变成了 './media/nana-LHLWYWZO.jpg' (这是依据项目发布后的文件路径)
-
file 换了
原本是 nana.jpg 换成了 nana-LHLWYWZO.jpg
有了这个 hash 就可以做缓存了
结论:Angular 在 build 的时候会对 CSS background-image 做 hash 处理。
好,我们接着来看 <img> element。
Template
<img src="../../public/nana.jpg" alt="yangmi">
ng build
两个知识点:
-
path 没有换
Angular 没有把开发路径换成发布后的路径,如果我们不做处理会导致最终图片访问失败。
-
没有 hash 版本的 img
Angular 也没有创建出 hash 版本的 img。
结论:<img> element 需要我们自己处理路径和 hash,Angular 完全没有处理,CSS background-image 才有。
解决方案:通常路径可以直接写发布后的路径,hash 可以使用 hash-file.online,总之就是没有工程化了。
相关 Github Issue – Images in templates are not fingerprinted
Angular Team 没有打算去支持它,目前也没有任何 workaround (工程化的方案)。
@Directive 指令不支持 CSS
这个 Github Issue – @Directive should support styles and styleUrls properties 排在第 5 名。
组件可以封装 CSS Styles,独立一个 .css 或者 .scss 文件,这个非常方便。
指令虽然没有 Template,但它的职责是 decorate 组件或 Element。你可以用指令去 add class,但这个 class 的样式谁负责呢?
为什么不可以是指令负责?
这就是大家正在争取的 feature,虽然已经争取了 7 年 (from 2017),而且还是第 5 名,但 Angular Team 任然无动于衷...😔
Workaround follow Angular Material
也谈不上 workaround 吧,就是一些粗糙的手法。
作为 UI 组件库,Angular Material 自然会遇到这个问题,那它怎么弄呢?
简单 -- 全局 CSS Styles 😂
MatRipple 是一个指令
使用方式
<button matRipple>Submit</button>
在任意一个 Element 上添加 attribute matRipple。
效果
点击后出现波纹。
MatRipple 的 CSS Styles 写在 _ripple.scss 里
咦...它们怎么连上的呢?
指令添加了 class
然后在项目的 styles.scss 里 @use @angular/material + @include mat.core
styles.scss 是全局 CSS Styles,每一个组件都会被 effect 到。
mat.core() 会把 _ripple.scss 放到全局,所以它们就连上了。
这个方案最大的问题就是无法按需打包,无论你有没有用到 MapRipple 指令,_ripple.scss 的 CSS Styles 都会被打包进项目里。
无能的 Reactive Forms
Reactive Forms 是 Angular v2.0 推出的功能,从 v2.0 到 v18 主要功能几乎完全没有修改和增强过。
只有 v14 的时候勉强加入了类型检测。是的,v2.0 到 v13 一直都是写 AnyScript...😨
这么糟糕的功能,难道没有人吐槽?没有人提 feature request 吗?
当然有!Github Issue – A proposal to improve ReactiveFormsModule,意见是多到...🙈
当然,Angular 团队是不可能去提升这些 feature 的,它们从来都不会搭理社区的意见,除非 Google 团队需要,否则 Reactive Forms 还会继续保持原样好多年呢。
近期一直在吹的 Signal,Reactive Forms 当然也不支持 Github Issue – Signals for Reactive and Template-based Forms。
你或许会疑惑...那些用 Angular 的人到底是怎样处理表单的呢?
Reactive Forms 不给力,社区又没见任何一个 workaround library...这不是很奇怪吗?
其实,一点也不奇怪,没轮子就造一个咯...这就是 Angular 用户会干的事儿。
自己做的轮子,自己用,也从不考虑共享给社区。
因为对于这些人来说,量身定做一个轮子是简单的,共享给社区是低价的 (因为别人的需求不一样,而且别人也能自己造)。
这也就造成了 Angular 社区轮子很少的原因 -- 因为不需要。
Angular + CSS 4 =💩
Angular 对 CSS 4 的 selector 支持不是很好,某些搭配会产生化学反应,时不时会给你个惊喜🎉。
当 :host 遇上 :has
HelloWorld 组件
<div class="container"> <div class="wrapper"> <h1>Hello World</h1> </div> </div> <div class="wrapper"> <h1>Hello World</h1> </div>
一个 .container > .wrapper > h1
一个 .wrapper > h1
HelloWorld Styles
:host { display: block; &:has(.wrapper) { background-color: pink; } .container:has(.wrapper) { background-color: lightblue; } }
组件内有 wrapper 就红色,container 内有 wrapper 就蓝色。
效果
没问题,那我们加一个条件,自由子层有 wrapper 才算
&:has(> .wrapper) { background-color: pink; } .container:has(> .wrapper) { background-color: lightblue; }
多了 '>' 符号
效果
红色没了...不对啊
组件的子层有 wrapper 啊,怎么会没有呢?
我们 ng build 看看最终生成出来的 CSS,看看区别是什么
// 没有箭头的 ok &:has(.wrapper) { background-color: pink; } // 有箭头的错了 &:has(> .wrapper) { background-color: pink; }
一个有箭头,一个没有箭头,ng build
[_nghost-%COMP%]:has(.wrapper) { background-color: pink; } [_nghost-%COMP%]:has(> .wrapper)[_ngcontent-%COMP%] { background-color: pink; }
有箭头的,后面多了一个 [_ngcontent-%COMP%],这就是导致它坏掉的原因。
至于这是啥...我也不知道,也懒得知道,多半是 Angular 团队的不作为。
目前我的 workaround 是使用 ::ng-deep
&:has(::ng-deep > .wrapper) { background-color: pink; } /* after ng build */ [_nghost-%COMP%]:has(> .wrapper) { background-color: pink; }
嗯...正常了。
这种化学反应只会出现在 :host + :has (CSS 4 selector) 搭配上。
假如不是 :host 而是子层,那效果是这样
.container:has(> .wrapper) { background-color: pink; } /* after ng build */ [_nghost-%COMP%] .container[_ngcontent-%COMP%]:has(> .wrapper)[_ngcontent-%COMP%] { background-color: pink; }
虽然结尾有 [_ngcontent-%COMP%],但它的匹配却是正确的...我没看懂,估计要源码才能解密了...以后吧。
当 ng-content ::ng-deep 遇上 CSS4 :not() complex selector
我提的 Github Issue – bug(Compiler): incorrect compile when using CSS 4 :not() complex selector
HelloWorld Styles
h1:not(.aa, .bb) { background-color: red; color: white; }
这是 CSS4 :not() complex selector。
当 h1 有 .aa 或 .bb 时就不要红色。
HelloWorld Template
<h1 class="aa">Hello World</h1> <h1 class="bb">Hello World</h1> <h1>Hello World</h1>
效果
第三个 h1 没有 .aa 也没有 .bb 所以红色。
现在我们加上 ::ng-deep
::ng-deep h1:not(.aa, .bb) { background-color: red; color: white; }
效果一摸一样。
好,我们把 h1 用 ng-content 的方式传进来
App Template
<app-hello-world> <h1 class="aa">Hello World</h1> <h1 class="bb">Hello World</h1> <h1>Hello World</h1> </app-hello-world>
HelloWorld Template
<ng-content />
按理说,我们使用了 ::ng-deep,哪怕是 <ng-content> 内都能渲染到,但是...
没有红色,我们把 CSS4 换成 CSS3 写法
/* stylelint-disable-next-line selector-not-notation */ ::ng-deep h1:not(.aa):not(.bb) { background-color: red; color: white; }
效果
竟然可以了...😔
我们看看 ng build 后它们各自长啥样
第一个 Only CSS4,没问题
h1:not(.aa, .bb) { background-color: red; color: white; } /* after ng build */ h1[_ngcontent-%COMP%]:not(.aa, .bb) { background-color: red; color: white; }
第二个,::ng-deep + CSS4,这个有问题
::ng-deep h1:not(.aa, .bb) { background-color: red; color: white; } /* after ng build */ h1:not(.aa, .bb)[_ngcontent-%COMP%] { background-color: red; color: white; }
注意看,结尾又是多了一个 [_ngcontent-%COMP%],这个我们上一 part 的例子雷同,估计是同一个问题。
第三个,::ng-deep + CSS3,没有问题
::ng-deep h1:not(.aa):not(.bb) { background-color: red; color: white; } /* after ng build */ h1:not(.aa):not(.bb) { background-color: red; color: white; }
没有 [_ngcontent-%COMP%] 就没有问题。
最后,我开的 Issue 没过多久就被 close 掉了,因为早就有类似的问题了啊。
之后统一追踪 Emulated view encapsulation incorrectly transforms CSS that uses :is() or :where()
总结
想用 CSS4 一定要谨慎,因为 Angular 总是比别人慢好多拍的,也希望这些 Issue 有天会得到团队的重视。
Passing undefined to @Input !== optional @Input
在 JavaScript,optional parameter / default parameter value 是这样工作的:
function doSomething(value: string = 'default value'){ console.log(value); } doSomething('a value'); // 'a value' doSomething(); // 'default value' doSomething(undefined); // 'default value'
传入 undefined 等价于没有传值。
然而,Angular 的 optional @input 却不是这样工作的:
export class HelloWorldComponent { readonly value = input<string>('default value'); constructor() { effect(() => console.log(this.value())); } }
HelloWorld 组件有一个 optional @Input value,并且配有 default value。
使用它
<app-hello-world value="a value" /> <!--log 'a value'--> <app-hello-world /> <!--log 'default value'-->
有传值,和没有传值的行为和 JS 一样。
但传入 undefined 却会直接报错。
因为 Angular 把 undefined 也当作值来看待,这点和 JS 不同。
假如我们希望它对待 undefined 等同于没有传值,那我们需要在内部自己做一层处理。
像这样:
export class HelloWorldComponent { private readonly defaultValue = 'default value'; readonly value = input(this.defaultValue, { transform: (value: string | undefined) => typeof value === 'string' ? value : this.defaultValue, // undefined 就输出 default value }); }
唉...只能说勉强能用吧。
Angular 不支持 AddEventListenerOptions (capture, once, passive)
这个
<button (click)="0">click me</button>
等价于
button.addEventListener('click', () => 0);
这个
<button (click.capture.once)="0">click me</button>
不等于
button.addEventListener('click', () => 0, { capture: true, once: true });
capture,once,passive 这些通通不支持。
要嘛自己写 DOM Manipulation,要嘛自定义 EventManagerPlugin。
相关 Github Issue – UseCapture:true event handlers (31-08-2016 – 今天 14-12-2024 依然 open)
our side 指的是 Google, Angular 是一款 Google 1st 的 Framework。
大总结
本篇分享 Angular 用户一定会遇到的大麻烦。