0%

简介

居中是CSS中永恒的话题,而垂直居中则是这个永恒话题中的重中之重,无论是日常工作,还是面试,你永远都绕不开垂直居中,今天咱们来详细讲解一下垂直居中。我们不会简单的罗列css样式,而是按不同分类来处理,这样更有利于理解。

inline或者inline-*元素的垂直居中(本质是文本垂直居中)

padding-top = padding-bottom

inline元素是指不会独占一行的元素,比如span, a, img等等。inline元素的垂直居中,可以通过设置padding-top和padding-bottom为相同的值来实现垂直居中。考虑如下html代码:

1
<span>This is a span</span>

我们先设置个背景色,方便查看span元素的高度,然后设置padding-toppadding-bottom为16px,这样span元素内的文本就垂直居中了。此方案也适用于多行文本。

1
2
3
background-color: green;
padding-top: 16px;
padding-bottom: 16px;

line-height = height

如果是对block element里面的文本垂直居中(比如div, p内的文本),那么可以尝试设置line-height等于height,这样也可以实现垂直居中。- 此方案不适用于多行文本。(多行文本时,文本会超出容器外,因为line-height本质上设置的是行与行之间的垂直距离)

1
<div class="content">This is a div</div>
1
2
3
background-color: green;
height: 60px;
line-height: 60px;

需要注意的是span属于inline元素,height对于inline元素是无效的。inline元素的宽度和高度由内容决定,所以heightwidth对于inline元素是无效的。但是line-height对于inline元素是有效的。

block elements的垂直居中

以如下代码为例,我们需要将子元素child垂直居中于父元素parent

1
2
3
<div class="parent">
<div class="child">Child</div>
</div>

此时,又分为两种情况,一种是我们知道子元素的高度,另一种是我们不知道子元素的高度。这两种情况有不同的处理方式。

知道元素的高度

  1. 设置父元素position: relative, 子元素position: absolute
  2. 设置子元素height: 100px, 这个是必须的,此条件就是元素高度已知。
  3. 设置子元素top: 50%, margin-top: -height/2(50px)
1
2
3
4
5
6
7
8
9
10
11
12
13
.parent {
position: relative;
height: 400px;
width: 100%;
background-color: red;
}
.child {
position: absolute;
height: 100px; /* You know the height of the element */
top: 50%;
margin-top: -50px; /* half of element height */
background-color: green;
}

不知道元素高度

大多数情况下,元素的高度是未知的,这时候可以使用使用transform: translateY(-50%);代替margin-top: -50px;

1
2
3
<div class="parent">
<div class="child">Child</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
.parent {
position: relative;
height: 400px;
background-color: red;
}
.child {
position: absolute;
top: 50%;
transform: translateY(-50%);
background-color: green;
}

使用table-cell

如果你不在乎子元素会被拉伸并填满父元素的话,可以使用table-cell来实现垂直居中。

1
2
3
<div class="parent">
<div class="child">Child</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
.parent {
position: relative;
display: table; /* 父元素使用table布局 */
height: 400px;
background-color: red;
}
.child {
display: table-cell; /* 子元素使用table-cell布局 */
vertical-align: middle; /* 垂直居中 */
background-color: green;
}

使用flex布局

这是目前来讲最方便的方式了,使用flex布局可以轻松实现水平和垂直居中。首先将父元素设置为flex布局,然后设置flex-direction: column;将布局方式改为纵向排列(默认是横向排列),然后设置justify-content: center;即可实现垂直居中。

1
2
3
<div class="parent">
<div class="child">Child</div>
</div>
1
2
3
4
5
6
7
.parent {
display: flex;
flex-direction: column;
justify-content: center;
height: 400px;
background-color: red;
}

References

  1. https://css-tricks.com/centering-css-complete-guide/

React Fiber架构

React的渲染可以分为两个主要阶段:Reconciler(协调阶段),和Committer(提交阶段)。

Reconciler(协调阶段)

工作内容: 构建Fiber树,比较新旧虚拟DOM的不同之处,生成一个变更记录,即一系列需要对真实DOM进行的操作。此阶段的特点:异步,并发,可中断。如果执行过程中有更高优先级的任务来了,那么会中断当前Reconciler的工作,转而处理更重要的任务。

Committer阶段

