Files
Neo-ZQYY/apps/miniprogram - 副本/doc/h5-to-miniprogram-pitfalls.md

477 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# H5 → 微信小程序转换避坑指南
> 基于本项目 `docs/h5_ui/` 原型与 `apps/miniprogram/miniprogram/pages/` 已转换页面的实际对比,结合微信小程序官方文档整理。
> 适用于后续页面开发(如 board-coach、task-list、customer-detail 等)的快速参考。
---
## 一、WXML vs HTML — 标签与结构
### 1.1 标签映射表
| H5 (HTML) | 小程序 (WXML) | 说明 |
|---------------------|----------------------|------|
| `<div>` | `<view>` | 最基础的容器 |
| `<span>` / `<p>` | `<text>` | 文本必须用 `<text>` 包裹才可选中/换行 |
| `<a href="...">` | `<navigator url="">` | 或用 `wx.navigateTo()` 编程式跳转 |
| `<img src="...">` | `<image src="" mode="">` | 必须指定 `mode`,默认 320×240 |
| `<input>` | `<input>``<t-input>` | 原生 input 事件名不同;推荐 TDesign |
| `<textarea>` | `<textarea>``<t-textarea>` | 同上 |
| `<button>` | `<button>``<t-button>` | 小程序 button 有 `open-type` 能力 |
| `<ul>/<li>` | `<view wx:for="{{list}}">` | 没有列表语义标签 |
| `<select>` | `<picker>``<t-picker>` | 完全不同的交互模式 |
| `<label for="id">` | `<label for="id">` | 支持,但 for 只能绑定 checkbox/radio/switch |
| `<svg>` 内联 | `<image src="xx.svg">` | 不支持内联 SVG只能作为图片引用 |
| `<iframe>` | `<web-view>` | 需配置业务域名白名单 |
### 1.2 实际对比login 页面
**H5 原型** — 内联 SVG 图标:
```html
<svg class="w-14 h-14 text-white" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10" .../>
</svg>
```
**小程序转换** — 改为 image 引用:
```xml
<image class="logo-icon" src="/assets/icons/logo-billiard.svg" mode="aspectFit" />
```
> **坑**:小程序不支持内联 SVG。所有 SVG 图标需提取为独立 `.svg` 文件放到 `assets/icons/`,通过 `<image>` 引用。
### 1.3 不存在的标签/属性
| H5 特性 | 小程序替代方案 |
|---------|---------------|
| `<h1>`~`<h6>` | `<text>` + 样式类 |
| `<table>` | `<view>` 手动布局 |
| `<form>` + `<input name>` | 小程序 `<form>` 或直接 `setData` 收集 |
| `onclick="fn()"` | `bindtap="fn"``bind:tap="fn"` |
| `class="a b c"` 动态 | `class="base {{condition ? 'a' : 'b'}}"` |
| `innerHTML` | `<rich-text nodes="{{html}}">` |
---
## 二、WXSS vs CSS — 样式差异
### 2.1 支持的选择器(有限)
| 选择器 | 支持 | 说明 |
|--------|------|------|
| `.class` | ✅ | |
| `#id` | ✅ | |
| `element` (如 `view`) | ✅ | |
| `element, element` | ✅ | 群组选择器 |
| `::before` / `::after` | ✅ | |
| `*` 通配符 | ❌ | Tailwind 的 `*` reset 全部失效 |
| `>` 子选择器 | ⚠️ | 部分版本支持,不推荐依赖 |
| `+` / `~` 兄弟选择器 | ⚠️ | 同上 |
| `:nth-child()` | ⚠️ | 部分支持 |
| `@media` | ✅ | 支持,但用 rpx 更好 |
### 2.2 rpx 单位 — 最大差异
H5 用 `px`/`rem`/`vw`,小程序用 `rpx`responsive pixel
- 屏幕宽度固定 = 750rpx
- iPhone6 上 1rpx = 0.5px,即 1px = 2rpx
- 设计稿以 750px 宽为基准时,数值直接写 rpx
**实际对比login 页面**
H5 原型Tailwind
```html
<div class="w-24 h-24 rounded-3xl"> <!-- 96px × 96px -->
```
小程序转换:
```css
.logo-box {
width: 192rpx; /* 96px × 2 = 192rpx */
height: 192rpx;
border-radius: 48rpx;
}
```
> **换算规则**H5 的 px 值 × 2 = rpx 值(基于 750 宽设计稿)。
### 2.2.1 H5 → 小程序全局缩放比例87.5%
H5 原型基于 375px 视口设计iPhone SE/6/7/8直接 ×2 转 rpx 后在大屏手机iPhone 15 Pro Max430pt 宽)上元素偏大。经实测对比,对所有 rpx 值统一乘以 0.875 缩放系数后视觉效果最佳。
**规则**
- 尺寸、间距、圆角、阴影偏移:`H5 px × 2 × 0.875` = 最终 rpx取偶数
- 字号:同上规则,如 `text-2xl`(24px) → 48rpx × 0.875 = 42rpx
- t-icon size同上规则`w-5`(20px) → 40rpx × 0.875 = 35rpx
- `max-width` 等约束宽度:同上规则
- 背景纹理间距(如十字纹 `bg-pattern`):不缩放,保持原值
**换算速查**(常用 Tailwind 值):
| Tailwind | H5 px | 原始 rpx | ×0.875 | 取整 rpx |
|----------|-------|----------|--------|----------|
| `text-xs` (12px) | 12 | 24 | 21 | 22 |
| `text-sm` (14px) | 14 | 28 | 24.5 | 24 |
| `text-base` (16px) | 16 | 32 | 28 | 28 |
| `text-lg` (18px) | 18 | 36 | 31.5 | 32 |
| `text-xl` (20px) | 20 | 40 | 35 | 36 |
| `text-2xl` (24px) | 24 | 48 | 42 | 42 |
| `gap-2` / `p-2` (8px) | 8 | 16 | 14 | 14 |
| `gap-3` / `p-3` (12px) | 12 | 24 | 21 | 22 |
| `gap-4` / `p-4` (16px) | 16 | 32 | 28 | 28 |
| `gap-5` / `p-5` (20px) | 20 | 40 | 35 | 36 |
| `gap-6` / `p-6` (24px) | 24 | 48 | 42 | 42 |
| `gap-8` / `p-8` (32px) | 32 | 64 | 56 | 56 |
| `w-10` / `h-10` (40px) | 40 | 80 | 70 | 70 |
| `w-14` / `h-14` (56px) | 56 | 112 | 98 | 98 |
| `w-24` / `h-24` (96px) | 96 | 192 | 168 | 168 |
| `w-28` / `h-28` (112px) | 112 | 224 | 196 | 196 |
| `rounded-xl` (12px) | 12 | 24 | 21 | 22 |
| `rounded-2xl` (16px) | 16 | 32 | 28 | 28 |
| `rounded-3xl` (24px) | 24 | 48 | 42 | 42 |
> **来源**no-permission 页面实测确定。先后尝试了非统一缩放、80%、87.5% 三种方案87.5% 在 iPhone 15 Pro Max 上与 H5 原型视觉一致度最高。后续所有页面转换统一使用此系数。
### 2.3 Tailwind CSS → 手写 WXSS
小程序不支持 Tailwind CSS无构建链集成所有 Tailwind 工具类必须手写为 WXSS。
| Tailwind 类 | WXSS 等价写法 |
|-------------|--------------|
| `flex flex-col items-center` | `display: flex; flex-direction: column; align-items: center;` |
| `gap-4` | `gap: 32rpx;`4 × 8px × 2rpx |
| `p-5` | `padding: 40rpx;` |
| `rounded-2xl` | `border-radius: 32rpx;` |
| `text-sm` | `font-size: 28rpx;` |
| `text-gray-7` | `color: #8b8b8b;` |
| `bg-white/60` | `background: rgba(255,255,255,0.6);` |
| `backdrop-blur-sm` | ❌ 不支持 `backdrop-filter` |
| `shadow-lg` | `box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.06);` |
| `min-h-screen` | `min-height: 100vh;` |
### 2.4 不支持/有差异的 CSS 特性
| CSS 特性 | 小程序支持情况 | 替代方案 |
|----------|---------------|---------|
| `backdrop-filter: blur()` | ❌ 不支持 | 用半透明背景色模拟 |
| `position: fixed` | ⚠️ 部分场景异常 | 用 `position: sticky` 或组件自带吸顶 |
| `@import url()` 远程 | ❌ | 只支持本地 `@import "path.wxss"` |
| `@font-face` 远程字体 | ⚠️ | 需 `wx.loadFontFace()` 动态加载 |
| CSS 变量 `var()` | ✅ 支持 | TDesign 大量使用 |
| `linear-gradient` | ✅ 支持 | 正常使用 |
| `animation` / `@keyframes` | ✅ 支持 | 正常使用 |
| `transition` | ✅ 支持 | 正常使用 |
| `env(safe-area-inset-*)` | ✅ 支持 | 刘海屏适配必用 |
### 2.5 样式作用域
- `app.wxss` = 全局样式
- 页面 `.wxss` = 仅当前页面生效(自动隔离)
- 组件 `.wxss` = 默认隔离(`styleIsolation: 'isolated'`
> **坑**H5 的全局 CSS reset如 `* { box-sizing: border-box; }`)在小程序中无效。需要在每个需要的元素上手动设置 `box-sizing`。
---
## 三、事件系统 — 最容易踩坑
### 3.1 事件绑定对比
| H5 | 小程序 | 说明 |
|----|--------|------|
| `onclick="fn()"` | `bindtap="fn"` | 不能传参! |
| `onclick="fn(1)"` | `data-id="1" bindtap="fn"` | 通过 dataset 传参 |
| `addEventListener` | 不支持 | 只能在 WXML 中声明式绑定 |
| `event.target.value` | `e.detail.value` | 取值路径不同 |
| `event.preventDefault()` | `catchtap` | 用 catch 前缀阻止冒泡 |
| `event.stopPropagation()` | `catchtap` | 同上 |
### 3.2 传参方式
**H5**
```html
<button onclick="handleClick(item.id, item.name)">点击</button>
```
**小程序**
```xml
<view data-id="{{item.id}}" data-name="{{item.name}}" bindtap="handleClick">点击</view>
```
```typescript
handleClick(e: WechatMiniprogram.TouchEvent) {
const { id, name } = e.currentTarget.dataset
}
```
> **坑**`data-` 属性名会自动转换 — 连字符转驼峰(`data-user-id` → `dataset.userId`),大写转小写(`data-userId` → `dataset.userid`)。
### 3.3 实际对比login 页面的协议勾选
**H5 原型**
```html
<input type="checkbox" id="agreeCheckbox" onchange="updateButtonState()">
<script>
checkbox.addEventListener('change', updateButtonState);
</script>
```
**小程序转换**
```xml
<view class="agreement" bindtap="onAgreeChange">
<view class="checkbox {{agreed ? 'checkbox--checked' : ''}}">
<t-icon wx:if="{{agreed}}" name="check" size="20rpx" color="#fff" />
</view>
</view>
```
```typescript
onAgreeChange() {
this.setData({ agreed: !this.data.agreed })
}
```
> **坑**:小程序没有原生 checkbox 的 `checked` 双向绑定,需要手动用 `setData` + 条件样式类模拟。
---
## 四、数据绑定与渲染
### 4.1 模板语法对比
| 功能 | H5 (原生/框架) | 小程序 WXML |
|------|----------------|-------------|
| 插值 | `${variable}` / `{{variable}}` | `{{variable}}` |
| 条件渲染 | `if/else` + DOM 操作 | `wx:if` / `wx:elif` / `wx:else` |
| 列表渲染 | `forEach` + `innerHTML` | `wx:for="{{list}}" wx:key="id"` |
| 显示/隐藏 | `style.display = 'none'` | `hidden="{{!show}}"``wx:if` |
| 动态 class | `classList.toggle()` | `class="base {{active ? 'on' : ''}}"` |
| 动态 style | `element.style.color = 'red'` | `style="color: {{color}};"` |
### 4.2 wx:if vs hidden
```xml
<!-- wx:if条件为 false 时不渲染 DOM切换时销毁/重建 -->
<view wx:if="{{status === 'pending'}}">审核中</view>
<!-- hidden始终渲染只切换 display -->
<view hidden="{{status !== 'pending'}}">审核中</view>
```
- 频繁切换 → 用 `hidden`(避免重复创建销毁)
- 初始条件不太可能变 → 用 `wx:if`(减少初始渲染量)
### 4.3 实际对比reviewing 页面的条件渲染
**H5 原型**:只有一种状态(审核中),用静态 HTML。
**小程序转换**:支持 pending/rejected 两种状态,用 `wx:if` 动态切换:
```xml
<view class="top-gradient top-gradient--{{status}}"></view>
<t-icon wx:if="{{status === 'pending'}}" name="time" size="112rpx" color="#fff" />
<t-icon wx:else name="close-circle" size="112rpx" color="#fff" />
<text class="main-title">{{status === 'pending' ? '申请审核中' : '申请未通过'}}</text>
```
> **坑**`wx:if` 中的表达式必须在 `{{}}` 内,且不支持复杂 JS 表达式(如函数调用)。需要复杂逻辑时用 WXS 或在 JS 中预处理好数据。
---
## 五、路由与导航
### 5.1 对比
| H5 | 小程序 | 说明 |
|----|--------|------|
| `window.location.href = 'xx.html'` | `wx.navigateTo({ url: '/pages/xx/xx' })` | 保留当前页,跳新页 |
| `window.location.replace()` | `wx.redirectTo()` | 关闭当前页,跳新页 |
| `history.back()` | `wx.navigateBack()` | 返回上一页 |
| 无直接等价 | `wx.reLaunch()` | 关闭所有页面,打开新页 |
| 无直接等价 | `wx.switchTab()` | 跳转 tabBar 页面 |
### 5.2 实际对比reviewing 页面的"更换账号"
**H5 原型**
```javascript
function switchAccount() {
localStorage.clear();
window.location.href = 'login.html';
}
```
**小程序转换**
```typescript
onSwitchAccount() {
const app = getApp<IAppOption>()
app.globalData.token = undefined
wx.removeStorageSync("token")
wx.removeStorageSync("refreshToken")
wx.reLaunch({ url: "/pages/login/login" }) // 清空页面栈
}
```
> **坑**
> - 页面栈最多 10 层,`navigateTo` 超过会静默失败
> - 跳转 tabBar 页面必须用 `switchTab`,用 `navigateTo` 会报错
> - 路径必须以 `/` 开头,且不带 `.wxml` 后缀
> - `reLaunch` 会销毁所有页面,适合登录/登出等场景
---
## 六、存储与网络
### 6.1 本地存储
| H5 | 小程序 | 说明 |
|----|--------|------|
| `localStorage.setItem(k, v)` | `wx.setStorageSync(k, v)` | 同步写入 |
| `localStorage.getItem(k)` | `wx.getStorageSync(k)` | 同步读取 |
| `localStorage.removeItem(k)` | `wx.removeStorageSync(k)` | 同步删除 |
| `localStorage.clear()` | `wx.clearStorageSync()` | 清空全部 |
| 上限 ~5MB | 上限 10MB | 小程序更大 |
> **坑**:小程序没有 Cookie登录态必须自行通过 Storage + header token 管理。
### 6.2 网络请求
| H5 | 小程序 | 说明 |
|----|--------|------|
| `fetch()` / `XMLHttpRequest` | `wx.request()` | 需配置域名白名单 |
| 无限制 | 并发上限 10 个 | 超出排队 |
| 任意域名 | 必须 HTTPS + 白名单 | 开发时可关闭校验 |
---
## 七、TDesign 组件替代 H5 原生元素
本项目使用 TDesign 小程序组件库,以下是常见替代关系:
| H5 原型元素 | TDesign 组件 | 注意事项 |
|-------------|-------------|---------|
| `<button>` | `<t-button>` | 用 CSS 变量定制样式,如 `--td-button-large-height` |
| `<input>` | `<t-input>` | 事件是 `bind:change` 而非 `bindinput` |
| `<textarea>` | `<t-textarea>` | 同上 |
| `<select>` | `<t-picker>` | 完全不同的交互 |
| `<checkbox>` | `<t-checkbox>` | 或手动实现(如 login 页) |
| `<radio>` | `<t-radio>` / `<t-radio-group>` | `bind:change` 取值 |
| SVG 图标 | `<t-icon name="xxx">` | TDesign 内置图标库 |
| 加载动画 | `<t-loading>` | 替代 CSS spinner |
| 弹窗 | `<t-dialog>` / `<t-toast>` | 替代 `alert()` / `confirm()` |
| 下拉刷新 | 页面 `onPullDownRefresh` | 在 page.json 中 `enablePullDownRefresh: true` |
### TDesign 样式覆盖 4 种方式
1. **CSS 变量**(推荐):`--td-button-large-height: 96rpx`
2. **外部样式类**`t-class="my-class"` + `.my-class { ... !important }`
3. **解除隔离**TDesign 已开启 `addGlobalClass`,页面样式可直接覆盖
4. **style 属性**`style="background: #f5f5f5; border-radius: 16rpx;"`
**实际示例**login 页面按钮定制):
```css
.login-btn {
--td-button-large-height: 96rpx !important;
--td-button-large-font-size: 32rpx !important;
--td-button-border-radius: 24rpx !important;
}
.login-btn--active {
background: linear-gradient(135deg, #0052d9, #3b82f6) !important;
box-shadow: 0 12rpx 32rpx rgba(0, 82, 217, 0.3);
}
```
---
## 八、高频踩坑清单
### 8.1 结构层
| # | 坑 | 说明 | 解决方案 |
|---|-----|------|---------|
| 1 | 内联 SVG 不支持 | WXML 不能写 `<svg>` 标签 | 提取为 `.svg` 文件,用 `<image>``<t-icon>` |
| 2 | 没有 DOM API | `document.getElementById` 等全部不可用 | 用 `this.setData()` 驱动视图更新 |
| 3 | `<text>` 内只能嵌套 `<text>` | 不能在 `<text>` 内放 `<view>` | 需要块级布局时外层用 `<view>` |
| 4 | `checked="false"` 是 true | 字符串 `"false"` 是 truthy | 必须写 `checked="{{false}}"` |
| 5 | `wx:key` 必须提供 | 列表渲染不加 key 会警告且性能差 | `wx:key="id"``wx:key="*this"` |
| 6 | `<block>` 不渲染 DOM | 只是逻辑包裹,不产生真实节点 | 需要样式时改用 `<view>` |
### 8.2 样式层
| # | 坑 | 说明 | 解决方案 |
|---|-----|------|---------|
| 7 | `*` 选择器无效 | 全局 reset 失效 | 逐个元素设置 `box-sizing` |
| 8 | `backdrop-filter` 不支持 | 毛玻璃效果无法实现 | 用半透明背景色 `rgba()` 近似 |
| 9 | `image` 默认 320×240 | 不设宽高会变形 | 始终指定 `width`/`height` + `mode` |
| 10 | rpx 小数精度 | 1rpx 在某些设备上不显示 | 边框最小用 2rpx |
| 11 | 组件样式隔离 | 页面样式穿不进自定义组件 | 用外部样式类或 `styleIsolation: 'shared'` |
| 12 | `!important` 滥用 | TDesign 组件内部样式优先级高 | 优先用 CSS 变量覆盖 |
### 8.3 逻辑层
| # | 坑 | 说明 | 解决方案 |
|---|-----|------|---------|
| 13 | `setData` 性能 | 大数据量传输卡顿 | 只传变化字段:`'list[2].name': 'new'` |
| 14 | 没有 Cookie | 登录态不能靠 Cookie | Storage + header token |
| 15 | `eval()` 不可用 | 动态代码执行被禁止 | 预编译逻辑 |
| 16 | 页面栈 10 层限制 | `navigateTo` 超过 10 层静默失败 | 合理使用 `redirectTo` / `reLaunch` |
| 17 | `alert()` 不存在 | 没有浏览器弹窗 API | `wx.showToast()` / `wx.showModal()` |
| 18 | `window` / `document` 不存在 | 所有 Web API 不可用 | 用 `wx.*` API 替代 |
### 8.4 TDesign 相关
| # | 坑 | 说明 | 解决方案 |
|---|-----|------|---------|
| 19 | `style: v2` 冲突 | app.json 中的 `"style": "v2"` 导致样式错乱 | 删除该配置 |
| 20 | npm 构建遗忘 | 安装新包后忘记构建 npm | 每次 `npm install` 后在开发者工具中"构建 npm" |
| 21 | 事件名差异 | TDesign 用 `bind:change`,原生用 `bindinput` | 查阅组件文档确认事件名 |
| 22 | 外部样式类命名 | `t-class` / `t-class-input` 等各组件不同 | 查阅组件文档的 External Classes |
---
## 九、转换 Checklist新页面开发用
开发新页面时,按此清单逐项检查:
- [ ] HTML 标签全部替换为 WXML 组件(`div→view``span→text``img→image`
- [ ] 内联 SVG 提取为文件,改用 `<image>``<t-icon>`
- [ ] Tailwind 类全部手写为 WXSSpx × 2 × 0.875 = rpx见 §2.2.1 缩放规则)
- [ ] `backdrop-filter` 等不支持的 CSS 改为替代方案
- [ ] 事件绑定改为 `bindtap` / `bind:change`,传参用 `data-*`
- [ ] `alert/confirm` 改为 `wx.showToast` / `wx.showModal`
- [ ] `localStorage` 改为 `wx.setStorageSync`
- [ ] 路由跳转改为 `wx.navigateTo` / `wx.reLaunch`
- [ ] 表单收集改为 `setData` + 事件回调
- [ ] 图片设置 `mode` 属性(`aspectFit` / `aspectFill` / `widthFix`
- [ ] 列表渲染加 `wx:key`
- [ ] 布尔属性用 `{{}}` 包裹(`checked="{{true}}"`
- [ ] TDesign 组件在页面 `.json` 中注册 `usingComponents`
- [ ] 安全区适配:`padding-top: env(safe-area-inset-top)`
- [ ] 页面配置:`enablePullDownRefresh``navigationBarTitleText`
---
## 十、board-customer 迁移经验补充
> 来源board-customer 页面 8 维度卡片迁移实战2026-03-07
### 10.1 复杂维度用独立布局
最专一维度的助教明细表不适合用通用的 `card-mid-row``card-grid`,直接用独立的 `loyal-table` 布局(左侧竖线 `border-left: 4rpx solid #eee` + 表头 + 数据行)。同时在助教行的 `wx:if` 中排除 `dimType !== 'loyal'`,避免信息重复。
**关键**:当某个维度的卡片结构与其他维度差异过大时,不要硬套通用模板,直接写独立布局更清晰。
### 10.2 heart-icon 组件TS observer 替代 WXS
小程序 WXS 不支持 emoji surrogate pair`\uD83D\uDC96`),渲染为乱码。解决方案:
- 用 TS `observers` 监听 `score` 属性变化,计算对应 emoji 字符串
- WXML 中用 `{{heartEmoji}}` 数据绑定渲染
- 样式:`font-size: 22rpx; line-height: 1; position: relative; top: -4rpx` 和文字对齐
### 10.3 助教字体颜色三态 + badge 渐变
助教名字颜色规则(通过 CSS class 控制):
- 跟 badge → `.assistant--assignee``color: #e34d59; font-weight: 700`(红色加粗)
- 弃 badge → `.assistant--abandoned``color: #a6a6a6`(灰色)
- 无 badge → `.assistant--normal``color: #242424`(黑色)
Badge 样式(白字 + 渐变背景 + 阴影):
- 跟:`background: linear-gradient(135deg, #e34d59, #f26a76); box-shadow: 0 2rpx 6rpx rgba(227,77,89,0.28)`
- 弃:`background: linear-gradient(135deg, #d4d4d4, #b6b6b6); box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.14)`