uni-app x 迁移实战:一个 20 屏项目从 Vue3 迁到原生渲染的完整记录

2026-10-04 0 608

先交代背景。手上的项目是一个工业巡检 App,2022 年立项,用 uni-app + Vue3 + TS 写的,前前后后 20 来个页面,跑在 Android 工控平板和 iOS 质检 PAD 上。整个项目最痛的点就一个:长列表卡。

巡检任务列表,一次要展示 800 到 1200 条记录,每条上有图标、三行文字、两个状态标签、一个操作按钮。用 vue 页面渲染,滚动到 500 条之后就开始掉帧,滚到 800 条基本是一边滚一边白屏。用 nvue 页面重写过一次,稍好一点,但 nvue 里很多 Vue 的写法不能用,维护成本极高,写了半年改回 vue 了。

去年下半年的时候看到 uni-app x 的正式版发布,说是编译到原生、性能接近原生体验,抱着试一试的心态,花了两周时间做了个分支迁移。结论先摆在前面:迁是值得的,但不是全迁,我最后只迁了 6 个页面。下面把整个过程摊开讲。

迁移之前,先诚实评估

拿到「要不要迁」这个问题的时候,我做的第一件事是列一张表,把项目里所有页面按「交互复杂度」和「渲染压力」两个维度分类。

页面类型 数量 交互复杂度 渲染压力 是否迁移
任务列表页 2 中 极高 迁移
巡检详情页 1 高 高 迁移
实时监控大屏 1 低 极高 迁移
图片标注页 1 高 高 迁移
报表查看页 1 中 中 迁移
登录、设置、消息等 14 低 低 不迁移

结论很清晰:只迁性能瓶颈的页面,其他保持原样。有人可能会问「能不能一半 vue 一半 uvue 混着跑」,答案是可以的,uni-app x 支持更细粒度的页面级配置,不需要整个项目一把梭。

顺便说一句,我见过太多团队一上来就「全项目迁移」,跑到第三周发现表单页面里某个三方组件不兼容、某个 CSS 属性不支持,返工,最后又改回来。别急,先解决真问题。

项目准备:先搞清版本和构建链

uni-app x 对 HBuilderX 的版本是有硬性要求的,而且从 4.24 之后每隔一两个版本都会有些重要更新。我用的当时是 4.45 稳定版,配套的 uni-app x 编译器版本是 1.x。这里有个隐形坑:不同版本对某些语法的支持程度不一样,尤其 CSS。同一个 CSS 属性在 4.24 里报错,在 4.45 里可能就能用了。团队协作的话,一定在 .gitignore 里明确锁定 HBuilderX 的版本要求,写在 README 里。

我建立了一个独立分支 feature/uvue,然后把原本的 pages.json 复制了一份,只改了一份,两边各自维护路由。这是为了后面如果迁移失败,能快速回滚。

项目结构:看似一样,实则不同

先说个好消息:uni-app x 的项目目录结构跟原来的 uni-app 基本一致。pages、static、components、pages.json、manifest.json,都在老地方。

但有三个关键差异,是迁移初期最容易踩的。

第一个差异:后缀名。原来 vue 页面用 .vue,uvue 页面用 .uvue。这个后缀很重要,HBuilderX 是根据后缀来选择编译器走 vue 还是走 uni-app x 的。

第二个差异:pages.json 里的 "style" 配置可以共存。同一个项目可以同时存在 .vue 页面和 .uvue 页面,路由配置里只要写清路径就行。我在迁移过程中把两者混跑了一段时间,验证稳定性。

第三个差异:App.vue 和 main.ts。原来的 App.vue 用 main.ts 里的 createSSRApp 挂载,uvue 版本的入口稍不一样,得用 uni-app x 的语法。我当时的做法是先保持原样,遇到报错再改。

语法差异:从「能用就行」到「严格约束」

这是迁移里最耗时间的部分。uvue 页面的 JS 层其实跟普通 Vue3 差异不大 —— 你还是能写 <script setup>、ref、computed、watch、onMounted,这些完全一样。但一旦碰到「跟渲染、跟原生能力、跟样式」相关的地方,约束就突然收紧了。

模板层的差异

模板语法基本一致,但有几个不能用的用法:

  • v-html 不支持。uni-app x 里原生渲染没有 DOM,不能直接塞 HTML 字符串。
  • <slot> 支持,但不能像 Web 那样做复杂的动态 slot 分发,尤其是带 scoped slot 的场景限制较多。
  • 动态组件 <component :is="..."> 的用法被限制了,需要改成 v-if 分支。
  • <transition> 组件不支持,所有动画得自己用 CSS 或者用 uni.createAnimation。

脚本层的差异

这块还好,大部分 JS 语法都能用,差异主要在两处。