工作内容:将Reconciler阶段生成的变更记录应用到真实的DOM上。此阶段是同步的,不可中断的。
工作阶段:

  1. Dom更新前 - useEffect在这个阶段执行,通过微任务队列异步执行(页面渲染后执行)
  2. Dom更新 - 执行真实DOM的更新
  3. Dom更新后 - useLayoutEffect在这个阶段执行,同步(页面渲染前执行)

注意页面的渲染和JS的执行是互斥的,只有JS代码执行完,页面才能渲染,这就是useLayoutEffect的作用,可以在页面渲染前执行一些操作,比如调整布局。

数据结构

Fiber架构中采用FiberNode和FiberTree来描述虚拟DOM树。FiberNode是一个双向链表,每个节点都有一个指向父节点的指针,一个指向子节点的指针,一个指向兄弟节点的指针。FiberTree是一个树形结构,由FiberNode组成。

React diff算法

树比较

只做同层级结点比较,如果结点不存在了,则直接删除。不会继续比较其子树。这避免了夸层级移动操作,对于跨层级移动操作,相当于删除再重建。同层级结点移动呢?可以处理。

组件比较

只做同类型的组件比较,比如div和div比较,p和p比较,只有组件的类型相同,才进入子树进行深层次比较。如果类型不一致,则删除重新创建。

元素比较

对于同层级的元素结点。

  1. 元素在新集合中,但是不在原来的集合中,属于全新的结点,对集合进行插入操作。
  2. 元素在原来的集合中,但是不在新的集合中,则删除该元素。
  3. 元素在新集合中,也在原来的集合中,且元素并未更新,只是位置发生了变化,则进行移动操作。

双缓冲策略:

  1. current树负责呈现当前页面,而所有的更新都由workInProgress树来承接,当变更完成需要渲染时,将workInProgress树变成current树。

setState是同步还是异步?

这个问题需要区分开来看,在React18之前,如果executionContext被赋值了,代表该任务已经进入React调度流程中,此时React会对该任务进行异步处理(批量处理),如果executionContext没有被赋值,代表该任务还没有进入React调度流程,此时React会对该任务进行同步处理。想setTimeout, setInterval等函数都是不会进入React调度流程的,所以是同步处理。而合成事件都会进入到React调度流程中,所以会被异步处理。在React18后,如果使用createRoot创建根节点,那么setState会变成同步的。但是如果还是使用传统的render方式,那么和React18之前的处理逻辑一样。

虚拟DOM

虚拟DOM是React的核心概念之一,它是一个轻量级的JavaScript对象,用来描述真实DOM的层次结构。React通过比较新旧虚拟DOM的差异,然后只更新需要更新的部分,从而提高页面的渲染性能。

虚拟DOM的量大作用:

  1. 跨平台,因为虚拟DOM只是一个数据结构,所以可以在不同平台上使用,比如浏览器,移动端App等。这是虚拟DOM的重要特性,很多人没有意识到。
  2. 高性能,通过比较新旧虚拟DOM的差异,只更新需要更新的部分,减少了对真实DOM的操作,提高了页面的渲染性能。

React中嵌套组件的渲染顺序,生命周期,销毁顺序。

渲染顺序

当React开始渲染过程时,它首先从上到下、从父到子地渲染组件树。这意味着父组件会先于其子组件进行渲染。一旦父组件完成它的渲染(包括执行相关的生命周期方法),React会递归地进入该父组件的子组件,并按照同样的方式渲染它们。

生命周期顺序

  • 首次挂载时:
    • 父组件:constructor -> static getDerivedStateFromProps -> render -> 子组件重复相同流程 -> 子组件componentDidMount -> 父组件componentDidMount
  • 更新时(由于props变化或state变化):
    • 父组件:static getDerivedStateFromProps -> shouldComponentUpdate -> render -> 子组件重复相同流程 -> 子组件componentDidUpdate -> 父组件componentDidUpdate

销毁顺序

当组件被卸载时,React会以相反的顺序卸载组件树中的组件。也就是说,React首先卸载最深层的子组件,然后逐步向上,直到根组件。

受控组件和非受控组件

受控组件是指组件的值是由React的state来控制的,比如下面的input,其中关键一句就是value={this.state.value} 这样就把input的value和组件的state关联起来了。

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
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};

this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}

handleChange(event) {
this.setState({value: event.target.value});
}

handleSubmit(event) {
alert('A name was submitted: ' + this.state.value);
event.preventDefault();
}

