组件属性

组件可以定义一些属性,用来接收组件使用者传入的值。组件的使用者可以在 WXML 节点中指定这些属性对应的值。

定义属性

使用 Definition 形式定义属性,写法如下:

export default Component({
  properties: {
    fooProp: {
      type: String,
      value: '',
    },
  },
})

使用 Chaining 形式定义属性,写法如下:

export default Component()
  .property('fooProp', {
    type: String,
    value: '',
  })
  .register()

上面的例子中定义了 fooProp 属性,定义时提供的 type 表示属性类型,value 表示初始值。

定义好属性之后,组件的使用者就可以在 WXML 节点中指定属性对应的值了,比如在页面中可以这样写:

<!-- page/index/index.wxml -->
<custom-component foo-prop="bar" />

在 WXML 中,驼峰写法的属性名可以换成连字符写法(当然也可以继续使用驼峰写法)。

给组件属性命名时,需要注意以下几点:

  • 属性名不能和 data 数据字段重名;
  • id class style slot 是 WXML 的保留属性,虽然可以设为组件属性,但无法在 WXML 中对它们赋值;
  • dataXxx 这样的属性名也无法在 WXML 中对它们赋值,因为 data-xxx="" 会被视为 dataset。

属性类型与简化定义

属性支持下表中列出的类型。

类型 默认值 说明
String '' 字符串
Number 数值
Boolean false 布尔值
Object null 对象
Array [] 数组
Function function () {} 函数
null null 表示属性支持任意类型

Function 类型仅 glass-easel 组件框架支持。在 exparser 组件框架中不能传递函数类型的属性,但可以将函数作为 Object 类型中的一个字段来传递。

如果一个属性的初始值 value 和它的类型默认值相同,就可以把属性定义简写为 type 本身,例如:

export default Component({
  properties: {
    fooProp: String,
  },
})
export default Component()
  .property('fooProp', String)
  .register()

初始值和默认值

初始值 指的是一个属性没有被指定值时所具有的值。

默认值 指的是一个属性没有被指定值时所具有的值,或者当属性被赋予非法值时,用来代替非法值的那个属性值。仅 glass-easel 组件框架支持。默认值需要使用一个函数来指定,写法如下:

export default Component({
  properties: {
    fooProp: {
      type: String,
      default: () => 'DEFAULT',
    },
  },
})
export default Component()
  .property('fooProp', {
    type: String,
    value: () => 'DEFAULT',
  })
  .register()

如果指定了默认值 default,就不能再指定初始值 value 了。

属性值类型转换

在 WXML 中为一个属性指定值时,会自动进行类型转换;如果转换失败,就会被视为非法值(这时会使用默认值来代替)。typenull 的属性不会进行类型转换。

举个例子,如果 custom-component 组件的 num-prop 属性是 Number 类型的,而在使用者的 WXML 中这样写:

<custom-component num-prop="1" />

这种情况下,num 属性会接收到数值 1

布尔类型属性值转换

对于 Boolean 类型的属性,如果在 WXML 中只写了属性名而没有指定值,会自动把它的值设为 true

<custom-component bool-prop />
<!-- 等价于 -->
<custom-component bool-prop="ANY" />
<!-- 等价于 -->
<custom-component bool-prop="{{true}}" />

如果想把它的值设为 false,就必须使用数据绑定写法,例如:

<custom-component bool-prop="{{false}}" />

对象、数组类型属性值转换

对象和数组类型的属性只能使用数据绑定写法,例如:

<custom-component array-prop="{{ [1, 2] }}" object-prop="{{ { a: 1, b: 2 } }}" />

在 glass-easel 组件框架中,绑定对象时外层的花括号可以省略:

<custom-component object-prop="{{ a: 1, b: 2 }}" />

可选类型

如果一个属性可能接受多种类型的值,可以用 optionalTypes 为它附加多个类型。例如:

export default Component({
  properties: {
    fooProp: {
      type: String,
      optionalTypes: [String, Boolean],
    },
  },
})
export default Component()
  .property('fooProp', {
    type: Number,
    optionalTypes: [String, Boolean],
  })
  .register()

