Web Components 与 Angular Component

Web Components 与 Angular Component

关于组件的概念,现在使用已经很广泛了,我们今天就来深入的了解一下 Web ComponentsAngular 当中的 Component

Web Components

W3C 为统一组件化标准方式,提出 Web Components 的标准,它允许我们创建可重用的定制元素(它们的功能封装在代码之外)并且在 Web 应用中使用它们,Web Components 标准主要包括以下几个重要的概念

  • Custom elements(自定义元素)
    • 可以创建自定义的 HTML 标记和元素
  • Shadow DOM(影子 DOM
    • 用于将封装的 Shadow DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能
    • 通过这种方式,可以保持元素的功能私有,这样就可以被脚本化和样式化的同时而不用担心与文档的其他部分发生冲突
  • HTML templatesHTML 模板)
    • 简单来说就是使用 <template><slot> 标签去预定义一些内容,但并不加载至页面,而是将来使用 JavaScript 代码去初始化它
    • 可以作为自定义元素结构的基础被多次重用

下面我们就通过一个简单的示例来看看 Web Components 到底是怎么使用的,例子摘取自 mdn/web-components-examples,但是稍微调整了一下,使用方式很简单,直接在页面当中使用我们自定义的组件即可,如下

1
2
3
<!-- 使用 -->
<component-a text="我是自定义组件 A"></component-a>
<component-b text="我是自定义组件 B"></component-b>

具体实现如下

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
// 定义组件 A 和 B
window.customElements.define('component-a',
class extends HTMLElement {
constructor() {
super()
const pElem = document.createElement('p')
pElem.textContent = this.getAttribute('text')
const shadowRoot = this.attachShadow({ mode: 'open' })
shadowRoot.appendChild(pElem)
}
}
)

window.customElements.define('component-b',
class extends HTMLElement {
constructor() {
super()
const pElem = document.createElement('p')
pElem.textContent = this.getAttribute('text')
const shadowRoot = this.attachShadow({ mode: 'closed' })
shadowRoot.appendChild(pElem)
}
}
)

document.querySelector('html').addEventListener('click', e => {
console.log(e.composed)
console.log(e.composedPath())
})