render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}

自定义hook

模拟componentDidMount

当我们为useEffect的依赖传递空数组时,它只会在组件初始化时执行一次,这就相当于componentDidMount。注意,这个只是粗略的模拟,因为useEffect是异步执行,且时机是在浏览器渲染完成后,而componentDidMount是在浏览器渲染前同步执行。

1
2
3
4
5
6
7
import {useEffect} from "react";

export const useMount = ((callBack) => {
useEffect(() => {
callBack();
}, []);
})

模拟componentDidUpdate

componentDidUpdate是在每次组件的state或props更新是调用的,但是组件mount的时候不调用,所以我们需要一个变量来标志组件是否是第一次加载,这里我们使用useRef来标志组件是否是第一次加载。因为useRef包裹的值在整个组件声明周期内引用不变(有点像静态变量),并且这个值改变后不会触发组件的重新渲染。

1
2
3
4
5
6
7
8
9
10
11
export const useUpdate = (callback, deps) => {
const isMounted = useRef(false); // 🚩 关键点

useEffect(() => {
if (isMounted.current) {
callback();
} else {
isMounted.current = true; // 挂载后切换标识
}
}, deps);
};

需要注意的是,如果在React18+严格模式下运行该函数,还是会执行一次,因为React18+的严格模式会执行两次。

1
2
3
4
5
6
7
createRoot(document.getElementById('root')).render(
<StrictMode> // 严格模式,去掉即可得到正确结果
<BrowserRouter>
<App/>
</BrowserRouter>
</StrictMode>,
)

模拟componentWillUnmount

这个就比较简单了,componentWillUnmount是在组件卸载时调用的,我们可以使用useEffect的返回值来模拟这个生命周期。

1
2
3
4
5
6
7
export const useUnmount = ((callback) => {
useEffect(() => {
return () => {
callback();
}
}, []);
})

  1. How to implement responsive layout with CSS?
  2. What’s flex layout in CSS?
  3. 简述CSS中盒模型是什么?
  4. grid布局极其优点?
  5. 手写一个圣杯布局。
  6. 如何实现垂直居中?除了flex布局还有其他方案吗?
  7. float不是完全脱离文档流,这点和position: absolute有区别的。
  8. CSS中有哪些实现透明的方式?
  9. CSS中选择器有哪些?优先级是怎样的?
  10. 如何实现一个三角形?
  11. 如何实现一个圆形?
  12. 如何实现一个梯形?

Injection Context - 顾名思义,注入上下文,说的通俗点,就是在Angular代码中,什么位置可以注入,比如我们最常用的constructor就属于一个Injection Context,因为你可以在constructor中注入服务。

Angular支持的Injection Context有如下几种:

  1. In class constructor
  2. In the initializer for fields of such classes.
  3. In the factory function specified for useFactory of a Provider or an @Injectable
  4. In the factory function specified for an InjectionToken.
  5. Within a stack frame that runs in an injection context. - 这是个啥?我咋看不懂捏?

In class constructor

constructor是我们最常用的注入位置,比如我们在组件中注入服务,就是在constructor中注入的。

新的写法, 使用inject函数

1
2
3
4
5
export class AppComponent {
constructor() {
private service: MyService = inject(MyService);
}
}

旧的写法

1
2
3
4
5
export class AppComponent {
constructor(private service: MyService) {
console.log(service);
}
}

In the initializer for fields of such classes

这个是啥意思呢?就是在类的字段初始化器中,也可以注入服务,比如下面的DataService.

1
2
3
4
5
6
export class AppComponent {
private service: MyService = inject(MyService);
constructor() {
console.log(this.service);
}
}

Stack frame in an injection context

有些函数被设计成可以运行在injection context中,比如我们常用的路由守卫(router guard), 之所以这样是为了能让我们在路由守卫中注入服务。比如下面的canActivateTeam函数,就是一个路由守卫。在这个函数里,我们可以注入PermissionsServiceUserToken。这样就可以判断用户是否有权限访问某个页面。

1
2
3
4
const canActivateTeam: CanActivateFn =
(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
return inject(PermissionsService).canActivate(inject(UserToken), route.params.id);
};

Run within an injection context

有时候我们需要讲一个函数运行在injection context中,但是当前上下文并不是injection context, 这时,我们可以使用runInInjectionContext函数来创建一个新的injection context, 然后在这个新的injection context中运行我们的函数。