使用 TypeScript 指定对象类型

使用 TypeScript 时,对于 typeArrayObject 的属性,会根据它初始值、默认值的类型作为这个属性字段的具体类型,来进行类型推断。

这时,可以通过 as 来指定具体类型:

export default Component()
  .property('objectProp', {
    type: Object,
    value: {} as {
      foo?: number
      bar?: string
    },
  })
  .register()

属性值变化监听器

属性值可以指定一个 observer,当属性值发生变化时,会调用这个函数。

export default Component()
  .property('foo', {
    type: String,
    observer(newVal, oldVal) {
      // 属性值变化后触发
    },
  })
  .register()

如果需要在 observer 中同步执行 setData 等更新操作,更推荐使用数据监听器,因为通常它有更好的性能。

对于 type 为 ObjectArray 的属性,如果通过该组件自身的 this.setData 来改变属性值的一个子字段,则依旧会触发属性 observer,且 observer 接收到的 newVal 是变化的那个子字段的值。

属性值比较器

仅 glass-easel 组件框架支持。

每当组件的使用者进行更新时,组件框架需要判断它使用的每个子组件是不是被更新了。框架的判断方法是,对比每个属性值,如果属性值不一样,就认为子组件被更新了。如果子组件被更新了,就会产生一定的更新开销。

对于子组件属性是数值、字符串、布尔值等基本类型时,框架可以直接对比它们的值。但对于对象、数组这样的值,框架难以比较,常常将它们直接视为被更新了。

这时可以通过 comparer 来指定一个属性值比较函数,避免不必要的更新、提升整体性能。如果 comparer 返回真,表示属性值变化了、组件需要更新。例如:

export default Component()
  .property('objectProp', {
    type: Object,
    comparer(newValue, oldValue) {
      return newValue.foo !== oldValue.foo || newValue.bar !== oldValue.bar
    },
  })
  .register()

页面根组件的属性值

对于页面根组件,属性值会从页面的 query 参数中获取(参数值会被自动 URL decode)。例如:

// pages/index/index.js
export default Component()
  .property('strProp', String)
  .property('numProp', Number)
  .register()

如果页面以 pages/index/index?strProp=foo&numProp=1 打开,那么 strProp 会被赋值 'foo'numProp 会被赋值 1

由于 query 参数是字符串,页面根组件的属性值应当是 StringNumber 类型的。

引用组件

一个由 JSON 配置、WXML 模板、WXSS 样式、JavaScript 脚本组成的自定义组件编写完成后,它就可以被其他组件引用了。

基本引用方式

在引用组件时,需要在 JSON 配置的 usingComponents 中指定引用的标签名和它对应的组件路径。下面用一个具体的例子来说明组件的引用方法。

首先,我们先编写一个自定义组件,位于 components/foo。首先编写 components/foo.json

{
  "component": true,
  "usingComponents": {}
}

然后编写 components/foo.wxml

<view class="foo">
  foo
</view>

编写 components/foo.wxss

.foo {
  color: blue;
}

最后编写 components/foo.js(以 Chaining 形式为例):

export default Component()
  .register()

这样,它就可以在其他自定义组件(包括页面)内使用。如果想在 pages/index/index 中引用它,那就在 pages/index/index.json 中这样写:

{
  "usingComponents": {
    "foo": "/components/foo"
  }
}

其中,"foo" 表示引用节点名,只能包含字母、数字、连字符和下划线,不能以数字开头。"/components/foo" 表示引用组件的路径,可以是相对路径或绝对路径。

然后,可以像使用内置组件那样,在 WXML 里面使用 <foo> 标签:

<foo />

slot 节点

在自定义组件中,可以使用一个特殊的标签 <slot />,用于接收外部传入的节点。

例如,在 components/foo.wxml 中:

<view class="foo">
  <slot />
</view>

pages/index/index.wxml 中,可以为 <foo> 添加子节点:

<foo>
  <view> bar </view>
</foo>

