一、引言:为什么要从「纯 Vue3 Web」迁移到 uni-app x?
许多团队已经有了一套成熟的 Vue3 Web 项目(基于 Vite、Vue Router、Pinia 等),跑在浏览器里一切正常。但随着业务发展,往往会遇到这些新需求:
- 需要上线微信/支付宝/抖音等小程序入口;
- 需要有一套「原生 App」承载更重的功能(推送、离线、深度系统能力);
- 维护多套代码(Web、一堆小程序、原生 App)成本太高。
uni-app x 的目标就是:让你继续写 Vue3 + TS 风格的代码,但可以一套工程覆盖 App + 各类小程序 + H5。
因此,对已有 Vue3 项目来说,一个自然的问题就是:
如何在不推倒重来的前提下,尽量平滑地迁移到 uni-app x?
本文将从整体策略、目录结构改造、路由/状态管理适配、组件与 API 替换等方面,给出一套可操作的迁移思路和步骤,并分析过程中可能的坑与注意点。
二、迁移前评估:先搞清楚自己是什么项目
迁移前不要急着动手,先回答几个关键问题:
-
当前项目的技术栈
- 是否使用:Vite、Vue Router、Pinia 或 Vuex、Axios、Element Plus/Ant Design Vue 等?
- 是否大量使用 DOM 直接操作、
window、document等 Web 专属 API?
-
业务复杂度与依赖
- 是否大量依赖第三方 UI 库、图表库(ECharts、AntV)、富文本编辑器、复杂表格等?
- 是否有强 Web 特性(如 iframe、浏览器插件接口、localStorage 逻辑等)?
-
迁移目标平台
- 必须支持哪些:App(iOS/Android)/ 微信小程序 / 其他小程序 / H5?
- 是否对某些平台有特别强的能力诉求(如推送、蓝牙、相机、文件系统)?
-
时间和人力约束
- 能不能接受一段时间的「双线维护(旧 Web + 新 uni-app x)」?
- 是否有安卓/iOS 原生同事能协助插件层能力?
根据评估结果,可以大致判断:
- 适合重用大量业务逻辑,只做外壳改造;
- 还是必须进行较重的架构重构(比如完全脱离 DOM 思维)。
三、迁移总体策略:不要一下子「全搬」,而是分层解耦
从纯 Web(Vue3 SPA)到 uni-app x,本质上是从:
css
Vue3 + Router + Web DOM + 浏览器特性
↓
Vue3 + uni-app x 组件体系 + 多端(App/小程序/H5)
迁移的关键策略是:
先把"与平台强绑定"的部分(路由、UI、API)剥离出来,把"与业务有关"的逻辑、数据、服务层抽出来复用。
可以按「三层架构」来思考:
-
业务逻辑层(可高度复用)
- 接口请求封装(API Service)
- 业务状态管理(Pinia Store)
- 领域模型与工具函数(utils, hooks)
-
页面 & 组件层(部分复用,需要适配)
-
原有的
.vue页面可以搬过去,但需要调整:- DOM 标签 -> uni-app 组件(
div->view,span->text等) - UI 库替换或重构(Element Plus -> 移动端自定义 UI / uni UI 等)
- DOM 标签 -> uni-app 组件(
-
-
基础设施层(需重构)
- 路由:Vue Router -> uni-app 页面路由机制
- 运行环境:浏览器 -> 多端运行(小程序/App/H5)
- 全局入口:
main.ts->App.vue+pages.json
四、实际迁移步骤:从创建 uni-app x 项目开始
4.1 步骤 1:新建一个 uni-app x 项目骨架
使用 HBuilderX 或 CLI 创建一个 uni-app x 项目(以 CLI 为例,命令以官方最新文档为准,下面用伪示例):
bash
# 假设已有相关 CLI 工具
npx degit dcloudio/uni-app-x-starter my-uniappx-app
cd my-uniappx-app
pnpm install # 或 npm/yarn
项目结构通常类似:
css
my-uniappx-app
├─ src
│ ├─ pages
│ │ └─ index
│ │ └─ index.vue
│ ├─ App.vue
│ ├─ main.ts
│ └─ ...
├─ pages.json
├─ manifest.json
└─ ...
先跑通基础项目(例如 H5 或 App 模拟器),确保环境与编译没问题。
4.2 步骤 2:抽取原项目的「可复用业务层」
在原 Vue3 项目中,重点抽离这些:
services/或api/:接口封装stores/:Pinia 或 Vuexutils/:通用工具函数- 纯 TS/JS 模块:与平台无关的业务逻辑
将它们复制到新项目的 src/shared/(或任意你喜欢的目录名),例如:
vbnet
src
├─ shared
│ ├─ api
│ │ └─ user.ts
│ ├─ stores
│ │ └─ user.ts
│ ├─ utils
│ │ └─ date.ts
│ └─ types
│ └─ user.ts
├─ pages
│ └─ index
│ └─ index.vue
└─ ...
4.2.1 网络请求封装适配
如果原来使用 axios,有两种做法:
- 做一个轻薄的适配层 :内部根据运行环境调用
uni.request或fetch - 或者直接改用
uni.request+ 自己封装
示例(简化版):
typescript
// src/shared/api/request.ts
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
interface RequestOptions<T = any> {
url: string
method?: HttpMethod
data?: Record<string, any>
headers?: Record<string, string>
}
export function request<T = any>(options: RequestOptions): Promise<T> {
const { url, method = 'GET', data, headers } = options
return new Promise((resolve, reject) => {
uni.request({
url,
method,
data,
header: headers,
success: (res) => {
// 根据你后端返回格式处理
const data = res.data as any
if (data.code === 0) {
resolve(data.data as T)
} else {
reject(new Error(data.message || 'Request error'))
}
},
fail: (err) => {
reject(err)
}
})
})
}
原来用 axios.get('/user') 的地方,就改成使用这个 request 封装。
4.2.2 状态管理:Pinia 基本可以直接复用
uni-app x 基于 Vue3,使用 Pinia 通常是可行的。只需在 main.ts 中按 Vue3 方式挂载:
javascript
// main.ts(uni-app x 项目)
import { createSSRApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
export function createApp() {
const app = createSSRApp(App)
const pinia = createPinia()
app.use(pinia)
return { app }
}
原项目的 Pinia store 代码几乎可以原样搬过来,如:
typescript
// src/shared/stores/user.ts
import { defineStore } from 'pinia'
import type { UserInfo } from '../types/user'
import { fetchUserInfo } from '../api/user'
export const useUserStore = defineStore('user', {
state: (): { info: UserInfo | null } => ({
info: null
}),
actions: {
async loadUser() {
this.info = await fetchUserInfo()
}
}
})
在 uni-app x 的页面里正常使用即可:
javascript
import { useUserStore } from '@/shared/stores/user'
const userStore = useUserStore()
userStore.loadUser()
4.3 步骤 3:重构路由结构:Vue Router -> pages.json
原 Vue3 SPA 中典型的路由配置大致是:
javascript
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import User from '@/views/User.vue'
const routes = [
{ path: '/', name: 'Home', component: Home },
{ path: '/user', name: 'User', component: User }
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
在 uni-app x 中,不使用 Vue Router 管理页面路由,而是:
- 用
pages.json声明页面; - 使用
uni.navigateTo/uni.redirectTo/uni.switchTab等 API 进行跳转。
例如:
json
// pages.json
{
"pages": [
{
"path": "pages/home/index",
"style": {
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/user/index",
"style": {
"navigationBarTitleText": "我的"
}
}
],
"tabBar": {
"color": "#666666",
"selectedColor": "#007aff",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/home/index",
"text": "首页",
"iconPath": "static/tab-home.png",
"selectedIconPath": "static/tab-home-active.png"
},
{
"pagePath": "pages/user/index",
"text": "我的",
"iconPath": "static/tab-user.png",
"selectedIconPath": "static/tab-user-active.png"
}
]
}
}
跳转示例:
javascript
// 在页面脚本中
const goUser = () => {
uni.navigateTo({ url: '/pages/user/index' })
}
如果你原来大量依赖「编程式路由 + 命名路由 + 路由守卫」,需要:
-
将全局守卫逻辑(如登录校验)转移到:
- 页面生命周期(
onLoad、onShow)里做校验; - 或封装为导航函数:
goUserPage()里统一判断登录态。
- 页面生命周期(
-
将路由参数 从
route.params/route.query改到:onLoad((options) => {...})中的参数;- 或通过
uni.navigateTo({ url: '/pages/detail/index?id=123' })传 query。
4.4 步骤 4:页面 & 组件改造:从 DOM -> uni-app 组件体系
这是最费时的部分,但也是「迁移成败的关键」。
4.4.1 基础标签替换
常见的替换规则(示意):
| Web (Vue3) 标签 | uni-app 推荐标签 | 说明 |
|---|---|---|
div |
view |
通用容器 |
span |
text |
行内文本 |
img |
image |
图片,支持多端能力 |
a |
navigator / view+跳转 |
页面跳转,用 uni.navigateTo 等 |
button |
button(uni 组件) |
支持表单、权限等能力 |
input |
input(uni 组件) |
不同平台封装 |
textarea |
textarea |
多行输入 |
示例:原 Web 代码(简化):
xml
<template>
<div class="card" @click="goDetail(item.id)">
<img :src="item.cover" class="cover" />
<div class="info">
<span class="title">{{ item.title }}</span>
<span class="desc">{{ item.desc }}</span>
</div>
</div>
</template>
迁移到 uni-app x:
arduino
<template>
<view class="card" @click="goDetail(item.id)">
<image :src="item.cover" class="cover" mode="aspectFill" />
<view class="info">
<text class="title">{{ item.title }}</text>
<text class="desc">{{ item.desc }}</text>
</view>
</view>
</template>
提示:
- 避免使用原生 DOM 相关 API(
document.querySelector等),改为 Vue 的响应式 + uni 组件能力。- 样式方面继续使用
rpx、flex 等,但要注意小程序与 H5 对部分 CSS 特性的支持差异。
4.4.2 UI 组件库的处理
如果原项目使用了 Element Plus / Ant Design Vue / View UI 等「PC Web UI 库」,一般:
-
不建议直接迁移:这些 UI 库大多为 PC/H5 设计,不适合 App/小程序体验和尺寸;
-
建议:
- 为移动端重新选择 uni-app/uni-app x 生态内的 UI 库(如 uView、uni-ui 等,看后续对 x 的适配);
- 或自行封装一套轻量 UI 组件库(Button、Cell、List、Dialog、Toast 等)。
迁移策略:
-
先识别项目中常用 UI 组件类型:表单、列表、弹窗、Tabs、Drawer 等;
-
在 uni-app x 项目中统一封装一层「业务 UI 组件库」 ,即便内部暂时用原生
view+text拼:- 例如
src/components/base/Button.vue、Dialog.vue等;
- 例如
-
业务页面只依赖这套「业务 UI 组件库」,未来要换实现也方便。
4.5 步骤 5:平台相关 API 替换:window/document -> uni.*
原来的 Vue3 Web 项目,常见这些写法:
window.localStorage、sessionStoragewindow.locationdocument.title = 'xxx'- 监听
window.addEventListener('resize', ...)
在 uni-app x 里,要换成跨端封装的方式,例如:
-
本地存储
- 使用
uni.setStorageSync/uni.getStorageSync等 - 封装一个 storage 工具:
javascript// src/shared/utils/storage.ts const TOKEN_KEY = 'TOKEN' export function setToken(token: string) { uni.setStorageSync(TOKEN_KEY, token) } export function getToken(): string | null { const t = uni.getStorageSync(TOKEN_KEY) return t || null } export function clearToken() { uni.removeStorageSync(TOKEN_KEY) } - 使用
-
页面标题
- 在
pages.json通过navigationBarTitleText设置; - 或调用:
uni.setNavigationBarTitle({ title: 'xxx' })。
- 在
-
窗口尺寸与滚动监听
- 使用
uni.getSystemInfo/uni.onWindowResize(不同端支持情况略有差异,要查文档); - 滚动监听通过
scroll-view/ 页面滚动事件实现,而非直接 DOM 监听。
- 使用
4.6 步骤 6:分阶段验证与发布策略
不要等「全项目迁移完」才开始验证,多阶段、小步快跑更靠谱:
-
阶段 1:最小可运行版本
- 至少有 1--2 个核心页面在 uni-app x 中可运行(H5 & 小程序/App 模拟器都跑通);
- 关键业务流程走通(登录 -> 主页 -> 某个主要业务)。
-
阶段 2:模块化迁移
- 按业务模块迁移,例如「用户中心模块」「订单模块」;
- 每迁移完成一个模块,就在测试环境整体验证。
-
阶段 3:灰度发布与 AB 测试(如果条件允许)
- 对移动端入口,逐渐导入一部分用户到新 uni-app x 客户端或小程序;
- 收集性能表现、崩溃率、用户反馈。
-
阶段 4:旧 Web 项目收缩职责
- 慢慢把纯 Web SPA 项目的核心功能剥离,只留下必要的 PC Web 功能;
- 移动端流量逐步切到 uni-app x 提供的多端入口。
五、迁移过程中的常见坑与应对
5.1 拼命想要「完全复用」原模板代码
很多同学迁移之初,会希望 .vue 页面一个字都不要改地搬过来,这通常是做不到的,主要原因:
- 标签体系不同:
div/span与view/text的语义和能力不同; - CSS 差异:小程序端对部分 CSS 支持不全;
- DOM API 不存在:uni-app 环境下没有真实 DOM。
建议 :
接受「逻辑可以高复用,UI 层需要适配」这个事实,提前预估这部分工作量。
5.2 忽略小程序/App 端的权限与能力差异
- 比如:文件下载、打开外链、支付、登录态管理,在小程序/App/H5 上都有差异;
- 不要把它们揉在一起写,建议封装为:
arduino
// src/shared/utils/platform.ts
export function isWeixinMiniProgram(): boolean {
// 参考 uni-app 平台判断写法
// #ifdef MP-WEIXIN
return true
// #endif
return false
}
再在业务逻辑里按平台区分处理。
有条件可以统一封装 service:例如 pay(order) 内部再根据平台调用不同实现。
5.3 图表、富文本等第三方库的适配
- ECharts/AntV 等在小程序 & App 端需要专门的 Canvas/组件适配;
- 富文本编辑器在移动端、小程序生态差异很大。
建议:
- 优先搜寻「uni-app/uni-app x 生态中已有的适配方案或组件库」;
- 实在没有,考虑为 App 和小程序端写专门版本,或者功能做轻量降级。
六、总结:从 Vue3 到 uni-app x 的核心经验
整体回顾:
-
不要从"Vue3 -> uni-app x"直接想,而是从「Web-only -> 多端」的角度思考;
-
成功迁移的关键在于:
- 抽离业务逻辑层(API、Store、Utils),尽可能与平台解耦;
- 重构 UI 与路由层,接受一定程度的「模版和样式重写」;
- 使用 uni-app/uni-app x 提供的跨端 API 替代浏览器专属能力。
对大多数中小团队来说,迁移的回报是:
- 从一个只能跑在浏览器里的 Vue3 SPA,升级成一套可覆盖 App + 小程序 + H5 的多端应用;
- 在后续需求演进中,「新平台支持」会变成「配置和适配问题」,而不是「新项目问题」。