微信小程序原生开发完全指南

从WXML、WXSS、JavaScript到JSON配置,全面掌握微信小程序原生开发的核心技术与最佳实践,包括生命周期管理、事件系统、自定义组件与网络请求。

微信小程序原生开发是所有跨端框架(Taro、Uni-app、WePY)的底层基础。无论是使用框架开发还是原生开发,深入理解微信小程序的核心技术栈——WXML、WXSS、JavaScript 和 JSON 配置——都是构建高质量小程序的必经之路。本文将系统性地剖析原生小程序开发的完整技术体系,从视图层到逻辑层,从基础配置到高级组件化,帮助开发者建立扎实的原生开发能力。

一、小程序技术架构概览

微信小程序采用双线程模型:逻辑层(JavaScriptCore)与渲染层(WebView)分离运行,通过 Native 层进行异步通信。这种架构带来了沙箱隔离的安全优势,但也决定了开发者在编写代码时必须遵循特定的通信范式。

┌──────────────┐      ┌──────────────┐      ┌──────────────┐
│   逻辑层     │      │    Native    │      │   渲染层     │
│  (JSCore)    │◄────►│   (Bridge)   │◄────►│  (WebView)   │
│  App Service │      │              │      │   View       │
└──────────────┘      └──────────────┘      └──────────────┘

理解这一架构至关重要:数据变更需经 Bridge 传递到渲染层,大量数据操作或频繁 setData 调用会成为性能瓶颈。开发者应当在逻辑层完成数据计算与状态管理,仅将最终结果通过 setData 推送给视图层。

二、WXML 视图层详解

WXML(WeiXin Markup Language)是小程序的视图描述语言,语法上借鉴了 Vue 的模板系统,但底层实现完全不同。

2.1 数据绑定与渲染

WXML 支持单向数据绑定,使用 Mustache 语法 {{ }} 将逻辑层数据渲染到视图层:

<!-- 基础数据绑定 -->
<view class="user-card">
  <text class="name">{{userInfo.nickName}}</text>
  <text class="level">等级: {{userInfo.level}}</text>
</view>

<!-- 条件渲染 -->
<view wx:if="{{isVip}}">
  <image src="/assets/vip-badge.png" />
</view>
<view wx:elif="{{isMember}}">
  <text>普通会员</text>
</view>
<view wx:else>
  <button bindtap="onRegister">立即注册</button>
</view>

<!-- 列表渲染 -->
<view class="product-list">
  <block wx:for="{{products}}" wx:for-item="item" wx:for-index="idx" wx:key="id">
    <view class="product-item" data-id="{{item.id}}" bindtap="onProductTap">
      <image src="{{item.coverUrl}}" mode="aspectFill" />
      <text class="title">{{item.title}}</text>
      <text class="price">¥{{item.price}}</text>
    </view>
  </block>
</view>

关键点在于 wx:key 的设置。缺少 wx:key 会导致列表重新渲染时 Diff 算法无法正确识别节点身份,引发性能下降甚至状态错乱。wx:key 的值可以是列表项的唯一属性名,也可以使用保留关键字 *this 表示列表项本身。

2.2 模板与引用

WXML 提供了 templateinclude 两种代码复用机制。template 支持参数传递,适合可复用的结构化组件;include 则是静态代码片段的引入,相当于将目标文件内容直接嵌入当前位置。

<!-- pages/common/product-card.wxml -->
<template name="productCard">
  <view class="card {{size}}">
    <image src="{{image}}" mode="aspectFill" lazy-load="{{true}}" />
    <view class="info">
      <text class="title">{{title}}</text>
      <text class="desc" wx:if="{{desc}}">{{desc}}</text>
      <view class="footer">
        <text class="price">¥{{price}}</text>
        <text class="sales">已售{{sales}}</text>
      </view>
    </view>
  </view>
</template>

<!-- pages/index/index.wxml -->
<import src="/pages/common/product-card.wxml" />
<template is="productCard" data="{{...item, size: 'large'}}" />

import 具有作用域隔离,只能使用被导入文件中定义的 templateinclude 则可以引入除 <template/><wxs/> 之外的所有内容。在大型项目中,合理使用模板系统能显著降低 WXML 的冗余度。

三、WXSS 样式系统

WXSS(WeiXin Style Sheets)在 CSS 基础上扩展了 rpx 单位和一些小程序特有的选择器限制。理解其特性与限制是写出高性能、易维护样式的前提。

3.1 rpx 响应式单位

rpx(responsive pixel)以 750rpx 为设计基准宽度,在所有设备上自动换算。开发者只需按照 iPhone 6/7/8 的 375px 逻辑宽度进行设计,将像素值乘以 2 即可得到对应的 rpx 值:

