组件树访问

通过组件树访问接口,可以直接获得另一个自定义组件实例 this。这样就可以自由地操作多个组件。

需要注意的是,滥用这个接口可能导致组件间过度耦合。对于更好的组件间通信方式,请参考 组件间交互概述 。

想要正确理解组件树的概念,请仔细阅读 组件框架模型 。

获取组件实例

两个有引用与被引用关系的组件,通常可以称为父组件和子组件。自定义组件可以获得引用它的自定义组件(父组件)实例、所有它引用的自定义组件(子组件)实例。

获取父组件实例

通过 this.selectOwnerComponent 可以获得父组件实例 this。对于页面根组件,将返回 null

export default Component({
  lifetimes: {
    attached() {
      const owner = this.selectOwnerComponent()
    },
  },
})

如果使用 Chaining 形式,可以结合 trait behaviors 来进行逻辑解耦。

// common.ts

export interface IFooTrait {
  bar(): string
}

export const fooTrait = Behavior.trait<IFooTrait>()
// 父组件可以实现 fooTrait
export default Component()
  .init(function ({ implement }) {
    implement(fooTrait, {
      bar() {
        return 'bar'
      },
    })
  })
  .register()
// 子组件使用 trait behaviors 接口来访问父组件
export default Component()
  .init(function ({ lifetime }) {
    lifetime('attached', () => {
      const owner = this.selectOwnerComponent()!
      const fooImpl = owner.traitBehavior(fooTrait)!
      fooImpl.bar() // 'bar'
    })
  })
  .register()

获取子组件实例

通过 this.selectComponent 可以获得自定义组件 Shadow 树中的任意自定义组件实例 this

调用时需要传入一个选择器 selector,如:this.selectComponent(".my-component")。选择器详细语法可查看 selector 语法参考文档。

另有一个类似的接口 this.selectAllComponents。他们的区别是,前者获取第一个匹配到选择器的节点,后者返回一个数组,包含所有匹配到的节点。

export default Component({
  lifetimes: {
    attached() {
      const comp = this.selectComponent(".my-component")
    },
  },
})

在上例中,将会获取到 Shadow 树中 classmy-component 的子组件实例 this

注意:默认情况下,微信小程序与插件之间、不同插件之间的组件将无法通过组件树访问接口得到组件实例(将返回 null)。如果想让一个组件在上述条件下依然能被 selectComponent 返回,可以自定义其返回结果(见下)。

自定义的组件实例获取结果

从基础库版本 2.2.3 开始提供支持。对于 exparser 组件框架,需要引入内置 behavior wx://component-export 这一功能才会生效。

selectOwnerComponent selectComponent selectAllComponents 返回的结果,可以通过 export 定义段来改变。

// 子组件 my-component 内部
Component({
  behaviors: ['wx://component-export'],
  export() {
    return { myField: 'myValue' }
  }
})
<!-- 使用自定义组件时 -->
<my-component id="the-id" />
// 父组件调用
const child = this.selectComponent('#the-id') // 等于 { myField: 'myValue' }

在上例中,父组件获取 idthe-id 的子组件实例时,得到的是对象 { myField: 'myValue' }

trait behaviors

如果一个组件需要暴露一些 JS 接口给另一些组件使用,可以用 methods、export 等方式,但它们都有些这样那样的问题。

现在最推荐的方式是使用 trait behaviors。它可以有效解耦逻辑,并具有很好的 TypeScript 支持。

目前,trait behaviors 只有 glass-easel 组件框架原生支持。想在 exparser 组件框架上使用,可以借助 chaining-api-polyfill 。但需要注意:chaining-api-polyfill 定义的 trait behaviors 只能在由它定义的组件中使用;反之,glass-easel 原生定义 trait behaviors 也只能在 glass-easel 原生定义的组件中使用。

trait behaviors 基本用法

设计理念上,trait behaviors 主要是配合 TypeScript 类型使用的。因此,以下例子都用 TypeScript 编写。

定义 trait behaviors

首先,使用 trait behaviors 定义一组接口。这组接口可以有个 TypeScript 声明。

// 定义一组接口
export interface IFooTrait {
  bar(): string
}

// 将 TypeScript 接口定义包装成 trait behavior
export const fooTrait = Behavior.trait<IFooTrait>()

实现 trait behaviors

提供接口实现的组件,就需要实现 trait behaviors。实现时,需要使用 Chaining 形式。

