• 下载
  • 社区

开发自定义组件

一个自定义组件由 axmljsacssjson 组成。

json

开发者需要在 .json 文件中通过 "component": true 声明是一个自定义组件,如果这个自定义组件还依赖了其它组件,则还需要额外声明依赖哪些自定义组件。例如:

{
  "component": true, // 必选,自定义组件的值必须是true
  "usingComponents": {
    "c1":"../x/index"
  }
}

说明:

属性 类型 是否必填 说明
component Boolean 声明是自定义组件
usingComponents Object 声明依赖的自定义组件所在路径: 项目绝对路径以 / 开头,相对路径以 ./ 或者 ../ 开头,npm 路径不以 / 开头

js

开发者可在 .js 文件中调用 Component() 定义自定义组件。例如:

Component({
  mixins:[{ didMount() {}, }],
  data: {y:2},
  props:{x:1},
  didUpdate(prevProps,prevData){},
  didUnmount(){},
  methods:{
    onMyClick(ev){
      my.alert({});
      this.props.onXX({ ...ev, e2:1});
    },
  },
})

data

data 为组件局部状态,和 Page 一样,可以通过 this.setData 更改,并会触发组件重新渲染。
1.7.2 开始,也可以通过 this.$spliceData 做数据的更改,详见Page.prototype.$spliceData

示例:

// /components/counter/index.js
Component({
  data: { counter: 0 }
});
// /components/counter/index.axml
<view>{{counter}}</view>
// /components/counter/index.json
{
  "component": true,
}

以上代码分别实现了自定义组件的三个要素:js、axml、json。在页面上使用, 首先需要在页面的 json 文件中声明依赖的自定义组件,和自定义组件依赖其他组件声明方式相同。

// /pages/index/index.json
{
  "usingComponents": {
    "my-component": "/components/counter/index"
  }
}

声明后即可在页面的 axml 中使用。

// /pages/index/index.axml
<my-component />

页面输出:

0

methods

自定义组件不仅可以渲染静态数据,也可以响应用户点击事件,进而处理并触发自定义组件重新渲染。

修改组件 axml:

// /components/counter/index.axml
<view>{{counter}}</view>
<button onTap="plusOne">+1</button>

在组件 js 中处理事件:

// /components/counter/index.js
Component({
  data: { counter: 0 },
  methods: {
    plusOne(e) {
      console.log(e);
      this.setData({ counter: this.data.counter + 1 });
    },
  },
});

注意

  • 与 Page 不同,自定义组件需要将事件处理函数定义在 methods 中。

现在页面会多渲染一个按钮,每次点击它页面的数字都会加 1。

props

自定义组件与外界并不隔离。目前为止的示例还是一个独立模块,如果想让它与外界交互,自定义组件可以接受外界的输入,做完处理之后,还可以通知外界说:我做完了。这些都可以通过 props 实现。
示例:

// /components/counter/index.js
Component({
  data: { counter: 0 },
  props: {
    onCounterPlusOne: (data) => console.log(data),
    extra: 'default extra',
  },
  methods: {
    plusOne(e) {
      console.log(e);
      const counter = this.data.counter + 1;
      this.setData({ counter });
      this.props.onCounterPlusOne(counter); // axml中的事件只能由methods中的方法响应
    },
  },
});

以上代码为 props 设置默认属性,然后在事件处理函数中通过 this.props 获取这些属性。
注意:

  • props 为外部传过来的属性,可指定默认属性,不能在自定义组件内部代码中修改。
  • 自定义组件的 axml 中可以直接引用 props 属性。
  • 自定义组件的 axml 中的事件只能由自定义组件的 js 的 methods 中的方法来响应, 如果需要调用父组件传递过来的函数,可以在 methods 中通过 this.props 调用
// /components/counter/index.axml
<view>{{counter}}</view>
<view>extra: {{extra}}</view>
<button onTap="plusOne">+1</button>

外部使用:不传递 props

// /pages/index/index.axml
<my-component />

页面输出:

0
extra: default extra
+1

此时并未传递参数,所以页面会显示组件 js 中 props 设定的默认值。

外部使用:传递 props

// /pages/index/index.js
Page({
  onCounterPlusOne(data) {
    console.log(data);
  }
});
// /pages/index/index.axml
<my-component extra="external extra" onCounterPlusOne="onCounterPlusOne" />

页面输出:

0
extra: external extra
+1