/* 设计稿宽度 375px,按钮宽度 120px -> 240rpx */
.action-btn {
  width: 240rpx;
  height: 88rpx;
  font-size: 32rpx;
  border-radius: 8rpx;
}

需要避免在需要精确控制的场景使用 rpx,例如 1px 边框在不同设备上可能因为换算产生模糊。此时推荐使用 px 单位配合 transform: scale() 实现高清边框:

.hd-border {
  position: relative;
}
.hd-border::after {
  content: '';
  position: absolute;
  top: 0; left: 0;
  width: 200%;
  height: 200%;
  border: 1px solid #e5e5e5;
  transform: scale(0.5);
  transform-origin: 0 0;
  pointer-events: none;
}

3.2 样式隔离与全局配置

小程序页面默认拥有样式隔离,页面内的样式不会影响其他页面。通过 app.wxss 可以定义全局通用样式,通过 @import 引入外部样式文件:

/* app.wxss - 全局样式 */
@import 'styles/variables.wxss';
@import 'styles/mixins.wxss';

page {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  color: #333;
  background-color: #f5f5f5;
}

.container {
  padding: 0 32rpx;
  box-sizing: border-box;
}

.text-ellipsis {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.text-multi-ellipsis {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

小程序选择器不支持伪类如 :hover:active(按钮组件有 hover-class 替代),也不支持属性选择器 [type="text"],层叠样式表的能力比标准 CSS 要受限。在实际开发中,推荐采用 BEM(Block-Element-Modifier)命名规范来组织类名,避免选择器冲突。

四、JavaScript 逻辑层

小程序的 JavaScript 运行在逻辑层沙箱中,无法直接操作 DOM 或 BOM,所有视图更新必须通过 PageComponent 构造器提供的 API 完成。

4.1 页面生命周期

// pages/detail/detail.js
Page({
  data: {
    product: null,
    loading: true,
    error: null
  },

  // 页面创建时执行,只触发一次
  onLoad(options) {
    const { id } = options;
    this.loadProductDetail(id);
  },

  // 页面首次渲染完成
  onReady() {
    // 可在此操作 canvas 或获取节点信息
    this.createSelectorQuery()
      .select('#product-swiper')
      .boundingClientRect(rect => {
        this.swiperHeight = rect.height;
      })
      .exec();
  },

  // 页面显示时触发
  onShow() {
    // 页面从后台切回前台时刷新数据
    if (this._needRefresh) {
      this.loadProductDetail(this.data.product?.id);
      this._needRefresh = false;
    }
  },

  // 页面隐藏时触发
  onHide() {
    // 保存未提交的表单数据
    this._draftData = { ...this.data.form };
  },

  // 页面卸载时触发
  onUnload() {
    // 清理定时器、取消网络请求
    clearInterval(this._pollingTimer);
    if (this._requestTask) {
      this._requestTask.abort();
    }
  },

  // 下拉刷新
  onPullDownRefresh() {
    this.loadProductDetail(this.data.product.id)
      .finally(() => wx.stopPullDownRefresh());
  },

  // 到达页面底部
  onReachBottom() {
    if (this.data.hasMore && !this.data.loadingMore) {
      this.loadMoreComments();
    }
  },

  // 页面滚动
  onPageScroll({ scrollTop }) {
    const threshold = 200;
    this.setData({
      showBackTop: scrollTop > threshold
    });
  },

  // 用户点击右上角分享
  onShareAppMessage() {
    const { product } = this.data;
    return {
      title: product.title,
      path: `/pages/detail/detail?id=${product.id}`,
      imageUrl: product.shareImage
    };
  },

  async loadProductDetail(id) {
    this.setData({ loading: true, error: null });
    try {
      const product = await request({
        url: `/api/products/${id}`,
        method: 'GET'
      });
      this.setData({ product, loading: false });
    } catch (err) {
      this.setData({ error: err.message, loading: false });
    }
  }
});

页面生命周期是小程序开发的核心概念。onLoad 仅执行一次,适合初始化参数解析与一次性数据加载;onShow/onHide 会在页面显隐时反复触发,适合管理刷新逻辑与状态保存;onUnload 是清理资源的最后机会,忘记释放定时器或取消请求会导致内存泄漏。

4.2 事件系统与数据传递

小程序事件分为冒泡事件(bindtap、bindtouchstart 等)和非冒泡事件(catchtap 等使用 catch 前缀)。

<!-- 事件传参通过 data-* 属性 -->
<view 
  class="action-btn {{item.disabled ? 'disabled' : ''}}"
  data-action="{{item.action}}"
  data-index="{{index}}"
  bindtap="handleAction"
>
  {{item.label}}
</view>
Page({
  handleAction(event) {
    const { action, index } = event.currentTarget.dataset;
    // event.currentTarget 指向绑定事件的元素
    // event.target 指向触发事件的原始元素,在列表中可能不同
    
    switch (action) {
      case 'buy':
        this.toBuyPage(index);
        break;
      case 'share':
        this.showShareSheet(index);
        break;
      default:
        console.warn('Unknown action:', action);
    }
  }
});

4.3 模块化管理

小程序支持 ES6 模块化语法,推荐将 API 请求、工具函数、常量配置拆分为独立模块:

// utils/request.js
const BASE_URL = 'https://api.example.com';

const request = (options) => {
  return new Promise((resolve, reject) => {
    wx.request({
      url: `${BASE_URL}${options.url}`,
      method: options.method || 'GET',
      data: options.data,
      header: {
        'Authorization': `Bearer ${wx.getStorageSync('token')}`,
        'Content-Type': 'application/json'
      },
      success: (res) => {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          resolve(res.data);
        } else if (res.statusCode === 401) {
          wx.navigateTo({ url: '/pages/login/login' });
          reject(new Error('Unauthorized'));
        } else {
          reject(new Error(res.data.message || 'Request failed'));
        }
      },
      fail: reject
    });
  });
};

export const get = (url, params) => request({ url, method: 'GET', data: params });
export const post = (url, data) => request({ url, method: 'POST', data });
export const put = (url, data) => request({ url, method: 'PUT', data });
export const del = (url) => request({ url, method: 'DELETE' });
// pages/list/list.js
import { get } from '../../utils/request';

Page({
  data: {
    items: [],
    page: 1,
    hasMore: true
  },

  async loadItems() {
    const { items, page } = this.data;
    const list = await get('/api/items', { page, size: 20 });
    this.setData({
      items: page === 1 ? list : [...items, ...list],
      hasMore: list.length === 20,
      page: page + 1
    });
  }
});

五、JSON 配置体系

小程序的配置分为三层:全局 app.json、页面 page.jsonsitemap.json,各自管辖不同范围的配置项。

5.1 app.json 全局配置

{
  "pages": [
    "pages/index/index",
    "pages/detail/detail",
    "pages/profile/profile",
    "pages/login/login"
  ],
  "window": {
    "navigationBarTitleText": "小程序示例",
    "navigationBarBackgroundColor": "#ffffff",
    "navigationBarTextStyle": "black",
    "backgroundColor": "#f5f5f5",
    "backgroundTextStyle": "dark",
    "enablePullDownRefresh": true,
    "onReachBottomDistance": 50
  },
  "tabBar": {
    "color": "#999999",
    "selectedColor": "#07c160",
    "backgroundColor": "#ffffff",
    "borderStyle": "black",
    "list": [
      { "pagePath": "pages/index/index", "text": "首页", "iconPath": "assets/tab-home.png", "selectedIconPath": "assets/tab-home-active.png" },
      { "pagePath": "pages/profile/profile", "text": "我的", "iconPath": "assets/tab-profile.png", "selectedIconPath": "assets/tab-profile-active.png" }
    ]
  },
  "networkTimeout": {
    "request": 10000,
    "downloadFile": 15000
  },
  "permission": {
    "scope.userLocation": {
      "desc": "你的位置信息将用于小程序位置接口的效果展示"
    }
  },
  "requiredBackgroundModes": ["audio", "location"],
  "lazyCodeLoading": "requiredComponents"
}

app.json 中的 pages 数组的顺序决定了小程序首次启动时加载的页面——数组第一项即首页。tabBar 最多支持 5 个标签页,其 pagePath 必须在 pages 数组中存在。lazyCodeLoading 设置为 requiredComponents 可以按需注入自定义组件代码,有效降低启动包体积。

5.2 页面级配置

{
  "navigationBarTitleText": "商品详情",
  "navigationBarBackgroundColor": "#fff",
  "usingComponents": {
    "product-swiper": "/components/product-swiper/product-swiper",
    "price-tag": "/components/price-tag/price-tag",
    "sku-selector": "/components/sku-selector/sku-selector"
  },
  "enablePullDownRefresh": true,
  "backgroundColor": "#f8f8f8"
}

每个页面目录下的 *.json 文件仅对该页面生效。usingComponents 是页面注册自定义组件的关键字段,未在此声明的组件无法在页面的 WXML 中使用。

六、App 实例与全局数据

// app.js
App({
  globalData: {
    userInfo: null,
    systemInfo: null,
    config: {
      apiBase: 'https://api.example.com',
      appVersion: '2.1.0'
    }
  },

  onLaunch(options) {
    // 小程序初始化
    this.checkUpdate();
    this.getSystemInfo();
    
    // 记录启动场景
    const { scene } = options;
    console.log('Launch scene:', scene);
  },

  onShow(options) {
    // 小程序展示在前台
  },

  onHide() {
    // 小程序进入后台
  },

  onError(msg) {
    // 全局错误监听
    console.error('Global error:', msg);
    // 可上报到错误监控服务
  },

  onPageNotFound(res) {
    wx.redirectTo({ url: '/pages/404/404' });
  },

  checkUpdate() {
    const updateManager = wx.getUpdateManager();
    updateManager.onCheckForUpdate((res) => {
      if (res.hasUpdate) {
        updateManager.onUpdateReady(() => {
          wx.showModal({
            title: '更新提示',
            content: '新版本已准备好,是否重启应用?',
            success: (res) => {
              if (res.confirm) updateManager.applyUpdate();
            }
          });
        });
      }
    });
  },

  getSystemInfo() {
    wx.getSystemInfo({
      success: (res) => {
        this.globalData.systemInfo = res;
      }
    });
  },

  // 全局工具方法
  getUserInfo() {
    return this.globalData.userInfo;
  }
});
// pages/profile/profile.js
const app = getApp();

Page({
  data: {
    userInfo: null
  },

  onLoad() {
    this.setData({
      userInfo: app.getUserInfo()
    });
  }
});

通过 getApp() 可以在任何页面获取 App 实例,但不应在 App 实例上频繁修改全局状态,以免导致不可预期的数据流问题。对于复杂的状态共享需求,应引入专门的状态管理方案(在后续文章中深入讨论)。

七、自定义组件开发

自定义组件是小程序实现 UI 复用与逻辑封装的核心手段。一个完整的组件由四个文件组成:.js.json.wxml.wxss

// components/count-down/count-down.js
Component({
  options: {
    styleIsolation: 'shared',  // 样式隔离策略
    multipleSlots: true        // 启用多 slot
  },

  properties: {
    targetTime: {
      type: Number,
      value: 0,
      observer(newVal) {
        if (newVal > 0) {
          this.startCountdown();
        }
      }
    },
    format: {
      type: String,
      value: 'HH:mm:ss'  // 默认格式
    }
  },

  data: {
    timeStr: '00:00:00',
    isExpired: false
  },

  lifetimes: {
    attached() {
      if (this.data.targetTime > 0) {
        this.startCountdown();
      }
    },
    detached() {
      clearInterval(this._timer);
    }
  },

  methods: {
    startCountdown() {
      clearInterval(this._timer);
      this._tick();
      this._timer = setInterval(() => this._tick(), 1000);
    },

    _tick() {
      const now = Date.now();
      const diff = this.data.targetTime - now;
      
      if (diff <= 0) {
        clearInterval(this._timer);
        this.setData({ timeStr: '已结束', isExpired: true });
        this.triggerEvent('expire');
        return;
      }

      const hours = Math.floor(diff / 3600000);
      const minutes = Math.floor((diff % 3600000) / 60000);
      const seconds = Math.floor((diff % 60000) / 1000);

      this.setData({
        timeStr: `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
      });
    }
  }
});
<!-- components/count-down/count-down.wxml -->
<view class="count-down {{isExpired ? 'expired' : ''}}">
  <slot name="prefix" />
  <text class="time">{{timeStr}}</text>
  <slot name="suffix" />
</view>

组件通过 properties 接收父组件传入的数据,data 维护组件私有状态。triggerEvent 用于向父组件派发事件,实现父子通信。在 lifetimes 中管理生命周期钩子,确保定时器及时清理,避免内存泄漏。

八、调试与开发工具

微信开发者工具提供了丰富的调试能力:

  • Console 面板:查看日志、执行临时 JavaScript 代码
  • Sources 面板:断点调试逻辑层代码
  • Network 面板:监控所有网络请求,查看请求头、响应体与耗时
  • Storage 面板:查看和修改本地缓存数据
  • AppData 面板:实时查看并修改页面数据,观察视图层变化
  • Sensor 面板:模拟地理位置、重力感应器等硬件条件

真机调试是排查特定环境问题的必要手段,特别是涉及扫码、支付、地理位置等需要在真机上验证的功能。使用「真机调试 2.0」可以获得与开发工具相近的调试体验。

九、总结

微信小程序原生开发虽然受到双线程模型和沙箱环境的约束,但正是这种架构保证了小程序的轻量、安全与快速启动。掌握 WXML 的模板语法、WXSS 的响应式布局和样式隔离、JavaScript 生命周期管理以及组件化封装能力,是构建任何复杂小程序应用的基础。

从原生开发出发,开发者可以更深刻地理解跨端框架的抽象原理,也能在遇到性能瓶颈或兼容问题时,直接介入底层进行优化。随着微信能力的持续扩展——视频号直播、微信客服、硬件蓝牙等——原生开发始终是获取平台最新能力的第一选择。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「miniprogram」更多文章

  1. 小程序全栈项目实战:从零构建电商应用
  2. 小程序自动化测试与 CI/CD 实践
  3. 小程序安全与合规实践