微信小程序原生开发详解

一、什么是微信小程序

微信小程序是一种运行在微信内的轻量级应用,无需下载安装,即点即用。它使用微信提供的原生开发方式,通过 WXML、WXSS、JavaScript/TypeScript 和 JSON 四种文件完成开发。

小程序 vs H5 vs App

维度微信小程序H5(移动端网页)原生 App
运行环境微信客户端(原生渲染)浏览器(WebView)系统原生
安装无需安装,扫码/搜索即用无需安装需从应用商店下载
发布更新微信审核后发布即时发布应用商店审核
性能接近原生依赖浏览器性能最好
离线能力部分支持有限完整
系统 API 访问微信 SDK 封装浏览器 API系统 API 完整
微信生态可直接调用微信支付、登录、分享需 H5 中转需接入微信 SDK

二、项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
my-miniprogram/
├── app.json # 全局配置
├── app.wxss # 全局样式
├── app.ts / app.js # 全局逻辑(App 实例)
├── project.config.json # 项目配置(IDE 用)
├── sitemap.json # 搜索配置
├── pages/ # 页面
│ ├── index/
│ │ ├── index.wxml # 模板
│ │ ├── index.wxss # 页面样式
│ │ ├── index.ts # 页面逻辑
│ │ └── index.json # 页面配置
│ └── logs/
│ └── ...
├── components/ # 自定义组件
│ └── card/
│ ├── card.wxml
│ ├── card.wxss
│ ├── card.ts
│ └── card.json
├── utils/ # 工具函数
│ └── api.ts
├── images/ # 图片资源
└── style/ # 公共样式

app.json——全局配置

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
{
"pages": [
"pages/index/index",
"pages/logs/logs"
],
"window": {
"navigationBarTitleText": "我的小程序",
"navigationBarBackgroundColor": "#409eff",
"backgroundColor": "#f5f5f5"
},
"tabBar": {
"color": "#999",
"selectedColor": "#409eff",
"list": [
{ "pagePath": "pages/index/index", "text": "首页", "iconPath": "images/home.png" },
{ "pagePath": "pages/logs/logs", "text": "日志", "iconPath": "images/log.png" }
]
},
"usingComponents": {},
"permission": {
"scope.userLocation": { "desc": "您的位置信息将用于展示附近门店" }
},
"requiredPrivateInfos": ["getLocation"],
"networkTimeout": {
"request": 10000,
"connectSocket": 10000
}
}

关键字段:

字段说明
pages页面路径列表,第一项为首页
window全局窗口样式
tabBar底部 Tab 栏配置
usingComponents全局注册自定义组件
permission权限申请说明
requiredPrivateInfos需用户授权才能使用的 API
networkTimeout网络超时配置

三、页面生命周期

App 生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app.ts
App({
onLaunch() {
// 小程序初始化完成(全局只执行一次)
wx.getSystemInfo({ success: res => console.log(res) });
},
onShow() {
// 小程序从前台切换到后台
},
onHide() {
// 小程序从前台切换到后台
},
globalData: {
userInfo: null,
},
});

Page 生命周期

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
// pages/index/index.ts
Page({
data: {
message: 'Hello',
userList: [],
},

onLoad(query) {
// 页面加载(只触发一次),query 为页面参数
console.log('页面参数:', query.id);
},

onShow() {
// 页面显示(每次进入页面都触发)
this.getUserList();
},

onReady() {
// 页面初次渲染完成(只触发一次)
},

onHide() {
// 页面隐藏(切入后台或跳转其他页面)
},

onUnload() {
// 页面卸载(关闭当前页面)
},

onPullDownRefresh() {
// 下拉刷新(需在 json 中配置 "enablePullDownRefresh": true)
this.getUserList();
wx.stopPullDownRefresh();
},

onReachBottom() {
// 上拉触底(用于分页加载)
this.loadMore();
},

onShareAppMessage() {
// 分享
return { title: '分享标题', path: '/pages/index/index' };
},

// 自定义方法
async getUserList() {
const res = await wx.request({ url: '/api/users' });
this.setData({ userList: res.data });
},
});
生命周期触发时机常见用途
onLoad页面加载时获取页面参数、初始化数据
onShow页面显示时刷新数据(每次进入都执行)
onReady页面渲染完成获取节点信息、动画初始化
onHide页面隐藏时暂停定时器、暂停播放
onUnload页面卸载时清理资源
onPullDownRefresh下拉刷新重新拉取数据
onReachBottom滚动触底分页加载