一是 any 类型。uvue 编译成原生代码的时候,如果变量类型推不出来,会编译失败。所以 let data: any = {} 这种写法要么改成明确的 interface,要么至少标注成 UTSJSONObject。这是从 TS 迁过来最不习惯的地方。

二是依赖 Web API 的代码。原来项目里有几处用了 window、document、localStorage 之类的东西。uvue 里没有这些,全部要换成 uni 的 API 或者 UTS 层面的实现。

样式的差异(重灾区)

我在迁移过程中碰到的问题,80% 都是 CSS。

第一是 不能给样式用行内 style=”…” 传变量,那种 :style="{ color: textColor }" 是可以的,但如果表达式太复杂,编译会出问题。稳妥的做法是定义 class,用 :class 切。

第二是部分 CSS 属性被阉割了。我遇到的具体例子有:background-image 只能用纯色和 linear-gradient,不能直接引用图片 path;box-shadow 支持,但某些复杂多层阴影会失效;filter: blur() 只有部分场景能用,性能还差。

第三是overflow 的行为跟 Web 不一样。原生渲染里的裁剪规则更严格一些,有些写法在 web 上可滚动,在 uvue 里完全不响应。

案例一:巡检任务列表的改造

这是迁移的核心目标,摆两个对比。

原来的 vue 页面大致是这样:

<template>
  <scroll-view scroll-y class="list" @scrolltolower="loadMore">
    <view v-for="item in tasks" :key="item.id" class="task-item">
      <image :src="item.icon" class="icon" />
      <view class="content">
        <text class="title">{{ item.title }}</text>
        <text class="subtitle">{{ item.location }} · {{ item.time }}</text>
        <view class="tags">
          <text v-for="t in item.tags" :key="t" class="tag">{{ t }}</text>
        </view>
      </view>
      <button @click="open(item)">处理</button>
    </view>
  </scroll-view>
</template>

迁到 uvue 里,业务结构基本没变,但有几处要改:

<template>
  <list-view class="list" @scrolltolower="loadMore">
    <list-item v-for="item in tasks" :key="item.id" class="task-item">
      <image :src="item.icon" class="icon" />
      <view class="content">
        <text class="title">{{ item.title }}</text>
        <text class="subtitle">{{ item.location }} · {{ item.time }}</text>
        <view class="tags">
          <text v-for="t in item.tags" :key="t" class="tag">{{ t }}</text>
        </view>
      </view>
      <button @click="open(item)">处理</button>
    </list-item>
  </list-view>
</template>

核心变化是 scroll-view + v-for 换成了 list-view + list-item。这就是 uni-app x 里做长列表的正确姿势:list-view 是原生虚拟列表容器,list-item 是它的子项。

这里有个细节:scroll-view 在 uvue 里也能用,但不具备虚拟化能力 —— 也就是说 800 条数据全都会创建原生 View 对象,性能上跟 vue 页面没多大区别。必须用 list-view 才有虚拟化效果。

实测结果:同样的 1000 条数据,滚动到底,vue 页面在 500 条左右就有明显掉帧,uvue 页面全程 60fps 上下,差不多是「能用」和「不能用」的差别。

但 list-view 也有坑。一是 v-for 的时候不能用 list-item 之外的包装元素,二是如果要动态高度,需要显式设置 item-size 才能达到最好的性能。这一点在官方文档里提得不多,我踩过才明白。

案例二:图片标注页的原生能力迁移

这个页面比较特殊,原来是 vue 页面里用 <canvas> 做手绘标注,然后 uni.canvasToTempFilePath 导出图片,再传给后端。迁到 uvue 里,canvas 的 API 兼容性不好,canvasToTempFilePath 也不支持。

我的做法是换一套思路:用 uvue 的 <canvas> 做绘制(uni-app x 有专门的 canvas 组件),导出图片放到原生层用 UTS 处理。既然是原生渲染,直接用 Android 的 Bitmap 和 iOS 的 UIImage 反而更顺。

写了一个简单的 UTS 插件,接收 canvas 传递过来的像素数据,转成平台原生图片,再写文件。这部分细节比较多,有兴趣的话可以单独写一篇。简单说结论:uvue 里遇到 Web API 缺位,不要硬凑,果断下潜到 UTS。

八个字总结踩过的坑

「能迁则迁,不能就绕」。下面分条说。

坑一:HBuilderX 版本必须锁死。团队里只要有人升级了 HBuilderX,其他没升级的人拉代码就会编译失败。建议用 .hbuilderx 目录做个简单的版本记录,或者干脆统一让 CI 用固定版本的 HBuilderX。

坑二:v-show 在 uvue 里是移除节点,不是 display: none。这是个反直觉的地方。原来 vue 里 v-show 切换只改 CSS,DOM 保留;uvue 里切换其实会销毁和重建原生 View,性能开销比 v-if 还大。频繁切换的场景,老老实实用 CSS 控制 opacity 或者用 v-if 一次到位。