这样,在 <view class="foo"> 中的 <slot /> 就会被替换成 <view> bar </view>。通过这个特性就可以将两个组件 WXML 结构相互穿插到一起。

slot 节点还有几种不同的模式,请参考 slot 模式 。

组件脚本

组件脚本是页面的逻辑核心。它可以通过改变页面数据来最终决定界面上展示的内容。

组件脚本可以用 JavaScript 编写。不过,对于复杂的组件,更推荐使用 TypeScript 编写:api-typings 库中包含重要的类型定义,可以用于规范代码编写。

无论创建多少个实例,组件脚本文件本身只会被执行一次。

在组件脚本中,必须使用 Page 构造器或 Component 构造器来定义组件(而且只能定义一次)。通常有以下几种定义方式,可任选其一:

  • Page 构造器适合用来构造非常简单的页面;
  • Component 构造器的 Definition 形式适合用来构造简单的自定义组件(包括页面);
  • Component 构造器的 Chaining 形式适合用来构造逻辑复杂的自定义组件(包括页面)、适合使用 TypeScript 编写。

在使用构造器时,推荐将它 export default 出来(尽管这不是必需的)。

Page 构造器

对于简单的页面,可以使用 Page 构造器进行构造。它的结构更简洁,但不能使用某些复杂特性。

export default Page({
  data: {
    foo: 'bar',
  },
  onLoad() {
    // 页面创建时执行
  },
  onButtonTap() {
    // 用户事件触发时执行
  },
})

Page 构造器只能使用数据、简单生命周期和事件响应函数。如果需要使用其他组件框架特性,就必须使用 Component 构造器。

Component 构造器的 Definition 形式

Component 构造器有两种使用形式:Definition 形式和 Chaining 形式。它们是可以混用的,实现时可以根据习惯任选其一。

export default Component({
  data: {
    foo: 'bar',
  },
  lifetimes: {
    attached() {
      // 页面创建时执行
    },
  },
  methods: {
    onButtonTap() {
      // 用户事件触发时执行
    },
  },
})

如果想要将一个已经用 Page 构造的页面改用 Component 构造,只需要将简单生命周期和事件响应函数移入 methods 中。

Component 构造器的 Chaining 形式

Chaining 形式仅 glass-easel 原生支持。对于 exparser,需要使用 chaining-api-polyfill 。

对于新代码,更推荐使用 Chaining 形式编写。因为 Chaining 形式更适合组织复杂的组件逻辑,对 TypeScript 的支持也更好。

export default Component()
  .data(() => ({
    foo: 'bar',
  }))
  .init(function ({ data, setData, lifetime, method }) {
    // 组件实例初始化时执行一次

    // 上方定义的 `data` 可以直接作为局部变量使用(不需要写成 `this.data`)
    // `setData` 亦然(不需要写成 `this.setData`)

    lifetime('attached', () => {
      // 页面创建时执行
      data.foo === 'bar' // true
    })

    const buttonTap = method(() => {
      // 用户事件触发时执行
      setData({ foo: 'new bar' })
    })

    return {
      buttonTap,
    }
  })
  .register()

Chaining 形式的实例局部变量

Chaining 形式的 .init(...) 是一个实例初始化函数,它对于每个实例执行一次。

在这个函数里,可以自由定义局部变量。这些变量在实例之间是相互独立的。这也就是 Chaining 形式最大的优势。

// 在 JS 文件中的全局量是所有实例共享的
const sharedVar = 'shared'

export default Component()
  .init(function () {
    // 在 init 中的变量是每个实例专属的
    const instanceVar = 'instance'
  })
  .register()

Chaining 形式的逻辑拆分与形式混用

Chaining 形式中,链式方法可以多次调用。可以借此将复杂的组件逻辑自然拆分为几个部分。

