stencil 组件


stencil 提供一些装饰器、生命周期钩子和渲染函数去编写一个组件。

装饰器

装饰器是一组用于声明组件元数据的函数,会在构建产物中移除,所以不会有运行时开销。

  • @Component() 声明一个类是组件
  • @Prop() 声明一个组件的特性或者属性
  • @State() 声明组件内部状态
  • @Watch() 监听 prop 或者 state 的改变,然后执行副作用
  • @Listen() 监听组件内部的 DOM 事件
  • @Event() 声明自定义事件
  • @Method() 暴露组件方法
  • @Element() 声明组件变化的自定义标签

生命周期

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

第一次挂载

connectedCallback
⬇️
componentWillLoad
⬇️
componentWillRender
⬇️
render
⬇️
componentDidRender
⬇️
componentDidLoad
⬇️
disconnectedCallback # 组件被移除

prop 或者 state 更新:

@Watch
⬇️
componentShouldUpdate
⬇️
componentWillUpdate
⬇️
componentWillRender
⬇️
render
⬇️
componentDidRender
⬇️
componentDidUpdate
⬇️
disconnectedCallback # 组件被移除

常用的:

connectedCallback 会调用多次:首次和移除后再添加到 DOM 都会调用,可设置定时器、监听原生事件等。

const el = document.createElement('my-cmp')
document.body.appendChild(el)
// connectedCallback() called
// componentWillLoad() called (first time)

el.remove()
// disconnectedCallback()

document.body.appendChild(el)
// connectedCallback() called again, but `componentWillLoad()` is not.

disconnectedCallback 组件从 DOM 中移除时调用,可在此做一些收尾工作。

componentWillLoad 在 render 之前调用, 调用一次
可再次发送 ajax 请求获取数据。

componentShouldUpdate(newValue,oldValue,property)

返回布尔值,决定组件是否重新渲染。

可以更新状态的钩子有:

componentWillLoad
@Watch
componentWillUpdate
componentWillRender

componentDidLoad(), componentDidUpdate() and componentDidRender() 更新状态,会导致再次渲染。

componentDidUpdate()、componentDidRender() 可能导致无限渲染。

父子组件的生命周期:

<cmp-a>
  <cmp-b>
    <cmp-c></cmp-c>
  </cmp-b>
</cmp-a>
cmp-a - componentWillLoad()
cmp-b - componentWillLoad()
cmp-c - componentWillLoad()

cmp-c - componentDidLoad()
cmp-b - componentDidLoad()
cmp-a - componentDidLoad()

Component Lifecycle Methods

应用加载事件

一个特殊的生命周期钩子,在整个应用加载完成后触发。

window.addEventListener('appload', event => {
  console.log(event.detail.namespace)
})

组件定义

import { Component, Prop, h, EventEmitter, Event } from '@stencil/core'

// 组件装饰器
@Component({
  tag: 'app-input', // 名字全局唯一
  styleUrl: 'index.scss', // 组件的样式
  shadow: true, // 开启 shadow root 封装组件样式
})
export class MyInput {
  @Prop() value: string | number = ''
  @Event({ eventName: 'input' }) input: EventEmitter
  // 直接使用属性名称作为事件名称
  @Event() inputChanged: EventEmitter

  onInput(e: Event) {
    // e.preventDefault() // 默认行为不行
    e.stopPropagation()
    const inputEle = e.target as HTMLInputElement
    this.input.emit(inputEle.value)
  }

  onChange(e: Event) {
    const inputEle = e.target as HTMLInputElement
    this.inputChanged.emit(inputEle.value)
  }

  render() {
    return <input value={this.value} onInput={e => this.onInput(e)} onChange={e => this.onChange(e)} />
  }
}

解读:

  • @Component装饰器声明该类是一个组件,传递一些元数据,比如组件的标签,标签必须全局唯一,且含有-,样式,是否开启 shadow 等。

tag 属性必需,更多参数

  • @Prop声明组件的属性

关于命名

在组件内部使用小驼峰命名,在 html 使用 dash-case 传递数据。

如何处理原生属性?

添加到自定义标签上。

如何传递对象、数组等复杂数据?

在 stencil 组件中,和 jsx 一样。

在 html 中,所有属性都是字符串,只能传递字符串

comInstance.setAttribute('prop', value) // 无效
comInstance.prop = value // work well 且对对象和数组无效

如何在 html 中修改 prop?

暴露方法和设置 prop 可变,在外部调用方法修改。

prop 的选项

export interface PropOptions {
  attribute?: string = false
  mutable?: boolean = false
  reflect?: boolean = false
}

prop 默认是组件内部不可变更的,否则触发警告通过 mutable 修改这个默认行为。

reflect:声明 DOM prop 是否对应到标签特性上,设置为 true,在 html 标签上,会显示该属性。

@Component({ tag: 'my-cmp' })
class Cmp {
  @Prop({ reflect: true }) message = 'Hello'
  @Prop({ reflect: false }) value = 'The meaning of life...'
  @Prop({ reflect: true }) number = 42
}

渲染结果:

<my-cmp message="Hello" number="42"></my-cmp>

不设置为 true,依然可以通过 DOM 对象拿到 prop。

修改特性名字 attribute

如何验证 prop?

在 Watch 中验证 prop 合法性,不合法抛出错误。

如何设置必需?

都默认可选,可在 Watch 验证是否必须。

  • @Event声明组件触发的事件,后面是事件名称

this.eventName.emit(data) 触发自定义事件,data 是发送到父组件的数据,监听 eventName 事件时通过 event.detail 获取到 data

在自定义标签上仍然能监听到原生事件,如何避免监听到原生事件呢?

阻止事件冒泡,必要时取消默认行为。