坑三:uni.getStorageSync 的行为变了。uvue 里同步 storage 读写只支持字符串,读写对象需要自己 stringify,或者用 uni.getStorage 的异步版本。

坑四:CSS 里的 flex: 1 在 uvue 里表现微妙。Web 上 flex: 1 是 flex: 1 1 0 的简写,uvue 里可能理解成 flex-grow: 1 但 basis 保留 auto,结果是撑不满。稳妥做法是显式写 flex: 1 1 0 或者 flex-grow: 1; flex-shrink: 1; flex-basis: 0。

坑五:position: fixed 里含 text 或input 时,键盘弹起可能被挡。这个问题在 vue 页面里也有,但 uvue 里更明显。解决办法是把输入区域整体上移,或者用 uni.onKeyboardHeightChange 监听键盘高度,动态设 padding-bottom。

坑六:uni.showToast 的 duration 上限变了。原来可以传 5000ms,uvue 里超过某个值会被截断。做长提示建议用 uni.showLoading 或者自定义组件。

坑七:图片加载失败没有 onerror 事件。uvue 的 image 组件没有 @error 回调(不同版本可能有差异),要靠 uni.getImageInfo 预检。这一条是最不习惯的。

坑八:dev 工具的热重载不如 vue 顺滑。uvue 项目的编译涉及原生代码生成,热重载偶尔会出现「旧状态没清掉」的情况。开发时建议每半小时重启一次 dev server,能省掉不少灵异现象。

性能对比:数据说话

测试机型是红米 Note 11(安卓 13)和 iPad 9(iPadOS 17),测的都是同一批数据、同一段网络请求。冷启动时间指的是 App 从点击图标到首页首屏可交互的毫秒数。

测试项 vue 页面 nvue 页面 uvue 页面
冷启动到首屏 2.8s 2.1s 1.6s
1000 条列表首次渲染 4.2s 2.5s 0.6s
滚动到 1000 条时的帧率 28fps 45fps 58fps
列表内存占用(1000 条) 180MB 120MB 52MB
页面切换动画流畅度 一般 较好 好

数据比我预想的要好。最惊艳的是列表首次渲染时间,从 4.2 秒降到 0.6 秒,因为虚拟列表只创建了可见的十来条。内存占用也降了三分之二。iPad 上的测试结果类似,但整体比安卓更好。

什么项目该迁,什么不该迁

说了这么多好处,也得泼盆冷水。不是所有项目都值得迁。

值得迁的:

  • 长列表、大表格、实时图表这类渲染密集的页面
  • 对滚动流畅度有明确要求的交互
  • 项目生命周期还长,团队有精力维护两套代码的

不建议一上来就迁的:

  • 页面以表单、设置、说明为主,几乎没有性能压力的项目
  • 重度依赖三方 UI 组件库的项目 —— uni-app x 的生态还在建设期,很多组件需要自己造
  • 主要跑在 H5 上的项目,uni-app x 的 H5 支持不如 App 端成熟
  • 依赖大量 Web API 的老项目,迁移成本可能超过收益

我的判断标准很简单:如果现在的痛点是「性能」,就迁;如果痛点是「开发速度」或者「生态丰富度」,先别动。

迁移流程建议

最后给一套我的实操步骤,供参考。

  1. 锁死 HBuilderX 版本,团队统一。
  2. 新建分支,不删原页面,uvue 页面跟 vue 页面并行存在。
  3. 选 1 到 2 个性能最痛的页面做试点,跑通上线。
  4. 试点成功之后,逐步扩大迁移范围,每次不超过 3 个页面。
  5. 迁移完成后,原 vue 页面再保留两个版本迭代周期,确认没有回归问题再删。
  6. 把踩过的坑、版本兼容性问题,写进团队的迁移手册。

两周的迁移给我最大的感触,倒不是性能提升了多少,而是意识到「Web 层兜不住的事,往下走一层就好了」。在 vue 页面里跟性能缠斗,用各种懒加载、虚拟滚动的库救场,本质都是在 JS 层打补丁。而 uni-app x 提供的思路是:让原生去做原生擅长的事。这个思路一旦转过来,很多以前觉得无解的问题,就变得简单了。

当然,uni-app x 现在还不是银弹,生态上的短板需要时间填。我的建议是:新项目可以放心用;老项目按需迁,别追求一次到位。工具是拿来解决问题的,不是拿来证明先进的。合适就好。

uni-app x 迁移实战:一个 20 屏项目从 Vue3 迁到原生渲染的完整记录
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 uniapp uni-app x 迁移实战:一个 20 屏项目从 Vue3 迁到原生渲染的完整记录 https://www.taomawang.com/web/uniapp/2862.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务