比如Angular框架要求effect函数是必须运行在injection context中,所以我们通常在构造函数体中运行effect函数,如果我们想在ngOnInit函数中运行effect函数呢?因为ngOnInit函数并不是injection context, 这时我们就可以使用runInInjectionContext函数来运行effect函数。

注意:使用runInInjectionContext函数需要一个EnvironmentInjector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export class CountComponent implements OnInit {
count = signal(0);
doubleCount = computed(() => this.count() * 2);

private environmentInjector = inject(EnvironmentInjector);

constructor() {
// effect(() => {
// console.log('count:', this.count());
// console.log('doubleCount:', this.doubleCount());
// });
}

ngOnInit() {
runInInjectionContext(this.environmentInjector, () => {
effect(() => {
console.log('count:', this.count());
console.log('doubleCount:', this.doubleCount());
});
})
}
}

References

  1. https://angular.dev/guide/di/dependency-injection-context

What’s Signal?

先看一下官方定义:

1
A signal is a wrapper around a value that notifies interested consumers when that value changes. Signals can contain any value, from primitives to complex data structures.

翻译一下:Signal是一个包装器,它包装了一个值,并在该值发生变化时通知感兴趣的消费者。Signal可以包含任何值,从基本类型到复杂的数据结构。据说signal借鉴了solid.js的思想(看这里),但是我没有考证过。

Signal是Angular中重要的新特性,从xxx版开始引入,至xxx版本稳定,Signal的出现,彻底更改了Angular的变更检测机制,原本基于Zone.js的变更检测机制被Signal + OnPush取代,Signal的出现,使得Angular的性能得到了极大的提升。

今天,我们就来揭开Signal的神秘面纱。

Why Signal?

首先,我们来探讨一下Angular为什么要引入Signal?

在Signal之前,Angular是基于Zone.js做变更检测的,不可否认Zone.js是一个非常强大的库,但是它也有一些缺点,比如:

  1. 性能问题:Zone.js的性能并不是很好,特别是在大型项目中,Zone.js的性能问题会暴露的更加明显。除非你使用了OnPush策略,否则Zone.js会在每次异步操作后都会触发变更检测,这样会导致性能问题。
  2. 由于Zone.js要monkey patch所有的异步操作(在Angular app启动时),所以Angular项目在启动的时候会有一些性能损失。
  3. 有些异步操作无法monkey patch,比如async/await,Angular的解决办法是将其降级到Promise。如今几乎所有浏览器都支持async/await,这种做法显然不太合理。

基于以上原因,Angular Team很早就考虑移除Zone.js,但是Zone.js的变更检测机制是Angular的核心,移除Zone.js,意味着Angular无法自动进行变更检测,也就是说变更检测的触发机制由Angular通过Zone.js自动检测,变成了需要用户手动触发,而Signal就是为此而服务的。

通过Signal定义的数据,当它变化时,Angular会自动进行变更检测,这样就不需要Zone.js了,也就解决了上面提到的问题。

signal 语法

Signal语法非常简单,下面来看看如何定义和使用signal。

定义signal

1
const count = signal(0);

上面是一个简单的signal定义,它的初始值是0。如果要定义复杂的值,可以使用泛型:

1
const user = signal<User>({name: 'zdd', age: 18}); // generic type

读取signal的值

1
2
// Signals are getter functions - calling them reads their value.
console.log('The count is: ' + count());

设置signal的值

1
count.set(1);

更新signal的值(根据前一个值)

1
2
// Signals can also be updated by passing a function that receives the current value and returns the new value.
count.update(value => value + 1);

signal的分类

Signal有两种类型:SignalCompute SignalSignal是指普通的signal,它是可读写的;Compute Signal是指由其他signal计算得到的signal,它是只读的。

上面的count就是普通的Signal,可读写,下面我们看一个compute signal的例子。下面代码中,doubleCount是一个compute signal,它的值是由count的值乘以2得到的。

1
2
const count = signal(0);
const doubleCount = computed(() => count() * 2); //
Signal Compute Signal
读写性 可读写 只读

一个例子

我们以一个todo list为例,来看看Signal的使用。首先我们来定义List组件:

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
// list-item.model.ts
import {Component, signal} from '@angular/core';
import {ListItemComponent} from '../list-item/list-item.component';
import {ListItem} from '../list-item/list-item.model';