如何监听?

  • @Listen(eventName)监听事件,绑定到组件上,可通过第二个参数配置绑定的元素。
    @Listen 监听全局事件很有用。

  • 在 jsx 中,通过onXxx监听

  • html 中通过addEventListener

更多事件信息

组件如何响应数据变化

当 props 和 state 改变,stencil 重新渲染,比较变化时,比较的时引用,所以数组和对象,引用不变,不会更新。

Reactive Data

组件使用

通过自定义标签 app-input 在 stencil 组件中使用:

import { Component, h, Host, State, Watch } from '@stencil/core'

@Component({
  tag: 'app-home',
  styleUrl: 'app-home.css',
  shadow: true,
})
export class AppHome {
  @State() input = 'hello world'

  onInput(e: CustomEvent<HTMLAppInputElement>) {
    this.input = e.detail as unknown as string
  }
  // NOTE 监听原生事件
  // 被组件内阻止事件冒泡后,监听不到
  onNativeChange(e: Event) {
    const inputEle = e.target as HTMLInputElement
    console.log('原生事件', inputEle.value)
  }

  onChange(e: CustomEvent<HTMLAppInputElement>) {
    console.log(e.detail)
  }

  @Watch('input')
  inputChanged(newValue: string, oldValue: string) {
    console.log(newValue, oldValue)
  }
  render() {
    return (
      <app-input
        value={this.input}
        onInput={e => this.onInput(e)}
        onInputChanged={this.onChange}
        onChange={this.onNativeChange}
      />
    )
  }
}

如何传递 slot

<Host>
  <slot name='prepend'></slot>
  <input value={this.value} onInput={e => this.onInput(e)} onChange={e => this.onChange(e)} />
  <slot name='append'>hello</slot>
</Host>

从父组件传递:

<app-input>
  {/* 不指定slot名字,无法处理 */}
  {/* <h1>header one</h1> */}
  <h2 slot='prepend'> append slot</h2>
  {/* <div slot='append'> */}
  <span slot='append'>append</span>
  <span slot='append'>append one</span>
  <span slot='append'>append another</span>
  {/* </div> */}
</app-input>

如何通过 slot 传递数据到父组件?

slot 的高级用法

如何暴露组件内部的方法供外部使用?

通过 @Method 暴露方法, ref 获取组件实例,调用组件方法。

  @Method() // @Method 装饰器要求方法返回Promise
  async getValue() {
    return this.value
  }

在父组件通过 ref 调用组件方法

@Component({
  tag: 'app-home',
  styleUrl: 'app-home.css',
  shadow: true,
})
export class AppHome {
  person: Person = { name: 'John', age: 23 }
  appInput!: HTMLAppInputElement
  componentWillLoad() {
    console.log('Component is about to be rendered', this.appInput)
  }
  componentDidLoad() {
    console.log(this.appInput) // 组件实例
    this.appInput.getValue().then(console.log)
    console.log(this.appInput.person) // 拿到自定义属性
    console.log(this.appInput.title) // 拿到原生属性
    // console.log(this.appInput?.onInput)// 拿不到没有暴露的方法
  }
  render() {
    return <app-input ref={refInput => (this.appInput = refInput)} person={this.person} title='input' />
  }
}

通过函数的方式绑定 ref 到组件上,组件挂载后,ref 是组件实例。

prop、method、原生属性是共有的,其他都是私有的。

@Element 装饰器

在组件内部获取自组件实例。

和在组件外部通过 ref 获取组件实例,值是同一个。

Host 组件

Host 组件一个内置组件,不会渲染到页面上。

常用的场景:

  • 在组件内部设置自定义标签的属性
import { Component, Host, h } from '@stencil/core'

@Component({ tag: 'todo-list' })
export class TodoList {
  @Prop() open = false
  render() {
    return (
      <Host
        aria-hidden={this.open ? 'false' : 'true'}
        class={{
          'todo-list': true,
          'is-open': this.open,
        }}
      />
    )
  }
}
  • 作为Fragment

样式

stencil 使用 Shadow DOM 要封装 DOM 和样式,内部样式不泄露到外部,外部样式不影响组件内部的 DOM。

@Component({
  shadow: true,// 启用 shadow DOM
})

不启用,和平时的 style 一样书写,就是全局样式。

启用后:

  • 样式隔离:内外部样式不相互影响。

  • 设置样式的选择改变

启用前,可通过自定义标签获取到 DOM

app-input {
}

启用后,自定义标签失效,使用 :host

:host {
}

启用后,对 :host 选择器有影响,目前还不清楚是什么影响。

都使用 :host

  • 获取 DOM 的方式改变
this.el.querySelector('div') // 启用前
this.el.shadowRoot.querySelector('div') // 启用后

如何从外部改变内部的样式?

CSS 变量往往设置成全局的,但是全局样式文件只能导入一个。

  • 全局样式

哪些情况该考虑使用全局样式:

  1. Theming: defining CSS variables used across the app

  2. 字体

  3. body background

  4. CSS resets

更多教程

函数组件

import { h } from '@stencil/core'
import './index.css'
export const Hello = props => <h1>Hello, {props.name}!</h1>

函数组件无法使用样式,且只能在 JSX 中使用,以大写字母开头,不能在 html 中使用,这个很坑爹。

函数组件还有以下限制:

  • 不会被编译成 web component,故无法在 html 中使用
  • 不能使用生命周期函数
  • 不会创建 DOM 节点
  • 无法使用 shadow DOM 和 scoped style,其实无法应用样式

这些特点,限制了函数组件的使用场景,除了 renderProp ,几乎无用。

renderProp 只能在 jsx 中使用。

How To Build Web Components Using Stencil JS

Creating Web Components with Stencil

你的前端框架要被 Web 组件取代了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值