Appearance
章节 2:组件与原生 API
学习目标
- 掌握基础组件与表单组件的使用方法
- 学会自定义组件开发与组件间通信
- 熟练使用网络请求 API 与本地存储
- 掌握媒体、定位、设备等原生 API
- 了解微信支付与分享能力的接入流程
2.1 基础组件与表单组件
2.1.1 视图容器组件
定义:小程序提供了一套丰富的内置组件,覆盖视图容器、表单、媒体、导航等常见 UI 需求。
xml
<!-- view——最基础的容器 -->
<view class="section">
<view class="section-title">基础容器</view>
</view>
<!-- scroll-view——可滚动区域 -->
<scroll-view scroll-y
bindscrolltolower="loadMore"
refresher-enabled
bindrefresherrefresh="onRefresh"
style="height: 400rpx;">
<view wx:for="{{ list }}" wx:key="id">{{ item.name }}</view>
</scroll-view>
<!-- swiper——轮播图 -->
<swiper indicator-dots="{{ true }}"
autoplay="{{ true }}"
interval="3000"
circular="{{ true }}"
indicator-color="rgba(255,255,255,0.6)"
indicator-active-color="#fff">
<swiper-item wx:for="{{ banners }}" wx:key="id">
<image src="{{ item.imageUrl }}" mode="aspectFill" />
</swiper-item>
</swiper>
<!-- movable-view——可拖动视图 -->
<movable-area style="width: 100%; height: 200px;">
<movable-view direction="all" style="width: 100rpx; height: 100rpx;">
拖动
</movable-view>
</movable-area>
<!-- cover-view——覆盖在原生组件上的视图 -->
<video src="..." style="width: 100%;">
<cover-view class="play-btn">▶</cover-view>
</video>2.1.2 表单组件
xml
<form bindsubmit="onSubmit">
<!-- input——输入框 -->
<view class="form-item">
<text>用户名</text>
<input name="username"
placeholder="请输入用户名"
maxlength="20"
type="text"
auto-focus="{{ false }}"
focus="{{ inputFocus }}" />
</view>
<view class="form-item">
<text>密码</text>
<input name="password"
type="password"
placeholder="请输入密码" />
</view>
<!-- textarea——多行输入 -->
<view class="form-item">
<text>备注</text>
<textarea name="remark"
placeholder="请输入备注"
maxlength="200"
show-count="{{ true }}"
auto-height="{{ true }}" />
</view>
<!-- picker——选择器 -->
<view class="form-item">
<text>城市</text>
<picker name="city"
mode="selector"
range="{{ cityList }}"
bindchange="onCityChange">
<view>{{ selectedCity || '请选择城市' }}</view>
</picker>
</view>
<view class="form-item">
<text>日期</text>
<picker name="date"
mode="date"
value="{{ date }}"
start="2024-01-01"
end="2026-12-31"
bindchange="onDateChange">
<view>{{ date || '请选择日期' }}</view>
</picker>
</view>
<!-- switch——开关 -->
<view class="form-item">
<text>接收通知</text>
<switch name="notify" checked="{{ true }}" color="#07c160" />
</view>
<!-- checkbox 和 radio -->
<view class="form-item">
<text>兴趣爱好</text>
<checkbox-group name="hobbies" bindchange="onHobbyChange">
<label wx:for="{{ hobbyList }}" wx:key="id">
<checkbox value="{{ item.value }}" />
<text>{{ item.name }}</text>
</label>
</checkbox-group>
</view>
<view class="form-item">
<text>性别</text>
<radio-group name="gender">
<label><radio value="male" />男</label>
<label><radio value="female" />女</label>
</radio-group>
</view>
<!-- slider——滑块 -->
<view class="form-item">
<text>价格区间</text>
<slider name="price" min="0" max="1000" step="10"
show-value="{{ true }}" bindchange="onPriceChange" />
</view>
<!-- button——按钮(支持开放能力) -->
<button form-type="submit" type="primary" loading="{{ submitting }}">
{{ submitting ? '提交中...' : '提交' }}
</button>
<button form-type="reset">重置</button>
</form>javascript
Page({
data: {
cityList: ["北京", "上海", "广州", "深圳", "杭州"],
selectedCity: "",
date: "2024-06-01",
hobbyList: [
{ id: "h1", value: "reading", name: "阅读" },
{ id: "h2", value: "sports", name: "运动" },
{ id: "h3", value: "music", name: "音乐" },
],
submitting: false,
},
onSubmit(e) {
console.log("表单数据:", e.detail.value);
},
onCityChange(e) {
const index = e.detail.value;
this.setData({ selectedCity: this.data.cityList[index] });
},
onDateChange(e) {
this.setData({ date: e.detail.value });
},
onHobbyChange(e) {
console.log("选中兴趣:", e.detail.value);
},
});2.1.3 导航与媒体组件
xml
<!-- navigator——页面导航 -->
<navigator url="/pages/detail/detail?id=1001" hover-class="navigator-hover">
跳转到详情页
</navigator>
<navigator url="/pages/index/index" open-type="switchTab">
切换到首页 Tab
</navigator>
<navigator url="https://www.example.com" open-type="webview">
打开网页
</navigator>
<!-- image——图片 -->
<image src="{{ product.image }}"
mode="widthFix"
lazy-load="{{ true }}"
show-menu-by-longpress="{{ true }}"
binderror="onImageError"
style="width: 100%;" />
<!-- video——视频 -->
<video src="https://example.com/video.mp4"
controls="{{ true }}"
autoplay="{{ false }}"
loop="{{ false }}"
muted="{{ false }}"
show-fullscreen-btn="{{ true }}"
enable-play-gesture="{{ true }}"
bindplay="onPlay"
bindpause="onPause"
binderror="onVideoError"
style="width: 100%; height: 400rpx;" />
<!-- audio——音频 -->
<audio src="{{ audioSrc }}"
poster="{{ audioPoster }}"
name="{{ audioName }}"
author="{{ audioAuthor }}"
controls="{{ true }}" />2.2 自定义组件开发
2.2.1 组件结构
定义:自定义组件允许开发者将复用的 UI 片段封装为独立模块,包含自己的 WXML、WXSS、JS 和 JSON 文件。
components/
├── product-card/
│ ├── product-card.wxml
│ ├── product-card.wxss
│ ├── product-card.js
│ └── product-card.json ← 必须声明 { "component": true }
└── rating/
├── rating.wxml
├── rating.wxss
├── rating.js
└── rating.json2.2.2 组件 JSON 配置
json
// components/product-card/product-card.json
{
"component": true,
"usingComponents": {}
}2.2.3 组件的 WXML 与 WXSS
xml
<!-- components/product-card/product-card.wxml -->
<view class="product-card" bindtap="onTap">
<image class="product-image" src="{{ image }}" mode="aspectFill" />
<view class="product-info">
<text class="product-name">{{ name }}</text>
<text class="product-desc">{{ description }}</text>
<view class="product-footer">
<text class="product-price">¥{{ price }}</text>
<text class="product-sales">已售 {{ sales }} 件</text>
</view>
<!-- 插槽(slot) -->
<slot name="footer"></slot>
</view>
</view>css
/* components/product-card/product-card.wxss */
.product-card {
display: flex;
padding: 20rpx;
background: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
}
.product-image {
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.product-name {
font-size: 28rpx;
font-weight: 600;
color: #1a1a1a;
/* 多行省略 */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.product-price {
font-size: 32rpx;
color: #ff4d4f;
font-weight: bold;
}
.product-sales {
font-size: 22rpx;
color: #999;
}2.2.4 组件的 JS 逻辑
javascript
// components/product-card/product-card.js
Component({
// 组件属性(外部传入)
properties: {
image: { type: String, value: "" },
name: { type: String, value: "" },
description: { type: String, value: "" },
price: { type: Number, value: 0 },
sales: { type: Number, value: 0 },
productId: { type: String, value: "" },
},
// 组件内部数据
data: {
formattedPrice: "",
},
// 监听属性变化
observers: {
price(newVal) {
this.setData({
formattedPrice: newVal.toFixed(2),
});
},
},
// 组件方法
methods: {
onTap() {
// 触发自定义事件,通知父页面
this.triggerEvent("producttap", {
id: this.properties.productId,
name: this.properties.name,
});
},
},
// 生命周期
lifetimes: {
created() {
console.log("组件创建");
},
attached() {
console.log("组件挂载到页面");
},
ready() {
console.log("组件渲染完成");
},
detached() {
console.log("组件卸载");
},
},
});2.2.5 使用组件
json
// pages/index/index.json —— 引用组件
{
"usingComponents": {
"product-card": "/components/product-card/product-card",
"rating": "/components/rating/rating"
}
}xml
<!-- pages/index/index.wxml —— 使用组件 -->
<product-card wx:for="{{ productList }}" wx:key="id"
image="{{ item.image }}"
name="{{ item.name }}"
price="{{ item.price }}"
productId="{{ item.id }}"
bind:producttap="onProductTap">
<!-- 插槽内容 -->
<view slot="footer">
<button size="mini" bindtap="onAddCart">加入购物车</button>
</view>
</product-card>javascript
Page({
onProductTap(e) {
const { id, name } = e.detail;
wx.navigateTo({ url: `/pages/detail/detail?id=${id}` });
},
});2.2.6 组件通信方式总结
| 方式 | 方向 | 说明 |
|---|---|---|
| properties | 父→子 | 父页面传入数据到组件 |
| 自定义事件 | 子→父 | triggerEvent 触发,父页面监听 |
selectComponent | 父→子 | 父页面获取组件实例调用方法 |
| 全局数据 | 双向 | 通过 App.globalData 或 store |
javascript
// 父页面主动调用组件方法(selectComponent)
Page({
onLoad() {
const card = this.selectComponent("#product-card-1");
card.setData({ name: "修改后的名称" });
card.someMethod(); // 调用组件内部方法
},
});2.3 网络请求与本地存储
2.3.1 wx.request 网络请求
定义:wx.request 是小程序发起 HTTP/HTTPS 请求的 API,需在 app.json 中配置 request 合法域名。
javascript
// app.json —— 配置请求域名
{
"requiredPrivateInfos": ["chooseLocation"],
"permission": {},
"networkTimeout": {
"request": 10000,
"connectSocket": 10000,
"uploadFile": 60000,
"downloadFile": 60000
},
"requiredBackgroundModes": []
}javascript
// 基础 GET 请求
wx.request({
url: "https://api.example.com/products",
method: "GET",
data: { page: 1, size: 20 },
header: {
"Content-Type": "application/json",
"Authorization": "Bearer " + getApp().globalData.token,
},
timeout: 10000,
success(res) {
console.log("请求成功:", res.data);
},
fail(err) {
console.error("请求失败:", err);
wx.showToast({ title: "网络错误", icon: "error" });
},
complete() {
wx.hideLoading(); // 无论成功失败都执行
},
});
// POST 请求
wx.request({
url: "https://api.example.com/login",
method: "POST",
data: { username: "admin", password: "123456" },
success(res) {
if (res.statusCode === 200) {
wx.setStorageSync("token", res.data.token);
}
},
});2.3.2 封装请求工具
javascript
// utils/request.js
const BASE_URL = "https://api.example.com";
function request(options) {
const token = wx.getStorageSync("token");
return new Promise((resolve, reject) => {
wx.request({
url: BASE_URL + options.url,
method: options.method || "GET",
data: options.data || {},
header: {
"Content-Type": "application/json",
Authorization: token ? `Bearer ${token}` : "",
...options.header,
},
timeout: options.timeout || 10000,
success(res) {
if (res.statusCode === 401) {
// Token 过期——跳转登录
wx.redirectTo({ url: "/pages/login/login" });
reject(new Error("未授权"));
} else if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data);
} else {
reject(new Error(`请求失败: ${res.statusCode}`));
}
},
fail(err) {
reject(err);
},
});
});
}
// 导出便捷方法
module.exports = {
get(url, data, options) {
return request({ ...options, url, method: "GET", data });
},
post(url, data, options) {
return request({ ...options, url, method: "POST", data });
},
put(url, data, options) {
return request({ ...options, url, method: "PUT", data });
},
del(url, data, options) {
return request({ ...options, url, method: "DELETE", data });
},
};2.3.3 本地存储
javascript
// ===== 同步 API(推荐)=====
// 存储
wx.setStorageSync("userInfo", { name: "张三", age: 25 });
wx.setStorageSync("token", "eyJhbGciOiJIUzI1NiIs...");
// 读取
const userInfo = wx.getStorageSync("userInfo");
const token = wx.getStorageSync("token");
// 删除
wx.removeStorageSync("token");
// 清空所有
wx.clearStorageSync();
// ===== 异步 API =====
wx.setStorage({
key: "cart",
data: [{ id: 1, count: 2 }],
success() {
console.log("存储成功");
},
});
wx.getStorage({
key: "cart",
success(res) {
console.log("购物车数据:", res.data);
},
});
// ===== 存储限制 =====
// 单个 key 上限:1MB
// 总存储上限:10MB(同一个小程序)
// 存储类型:支持 Object、Array、String、Number 等2.4 媒体、定位与设备 API
2.4.1 媒体 API
javascript
// ===== 图片 =====
// 选择图片
wx.chooseImage({
count: 9, // 最多 9 张
sizeType: ["original", "compressed"],
sourceType: ["album", "camera"],
success(res) {
console.log("选择的图片:", res.tempFilePaths);
this.setData({ images: res.tempFilePaths });
},
});
// 预览图片
wx.previewImage({
current: "https://example.com/1.jpg", // 当前显示的图片
urls: ["https://example.com/1.jpg", "https://example.com/2.jpg"],
});
// 保存图片到相册
wx.saveImageToPhotosAlbum({
filePath: "https://example.com/image.jpg",
success() {
wx.showToast({ title: "保存成功" });
},
});
// ===== 拍照 =====
// 使用 camera 组件
// <camera device-position="back" flash="auto" binderror="error" style="width: 100%; height: 300px;"></camera>
// 在相机组件上覆盖 <cover-view> 实现自定义 UI
// ===== 录音 =====
const recorderManager = wx.getRecorderManager();
recorderManager.onStart(() => console.log("录音开始"));
recorderManager.onStop((res) => console.log("录音文件:", res.tempFilePath));
// 开始录音
recorderManager.start({
duration: 60000,
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 48000,
format: "mp3",
});
// ===== 视频 =====
wx.chooseVideo({
sourceType: ["album", "camera"],
maxDuration: 60,
camera: "back",
success(res) {
console.log("视频路径:", res.tempFilePath);
},
});2.4.2 位置与地图 API
javascript
// 获取当前位置
wx.getLocation({
type: "gcj02", // 国测局坐标
success(res) {
console.log("纬度:", res.latitude, "经度:", res.longitude);
this.setData({
latitude: res.latitude,
longitude: res.longitude,
});
},
fail(err) {
console.error("获取位置失败:", err);
},
});
// 选择位置
wx.chooseLocation({
success(res) {
console.log("位置名称:", res.name);
console.log("详细地址:", res.address);
console.log("坐标:", res.latitude, res.longitude);
},
});
// 打开地图(导航)
wx.openLocation({
latitude: 39.9042,
longitude: 116.4074,
name: "北京市中心",
address: "北京市东城区",
scale: 15,
});
// ===== 使用 map 组件 =====
// <map
// latitude="{{ latitude }}"
// longitude="{{ longitude }}"
// markers="{{ markers }}"
// scale="14"
// show-location="{{ true }}"
// style="width: 100%; height: 500rpx;" />2.4.3 设备 API
javascript
// 获取系统信息
const systemInfo = wx.getSystemInfoSync();
console.log({
brand: systemInfo.brand, // 手机品牌
model: systemInfo.model, // 手机型号
pixelRatio: systemInfo.pixelRatio, // 像素比
screenWidth: systemInfo.screenWidth,
screenHeight: systemInfo.screenHeight,
windowWidth: systemInfo.windowWidth,
windowHeight: systemInfo.windowHeight,
statusBarHeight: systemInfo.statusBarHeight,
language: systemInfo.language,
version: systemInfo.version, // 微信版本
platform: systemInfo.platform, // ios / android / devtools
SDKVersion: systemInfo.SDKVersion, // 基础库版本
});
// 获取网络状态
wx.getNetworkType({
success(res) {
console.log("网络类型:", res.networkType);
// wifi / 4g / 3g / 2g / unknown / none
},
});
// 监听网络变化
wx.onNetworkStatusChange((res) => {
console.log("网络是否连接:", res.isConnected);
console.log("网络类型:", res.networkType);
});
// 获取设备电量
wx.getBatteryInfo({
success(res) {
console.log("电量:", res.level, "%");
console.log("是否充电:", res.isCharging);
},
});
// 扫码
wx.scanCode({
onlyFromCamera: false,
scanType: ["barCode", "qrCode"],
success(res) {
console.log("扫码结果:", res.result);
console.log("扫码类型:", res.scanType);
},
});
// 振动
wx.vibrateShort({ type: "medium" }); // 短振动
wx.vibrateLong(); // 长振动
// 设置屏幕亮度
wx.setScreenBrightness({ value: 0.8 });
// 拨打电话
wx.makePhoneCall({ phoneNumber: "10086" });
// 剪贴板
wx.setClipboardData({
data: "要复制的内容",
success() {
wx.showToast({ title: "已复制" });
},
});2.5 支付与分享能力
2.5.1 微信支付
定义:微信支付是微信内置的支付能力,需要后端生成预支付订单并返回支付参数。
支付流程:
小程序前端 后端服务器 微信支付
│ │ │
├── 请求下单 ───────────→ │ │
│ ├── 调用统一下单 API ──→ │
│ │ ←── 返回 prepay_id ─ │
│ ←── 返回支付参数 ─────── │ │
│ │ │
├── wx.requestPayment() ── │ │
│ │ ├── 用户确认支付
│ ←── 支付结果回调 ─────── │ │
│ │ │
├── 查询订单状态 (可选) ──→ │ │javascript
// 支付流程示例
function requestPayment(orderInfo) {
// orderInfo 由后端返回,包含以下字段
// { timeStamp, nonceStr, package, signType, paySign }
wx.requestPayment({
timeStamp: orderInfo.timeStamp,
nonceStr: orderInfo.nonceStr,
package: orderInfo.package, // 格式: prepay_id=xxx
signType: orderInfo.signType || "RSA",
paySign: orderInfo.paySign,
success(res) {
// 支付成功(最终结果以服务端回调为准)
wx.showToast({ title: "支付成功", icon: "success" });
},
fail(err) {
// 支付失败(用户取消、余额不足等)
if (err.errMsg.includes("cancel")) {
wx.showToast({ title: "已取消支付", icon: "none" });
} else {
wx.showToast({ title: "支付失败", icon: "error" });
}
},
});
}2.5.2 分享能力
javascript
// ===== 方式一:页面级分享(onShareAppMessage)=====
Page({
onShareAppMessage() {
return {
title: "发现一个好物!",
path: "/pages/detail/detail?id=" + this.data.productId,
imageUrl: this.data.productImage, // 自定义分享图
};
},
// 分享到朋友圈
onShareTimeline() {
return {
title: this.data.productName,
query: "id=" + this.data.productId,
};
},
});
// ===== 方式二:按钮触发分享 =====
// <button open-type="share">分享到好友</button>
// <button open-type="share" data-channel="timeline">分享到朋友圈</button>
// ===== 方式三:wx.shareAppMessage(主动分享)=====
wx.shareAppMessage({
title: "分享标题",
path: "/pages/index/index",
});2.5.3 其他开放能力
xml
<!-- 客服会话 -->
<button open-type="contact" session-from="userId_1001">联系客服</button>
<!-- 获取手机号 -->
<button open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber">
获取手机号
</button>
<!-- 打开 App -->
<button open-type="launchApp" app-parameter="from-miniprogram">打开 App</button>
<!-- 添加到我的小程序 -->
<button open-type="favorite" bindfavorite="onFavorite">收藏</button>javascript
Page({
onGetPhoneNumber(e) {
if (e.detail.errMsg === "getPhoneNumber:ok") {
// 加密的手机号数据(需后端解密)
const { encryptedData, iv } = e.detail;
// 发送到后端进行解密
wx.request({
url: "https://api.example.com/decrypt-phone",
method: "POST",
data: { encryptedData, iv },
success(res) {
console.log("解密后的手机号:", res.data.phoneNumber);
},
});
}
},
});小结
- 基础组件:
view/scroll-view/swiper是布局三件套;form/input/picker/button是表单核心 - 自定义组件:
Component构建器 +properties接收数据 +triggerEvent向父页面通信 - 网络与存储:封装 Promise 化的
wx.request工具函数;wx.setStorageSync/wx.getStorageSync处理本地数据 - 原生 API:
chooseImage/getLocation/scanCode是小程序最常用的三大硬件能力 - 支付与分享:支付需要服务端配合完成预下单;
onShareAppMessage实现页面级分享
练习
- 表单组件:创建注册页面,包含用户名、密码、手机号、性别(radio)、城市(picker)和协议确认(switch),提交时打印表单数据。
- 自定义组件:封装一个评分组件(
rating),支持星星数量和当前评分的属性,点击星星时触发事件返回评分值。 - 网络请求:使用 wx.request 调用
https://api.github.com/repos/axios/axios获取仓库信息,展示名称、描述、star 数和 fork 数。 - 本地存储:实现一个简单的购物车功能——点击商品时存储商品 ID 和数量到本地缓存,再读取显示。
- 设备 API:编写页面获取并展示当前设备的品牌、型号、网络类型、屏幕宽度和微信版本。