@Component({
selector: 'app-list',
imports: [ListItemComponent],
templateUrl: './list.component.html',
styleUrl: './list.component.scss'
})
export class ListComponent {
readonly listItems = signal<ListItem[]>([
{id: '1', name: 'item 1'},
{id: '2', name: 'item 2'},
])

addItem() {
const nextId = (this.listItems().length + 1).toString();
this.listItems.set([...this.listItems(), {id: nextId, name: `item ${nextId}`}]);
}

removeItem(id: string) {
this.listItems.set(this.listItems().filter(item => item.id !== id));
}
}

在上面的代码中,我们使用readonly listItems = signal<ListItem[]>([...]);定义了一个signal, 它的初始值是一个包含两个ListItem的数组。我们还定义了一个addItem方法,用来添加一个新的ListItem,以及一个removeItem方法,用来删除一个ListItem。
signal与普通的js变量不同,它的读取和写入需要特殊的语法。

  • 读取signal的值:this.listItems() - 注意不是this.listItems,后面要加括号,有点像函数调用。
  • 写入signal的值:this.listItems.set(newValue)
1
2
3
4
5
6
7
8
9
<!--list.component.html--> 
<p>list works!</p>
<div class="list">
@for (item of listItems(); track item.id) {
<app-list-item [id]="item.id" [name]="item.name" (removeItem)="removeItem($event)" />
}
</div>

<button class="add-button" (click)="addItem()">Add Item</button>

然后我们来定义ListItem组件:

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
// list-item.model.ts
import {Component, input, output} from '@angular/core';

@Component({
selector: 'app-list-item',
imports: [],
template: `
<div class="list-item">
<div class="list-item-content">
<div>id: {{id()}}</div>
<div>name: {{name()}}</div>
</div>
<div class="list-item-actions">
<button (click)="onRemoveItem(id())">Remove</button>
</div>
</div>
`,
styleUrl: 'list-item.component.scss'
})
export class ListItemComponent {
id = input.required<string>();
name = input(''); // 注意,这是新的input语法。

removeItem = output<string>(); // 这是新的output语法。

onRemoveItem(id: string) {
this.removeItem.emit(id);
}
}

Compute Signal

Compute Signal是指依赖于其他Signal计算得到的Signal,它是只读的,当依赖的signal值变化时,compute signal的值也会相应变化。下面是一个例子:

1
2
const count: WritableSignal<number> = signal(0);
const doubleCount: Signal<number> = computed(() => count() * 2);

Computed signal有两个重要特性:

  1. lazy load - 直到你第一次读取它的值时,它才会计算。
  2. memoization - 第一次读取后缓存值,当依赖的signal值没有变化时,它不会重新计算,而是直接读取缓存的值。当依赖的signal值变化时,重新计算并缓存。

比较函数

signal是如何比较新旧值的呢?signal使用Object.is来比较新旧值,如果新旧值相等,那么signal不会触发变更检测。对于js对象来说,Object.is相当于引用(地址)比较,也就是说只有两个对象引用相等时,Object.is才会返回true。

1
2
3
4
5
const a = [1, 2, 3];
const b = [1, 2, 3];
const c = a;
console.log(Object.is(a, b)); // false
console.log(Object.is(a, c)); // true

创建signal时,你可以指定一个比较函数来改变默认的比较方式,比如使用lodashisEqual函数来进行深比较,这样当两个对象的值相等时,signal也会触发变更检测。

1
2
3
4
5
6
import _ from 'lodash';
const data = signal(['test'], {equal: _.isEqual});
// Even though this is a different array instance, the deep equality
// function will consider the values to be equal, and the signal won't
// trigger any updates.
data.set(['test']);

References

  1. https://angular.dev/guide/signals
  2. https://fullstackladder.dev/blog/2023/05/07/introduction-angular-signals/

今天这篇我们讲解一下Angular中的Input,Input是Angular中的一个装饰器,它用来接收父组件传递过来的数据。

传统的@Input()写法

为了方便展示,我们定义两个组件, 一个父组件:ListComponent, 一个子组件:ListItemComponent。为了便于展示,我们将template和style都写在component.ts中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// list-item.component.ts
import {Component, Input} from '@angular/core';