export default Component()
  .init(function ({ implement }) {
    // implement 应在 init 函数返回前调用
    // 不应该写在生命周期函数、事件回调函数、其他方法内部
    implement(fooTrait, {
      bar() {
        return 'bar'
      },
    })
  })
  .register()

调用 trait behaviors 实现

对于已经实现了 trait behaviors 的组件,通过这个组件的 this 可以访问到它的实现。

export default Component()
  .init(function ({ implement, lifetime }) {
    // 实现 trait behavior
    implement(fooTrait, {
      bar() {
        return 'bar'
      },
    })

    lifetime('attached', () => {
      // 获取组件实例 `this` 对 fooTrait 的实现(未实现的话,返回 undefined)
      const fooImpl = this.traitBehavior(fooTrait)!

      // 可以调用 trait behaviors 中的方法
      fooImpl.bar() // 'bar'
    })
  })
  .register()

实践中,更常见的情况是 trait behaviors 配合 组件间关系 或 组件树访问 来一同使用,请参考它们的文档。

trait behaviors 的扩展方法

trait behaviors 本身可以包含一些扩展方法。这些扩展方法可以基于已有方法扩展出一些新的接口,有时可以用来简化代码。

具体来说,Behavior.trait 可以提供一个转换函数,例如:

// 实现者需要实现的方法
interface IPlusTrait {
  plus(a: number, b: number): number
}

// 调用者可以调用的方法
interface IPlusMinusTrait extends IPlusTrait {
  minus(a: number, b: number): number
}

// Behavior.trait 可以传递一个转换函数,将实现者实现的 impl 转换为调用者可以获取的对象
export const plusMinusTrait = Behavior.trait<IPlusTrait, IPlusMinusTrait>((impl) => ({
  // 添加一些调用者可以调用的方法
  minus(a: number, b: number) {
    return impl.plus(a, -b)
  },
  ...impl
}))

转换函数也可以裁剪掉一些不暴露给调用者的接口(不完全暴露 ...impl)。

slot 模式

每个组件实例都拥有独立的 Shadow 树,代表组件自身模板中的节点结构。当组件的使用者在组件标签内放置子节点时,这些子节点会被视为 slot 内容,放置到 <slot> 的位置上。换句话说,<slot> 是一个占位符,代表 slot 内容所在的位置。

自定义组件使用 <slot> 时,slot 有三种工作模式可选。每个组件都可以任选其一。

  • 单一 slot 是默认情况,仅支持一个 <slot> 节点;
  • 多 slot 支持多个 <slot>,每个 slot 需要有一个名字 <slot name="xxx">
  • 动态 slot 支持在 wx:for 内部使用 <slot>,同时可以向 slot 内容节点树传递数据。

单一 slot

默认情况下激活的是单一 slot 模式。这种模式具有最优的性能。

在这个模式下,自定义组件 WXML 中只能包含一个 <slot> 节点。例如:

<!-- custom-component.wxml -->
<view>
  <slot />
</view>

使用这个自定义组件时,传入内容节点:

<custom-component>
  <view> content </view>
</custom-component>

<view> content </view> 最终会被放置到 <slot> 的位置上。

如果在自定义组件 WXML 内写了多个 <slot> 节点,只有第一个生效。

多 slot

在组件 options 中声明 multipleSlots: true 可激活多 slot 模式。

// Definition 形式的 custom-component.json
export default Component({
  options: {
    multipleSlots: true,
  },
})
// Chaining 形式的 custom-component.json
export default Component()
  .options({ multipleSlots: true })
  .register()

多 slot 模式允许自定义组件 WXML 中包含多个 <slot> 节点。每个 <slot> 都必须有名字 name,其值必须是一个唯一的字符串。例如:

<!-- custom-component.wxml -->
<view class="foo-wrapper">
  <slot name="foo" />
</view>
<view class="bar-wrapper">
  <slot name="bar" />
</view>

在使用这个自定义组件时,需要为每个 slot 内容节点指定它所在的 slot 名字,例如:

<custom-component>
  <!-- 这个 <view> 会放入 <slot name="foo"> 中 -->
  <view slot="foo"> foo content </view>

  <!-- 可以用 block 来避免额外生成一个 <view> -->
  <block slot="bar"> bar content </block>
</custom-component>

静态 slot 的 slot 内容生命周期