四、数据绑定与渲染

WXML 模板语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 数据绑定 -->
<view>{{ message }}</view>
<view id="item-{{id}}"></view>

<!-- 条件渲染 -->
<view wx:if="{{isLogin}}">已登录</view>
<view wx:else>未登录</view>

<view wx:if="{{score >= 90}}">A</view>
<view wx:elif="{{score >= 60}}">B</view>
<view wx:else>C</view>

<!-- 列表渲染 -->
<view wx:for="{{userList}}" wx:for-item="user" wx:for-index="i" wx:key="id">
<text>{{i + 1}}.{{user.name}}</text>
</view>

<!-- 模板引用 -->
<import src="template.wxml" />
<template is="card" data="{{item}}" />

wx:if vs hidden

wx:ifhidden
渲染方式条件为真时才渲染 DOM始终渲染,只是控制 display
切换代价高(节点创建/销毁)低(仅隐藏)
适用场景切换频率低切换频率高

setData——更新页面数据

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
Page({
data: {
count: 0,
user: { name: 'Alice', age: 25 },
list: [],
},

increment() {
this.setData({
count: this.data.count + 1,
});
},

updateUser() {
// 更新对象的某个属性
this.setData({
'user.name': 'Bob',
});
},

addItem(item) {
// 数组追加(不推荐直接修改原数组)
this.setData({
list: [...this.data.list, item],
});
},
});

setData 的性能注意事项

1
2
3
4
5
├── setData 是 Native 与 JS 线程的通信,有成本
├── 每次 setData 不要传递过大体积数据(建议 < 1MB)
├── 不在 setData 中传递不需要渲染的数据
├── 频繁调用的场景(如进度条)可考虑节流
└── 用路径更新代替整体更新:'user.name' 优于 user: { ... }

五、WXSS 样式

尺寸单位:rpx

1
2
3
4
5
6
7
8
9
10
11
12
/* rpx:responsive pixel,根据屏幕宽度自适应 */
/* 设计稿宽度通常为 750rpx */
.container {
width: 750rpx; /* 满屏宽度 */
padding: 20rpx; /* 四周间距 */
font-size: 28rpx; /* 字号 */
}

.card {
width: 340rpx; /* 两列布局每列宽度 */
margin: 10rpx;
}

WXSS 支持大部分 CSS 属性,但有差异:

1
2
3
支持:flex、grid、position、transition、animation、transform
不支持:部分 CSS3 高级选择器、backdrop-filter(部分版本支持)
无需考虑:浏览器兼容性(微信内置浏览器统一)

全局 vs 页面样式

1
2
3
4
5
6
7
8
9
10
11
/* app.wxss:全局样式,所有页面共享 */
page {
background-color: #f5f5f5;
font-size: 28rpx;
color: #333;
}

/* pages/index/index.wxss:页面级样式,仅当前页面生效 */
.index-container {
padding: 20rpx;
}

内联样式

1
<view style="color: {{dynamicColor}}; font-size: {{size}}px"></view>

注意:小程序支持用变量动态设置内联样式,但 style 中的属性名用驼峰(backgroundColor)或连字符(background-color)均可。

六、路由导航

1
2
3
4
5
6
7
8
9
10
11
// 声明式导航
<navigator url="/pages/detail/detail?id=1">跳转详情</navigator>
<navigator url="/pages/index/index" open-type="switchTab">跳转到 Tab</navigator>
<navigator url="/pages/login/login" open-type="redirect">重定向</navigator>