@Component({
selector: 'app-list-item',
imports: [],
template: `
<div class="list-item">
<div>id: {{id}}</div>
<div>name: {{name}}</div>
</div>
`,
styles: [`
.list-item {
border: 1px solid #ccc;
margin-bottom: 10px;
padding: 10px;
}
`]
})
export class ListItemComponent {
@Input({required: true}) id: string = '';
@Input() name: string = '';
}

ListItemComponent中,我们定义了两个属性:idname,并且使用了@Input装饰器。@Input装饰器有一个可选的参数required,如果设置为true,则表示这个属性是必须的,如果使用组件时没有给该字段赋值,则会报错。

接下来我们在ListComponent中使用ListItemComponent组件,并传递数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// list.component.ts
import { Component } from '@angular/core';
import {ListItemComponent} from '../list-item/list-item.component';

@Component({
selector: 'app-list',
imports: [ListItemComponent],
template: `
<div class="list">
@for (item of listItems; track item.id) {
<app-list-item [id]="item.id" [name]="item.name"/>
}
</div>
`,
styleUrl: './list.component.scss'
})
export class ListComponent {
listItems = [
{id: '1', name: 'item 1'},
{id: '2', name: 'item 2'},
]
}

@Input定义的字段,需要通过property binding的方式传递数据,即[id]="item.id"[name]="item.name"

基于signal的input写法

以上是旧版的写法,从Angular 17.1开始,我们可以使用新版的基于signal的input语法了。

1
2
3
4
5
6
7
8
9
import {Component, input} from '@angular/core'; // import 'input'

export class ListItemComponent {
// @Input({required: true}) id: string = '';
// @Input() name: string = '';

id = input.required<string>();
name = input<string>('');
}

由于这种类型的input的值对应的是一个signal, 所以读取值的时候,要加(),id -> id(), name -> name()

1
2
3
4
5
6
template: `
<div class="list-item">
<div>id: {{id()}}</div>
<div>name: {{name()}}</div>
</div>
`

default value

1
value = input(0); // 0 is default value

specify type

Note that, for simple values, typescript can infer the type by value, but for complex types, you need to specify the type.

1
2
value = input<number>(0); // value = input(0); // totally ok
value = input<string>(''); // value = input(''); // totally ok

required

1
value = input.required<number>(); // required

Input transform

1
2
3
4
5
6
7
8
9
export class ListItemComponent {
id = input.required<string>();
name = input('', {transform: trimString}); // apply transform
}

// define transform function, transform function should be statically analyzable and pure function.
function trimString(value: string | undefined): string {
return value?.trim() ?? '';
}

Built-in transform

  1. booleanAttribute - imitates the behavior of standard HTML boolean attributes, where the presence of the attribute indicates a “true” value. However, Angular’s booleanAttribute treats the literal string “false” as the boolean false.
  2. numberAttribute - attempts to parse the given value to a number, producing NaN if parsing fails.

Input alias

1
value = input(0, {alias: 'sliderValue'}); // aliasValue is the alias name

在模板中使用sliderValue, 注意,组件代码中仍然只能使用value

1
<custom-slider [sliderValue]="50" />

References

  1. https://angular.dev/guide/components/inputs

Reactive Forms

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
import {Component} from '@angular/core';
import {ReactiveFormsModule, FormControl, FormGroup} from '@angular/forms';

@Component({
selector: 'app-root',
template: `
<form [formGroup]="profileForm" (ngSubmit)="handleSubmit()">
<label>
Name
<input type="text" formControlName="name" />
</label>
<label>
Email
<input type="email" formControlName="email" />
</label>
<button type="submit">Submit</button>
<h2>Profile Form</h2>
<p>Name: {{profileForm.value.name}}</p>
<p>Email: {{profileForm.value.email}}</p>
</form>
`,
imports: [ReactiveFormsModule],
})
export class AppComponent {
profileForm = new FormGroup({
name: new FormControl(''),
email: new FormControl(''),
});

handleSubmit() {
alert(this.profileForm.value.name + ' | ' + this.profileForm.value.email);
}
}

今天这篇主要讨论一下Angular框架如何处理样式。

Angular如何隔离样式

因为Angular是组件话的,每一个Component有自己的样式文件,那么Angular是如何保证多个组件之间的样式不会互相影响的呢?

Angular的样式封装