此时传递了参数,所以页面会显示外部传递的 extra 值 external extra
注意

  • 外部使用自定义组件时,如果传递参数是函数,一定要on 为前缀,否则会将其处理为字符串。

组件生命周期

自定义组件通过传递 props 属性实现了与外部调用者交互。但有时自定义组件依赖外部数据,比如希望在自定义组件中向服务端发送请求获取数据;又或者希望在确保组件已经渲染到页面上之后,再做某些操作。为此自定义组件提供了三个生命周期函数: didMountdidUpdatedidUnmount

didMount

didMount 为自定义组件首次渲染完毕后的回调,此时页面已经渲染,通常在这时请求服务端数据。
示例代码:

Component({
  data: {},
  didMount() {
    let that = this;
    my.httpRequest({
      url: 'http://httpbin.org/post',
      success: function(res) {
		console.log(res);
        that.setData({name: 'xiaoming'});               
      }
    });
  },
});

didUpdate

didUpdate 为自定义组件数据更新后的回调,每次组件数据变更的时候都会调用。
示例代码:

Component({
  data: {},
  didUpdate(prevProps, prevData) {
    console.log(prevProps, this.props, prevData, this.data);
  },
});

注意

  • 组件内部调用 this.setData 会触发 didUpdate
  • 外部调用者调用 this.setData 也会触发 didUpdate

didUnmount

didUnmount 为自定义组件被卸载后的回调,每当组件实例从页面卸载的时候都会触发此回调。
示例代码:

Component({
  data: {},
  didUnmount() {
    console.log(this);
  },
});

mixins

开发者有时候可能会实现多个自定义组件,而这些自定义组件可能会有些公共逻辑要处理,小程序提供 mixins 用于解决这种情况。
示例代码:

// /minxins/lifecylce.js
export default {
  didMount(){},
  didUpdate(prevProps,prevData){},
  didUnmount(){},
};
// /pages/components/xx/index.js
import lifecylce from '../../minxins/lifecylce';

const initialState = {
  data: {
    y: 2
  },
};

const defaultProps = {
  props: {
    a: 3,
  },
};

const methods = {
  methods: {
  	onTapHandler() {},
  },
}

Component({
  mixins: [
    lifecylce,
    initialState,
	defaultProps,
	methods
  ],
  data: {
    x: 1,
  },
});

注意

  • 每一个 mixin 只能包含 props、data、methods、didMount、didUpdate、didUnmount 等属性。
  • 多个 mixin 项中的属性 key 要确保唯一性,否则会报错。

其他组件实例属性

除了 data、setData、props 等属性外,组件实例还有如下属性:

  • is: 组件路径
  • $page: 组件所属页面实例
  • $id: 组件 id,可直接在组件 axml 中渲染值

不要忘记,在组件中可以使用 my 调用 api。

// /components/xx/index.js
Component({
  didMount(){
    this.$page.xxCom = this; // 通过此操作可以将组件实例挂载到所属页面实例上
    console.log(this.is);
	console.log(this.$page);
	console.log(this.$id);
  }
});
<!-- /components/xx/index.axml 组件 id 可直接在组件 axml 中渲染值 -->
<view>{{$id}}</view>
// /pages/index/index.json
{
  "usingComponents": {
  	"xx": "/components/xx/index"
  }
}
<!-- /pages/index/index.axml -->
<xx />
Page({
  onReady() {
    console.log(this.xxCom); // 可以访问当前页面所挂载的组件
  },
})

当组件在页面上渲染后,执行 didMount 回调,控制台输出大概是这样的:

/components/xx/index
{$viewId: 51, route: "pages/index/index"}
1

axml

axml 是自定义组件必选部分。

示例:

<!-- /components/xx/index.axml -->
<view onTap="onMyClick" id="c-{{$id}}"/>
Component({
  methods: {
  	onMyClick(e) {
	  console.log(this.is, this.$id);
	},
  },
});

注意

  • 与页面不同,用户自定义事件需要放到 methods 里面。

slot(插槽)

通过在组件 js 中支持 props,自定义组件可以和外部调用者交互,接受外部调用者传来的数据,同时可以调用外部调用者传来的函数,通知外部调用者组件内部的变化。

但是这样还不够,我们的自定义组件还不够灵活,我们要的不仅仅是数据的处理与通知,我们希望自定义组件的 axml 结构可以使用外部调用者传来的 axml 组装。也就是说,我们想外部调用者可以传递 axml 给自定义组件,自定义组件使用其组装出最终的组件 axml 结构。