export default Component()
  // 组件逻辑单元 A
  .data(() => ({
    appleCount: 0,
    applePrice: 1.2,
  }))
  .init(function ({ data, setData, method }) {
    const buyApple = method(() => {
      setData({ appleCount: data.appleCount + 1 })
    })
    return { buyApple }
  })

  // 组件逻辑单元 B
  .data(() => ({
    bananaCount: 0,
    bananaPrice: 3.4,
  }))
  .init(function ({ data, setData, method }) {
    const buyBanana = method(() => {
      setData({ bananaCount: data.bananaCount + 1 })
    })
    return { buyBanana }
  })
  .register()

也可以将 Definition 形式混入到 Chaining 形式中。

export default Component()
  // 组件逻辑单元 A(使用 Definition 形式)
  .definition({
    data: {
      appleCount: 0,
      applePrice: 1.2,
    },
    methods: {
      buyApple() {
        this.setData({
          data: this.data.appleCount + 1,
        })
      },
    },
  })

  // 组件逻辑单元 B
  .data(() => ({
    bananaCount: 0,
    bananaPrice: 3.4,
  }))
  .init(function ({ data, setData, method }) {
    const buyBanana = method(() => {
      setData({ bananaCount: data.bananaCount + 1 })
    })
    return { buyBanana }
  })
  .register()

组件框架

组件化

微信小程序的界面代码组织以 组件 作为基本单位。

一个页面可以由很多组件组合而成。这些组件分为两类:

  • 基础组件 是不可再分解的基础功能单元,如 <view> <image>
  • 可以用若干组件拼成一个大的组件,这样的组件称为 自定义组件 。

换句话说,页面由基础组件和自定义组件组合而成,而其中的自定义组件又由基础组件和其他自定义组件组合而成。特别地:

  • 页面实际上就是一个很大的自定义组件,也称为 页面根组件,所以页面具备所有自定义组件具有的特性;
  • 自定义组件可以递归地引用自身(但无限递归会导致栈溢出,请控制好递归条件)。

自定义组件相关特性自微信小程序基础库版本 1.6.3 开始支持。

组件代码构成

每个自定义组件的代码由四部分构成:

  • JSON 配置;
  • WXML 模板
  • WXSS 样式
  • JavaScript(或 TypeScript)脚本

这四个文件必须放在同一文件路径下、仅有文件扩展名不同。

其中,JSON 配置中需要至少包含 usingComponentscomponent 其中一项。

对于页面根组件,通常可以这样写:

{
  "usingComponents": {}
}

对于非页面根组件的自定义组件,通常可以这样写:

{
  "component": true,
  "usingComponents": {}
}

usingComponents 表示这个组件依赖的其他组件,可参考 组件引用 文档。从另一个角度说,一个组件的 JSON 配置、WXML 模板、WXSS 样式、JavaScript 脚本写完后,它就可以被其他组件引用了。

component 字段并不是必需的。它只会影响一些组件框架细节表现;即使未写明,只要有 usingComponents 字段,它依然可以作为组件使用。不过,仍然建议为所有非页面根的组件添加 component 字段,作为标识。

在开发者工具中预览效果

组件框架选择

目前,有两个组件框架可选。

exparser 是传统的组件框架,对旧代码具有最佳的兼容性。但由于历史原因,它缺少部分新特性、性能也不是最优。

glass-easel 是新一代的组件框架,由 更多特性更优性能 。如果想要深入了解它本身,可以参考 glass-easel 开源项目 。

在使用传统的 WebView 渲染引擎时,每个页面都可以选择使用其中一个组件框架来渲染。默认情况下,使用 exparser 作为组件框架,如果想改用 glass-easel,需要在页面 JSON 配置中声明:

{
  "usingComponents": {},
  "componentFramework": "glass-easel",
  "glassEaselWebview": true
}

关于上述配置的详细说明,请参考 迁移到 glass-easel 文档。

如果使用 Skyline 渲染引擎 ,就只能选用 glass-easel 作为组件框架。

其中的 componentFramework 字段可以放在 app.json 中使它全局生效。

页面中的自定义组件跟随其所在页面选用的组件框架。

尽管这两个组件框架有极高的相似性,它们之间仍有一些细微差异。可参考 迁移到 glass-easel 来进行组件框架升级。