Vue3 项目如何迁移到 uni-app x:从纯 Web 到多端应用的系统指南

0 阅读10分钟

一、引言:为什么要从「纯 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 替换等方面,给出一套可操作的迁移思路和步骤,并分析过程中可能的坑与注意点。


二、迁移前评估:先搞清楚自己是什么项目

迁移前不要急着动手,先回答几个关键问题:

  1. 当前项目的技术栈

    • 是否使用:Vite、Vue Router、Pinia 或 Vuex、Axios、Element Plus/Ant Design Vue 等?
    • 是否大量使用 DOM 直接操作、windowdocument 等 Web 专属 API?
  2. 业务复杂度与依赖

    • 是否大量依赖第三方 UI 库、图表库(ECharts、AntV)、富文本编辑器、复杂表格等?
    • 是否有强 Web 特性(如 iframe、浏览器插件接口、localStorage 逻辑等)?
  3. 迁移目标平台

    • 必须支持哪些:App(iOS/Android)/ 微信小程序 / 其他小程序 / H5?
    • 是否对某些平台有特别强的能力诉求(如推送、蓝牙、相机、文件系统)?
  4. 时间和人力约束

    • 能不能接受一段时间的「双线维护(旧 Web + 新 uni-app x)」?
    • 是否有安卓/iOS 原生同事能协助插件层能力?

根据评估结果,可以大致判断:

  • 适合重用大量业务逻辑,只做外壳改造
  • 还是必须进行较重的架构重构(比如完全脱离 DOM 思维)。

三、迁移总体策略:不要一下子「全搬」,而是分层解耦

从纯 Web(Vue3 SPA)到 uni-app x,本质上是从:

Vue3 + Router + Web DOM + 浏览器特性
          ↓
Vue3 + uni-app x 组件体系 + 多端(App/小程序/H5

迁移的关键策略是:

先把“与平台强绑定”的部分(路由、UI、API)剥离出来,把“与业务有关”的逻辑、数据、服务层抽出来复用。

可以按「三层架构」来思考:

  1. 业务逻辑层(可高度复用)

    • 接口请求封装(API Service)
    • 业务状态管理(Pinia Store)
    • 领域模型与工具函数(utils, hooks)
  2. 页面 & 组件层(部分复用,需要适配)

    • 原有的 .vue 页面可以搬过去,但需要调整:

      • DOM 标签 -> uni-app 组件(div -> viewspan -> text 等)
      • UI 库替换或重构(Element Plus -> 移动端自定义 UI / uni UI 等)
  3. 基础设施层(需重构)

    • 路由: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 为例,命令以官方最新文档为准,下面用伪示例):

# 假设已有相关 CLI 工具
npx degit dcloudio/uni-app-x-starter my-uniappx-app
cd my-uniappx-app
pnpm install # 或 npm/yarn

项目结构通常类似:

my-uniappx-app
├─ src
│  ├─ pages
│  │  └─ index
│  │     └─ index.vue
│  ├─ App.vue
│  ├─ main.ts
│  └─ ...
├─ pages.json
├─ manifest.json
└─ ...

先跑通基础项目(例如 H5 或 App 模拟器),确保环境与编译没问题。

4.2 步骤 2:抽取原项目的「可复用业务层」

在原 Vue3 项目中,重点抽离这些:

  1. services/api/:接口封装
  2. stores/:Pinia 或 Vuex
  3. utils/:通用工具函数
  4. 纯 TS/JS 模块:与平台无关的业务逻辑

将它们复制到新项目的 src/shared/(或任意你喜欢的目录名),例如:

src
├─ shared
│  ├─ api
│  │  └─ user.ts
│  ├─ stores
│  │  └─ user.ts
│  ├─ utils
│  │  └─ date.ts
│  └─ types
│     └─ user.ts
├─ pages
│  └─ index
│     └─ index.vue
└─ ...

4.2.1 网络请求封装适配

如果原来使用 axios,有两种做法:

  • 做一个轻薄的适配层:内部根据运行环境调用 uni.requestfetch
  • 或者直接改用 uni.request + 自己封装

示例(简化版):

// 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 方式挂载:

// 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 代码几乎可以原样搬过来,如:

// 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 的页面里正常使用即可:

import { useUserStore } from '@/shared/stores/user'

const userStore = useUserStore()
userStore.loadUser()

4.3 步骤 3:重构路由结构:Vue Router -> pages.json

原 Vue3 SPA 中典型的路由配置大致是:

// 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 进行跳转。

例如:

// 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"
      }
    ]
  }
}

跳转示例:

