1
This commit is contained in:
255
_DEL/wechat-miniprogram/steering/app-service.md
Normal file
255
_DEL/wechat-miniprogram/steering/app-service.md
Normal 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
|
||||
304
_DEL/wechat-miniprogram/steering/best-practices.md
Normal file
304
_DEL/wechat-miniprogram/steering/best-practices.md
Normal 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.setStorage(10MB) |
|
||||
| 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
|
||||
- 自定义 tabBar:https://developers.weixin.qq.com/miniprogram/dev/framework/ability/custom-tabbar.html
|
||||
364
_DEL/wechat-miniprogram/steering/builtin-components.md
Normal file
364
_DEL/wechat-miniprogram/steering/builtin-components.md
Normal 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 World
|
||||
</text>
|
||||
```
|
||||
- `selectable`:文本是否可选
|
||||
- `space`:显示连续空格(ensp/emsp/nbsp)
|
||||
- `decode`:是否解码(` ` `<` 等)
|
||||
- **注意**:`<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
|
||||
443
_DEL/wechat-miniprogram/steering/custom-component.md
Normal file
443
_DEL/wechat-miniprogram/steering/custom-component.md
Normal 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 识别组件内部 button(2.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
|
||||
188
_DEL/wechat-miniprogram/steering/framework-core.md
Normal file
188
_DEL/wechat-miniprogram/steering/framework-core.md
Normal 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
|
||||
431
_DEL/wechat-miniprogram/steering/frontend-api.md
Normal file
431
_DEL/wechat-miniprogram/steering/frontend-api.md
Normal 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 string;POST 请求 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
|
||||
362
_DEL/wechat-miniprogram/steering/login-auth.md
Normal file
362
_DEL/wechat-miniprogram/steering/login-auth.md
Normal 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
|
||||
302
_DEL/wechat-miniprogram/steering/server-api.md
Normal file
302
_DEL/wechat-miniprogram/steering/server-api.md
Normal 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
|
||||
511
_DEL/wechat-miniprogram/steering/tdesign.md
Normal file
511
_DEL/wechat-miniprogram/steering/tdesign.md
Normal file
@@ -0,0 +1,511 @@
|
||||
# TDesign 小程序组件库
|
||||
|
||||
> 官方文档:https://tdesign.tencent.com/miniprogram/overview
|
||||
> GitHub:https://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>
|
||||
```
|
||||
|
||||
## 在线查询
|
||||
|
||||
如需查看某个具体组件的完整 API(Props / 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
|
||||
308
_DEL/wechat-miniprogram/steering/view-layer.md
Normal file
308
_DEL/wechat-miniprogram/steering/view-layer.md
Normal 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>
|
||||
|
||||
<!-- 值为 *this:item 本身是唯一字符串或数字 -->
|
||||
<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 单位
|
||||
|
||||
rpx(responsive 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` 中相同的选择器
|
||||
|
||||
## WXS(WeiXin 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 中不能调用小程序 API(wx.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/
|
||||
- WXSS:https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxss.html
|
||||
- WXS:https://developers.weixin.qq.com/miniprogram/dev/reference/wxs/
|
||||
- 事件:https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/event.html
|
||||
Reference in New Issue
Block a user