Angular中有三种样式封装方式:

  • Emulated:默认的样式封装方式,通过给每个组件的样式添加一个唯一的属性,来实现样式的隔离。
  • ShadowDom:使用原生的Shadow DOM来实现样式的隔离。
  • None:不对样式进行封装,直接使用全局样式。
    关于这三种样式封装方式的详细介绍,可以参考angular-view-encapsulation

ng::deep

如何处理全局样式

这篇探讨一下Angular中::ng-deep伪类的用法。

::ng-deep是什么?

以下是::ng-deep的官方描述:

(deprecated) /deep/, >>>, and ::ng-deep
Component styles normally apply only to the HTML in the component’s own template.

Applying the ::ng-deep pseudo-class to any CSS rule completely disables view-encapsulation for that rule. Any style with ::ng-deep applied becomes a global style. In order to scope the specified style to the current component and all its descendants, be sure to include the :host selector before ::ng-deep. If the ::ng-deep combinator is used without the :host pseudo-class selector, the style can bleed into other components.

为什么需要::ng-deep?

根据我的经验,使用::ng-deep的场景有:

  1. 第三方库的样式覆盖,因为第三方库的样式有时候是无法直接通过选择器来修改的,这时候就需要使用::ng-deep

为什么要配合:host一起使用?

在我们日常的项目中,::ng-deep很少单独使用,由上面对ng::deep的描述可知,它相当于将样式变成了全局样式,如果不加:host,那么这个样式会影响到所有的组件,加了:host,则只会影响到当前组件及其后代组件。

注意:

  1. 使用了::ng-deep的组件,只有显示在页面上时(该组建对应的前端路由生效时),才会影响其他组件的样式。如果该组件没有显示在页面上,那么它的样式是不会影响其他组件的。
  2. 如果当前页面只显示了使用了::ng-deep的组件,而没有显示其他组件,那么ng::deep的样式也不会影响到其他组件。

也就是说使用了::ng-deep的组件,只有和其他组件一起显示在页面上,才会影响其他组件的样式。

实际的例子,假设有组件A在其样式文件中使用了ng::deep,对于三级标题,将其文本设置为红色。

1
2
3
4
/* style for Component A */
::ng-deep h3 {
color: red;
}

组件B也有一个三级标题,但是没有设置样式。

如果组件A和组件B同时显示在页面上,那么组件A的样式会覆盖组件B的样式,此时页面上的所有h3标题都会显示为红色。

1
2
<app-component-a></app-component-a>
<app-component-b></app-component-b>

我们在浏览器中inspect,可以看到组件A设置的三级标题样式放到了整个html的head部分。

如果组件A中在::ng-deep前加上:host,则只有组件A中的h3标题显示为红色,组件B中的h3标题不会受到影响。

1
2
3
4
/* style for Component A */
:host ::ng-deep h3 {
color: red;
}

为啥加上:host后,就不影响别的组件了呢,因为:host表示这个样式只针对当前组件和其子组件生效,由生成后的css文件也可看出这点,请看下图。

h3之前多了一个限定字符串_nghost-ng-c2124967347,这个字符串正好是组件A的选择器,这样就保证了这个样式只会影响到组件A。而组件B有一个不同的选择器_nghost-ng-c2124967348,所以组件B的h3标题不会受到影响。

如果我们不加:host,那么生成的css文件中就没有这个限定字符串,这样就会影响到所有的组件。

angular-style-ngdeep

::ng-deep只对ViewEncapsulation.Emulated有效

::ng-deep只对encapsulation: ViewEncapsulation.Emulated有效,对于encapsulation: ViewEncapsulation.ShadowDomencapsulation: ViewEncapsulation.None无效。

字符串在任何编程语言中都是非常重要的数据类型,对字符串的操作是程序员必备的技能,这篇文章探讨一下Javascript中常见的字符串操作,没有什么高深的内容,算是一篇笔记吧。

字符串反转

JS中String对象上没有reverse方法,但是Array对象上有reverse方法,所以我们可以先把字符串转成数组,然后再调用reverse方法,最后再把数组转回字符串。

1
2
3
function reverseString(str) {
return str.split('').reverse().join('');
}

除了用split('')方法,我们还可以用Array.from方法,这个方法可以把类数组对象或者可迭代对象转成数组。

1
2
3
function reverseString(str) {
return Array.from(str).reverse().join('');
}

当然也可以使用...扩展运算符,这个运算符可以把可迭代对象转成数组。

1
2
3
function reverseString(str) {
return [...str].reverse().join('');
}