为此,小程序提供了 slot

default slot (默认插槽)

示例代码:

<!-- /components/xx/index.axml -->
<view>
  <slot>
    <view>default slot & default value</view>
  </slot>
  <view>other</view>
</view>

调用者不传递 axml

<!-- /pages/index/index.axml -->
<xx />

页面输出:

default slot & default value
other

调用者传递 axml

<!-- /pages/index/index.axml -->
<xx>
  <view>xx</view>
  <view>yy</view>
</xx>

页面输出:

xx
yy
other

可以将 slot 理解为插槽,default slot 就是默认插槽,如果调用者在组件标签 <xx> 之间不传递 axml,则渲染的是默认插槽。而如果调用者在组件标签 <xx> 之间传递有 axml,则使用其替代默认插槽,进而组装出最终的 axml 以供渲染。

named slot (具名插槽)

仅仅有 default slot 显然是不够灵活的,因为它只能传递一份 axml,而如果我们的组件比较复杂的话,我们希望可以在不同位置渲染不同的 axml,这就需要可以传递多个 axml。这就需要 named slot 了。
示例代码:

<!-- /components/xx/index.axml -->
<view>
  <slot>
    <view>default slot & default value</view>
  </slot>
  <slot name="header"/>
  <view>body</view>
  <slot name="footer"/>
</view>

只传递具名插槽

<!-- /pages/index/index.axml -->
<xx>
  <view slot="header">header</view>
  <view slot="footer">footer</view>
</xx>

页面输出:

default slot & default value
header
body
footer

传递具名插槽与默认插槽:

<!-- /pages/index/index.axml -->
<xx>
  <view>this is to default slot</view>
  <view slot="header">header</view>
  <view slot="footer">footer</view>
</xx>

页面输出:

this is to default slot
header
body
footer

named slot 就是具名插槽,外部调用者可以在自定义组件标签的子标签中指定要将哪一部分的 axml 放入到自定义组件的哪个具名插槽中。而自定义组件标签的子标签中没有指定具名插槽的部分则会放入到默认插槽上。如果仅仅传递了具名插槽,则默认插槽不会被覆盖。

slot-scope (作用域插槽)

到此我们的自定义组件已经比较灵活了,但是还不够灵活。通过使用 named slot,自定义组件的 axml 要么使用自定义组件自己的 axml,要么使用外部调用者(比如页面)的 axml。

使用自定义组件自己的 axml,可以访问到组件内部的数据,同时通过 props 属性,可以访问到外部调用者的数据。
示例:

// /components/xx/index.js
Component({
  data: {
    x: 1,
  },
  props: {
    y: '',
  },
});
<!-- /components/xx/index.axml -->
<view>component data: {{x}}</view>
<view>page data: {{y}}</view>
// /pages/index/index.js
Page({
  data: { y: 2 },
});
<!-- /pages/index/index.axml -->
<xx y="{{y}}" />

页面输出:

component data: 1
page data: 2

而自定义组件通过 slot 使用外部调用者(比如页面)的 axml 时,却只能访问到外部调用者的数据。

<!-- /components/xx/index.axml -->
<view>
  <slot>
    <view>default slot & default value</view>
  </slot>
  <view>body</view>
</view>
// /pages/index/index.js
Page({
  data: { y: 2 },
});
<!-- /pages/index/index.axml -->
<xx>
  <view>page data: {{y}}</view>
</xx>

页面输出:

page data: 2

我们有什么办法,让插槽内容可以访问到组件内部的数据呢?答案是 slot scope
示例:

// /components/xx/index.js
Component({
  data: {
    x: 1,
  },
});
<!-- /components/xx/index.axml -->
<view>
  <slot x="{{x}}">
    <view>default slot & default value</view>
  </slot>
  <view>body</view>
</view>
// /pages/index/index.js
Page({
  data: { y: 2 },
});
<!-- /pages/index/index.axml -->
<xx>
  <view slot-scope="props">
    <view>component data: {{props.x}}</view>
    <view>page data: {{y}}</view>
  </view>
</xx>

页面输出:

component data: 1
page data: 2
body

如上所示,自定义组件通过定义 slot 属性的方式暴露组件内部数据,页面使用组件时,通过 slot-scope 申明为作用域插槽,属性值定义临时变量名 props,即可访问到组件内部数据。

acss

和页面一样,自定义组件也可以定义自己的 acss 样式。acss 会自动被引入使用组件的页面,不需要页面手动引入。