19 KiB
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 图标:
<svg class="w-14 h-14 text-white" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10" .../>
</svg>
小程序转换 — 改为 image 引用:
<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):
<div class="w-24 h-24 rounded-3xl"> <!-- 96px × 96px -->
小程序转换:
.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 Max,430pt 宽)上元素偏大。经实测对比,对所有 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:
<button onclick="handleClick(item.id, item.name)">点击</button>
小程序:
<view data-id="{{item.id}}" data-name="{{item.name}}" bindtap="handleClick">点击</view>
handleClick(e: WechatMiniprogram.TouchEvent) {
const { id, name } = e.currentTarget.dataset
}
坑:
data-属性名会自动转换 — 连字符转驼峰(data-user-id→dataset.userId),大写转小写(data-userId→dataset.userid)。
3.3 实际对比:login 页面的协议勾选
H5 原型:
<input type="checkbox" id="agreeCheckbox" onchange="updateButtonState()">
<script>
checkbox.addEventListener('change', updateButtonState);
</script>
小程序转换:
<view class="agreement" bindtap="onAgreeChange">
<view class="checkbox {{agreed ? 'checkbox--checked' : ''}}">
<t-icon wx:if="{{agreed}}" name="check" size="20rpx" color="#fff" />
</view>
</view>
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
<!-- 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 动态切换:
<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 原型:
function switchAccount() {
localStorage.clear();
window.location.href = 'login.html';
}
小程序转换:
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 种方式
- CSS 变量(推荐):
--td-button-large-height: 96rpx - 外部样式类:
t-class="my-class"+.my-class { ... !important } - 解除隔离:TDesign 已开启
addGlobalClass,页面样式可直接覆盖 - style 属性:
style="background: #f5f5f5; border-radius: 16rpx;"
实际示例(login 页面按钮定制):
.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 类全部手写为 WXSS(px × 2 × 0.875 = rpx,见 §2.2.1 缩放规则)
backdrop-filter等不支持的 CSS 改为替代方案- 事件绑定改为
bindtap/bind:change,传参用data-* alert/confirm改为wx.showToast/wx.showModallocalStorage改为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)