小程序组件化开发深度解析:自定义组件、抽象节点与插件系统
小程序的组件化体系不是Web组件标准的简单移植——它运行在双线程架构之上,拥有独立的作用域、样式隔离和插槽机制。本文从实际工程出发,完整剖析小程序自定义组件的设计模式、性能优化与高级应用。
一、背景与意义
为什么小程序需要组件化?
2017年微信小程序上线时,官方只提供了基础组件(view、text、image等),业务组件全部靠WXML的<include>和<import>在模板层面复用。这种模式的缺陷很明显:
- 无样式隔离:一个页面的CSS可能污染另一个页面的组件
- 无作用域隔离:data和函数在页面全局作用域下,命名冲突是必然的
- 无生命周期管理:组件不可感知自己的可见性变化
- 无法传递复杂slot:WXML的template只能做简单的参数替换
微信在2018年推出了自定义组件(Component构造器),从根本上解决了上述问题——每个组件拥有独立的作用域、样式、slot和生命周期。
组件化在小程序中的价值:
- 代码复用:一个
<price-tag>组件可以在10个页面复用 - 并行开发:每个组件可以由不同开发者独立完成
- 统一维护:UI更新只需修改组件定义
- 性能优化:组件级别的条件渲染、懒加载
二、概念与定义
2.1 组件化体系总览
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
小程序组件体系
├── 基础组件 (Native Components)
│ ├── view, text, image, scroll-view
│ ├── swiper, movable-view, cover-view
│ └── canvas, video, map, web-view
├── 自定义组件 (Custom Components)
│ ├── Component构造器
│ ├── 样式隔离 (styleIsolation)
│ ├── 插槽 (slot)
│ └── 抽象节点 (Component Generics)
├── 组件间通信
│ ├── 属性传递 (properties)
│ ├── 事件触发 (triggerEvent)
│ ├── relations (相邻组件关系)
│ └── 全局事件 (EventChannel)
└── 第三方组件
└── npm包引入 / 小程序插件提供
2.2 自定义组件与页面的核心区别
| 维度 | Page | Component |
|---|---|---|
| 构造器 | Page() | Component() |
| 数据传递 | 无标准方式 | properties(父→子) |
| 事件通知 | 函数直接调用 | triggerEvent(子→父) |
| 样式隔离 | 无(全局样式影响) | 默认隔离(apply-shared) |
| 生命周期 | 5个页面级 | 6个组件级 + 3个页面关联 |
| 多实例 | 不支持 | 支持(同一组件复用) |
| npm引入 | 不支持 | 支持 |
三、最小示例
3.1 创建一个简单的自定义组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- components/star-rating/star-rating.wxml -->
<view class="star-rating {{customClass}}">
<view
class="star-wrapper"
wx:for="{{stars}}"
wx:for-item="filled"
wx:key="index"
data-index="{{index}}"
bind:tap="onStarTap"
>
<image
class="star-icon"
src="{{filled ? filledSrc : emptySrc}}"
mode="aspectFit"
/>
</view>
<text class="rating-text" wx:if="{{showText}}">
{{currentValue}}/{{max}}
</text>
</view>
/* components/star-rating/star-rating.wxss */
.star-rating {
display: flex;
align-items: center;
gap: 4rpx;
}
.star-wrapper {
display: inline-flex;
padding: 4rpx;
}
.star-icon {
width: 48rpx;
height: 48rpx;
transition: transform 0.2s ease;
}
.star-icon:active {
transform: scale(1.2);
}
.rating-text {
font-size: 24rpx;
color: #999;
margin-left: 8rpx;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// components/star-rating/star-rating.js
Component({
// 组件配置
options: {
multipleSlots: true, // 启用多插槽
styleIsolation: 'isolated', // 样式隔离
},
// 外部传入的属性
properties: {
value: {
type: Number,
value: 0,
observer: 'onValueChange', // 属性变化监听
},
max: {
type: Number,
value: 5,
},
size: {
type: String,
value: 'medium', // small / medium / large
},
showText: {
type: Boolean,
value: false,
},
readonly: {
type: Boolean,
value: false,
},
customClass: {
type: String,
value: '',
},
},
// 组件内部数据
data: {
currentValue: 0,
stars: [false, false, false, false, false],
},
// 属性监听器
observers: {
'value, max': function(newValue, newMax) {
this.updateStars(newValue, newMax);
},
},
// 组件生命周期
lifetimes: {
attached() {
// 初始化星星状态
this.updateStars(this.data.value, this.data.max);
},
},
methods: {
// 更新星星显示
updateStars(value, max) {
const stars = Array.from({ length: max }, (_, i) => i < value);
this.setData({
currentValue: value,
stars: stars,
});
},
// 点击星星
onStarTap(event) {
if (this.data.readonly) return;
const index = event.currentTarget.dataset.index;
const newValue = index + 1;
// 触发自定义事件——通知父组件
this.triggerEvent('change', {
value: newValue,
oldValue: this.data.currentValue,
});
// 触觉反馈
wx.vibrateShort({ type: 'light' });
},
// 属性变化回调
onValueChange(newVal) {
console.log('[StarRating] Value changed:', newVal);
},
// 对外接口——重置评分
reset() {
this.setData({ currentValue: 0 });
this.updateStars(0, this.data.max);
},
},
});
// 组件使用的JSON配置
// components/star-rating/star-rating.json
// {
// "component": true,
// "usingComponents": {},
// "styleIsolation": "isolated"
// }
1
2
3
4
5
6
7
8
9
<!-- 在页面中使用 -->
<star-rating
value="{{product.rating}}"
max="5"
showText="{{true}}"
size="large"
bind:change="onRatingChange"
class="product-rating"
/>
3.2 组件间的属性传递与事件通信
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 父子组件通信全链路示例
// ===== 父组件(页面或父组件) =====
// pages/product/product.wxml
<view class="product-page">
<custom-header title="商品详情" bind:back="onBack" />
<custom-product-gallery
images="{{product.images}}"
current-index="{{currentImageIndex}}"
bind:imageChange="onGalleryImageChange"
/>
<custom-price-tag
price="{{product.price}}"
original-price="{{product.originalPrice}}"
bind:addToCart="onAddToCart"
bind:buyNow="onBuyNow"
>
<!-- 具名插槽 -->
<view slot="badge" class="hot-badge">热卖</view>
</custom-price-tag>
</view>
// pages/product/product.js
Page({
onAddToCart(event) {
const { skuId, quantity, price } = event.detail;
// 事件对象.detail 就是triggerEvent传递的数据
console.log('添加到购物车:', skuId, quantity, price);
this.addToCart(skuId, quantity);
},
});
// ===== 子组件(custom-price-tag) =====
// 内部使用triggerEvent通知父组件
Component({
properties: { price: Number, originalPrice: Number },
methods: {
handleAddToCart() {
this.triggerEvent('addToCart', {
skuId: this.data.selectedSkuId,
quantity: this.data.quantity,
price: this.data.price,
}, {
bubbles: true, // 允许事件冒泡
composed: true, // 允许穿越组件边界
capturePhase: false, // 不在捕获阶段触发
});
},
},
});
四、核心知识点拆解
4.1 样式隔离机制
小程序自定义组件提供了三种样式隔离选项:
1
2
3
4
5
6
7
8
Component({
options: {
// 'isolated'(默认):组件内外样式完全隔离
// 'apply-shared':页面样式会影响组件,但组件样式不影响页面
// 'shared':组件样式和页面样式互相影响
styleIsolation: 'isolated',
},
});
样式隔离的实现原理:
1
2
3
4
5
6
7
8
9
10
11
12
/* 隔离模式下,小程序会对组件的CSS选择器自动添加作用域前缀 */
/* 开发者的CSS */
.star-rating { display: flex; }
/* 编译后的CSS(自动添加组件ID前缀) */
.c-GENERATED_ID .star-rating { display: flex; }
/* 跨组件样式的选择器不会被正确处理 */
/*
❌ 页面选择器 .page-class .star-rating 不会影响隔离组件内的.star-rating
✅ 使用 externalClasses 让父组件影响子组件样式
*/
externalClasses——受控的外部样式:
1
2
3
4
5
// price-tag.js
Component({
externalClasses: ['price-class', 'original-class'],
// 允许父组件通过自定义属性传递样式类
});
1
2
3
4
5
<!-- 父组件中 -->
<price-tag
price-class="my-price"
original-class="strikethrough"
/>
1
2
3
/* 父组件的样式 */
.my-price { color: #ff4500; font-size: 32rpx; font-weight: bold; }
.strikethrough { text-decoration: line-through; color: #999; }
4.2 插槽(Slot)与多插槽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 组件定义:card.wxml -->
<view class="card {{customClass}}">
<!-- 默认插槽 -->
<view class="card-header">
<slot name="header">
<!-- 默认内容(无插槽时显示) -->
<text class="default-title">{{title}}</text>
</slot>
</view>
<!-- 核心内容插槽 -->
<view class="card-body">
<slot />
</view>
<!-- 底部插槽 -->
<view class="card-footer" wx:if="{{showFooter}}">
<slot name="footer" />
</view>
</view>
1
2
3
4
5
6
7
8
// card.js
Component({
options: { multipleSlots: true }, // 启用多插槽
properties: {
title: { type: String, value: '卡片标题' },
showFooter: { type: Boolean, value: false },
},
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 使用时的插槽填充 -->
<custom-card title="商品推荐" showFooter="{{true}}">
<!-- 具名插槽:header -->
<view slot="header" class="custom-header">
<image src="/images/icon.png" class="header-icon" />
<text>今日精选</text>
</view>
<!-- 默认插槽:body -->
<view class="product-list">
<view wx:for="{{products}}" wx:key="id">{{item.name}}</view>
</view>
<!-- 具名插槽:footer -->
<view slot="footer" class="custom-footer">
<button size="mini" bind:tap="viewMore">查看更多</button>
</view>
</custom-card>
4.3 抽象节点(Component Generics)
抽象节点允许组件定义中的某个节点在使用时再指定具体实现:
1
2
3
4
5
6
7
8
9
10
11
// 定义:grid-container.js
// 这个组件定义了一个网格布局
Component({
// 声明需要一个可替换的子组件
abstractNodes: {
gridItem: {
type: 'component', // 抽象节点类型
default: 'default-grid-item', // 默认实现
},
},
});
1
2
3
4
5
6
7
<!-- grid-container.wxml -->
<view class="grid">
<block wx:for="{{items}}" wx:key="id">
<!-- 使用抽象节点 -->
<grid-item item="{{item}}" index="{{index}}" />
</block>
</view>
1
2
3
4
5
6
7
8
// grid-container.json
{
"component": true,
"usingComponents": {},
"componentGenerics": {
"grid-item": true
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 使用方:页面中 -->
<!-- pages/home/home.json -->
{
"usingComponents": {
"grid-container": "/components/grid-container/grid-container",
"product-grid-item": "/components/product-grid-item/product-grid-item",
"category-grid-item": "/components/category-grid-item/category-grid-item"
}
}
<!-- pages/home/home.wxml -->
<!-- 运行时指定抽象节点的具体实现 -->
<grid-container
generic:grid-item="product-grid-item"
items="{{products}}"
/>
<grid-container
generic:grid-item="category-grid-item"
items="{{categories}}"
/>
抽象节点的价值:
- 组件结构复用(网格、列表、轮播等布局)
- 内容渲染解耦(每个格子渲染什么由父组件决定)
- 减少重复的WXML模板
4.4 组件关系(relations)
relations用于管理定义了关联的组件之间的关系,常用于”父子”或”兄弟”组件之间的通信:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 父组件:form-group.js
Component({
relations: {
'../form-control/form-control': {
type: 'child', // 关联类型
linked(target) {
// 子组件被挂载时触发
console.log('子组件加入:', target);
this._controls.push(target);
},
unlinked(target) {
// 子组件被移除时触发
this._controls = this._controls.filter(c => c !== target);
},
linkChanged(target) {
// 子组件关联变化时
},
},
},
lifetimes: {
attached() {
this._controls = [];
},
},
methods: {
// 获取所有子控件值
getValues() {
const values = {};
this._controls.forEach(control => {
values[control.data.name] = control.data.value;
});
return values;
},
// 验证所有子控件
validate() {
return this._controls.every(control => control.validate?.());
},
},
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 子组件:form-control.js
Component({
relations: {
'../form-group/form-group': {
type: 'parent',
linked(parent) {
console.log('已注册到父组件:', parent);
},
},
},
methods: {
validate() {
// 验证逻辑
return !!this.data.value;
},
// 通过relations获取父组件
getParentGroup() {
const parent = this.getRelationNodes('../form-group/form-group');
return parent[0];
},
},
});
五、实战案例:构建完整的商品卡片组件
5.1 组件设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!-- components/product-card/product-card.wxml -->
<view class="product-card {{customClass}}" style="{{cardStyle}}">
<!-- 图片区域 -->
<view class="image-wrapper" bind:tap="onCardTap">
<image
class="product-image"
src="{{imageSrc}}"
mode="widthFix"
lazy-load="{{true}}"
webp="{{true}}"
/>
<!-- 标签右上角 -->
<view class="tag-badge" wx:if="{{badgeText}}">
<text>{{badgeText}}</text>
</view>
</view>
<!-- 信息区域 -->
<view class="info-section">
<view class="title-row">
<text class="product-title" lines="{{2}}">{{title}}</text>
<image
class="favorite-icon"
src="{{isFavorited ? '/images/heart-filled.png' : '/images/heart-empty.png'}}"
bind:tap="onFavoriteTap"
data-stop-propagation="true"
/>
</view>
<view class="desc-row" wx:if="{{description}}">
<text class="product-desc">{{description}}</text>
</view>
<view class="price-row">
<text class="current-price">¥{{currentPrice}}</text>
<text class="original-price" wx:if="{{originalPrice > currentPrice}}">
¥{{originalPrice}}
</text>
<text class="sales-count">已售{{salesCount}}件</text>
</view>
<view class="action-row">
<!-- 默认插槽:可插入购物车按钮、立即购买等 -->
<slot name="actions" />
</view>
</view>
</view>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// components/product-card/product-card.js
Component({
options: {
multipleSlots: true,
styleIsolation: 'apply-shared',
},
properties: {
// 基础信息
productId: { type: String, required: true },
title: { type: String, value: '' },
description: { type: String, value: '' },
imageSrc: { type: String, value: '' },
// 价格
currentPrice: { type: Number, value: 0 },
originalPrice: { type: Number, value: 0 },
currency: { type: String, value: '¥' },
// 统计
salesCount: { type: Number, value: 0 },
rating: { type: Number, value: 0 },
// UI配置
badgeText: { type: String, value: '' },
cardStyle: { type: String, value: '' },
isFavorited: { type: Boolean, value: false },
layout: { type: String, value: 'vertical' }, // vertical / horizontal
},
data: {
imageLoaded: false,
imageError: false,
},
observers: {
'imageSrc': function(newSrc) {
if (newSrc) {
this.setData({ imageLoaded: false, imageError: false });
}
},
'layout': function(newLayout) {
this.setData({ isHorizontal: newLayout === 'horizontal' });
},
},
lifetimes: {
attached() {
// 从缓存恢复收藏状态
this.checkFavoriteStatus();
},
},
methods: {
// 点击卡片——跳转详情页
onCardTap() {
const url = `/pages/detail/detail?id=${this.data.productId}`;
// 支持用户拦截跳转
const canNavigate = this._emitHook('beforeNavigate', { productId: this.data.productId });
if (canNavigate !== false) {
wx.navigateTo({ url });
}
},
// 收藏/取消收藏
onFavoriteTap(event) {
if (event.currentTarget.dataset.stopPropagation) {
return;
}
const newStatus = !this.data.isFavorited;
this.triggerEvent('favoriteChange', {
productId: this.data.productId,
isFavorited: newStatus,
});
// 乐观更新UI
this.setData({ isFavorited: newStatus });
},
// 图片加载完成
onImageLoad() {
this.setData({ imageLoaded: true });
this.triggerEvent('imageLoaded', { productId: this.data.productId });
},
// 图片加载失败
onImageError() {
this.setData({ imageError: true });
this.triggerEvent('imageError', { productId: this.data.productId });
},
// 内部钩子机制
_emitHook(name, data) {
const event = new CustomEvent(name, { detail: data });
this.triggerEvent(name, data);
return true;
},
// 检查收藏状态
checkFavoriteStatus() {
const key = `_fav_${this.data.productId}`;
const cached = wx.getStorageSync(key);
if (cached !== undefined && cached !== '') {
this.setData({ isFavorited: Boolean(cached) });
}
},
},
// 对外暴露的方法
methods: {
// 刷新组件数据
refreshData(newData) {
this.setData(newData);
},
},
});
5.2 引入NPM组件(以Vant Weapp为例)
1
2
3
4
5
6
// package.json
{
"dependencies": {
"@vant/weapp": "^1.10.0"
}
}
1
2
3
4
5
6
7
8
// 在页面的json中使用
{
"usingComponents": {
"van-button": "@vant/weapp/button",
"van-dialog": "@vant/weapp/dialog",
"van-toast": "@vant/weapp/toast"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 页面中使用Vant组件 -->
<van-dialog
use-slot
title="确认购买"
show="{{showConfirmDialog}}"
bind:close="onDialogClose"
>
<view class="dialog-content">
<custom-product-card
product-id="{{product.id}}"
title="{{product.title}}"
current-price="{{product.price}}"
/>
</view>
<view slot="footer">
<van-button type="default" bind:tap="cancelPurchase">取消</van-button>
<van-button type="primary" bind:tap="confirmPurchase">确认支付</van-button>
</view>
</van-dialog>
六、底层原理
6.1 Exparser组件系统原理
小程序的组件渲染系统叫Exparser,它基于Web Components规范的自定义元素(Custom Elements)思路,但运行在双线程架构之上:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// Exparser的简化运行机制
class Exparser {
constructor() {
this.componentRegistry = new Map();
this.renderedInstances = new Map();
}
// 注册组件定义
registerComponent(config) {
this.componentRegistry.set(config.name, config);
}
// 创建组件实例(在渲染层WebView中)
createComponent(componentName, props) {
const config = this.componentRegistry.get(componentName);
if (!config) throw new Error(`Unknown component: ${componentName}`);
// 创建组件实例
const instance = {
id: generateId(),
name: componentName,
data: { ...config.data, ...props },
methods: config.methods,
children: [],
parent: null,
slots: {},
};
// 编译WXML模板,生成虚拟DOM
instance.virtualDOM = this.compileTemplate(config.template, instance);
// 挂载到组件树
this.mountComponent(instance);
return instance;
}
// 更新组件数据
updateComponent(instanceId, newData) {
const instance = this.renderedInstances.get(instanceId);
if (!instance) return;
// 合并数据
Object.assign(instance.data, newData);
// 脏检查:找出变化的部分
const changes = this.diff(instance.data, instance.previousData);
// 局部更新虚拟DOM
for (const [path, value] of changes) {
this.patchVirtualDOM(instance, path, value);
}
// 触发重绘
this.scheduleRender(instance);
}
}
6.2 组件实例的唯一性管理
小程序通过自动生成的组件ID保证组件实例的唯一性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 每个组件实例在渲染层和逻辑层有对应的ID
// 逻辑层:JSCore中的组件实例ID
// 渲染层:WebView中的组件实例ID
// 两者通过Native Bridge映射
// 逻辑层的setData实际包含组件ID信息:
// Native接收到的消息:
{
type: 'setData',
componentId: 'c_abc123', // 组件实例ID
data: { value: 5 }, // 增量数据
pageId: 'p_home', // 所属页面
}
// 渲染层根据componentId找到对应的组件实例
七、高频面试题解析
Q1: 小程序自定义组件和Vue/React组件的主要区别是什么?
A:1) 小程序组件运行在双线程架构上,setData是通信桥,不能直接操作DOM;2) 小程序组件有天然的样式隔离(isolated模式),无需CSS-in-JS或CSS Modules;3) 小程序组件的生命周期比Web组件更复杂(应用、页面、组件三层);4) 小程序的slot不支持作用域插槽(scoped slot);5) 小程序组件无法在JS中动态创建,必须先在模板中声明。
Q2: Component的properties支持哪些类型和观测模式?
A:properties支持8种类型:String, Number, Boolean, Object, Array, null(任意类型)。观测模式有三种:1) observer: 属性变化回调;2) 在observers中监听通配符(**);3) 监听逗号分隔的多个属性('prop1, prop2')。注意:observer的首次设置也会触发,如果需要”仅监听变化”可以用lifetimes的attached做标志位过滤。
Q3: Component中为什么不能通过this.data.xxx = value修改数据?
A:因为小程序的双线程架构要求:逻辑层数据变化必须通过Native Bridge同步到渲染层。直接修改this.data只改变了逻辑层的内存数据,不会触发渲染层的更新。必须使用this.setData()才能在两个线程间同步数据。这与Vue(通过Proxy自动追踪)和React(通过setState)的设计哲学不同,更接近手动模式。
Q4: 自定义组件如何实现v-model类似的双向绑定?
A:小程序没有直接的双向绑定语法,但可以通过属性+事件的模式实现:
1
2
3
4
5
6
7
8
9
10
Component({
properties: {
value: { type: String, value: '' },
},
methods: {
onInput(e) {
this.triggerEvent('input', { value: e.detail.value });
},
},
});
父组件监听input事件来同步数据。社区有model:前缀的简化写法,但本质还是属性+事件模式。
Q5: 组件过多时如何优化性能?
A:1) 使用wx:if而非hidden来完全移除不在视图内的组件;2) 使用lazy-load延迟加载图片内容组件;3) 避免深层嵌套的组件结构(建议不超过5层);4) 使用selectComponent获取单实例而非遍历所有;5) 属性传值避免传递完整的大对象,尽量使用路径key;6) 对固定不变的内容使用wx:key帮助Exparser做重排优化。
八、总结与扩展
小程序组件化开发走过了从”页面巨无霸”到”细粒度组件复用”的演进之路。核心收获:
- 组件是架构的最小单元——每个页面由多个组件组合而成
- 隔离带来信任——styleIsolation让组件可以安全地在任何页面使用
- 通信要规范——属性向下、事件向上、relations处理联动
- 性能看setData——组件的setData与页面的setData一样要谨慎
进阶方向:
- 纯函数组件(Functional Components):微信正在试验的无状态函数式组件
- 自定义组件npm包发布:将业务组件封装为npm包,多项目共享
- 可复用组件库搭建:企业内部组件库,使用lerna管理多包
- Taro/UniApp跨端组件:一套组件代码同时生成多个平台的组件代码
组件化的本质不是技术,而是思维——将复杂的UI拆分出独立的、可测试的、可组合的单元。当你能把一个页面拆成多个独立组件时,你就不再是用”页面”来思考,而是用”组件树”来思考了。