微信小程序一键迁移 uni-app 跨端实践:36个页面从 WXML 到 Vue 的完整改造记录 记录了一个 36+ 页面的微信小程序原生项目完整迁移到 uni-app(Vue 3 + Vite)的全过程,涵盖 wx→uni API 替换、模板语法转换、平台差异处理等实战经验。
- uni-app
- 小程序
- Vue
- 跨端开发
- 前端架构
背景
最近接手了一个民宿旅居预订平台的微信小程序项目——"中游链",36+ 个页面,80+ 个后端接口,完整的预订、优惠券、分销、电子名片系统。项目原生微信小程序(WXML + WXSS + JS)编写的,仅支持微信端。
需求很明确:App 端也要上,而且后续可能还要 H5。这意味着需要从原生小程序迁移到跨端方案。
选型上最终选择了 uni-app(Vue 3 + Vite),原因有三:
- API 高度兼容:
wx.*到uni.*的映射几乎是 1:1 - 条件编译:平台差异化代码通过
#ifdef处理,不需要维护多套代码 - HBuilderX 生态:发布、打包、云服务一体化
迁移策略:不破不立
项目有一半的逻辑层代码(utils + services + app.js)是纯 JS,理论上可以复用,但 WXML 模板和 WXSS 样式必须重写。
最终决定新建 uni-app 项目,逐步迁移。原项目保留不动,新项目从零搭建基础设施。
整体路线
第一阶段:基础设施搭建
├── 项目脚手架(package.json / vite.config.js / manifest.json)
├── App 入口迁移(app.js 582行 → App.vue)
├── 全局样式迁移(app.wxss 415行 → App.vue style)
├── 工具函数迁移(12个 utils,wx→uni API 替换)
└── API 层迁移(services/api.js 80+接口)
第二阶段:页面迁移
├── 核心页面:首页(831行JS + 333行WXML + 731行WXSS)
├── 复杂页面:详情页 + 预订 + 订单 + 个人中心 + 登录
├── 业务页面:优惠券 + 分销 + 搜索 + 分类
└── 信息页面:帮助 + 协议 + 隐私 + 名片(约25页)
关键迁移技巧
1. wx → uni 的批量替换
这个最简单但也最繁琐。纯文本替换搞定 90%:
| 微信 API | uni-app API |
|---|---|
wx.request | uni.request |
wx.navigateTo | uni.navigateTo |
wx.showToast | uni.showToast |
wx.getStorageSync | uni.getStorageSync |
wx.login | uni.login |
wx.uploadFile | uni.uploadFile |
踩坑提示:wx.getMenuButtonBoundingClientRect() 只能在微信小程序端使用,App 端没有这个概念。需要条件编译处理自定义导航栏的高度计算。
2. WXML → Vue Template 语法对照
这是工作量最大的部分。36 个页面逐个改:
<!-- 微信 WXML -->
<view wx:if="{{show}}">
<text wx:for="{{list}}" wx:key="id" bindtap="onTap">{{item.name}}</text>
</view>
<!-- Vue -->
<view v-if="show">
<text v-for="item in list" :key="item.id" @tap="onTap">{{ item.name }}</text>
</view>
核心变化:
wx:if→v-if(wx:elif→v-else-if,wx:else→v-else)wx:for="{{list}}"→v-for="item in list"wx:key→:keybindtap→@tap,catchtap→@tap.stopbindchange/bindinput/bindconfirm→@change/@input/@confirm<block>→<template>或直接用原生标签
3. Page({}) → Vue Options API
微信小程序的 Page({}) 和 Vue 组件之间有个核心差异:数据响应式。
// 微信小程序
Page({
data: { count: 0 },
methods: {
increment() {
this.setData({ count: this.data.count + 1 })
}
}
})
// Vue
export default {
data() { return { count: 0 } },
methods: {
increment() {
this.count++ // 直接赋值
}
}
}
关键区别:Vue 中不需要 this.setData(),直接修改属性即可触发视图更新。
4. App({}) → App.vue + main.js
微信小程序的 App({}) 需要拆分为 uni-app 的 App.vue 和 main.js:
// main.js
import App from './App'
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
return { app }
}
<!-- App.vue -->
<script setup>
import { onLaunch, onShow } from '@dcloudio/uni-app'
onLaunch(() => {
// 原来 App.onLaunch 中的逻辑
})
</script>
5. 条件编译处理平台差异
这是 uni-app 最大的优势。同样的组件,在不同平台表现不同:
<template>
<!-- 微信小程序:使用 open-type 获取手机号 -->
<!-- #ifdef MP-WEIXIN -->
<button open-type="getPhoneNumber" @getphonenumber="getPhone">
微信一键登录
</button>
<!-- #endif -->
<!-- App 端:使用短信验证码 -->
<!-- #ifdef APP-PLUS -->
<button @tap="showSmsLogin">
短信验证码登录
</button>
<!-- #endif -->
</template>
项目中需要条件编译的部分:
| 模块 | 微信小程序 | App 端 |
|---|---|---|
| 登录 | uni.login() 获取 code | 短信验证码 |
| 支付 | 微信支付 JSAPI | 微信/支付宝支付 |
| 订阅消息 | uni.requestSubscribeMessage | 移除 |
| 版本更新 | getUpdateManager | 移除 |
| 腾讯广告 SDK | @dn-sdk/miniprogram | 移除 |
6. 微信 Behavior → Vue Composable
原项目使用了微信 Behavior 实现装修页面的逻辑复用(表单、倒计时、计算器等)。迁移到 uni-app 后用 Vue 3 的 composable 替代:
// composables/useDecorate.js
import { ref, reactive } from 'vue'
export function useDecorate() {
const formValues = reactive({})
const countdownShow = ref(false)
function onFormInput(e) {
const field = e.currentTarget.dataset.field
formValues[field] = e.detail.value
}
// ... 更多逻辑
return {
formValues, countdownShow,
onFormInput,
// ...
}
}
在页面中使用:
<script>
import { useDecorate } from '@/composables/useDecorate'
export default {
setup() {
const { formValues, onFormInput } = useDecorate()
return { formValues, onFormInput }
}
}
</script>
迁移数据统计
| 维度 | 数据 |
|---|---|
| 页面数量 | 36 个 |
| 迁移代码行数 | ~15000 行 JS + ~8000 行 WXML + ~10000 行 WXSS |
| Vue 文件数量 | 40 个(1 App + 3 组件 + 36 页面) |
| 工具函数 | 14 个文件 |
| API 接口 | 80+ |
| 迁移耗时 | ~4 小时(借助 AI 批量转换 + 手动调优) |
踩坑记录
1. getMenuButtonBoundingClientRect 兼容性
微信小程序的胶囊信息在 App 端不存在,自定义导航栏高度需要兜底:
// #ifdef MP-WEIXIN
const menuButton = uni.getMenuButtonBoundingClientRect()
const navHeight = menuButton.height + (menuButton.top - sysInfo.statusBarHeight) * 2 + sysInfo.statusBarHeight
// #endif
// #ifndef MP-WEIXIN
const navHeight = 88 // 固定值兜底
// #endif
2. <scroll-view> 的 enhanced 属性
微信小程序中 scroll-view 的 enhanced 属性在 App 端不支持,需要移除。
3. 模板中的 {{item}} 冲突
WXML 的 wx:for="{{list}}" 中默认 item 和 index 变量在 Vue 中同样适用,但 Vue 的 v-for 中 item 引用不需要双花括号包裹:
<!-- 错误:WXML 写法混入 Vue -->
<text>{{item.name}}</text>
<!-- item.name 已自动求值,不需要外层 {{}} -->
实际上 Vue 中 {{ expr }} 是文本插值,WXML 中也是 {{ expr }},所以这部分语法是一致的。错误的写法是 wx:for="{{list}}" 这种在 Vue 中应为 v-for="item in list"。
4. wx.requestSubscribeMessage 仅在微信端可用
订阅消息是微信小程序特有的能力,App 端没有这个 API。用条件编译包裹整个 subscribe.js 工具模块。
总结
从原生微信小程序迁移到 uni-app,本质上是一个模板语法转换 + API 适配 + 架构重构的过程。
核心经验:
-
不要试图在原项目上改。原生小程序
Page({})和 Vue 组件的架构差异太大,混用只会更混乱。新建项目逐步迁移是最干净的方案。 -
逻辑层迁移成本远低于视图层。纯 JS 的工具函数和 API 封装只需要
wx→uni替换就能跑,真正的成本在 36 个页面的 WXML → Vue template 转换。 -
条件编译是 uni-app 的灵魂。没有条件编译,跨端项目就会变成 if-else 地狱。用好
#ifdef/#ifndef可以把平台差异控制在最小范围。 -
善用工具批量处理。对于大量重复的模板转换(36 个页面),纯手工改效率极低。结合 AI + 正则批量替换,可以大幅缩短迁移时间。
最终项目已完全跑通微信小程序和 App 端,H5 端也基本可用。代码已开源在 DCloud 插件市场,欢迎交流讨论。