2、创建第一个组件:组件文件结构、Component构造器、properties与data的区别

好,咱们现在正式开始动手写组件。

说实话,很多新手学小程序组件化,第一个坎儿就是——不知道文件该怎么组织。我记得我刚接触那会儿,直接把页面里的代码复制到一个新文件夹里,结果跑都跑不起来。后来才明白,组件有自己的一套规矩。

2.1 组件文件结构:一个组件就是一个文件夹

在小程序里,一个组件通常由四个文件组成。我习惯把它们放在 components/ 目录下,每个组件一个独立文件夹。

components/
└── my-component/
    ├── my-component.json      // 组件配置文件
    ├── my-component.wxml      // 组件模板
    ├── my-component.wxss      // 组件样式
    └── my-component.js        // 组件逻辑

嗯,这里要注意:组件必须要有 .json 文件,而且里面要声明 "component": true。不然小程序会把它当成普通页面来处理。

核心区别:页面用 Page() 构造,组件用 Component() 构造。这是两条完全不同的路。

举个例子,my-component.json 长这样:

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

你想想看,如果不加 "component": true,小程序就会报错,说找不到这个组件。我在项目中遇到过好几次这种低级错误,排查了半天才发现是配置文件漏了。

2.2 Component 构造器:组件的核心入口

组件的逻辑代码写在 .js 文件里,用的是 Component() 构造器。它和 Page() 长得有点像,但内部结构完全不同。

Component({
  // 组件的属性列表
  properties: {
    title: {
      type: String,
      value: '默认标题'
    }
  },

  // 组件的内部数据
  data: {
    count: 0
  },

  // 组件的方法列表
  methods: {
    handleTap() {
      this.setData({
        count: this.data.count + 1
      })
    }
  }
})

说白了,Component() 就是一个对象,里面塞了三个核心部分:propertiesdatamethods。我刚开始写的时候,总把 methods 里的方法直接写在最外层,结果怎么都调不到——因为组件的方法必须放在 methods 里,这是规矩。

避坑指南:我曾经把页面里的 onLoadonShow 这些生命周期直接搬到组件里,结果发现根本不会触发。组件的生命周期是 createdattachedready 这些,别搞混了。

2.3 properties 与 data 的区别:一个对外,一个对内

这两个东西,初学者最容易搞混。我简单总结一下:

  • properties:对外暴露的属性,由父组件传入。说白了就是「别人给你的值」。
  • data:组件内部的状态,自己管自己。说白了就是「你自己的值」。

为什么会这样设计?你想想看,如果所有数据都写在 data 里,那父组件怎么控制子组件?反过来,如果所有数据都靠外部传入,那组件内部的状态又怎么维护?

咱们来看个实际例子:

// 父组件页面
<my-component title="我的标题" />

// 子组件 my-component.js
Component({
  properties: {
    title: {
      type: String,
      value: '默认标题'
    }
  },
  data: {
    internalCount: 0
  },
  methods: {
    increment() {
      this.setData({
        internalCount: this.data.internalCount + 1
      })
    }
  }
})

这里 title 是父组件传进来的,组件内部不能直接修改它。而 internalCount 是组件自己的状态,想怎么改就怎么改。

注意:properties 虽然可以修改,但我不建议你直接改。正确的做法是:如果父组件需要更新,让父组件自己改;如果组件内部需要响应变化,用 observers 监听器。

我整理了一个对比表格,方便你记忆:

对比项 properties data
数据来源 父组件传入 组件内部定义
修改权限 不建议直接修改 可以随意修改
用途 对外接口,接收参数 内部状态,管理逻辑
更新方式 父组件重新传入 this.setData()
典型场景 标题、颜色、开关状态 计数器、加载状态、临时数据

嗯,这里有个小技巧:properties 也可以设置默认值。如果父组件没传,就用默认值。这样组件即使单独使用也不会报错。

我个人习惯:把需要外部控制的配置项都放在 properties 里,把组件内部运算产生的临时数据放在 data 里。这样分工明确,代码也更容易维护。

2.4 实战:写一个最简单的组件

咱们把上面的知识点串起来,写一个完整的组件。假设我们要做一个「自定义按钮」:

// components/custom-button/custom-button.json
{
  "component": true,
  "usingComponents": {}
}

// components/custom-button/custom-button.wxml
<view class="btn" bindtap="onTap">
  <text>{{text}}</text>
  <text>点击次数:{{count}}</text>
</view>

// components/custom-button/custom-button.wxss
.btn {
  padding: 10px 20px;
  background: #07c160;
  color: white;
  border-radius: 8px;
  text-align: center;
}

// components/custom-button/custom-button.js
Component({
  properties: {
    text: {
      type: String,
      value: '按钮'
    }
  },
  data: {
    count: 0
  },
  methods: {
    onTap() {
      this.setData({
        count: this.data.count + 1
      })
    }
  }
})

然后在页面里引用它:

// 页面 .json 文件
{
  "usingComponents": {
    "custom-button": "/components/custom-button/custom-button"
  }
}

// 页面 .wxml 文件
<custom-button text="点我试试" />

你看,一个完整的组件就出来了。父组件传了个 text,组件内部自己维护了一个 count。点击按钮,count 增加,但 text 不会变——这就是 properties 和 data 的分工。

避坑指南:我曾经在组件里直接修改 properties 的值,结果父组件那边数据不同步,排查了半天才发现问题。记住:properties 是「只读」的,要改就让父组件改。

好了,第一个组件就讲到这里。下一节咱们聊聊组件之间的通信——父组件怎么传数据给子组件,子组件怎么通知父组件。嗯,这才是组件化真正好用的地方。