Skip to content

章节 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.json

2.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);
        },
      });
    }
  },
});

小结

  1. 基础组件view/scroll-view/swiper 是布局三件套;form/input/picker/button 是表单核心
  2. 自定义组件Component 构建器 + properties 接收数据 + triggerEvent 向父页面通信
  3. 网络与存储:封装 Promise 化的 wx.request 工具函数;wx.setStorageSync / wx.getStorageSync 处理本地数据
  4. 原生 APIchooseImage / getLocation / scanCode 是小程序最常用的三大硬件能力
  5. 支付与分享:支付需要服务端配合完成预下单;onShareAppMessage 实现页面级分享

练习

  1. 表单组件:创建注册页面,包含用户名、密码、手机号、性别(radio)、城市(picker)和协议确认(switch),提交时打印表单数据。
  2. 自定义组件:封装一个评分组件(rating),支持星星数量和当前评分的属性,点击星星时触发事件返回评分值。
  3. 网络请求:使用 wx.request 调用 https://api.github.com/repos/axios/axios 获取仓库信息,展示名称、描述、star 数和 fork 数。
  4. 本地存储:实现一个简单的购物车功能——点击商品时存储商品 ID 和数量到本地缓存,再读取显示。
  5. 设备 API:编写页面获取并展示当前设备的品牌、型号、网络类型、屏幕宽度和微信版本。

Python 学习资料