很简单的一个示例,就算没有了解过 Web Components 相关知识也可以看懂大概,我们来简单的梳理一下

  • window.customElements,简单来说就是用来定义一个自定义标签(custom elements
  • attachShadow,给指定的元素挂载一个 Shadow DOM,并且返回它的 ShadowRoot,简单来说就是返回指定 Shadow DOM 封装模式,有下面两种方式
    • open,指定为开放的封装模式
    • closed,指定为关闭的封装模式,会让该 ShadowRoot 的内部实现无法被 JavaScript 访问及修改,也就是说将该实现不公开(比如 <video> 标签)

另外,在 Web Components 当中也是有生命周期回调函数存在的,可以指定多个不同的回调函数,它们将会在元素的不同生命时期被调用,主要有下面四个

  • connectedCallback,当 customElements 首次被插入文档 DOM 时,被调用
  • disconnectedCallback,当 customElements 从文档 DOM 中删除时,被调用
  • adoptedCallback,当 customElements 被移动到新的文档时,被调用
  • attributeChangedCallback,当 customElements 增加、删除、修改自身属性时,被调用

上面就是一个简单的 Web Components 示例,这里也就只简单的介绍一下,如果想了解更多,可以参考 Web Components

在了解了 Web Components 的基本概念以后,我们就来看看 Angular 当中的 Component

Angular Component

Angular 当中,Component 属于指令的一种,即组件继承于指令(详细可见 packages/core/src/metadata/directives.ts),所以我们可以简单的将其理解为拥有模板的指令(其它两种是属性型指令和结构型指令),基本组成如下

1
2
3
4
5
6
7
@Component({
selector: 'hello',
templateUrl: './hello.component.html',
styleUrls: ['./hello.component.scss']
})

export class HelloComponent implements OnInit { }

主要分为以下几部分

  • 组件装饰器,每个组件类必须用 @component 进行装饰才能成为 Angular 组件
  • 组件元数据,指的是 selectortemplate 这一系列的属性
  • 组件模板,每个组件都会关联一个模板,这个模板最终会渲染到页面上,页面上这个 DOM 元素就是此组件实例的宿主元素
    • 一般来说有两种引入方式 templateUrltemplate,区别就是内联和外链
  • 组件类,组件实际上也是一个普通的类,组件的逻辑都在组件类里定义并实现
  • 组件接口,组件可以定义内部需要实现的接口(比如上面的 OnInit 对应着组件的生命周期钩子 ngOnInit()

组件元数据

主要分为两种,自身元数据属性和从 core/Directive 上继承过来的,先来看自身元数据属性

自身元数据属性

名称 类型 作用
animations AnimationEntryMetadata[] 设置组件的动画
changeDetection ChangeDetectionStrategy 设置组件的变化监测策略
encapsulation ViewEncapsulation 设置组件的视图包装选项
entryComponents any[] 设置将被动态插入到该组件视图中的组件列表
interpolation [string, string] 自定义组件的插值标记,默认是双大括号
moduleId string 设置该组件在 ES/CommonJS 规范下的模块 id,它被用于解析模板样式的相对路径
styleUrls string[] 设置组件引用的外部样式文件
styles string[] 设置组件使用的内联样式
template string 设置组件的内联模板
templateUrl string 设置组件模板所在路径(外链)
viewProviders Provider[] 设置组件及其所有子组件(不含 ContentChildren)可用的服务

从 core/Directive 继承

名称 类型 作用
exportAs string 设置组件实例在模板中的别名,使得可以在模板中调用
host {[key: string]: string} 设置组件的事件、动作和属性等
inputs string[] 设置组件的输入属性
outputs string[] 设置组件的输出属性
providers Provider[] 设置组件及其所有子组件(含 ContentChildren)可用的服务(依赖注入)
queries {[key: string]: any} 设置需要被注入到组件的查询
selector string 设置用于在模板中识别该组件的 CSS 选择器(组件的自定义标签)

下面我们就来看看一些比较常用的元数据的具体含义

inputs

有两种写法,第一种方式不太推荐使用,用的比较多的是下面那种,推荐在组件当中使用 @Input() 来进行接收

1
2
3
4
5
6
7
8
@Component({
selector: 'hello-component',
inputs: ['param']
})

export class HelloComponent {
param: any
}

等价于下面这种

1
2
3
4
5
6
7
@Component({
selector: 'hello-component'
})

export class HelloComponent {
@Input() param: any
}

outputs

同上,和 inputs 类似

1
2
3
4
5
6
7
8
@Component({
selector: 'hello-component',
outputs: ['test']
})

export class HelloComponent {
test = new eventEmitter<false>()
}

等价于

1
2
3
4
5
6
7
@Component({
selector: 'hello-component'
})

export class HelloComponent {
@Output() test = new eventEmitter<false>()
}

host

host 主要用来绑定事件,同上面一样,还是推荐使用 @HostBinding 来进行绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component({
selector: 'hello-component',
host: {
'(click)': 'onClick($event.target)', // 事件
'role': 'nav', // 属性
'[class.pressed]': 'isPressed', // 类
}
})

export class HelloComponent {
isPressed: boolean = true
onClick(elem: HTMLElement) {
console.log(elem)
}
}

等价于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component({
selector: 'hello-component'
})

export class HelloComponent {

@HostBinding('attr.role') role = 'nav'
@HostBinding('class.pressed') isPressed: boolean = true
@HostListener('click', ['$event.target'])

onClick(elem: HTMLElement) {
console.log(elem)
}

}

queries

主要用来视图查询,就是 @ViewChild 另外一种写法,推荐使用 @ViewChild 装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component({
selector: 'hello-component',
template: `
<input #theInput type='text' />
<div>Demo Component</div>
`,
queries: {
theInput: new ViewChild('theInput')
}
})

export class HelloComponent {
theInput: ElementRef
}

等价于

1
2
3
4
5
6
7
8
9
10
11
@Component({
selector: 'hello-component',
template: `
<input #theInput type='text' />
<div>Demo Component</div>
`
})

export class HelloComponent {
@ViewChild('theInput') theInput: ElementRef
}

queries

这个主要用来内容查询使用的,也就是 @ContentChild 装饰器,不过一般情况下使用较少,模版如下

1
2
3
<my-list>
<li *ngFor="let item of items">{{item}}</li>
</my-list>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Directive({
selector: 'li'
})

export class ListItem {}

@Component({
selector: 'my-list',
template: `
<ul>
<ng-content></ng-content>
</ul>
`,
queries: {
items: new ContentChild(ListItem)
}
})

export class MyListComponent {
items: QueryList<ListItem>
}

等价于

1
2
3
4
5
6
7
8
9
10
11
12
@Component({
selector: 'my-list',
template: `
<ul>
<ng-content></ng-content>
</ul>
`
})

export class MyListComponent {
@ContentChild(ListItem) items: QueryList<ListItem>
}

styleUrls 和 styles

这两个元数据一般是用来设置样式,styleUrlsstyles 是允许同时指定的,不过两者之间存在优先级的关系,如下

1
模板内联样式 > styleUrls > styles

不过一般还是建议使用 styleUrls 引用外部样式表文件,这样代码结构相比 styles 更清晰、更易于管理

changeDetection

这个参数主要用来设置组件的变换检测机制,有两种取值方式 DefaultOnPush,默认为 Default

  • ChangeDetectionStrategy.Default
    • 组件的每次变化监测都会检查其内部的所有数据(引用对象也会深度遍历),以此得到前后的数据变化
  • ChangeDetectionStrategy.OnPush
    • 组件的变化监测只检查输入属性(即 @Input 修饰的变量)的值是否发生变化,当这个值为引用类型(ObjectArray 等)时,则只对比该值的引用

显然,OnPush 策略相比 Default 降低了变化监测的复杂度,很好地提升了变化监测的性能,如果组件的更新只依赖输入属性的值,那么在该组件上使用 OnPush 策略是一个很好的选择

encapsulation

关于这个属性的详细介绍可以参考 :host 和 ::ng-deep,简单来说就是控制视图的封装模式,有三种模式,原生(Native)、仿真(Emulated)和无(None

生命周期

Angular 使用构造函数新建组件后,就会按下面的顺序在特定时刻调用这些生命周期钩子方法

生命周期钩子 调用时机
ngOnChanges ngOnInit 之前调用,或者当组件输入数据(通过 @Input 装饰器显式指定的那些变量)变化时调用
ngOnInit 第一次 ngOnChanges 之后调用,建议此时获取数据,不要在构造函数中获取
ngDoCheck 每次变化监测发生时被调用
ngAfterContentInit 使用将外部内容嵌入到组件视图后被调用,第一次 ngDoCheck 之后调用且只执行一次(只适用组件)
ngAfterContentChecked ngAfterContentInit 后被调用,或者每次变化监测发生时被调用(只适用组件)
ngAfterViewInit 创建了组件的视图及其子视图之后被调用(只适用组件)
ngAfterViewChecked ngAfterViewInit,或者每次子组件变化监测时被调用(只适用组件)
ngOnDestroy 销毁指令或者组件之前触发,此时应将不会被垃圾回收器自动回收的资源(比如已订阅的观察者事件、绑定过的 DOM 事件、通过 setTimeoutsetInterval 设置过的计时器等等)手动销毁掉

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×