// 编程式导航
wx.navigateTo({ url: '/pages/detail/detail?id=1' }); // 保留当前页面,跳转
wx.redirectTo({ url: '/pages/login/login' }); // 关闭当前页面,跳转
wx.switchTab({ url: '/pages/index/index' }); // 跳转到 Tab 页
wx.navigateBack({ delta: 1 }); // 返回上一页
wx.reLaunch({ url: '/pages/index/index' }); // 关闭所有页面,跳转
API当前页面跳转目标使用场景
navigateTo保留非 Tab 页详情页、表单页
redirectTo关闭非 Tab 页登录页、支付结果
switchTab关闭Tab 页切换底部 Tab
navigateBack关闭上一页返回
reLaunch全部关闭任意页异常后重置

七、自定义组件

1
2
3
4
5
// components/card/card.json
{
"component": true,
"usingComponents": {}
}
1
2
3
4
5
6
7
8
9
<!-- components/card/card.wxml -->
<view class="card" bind:tap="onClick">
<image src="{{avatar}}" class="avatar" mode="aspectFill" />
<view class="info">
<text class="name">{{name}}</text>
<text class="desc">{{desc}}</text>
</view>
<slot />
</view>
1
2
3
4
5
6
7
/* components/card/card.wxss */
.card {
display: flex;
padding: 20rpx;
background: #fff;
border-radius: 12rpx;
}
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
// components/card/card.ts
Component({
properties: {
name: { type: String, value: '' },
desc: { type: String, value: '' },
avatar: { type: String, value: '' },
},

data: {
// 内部状态
},

// 组件生命周期
lifetimes: {
attached() { /* 组件进入页面节点树时 */ },
detached() { /* 组件从页面节点树移除时 */ },
},

// 页面生命周期(组件感知页面行为)
pageLifetimes: {
show() { /* 页面显示时 */ },
hide() { /* 页面隐藏时 */ },
},

methods: {
onClick() {
this.triggerEvent('tap', { id: this.properties.name });
},
},
});

组件通信

1
2
3
4
父 → 子:通过 properties 传递数据
子 → 父:通过 triggerEvent 触发事件
兄弟组件:通过父组件中转或全局事件
跨页面:全局数据 App.globalData、缓存、EventBus
1
2
3
4
5
6
7
<!-- 使用组件 -->
<card
name="{{user.name}}"
desc="{{user.desc}}"
avatar="{{user.avatar}}"
bind:tap="onCardTap"
/>
1
2
3
onCardTap(e: WechatMiniprogram.CustomEvent) {
console.log('点击了:', e.detail.id);
}

八、常用 API

网络请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 封装请求
const request = <T>(url: string, data?: Record<string, any>, method = 'GET'): Promise<T> => {
return new Promise((resolve, reject) => {
wx.request({
url: `https://api.example.com${url}`,
data,
method,
header: {
Authorization: `Bearer ${wx.getStorageSync('token')}`,
},
success: res => resolve(res.data as T),
fail: reject,
});
});
};

// 使用
async function getUserList() {
const res = await request<{ name: string }[]>('/users');
this.setData({ userList: res });
}

本地存储

1
2
3
4
5
6
7
8
// 同步
wx.setStorageSync('key', 'value');
const val = wx.getStorageSync('key');
wx.removeStorageSync('key');
wx.clearStorageSync();

// 异步(key 有上限,总容量不超过 10MB)
wx.setStorage({ key: 'token', data: 'xxx' });

用户登录

1
2
3
4
5
6
7
8
9
10
11
12
13
// 微信登录流程
App({
async onLaunch() {
// 1. 获取登录 code
const { code } = await wx.login();

// 2. 发送 code 到后端换取自定义登录态
const res = await request('/auth/login', { code });

// 3. 存储 token
wx.setStorageSync('token', res.token);
},
});

获取用户信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 头像昵称(新版,无需授权弹窗)
<button open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">获取头像</button>

onChooseAvatar(e) {
this.setData({ avatarUrl: e.detail.avatarUrl });
}

// 手机号(需付费模板消息)
<button open-type="getPhoneNumber" bind:getphonenumber="onGetPhone">获取手机号</button>

async onGetPhone(e) {
if (e.detail.code) {
const res = await request('/auth/phone', { code: e.detail.code });
this.setData({ phone: res.phoneNumber });
}
}

