开发自定义组件

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

json

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

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

说明:

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

js

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

  1. Component({
  2. mixins:[{ didMount() {}, }],
  3. data: {y:2},
  4. props:{x:1},
  5. didUpdate(prevProps,prevData){},
  6. didUnmount(){},
  7. methods:{
  8. onMyClick(ev){
  9. my.alert({});
  10. this.props.onXX({ ...ev, e2:1});
  11. },
  12. },
  13. })

data

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

示例:

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

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

  1. // /pages/index/index.json
  2. {
  3. "usingComponents": {
  4. "my-component": "/components/counter/index"
  5. }
  6. }

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

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

页面输出:

  1. 0

methods

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

修改组件的axml:

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

在组件的js中处理事件:

  1. // /components/counter/index.js
  2. Component({
  3. data: { counter: 0 },
  4. methods: {
  5. plusOne(e) {
  6. console.log(e);
  7. this.setData({ counter: this.data.counter + 1 });
  8. },
  9. },
  10. });

注意:

  • Page不同,自定义组件需要将事件处理函数定义在 methods 中。
    现在页面会多渲染一个按钮,每次点击它都会将页面的数字加1。

props

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

  1. // /components/counter/index.js
  2. Component({
  3. data: { counter: 0 },
  4. props: {
  5. onCounterPlusOne: (data) => console.log(data),
  6. extra: 'default extra',
  7. },
  8. methods: {
  9. plusOne(e) {
  10. console.log(e);
  11. const counter = this.data.counter + 1;
  12. this.setData({ counter });
  13. this.props.onCounterPlusOne(counter); // axml中的事件只能由methods中的方法响应
  14. },
  15. },
  16. });

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

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

外部使用:不传递 props

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

页面输出:

  1. 0
  2. extra: default extra
  3. +1

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

外部使用:传递 props

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

页面输出:

  1. 0
  2. extra: external extra
  3. +1

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

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

组件生命周期

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

didMount

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

  1. Component({
  2. data: {},
  3. didMount() {
  4. let that = this;
  5. my.httpRequest({
  6. url: 'http://httpbin.org/post',
  7. success: function(res) {
  8. that.setData({name: 'xiaoming'})
  9. }
  10. });
  11. },
  12. });

didUpdate

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

  1. Component({
  2. data: {},
  3. didUpdate(prevProps,prevData) {
  4. console.log(prevProps, this.props, prevData, this.data)
  5. },
  6. });

注意:

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

didUnmount

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

  1. Component({
  2. data: {},
  3. didUnmount() {
  4. console.log(this)
  5. },
  6. });

mixins

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

  1. // /minxins/lifecylce.js
  2. export default {
  3. didMount(){},
  4. didUpdate(prevProps,prevData){},
  5. didUnmount(){},
  6. };
  1. // /pages/components/xx/index.js
  2. import lifecylce from '../../minxins/lifecylce';
  3. const initialState = {
  4. data: {
  5. y: 2
  6. },
  7. };
  8. const defaultProps = {
  9. props: {
  10. a: 3,
  11. },
  12. };
  13. const methods = {
  14. methods: {
  15. onTapHandler() {},
  16. },
  17. }
  18. Component({
  19. mixins: [
  20. lifecylce,
  21. initialState,
  22. defaultProps,
  23. methods
  24. ],
  25. data: {
  26. x: 1,
  27. },
  28. });

注意:

  • 每一个 mixin 只能包含 propsdatamethodsdidMountdidUpdatedidUnmount等属性。
  • 多个 mixin 中的属性 key 要确保不同,否则会报错。

其他组件实例属性

除了 datasetDataprops等属性外,组件实例上还有如下属性:

  • is: 组件路径
  • $page: 组件所属页面实例
  • $id: 组件 id,在 axml 中也可直接渲染
不要忘记,在组件中可以使用 my 调用 api。
  1. // /components/xx/index.js
  2. Component({
  3. didMount(){
  4. this.$page.xxCom = this; // 通过此操作可以将组件实例挂载到所属页面实例上
  5. console.log(this.is);
  6. console.log(this.$page);
  7. console.log(this.$id);
  8. }
  9. });
  1. <!-- /components/xx/index.axml 组件id可直接在组件axml中渲染 -->
  2. <view>{{$id}}</view>
  1. // /pages/index/index.json
  2. {
  3. "usingComponents": {
  4. "xx": "/components/xx/index"
  5. }
  6. }
  1. <!-- /pages/index/index.axml -->
  2. <xx />
  1. Page({
  2. onReady() {
  3. console.log(this.xxCom); // 可以访问当前页面所挂载的组件
  4. },
  5. })

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

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

axml

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

示例:

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

注意:

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

slot

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

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

为此,小程序提供了slot

default slot

示例代码:

  1. <!-- /components/xx/index.axml -->
  2. <view>
  3. <slot>
  4. <view>default slot & default value</view>
  5. </slot>
  6. <view>other</view>
  7. </view>

调用者不传递 axml

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

页面输出:

  1. default slot & default value
  2. other

调用者传递 axml

  1. <!-- /pages/index/index.axml -->
  2. <xx>
  3. <view>xx</view>
  4. <view>yy</view>
  5. </xx>

页面输出:

  1. xx
  2. yy
  3. other

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

named slot

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

  1. <!-- /components/xx/index.axml -->
  2. <view>
  3. <slot>
  4. <view>default slot & default value</view>
  5. </slot>
  6. <slot name="header"/>
  7. <view>body</view>
  8. <slot name="footer"/>
  9. </view>

只传递命名槽位

  1. <!-- /pages/index/index.axml -->
  2. <xx>
  3. <view slot="header">header</view>
  4. <view slot="footer">footer</view>
  5. </xx>

页面输出:

  1. default slot & default value
  2. header
  3. body
  4. footer

传递命名slot与默认slot

  1. <!-- /pages/index/index.axml -->
  2. <xx>
  3. <view>this is to default slot</view>
  4. <view slot="header">header</view>
  5. <view slot="footer">footer</view>
  6. </xx>

页面输出:

  1. this is to default slot
  2. header
  3. body
  4. footer

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

slot-scope

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

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

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

页面输出:

  1. component data: 1
  2. page data: 2

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

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

页面输出:

  1. page data: 2

我们有什么办法,让外部调用者传递的 axml 可以访问到组件内部的数据呢?答案是:slot scope示例:

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

页面输出:

  1. component data: 1
  2. page data: 2
  3. body

在外部调用者使用组件自定义标签的时候,使用slot-scope属性,slot-scope 的值将被用作一个临时变量名,此变量接收从自定义组件 axml 传递过来的 prop 对象。

acss

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

原文: https://docs.alipay.com/mini/framework/develop-custom-component