// 在页面脚本中
const goUser = () => {
  uni.navigateTo({ url: '/pages/user/index' })
}

如果你原来大量依赖「编程式路由 + 命名路由 + 路由守卫」,需要:

  • 全局守卫逻辑(如登录校验)转移到:

    • 页面生命周期(onLoadonShow)里做校验;
    • 或封装为导航函数: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 推荐标签说明
divview通用容器
spantext行内文本
imgimage图片,支持多端能力
anavigator / view+跳转页面跳转,用 uni.navigateTo
buttonbutton(uni 组件)支持表单、权限等能力
inputinput(uni 组件)不同平台封装
textareatextarea多行输入

示例:原 Web 代码(简化):

<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:

<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 等)。

迁移策略:

  1. 先识别项目中常用 UI 组件类型:表单、列表、弹窗、Tabs、Drawer 等;

  2. 在 uni-app x 项目中统一封装一层「业务 UI 组件库」,即便内部暂时用原生 view + text 拼:

    • 例如 src/components/base/Button.vueDialog.vue 等;
  3. 业务页面只依赖这套「业务 UI 组件库」,未来要换实现也方便。


4.5 步骤 5:平台相关 API 替换:window/document -> uni.*

原来的 Vue3 Web 项目,常见这些写法:

  • window.localStoragesessionStorage
  • window.location
  • document.title = 'xxx'
  • 监听 window.addEventListener('resize', ...)

在 uni-app x 里,要换成跨端封装的方式,例如:

  1. 本地存储

    • 使用 uni.setStorageSync / uni.getStorageSync
    • 封装一个 storage 工具:
    // 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)
    }
    
  2. 页面标题

    • pages.json 通过 navigationBarTitleText 设置;
    • 或调用:uni.setNavigationBarTitle({ title: 'xxx' })
  3. 窗口尺寸与滚动监听

    • 使用 uni.getSystemInfo / uni.onWindowResize(不同端支持情况略有差异,要查文档);
    • 滚动监听通过 scroll-view / 页面滚动事件实现,而非直接 DOM 监听。

4.6 步骤 6:分阶段验证与发布策略

不要等「全项目迁移完」才开始验证,多阶段、小步快跑更靠谱:

  1. 阶段 1:最小可运行版本

    • 至少有 1–2 个核心页面在 uni-app x 中可运行(H5 & 小程序/App 模拟器都跑通);
    • 关键业务流程走通(登录 -> 主页 -> 某个主要业务)。
  2. 阶段 2:模块化迁移

    • 按业务模块迁移,例如「用户中心模块」「订单模块」;
    • 每迁移完成一个模块,就在测试环境整体验证。
  3. 阶段 3:灰度发布与 AB 测试(如果条件允许)

    • 对移动端入口,逐渐导入一部分用户到新 uni-app x 客户端或小程序;
    • 收集性能表现、崩溃率、用户反馈。
  4. 阶段 4:旧 Web 项目收缩职责

    • 慢慢把纯 Web SPA 项目的核心功能剥离,只留下必要的 PC Web 功能;
    • 移动端流量逐步切到 uni-app x 提供的多端入口。

五、迁移过程中的常见坑与应对

5.1 拼命想要「完全复用」原模板代码

很多同学迁移之初,会希望 .vue 页面一个字都不要改地搬过来,这通常是做不到的,主要原因:

  • 标签体系不同:div/spanview/text 的语义和能力不同;
  • CSS 差异:小程序端对部分 CSS 支持不全;
  • DOM API 不存在:uni-app 环境下没有真实 DOM。

建议
接受「逻辑可以高复用,UI 层需要适配」这个事实,提前预估这部分工作量。

5.2 忽略小程序/App 端的权限与能力差异

  • 比如:文件下载、打开外链、支付、登录态管理,在小程序/App/H5 上都有差异;
  • 不要把它们揉在一起写,建议封装为:
// 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 的核心经验

整体回顾:

  1. 不要从“Vue3 -> uni-app x”直接想,而是从「Web-only -> 多端」的角度思考

  2. 成功迁移的关键在于:

    • 抽离业务逻辑层(API、Store、Utils),尽可能与平台解耦;
    • 重构 UI 与路由层,接受一定程度的「模版和样式重写」;
    • 使用 uni-app/uni-app x 提供的跨端 API 替代浏览器专属能力。

对大多数中小团队来说,迁移的回报是:

  • 从一个只能跑在浏览器里的 Vue3 SPA,升级成一套可覆盖 App + 小程序 + H5 的多端应用;
  • 在后续需求演进中,「新平台支持」会变成「配置和适配问题」,而不是「新项目问题」。