单一 slot 和多 slot 模式统一称为静态 slot 模式。

在静态 slot 下,即使自定义组件没有 <slot> 节点,slot 内容节点依然会被创建并触发 attached detached 等生命周期。例如,如果在自定义组件里面这么写:

<!-- custom-component.wxml -->
<view wx:if="{{ cond }}">
  <slot />
</view>

而在页面内这样写:

<custom-component>
  <view> content </view>
  <another-custom-component />
</custom-component>

如果 cond 为假,<slot> 节点就不存在,但是:

  • 即使 <slot> 节点不存在,<view> content </view><another-custom-component> 仍然会被创建,<another-custom-component>attached 生命周期依然会被触发;
  • 如果后续 cond 变成真,<slot> 节点会被创建,但是 <view> content </view><another-custom-component> 并不会被重新创建,<another-custom-component>attached 生命周期也不会再次触发。

如果想要 slot 内容与 <slot> 节点一起创建或销毁,可以使用动态 slot 模式。

动态 slot

动态 slot 模式允许自定义组件使用若干个 <slot> 节点。

在组件 options 中声明 dynamicSlots: true 可激活动态 slot 模式。

// Definition 形式的 custom-component.json
export default Component({
  options: {
    dynamicSlots: true,
  },
})
// Chaining 形式的 custom-component.json
export default Component()
  .options({ dynamicSlots: true })
  .register()

在使用动态 slot 时,每个 <slot> 拥有 slot 内容节点树的一份拷贝(这份拷贝会随着 <slot> 一同创建和销毁)。例如对于组件:

<!-- custom-component.wxml -->
<view class="foo">
  <slot />
</view>
<view class="bar">
  <slot />
</view>

在页面内这样写:

<custom-component>
  <view> content </view>
</custom-component>

将会最终出现两个 <view> content </view>,分别位于 <view class="foo"><view class="bar"> 内部。

同时,<slot> 节点可以传递数据字段,让 slot 内容节点树使用。这使得动态 slot 非常适合 <slot> 节点出现在 wx:for 列表内部的情况。例如对于组件:

<!-- custom-component.wxml -->
<view wx:for="{{ list }}">
  <slot list-index="{{ index }}" item="{{ item }}" />
</view>

通过 slot: 语法,在页面内可以接收到 list-indexitem

<custom-component>
  <view slot:item> {{ item }} </view>
</custom-component>

使用 slot: 接收数据时,可以用 = 来指定一个别名。此外,还可以使用 <block> 来避免生成额外的节点:

<custom-component>
  <block slot:listIndex="{{ i }}"> {{ i }} </block>
  <block slot:item> {{ item }} </block>
</custom-component>

动态 slot 模式同样支持类似多 slot 的 name 属性,不过一般并不常用。

组件间交互概述

不同的组件之间常常需要有逻辑上的交互。组件框架提供了多种组件间交互的方式。

父组件与子组件间的通信

两个有引用与被引用关系的组件,通常可以称为父组件和子组件。父组件通过 usingComponents 引用子组件,并在 WXML 中使用子组件的节点。

  • 如果父组件想要传递数据给子组件、触发子组件的特定逻辑,最佳的方式是通过 组件属性 来传递。
  • 如果子组件想要传递数据给父组件、触发父组件的特定逻辑,最佳的方式是通过 事件系统 来传递。
  • 如果想要在父组件和子组件之间同步数据,可以通过 双向绑定 来同步。

slot 也可以用于向 slot 内容节点树中传递数据,请参考 动态 slot 。

同一个 Shadow 树内节点之间的通信

在同一个 Shadow 树内,节点之间有时也需要有逻辑交互。

例如,想要自行实现 <my-form><my-button> 组件,它们是被页面这样使用的:

<!-- 页面的 WXML -->
<my-form>
  <my-button> Submit </my-button>
</my-form>

<my-button> 被点击时,需要触发在 Shadow 树上的 <my-form> 组件的提交行为。

这种情况下就需要使用 组件间关系 来实现。

直接组件实例访问

有时,上述方式都不够灵活、难以满足复杂的组件交互要求。此时可以使用 节点树访问 的方式,直接获得想要的组件实例 this

不过,这种方式如果不当使用,容易导致组件间过度耦合,因此不优先推荐。如果需要使用,建议配合 trait behaviors 来进行组件间逻辑解耦。