This commit is contained in:
Neo
2026-03-15 10:15:02 +08:00
parent 2dd217522c
commit 72bb11b34f
916 changed files with 65306 additions and 16102803 deletions

View File

@@ -0,0 +1,255 @@
# 逻辑层App Service
> 官方文档https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/
逻辑层使用 JavaScript 引擎运行,不在浏览器中,没有 `window``document` 等 Web API。
## App() — 注册小程序
```javascript
App({
onLaunch(options) {
// 小程序初始化时触发,全局只触发一次
// options.scene — 场景值
// options.query — 启动参数
// options.path — 启动页面路径
},
onShow(options) {
// 小程序启动或从后台进入前台时触发
},
onHide() {
// 小程序从前台进入后台时触发
},
onError(msg) {
// 小程序发生脚本错误或 API 调用失败时触发
console.error(msg)
},
onUnhandledRejection(res) {
// 未处理的 Promise 拒绝事件
console.warn(res.reason, res.promise)
},
onPageNotFound(res) {
// 页面不存在时触发
wx.redirectTo({ url: 'pages/...' })
},
onThemeChange({ theme }) {
// 系统主题变更dark / light
},
globalData: {
userInfo: null
}
})
```
获取 App 实例:
```javascript
const app = getApp()
console.log(app.globalData)
```
**注意**
- 不要在 `App()` 内调用 `getApp()`,使用 `this` 即可
- 不要在 `onLaunch` 时调用 `getCurrentPages()`,此时 page 还没有生成
## Page() — 注册页面
```javascript
Page({
data: {
text: 'Hello',
array: [{ msg: '1' }, { msg: '2' }]
},
// ===== 生命周期 =====
onLoad(options) {
// 页面加载时触发options 为页面路由参数
// 一个页面只会调用一次
},
onShow() {
// 页面显示/切入前台时触发
},
onReady() {
// 页面初次渲染完成时触发
// 一个页面只会调用一次,代表页面已可以和视图层交互
},
onHide() {
// 页面隐藏/切入后台时触发(如 navigateTo 或底部 tab 切换)
},
onUnload() {
// 页面卸载时触发(如 redirectTo 或 navigateBack
},
// ===== 页面事件处理 =====
onPullDownRefresh() {
// 下拉刷新(需在 json 中开启 enablePullDownRefresh
// 处理完后调用 wx.stopPullDownRefresh()
},
onReachBottom() {
// 上拉触底(可在 json 中设置 onReachBottomDistance
},
onShareAppMessage(res) {
// 用户点击右上角转发
// res.from: 'button' 或 'menu'
return {
title: '自定义转发标题',
path: '/pages/index/index',
imageUrl: '' // 自定义图片路径
}
},
onShareTimeline() {
// 分享到朋友圈(基础库 2.11.3+
return { title: '', query: '', imageUrl: '' }
},
onPageScroll(res) {
// 页面滚动时触发res.scrollTop 为垂直滚动距离(px)
// 注意:频繁触发,避免在此做复杂操作
},
onResize(res) {
// 页面尺寸变化时触发(如屏幕旋转)
},
onTabItemTap(item) {
// 当前是 tab 页时,点击 tab 时触发
// item.index / item.pagePath / item.text
},
// ===== 自定义方法 =====
viewTap() {
this.setData({
text: 'Set some data for updating view.'
})
}
})
```
### 页面生命周期顺序
```
onLoad → onShow → onReady → [onHide → onShow] → onUnload
```
### setData 详解
```javascript
// 基本用法
this.setData({
text: 'changed data'
})
// 修改数组某一项
this.setData({
'array[0].msg': 'changed'
})
// 修改对象某个属性
this.setData({
'object.key': 'value'
})
// 带回调
this.setData({ text: 'new' }, function() {
// setData 引起的界面更新渲染完毕后的回调
})
```
**setData 性能注意事项**
- 数据量不宜过大(单次 setData 不超过 1MB建议不超过 256KB
- 不要频繁调用(如 onPageScroll 中不要每次都 setData
- 只传需要更新的数据,不要整个 data 都传
- 后台页面不要 setData页面 onHide 后避免 setData
## 页面路由
框架以栈的形式维护当前所有页面,最多 10 层。
| 路由方式 | 触发时机 | 路由前页面 | 路由后页面 |
|----------|----------|-----------|-----------|
| 初始化 | 小程序打开第一个页面 | | onLoad, onShow |
| 打开新页面 | wx.navigateTo / `<navigator open-type="navigate">` | onHide | onLoad, onShow |
| 页面重定向 | wx.redirectTo / `<navigator open-type="redirect">` | onUnload | onLoad, onShow |
| 页面返回 | wx.navigateBack / 用户左上角返回 | onUnload | onShow |
| Tab 切换 | wx.switchTab / `<navigator open-type="switchTab">` / 用户切换 Tab | | 各种情况 |
| 重启动 | wx.reLaunch / `<navigator open-type="reLaunch">` | onUnload | onLoad, onShow |
```javascript
// 保留当前页面,跳转到新页面(页面栈 +1
wx.navigateTo({ url: '/pages/detail/detail?id=1' })
// 关闭当前页面,跳转到新页面(页面栈不变)
wx.redirectTo({ url: '/pages/detail/detail?id=1' })
// 关闭所有页面,打开某个页面
wx.reLaunch({ url: '/pages/index/index' })
// 跳转到 tabBar 页面,关闭其他所有非 tabBar 页面
wx.switchTab({ url: '/pages/index/index' })
// 返回上一页delta 为返回的页面数)
wx.navigateBack({ delta: 1 })
```
### 页面间通信EventChannel
```javascript
// 页面 A
wx.navigateTo({
url: '/pages/B/B',
events: {
// 监听来自 B 页面的事件
acceptDataFromOpenedPage(data) {
console.log(data)
}
},
success(res) {
// 向 B 页面发送数据
res.eventChannel.emit('acceptDataFromOpenerPage', { data: 'test' })
}
})
// 页面 B
Page({
onLoad() {
const eventChannel = this.getOpenerEventChannel()
// 向 A 页面发送数据
eventChannel.emit('acceptDataFromOpenedPage', { data: 'from B' })
// 监听来自 A 页面的数据
eventChannel.on('acceptDataFromOpenerPage', (data) => {
console.log(data)
})
}
})
```
## 模块化
```javascript
// common.js
function sayHello(name) {
console.log(`Hello ${name}!`)
}
module.exports.sayHello = sayHello
// 使用
const common = require('common.js')
common.sayHello('MINA')
```
**注意**
- 小程序不支持直接引入 `node_modules`,需使用 npm 构建或手动拷贝
- 每个文件有独立作用域,不同文件中可声明同名变量
- 通过 `getApp()` 获取全局数据
## getCurrentPages()
```javascript
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1] // 当前页面
const prevPage = pages[pages.length - 2] // 上一个页面
// 可以通过 prevPage.setData() 修改上一页数据(返回时生效)
```
## 在线查询
如需更详细信息,可抓取:
- App 参考https://developers.weixin.qq.com/miniprogram/dev/reference/api/App.html
- Page 参考https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html
- 路由https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/route.html

View File

@@ -0,0 +1,304 @@
# 开发最佳实践与常见坑
> 综合官方文档与社区经验
## setData 性能优化
setData 是小程序性能的关键瓶颈因为数据需要从逻辑层JS 线程)序列化后传输到视图层(渲染线程)。
### 原则
1. **减少数据量**:只传需要更新的字段
```javascript
// ❌ 错误:传整个列表
this.setData({ list: this.data.list })
// ✅ 正确:只更新变化的项
this.setData({ 'list[2].name': 'new name' })
```
2. **减少调用频率**:合并多次 setData
```javascript
// ❌ 错误:多次调用
this.setData({ a: 1 })
this.setData({ b: 2 })
this.setData({ c: 3 })
// ✅ 正确:合并为一次
this.setData({ a: 1, b: 2, c: 3 })
```
3. **后台页面不要 setData**
```javascript
// ❌ 错误:页面隐藏后仍在 setData如定时器
onShow() {
this._timer = setInterval(() => {
this.setData({ time: Date.now() })
}, 1000)
},
// ✅ 正确:页面隐藏时停止
onHide() {
clearInterval(this._timer)
}
```
4. **避免在 onPageScroll 中 setData**
```javascript
// ❌ 错误
onPageScroll(e) {
this.setData({ scrollTop: e.scrollTop })
}
// ✅ 正确:用 WXS 响应事件或节流
onPageScroll: throttle(function(e) {
if (this._needUpdate) {
this.setData({ isTop: e.scrollTop < 100 })
}
}, 100)
```
5. **大列表用纯数据字段**
```javascript
Component({
options: { pureDataPattern: /^_/ },
data: {
displayList: [], // 用于渲染
_rawList: [] // 纯数据,不传输到视图层
}
})
```
## 分包加载
### 基本分包
```json
// app.json
{
"pages": [
"pages/index/index",
"pages/logs/logs"
],
"subpackages": [
{
"root": "packageA",
"name": "pack-a",
"pages": [
"pages/cat/cat",
"pages/dog/dog"
]
},
{
"root": "packageB",
"name": "pack-b",
"pages": [
"pages/apple/apple"
]
}
]
}
```
**限制**
- 整个小程序所有分包大小不超过 20MB使用分包时
- 单个分包/主包大小不超过 2MB
- tabBar 页面必须在主包
### 独立分包
不依赖主包即可运行,适合独立功能页面(如活动页)。
```json
{
"subpackages": [
{
"root": "packageIndependent",
"pages": ["pages/activity/activity"],
"independent": true
}
]
}
```
**注意**独立分包中不能使用主包的公共资源js/组件/样式)。
### 分包预下载
```json
{
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["packageA"]
},
"pages/logs/logs": {
"network": "wifi",
"packages": ["packageB"]
}
}
}
```
## 小程序与 H5 的关键差异
| 特性 | H5 | 小程序 |
|------|-----|--------|
| DOM 操作 | 支持 document/window | ❌ 不支持 |
| BOM | 支持 | ❌ 不支持 |
| 路由 | URL hash/history | 页面栈(最多 10 层) |
| 样式 | 完整 CSS | WXSS部分 CSS 不支持) |
| 脚本 | 完整 JS + Web API | JS无 DOM API |
| 渲染 | 单线程 | 双线程(逻辑层 + 视图层) |
| 网络请求 | fetch/XMLHttpRequest | wx.request需配置域名 |
| 本地存储 | localStorage | wx.setStorage10MB |
| Cookie | 支持 | ❌ 不支持(需自行管理) |
| 动态创建元素 | 支持 | ❌ 不支持 |
| eval / new Function | 支持 | ❌ 不支持 |
| SVG | 支持 | 部分支持image src 可用) |
### 常见迁移坑
1. **没有 Cookie**:登录态需要自行通过 header 传递 token
2. **没有 DOM**:不能用 jQuery、不能 `document.getElementById`
3. **不支持动态执行代码**`eval()``new Function()` 都不可用
4. **样式差异**
- 不支持 `*` 通配符选择器
- 不支持 `>` `+` `~` 等关系选择器(部分版本已支持)
- 不支持 `@media` 的部分写法
- 不支持 `position: fixed` 在某些场景下的表现
5. **页面栈限制**:最多 10 层,超过后 navigateTo 会失败
6. **包大小限制**:主包 2MB总包 20MB
7. **网络请求域名白名单**:必须在管理后台配置
8. **不支持 npm 直接引入**:需要通过开发者工具构建 npm
## TypeScript 支持
小程序原生支持 TypeScript
```json
// tsconfig.json项目根目录
{
"compilerOptions": {
"strict": true,
"target": "ES2017",
"module": "ESNext",
"moduleResolution": "Node",
"lib": ["ES2017"],
"typeRoots": ["./typings"]
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}
```
```typescript
// 页面 .ts 文件
Page({
data: {
msg: 'Hello' as string,
list: [] as Array<{ id: number; name: string }>
},
onLoad(options: Record<string, string | undefined>) {
const id = options.id
}
})
// 组件 .ts 文件
Component({
properties: {
title: { type: String, value: '' }
},
data: {
count: 0 as number
},
methods: {
increment() {
this.setData({ count: this.data.count + 1 })
}
}
})
```
## npm 支持
1. 在小程序根目录执行 `npm install`
2. 在开发者工具中:工具 → 构建 npm
3. 构建后会生成 `miniprogram_npm` 目录
4. 使用:`const dayjs = require('dayjs')`
**注意**
- 不是所有 npm 包都能在小程序中使用(不能依赖 Node.js 内置模块或浏览器 API
- 每次 `npm install` 后都需要重新构建 npm
## 自定义 tabBar
```json
// app.json
{
"tabBar": {
"custom": true,
"list": [
{ "pagePath": "pages/index/index", "text": "首页" },
{ "pagePath": "pages/mine/mine", "text": "我的" }
]
}
}
```
在根目录创建 `custom-tab-bar/` 组件:
```
custom-tab-bar/
├── index.js
├── index.json
├── index.wxml
└── index.wxss
```
## 骨架屏
开发者工具支持自动生成骨架屏:
1. 在模拟器中预览页面
2. 点击模拟器右下角「...」→「生成骨架屏」
3. 会自动生成 `页面名.skeleton.wxml``页面名.skeleton.wxss`
```json
// 页面 json 中引用
{
"initialRenderingCache": "static"
}
```
## 常见审核被拒原因
1. **功能不完整**:提交审核时确保所有功能可用
2. **测试账号未提供**:需要登录的小程序必须提供测试账号
3. **类目不符**:选择的服务类目与实际功能不匹配
4. **诱导分享/关注**:不能强制用户分享或关注公众号才能使用
5. **虚拟支付**iOS 不允许虚拟商品使用微信支付(需走 IAP
6. **内容违规**UGC 内容需要内容安全检测
7. **隐私协议**:需要配置隐私保护指引
8. **授权滥用**:不能在首页就弹出授权请求,需要在使用时才请求
## 调试技巧
```javascript
// 真机调试日志
const log = wx.getRealtimeLogManager()
log.info('info message')
log.warn('warn message')
log.error('error message')
// 性能监控
const performance = wx.getPerformance()
const observer = performance.createObserver((entryList) => {
console.log(entryList.getEntries())
})
observer.observe({ entryTypes: ['render', 'script', 'navigation'] })
```
## 在线查询
- 性能优化https://developers.weixin.qq.com/miniprogram/dev/framework/performance/tips.html
- 分包加载https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages.html
- npm 支持https://developers.weixin.qq.com/miniprogram/dev/devtools/npm.html
- 自定义 tabBarhttps://developers.weixin.qq.com/miniprogram/dev/framework/ability/custom-tabbar.html

View File

@@ -0,0 +1,364 @@
# 内置组件
> 官方文档https://developers.weixin.qq.com/miniprogram/dev/component/
## 视图容器
### view
基础视图容器,类似 HTML 的 `<div>`
```xml
<view class="container" hover-class="hover" hover-stay-time="400">
内容
</view>
```
| 属性 | 类型 | 说明 |
|------|------|------|
| hover-class | string | 按下去的样式类none 为不设置 |
| hover-stop-propagation | boolean | 阻止祖先节点出现点击态 |
| hover-start-time | number | 按住后多久出现点击态(ms),默认 50 |
| hover-stay-time | number | 松开后点击态保留时间(ms),默认 400 |
### scroll-view
可滚动视图区域。
```xml
<!-- 纵向滚动需设置固定高度 -->
<scroll-view
scroll-y
style="height: 300px;"
bindscrolltolower="loadMore"
bindscroll="onScroll"
scroll-into-view="{{toView}}"
scroll-top="{{scrollTop}}"
refresher-enabled="{{true}}"
bindrefresherrefresh="onRefresh"
>
<view id="item1">A</view>
<view id="item2">B</view>
<view id="item3">C</view>
</scroll-view>
```
| 属性 | 类型 | 说明 |
|------|------|------|
| scroll-x | boolean | 允许横向滚动 |
| scroll-y | boolean | 允许纵向滚动 |
| upper-threshold | number | 距顶部/左边多远时触发 scrolltoupper(px),默认 50 |
| lower-threshold | number | 距底部/右边多远时触发 scrolltolower(px),默认 50 |
| scroll-top | number | 设置竖向滚动条位置 |
| scroll-into-view | string | 滚动到某子元素id |
| scroll-with-animation | boolean | 滚动动画过渡 |
| enable-back-to-top | boolean | iOS 点击状态栏回到顶部 |
| refresher-enabled | boolean | 开启自定义下拉刷新2.10.1+ |
| refresher-triggered | boolean | 设置刷新状态 |
| enhanced | boolean | 增强模式2.12.0+,支持 scroll-into-view 动画等) |
### swiper / swiper-item
滑块视图容器(轮播图)。
```xml
<swiper
indicator-dots="{{true}}"
autoplay="{{true}}"
interval="{{3000}}"
duration="{{500}}"
circular="{{true}}"
bindchange="swiperChange"
>
<swiper-item wx:for="{{imgUrls}}" wx:key="*this">
<image src="{{item}}" mode="aspectFill"/>
</swiper-item>
</swiper>
```
### movable-area / movable-view
可拖拽区域。
```xml
<movable-area style="height: 200px; width: 200px; background: red;">
<movable-view direction="all" style="height: 50px; width: 50px; background: blue;">
text
</movable-view>
</movable-area>
```
### cover-view / cover-image
覆盖在原生组件map、video、canvas、camera、live-player、live-pusher之上的视图。
## 基础内容
### text
```xml
<text selectable="{{true}}" space="ensp" decode="{{true}}">
Hello &nbsp; World
</text>
```
- `selectable`:文本是否可选
- `space`显示连续空格ensp/emsp/nbsp
- `decode`:是否解码(`&nbsp;` `&lt;` 等)
- **注意**`<text>` 组件内只支持嵌套 `<text>`
### rich-text
```xml
<rich-text nodes="{{htmlNodes}}"></rich-text>
```
```javascript
data: {
htmlNodes: '<div class="div_class"><h5>标题</h5><p>段落</p></div>'
// 也支持 nodes 数组格式
}
```
### icon
```xml
<icon type="success" size="23" color="green"/>
```
type 可选success, success_no_circle, info, warn, waiting, cancel, download, search, clear
### progress
```xml
<progress percent="80" show-info stroke-width="3" activeColor="#00CC00"/>
```
## 表单组件
### button
```xml
<button
type="primary"
size="default"
loading="{{isLoading}}"
disabled="{{isDisabled}}"
open-type="getPhoneNumber"
bindgetphonenumber="getPhoneNumber"
>
获取手机号
</button>
```
| open-type | 说明 |
|-----------|------|
| contact | 打开客服会话 |
| share | 触发转发 |
| getPhoneNumber | 获取手机号(需配合 bindgetphonenumber |
| getUserInfo | 已废弃2.27.1+ |
| launchApp | 打开 APP |
| openSetting | 打开授权设置页 |
| chooseAvatar | 获取用户头像(基础库 2.21.2+ |
| agreePrivacyAuthorization | 同意隐私协议2.33.2+ |
### input
```xml
<input
type="text"
placeholder="请输入"
value="{{inputValue}}"
bindinput="onInput"
bindfocus="onFocus"
bindblur="onBlur"
bindconfirm="onConfirm"
maxlength="140"
confirm-type="send"
adjust-position="{{true}}"
/>
```
| type | 说明 |
|------|------|
| text | 文本 |
| number | 数字 |
| idcard | 身份证 |
| digit | 带小数点数字 |
| nickname | 昵称输入(基础库 2.21.2+ |
| safe-password | 密码安全输入 |
### textarea
```xml
<textarea
value="{{content}}"
placeholder="请输入内容"
maxlength="-1"
auto-height
bindinput="onInput"
bindblur="onBlur"
show-confirm-bar="{{false}}"
/>
```
### picker
```xml
<!-- 普通选择器 -->
<picker mode="selector" range="{{array}}" bindchange="pickerChange">
<view>当前选择:{{array[index]}}</view>
</picker>
<!-- 多列选择器 -->
<picker mode="multiSelector" range="{{multiArray}}" bindchange="multiChange" bindcolumnchange="columnChange">
<view>{{multiArray[0][multiIndex[0]]}} - {{multiArray[1][multiIndex[1]]}}</view>
</picker>
<!-- 时间选择器 -->
<picker mode="time" value="{{time}}" start="09:00" end="21:00" bindchange="timeChange">
<view>{{time}}</view>
</picker>
<!-- 日期选择器 -->
<picker mode="date" value="{{date}}" start="2020-01-01" end="2030-12-31" bindchange="dateChange">
<view>{{date}}</view>
</picker>
<!-- 省市区选择器 -->
<picker mode="region" value="{{region}}" bindchange="regionChange">
<view>{{region[0]}} - {{region[1]}} - {{region[2]}}</view>
</picker>
```
### form
```xml
<form bindsubmit="formSubmit" bindreset="formReset">
<input name="username" placeholder="用户名"/>
<switch name="agree" checked/>
<slider name="age" show-value/>
<button form-type="submit">提交</button>
<button form-type="reset">重置</button>
</form>
```
```javascript
formSubmit(e) {
e.detail.value // { username: '...', agree: true, age: 50 }
}
```
### 其他表单组件
- `checkbox-group` + `checkbox`
- `radio-group` + `radio`
- `slider`
- `switch`
- `label`(绑定表单控件)
## 导航
### navigator
```xml
<navigator url="/pages/detail/detail?id=1" open-type="navigate">
跳转到详情
</navigator>
<navigator url="/pages/index/index" open-type="switchTab">
切换到首页 Tab
</navigator>
<navigator open-type="navigateBack" delta="1">
返回上一页
</navigator>
```
## 媒体组件
### image
```xml
<image
src="{{imgUrl}}"
mode="aspectFill"
lazy-load
show-menu-by-longpress
binderror="imgError"
bindload="imgLoad"
/>
```
| mode | 说明 |
|------|------|
| scaleToFill | 不保持比例缩放,填满 |
| aspectFit | 保持比例,完整显示(可能留白) |
| aspectFill | 保持比例,填满(可能裁剪) |
| widthFix | 宽度不变,高度自适应 |
| heightFix | 高度不变宽度自适应2.10.3+ |
| top/bottom/center/left/right | 不缩放,显示对应区域 |
**注意**image 默认宽 320px、高 240px。
### video
```xml
<video
src="{{videoUrl}}"
controls
autoplay="{{false}}"
loop="{{false}}"
muted="{{false}}"
initial-time="0"
show-fullscreen-btn
show-play-btn
enable-progress-gesture
bindplay="onPlay"
bindpause="onPause"
bindended="onEnded"
binderror="onError"
/>
```
### camera
```xml
<camera
device-position="back"
flash="auto"
bindscancode="onScanCode"
style="width: 100%; height: 300px;"
/>
```
## 地图
### map
```xml
<map
longitude="{{longitude}}"
latitude="{{latitude}}"
scale="16"
markers="{{markers}}"
polyline="{{polyline}}"
show-location
style="width: 100%; height: 300px;"
bindmarkertap="onMarkerTap"
bindregionchange="onRegionChange"
/>
```
## 画布
### canvas
```xml
<!-- 新版 Canvas 2D推荐基础库 2.9.0+ -->
<canvas type="2d" id="myCanvas" style="width: 300px; height: 200px;"/>
```
```javascript
const query = wx.createSelectorQuery()
query.select('#myCanvas')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node
const ctx = canvas.getContext('2d')
const dpr = wx.getWindowInfo().pixelRatio
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr)
ctx.fillRect(0, 0, 100, 100)
})
```
## 开放能力组件
### web-view
```xml
<!-- 承载网页的容器,会自动铺满整个小程序页面 -->
<web-view src="https://mp.weixin.qq.com/"></web-view>
```
- 需要在小程序管理后台配置业务域名
- 个人类型小程序暂不支持
### open-data已限制
```xml
<!-- 基础库 2.0.1+ 起,大部分 type 已不再返回真实数据 -->
<open-data type="userAvatarUrl"></open-data>
<open-data type="userNickName"></open-data>
```
## 在线查询
组件文档非常详细,如需查看某个具体组件的完整属性和事件,可抓取:
- 组件总览https://developers.weixin.qq.com/miniprogram/dev/component/
- 具体组件:`https://developers.weixin.qq.com/miniprogram/dev/component/{组件名}.html`
例如https://developers.weixin.qq.com/miniprogram/dev/component/scroll-view.html

View File

@@ -0,0 +1,443 @@
# 自定义组件
> 官方文档https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/
基础库 1.6.3+ 支持。
## 创建自定义组件
一个自定义组件由 `json` `wxml` `wxss` `js` 四个文件组成。
```json
// my-component.json — 声明为组件
{ "component": true }
```
```xml
<!-- my-component.wxml -->
<view class="inner">
{{innerText}}
<slot></slot>
</view>
```
```css
/* my-component.wxss — 样式只作用于本组件 */
.inner { color: red; }
```
```javascript
// my-component.js
Component({
properties: {
innerText: {
type: String,
value: 'default value'
}
},
data: {
someData: {}
},
methods: {
customMethod() {}
}
})
```
### 使用自定义组件
```json
// 页面或组件的 .json
{
"usingComponents": {
"my-component": "/components/my-component/my-component"
}
}
```
```xml
<!-- 页面 wxml -->
<my-component inner-text="Some text">
<view>这里是插入到 slot 中的内容</view>
</my-component>
```
**注意事项**
- 标签名只能是小写字母、中划线、下划线的组合
- 组件和页面所在项目根目录名不能以 "wx-" 为前缀
- 使用 `usingComponents` 会使页面的 `this` 原型稍有差异(多了 `selectComponent` 等方法)
- 使用 `usingComponents` 时,`setData` 内容不会被深复制(性能优化)
## Component() 构造器
```javascript
Component({
// ===== 组件属性(外部传入) =====
properties: {
myProperty: {
type: String, // 类型String, Number, Boolean, Object, Array, null(任意)
value: '', // 默认值
observer(newVal, oldVal) {
// 属性变化时触发(已不推荐,建议用 observers
}
},
myProperty2: String // 简化定义
},
// ===== 组件内部数据 =====
data: {
someData: 'initial'
},
// ===== 生命周期(推荐写在 lifetimes 中) =====
lifetimes: {
created() {
// 组件实例刚被创建时
// 此时不能调用 setData通常用于给 this 添加自定义属性
},
attached() {
// 组件实例进入页面节点树时
// 大多数初始化工作在此进行
},
ready() {
// 组件在视图层布局完成后
},
moved() {
// 组件实例被移动到节点树另一个位置时
},
detached() {
// 组件实例被从页面节点树移除时
// 清理工作(如清除定时器)
},
error(err) {
// 组件方法抛出错误时(基础库 2.4.1+
}
},
// ===== 组件所在页面的生命周期 =====
pageLifetimes: {
show() {
// 页面被展示时
},
hide() {
// 页面被隐藏时
},
resize(size) {
// 页面尺寸变化时
},
routeDone() {
// 页面路由动画完成时(基础库 2.31.2+
}
},
// ===== 数据监听器(基础库 2.6.1+ =====
observers: {
'numberA, numberB'(numberA, numberB) {
// numberA 或 numberB 变化时触发
this.setData({ sum: numberA + numberB })
},
'some.subfield'(subfield) {
// 监听子数据字段
},
'arr[12]'(val) {
// 监听数组某一项
},
'some.field.**'(field) {
// 使用通配符监听所有子数据字段
},
'**'() {
// 监听所有 setData每次 setData 都触发,慎用)
}
},
// ===== 方法 =====
methods: {
onMyButtonTap() {
this.setData({ someData: 'new value' })
},
// 内部方法建议以下划线开头
_myPrivateMethod() {
this.setData({ 'A.B': 'myPrivateData' })
}
},
// ===== behaviors =====
behaviors: [],
// ===== 其他选项 =====
options: {
multipleSlots: true, // 启用多 slot默认只能一个
styleIsolation: 'isolated', // 样式隔离模式
pureDataPattern: /^_/, // 纯数据字段正则
virtualHost: true // 虚拟化组件节点(基础库 2.11.2+
},
// ===== 外部样式类 =====
externalClasses: ['my-class'],
// ===== 组件间关系 =====
relations: {},
// ===== 导出(配合 wx://component-export =====
export() {
return { myField: 'myValue' }
}
})
```
## 组件模板和样式
### 多 slot
```javascript
// 组件 js
Component({
options: { multipleSlots: true }
})
```
```xml
<!-- 组件 wxml -->
<view>
<slot name="before"></slot>
<view>组件内部内容</view>
<slot name="after"></slot>
</view>
<!-- 使用 -->
<my-component>
<view slot="before">before 内容</view>
<view slot="after">after 内容</view>
</my-component>
```
### 样式隔离 styleIsolation
```javascript
Component({
options: {
styleIsolation: 'isolated'
// 'isolated'(默认):组件样式完全隔离
// 'apply-shared':页面 wxss 样式会影响组件,但组件不影响页面
// 'shared':页面和组件样式互相影响
}
})
```
也可在 json 中配置:
```json
{ "styleIsolation": "isolated" }
```
### 外部样式类
```javascript
// 组件
Component({
externalClasses: ['my-class']
})
```
```xml
<!-- 组件 wxml -->
<view class="my-class">这段文本的颜色由外部决定</view>
<!-- 使用时 -->
<my-component my-class="red-text"/>
```
## 组件间通信
### 父 → 子properties
```xml
<my-component prop-a="{{dataA}}" prop-b="staticValue"/>
```
### 子 → 父triggerEvent
```javascript
// 子组件
Component({
methods: {
onTap() {
this.triggerEvent('myevent', { value: 'data' }, {
bubbles: false, // 是否冒泡
composed: false, // 是否穿越组件边界
capturePhase: false // 是否有捕获阶段
})
}
}
})
```
```xml
<!-- 父组件/页面 -->
<my-component bind:myevent="onMyEvent"/>
<!-- 或 bindmyevent="onMyEvent" -->
```
```javascript
// 父组件/页面
Page({
onMyEvent(e) {
e.detail // { value: 'data' }
}
})
```
### 父获取子实例selectComponent
```xml
<my-component id="the-id" class="the-class"/>
```
```javascript
const child = this.selectComponent('#the-id')
// 或 this.selectComponent('.the-class')
child.setData({ ... })
child.someMethod()
```
## behaviors代码复用
类似 mixins / traits。
```javascript
// my-behavior.js
module.exports = Behavior({
behaviors: [], // 可以引用其他 behavior
properties: {
myBehaviorProperty: { type: String }
},
data: {
myBehaviorData: {}
},
attached() {},
methods: {
myBehaviorMethod() {}
}
})
```
```javascript
// 组件中使用
const myBehavior = require('my-behavior')
Component({
behaviors: [myBehavior],
// 组件自身的 properties/data/methods 会与 behavior 合并
// 同名字段:组件 > behavior > 更早的 behavior
// 同名生命周期都会执行behavior 先于组件)
})
```
### 内置 behaviors
| behavior | 说明 |
|----------|------|
| `wx://form-field` | 使组件像表单控件form 可识别 |
| `wx://form-field-group` | form 识别组件内部所有表单控件2.10.2+ |
| `wx://form-field-button` | form 识别组件内部 button2.10.3+ |
| `wx://component-export` | 自定义 selectComponent 返回值2.2.3+ |
## 纯数据字段
不用于渲染的数据,不会参与 setData 传输,提升性能。
```javascript
Component({
options: {
pureDataPattern: /^_/ // 以 _ 开头的字段为纯数据
},
data: {
a: true, // 普通数据,参与渲染
_b: true // 纯数据,不参与渲染
}
})
```
## 组件间关系 relations
```javascript
// custom-ul
Component({
relations: {
'./custom-li': {
type: 'child',
linked(target) {}, // 子组件 attached 时
linkChanged(target) {},
unlinked(target) {} // 子组件 detached 时
}
}
})
// custom-li
Component({
relations: {
'./custom-ul': {
type: 'parent',
linked(target) {},
linkChanged(target) {},
unlinked(target) {}
}
}
})
```
**type 可选值**`parent` / `child` / `ancestor` / `descendant`
## 抽象节点 componentGenerics
```json
// selectable-group.json
{
"componentGenerics": {
"selectable": {
"default": "path/to/default"
}
}
}
```
```xml
<!-- selectable-group.wxml -->
<view wx:for="{{labels}}">
<selectable disabled="{{false}}"></selectable>
</view>
<!-- 使用时指定具体组件 -->
<selectable-group generic:selectable="custom-radio"/>
```
## 用 Component 构造器构造页面
```javascript
Component({
properties: {
paramA: Number, // 接收页面参数 ?paramA=123
paramB: String
},
methods: {
onLoad() {
this.data.paramA // 123
},
onShow() {},
onPullDownRefresh() {}
// 页面生命周期写在 methods 中
}
})
```
对应 json 需包含 `usingComponents`
```json
{ "usingComponents": {} }
```
好处:可以使用 behaviors 提取所有页面公用代码。
## 在线查询
如需更详细信息,可抓取:
- Component 参考https://developers.weixin.qq.com/miniprogram/dev/reference/api/Component.html
- Behavior 参考https://developers.weixin.qq.com/miniprogram/dev/reference/api/Behavior.html
- 组件生命周期https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/lifetimes.html

View File

@@ -0,0 +1,188 @@
# 框架核心Framework Core
> 官方文档https://developers.weixin.qq.com/miniprogram/dev/framework/
## 架构概述
小程序框架MINA分为两部分
- **逻辑层App Service**:运行 JavaScript处理数据和业务逻辑
- **视图层View**:渲染 WXML + WXSS展示 UI
逻辑层和视图层分别运行在不同的线程中,通过 Native 层进行数据传输和事件通信。
**关键限制**:逻辑层不运行在浏览器中,没有 `window``document` 等 Web API。
## 目录结构
一个小程序主体部分由三个文件组成(必须放在项目根目录):
| 文件 | 必需 | 作用 |
|------|------|------|
| `app.js` | 是 | 小程序逻辑App() 注册) |
| `app.json` | 是 | 小程序公共配置 |
| `app.wxss` | 否 | 小程序公共样式表 |
一个小程序页面由四个文件组成:
| 文件 | 必需 | 作用 |
|------|------|------|
| `页面.js` | 是 | 页面逻辑Page() 注册) |
| `页面.wxml` | 是 | 页面结构(模板) |
| `页面.wxss` | 否 | 页面样式 |
| `页面.json` | 否 | 页面配置 |
## app.json 全局配置
```json
{
"pages": [
"pages/index/index",
"pages/logs/logs"
],
"window": {
"navigationBarTitleText": "小程序",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#eeeeee",
"backgroundTextStyle": "light",
"enablePullDownRefresh": false
},
"tabBar": {
"color": "#999",
"selectedColor": "#333",
"backgroundColor": "#fff",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "images/tab/home.png",
"selectedIconPath": "images/tab/home-active.png"
}
]
},
"networkTimeout": {
"request": 10000,
"downloadFile": 10000
},
"subpackages": [],
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
}
},
"lazyCodeLoading": "requiredComponents"
}
```
### pages 配置
- 数组第一项为小程序初始页面(首页)
- 不需要写文件后缀,框架会自动寻找 `.json` `.js` `.wxml` `.wxss` 四个文件
- 新增/减少页面需要修改 pages 数组
### window 配置
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| navigationBarBackgroundColor | HexColor | #000000 | 导航栏背景颜色 |
| navigationBarTextStyle | string | white | 导航栏标题颜色,仅支持 black / white |
| navigationBarTitleText | string | | 导航栏标题文字 |
| navigationStyle | string | default | 导航栏样式custom 为自定义导航栏(只保留右上角胶囊按钮) |
| backgroundColor | HexColor | #ffffff | 窗口背景色 |
| backgroundTextStyle | string | dark | 下拉 loading 样式,仅支持 dark / light |
| enablePullDownRefresh | boolean | false | 是否开启全局下拉刷新 |
| onReachBottomDistance | number | 50 | 页面上拉触底事件触发时距页面底部距离px |
### tabBar 配置
- `list` 数组最少 2 个、最多 5 个 tab
- tabBar 页面必须在 pages 数组中
- `position` 可选 `bottom`(默认)或 `top`(顶部时不显示 icon
## 页面配置page.json
每个页面可以有自己的 `.json` 文件,覆盖 `app.json``window` 的配置:
```json
{
"navigationBarTitleText": "页面标题",
"enablePullDownRefresh": true,
"usingComponents": {
"my-component": "/components/my-component/my-component"
}
}
```
## sitemap.json
配置小程序及其页面是否允许被微信索引:
```json
{
"rules": [
{
"action": "allow",
"page": "*"
}
]
}
```
## 场景值
场景值用于描述用户进入小程序的路径,常见场景值:
| 场景值 | 说明 |
|--------|------|
| 1001 | 发现栏小程序主入口 |
| 1007 | 单人聊天会话中的小程序消息卡片 |
| 1008 | 群聊会话中的小程序消息卡片 |
| 1011 | 扫描二维码 |
| 1012 | 长按识别二维码 |
| 1020 | 公众号 profile 页相关小程序列表 |
| 1035 | 公众号自定义菜单 |
| 1036 | App 分享消息卡片 |
| 1037 | 小程序打开小程序 |
| 1038 | 从另一个小程序返回 |
| 1043 | 公众号模板消息 |
| 1047 | 扫描小程序码 |
| 1048 | 长按识别小程序码 |
| 1089 | 微信聊天主界面下拉 |
可在 `App.onLaunch` / `App.onShow` 中通过 `options.scene` 获取。
## 基础库版本兼容
```javascript
// 方式一wx.canIUse
if (wx.canIUse('openBluetoothAdapter')) {
wx.openBluetoothAdapter()
} else {
wx.showModal({ title: '提示', content: '当前微信版本过低,无法使用该功能' })
}
// 方式二:比较版本号
function compareVersion(v1, v2) {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) v1.push('0')
while (v2.length < len) v2.push('0')
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i])
const num2 = parseInt(v2[i])
if (num1 > num2) return 1
else if (num1 < num2) return -1
}
return 0
}
const SDKVersion = wx.getSystemInfoSync().SDKVersion
if (compareVersion(SDKVersion, '1.1.0') >= 0) {
wx.openBluetoothAdapter()
}
```
## 在线查询
如需更详细的配置项说明,可直接抓取:
- 全局配置https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html
- 页面配置https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/page.html
- 场景值列表https://developers.weixin.qq.com/miniprogram/dev/reference/scene-list.html

View File

@@ -0,0 +1,431 @@
# 前端 API
> 官方文档https://developers.weixin.qq.com/miniprogram/dev/api/
## API 调用约定
小程序 API 分三类:
- **事件监听 API**:以 `on` 开头,如 `wx.onSocketOpen`
- **同步 API**:以 `Sync` 结尾,如 `wx.setStorageSync`
- **异步 API**:大多数 API支持回调和 Promise
```javascript
// 回调风格
wx.request({
url: 'https://example.com/api',
success(res) { console.log(res.data) },
fail(err) { console.error(err) },
complete() { /* 无论成功失败都执行 */ }
})
// Promise 风格(基础库 2.10.2+,除部分 API 外均支持)
const res = await wx.request({ url: 'https://example.com/api' })
// 注意:部分 API 不支持 Promise如 wx.downloadFile、wx.connectSocket 等
```
## 路由
```javascript
// 保留当前页面,跳转(栈 +1最多 10 层)
wx.navigateTo({
url: '/pages/detail/detail?id=1&name=test',
events: { /* EventChannel 监听 */ },
success(res) { res.eventChannel.emit('data', {}) }
})
// 关闭当前页面,跳转
wx.redirectTo({ url: '/pages/other/other' })
// 关闭所有页面,打开
wx.reLaunch({ url: '/pages/index/index' })
// 跳转到 tabBar 页面(关闭其他非 tabBar 页面)
wx.switchTab({ url: '/pages/index/index' })
// ⚠️ switchTab 不支持带参数
// 返回
wx.navigateBack({ delta: 1 })
```
## 网络
### wx.request — HTTP 请求
```javascript
wx.request({
url: 'https://example.com/api/data',
method: 'POST', // GET, POST, PUT, DELETE, OPTIONS, HEAD, TRACE, CONNECT
data: { key: 'value' },
header: {
'content-type': 'application/json',
'Authorization': 'Bearer token'
},
timeout: 60000, // 超时时间(ms)
dataType: 'json', // 返回数据自动 JSON.parse
responseType: 'text', // text 或 arraybuffer
enableHttp2: false,
enableQuic: false,
enableCache: false,
success(res) {
res.statusCode // HTTP 状态码
res.data // 响应数据
res.header // 响应头
},
fail(err) {
err.errMsg // 错误信息
}
})
```
**注意事项**
- 需在小程序管理后台配置合法域名request 合法域名)
- 默认超时 60s可在 `app.json``networkTimeout.request` 配置
- 最大并发限制 10 个
- HTTPS 只支持 TLS 1.2+
- `data` 为 Object 时GET 请求会序列化为 query stringPOST 请求 header 为 `application/json` 时序列化为 JSON`application/x-www-form-urlencoded` 时序列化为 query string
### wx.uploadFile — 上传文件
```javascript
wx.chooseImage({
success(res) {
wx.uploadFile({
url: 'https://example.com/upload',
filePath: res.tempFilePaths[0],
name: 'file',
formData: { user: 'test' },
success(uploadRes) {
console.log(uploadRes.data)
}
})
}
})
```
### wx.downloadFile — 下载文件
```javascript
wx.downloadFile({
url: 'https://example.com/file.pdf',
success(res) {
if (res.statusCode === 200) {
wx.openDocument({ filePath: res.tempFilePath })
}
}
})
```
### WebSocket
```javascript
const socketTask = wx.connectSocket({
url: 'wss://example.com/ws',
header: { 'Authorization': 'Bearer token' }
})
socketTask.onOpen(() => {
socketTask.send({ data: 'hello' })
})
socketTask.onMessage((res) => {
console.log(res.data)
})
socketTask.onClose(() => {})
socketTask.onError((err) => {})
socketTask.close()
```
## 数据缓存
```javascript
// 异步
wx.setStorage({ key: 'key', data: 'value' })
wx.getStorage({
key: 'key',
success(res) { console.log(res.data) }
})
wx.removeStorage({ key: 'key' })
wx.clearStorage()
wx.getStorageInfo({
success(res) {
res.keys // 所有 key
res.currentSize // 当前占用(KB)
res.limitSize // 限制大小(KB)
}
})
// 同步
wx.setStorageSync('key', 'value')
const value = wx.getStorageSync('key')
wx.removeStorageSync('key')
wx.clearStorageSync()
```
**注意**
- 单个 key 上限 1MB
- 总上限 10MB
- 隔离策略:同一小程序不同用户数据隔离
## 界面
### 交互反馈
```javascript
// Toast
wx.showToast({ title: '成功', icon: 'success', duration: 2000 })
wx.showToast({ title: '加载中', icon: 'loading' })
wx.showToast({ title: '自定义图标', icon: 'none' }) // 无图标,可显示两行文字
wx.hideToast()
// Loading
wx.showLoading({ title: '加载中', mask: true })
wx.hideLoading()
// Modal
wx.showModal({
title: '提示',
content: '确定删除?',
confirmText: '确定',
cancelText: '取消',
success(res) {
if (res.confirm) { /* 确定 */ }
else if (res.cancel) { /* 取消 */ }
}
})
// ActionSheet
wx.showActionSheet({
itemList: ['选项A', '选项B', '选项C'],
success(res) {
console.log(res.tapIndex) // 0, 1, 2
}
})
```
### 导航栏
```javascript
wx.setNavigationBarTitle({ title: '新标题' })
wx.setNavigationBarColor({
frontColor: '#ffffff', // 仅支持 #ffffff 和 #000000
backgroundColor: '#ff0000',
animation: { duration: 400, timingFunc: 'easeIn' }
})
wx.showNavigationBarLoading()
wx.hideNavigationBarLoading()
```
### 下拉刷新
```javascript
wx.startPullDownRefresh() // 触发下拉刷新
wx.stopPullDownRefresh() // 停止下拉刷新
```
### 滚动
```javascript
wx.pageScrollTo({
scrollTop: 0,
duration: 300
})
```
### TabBar
```javascript
wx.showTabBar()
wx.hideTabBar()
wx.setTabBarBadge({ index: 0, text: '1' })
wx.removeTabBarBadge({ index: 0 })
wx.showTabBarRedDot({ index: 0 })
wx.hideTabBarRedDot({ index: 0 })
wx.setTabBarItem({
index: 0,
text: '新文字',
iconPath: '/images/new-icon.png',
selectedIconPath: '/images/new-icon-active.png'
})
wx.setTabBarStyle({
color: '#000',
selectedColor: '#ff0000',
backgroundColor: '#ffffff'
})
```
## 媒体
### 图片
```javascript
// 选择图片(从相册或拍照)
wx.chooseMedia({
count: 9,
mediaType: ['image'],
sourceType: ['album', 'camera'],
sizeType: ['original', 'compressed'],
success(res) {
res.tempFiles // [{ tempFilePath, size, ... }]
}
})
// 预览图片
wx.previewImage({
current: 'url', // 当前显示图片的链接
urls: ['url1', 'url2', 'url3']
})
// 获取图片信息
wx.getImageInfo({
src: 'path/or/url',
success(res) {
res.width
res.height
res.path
res.orientation
res.type
}
})
// 保存图片到相册
wx.saveImageToPhotosAlbum({
filePath: 'tempFilePath',
success() {}
})
```
## 位置
```javascript
// 获取位置(需要用户授权 scope.userLocation
wx.getLocation({
type: 'gcj02', // wgs84 或 gcj02
success(res) {
res.latitude
res.longitude
res.speed
res.accuracy
}
})
// 打开地图选择位置
wx.chooseLocation({
success(res) {
res.name
res.address
res.latitude
res.longitude
}
})
// 打开内置地图查看位置
wx.openLocation({
latitude: 23.099994,
longitude: 113.324520,
name: '位置名',
address: '详细地址',
scale: 18
})
```
## 文件系统
```javascript
const fs = wx.getFileSystemManager()
// 读文件
fs.readFile({
filePath: `${wx.env.USER_DATA_PATH}/hello.txt`,
encoding: 'utf8',
success(res) { console.log(res.data) }
})
// 写文件
fs.writeFile({
filePath: `${wx.env.USER_DATA_PATH}/hello.txt`,
data: 'some text',
encoding: 'utf8',
success() {}
})
// 其他appendFile, mkdir, rmdir, readdir, stat, unlink, rename, copyFile, access
```
## WXML 节点查询
```javascript
// SelectorQuery
const query = wx.createSelectorQuery()
query.select('#the-id').boundingClientRect((rect) => {
rect.top // 节点上边界坐标
rect.right
rect.bottom
rect.left
rect.width
rect.height
})
query.selectViewport().scrollOffset((res) => {
res.scrollTop
res.scrollLeft
})
query.exec()
// 在组件中使用
const query = this.createSelectorQuery()
query.select('#the-id').boundingClientRect().exec(callback)
// IntersectionObserver监听元素与视口交叉
const observer = wx.createIntersectionObserver(this, { thresholds: [0, 0.5, 1] })
observer.relativeToViewport({ bottom: 100 })
observer.observe('.target-class', (res) => {
res.intersectionRatio // 交叉比例
res.intersectionRect // 交叉区域
})
// 停止监听
observer.disconnect()
```
## 系统信息
```javascript
// 同步获取(推荐用新 API
const windowInfo = wx.getWindowInfo()
// windowInfo.windowWidth / windowInfo.windowHeight / windowInfo.pixelRatio
// windowInfo.statusBarHeight / windowInfo.safeArea
const appBaseInfo = wx.getAppBaseInfo()
// appBaseInfo.SDKVersion / appBaseInfo.language / appBaseInfo.theme
const deviceInfo = wx.getDeviceInfo()
// deviceInfo.platform / deviceInfo.brand / deviceInfo.model / deviceInfo.system
// 旧 API仍可用但不推荐
const sysInfo = wx.getSystemInfoSync()
```
## 转发分享
```javascript
// 页面中定义 onShareAppMessage 即可开启转发
Page({
onShareAppMessage(res) {
if (res.from === 'button') {
// 来自页面内转发按钮
}
return {
title: '自定义转发标题',
path: '/pages/index/index?id=123',
imageUrl: '/images/share.png'
}
},
onShareTimeline() {
// 分享到朋友圈(基础库 2.11.3+
return {
title: '朋友圈标题',
query: 'id=123',
imageUrl: '/images/share.png'
}
}
})
```
```xml
<!-- 页面内转发按钮 -->
<button open-type="share">转发</button>
```
## 在线查询
API 文档非常庞大,如需查看某个具体 API 的完整参数,可抓取:
- API 总览https://developers.weixin.qq.com/miniprogram/dev/api/
- 具体 API`https://developers.weixin.qq.com/miniprogram/dev/api/{分类}/{api名}.html`
例如https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html

View File

@@ -0,0 +1,362 @@
# 登录与鉴权
> 官方文档https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
## 登录流程概览
```
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 小程序 │ │ 开发者服务器 │ │ 微信服务器 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ 1. wx.login() │ │
│ ──────────────>│ │
│ 返回 code │ │
│ │ │
│ 2. wx.request │ │
│ 发送 code │ │
│ ──────────────>│ │
│ │ 3. code2Session│
│ │ ──────────────>│
│ │ 返回 openid + │
│ │ session_key │
│ │ <──────────────│
│ │ │
│ │ 4. 生成自定义 │
│ │ 登录态 token │
│ │ │
│ 5. 返回 token │ │
│ <──────────────│ │
│ │ │
│ 6. 后续请求 │ │
│ 携带 token │ │
│ ──────────────>│ │
│ │ 7. 校验 token │
│ │ 查 openid │
```
## 前端登录实现
### 基本登录
```javascript
// app.js 或 utils/auth.js
async function login() {
// 1. 调用 wx.login 获取 code
const { code } = await wx.login()
// 2. 发送 code 到后端换取 token
const res = await wx.request({
url: 'https://your-server.com/api/auth/login',
method: 'POST',
data: { code }
})
if (res.data.token) {
// 3. 存储 token
wx.setStorageSync('token', res.data.token)
return res.data
} else {
throw new Error('登录失败')
}
}
```
### 检查登录态
```javascript
// 检查 session_key 是否过期
function checkSession() {
return new Promise((resolve, reject) => {
wx.checkSession({
success: () => resolve(true), // session_key 未过期
fail: () => resolve(false) // session_key 已过期,需重新 login
})
})
}
// 启动时检查
async function checkAndLogin() {
const token = wx.getStorageSync('token')
if (!token) {
return await login()
}
const isSessionValid = await checkSession()
if (!isSessionValid) {
// session_key 过期,重新登录
return await login()
}
return { token }
}
```
### 封装请求(自动携带 token
```typescript
// utils/request.ts
interface RequestOptions {
url: string
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
data?: any
header?: Record<string, string>
}
function request(options: RequestOptions): Promise<any> {
const token = wx.getStorageSync('token')
return new Promise((resolve, reject) => {
wx.request({
...options,
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
...options.header
},
success(res) {
if (res.statusCode === 401) {
// token 过期,重新登录
wx.removeStorageSync('token')
login().then(() => {
// 重试原请求
request(options).then(resolve).catch(reject)
})
return
}
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data)
} else {
reject(res)
}
},
fail: reject
})
})
}
```
## 后端登录实现Python / FastAPI 示例)
```python
import httpx
from fastapi import APIRouter, HTTPException, Depends
from datetime import datetime, timedelta
import jwt
router = APIRouter()
APPID = "your_appid"
SECRET = "your_secret"
JWT_SECRET = "your_jwt_secret"
@router.post("/auth/login")
async def login(code: str):
# 1. 用 code 换取 openid + session_key
url = "https://api.weixin.qq.com/sns/jscode2session"
params = {
"appid": APPID,
"secret": SECRET,
"js_code": code,
"grant_type": "authorization_code"
}
async with httpx.AsyncClient() as client:
resp = await client.get(url, params=params)
data = resp.json()
if "errcode" in data and data["errcode"] != 0:
raise HTTPException(400, f"微信登录失败: {data.get('errmsg')}")
openid = data["openid"]
session_key = data["session_key"]
unionid = data.get("unionid")
# 2. 查找或创建用户
user = await find_or_create_user(openid, unionid)
# 3. 生成 JWT token
token = jwt.encode({
"sub": str(user.id),
"openid": openid,
"exp": datetime.utcnow() + timedelta(days=7)
}, JWT_SECRET, algorithm="HS256")
# 4. 缓存 session_key用于后续解密
await cache_session_key(openid, session_key)
return {"token": token, "user": user.to_dict()}
```
## 获取手机号
### 前端
```xml
<!-- 基础库 2.21.2+ 推荐用法 -->
<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber">
授权手机号
</button>
```
```javascript
Page({
getPhoneNumber(e) {
if (e.detail.errMsg === 'getPhoneNumber:ok') {
const code = e.detail.code // 动态令牌(新版)
// 发送 code 到后端
wx.request({
url: 'https://your-server.com/api/auth/phone',
method: 'POST',
data: { code },
success(res) {
console.log('手机号:', res.data.phoneNumber)
}
})
} else {
console.log('用户拒绝授权')
}
}
})
```
### 后端
```python
@router.post("/auth/phone")
async def get_phone(code: str, token: str = Depends(get_current_token)):
access_token = await get_access_token()
url = f"https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={access_token}"
async with httpx.AsyncClient() as client:
resp = await client.post(url, json={"code": code})
data = resp.json()
if data.get("errcode") != 0:
raise HTTPException(400, f"获取手机号失败: {data.get('errmsg')}")
phone_info = data["phone_info"]
phone_number = phone_info["phoneNumber"]
# 更新用户手机号
await update_user_phone(token.openid, phone_number)
return {"phoneNumber": phone_number}
```
## 用户信息获取(当前方案)
`wx.getUserProfile` 已于基础库 2.27.1 废弃。当前获取用户头像和昵称的方式:
### 头像昵称填写能力
```xml
<!-- 头像选择 -->
<button open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
<image src="{{avatarUrl}}" class="avatar"/>
</button>
<!-- 昵称填写type="nickname" 自动弹出微信昵称) -->
<input type="nickname" placeholder="请输入昵称" bindchange="onNicknameChange"/>
```
```javascript
Page({
data: {
avatarUrl: '/images/default-avatar.png',
nickname: ''
},
onChooseAvatar(e) {
const { avatarUrl } = e.detail
// avatarUrl 是临时路径,需上传到自己服务器
this.setData({ avatarUrl })
this.uploadAvatar(avatarUrl)
},
onNicknameChange(e) {
this.setData({ nickname: e.detail.value })
},
async uploadAvatar(tempPath) {
wx.uploadFile({
url: 'https://your-server.com/api/upload/avatar',
filePath: tempPath,
name: 'file',
success(res) {
const data = JSON.parse(res.data)
// 保存头像 URL
}
})
}
})
```
## 授权管理
```javascript
// 查看当前授权状态
wx.getSetting({
success(res) {
res.authSetting['scope.userLocation'] // true/false/undefined
res.authSetting['scope.writePhotosAlbum']
// undefined = 未请求过true = 已授权false = 已拒绝
}
})
// 提前请求授权
wx.authorize({
scope: 'scope.userLocation',
success() { /* 授权成功 */ },
fail() { /* 用户拒绝 */ }
})
// 打开设置页(用户之前拒绝后,引导重新授权)
wx.openSetting({
success(res) {
res.authSetting // 最新的授权状态
}
})
```
### 常用 scope
| scope | 说明 | 对应 API |
|-------|------|----------|
| scope.userLocation | 精确地理位置 | wx.getLocation |
| scope.userFuzzyLocation | 模糊地理位置 | wx.getFuzzyLocation |
| scope.record | 麦克风 | wx.startRecord |
| scope.camera | 摄像头 | camera 组件 |
| scope.writePhotosAlbum | 保存到相册 | wx.saveImageToPhotosAlbum |
| scope.bluetooth | 蓝牙 | wx.openBluetoothAdapter |
| scope.addPhoneContact | 添加到联系人 | wx.addPhoneContact |
| scope.addPhoneCalendar | 添加到日历 | wx.addPhoneCalendar |
| scope.werun | 微信运动步数 | wx.getWeRunData |
| scope.userInfo | 已废弃 | - |
## 安全注意事项
1. **session_key 绝不能下发到前端**
2. **code 只能使用一次**,且有效期很短(约 5 分钟)
3. **不要在前端存储 openid**,通过 token 在后端关联
4. **JWT token 设置合理过期时间**(建议 7 天,配合 refresh token
5. **HTTPS 是强制要求**,所有网络请求必须 HTTPS
6. **敏感数据解密**要在服务端进行,不要在前端解密
7. **access_token 要在服务端缓存**,不要每次请求都重新获取
8. **unionid 需要绑定开放平台**才能获取,用于跨应用用户关联
## 常见问题
### Q: wx.login 的 code 可以多次使用吗?
A: 不可以,每个 code 只能使用一次,且有效期约 5 分钟。
### Q: session_key 什么时候会过期?
A: 微信不会通知过期时间,需要通过 `wx.checkSession` 检查。用户越频繁使用小程序session_key 有效期越长。
### Q: 如何获取 unionid
A: 需要在微信开放平台绑定小程序。绑定后code2Session 会返回 unionid。
### Q: getUserProfile 废弃后怎么获取用户信息?
A: 使用头像昵称填写能力(`<button open-type="chooseAvatar">` + `<input type="nickname">`),让用户主动填写。
## 在线查询
- 登录流程https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
- 手机号快速验证https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html
- 用户信息https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html
- 授权https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/authorize.html

View File

@@ -0,0 +1,302 @@
# 服务端 API
> 官方文档https://developers.weixin.qq.com/miniprogram/dev/api-backend/
服务端 API 是小程序后端服务器调用微信服务器的 HTTP 接口。
## access_token
几乎所有服务端 API 都需要 access_token。
```
GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
```
响应:
```json
{
"access_token": "ACCESS_TOKEN",
"expires_in": 7200
}
```
**注意事项**
- 有效期 2 小时,需要定时刷新并缓存
- 每日调用上限有限制
- 多服务器部署时需要中控服务器统一管理 access_token
- 刷新 access_token 会使旧的立即失效
## 登录 — code2Session
```
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
```
响应:
```json
{
"openid": "OPENID",
"session_key": "SESSION_KEY",
"unionid": "UNIONID",
"errcode": 0,
"errmsg": "ok"
}
```
| 参数 | 说明 |
|------|------|
| openid | 用户唯一标识(同一小程序内唯一) |
| session_key | 会话密钥(用于解密用户数据) |
| unionid | 用户在开放平台的唯一标识(需绑定开放平台) |
**安全要求**
- `session_key` 不能下发到前端
- `js_code` 只能使用一次
- 该接口不需要 access_token
## 手机号
### 获取手机号(新版,推荐)
前端通过 `<button open-type="getPhoneNumber">` 获取 code后端用 code 换取手机号:
```
POST https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=ACCESS_TOKEN
{
"code": "动态令牌code"
}
```
响应:
```json
{
"errcode": 0,
"errmsg": "ok",
"phone_info": {
"phoneNumber": "13800138000",
"purePhoneNumber": "13800138000",
"countryCode": "86",
"watermark": {
"timestamp": 1637744274,
"appid": "APPID"
}
}
}
```
### 旧版解密方式(不推荐)
使用 session_key + iv 解密 encryptedData获取手机号。
## 小程序码
### 获取不限制的小程序码(推荐)
```
POST https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN
{
"scene": "id=123",
"page": "pages/index/index",
"check_path": true,
"env_version": "release",
"width": 430,
"auto_color": false,
"line_color": {"r": 0, "g": 0, "b": 0},
"is_hyaline": false
}
```
- `scene` 最大 32 个可见字符
- 返回二进制图片数据Content-Type: image/jpeg 或 image/png
- 数量不限制
### 获取小程序码(有限制)
```
POST https://api.weixin.qq.com/wxa/getwxacode?access_token=ACCESS_TOKEN
{
"path": "pages/index/index?id=123",
"width": 430
}
```
- `path` 可带参数,最大 128 字节
- 总数限制 10 万个
### 获取小程序二维码(有限制)
```
POST https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token=ACCESS_TOKEN
{
"path": "pages/index/index?id=123",
"width": 430
}
```
- 总数限制 10 万个
## 订阅消息
### 发送订阅消息
```
POST https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=ACCESS_TOKEN
{
"touser": "OPENID",
"template_id": "TEMPLATE_ID",
"page": "pages/index/index",
"miniprogram_state": "formal",
"lang": "zh_CN",
"data": {
"thing1": { "value": "订单已发货" },
"time2": { "value": "2025-01-01 12:00" },
"character_string3": { "value": "SF1234567890" }
}
}
```
**前端需先请求用户授权**
```javascript
wx.requestSubscribeMessage({
tmplIds: ['TEMPLATE_ID'],
success(res) {
// res[TEMPLATE_ID] === 'accept' 表示用户同意
}
})
```
## 客服消息
### 发送客服消息
```
POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
// 文本消息
{
"touser": "OPENID",
"msgtype": "text",
"text": { "content": "Hello" }
}
// 图片消息
{
"touser": "OPENID",
"msgtype": "image",
"image": { "media_id": "MEDIA_ID" }
}
// 小程序卡片
{
"touser": "OPENID",
"msgtype": "miniprogrampage",
"miniprogrampage": {
"title": "标题",
"pagepath": "pages/index/index",
"thumb_media_id": "MEDIA_ID"
}
}
```
## 内容安全
### 文本内容安全检测
```
POST https://api.weixin.qq.com/wxa/msg_sec_check?access_token=ACCESS_TOKEN
{
"content": "待检测文本",
"version": 2,
"scene": 1,
"openid": "OPENID"
}
```
scene 值1-社交日志 2-评论 3-论坛 4-社交日志
### 图片内容安全检测
```
POST https://api.weixin.qq.com/wxa/img_sec_check?access_token=ACCESS_TOKEN
// multipart/form-data 上传图片
```
## 数据分析
```
// 获取日访问数据
POST https://api.weixin.qq.com/datacube/getweanalysisappiddailyvisittrend?access_token=ACCESS_TOKEN
{ "begin_date": "20250101", "end_date": "20250101" }
// 获取用户画像
POST https://api.weixin.qq.com/datacube/getweanalysisappiduserportrait?access_token=ACCESS_TOKEN
{ "begin_date": "20250101", "end_date": "20250107" }
```
## URL Scheme / URL Link
### 生成 URL Scheme用于短信/邮件等外部跳转)
```
POST https://api.weixin.qq.com/wxa/generatescheme?access_token=ACCESS_TOKEN
{
"jump_wxa": {
"path": "pages/index/index",
"query": "id=123",
"env_version": "release"
},
"is_expire": true,
"expire_type": 0,
"expire_time": 1672502400
}
```
### 生成 URL Link用于短信/邮件等外部跳转)
```
POST https://api.weixin.qq.com/wxa/generate_urllink?access_token=ACCESS_TOKEN
{
"path": "pages/index/index",
"query": "id=123",
"is_expire": true,
"expire_type": 0,
"expire_time": 1672502400,
"env_version": "release"
}
```
## 常见错误码
| errcode | 说明 |
|---------|------|
| -1 | 系统繁忙 |
| 0 | 请求成功 |
| 40001 | access_token 无效或过期 |
| 40013 | 不合法的 AppID |
| 40029 | 不合法的 code已使用或过期 |
| 40125 | 不合法的 appsecret |
| 41002 | 缺少 appid |
| 41004 | 缺少 appsecret |
| 42001 | access_token 过期 |
| 45009 | 调用超过频率限制 |
| 45011 | API 调用太频繁 |
| 48001 | API 未授权 |
| 61024 | 该 code 已被使用 |
## 在线查询
如需查看某个具体服务端 API 的完整参数,可抓取:
- 服务端 API 总览https://developers.weixin.qq.com/miniprogram/dev/api-backend/
- 登录https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html
- 手机号https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/phonenumber/phonenumber.getPhoneNumber.html
- 小程序码https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html
- 订阅消息https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html

View File

@@ -0,0 +1,511 @@
# TDesign 小程序组件库
> 官方文档https://tdesign.tencent.com/miniprogram/overview
> GitHubhttps://github.com/Tencent/tdesign-miniprogram
TDesign 是腾讯出品的企业级设计体系,提供微信小程序组件库,包含 60+ 高质量组件。
## 安装
```bash
npm i tdesign-miniprogram -S --production
```
安装后在微信开发者工具中构建 npm`工具 → 构建 npm`
构建时若出现 `NPM packages not found`,在 `project.config.json` 补充:
```json
{
"setting": {
"packNpmManually": true,
"packNpmRelationList": [
{
"packageJsonPath": "./package.json",
"miniprogramNpmDistDir": "./miniprogram/"
}
]
}
}
```
构建成功后勾选 `将 JS 编译成 ES5`
## 必要配置
### 移除 style: v2
`app.json` 中的 `"style": "v2"` 移除,否则会导致 TDesign 组件样式错乱。
### TypeScript 配置
如果使用 TypeScript 开发,修改 `tsconfig.json`
```json
{
"compilerOptions": {
"paths": {
"tdesign-miniprogram/*": ["./miniprogram/miniprogram_npm/tdesign-miniprogram/*"]
}
}
}
```
### 最低基础库版本
`^2.12.0`
## 使用组件
### 引入
在页面或组件的 `.json` 中注册:
```json
{
"usingComponents": {
"t-button": "tdesign-miniprogram/button/button"
}
}
```
全局引入则在 `app.json` 中配置 `usingComponents`
### 使用
```xml
<t-button theme="primary">按钮</t-button>
```
### 引入路径规则
所有组件路径格式:`tdesign-miniprogram/{组件名}/{组件名}`
```json
{
"usingComponents": {
"t-button": "tdesign-miniprogram/button/button",
"t-input": "tdesign-miniprogram/input/input",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-image": "tdesign-miniprogram/image/image",
"t-dialog": "tdesign-miniprogram/dialog/dialog",
"t-toast": "tdesign-miniprogram/toast/toast",
"t-navbar": "tdesign-miniprogram/navbar/navbar",
"t-tabs": "tdesign-miniprogram/tabs/tabs",
"t-tab-panel": "tdesign-miniprogram/tab-panel/tab-panel",
"t-popup": "tdesign-miniprogram/popup/popup",
"t-picker": "tdesign-miniprogram/picker/picker",
"t-picker-item": "tdesign-miniprogram/picker-item/picker-item",
"t-tag": "tdesign-miniprogram/tag/tag",
"t-avatar": "tdesign-miniprogram/avatar/avatar",
"t-badge": "tdesign-miniprogram/badge/badge",
"t-search": "tdesign-miniprogram/search/search",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-loading": "tdesign-miniprogram/loading/loading",
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton",
"t-swipe-cell": "tdesign-miniprogram/swipe-cell/swipe-cell",
"t-tab-bar": "tdesign-miniprogram/tab-bar/tab-bar",
"t-tab-bar-item": "tdesign-miniprogram/tab-bar-item/tab-bar-item"
}
}
```
## 完整组件列表
### 基础6
| 组件 | 标签 | 说明 |
|------|------|------|
| Button 按钮 | `t-button` | 主按钮、次按钮、文字按钮、图标按钮 |
| Divider 分割线 | `t-divider` | 内容分隔 |
| Fab 悬浮按钮 | `t-fab` | 浮动操作按钮 |
| Icon 图标 | `t-icon` | 内置图标库 |
| Layout 布局 | `t-row` / `t-col` | 栅格布局 |
| Link 链接 | `t-link` | 文字链接 |
### 导航8
| 组件 | 标签 | 说明 |
|------|------|------|
| BackTop 返回顶部 | `t-back-top` | 长页面返回顶部 |
| Drawer 抽屉 | `t-drawer` | 侧边滑出面板 |
| Indexes 索引 | `t-indexes` | 字母索引列表 |
| Navbar 导航条 | `t-navbar` | 自定义顶部导航栏 |
| SideBar 侧边导航栏 | `t-side-bar` / `t-side-bar-item` | 侧边分类导航 |
| Steps 步骤条 | `t-steps` / `t-step-item` | 流程步骤展示 |
| TabBar 底部标签栏 | `t-tab-bar` / `t-tab-bar-item` | 底部导航 |
| Tabs 选项卡 | `t-tabs` / `t-tab-panel` | 顶部选项卡切换 |
### 输入16
| 组件 | 标签 | 说明 |
|------|------|------|
| Calendar 日历 | `t-calendar` | 日期选择 |
| Cascader 级联选择器 | `t-cascader` | 多级联动选择 |
| CheckBox 多选框 | `t-checkbox` / `t-checkbox-group` | 多选 |
| ColorPicker 颜色选择器 | `t-color-picker` | 颜色选取 |
| DateTimePicker 日期选择器 | `t-date-time-picker` | 日期时间选择 |
| Input 输入框 | `t-input` | 文本输入 |
| Picker 选择器 | `t-picker` / `t-picker-item` | 滚动选择 |
| Radio 单选框 | `t-radio` / `t-radio-group` | 单选 |
| Rate 评分 | `t-rate` | 星级评分 |
| Search 搜索框 | `t-search` | 搜索输入 |
| Slider 滑动选择器 | `t-slider` | 滑块选择 |
| Stepper 步进器 | `t-stepper` | 数量加减 |
| Switch 开关 | `t-switch` | 开关切换 |
| Textarea 多行文本框 | `t-textarea` | 多行输入 |
| TreeSelect 树形选择器 | `t-tree-select` | 树形多级选择 |
| Upload 上传 | `t-upload` | 文件/图片上传 |
### 数据展示18
| 组件 | 标签 | 说明 |
|------|------|------|
| Avatar 头像 | `t-avatar` / `t-avatar-group` | 用户头像 |
| Badge 徽章 | `t-badge` | 消息提示红点/数字 |
| Cell 单元格 | `t-cell` / `t-cell-group` | 列表项 |
| Collapse 折叠面板 | `t-collapse` / `t-collapse-panel` | 可展开/收起内容 |
| CountDown 倒计时 | `t-count-down` | 倒计时显示 |
| Empty 空状态 | `t-empty` | 无数据提示 |
| Footer 页脚 | `t-footer` | 页面底部信息 |
| Grid 宫格 | `t-grid` / `t-grid-item` | 宫格布局 |
| Image 图片 | `t-image` | 增强图片(懒加载、加载状态) |
| ImageViewer 图片预览 | `t-image-viewer` | 图片放大预览 |
| Progress 进度条 | `t-progress` | 进度展示 |
| QRCode 二维码 | `t-qrcode` | 二维码生成 |
| Result 结果 | `t-result` | 操作结果反馈 |
| Skeleton 骨架屏 | `t-skeleton` | 加载占位 |
| Sticky 吸顶容器 | `t-sticky` | 滚动吸顶 |
| Swiper 轮播图 | `t-swiper` / `t-swiper-nav` | 轮播展示 |
| Tag 标签 | `t-tag` / `t-check-tag` | 标签展示/可选标签 |
| Watermark 水印 | `t-watermark` | 页面水印 |
### 反馈13
| 组件 | 标签 | 说明 |
|------|------|------|
| ActionSheet 动作面板 | `t-action-sheet` | 底部弹出操作列表 |
| Dialog 对话框 | `t-dialog` | 模态对话框 |
| DropdownMenu 下拉菜单 | `t-dropdown-menu` / `t-dropdown-item` | 下拉筛选 |
| Guide 引导 | `t-guide` | 新手引导 |
| Loading 加载 | `t-loading` | 加载中状态 |
| Message 全局提示 | `t-message` | 顶部消息提示 |
| NoticeBar 消息提醒 | `t-notice-bar` | 通知栏 |
| Overlay 遮罩层 | `t-overlay` | 背景遮罩 |
| Popover 弹出气泡 | `t-popover` | 气泡提示/菜单 |
| Popup 弹出层 | `t-popup` | 通用弹出层 |
| PullDownRefresh 下拉刷新 | `t-pull-down-refresh` | 下拉刷新 |
| SwipeCell 滑动操作 | `t-swipe-cell` | 左右滑动操作 |
| Toast 轻提示 | `t-toast` | 轻量提示 |
## 常用组件用法示例
### Button 按钮
```xml
<!-- 主题 -->
<t-button theme="primary">主按钮</t-button>
<t-button theme="default">次按钮</t-button>
<t-button theme="danger">危险按钮</t-button>
<t-button theme="light">浅色按钮</t-button>
<!-- 变体 -->
<t-button variant="base">填充</t-button>
<t-button variant="outline">描边</t-button>
<t-button variant="dashed">虚框</t-button>
<t-button variant="text">文字</t-button>
<!-- 尺寸 -->
<t-button size="large">大按钮</t-button>
<t-button size="medium">中按钮</t-button>
<t-button size="small">小按钮</t-button>
<t-button size="extra-small">超小按钮</t-button>
<!-- 块级 -->
<t-button block theme="primary">块级按钮</t-button>
<!-- 图标按钮 -->
<t-button theme="primary" icon="app">带图标</t-button>
<!-- 加载状态 -->
<t-button theme="primary" loading>加载中</t-button>
<!-- 禁用 -->
<t-button theme="primary" disabled>禁用</t-button>
```
### Input 输入框
```xml
<t-input
label="标签"
placeholder="请输入"
value="{{value}}"
bind:change="onChange"
maxlength="20"
type="text"
clearable
/>
<!-- 带前缀图标 -->
<t-input
prefixIcon="search"
placeholder="搜索"
bind:change="onSearch"
/>
<!-- 密码输入 -->
<t-input
label="密码"
type="password"
placeholder="请输入密码"
clearable
/>
```
### Cell 单元格
```xml
<t-cell-group>
<t-cell title="单行标题" arrow />
<t-cell title="单行标题" note="辅助信息" arrow />
<t-cell title="单行标题" description="描述信息" arrow />
<t-cell title="单行标题" left-icon="user" arrow />
</t-cell-group>
```
### Dialog 对话框
```xml
<t-dialog
visible="{{showDialog}}"
title="对话框标题"
content="对话框内容"
confirm-btn="确认"
cancel-btn="取消"
bind:confirm="onConfirm"
bind:cancel="onCancel"
/>
```
```javascript
// 命令式调用
const dialog = this.selectComponent('#t-dialog')
dialog.open()
// 或
dialog.close()
```
### Toast 轻提示
```xml
<t-toast id="t-toast" />
```
```javascript
import Toast from 'tdesign-miniprogram/toast/index'
Toast({
context: this,
selector: '#t-toast',
message: '提示信息',
theme: 'success', // success / warning / error / loading
duration: 2000
})
```
### Popup 弹出层
```xml
<t-popup visible="{{visible}}" placement="bottom" bind:visible-change="onVisibleChange">
<view class="popup-content">弹出内容</view>
</t-popup>
```
### Tabs 选项卡
```xml
<t-tabs defaultValue="{{0}}" bind:change="onTabChange">
<t-tab-panel label="标签1" value="0">内容1</t-tab-panel>
<t-tab-panel label="标签2" value="1">内容2</t-tab-panel>
<t-tab-panel label="标签3" value="2">内容3</t-tab-panel>
</t-tabs>
```
### Navbar 导航条
```xml
<t-navbar
title="页面标题"
left-arrow
bind:go-back="onGoBack"
/>
<!-- 自定义导航栏(需在 page.json 设置 navigationStyle: custom -->
<t-navbar title="自定义" left-arrow fixed>
<view slot="capsule">胶囊区域</view>
</t-navbar>
```
### TabBar 底部标签栏
```xml
<t-tab-bar value="{{activeTab}}" bind:change="onTabBarChange">
<t-tab-bar-item value="home" icon="home">首页</t-tab-bar-item>
<t-tab-bar-item value="user" icon="user">我的</t-tab-bar-item>
</t-tab-bar>
```
### Search 搜索框
```xml
<t-search
placeholder="搜索"
value="{{searchValue}}"
bind:change="onSearchChange"
bind:submit="onSearchSubmit"
bind:clear="onSearchClear"
/>
```
### Empty 空状态
```xml
<t-empty icon="folder-open" description="暂无数据" />
```
### Loading 加载
```xml
<t-loading theme="circular" size="40rpx" text="加载中..." />
```
### Skeleton 骨架屏
```xml
<t-skeleton loading="{{loading}}" row-col="{{rowCol}}">
<view>实际内容</view>
</t-skeleton>
```
```javascript
data: {
loading: true,
rowCol: [
{ width: '100%', height: '340rpx' },
[{ width: '45%' }, { width: '45%' }],
{ width: '100%' },
{ width: '60%' }
]
}
```
## 样式覆盖4 种方式)
### 1. style / custom-style 属性
```xml
<t-button style="color: red">按钮</t-button>
<t-button custom-style="color: red">按钮</t-button>
```
开启 virtualHost 时两者效果一致;未开启时只能用 `custom-style`
### 2. 解除样式隔离
TDesign 全体组件开启了 `addGlobalClass`,页面样式可直接覆盖:
```css
/* 页面 wxss */
.t-button--primary {
background-color: navy;
}
```
在自定义组件中使用需开启 `styleIsolation: 'shared'`
### 3. 外部样式类
```xml
<t-button t-class="my-btn-class">按钮</t-button>
```
```css
.my-btn-class {
color: red !important;
}
```
每个组件支持的外部样式类见组件文档(如 `t-class``t-class-icon``t-class-content` 等)。
### 4. CSS 变量
```css
page {
--td-brand-color: navy; /* 主题色 */
--td-success-color: #00a870; /* 成功色 */
--td-warning-color: #ed7b2f; /* 警告色 */
--td-error-color: #e34d59; /* 错误色 */
}
```
每个组件都有独立的 CSS 变量,见组件文档的 CSS Variables 部分。
## 自定义主题
全局 Design Token 变量定义:[_variables.less](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/common/style/_variables.less)
```css
/* app.wxss — 全局主题定制 */
page {
--td-brand-color: #0052d9;
--td-brand-color-light: #d9e1ff;
--td-success-color: #00a870;
--td-warning-color: #ed7b2f;
--td-error-color: #e34d59;
/* 文字颜色 */
--td-text-color-primary: rgba(0, 0, 0, 0.9);
--td-text-color-secondary: rgba(0, 0, 0, 0.6);
--td-text-color-placeholder: rgba(0, 0, 0, 0.26);
--td-text-color-disabled: rgba(0, 0, 0, 0.26);
/* 背景颜色 */
--td-bg-color-container: #fff;
--td-bg-color-page: #f3f3f3;
/* 圆角 */
--td-radius-default: 12rpx;
--td-radius-large: 18rpx;
--td-radius-round: 999px;
/* 字体 */
--td-font-size-s: 24rpx;
--td-font-size-base: 28rpx;
--td-font-size-m: 32rpx;
--td-font-size-l: 36rpx;
}
```
## 深色模式
TDesign 1.3.0+ 支持深色模式。
### 开启步骤
1. `app.json` 添加 `"darkmode": true`
2. `app.wxss` 引入主题变量:
```css
@import 'miniprogram_npm/tdesign-miniprogram/common/style/theme/_index.wxss';
```
3. 页面样式使用 CSS Variable
```css
.text {
color: var(--td-text-color-secondary);
}
```
### 特殊组件适配
自定义 TabBar 和 root-portal 内的组件需手动添加 `.page` 类名:
```xml
<view class="page">
<t-tab-bar />
</view>
```
## 在线查询
如需查看某个具体组件的完整 APIProps / Events / Slots / CSS Variables可抓取
- 组件总览https://tdesign.tencent.com/miniprogram/overview
- 具体组件:`https://tdesign.tencent.com/miniprogram/components/{组件名}`
例如https://tdesign.tencent.com/miniprogram/components/button
- 快速开始https://tdesign.tencent.com/miniprogram/getting-started
- 样式覆盖https://tdesign.tencent.com/miniprogram/custom-style
- 自定义主题https://tdesign.tencent.com/miniprogram/custom-theme
- 深色模式https://tdesign.tencent.com/miniprogram/dark-mode
- CSS 变量定义https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/common/style/_variables.less

View File

@@ -0,0 +1,308 @@
# 视图层WXML / WXSS / WXS
> 官方文档https://developers.weixin.qq.com/miniprogram/dev/framework/view/
## WXML — 模板语法
### 数据绑定
```xml
<!-- 简单绑定 -->
<view>{{ message }}</view>
<!-- 组件属性绑定(需在双引号内) -->
<view id="item-{{id}}"></view>
<!-- 控制属性绑定 -->
<view wx:if="{{condition}}"></view>
<!-- 关键字绑定(注意 true/false 需要在 {{}} 内) -->
<checkbox checked="{{false}}"></checkbox>
<!-- ⚠️ 错误写法checked="false" 会被当作字符串 "false",结果为 true -->
<!-- 运算 -->
<view>{{ a + b }} + {{ c }} + d</view>
<view>{{"hello " + name}}</view>
<view>{{object.key}} {{array[0]}}</view>
<!-- 三元运算 -->
<view hidden="{{flag ? true : false}}">Hidden</view>
<!-- 组合(数组) -->
<view wx:for="{{[zero, 1, 2, 3, 4]}}">{{item}}</view>
<!-- 组合(对象) -->
<template is="objectCombine" data="{{for: a, bar: b}}"></template>
<!-- 展开运算符 -->
<template is="objectCombine" data="{{...obj1, ...obj2, e: 5}}"></template>
```
### 列表渲染 wx:for
```xml
<!-- 基本用法:默认 item 和 index -->
<view wx:for="{{array}}">
{{index}}: {{item.message}}
</view>
<!-- 自定义变量名 -->
<view wx:for="{{array}}" wx:for-index="idx" wx:for-item="itemName">
{{idx}}: {{itemName.message}}
</view>
<!-- 嵌套 -->
<view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9]}}" wx:for-item="i">
<view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9]}}" wx:for-item="j">
<view wx:if="{{i <= j}}">
{{i}} * {{j}} = {{i * j}}
</view>
</view>
</view>
<!-- ⚠️ wx:key 非常重要,用于列表项的唯一标识 -->
<!-- 值为字符串item 的某个 property 名(该 property 值需唯一) -->
<switch wx:for="{{objectArray}}" wx:key="unique">{{item.id}}</switch>
<!-- 值为 *thisitem 本身是唯一字符串或数字 -->
<switch wx:for="{{numberArray}}" wx:key="*this">{{item}}</switch>
```
**wx:key 的作用**:当数据改变触发重新渲染时,带有 key 的组件会被重新排序而非重新创建,保持自身状态(如 `<input>` 的输入内容、`<switch>` 的选中状态)。
### 条件渲染
```xml
<!-- wx:if / wx:elif / wx:else -->
<view wx:if="{{length > 5}}">1</view>
<view wx:elif="{{length > 2}}">2</view>
<view wx:else>3</view>
<!-- block wx:if不会渲染为真实 DOM -->
<block wx:if="{{true}}">
<view>view1</view>
<view>view2</view>
</block>
```
**wx:if vs hidden**
- `wx:if` 是惰性的,条件为 false 时不渲染,切换时销毁/重建
- `hidden` 始终渲染,只是切换显示/隐藏(类似 CSS display:none
- 频繁切换用 `hidden`,运行时条件不大可能改变用 `wx:if`
### 模板 template
```xml
<!-- 定义模板 -->
<template name="msgItem">
<view>
<text>{{index}}: {{msg}}</text>
<text>Time: {{time}}</text>
</view>
</template>
<!-- 使用模板 -->
<template is="msgItem" data="{{...item}}"/>
<!-- 动态模板名 -->
<template is="{{item % 2 == 0 ? 'even' : 'odd'}}"/>
```
### 引用
```xml
<!-- import引入目标文件中定义的 template -->
<import src="item.wxml"/>
<template is="item" data="{{text: 'forbar'}}"/>
<!-- ⚠️ import 有作用域:只会引入目标文件中定义的 template不会引入目标文件 import 的 template不递归 -->
<!-- include将目标文件除 <template/> <wxs/> 外的整个代码引入 -->
<include src="header.wxml"/>
<view>body</view>
<include src="footer.wxml"/>
```
## WXSS — 样式
### rpx 单位
rpxresponsive pixel可以根据屏幕宽度进行自适应。规定屏幕宽为 750rpx。
| 设备 | rpx 换算 px | px 换算 rpx |
|------|------------|------------|
| iPhone5 | 1rpx = 0.42px | 1px = 2.34rpx |
| iPhone6 | 1rpx = 0.5px | 1px = 2rpx |
| iPhone6 Plus | 1rpx = 0.552px | 1px = 1.81rpx |
**建议**:开发时以 iPhone6 为视觉稿标准750px 宽1px = 1rpx。
### 样式导入
```css
/* common.wxss */
.small-p { padding: 5px; }
/* app.wxss */
@import "common.wxss";
.middle-p { padding: 15px; }
```
### 选择器支持
| 选择器 | 示例 | 说明 |
|--------|------|------|
| .class | `.intro` | 类选择器 |
| #id | `#firstname` | ID 选择器 |
| element | `view` | 元素选择器 |
| element, element | `view, checkbox` | 群组选择器 |
| ::after | `view::after` | 伪元素 |
| ::before | `view::before` | 伪元素 |
### 内联样式
```xml
<!-- style动态样式运行时解析尽量避免静态样式写在 style 中 -->
<view style="color:{{color}};"/>
<!-- class静态样式写在 class 中 -->
<view class="normal_view"/>
```
### 全局样式与局部样式
- `app.wxss` 为全局样式,作用于每个页面
- 页面的 `.wxss` 只对当前页面生效,会覆盖 `app.wxss` 中相同的选择器
## WXSWeiXin Script
WXS 是小程序的一套脚本语言,可以在 WXML 中使用。**WXS 运行在视图层**,比 JS 逻辑层快(不需要跨线程通信)。
```xml
<!-- 内联 WXS -->
<wxs module="m1">
var msg = "hello world";
module.exports.message = msg;
</wxs>
<view>{{m1.message}}</view>
<!-- 外部 WXS 文件 -->
<wxs src="./tools.wxs" module="tools"/>
<view>{{tools.msg}}</view>
<view>{{tools.bar(tools.FOO)}}</view>
```
```javascript
// tools.wxs
var foo = "'hello world' from tools.wxs"
var bar = function(d) {
return d
}
module.exports = {
FOO: foo,
bar: bar,
}
// ⚠️ WXS 不支持 ES6 语法箭头函数、let/const、解构等
// ⚠️ WXS 中不能调用小程序 APIwx.xxx
```
**WXS 典型用途**
- 格式化数据(日期、金额、文本截断)
- 在视图层做简单计算,避免 setData 开销
- 响应事件WXS 事件响应iOS 上性能更好)
## 事件系统
### 事件分类
| 类型 | 说明 | 示例 |
|------|------|------|
| 冒泡事件 | 向父节点传递 | tap, longpress, touchstart, touchmove, touchend, touchcancel |
| 非冒泡事件 | 不向父节点传递 | submit, input, scroll 等组件特有事件 |
### 事件绑定
```xml
<!-- bind不阻止冒泡 -->
<view bindtap="handleTap">Click me</view>
<view bind:tap="handleTap">Click me</view>
<!-- catch阻止冒泡 -->
<view catchtap="handleTap">Click me</view>
<!-- mut-bind互斥事件绑定基础库 2.8.2+ -->
<view mut-bind:tap="handleTap">
<button mut-bind:tap="handleButtonTap">按钮</button>
</view>
<!-- 同一冒泡路径上的 mut-bind 只会有一个被触发 -->
<!-- capture-bind捕获阶段绑定 -->
<view capture-bind:tap="handleCapture">
<view bindtap="handleTap">inner</view>
</view>
<!-- capture-catch捕获阶段中断 -->
<view capture-catch:tap="handleCapture">
<view bindtap="handleTap">inner不会触发</view>
</view>
```
### 事件对象
```javascript
Page({
handleTap(e) {
e.type // 事件类型,如 "tap"
e.timeStamp // 事件生成时的时间戳
e.target // 触发事件的源组件(可能是子组件)
e.currentTarget // 事件绑定的当前组件
e.detail // 额外信息,如 tap 的 { x, y }
e.touches // 触摸事件的触摸点信息数组
e.changedTouches // 变化的触摸点信息数组
e.mark // 事件标记(基础库 2.7.1+
// target 和 currentTarget 的区别
e.target.id // 触发事件的组件 id
e.target.dataset // 触发事件的组件的 data-xxx 属性集合
e.currentTarget.id // 绑定事件的组件 id
e.currentTarget.dataset
}
})
```
### dataset
```xml
<!-- data-xxx 属性传递数据 -->
<view data-alpha-beta="1" data-alphaBeta="2" bindtap="handleTap">
Click
</view>
```
```javascript
handleTap(e) {
e.currentTarget.dataset.alphaBeta // "1"(连字符转驼峰)
e.currentTarget.dataset.alphabeta // "2"(大写转小写)
}
```
### mark基础库 2.7.1+
```xml
<!-- mark 可以在冒泡路径上所有节点收集 -->
<view mark:myMark="last" bindtap="bindViewTap">
<button mark:anotherMark="leaf" bindtap="bindButtonTap">按钮</button>
</view>
```
```javascript
bindButtonTap(e) {
e.mark // { myMark: "last", anotherMark: "leaf" }
// mark 会合并冒泡路径上所有的 mark
}
```
## 在线查询
如需更详细信息,可抓取:
- WXML 语法https://developers.weixin.qq.com/miniprogram/dev/reference/wxml/
- WXSShttps://developers.weixin.qq.com/miniprogram/dev/framework/view/wxss.html
- WXShttps://developers.weixin.qq.com/miniprogram/dev/reference/wxs/
- 事件https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/event.html