支付

1
2
3
4
5
6
7
8
9
10
11
12
13
async function wxPay(orderId: string) {
const { payment } = await request('/pay/unified-order', { orderId });

wx.requestPayment({
timeStamp: payment.timeStamp,
nonceStr: payment.nonceStr,
package: payment.package,
signType: 'RSA',
paySign: payment.paySign,
success() { wx.showToast({ title: '支付成功' }); },
fail() { wx.showToast({ title: '支付失败', icon: 'error' }); },
});
}

九、小程序与 H5 的差异

不支持 DOM / BOM API

小程序环境中没有 windowdocumentnavigator 等浏览器全局对象。常见替代:

浏览器 API小程序替代
document.querySelectorwx.createSelectorQuery
window.addEventListenerwx.onAppShowwx.onAppHide
localStoragewx.setStorageSync
fetch / XMLHttpRequestwx.request
Image 对象wx.getImageInfo
audio / video 标签wx.createInnerAudioContext
navigator.geolocationwx.getLocation
window.innerWidthwx.getSystemInfoSync().windowWidth

包大小限制

1
2
3
主包:不超过 2MB(含代码 + 资源)
分包:每个子包不超过 2MB,总包不超过 20MB
静态资源(图片/音频等)建议放 CDN,不要打包进项目

分包配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"pages": ["pages/index/index"],
"subPackages": [
{
"root": "packageA",
"pages": ["pages/order/order", "pages/cart/cart"]
},
{
"root": "packageB",
"independent": true,
"pages": ["pages/activity/activity"]
}
],
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["packageA"]
}
}
}

渲染性能

1
2
3
4
5
├── 双线程模型:渲染层(WebView)+ 逻辑层(JSCore)
├── setData 跨线程通信有开销
├── 频繁或大数据量 setData 会导致卡顿
├── 列表渲染建议使用 wx:key + 分页加载
└── 复杂页面考虑用 <scroll-view> 替代 Page 原生滚动(精细控制)

十、常见问题

Q1: 小程序页面跳转后 data 还在吗

页面跳转后,当前页面的 data 保存在内存中(navigateTo 保留,redirectTo 销毁)。返回后 data 恢复,onLoad 不会重新触发。如果每次进入页面都需要刷新数据,把请求放在 onShow 中。

Q2: 如何全局管理登录态

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
// app.ts
App({
globalData: { token: '' },

async checkLogin() {
const token = wx.getStorageSync('token');
if (!token) return false;

try {
const res = await wx.request({ url: '/api/auth/verify' });
return res.statusCode === 200;
} catch {
return false;
}
},
});

// pages/index/index.ts
const app = getApp();
Page({
async onShow() {
const isLogin = await app.checkLogin();
if (!isLogin) {
wx.navigateTo({ url: '/pages/login/login' });
}
},
});

Q3: 小程序如何处理大图片

1
2
3
4
5
├── 图片用 CDN 链接,不放入项目包
├── 列表中的图片用 image 组件的 lazy-load 属性
├── mode="widthFix" 保持比例自适应
├── 预加载首屏之外的图片
└── 使用 webp 格式(CDN 转换)

Q4: 小程序和 H5 哪个更适合做商城

场景推荐原因
强依赖微信生态(支付、分享)小程序原生支付体验好,可分享到聊天/朋友圈
需要频繁更新、无需审核H5审核周期 1-7 天
高交互复杂功能小程序性能接近原生
SEO 需求H5小程序内容不可被搜索引擎索引
超 2MB 业务复杂小程序(分包)分包后可超过 2MB

十一、推荐学习路径

  1. 注册小程序账号,下载开发者工具,跑通官方 demo
  2. 掌握页面生命周期和 setData 更新机制
  3. 写一个列表页 + 详情页,理解导航和数据传递
  4. 封装自定义组件,理解组件通信
  5. 接入微信登录和支付流程
  6. 配置分包和预加载,优化首屏性能和包体积
  7. 阅读小程序官方文档,逐个了解常用 API