HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

uni-ai x 和ChatGPT、Gemini、豆包、DeepSeek的横评测试报告

uni-ai x

2026年11月3日,我们发布了《uni-ai x 和ChatGPT、Gemini、豆包、DeepSeek的横评测试报告》。

为了更直观地呈现一次回复中的累计卡顿,本次将“慢帧平均耗时”调整为“慢帧总耗时”:把每个慢帧超出 16.67ms 的时间相加,再取五轮均值。

其他应用已更新 Android 版本,本次据此重新测试。(ChatGPT 测试时已有新版,但多次未能完整输出测试 Markdown,调整提示词仍无效,故沿用 1.2026.244 版数据并按新口径重算。)

uni-ai x 是基于uni-app x 蒸汽模式的、开源的AI聊天客户端。支持Android、iOS、鸿蒙、web和小程序平台。开源地址见:https://gitcode.com/dcloud/uni-ai-x

web平台开源的 AI Chat 应用较多,但App平台的成熟开源的 AI Chat 客户端几乎只有 uni-ai x。

我们来对比下,这个开源产品,和主流的闭源AI客户端,包括 ChatGPT、Gemini、豆包、DeepSeek,他们在功能和性能两方面都有什么差别。

本评测主要站在开发者视角,不是C端用户视角。

AI客户端是一个技术挑战较高的应用。

  • 功能方面,markdown有很多细节,多种代码语言高亮、表格、公式和图表,在所有平台都处理完善的工作量较大。
  • 性能方面,在大模型流式响应的过程中,客户端需要持续接收流式二进制数据、流式解码、流式解析markdown、流式渲染在界面上。很容易掉帧。

还有一些功能、性能联动性问题。比如

  1. 为了处理所有markdown场景,一些AI客户端会借助webview,比如各种mermaid图表,这又会引发性能问题。
  2. 为了让流式渲染性能好一点,很多客户端都不使用系统的text组件,但这又会造成内容拖选需额外开发处理。甚至一些AI客户端的内容选择,需要单独开一个新界面。

测试环境

测试设备:

  • Android:小米 11 Pro,Android 14,屏幕刷新率设为 120Hz,未 root。
  • 鸿蒙:华为 nova 12,HarmonyOS 6.1.0.135,屏幕刷新率 120Hz。

Android上可通过adb命令监控三方应用帧率,鸿蒙上通过 HDC 和 SmartPerf 采集。iOS官方未提供监测三方应用帧率的功能,故未对比。

本次测试的应用版本:

应用 Android 版本 鸿蒙版本
Uni-AI X 3.1.6 3.1.1
豆包 15.7.0 15.5.0
DeepSeek 2.7.4 ❌
ChatGPT 1.2026.244 ❌
Gemini 1.0.995038376 ❌

各家AI应用均支持iOS和Android,但只有 uni-ai x 和豆包支持了鸿蒙。

markdown功能评测

主流AI客户端都有很多业务功能,这些无需比较。这里重点比较的就是markdown功能和使用体验。

markdown在web平台是非常成熟的。各家AI的网页端聊天界面,markdown功能是完整版。但在app原生环境下,web平台的成熟库无法使用,需要自己开发各平台功能支持。

这导致各家AI客户端的markdown的功能覆盖度显示出差异。

本次对比进行了 Markdown 功能测试,基本功能各家AI客户端均能支持,但也存在部分细节不完善的情况。

Android平台功能对比

对比项 Uni-AI X ChatGPT Gemini 豆包 DeepSeek
多语言代码高亮 ✅ ⚠️ ❌ ✅ ✅
图表 ✅ ⚠️ ❌ ✅ ✅
任务列表 ✅ ❌ ❌ ✅ ✅

多语言代码高亮。除了Gemini外,其他AI客户端均支持主流编程语言高亮。

对于国内的编程语言,如鸿蒙和小程序平台特有语言,包括ets、wxs、ajs、wxml。国外的AI客户端支持不完善。

语言高亮的细节也有差异,部分AI客户端只有基本高亮,着色不够丰富。

uni-ai-x 支持扩展所有 VS Code 能高亮的编程语言,并且着色丰富度与PC上的效果一样。

Gemini客户端不支持渲染图表:

Gemini 图表显示效果

ChatGPT 图表需手动点击触发。使用不方便,但这可以让ChatGPT在流式渲染时压力变轻。如下图,它默认显示markdown原文,需要点击右侧的播放按钮,进行一段时间等待,才能显示渲染后的图表。

ChatGPT 图表手动渲染入口与显示效果

国内几家AI客户端均可以直接显示图表。

任务列表,ChatGPT和Gemini以Markdown原文显示,不如国内产品精细:

ChatGPT 与 Gemini 任务列表显示效果

国内AI客户端均支持任务列表。

下图为豆包:

豆包任务列表显示效果

下图为deepseek

DeepSeek 任务列表显示效果

下图为 uni-ai x:

uni-ai x 任务列表显示效果

可以看出仅 uni-ai x 和 豆包 支持的最完善。

uni-ai x 和豆包Android版完整覆盖所测功能;Gemini 缺少的项目最多。

鸿蒙平台功能对比

仅uni-ai x 和豆包支持鸿蒙平台。

uni-ai x本身是一套代码,各端表现是一样。豆包的鸿蒙版,弱于Android版,如下:

对比项 Uni-AI X 豆包
图表 ✅ ❌
任务列表渲染 ✅ ❌

以下为鸿蒙系统下豆包的图表及任务列表的截图

豆包鸿蒙版图表显示效果

豆包鸿蒙版任务列表显示效果

表格横向滚动体验

在表格横向滚动流畅性上,豆包无论在安卓还是鸿蒙端都不够顺滑。其余AI客户端均为常规滚动效果,而豆包的交互更偏向拖拽,遇到宽度较大的表格时,无法快速滚动。

小结一下功能部分:uni-ai x在markdown处理和平台功能拉齐方面最完善。Gemini功能欠缺最多。

性能评测

性能评测比功能评测要复杂。

首先要选定关键性能指标,其次要拉齐测试环境。

本报告聚焦流式响应期间的掉帧情况,这是性能难点。对于常规App而言,不掉帧是基本需求。但在流式渲染时,各家均会掉帧,只是比谁掉帧更少。

Android平台掉帧测试

在相同手机上,打开HWUI呈现模式分析,用5款AI客户端执行相同的AI请求,确保相同的AI返回(具体提示词较长,详见附录),然后观察屏幕显示的掉帧情况。

HWUI呈现模式分析仅用于视频录屏观察;正式采集表格数据时未开启HWUI,慢帧数据由 ADB 命令 adb shell dumpsys gfxinfo <渲染进程> framestats 获取逐帧时间戳后计算。

柱状图越高,掉帧越严重。

先来看直观的视频录屏对比。

Uni-AI X:

观看 Uni-AI X 视频

ChatGPT:

观看 ChatGPT 视频

Gemini:

观看 Gemini 视频

豆包:

观看豆包视频

DeepSeek:

观看 DeepSeek 视频

多客户端对比视频:

从左至右依次:uni-ai-x、Gemini、ChatGPT

观看 Uni-AI X、Gemini、ChatGPT 对比视频

从左至右依次:uni-ai-x、豆包、deepseek

观看 Uni-AI X、豆包、DeepSeek 对比视频

从视频中可以观察到:

  • Uni-AI X 在具备完整 Markdown 功能的情况下,整个流式输出过程中很少掉帧。
  • ChatGPT 在新增渲染内容时会出现掉帧,处理到代码块时尤为明显。ChatGPT 在流式期间默认不渲染图表,减轻了掉帧程度。
  • Gemini 掉帧会随回复内容增加而逐步增多;它没有代码高亮、没有图表渲染,也没有流式始终自动滚动到底的功能,这些功能也会增加渲染压力。
  • 豆包的慢帧次数和慢帧总耗时表现排名靠后。掉帧会随回复内容增加而增多。当然它支持的 Markdown 内容比 ChatGPT、Gemini 更多。
  • DeepSeek 整体掉帧情况比其他主流闭源AI客户端要好一些,处理图表时掉帧比较明显。

需要强调的是,虽然所有AI客户端输出的是相同的Markdown内容,但Gemini 和ChatGPT不渲染任务列表,流式期间也不渲染图表,Gemini甚至不渲染代码高亮,这都会让国外这2款AI客户端的渲染压力更轻。

视频呈现的是感受,具体的数据相差多少呢?

因为不同AI回复的耗时不同,测试期间产生的总帧数也不同。有些帧在空等不渲染,所以直接请求所有帧的平均值并不准确。

重点是关注耗时大于等于16.67ms(常规 60fps 预算线)的帧,在呈现模式分析中它已超过基准红线。称之为“慢帧”。

这里关注 2 个重要指标:慢帧次数(耗时超标的频次)和慢帧总耗时(累计超时量)。

它们分别表达有多少帧卡了、累计超出预算多少时间。

注意:慢帧次数衡量的是渲染耗时超标的帧数,不直接等同于屏幕物理丢帧数(例如卡顿 100ms 也只计为 1 个慢帧)。

慢帧次数取五轮平均值;慢帧总耗时按每个慢帧超出 16.67ms 的部分求和,再除以五轮轮数。

注:以下数据为5轮均值,5轮的明细数据见后续《明细数据》章节。

应用 慢帧次数/轮 慢帧总耗时/轮
Uni-AI X 20.4 186.50ms
DeepSeek 149.2 1697.34ms
Gemini 240.4 9353.13ms
ChatGPT 368.4 6556.40ms
豆包 671.6 27242.45ms

这个数据与视频的条形图感受是一致的。uni-ai x 很少掉帧,偶有掉帧其幅度也较轻。并且需要再次强调,uni-ai x 是在完整渲染本次所测 Markdown 功能的前提下取得这个测试数据。

再看下 uni-ai x 的数据和其他AI客户端的倍数关系。以uni-ai x的数据为1,看看其他AI客户端是uni-ai x的几倍?

Android版 Uni-AI X 慢帧次数倍数 慢帧总耗时倍数
DeepSeek 7.31 倍 9.10 倍
Gemini 11.78 倍 50.15 倍
ChatGPT 18.06 倍 35.16 倍
豆包 32.92 倍 146.07 倍

鸿蒙端掉帧测试

主流AI客户端目前仅 uni-ai x 和豆包支持鸿蒙。在华为 nova 12 上进行测试。

鸿蒙没有像Android那样自带渲染呈现条形图,所以没有视频录像对比。直接看采集的数据对比

流式响应的慢帧数据(以 5 轮均值统计):

鸿蒙版 慢帧次数/轮 慢帧总耗时/轮
Uni-AI X 73.4 2563.86ms
豆包 732.0 19726.75ms

豆包的慢帧次数是 uni-ai x的约10倍,慢帧总耗时约为 7.69 倍。

再看下uni-ai x的数据和豆包的倍数关系。以uni-ai x的数据为1,看看其他AI客户端是uni-ai x的几倍:

应用 慢帧次数倍数 慢帧总耗时倍数
豆包 9.97 倍 7.69 倍

需要考虑到这个数据的前提是,豆包的鸿蒙版,并不支持图表和任务列表渲染,而uni-ai x是完整渲染的。如果豆包补全这些功能,渲染压力会更大。

其他性能测试

除了流式渲染期间外,我们也考察了其他关键性能指标。比如长会话滚动掉帧测试、APP启动速度测试。这些指标都在合理范围内,就无需罗列明细数据了。

可见最大的压力依然是流式渲染。

  • 长会话滚动掉帧测试
    几款产品在长会话滚动上整体表现都不错,没有明显掉帧,相互之间差距很小。
    可以看下 uni-ai-x 的长会话滚动效果:
    观看 Uni-AI X 长会话滚动视频

  • App启动速度
    各款应用启动体验都在合理区间,不存在明显的启动卡顿或延迟差异。
    下面是 uni-ai-x 的启动录屏:
    观看 Uni-AI X 启动视频

实验方法及重现方式

开发者可在Android手机端打开开发者选项,显示HWUI呈现模式分析的条形图。

然后安装各个AI客户端,输入附录中的提示词,简单直观的了解各个AI客户端的掉帧情况。

如需要详细的数据,则需要参考如下实验方法。

为了方便大家复现实验,我们提供了一个开源小工具,更方便测试帧率数据,地址:https://gitcode.com/dcloud/app-performance-tool

测试环境准备

控制项目 测试条件
设备与系统 小米 11 Pro(M2102K1AC),Android 14
刷新率 120Hz(屏幕每秒刷新 120 次)
Root 状态 未 root(未获取 Android 超级用户权限)
流式回复各轮电量 97%–100%
供电状态 均供电
网络环境 各轮测试均连接同一 Wi-Fi 网络
流式回复各轮结束时电池温度 33–38°C
流式回复场景 输入附录中的固定提示词,采集完整 Markdown 流式回复

电量和电池温度通过 adb shell dumpsys battery 读取,系统热状态通过 adb shell dumpsys thermalservice 读取;屏幕刷新率通过 adb shell dumpsys display 读取。

ChatGPT、DeepSeek、Gemini 和豆包均内置 Baseline Profile,安装时进行了AOT优化。Uni-AI X 3.1.6 测试时使用 ADB speed 命令进行了AOT优化拉齐。

指标计算

Android 单帧耗时按 HWUI 的计算方式得出:FrameCompleted - IntendedVsync - max(0, SyncStart - SyncQueued)(纳秒)。

指标 计算方式
慢帧次数 每轮统计单帧耗时 >= 16,670,000 ns 的帧数,五轮总数 ÷ 5
慢帧总耗时/轮 五轮所有慢帧的 (单帧耗时 - 16,670,000 ns) 之和 ÷ 5 ÷ 1,000,000(ms)

例如,Uni-AI X 的五轮共有 3,604 个系统累计渲染帧,其中逐帧时间戳确认 102 个慢帧,慢帧占比为 102 ÷ 3,604 × 100% = 2.83%;五轮超额耗时合计 932.491755ms,平均 932.491755 ÷ 5 = 186.50ms/轮。

完整回复区间从清零帧统计的命令返回后开始,覆盖提交提示词后的等待和流式显示;固定回复全部显示完成后,以最终帧统计查询完成时刻作为终点。
有效轮次是设备条件符合要求且帧统计完整的一轮。

鸿蒙通过 HDC 调用 SmartPerf 取得相邻画面呈现间隔,无法像 Android gfxinfo 一样取得完整单帧耗时。因此以 SmartPerf 提供的 fpsJitters 帧间隔数据(纳秒)计算:间隔 >=16,670,000 ns 计 1 次慢帧;慢帧次数为五轮总数 ÷ 5,慢帧总耗时/轮为五轮慢帧间隔各自减去 16,670,000ns 后求和 ÷ 5 ÷ 1,000,000(ms)。该指标反映画面更新累计超出预算的时间,可在相同鸿蒙设备、相同场景下比较应用。

注:测试机虽设为 120Hz,但报告采用 16.67ms(60fps 基准线)作为慢帧门槛,聚焦严重卡顿;Android 按单帧渲染耗时判断,鸿蒙按相邻画面呈现间隔判断,仅在各自系统内与竞品横向对比。

测试步骤

  1. 每轮重新安装应用,登录并准备空白会话;重启手机后静置 2 分钟,关闭其他应用。这样让每轮从尽量一致的应用状态和设备状态开始,避免沿用上一轮的聊天内容或运行状态。
  2. 执行 adb shell dumpsys gfxinfo <渲染进程> reset 清零此前帧数。命令结束后记下区间起点;待采集工具提示正式开始,再发送固定提示词。从本次任务开始统计帧数。
  3. 固定回复全部接收并完成显示后结束采集,读取最终帧统计。这样让帧统计覆盖完整的流式回复和 Markdown 显示过程。
  4. 每款应用统计 5 个有效轮次。帧数按 5 轮合计,慢帧占比用合计慢帧数除以合计总帧数;时长和慢帧总耗时按轮取算术平均。

鸿蒙端使用 HDC 连接华为 nova 12,SmartPerf 按应用包名采集 FPS 和帧间隔。

采集命令

Android

以下命令由电脑通过 Android 调试桥(ADB)发送到手机;<package> 表示应用包名,<renderer> 表示实际渲染界面的进程名。Gemini 的 <renderer> 为 com.google.android.googlequicksearchbox:search。

  • 清零渲染进程此前累计的帧统计,并以命令结束时刻作为正式区间起点:adb shell dumpsys gfxinfo <renderer> reset
  • 读取流式回复期间总帧数和逐帧纳秒时间戳,并统计耗时 16.67ms 及以上的帧数:adb shell dumpsys gfxinfo <renderer> framestats
  • 核对前台页面的进程名并查找实际渲染进程:adb shell dumpsys activity activities、adb shell ps -A
  • 读取应用版本:adb shell dumpsys package <package>
  • 读取电量和电池温度:adb shell dumpsys battery
  • 读取 Android 系统热状态:adb shell dumpsys thermalservice
  • 读取显示配置和屏幕刷新率:adb shell dumpsys display

鸿蒙

按包名启动采集,回复显示完成后停止,并按停止命令返回的路径取回数据:

hdc -t <设备> shell SP_daemon -start -PKG <包名> -f -r -print  
hdc -t <设备> shell SP_daemon -stop  
hdc -t <设备> file recv <采集文件路径> <本机文件>

明细数据

Android端流式回复汇总

每款应用均纳入 5 轮。表中每轮平均慢帧数 = 五轮慢帧总数 ÷ 5;例如 Uni-AI X 为 102 ÷ 5 = 20.4 帧。

应用 平均时长 (s) 5 轮总渲染帧数 >=16.67ms 慢帧数 每轮平均慢帧数 慢帧占比 慢帧总耗时/轮
ChatGPT 23.58 8,766 1,842 368.4 21.01% 6556.40ms
Uni-AI X 13.90 3,604 102 20.4 2.83% 186.50ms
Gemini 12.44 2,671 1,202 240.4 45.00% 9353.13ms
DeepSeek 11.05 3,329 746 149.2 22.41% 1697.34ms
豆包 33.28 6,139 3,358 671.6 54.70% 27242.45ms

Android端流式回复逐轮明细

应用 轮次 时长(s) 总帧 >=16.67ms 慢帧数 慢帧占比 慢帧总耗时
ChatGPT 1 27.36 1973 375 19.01% 5913.41ms
ChatGPT 2 23.30 1763 371 21.04% 6452.02ms
ChatGPT 3 32.00 2387 462 19.35% 7488.04ms
ChatGPT 4 17.88 1361 306 22.48% 6336.57ms
ChatGPT 5 17.39 1282 328 25.59% 6591.96ms
Uni-AI X 1 13.13 778 24 3.08% 346.67ms
Uni-AI X 2 12.27 704 19 2.70% 79.77ms
Uni-AI X 3 15.25 529 18 3.40% 116.81ms
Uni-AI X 4 15.40 1061 20 1.89% 143.36ms
Uni-AI X 5 13.43 532 21 3.95% 245.89ms
Gemini 1 12.67 440 200 45.45% 9203.84ms
Gemini 2 12.68 633 332 52.45% 9697.10ms
Gemini 3 11.91 517 233 45.07% 9320.57ms
Gemini 4 11.92 540 220 40.74% 9535.39ms
Gemini 5 13.04 541 217 40.11% 9008.75ms
DeepSeek 1 10.34 631 157 24.88% 2044.42ms
DeepSeek 2 11.51 687 135 19.65% 1737.41ms
DeepSeek 3 10.36 623 139 22.31% 1680.00ms
DeepSeek 4 12.46 770 142 18.44% 1509.42ms
DeepSeek 5 10.56 618 173 27.99% 1515.48ms
豆包 1 30.84 1155 643 55.67% 24534.84ms
豆包 2 31.53 1195 642 53.72% 27186.13ms
豆包 3 37.29 1398 785 56.15% 29360.69ms
豆包 4 33.30 1227 668 54.44% 27488.70ms
豆包 5 33.44 1164 620 53.26% 27641.89ms

鸿蒙端流式回复汇总

应用 轮数 平均 FPS 平均帧间隔 慢帧次数/轮 慢帧总耗时/轮 平均正式时长
Uni-AI X 3.1.1 5 40.96 13.33 ms 73.4 2563.86 ms 20.19 s
豆包 15.5.0 5 24.18 36.47 ms 732.0 19726.75 ms 44.93 s

鸿蒙端逐轮明细

应用 轮次 正式时长 (s) 平均 FPS 平均帧间隔 慢帧次数 慢帧总耗时
Uni-AI X 1 22.34 43.50 12.68 ms 77 2717.38 ms
Uni-AI X 2 17.35 32.85 13.95 ms 64 1822.05 ms
Uni-AI X 3 18.36 38.71 13.77 ms 68 2332.79 ms
Uni-AI X 4 18.48 40.21 13.50 ms 63 2365.54 ms
Uni-AI X 5 24.42 49.55 12.78 ms 95 3581.52 ms
豆包 1 44.36 27.20 30.47 ms 797 17281.43 ms
豆包 2 43.61 26.80 30.84 ms 816 17472.78 ms
豆包 3 43.54 26.65 32.23 ms 812 18589.81 ms
豆包 4 50.38 12.30 56.68 ms 416 26826.14 ms
豆包 5 42.77 27.92 32.12 ms 819 18463.59 ms

uni-ai x的更多介绍

uni-ai x 是 uni-ai 的升级版。uni-ai 基于 uni-app 的前端应用,而 uni-ai x 基于uni-app x 的前端应用,并支持蒸汽模式。

uni-ai x 的跨平台和性能表现建立在uni-app x 蒸汽模式的基础上。蒸汽模式的渲染性能另有评测benchmark:

uni-ai x 是开源的,地址在:https://gitcode.com/dcloud/uni-ai-x

欢迎开发者重现实验、共同完善开源项目。

在AI时代,获取和打磨一个重要的基建工具,给你的用户更好的AI交互体验!

附录:固定测试提示词

需要一个严格的提示词,确保各家AI返回的markdown内容一致,否则数据没有比较意义。

不同应用使用相同、固定的提示词,要求 AI 原样输出包含所有markdown格式的内容,包括标题、列表、任务复选框、普通及超宽表格、块引用、代码、超链接、数学公式、Mermaid 流程/时序/甘特/状态图、分割线、脚注和自动链接的 Markdown 文档。固定提示词全文见下。

【严格指令,禁止任何修改】你必须严格遵守:  
1. 禁止输出任何前置介绍、总结、对话文字;  
2. 禁止增减、修改、重排、精简下面Markdown文本,一字不差原样输出;  
3. 不要用```markdown包裹输出内容;  
4. 输出直接从 `# Markdown 全元素渲染测试文档` 开始,直到文档末尾结束,后面不能加任何文字。  
待输出固定内容:  
# 完整 Markdown 功能演示  
---  
## 标题(1~6级)  
# 一级标题  
## 二级标题  
### 三级标题  
#### 四级标题  
##### 五级标题  
###### 六级标题  
---  
## 列表  
### 无序列表  
- 第一项  
- 第二项  
    - 嵌套项 1  
    - 嵌套项 2  
        - 第三层嵌套  
- 第三项  
### 有序列表  
1. 第一步  
2. 第二步  
    1. 子步骤 A  
    2. 子步骤 B  
3. 第三步  
### 任务复选框列表  
- [x] 已完成任务  
- [ ] 未完成任务  
- [ ] 另一个待办项  
- [x] 子任务已完成  
---  
## 表格  
### 普通表格  
| 姓名 | 年龄 | 城市 |  
|------|------|------|  
| 张三 | 25 | 北京 |  
| 李四 | 30 | 上海 |  
| 王五 | 28 | 深圳 |  
### 短表格  
| 项目 | 值 |  
|------|----|  
| 状态 | 正常 |  
| 版本 | 1.0.0 |  
### 超宽长表格(用于横向滚动测试)  
| 列1 | 列2 | 列3 | 列4 | 列5 | 列6 | 列7 | 列8 | 列9 | 列10 | 列11 | 列12 | 列13 | 列14 | 列15 |  
|-----|-----|-----|-----|-----|-----|-----|-----|-----|------|------|------|------|------|------|  
| 数据1 | 数据2 | 数据3 | 数据4 | 数据5 | 数据6 | 数据7 | 数据8 | 数据9 | 数据10 | 数据11 | 数据12 | 数据13 | 数据14 | 数据15 |  
| 更多1 | 更多2 | 更多3 | 更多4 | 更多5 | 更多6 | 更多7 | 更多8 | 更多9 | 更多10 | 更多11 | 更多12 | 更多13 | 更多14 | 更多15 |  
---  
## 块引用  
> 这是一段普通的引用文本。  
>  
> - 引用中的列表项  
> - 另一个列表项  
>  
> 引用文本的换行继续。  
> > 嵌套引用内容  
---  
## 代码块  
### JS 代码示例  
\`\`\`javascript  
// 一个 JavaScript 示例  
function fibonacci(n) {  
if (n <= 1) return n;  
return fibonacci(n - 1) + fibonacci(n - 2);  
}  
const result = fibonacci(10);  
alert(result);  
// 超长单行代码测试  
// 下面这行代码非常长,用于测试代码块的横向滚动功能。这里包含了很多内容aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc  
const longLineDemo = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => x * 2).filter(x => x > 10).reduce((acc, x) => acc + x, 0);  
\`\`\`  
---  
## 超链接  
### 外部链接  
[访问百度](https://www.baidu.com)  
[打开 GitHub](https://github.com)  
### 相对路径链接  
[查看文档](./docs/readme.md)  
[项目主页](../index.html)  
---  
## 数学公式  
### 行内公式  
爱因斯坦的质能方程:$E = mc^2$  
勾股定理:$a^2 + b^2 = c^2$  
### 多行块公式  
#### 代数公式  
$$  
\begin{aligned}  
(x + y)^3 &= x^3 + 3x^2y + 3xy^2 + y^3 \  
\sum_{i=1}^{n} i &= \frac{n(n+1)}{2}  
\end{aligned}  
$$  
#### 矩阵公式  
$$  
\mathbf{A} = \begin{pmatrix}  
a_{11} & a_{12} & \cdots & a_{1n} \  
a_{21} & a_{22} & \cdots & a_{2n} \  
\vdots & \vdots & \ddots & \vdots \  
a_{m1} & a_{m2} & \cdots & a_{mn}  
\end{pmatrix}  
$$  
#### 化学公式  
$$\text{2H}_2 + \text{O}_2 \xrightarrow{\text{点燃}} 2\text{H}_2\text{O}$$  
$$\text{C}_6\text{H}_{12}\text{O}_6 + 6\text{O}_2 \rightarrow 6\text{CO}_2 + 6\text{H}_2\text{O}$$  
#### 超长公式  
$$  
\lim_{x \to \infty} \frac{\sum_{k=1}^{n} \left( \frac{1}{k^2} \right) \cdot \sqrt{2\pi} \cdot e^{-\frac{x^2}{2}}}{\sqrt{\int_{-\infty}^{\infty} e^{-x^2} dx}} = \frac{\Gamma(\frac{1}{2})}{2} \cdot \frac{\pi}{\sqrt{2}} \cdot \left( \frac{\partial}{\partial x} \ln(x^2 + y^2) \right)^n \quad \text{其中} \quad n \to \infty  
$$  
---  
## Mermaid 图表  
### 第一种:图表(flowchart)  
\`\`\`mermaid  
graph TD  
A[开始] --> B{判断条件}  
B -->|是| C[执行操作1]  
B -->|否| D[执行操作2]  
C --> E[结束]  
D --> E  
\`\`\`  
### 第二种:时序图(sequence)  
\`\`\`mermaid  
sequenceDiagram  
participant A as 客户端  
participant B as 服务器  
A->>B: 发送请求  
B-->>A: 返回响应  
A->>A: 处理数据  
\`\`\`  
### 第三种:甘特图(gantt)  
\`\`\`mermaid  
gantt  
title 项目计划  
dateFormat YYYY-MM-DD  
section 开发  
需求分析 :a1, 2024-01-01, 5d  
代码开发 :a2, after a1, 10d  
测试 :a3, after a2, 5d  
\`\`\`  
### 第四种:状态图(state)  
\`\`\`mermaid  
stateDiagram-v2  
[*] --> 待处理  
待处理 --> 处理中: 开始  
处理中 --> 已完成: 完成  
处理中 --> 失败: 出错  
失败 --> 待处理: 重试  
已完成 --> [*]  
\`\`\`  
---  
## 分割线  
### 写法一:三个短横线  
---  
### 写法二:三个星号  
***  
### 写法三:三个下划线  
___  
---  
## 其他特殊语法  
### 任务列表(完整示例)  
- [x] 支持 Markdown 语法  
- [x] 支持数学公式  
- [x] 支持 Mermaid 插件  
- [x] 支持代码高亮  
- [ ] 待实现功能  
- [ ] 更多功能  
### 脚注语法  
这里是一个带脚注的文本[^1],这里还有一个[^2]。  
[^1]: 这是第一个脚注的详细说明。  
[^2]: 这是第二个脚注的内容。  
### 自动邮箱链接  
联系邮箱:<example@email.com>  
### 自动链接  
直接访问 https://www.example.com 可以看到效果。  
---  
以上就是所有 Markdown 语法的完整演示。✅
继续阅读 »

2026年11月3日,我们发布了《uni-ai x 和ChatGPT、Gemini、豆包、DeepSeek的横评测试报告》。

为了更直观地呈现一次回复中的累计卡顿,本次将“慢帧平均耗时”调整为“慢帧总耗时”:把每个慢帧超出 16.67ms 的时间相加,再取五轮均值。

其他应用已更新 Android 版本,本次据此重新测试。(ChatGPT 测试时已有新版,但多次未能完整输出测试 Markdown,调整提示词仍无效,故沿用 1.2026.244 版数据并按新口径重算。)

uni-ai x 是基于uni-app x 蒸汽模式的、开源的AI聊天客户端。支持Android、iOS、鸿蒙、web和小程序平台。开源地址见:https://gitcode.com/dcloud/uni-ai-x

web平台开源的 AI Chat 应用较多,但App平台的成熟开源的 AI Chat 客户端几乎只有 uni-ai x。

我们来对比下,这个开源产品,和主流的闭源AI客户端,包括 ChatGPT、Gemini、豆包、DeepSeek,他们在功能和性能两方面都有什么差别。

本评测主要站在开发者视角,不是C端用户视角。

AI客户端是一个技术挑战较高的应用。

  • 功能方面,markdown有很多细节,多种代码语言高亮、表格、公式和图表,在所有平台都处理完善的工作量较大。
  • 性能方面,在大模型流式响应的过程中,客户端需要持续接收流式二进制数据、流式解码、流式解析markdown、流式渲染在界面上。很容易掉帧。

还有一些功能、性能联动性问题。比如

  1. 为了处理所有markdown场景,一些AI客户端会借助webview,比如各种mermaid图表,这又会引发性能问题。
  2. 为了让流式渲染性能好一点,很多客户端都不使用系统的text组件,但这又会造成内容拖选需额外开发处理。甚至一些AI客户端的内容选择,需要单独开一个新界面。

测试环境

测试设备:

  • Android:小米 11 Pro,Android 14,屏幕刷新率设为 120Hz,未 root。
  • 鸿蒙:华为 nova 12,HarmonyOS 6.1.0.135,屏幕刷新率 120Hz。

Android上可通过adb命令监控三方应用帧率,鸿蒙上通过 HDC 和 SmartPerf 采集。iOS官方未提供监测三方应用帧率的功能,故未对比。

本次测试的应用版本:

应用 Android 版本 鸿蒙版本
Uni-AI X 3.1.6 3.1.1
豆包 15.7.0 15.5.0
DeepSeek 2.7.4 ❌
ChatGPT 1.2026.244 ❌
Gemini 1.0.995038376 ❌

各家AI应用均支持iOS和Android,但只有 uni-ai x 和豆包支持了鸿蒙。

markdown功能评测

主流AI客户端都有很多业务功能,这些无需比较。这里重点比较的就是markdown功能和使用体验。

markdown在web平台是非常成熟的。各家AI的网页端聊天界面,markdown功能是完整版。但在app原生环境下,web平台的成熟库无法使用,需要自己开发各平台功能支持。

这导致各家AI客户端的markdown的功能覆盖度显示出差异。

本次对比进行了 Markdown 功能测试,基本功能各家AI客户端均能支持,但也存在部分细节不完善的情况。

Android平台功能对比

对比项 Uni-AI X ChatGPT Gemini 豆包 DeepSeek
多语言代码高亮 ✅ ⚠️ ❌ ✅ ✅
图表 ✅ ⚠️ ❌ ✅ ✅
任务列表 ✅ ❌ ❌ ✅ ✅

多语言代码高亮。除了Gemini外,其他AI客户端均支持主流编程语言高亮。

对于国内的编程语言,如鸿蒙和小程序平台特有语言,包括ets、wxs、ajs、wxml。国外的AI客户端支持不完善。

语言高亮的细节也有差异,部分AI客户端只有基本高亮,着色不够丰富。

uni-ai-x 支持扩展所有 VS Code 能高亮的编程语言,并且着色丰富度与PC上的效果一样。

Gemini客户端不支持渲染图表:

Gemini 图表显示效果

ChatGPT 图表需手动点击触发。使用不方便,但这可以让ChatGPT在流式渲染时压力变轻。如下图,它默认显示markdown原文,需要点击右侧的播放按钮,进行一段时间等待,才能显示渲染后的图表。

ChatGPT 图表手动渲染入口与显示效果

国内几家AI客户端均可以直接显示图表。

任务列表,ChatGPT和Gemini以Markdown原文显示,不如国内产品精细:

ChatGPT 与 Gemini 任务列表显示效果

国内AI客户端均支持任务列表。

下图为豆包:

豆包任务列表显示效果

下图为deepseek

DeepSeek 任务列表显示效果

下图为 uni-ai x:

uni-ai x 任务列表显示效果

可以看出仅 uni-ai x 和 豆包 支持的最完善。

uni-ai x 和豆包Android版完整覆盖所测功能;Gemini 缺少的项目最多。

鸿蒙平台功能对比

仅uni-ai x 和豆包支持鸿蒙平台。

uni-ai x本身是一套代码,各端表现是一样。豆包的鸿蒙版,弱于Android版,如下:

对比项 Uni-AI X 豆包
图表 ✅ ❌
任务列表渲染 ✅ ❌

以下为鸿蒙系统下豆包的图表及任务列表的截图

豆包鸿蒙版图表显示效果

豆包鸿蒙版任务列表显示效果

表格横向滚动体验

在表格横向滚动流畅性上,豆包无论在安卓还是鸿蒙端都不够顺滑。其余AI客户端均为常规滚动效果,而豆包的交互更偏向拖拽,遇到宽度较大的表格时,无法快速滚动。

小结一下功能部分:uni-ai x在markdown处理和平台功能拉齐方面最完善。Gemini功能欠缺最多。

性能评测

性能评测比功能评测要复杂。

首先要选定关键性能指标,其次要拉齐测试环境。

本报告聚焦流式响应期间的掉帧情况,这是性能难点。对于常规App而言,不掉帧是基本需求。但在流式渲染时,各家均会掉帧,只是比谁掉帧更少。

Android平台掉帧测试

在相同手机上,打开HWUI呈现模式分析,用5款AI客户端执行相同的AI请求,确保相同的AI返回(具体提示词较长,详见附录),然后观察屏幕显示的掉帧情况。

HWUI呈现模式分析仅用于视频录屏观察;正式采集表格数据时未开启HWUI,慢帧数据由 ADB 命令 adb shell dumpsys gfxinfo <渲染进程> framestats 获取逐帧时间戳后计算。

柱状图越高,掉帧越严重。

先来看直观的视频录屏对比。

Uni-AI X:

观看 Uni-AI X 视频

ChatGPT:

观看 ChatGPT 视频

Gemini:

观看 Gemini 视频

豆包:

观看豆包视频

DeepSeek:

观看 DeepSeek 视频

多客户端对比视频:

从左至右依次:uni-ai-x、Gemini、ChatGPT

观看 Uni-AI X、Gemini、ChatGPT 对比视频

从左至右依次:uni-ai-x、豆包、deepseek

观看 Uni-AI X、豆包、DeepSeek 对比视频

从视频中可以观察到:

  • Uni-AI X 在具备完整 Markdown 功能的情况下,整个流式输出过程中很少掉帧。
  • ChatGPT 在新增渲染内容时会出现掉帧,处理到代码块时尤为明显。ChatGPT 在流式期间默认不渲染图表,减轻了掉帧程度。
  • Gemini 掉帧会随回复内容增加而逐步增多;它没有代码高亮、没有图表渲染,也没有流式始终自动滚动到底的功能,这些功能也会增加渲染压力。
  • 豆包的慢帧次数和慢帧总耗时表现排名靠后。掉帧会随回复内容增加而增多。当然它支持的 Markdown 内容比 ChatGPT、Gemini 更多。
  • DeepSeek 整体掉帧情况比其他主流闭源AI客户端要好一些,处理图表时掉帧比较明显。

需要强调的是,虽然所有AI客户端输出的是相同的Markdown内容,但Gemini 和ChatGPT不渲染任务列表,流式期间也不渲染图表,Gemini甚至不渲染代码高亮,这都会让国外这2款AI客户端的渲染压力更轻。

视频呈现的是感受,具体的数据相差多少呢?

因为不同AI回复的耗时不同,测试期间产生的总帧数也不同。有些帧在空等不渲染,所以直接请求所有帧的平均值并不准确。

重点是关注耗时大于等于16.67ms(常规 60fps 预算线)的帧,在呈现模式分析中它已超过基准红线。称之为“慢帧”。

这里关注 2 个重要指标:慢帧次数(耗时超标的频次)和慢帧总耗时(累计超时量)。

它们分别表达有多少帧卡了、累计超出预算多少时间。

注意:慢帧次数衡量的是渲染耗时超标的帧数,不直接等同于屏幕物理丢帧数(例如卡顿 100ms 也只计为 1 个慢帧)。

慢帧次数取五轮平均值;慢帧总耗时按每个慢帧超出 16.67ms 的部分求和,再除以五轮轮数。

注:以下数据为5轮均值,5轮的明细数据见后续《明细数据》章节。

应用 慢帧次数/轮 慢帧总耗时/轮
Uni-AI X 20.4 186.50ms
DeepSeek 149.2 1697.34ms
Gemini 240.4 9353.13ms
ChatGPT 368.4 6556.40ms
豆包 671.6 27242.45ms

这个数据与视频的条形图感受是一致的。uni-ai x 很少掉帧,偶有掉帧其幅度也较轻。并且需要再次强调,uni-ai x 是在完整渲染本次所测 Markdown 功能的前提下取得这个测试数据。

再看下 uni-ai x 的数据和其他AI客户端的倍数关系。以uni-ai x的数据为1,看看其他AI客户端是uni-ai x的几倍?

Android版 Uni-AI X 慢帧次数倍数 慢帧总耗时倍数
DeepSeek 7.31 倍 9.10 倍
Gemini 11.78 倍 50.15 倍
ChatGPT 18.06 倍 35.16 倍
豆包 32.92 倍 146.07 倍

鸿蒙端掉帧测试

主流AI客户端目前仅 uni-ai x 和豆包支持鸿蒙。在华为 nova 12 上进行测试。

鸿蒙没有像Android那样自带渲染呈现条形图,所以没有视频录像对比。直接看采集的数据对比

流式响应的慢帧数据(以 5 轮均值统计):

鸿蒙版 慢帧次数/轮 慢帧总耗时/轮
Uni-AI X 73.4 2563.86ms
豆包 732.0 19726.75ms

豆包的慢帧次数是 uni-ai x的约10倍,慢帧总耗时约为 7.69 倍。

再看下uni-ai x的数据和豆包的倍数关系。以uni-ai x的数据为1,看看其他AI客户端是uni-ai x的几倍:

应用 慢帧次数倍数 慢帧总耗时倍数
豆包 9.97 倍 7.69 倍

需要考虑到这个数据的前提是,豆包的鸿蒙版,并不支持图表和任务列表渲染,而uni-ai x是完整渲染的。如果豆包补全这些功能,渲染压力会更大。

其他性能测试

除了流式渲染期间外,我们也考察了其他关键性能指标。比如长会话滚动掉帧测试、APP启动速度测试。这些指标都在合理范围内,就无需罗列明细数据了。

可见最大的压力依然是流式渲染。

  • 长会话滚动掉帧测试
    几款产品在长会话滚动上整体表现都不错,没有明显掉帧,相互之间差距很小。
    可以看下 uni-ai-x 的长会话滚动效果:
    观看 Uni-AI X 长会话滚动视频

  • App启动速度
    各款应用启动体验都在合理区间,不存在明显的启动卡顿或延迟差异。
    下面是 uni-ai-x 的启动录屏:
    观看 Uni-AI X 启动视频

实验方法及重现方式

开发者可在Android手机端打开开发者选项,显示HWUI呈现模式分析的条形图。

然后安装各个AI客户端,输入附录中的提示词,简单直观的了解各个AI客户端的掉帧情况。

如需要详细的数据,则需要参考如下实验方法。

为了方便大家复现实验,我们提供了一个开源小工具,更方便测试帧率数据,地址:https://gitcode.com/dcloud/app-performance-tool

测试环境准备

控制项目 测试条件
设备与系统 小米 11 Pro(M2102K1AC),Android 14
刷新率 120Hz(屏幕每秒刷新 120 次)
Root 状态 未 root(未获取 Android 超级用户权限)
流式回复各轮电量 97%–100%
供电状态 均供电
网络环境 各轮测试均连接同一 Wi-Fi 网络
流式回复各轮结束时电池温度 33–38°C
流式回复场景 输入附录中的固定提示词,采集完整 Markdown 流式回复

电量和电池温度通过 adb shell dumpsys battery 读取,系统热状态通过 adb shell dumpsys thermalservice 读取;屏幕刷新率通过 adb shell dumpsys display 读取。

ChatGPT、DeepSeek、Gemini 和豆包均内置 Baseline Profile,安装时进行了AOT优化。Uni-AI X 3.1.6 测试时使用 ADB speed 命令进行了AOT优化拉齐。

指标计算

Android 单帧耗时按 HWUI 的计算方式得出:FrameCompleted - IntendedVsync - max(0, SyncStart - SyncQueued)(纳秒)。

指标 计算方式
慢帧次数 每轮统计单帧耗时 >= 16,670,000 ns 的帧数,五轮总数 ÷ 5
慢帧总耗时/轮 五轮所有慢帧的 (单帧耗时 - 16,670,000 ns) 之和 ÷ 5 ÷ 1,000,000(ms)

例如,Uni-AI X 的五轮共有 3,604 个系统累计渲染帧,其中逐帧时间戳确认 102 个慢帧,慢帧占比为 102 ÷ 3,604 × 100% = 2.83%;五轮超额耗时合计 932.491755ms,平均 932.491755 ÷ 5 = 186.50ms/轮。

完整回复区间从清零帧统计的命令返回后开始,覆盖提交提示词后的等待和流式显示;固定回复全部显示完成后,以最终帧统计查询完成时刻作为终点。
有效轮次是设备条件符合要求且帧统计完整的一轮。

鸿蒙通过 HDC 调用 SmartPerf 取得相邻画面呈现间隔,无法像 Android gfxinfo 一样取得完整单帧耗时。因此以 SmartPerf 提供的 fpsJitters 帧间隔数据(纳秒)计算:间隔 >=16,670,000 ns 计 1 次慢帧;慢帧次数为五轮总数 ÷ 5,慢帧总耗时/轮为五轮慢帧间隔各自减去 16,670,000ns 后求和 ÷ 5 ÷ 1,000,000(ms)。该指标反映画面更新累计超出预算的时间,可在相同鸿蒙设备、相同场景下比较应用。

注:测试机虽设为 120Hz,但报告采用 16.67ms(60fps 基准线)作为慢帧门槛,聚焦严重卡顿;Android 按单帧渲染耗时判断,鸿蒙按相邻画面呈现间隔判断,仅在各自系统内与竞品横向对比。

测试步骤

  1. 每轮重新安装应用,登录并准备空白会话;重启手机后静置 2 分钟,关闭其他应用。这样让每轮从尽量一致的应用状态和设备状态开始,避免沿用上一轮的聊天内容或运行状态。
  2. 执行 adb shell dumpsys gfxinfo <渲染进程> reset 清零此前帧数。命令结束后记下区间起点;待采集工具提示正式开始,再发送固定提示词。从本次任务开始统计帧数。
  3. 固定回复全部接收并完成显示后结束采集,读取最终帧统计。这样让帧统计覆盖完整的流式回复和 Markdown 显示过程。
  4. 每款应用统计 5 个有效轮次。帧数按 5 轮合计,慢帧占比用合计慢帧数除以合计总帧数;时长和慢帧总耗时按轮取算术平均。

鸿蒙端使用 HDC 连接华为 nova 12,SmartPerf 按应用包名采集 FPS 和帧间隔。

采集命令

Android

以下命令由电脑通过 Android 调试桥(ADB)发送到手机;<package> 表示应用包名,<renderer> 表示实际渲染界面的进程名。Gemini 的 <renderer> 为 com.google.android.googlequicksearchbox:search。

  • 清零渲染进程此前累计的帧统计,并以命令结束时刻作为正式区间起点:adb shell dumpsys gfxinfo <renderer> reset
  • 读取流式回复期间总帧数和逐帧纳秒时间戳,并统计耗时 16.67ms 及以上的帧数:adb shell dumpsys gfxinfo <renderer> framestats
  • 核对前台页面的进程名并查找实际渲染进程:adb shell dumpsys activity activities、adb shell ps -A
  • 读取应用版本:adb shell dumpsys package <package>
  • 读取电量和电池温度:adb shell dumpsys battery
  • 读取 Android 系统热状态:adb shell dumpsys thermalservice
  • 读取显示配置和屏幕刷新率:adb shell dumpsys display

鸿蒙

按包名启动采集,回复显示完成后停止,并按停止命令返回的路径取回数据:

hdc -t <设备> shell SP_daemon -start -PKG <包名> -f -r -print  
hdc -t <设备> shell SP_daemon -stop  
hdc -t <设备> file recv <采集文件路径> <本机文件>

明细数据

Android端流式回复汇总

每款应用均纳入 5 轮。表中每轮平均慢帧数 = 五轮慢帧总数 ÷ 5;例如 Uni-AI X 为 102 ÷ 5 = 20.4 帧。

应用 平均时长 (s) 5 轮总渲染帧数 >=16.67ms 慢帧数 每轮平均慢帧数 慢帧占比 慢帧总耗时/轮
ChatGPT 23.58 8,766 1,842 368.4 21.01% 6556.40ms
Uni-AI X 13.90 3,604 102 20.4 2.83% 186.50ms
Gemini 12.44 2,671 1,202 240.4 45.00% 9353.13ms
DeepSeek 11.05 3,329 746 149.2 22.41% 1697.34ms
豆包 33.28 6,139 3,358 671.6 54.70% 27242.45ms

Android端流式回复逐轮明细

应用 轮次 时长(s) 总帧 >=16.67ms 慢帧数 慢帧占比 慢帧总耗时
ChatGPT 1 27.36 1973 375 19.01% 5913.41ms
ChatGPT 2 23.30 1763 371 21.04% 6452.02ms
ChatGPT 3 32.00 2387 462 19.35% 7488.04ms
ChatGPT 4 17.88 1361 306 22.48% 6336.57ms
ChatGPT 5 17.39 1282 328 25.59% 6591.96ms
Uni-AI X 1 13.13 778 24 3.08% 346.67ms
Uni-AI X 2 12.27 704 19 2.70% 79.77ms
Uni-AI X 3 15.25 529 18 3.40% 116.81ms
Uni-AI X 4 15.40 1061 20 1.89% 143.36ms
Uni-AI X 5 13.43 532 21 3.95% 245.89ms
Gemini 1 12.67 440 200 45.45% 9203.84ms
Gemini 2 12.68 633 332 52.45% 9697.10ms
Gemini 3 11.91 517 233 45.07% 9320.57ms
Gemini 4 11.92 540 220 40.74% 9535.39ms
Gemini 5 13.04 541 217 40.11% 9008.75ms
DeepSeek 1 10.34 631 157 24.88% 2044.42ms
DeepSeek 2 11.51 687 135 19.65% 1737.41ms
DeepSeek 3 10.36 623 139 22.31% 1680.00ms
DeepSeek 4 12.46 770 142 18.44% 1509.42ms
DeepSeek 5 10.56 618 173 27.99% 1515.48ms
豆包 1 30.84 1155 643 55.67% 24534.84ms
豆包 2 31.53 1195 642 53.72% 27186.13ms
豆包 3 37.29 1398 785 56.15% 29360.69ms
豆包 4 33.30 1227 668 54.44% 27488.70ms
豆包 5 33.44 1164 620 53.26% 27641.89ms

鸿蒙端流式回复汇总

应用 轮数 平均 FPS 平均帧间隔 慢帧次数/轮 慢帧总耗时/轮 平均正式时长
Uni-AI X 3.1.1 5 40.96 13.33 ms 73.4 2563.86 ms 20.19 s
豆包 15.5.0 5 24.18 36.47 ms 732.0 19726.75 ms 44.93 s

鸿蒙端逐轮明细

应用 轮次 正式时长 (s) 平均 FPS 平均帧间隔 慢帧次数 慢帧总耗时
Uni-AI X 1 22.34 43.50 12.68 ms 77 2717.38 ms
Uni-AI X 2 17.35 32.85 13.95 ms 64 1822.05 ms
Uni-AI X 3 18.36 38.71 13.77 ms 68 2332.79 ms
Uni-AI X 4 18.48 40.21 13.50 ms 63 2365.54 ms
Uni-AI X 5 24.42 49.55 12.78 ms 95 3581.52 ms
豆包 1 44.36 27.20 30.47 ms 797 17281.43 ms
豆包 2 43.61 26.80 30.84 ms 816 17472.78 ms
豆包 3 43.54 26.65 32.23 ms 812 18589.81 ms
豆包 4 50.38 12.30 56.68 ms 416 26826.14 ms
豆包 5 42.77 27.92 32.12 ms 819 18463.59 ms

uni-ai x的更多介绍

uni-ai x 是 uni-ai 的升级版。uni-ai 基于 uni-app 的前端应用,而 uni-ai x 基于uni-app x 的前端应用,并支持蒸汽模式。

uni-ai x 的跨平台和性能表现建立在uni-app x 蒸汽模式的基础上。蒸汽模式的渲染性能另有评测benchmark:

uni-ai x 是开源的,地址在:https://gitcode.com/dcloud/uni-ai-x

欢迎开发者重现实验、共同完善开源项目。

在AI时代,获取和打磨一个重要的基建工具,给你的用户更好的AI交互体验!

附录:固定测试提示词

需要一个严格的提示词,确保各家AI返回的markdown内容一致,否则数据没有比较意义。

不同应用使用相同、固定的提示词,要求 AI 原样输出包含所有markdown格式的内容,包括标题、列表、任务复选框、普通及超宽表格、块引用、代码、超链接、数学公式、Mermaid 流程/时序/甘特/状态图、分割线、脚注和自动链接的 Markdown 文档。固定提示词全文见下。

【严格指令,禁止任何修改】你必须严格遵守:  
1. 禁止输出任何前置介绍、总结、对话文字;  
2. 禁止增减、修改、重排、精简下面Markdown文本,一字不差原样输出;  
3. 不要用```markdown包裹输出内容;  
4. 输出直接从 `# Markdown 全元素渲染测试文档` 开始,直到文档末尾结束,后面不能加任何文字。  
待输出固定内容:  
# 完整 Markdown 功能演示  
---  
## 标题(1~6级)  
# 一级标题  
## 二级标题  
### 三级标题  
#### 四级标题  
##### 五级标题  
###### 六级标题  
---  
## 列表  
### 无序列表  
- 第一项  
- 第二项  
    - 嵌套项 1  
    - 嵌套项 2  
        - 第三层嵌套  
- 第三项  
### 有序列表  
1. 第一步  
2. 第二步  
    1. 子步骤 A  
    2. 子步骤 B  
3. 第三步  
### 任务复选框列表  
- [x] 已完成任务  
- [ ] 未完成任务  
- [ ] 另一个待办项  
- [x] 子任务已完成  
---  
## 表格  
### 普通表格  
| 姓名 | 年龄 | 城市 |  
|------|------|------|  
| 张三 | 25 | 北京 |  
| 李四 | 30 | 上海 |  
| 王五 | 28 | 深圳 |  
### 短表格  
| 项目 | 值 |  
|------|----|  
| 状态 | 正常 |  
| 版本 | 1.0.0 |  
### 超宽长表格(用于横向滚动测试)  
| 列1 | 列2 | 列3 | 列4 | 列5 | 列6 | 列7 | 列8 | 列9 | 列10 | 列11 | 列12 | 列13 | 列14 | 列15 |  
|-----|-----|-----|-----|-----|-----|-----|-----|-----|------|------|------|------|------|------|  
| 数据1 | 数据2 | 数据3 | 数据4 | 数据5 | 数据6 | 数据7 | 数据8 | 数据9 | 数据10 | 数据11 | 数据12 | 数据13 | 数据14 | 数据15 |  
| 更多1 | 更多2 | 更多3 | 更多4 | 更多5 | 更多6 | 更多7 | 更多8 | 更多9 | 更多10 | 更多11 | 更多12 | 更多13 | 更多14 | 更多15 |  
---  
## 块引用  
> 这是一段普通的引用文本。  
>  
> - 引用中的列表项  
> - 另一个列表项  
>  
> 引用文本的换行继续。  
> > 嵌套引用内容  
---  
## 代码块  
### JS 代码示例  
\`\`\`javascript  
// 一个 JavaScript 示例  
function fibonacci(n) {  
if (n <= 1) return n;  
return fibonacci(n - 1) + fibonacci(n - 2);  
}  
const result = fibonacci(10);  
alert(result);  
// 超长单行代码测试  
// 下面这行代码非常长,用于测试代码块的横向滚动功能。这里包含了很多内容aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc  
const longLineDemo = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(x => x * 2).filter(x => x > 10).reduce((acc, x) => acc + x, 0);  
\`\`\`  
---  
## 超链接  
### 外部链接  
[访问百度](https://www.baidu.com)  
[打开 GitHub](https://github.com)  
### 相对路径链接  
[查看文档](./docs/readme.md)  
[项目主页](../index.html)  
---  
## 数学公式  
### 行内公式  
爱因斯坦的质能方程:$E = mc^2$  
勾股定理:$a^2 + b^2 = c^2$  
### 多行块公式  
#### 代数公式  
$$  
\begin{aligned}  
(x + y)^3 &= x^3 + 3x^2y + 3xy^2 + y^3 \  
\sum_{i=1}^{n} i &= \frac{n(n+1)}{2}  
\end{aligned}  
$$  
#### 矩阵公式  
$$  
\mathbf{A} = \begin{pmatrix}  
a_{11} & a_{12} & \cdots & a_{1n} \  
a_{21} & a_{22} & \cdots & a_{2n} \  
\vdots & \vdots & \ddots & \vdots \  
a_{m1} & a_{m2} & \cdots & a_{mn}  
\end{pmatrix}  
$$  
#### 化学公式  
$$\text{2H}_2 + \text{O}_2 \xrightarrow{\text{点燃}} 2\text{H}_2\text{O}$$  
$$\text{C}_6\text{H}_{12}\text{O}_6 + 6\text{O}_2 \rightarrow 6\text{CO}_2 + 6\text{H}_2\text{O}$$  
#### 超长公式  
$$  
\lim_{x \to \infty} \frac{\sum_{k=1}^{n} \left( \frac{1}{k^2} \right) \cdot \sqrt{2\pi} \cdot e^{-\frac{x^2}{2}}}{\sqrt{\int_{-\infty}^{\infty} e^{-x^2} dx}} = \frac{\Gamma(\frac{1}{2})}{2} \cdot \frac{\pi}{\sqrt{2}} \cdot \left( \frac{\partial}{\partial x} \ln(x^2 + y^2) \right)^n \quad \text{其中} \quad n \to \infty  
$$  
---  
## Mermaid 图表  
### 第一种:图表(flowchart)  
\`\`\`mermaid  
graph TD  
A[开始] --> B{判断条件}  
B -->|是| C[执行操作1]  
B -->|否| D[执行操作2]  
C --> E[结束]  
D --> E  
\`\`\`  
### 第二种:时序图(sequence)  
\`\`\`mermaid  
sequenceDiagram  
participant A as 客户端  
participant B as 服务器  
A->>B: 发送请求  
B-->>A: 返回响应  
A->>A: 处理数据  
\`\`\`  
### 第三种:甘特图(gantt)  
\`\`\`mermaid  
gantt  
title 项目计划  
dateFormat YYYY-MM-DD  
section 开发  
需求分析 :a1, 2024-01-01, 5d  
代码开发 :a2, after a1, 10d  
测试 :a3, after a2, 5d  
\`\`\`  
### 第四种:状态图(state)  
\`\`\`mermaid  
stateDiagram-v2  
[*] --> 待处理  
待处理 --> 处理中: 开始  
处理中 --> 已完成: 完成  
处理中 --> 失败: 出错  
失败 --> 待处理: 重试  
已完成 --> [*]  
\`\`\`  
---  
## 分割线  
### 写法一:三个短横线  
---  
### 写法二:三个星号  
***  
### 写法三:三个下划线  
___  
---  
## 其他特殊语法  
### 任务列表(完整示例)  
- [x] 支持 Markdown 语法  
- [x] 支持数学公式  
- [x] 支持 Mermaid 插件  
- [x] 支持代码高亮  
- [ ] 待实现功能  
- [ ] 更多功能  
### 脚注语法  
这里是一个带脚注的文本[^1],这里还有一个[^2]。  
[^1]: 这是第一个脚注的详细说明。  
[^2]: 这是第二个脚注的内容。  
### 自动邮箱链接  
联系邮箱:<example@email.com>  
### 自动链接  
直接访问 https://www.example.com 可以看到效果。  
---  
以上就是所有 Markdown 语法的完整演示。✅
收起阅读 »

iOS 集成/制作DCloudUTSExtAPI文档过于落后,能不能更新下

uni-app x

例如官网文档上的 配置uts-config.json

uni-getLocation-systemy模块需要添加如下配置
{
"providers": [{
"name":"system",
"service":"location",
"class":"UTSSDKModulesDCloudUniGetLocationSystemUniLocationSystemProviderImpl"
}]
}
根据这样配置。获取位置功能根本不生效
必须改成
{
"providers": [{
"name":"system",
"service":"location",
"class":"UTSSDKModulesDCloudUniLocationSystemUniLocationSystemProviderImpl"
}]
}
才能生效
还有文档上根据模块引入源文件,也是不全的,具体哪些不全忘记了,只是把上面那个印象深刻的举例说明了

继续阅读 »

例如官网文档上的 配置uts-config.json

uni-getLocation-systemy模块需要添加如下配置
{
"providers": [{
"name":"system",
"service":"location",
"class":"UTSSDKModulesDCloudUniGetLocationSystemUniLocationSystemProviderImpl"
}]
}
根据这样配置。获取位置功能根本不生效
必须改成
{
"providers": [{
"name":"system",
"service":"location",
"class":"UTSSDKModulesDCloudUniLocationSystemUniLocationSystemProviderImpl"
}]
}
才能生效
还有文档上根据模块引入源文件,也是不全的,具体哪些不全忘记了,只是把上面那个印象深刻的举例说明了

收起阅读 »

应用商店百万级、千万级下载量,这些App已用上 uni-app x 蒸汽模式

uni-app x

uni-app x 蒸汽模式,拥有快于原生的渲染速度、支持发布后热更新、AI友好等特点。

但开发者仍然有疑问:

  1. uni-app x蒸汽模式成熟了吗?
  2. 有没有真正上线运营的App?
  3. 老项目升级到蒸汽模式,要投入多少人、花多长时间?

最近,我们回访了一些uni-app x 蒸汽模式的开发者,收集了一批案例,采访到了开发者的真实感受,他们花费了多久、对什么满意、还有什么期待。

先来看案例。

uni-app x 蒸汽模式案例

打开uni-app x官方案例页,可以看到很多蒸汽模式案例,其中不乏应用商店下载量超百万、千万的大用户量应用。

应用范围分布较广,覆盖政企应用、心理咨询、回收电商、教育学习、金融股讯、娱乐社交、生活工具等不同业务。

> 下载量数据来源于各大应用商店公开可查看的下载数累加。

应用商店下载量千万级

下载体验:Android · iOS · 鸿蒙,微信小程序搜索“抱抱海心理”。

应用商店下载量百万级

下载体验:Android · iOS,微信小程序搜索“京大大回收”。

应用商店下载量十万级

这些应用在各平台的下载入口,可查看完整案例页。

更多案例

完整案例介绍及Android、iOS、鸿蒙等平台下载入口,请查看uni-app x官方案例页。也欢迎开发者提交自己的案例。

开发者访谈

我们访谈了一些开发者,他们有的之前使用老版uni-app、有的使用uni-app x的vdom模式、有的使用flutter,都迁移到了uni-app x 蒸汽模式,各自有一些代表性。

  • 语法分析助手:从老版uni-app迁移到uni-app x蒸汽模式。开发者反馈“速度确实快很多”,同一套代码还发布了小程序和PC版;
  • 金股讯:从uni-app x VDOM模式升级到蒸汽模式。相比uts,蒸汽模式基于ts/js,更符合开发习惯;
  • 鲸鲨记账:从Flutter重构到uni-app x蒸汽模式。团队为了兼容鸿蒙而迁移,并感受到了长列表性能的提升。

这3个访谈案例均是应用商店下载量十万级的应用,都是积累了大量真实用户、正式运营的产品。

他们选择uni-app x蒸汽模式,需要考虑已有业务的延续、真实用户的体验,以及后续版本的持续迭代。
对准备选型的开发者而言,这样的案例实践更有参考价值,大家可以直接到应用商店下载体验。

语法分析助手:1个人1个月,速度和多端复用都有收获

语法分析助手是一款英语学习工具。为了后续增加更多原生功能,开发者决定从老版uni-app升级,此前曾尝试升级到uni-app x VDOM模式,但未成功,最终在蒸汽模式下完成了迁移。

这次迁移由1个人完成,耗时约1个月,主要借助Cursor。 老项目中大量业务代码本来就使用TS编写,迁移到蒸汽模式比较方便。

谈到升级后的体验,他的回答很直接:

> “速度确实快很多。”

另一项收获是多端复用。开发者反馈,这次用同一套代码发布了App、小程序、网站,还提供了桌面版本。其中,桌面版本采用Tauri封装Web内容。

大家可以在App Store、华为应用市场下载体验,也可以在微信小程序中搜索“语法分析助手”。

金股讯:约15人天,让开发方式更符合习惯

金股讯在最初App立项时,就选择了uni-app x,也是较早基于uni-app x上架应用市场的产品之一。本次升级,是从VDOM模式迁移到蒸汽模式。

他的升级动机很有代表性。开发者并没有把提升性能作为主要原因,而是为了开发效率:

> “蒸汽模式更符合开发习惯。”

进一步交流后,这里的“开发习惯”,主要指UTS与普通TS/JS开发方式的差异。在AI开发时代,标准ts比uts有更明显的提效优势。

升级投入了多少工作量?开发者给出的口径是:

> “在AI的加持下,大概要15个人天。”

迁移过程中,开发者使用了uni-agent和Claude Code。

仍在使用uni-app x vdom 模式的开发者,可以参考金股讯的做法,借助AI升级到蒸汽模式,让开发效率更高。

uni-agent已经内置 vdom 升级蒸汽模式的专项skill,升级速度会更快。

金股讯在应用宝、App Store和鸿蒙应用市场均有下载入口。

鲸鲨记账:从Flutter迁入,长列表体验获得提升

鲸鲨记账是一款AI记账工具。和前两个案例不同,它原来使用Flutter开发。为了兼容鸿蒙,团队选择用uni-app x重构,并且直接采用蒸汽模式。

据团队介绍,大约1个月完成重构,大量使用了AI辅助。

具体操作为:让AI先理解原项目的业务逻辑,再用AI重写uni-app x代码。工程师更多时间花在页面样式还原和测试验收上。

这也是已有产品更换技术栈进行重构时,可以借鉴的一种做法:把原项目作为业务参考,让AI辅助转换实现,开发者集中精力检查页面、交互和功能是否符合预期。

从Flutter迁到uni-app x,性能体验有没有变化?开发者的反馈是:

> “长列表方面性能是有提升的,普通页面差不多,基本感受不出来。”

这个反馈很具体。普通页面保持了接近的使用感受,而长列表这样的场景获得了性能收益。对记账类产品而言,账单记录这类长列表,是值得重点关注的体验环节。

再加上支持鸿蒙的技术路线,鲸鲨记账从Flutter迁移到uni-app x蒸汽模式,同时满足了多端覆盖和性能体验两方面的需求。

大家可以通过应用宝、App Store和鸿蒙应用市场下载体验。

当然除了长列表,uni-app x 蒸汽模式相比flutter,还具备更多优势:

  • 支持iOS液态玻璃、鸿蒙沉浸光感
  • 没有原生混合渲染问题
  • 支持发布后的热更新

基础API和开发体验,还需要继续完善

在此次回访中,开发者也提出了一些很具体的建议。

  • 金股讯开发者希望补齐QQ登录、QQ分享等常用能力
  • 语法分析助手开发者则提到了SQLite数据库等基础接口的需求 (官方已提供SQLite插件:https://ext.dcloud.net.cn/plugin?id=29856)
  • 也有开发者反馈全量编译等待较长 (后续已解决,5.31+ 使用lang="ts"或"js")

这些都是实际业务中会用到的功能,支持得越完善,开发者就越能减少自行适配的工作,开发体验就更丝滑。

AI辅助升级,已经有实际项目验证

这3个案例有一个共同点:开发者都借助了AI。

AI非常擅长翻译。

AI可以帮助理解已有业务、转换代码、调整页面,再配合开发者逐项验证。原来需要大量手工处理的迁移工作,现在有了更高效的完成方式。

其中,金股讯已经实际使用uni-agent参与升级。语法分析助手和鲸鲨记账迁移时主要使用Cursor,我们也向他们推荐了uni-agent。

对于准备升级的开发者,建议优先试试uni-agent。

uni-agent是DCloud面向uni-app(x)开发者提供的AI编程助手,内置了针对uni产品的技能和知识库,完善了开发、验证、测试工具链。

  • uni-agent的知识库包含了uni最新文档,它帮助训练数据滞后的AI大模型更好的了解最新的uni-app x。并且比联网搜索更快、更准、更省token。
  • uni-agent连接了DCloud issues和DCloud问答社区的数据。可以发现某个bug是否已经存在、在哪个版本已经修复。
  • uni-agent连接了插件市场精品插件数据,可以判断某个开发者的需求无需从头编写,直接用精品库,更快、更省token、更稳定。
  • uni-agent集成了大量自动化工具,让AI可以自动化工作,自己写代码、自己调试、自己测试、自己发布,数倍提升效率
  • uni-agent支持手机App,让你随时随地指挥你的数字员工。还可以在手机端打包,预览web和apk的效果。
  • uni-agent支持模型分层,会把简单问题交给便宜的模型、复杂问题交给专业的模型,节省token成本。

对于uni-app和uni-app x VDOM应用的升级:

  • 对于uni-app x VDOM模式,uni-agent已有升级蒸汽模式的内置技能,官方的hello uni-app x示例有400多个页面,还有uni-id、uni-pay、uni-ai等开源库,之前都是VDOM选项式页面,均借助uni-agent转升级到了蒸汽模式。VDOM升级蒸汽模式

  • 老版uni-app项目,建议按照官方升级指南,让uni-agent分阶段处理。

uni-agent会大幅提升迁移效率,相信在现在的uni-agent支持下,开发者实际迁移的耗时会比首批案例开发者的耗时短更多。

现在,可以用自己的项目试一试了

这些真实项目,让迁移投入和升级收益有了具体参照:速度、长列表体验、开发习惯和多端代码复用,都有开发者给出了自己的答案。

对于还停留在早期印象的开发者,建议重新体验一下今天的uni-app x蒸汽模式。

欢迎分享你的蒸汽模式开发经验

如果你已经用上uni-app x蒸汽模式,欢迎到知乎、掘金、CSDN等技术社区,或自己的公众号发表文章,分享真实项目中的开发实践。

文章发布后,欢迎把文章链接发给我们,申请uni-agent资源包等奖励。希望这些资源能支持大家继续开发、持续分享,让更多开发者了解uni-app x蒸汽模式,也帮助正在选型或准备升级的团队少走弯路。

我们尤其欢迎老项目升级到蒸汽模式的经验:原来采用什么技术方案,升级的诉求,投入了多少时间,遇到了哪些问题,又是怎样解决的,升级后在性能体验、开发效率和多端复用上有哪些收获。新项目从技术选型到开发上线的经验,也同样欢迎。

也欢迎已经使用uni-app x的开发者,在案例页贡献自己的应用。

继续阅读 »

uni-app x 蒸汽模式,拥有快于原生的渲染速度、支持发布后热更新、AI友好等特点。

但开发者仍然有疑问:

  1. uni-app x蒸汽模式成熟了吗?
  2. 有没有真正上线运营的App?
  3. 老项目升级到蒸汽模式,要投入多少人、花多长时间?

最近,我们回访了一些uni-app x 蒸汽模式的开发者,收集了一批案例,采访到了开发者的真实感受,他们花费了多久、对什么满意、还有什么期待。

先来看案例。

uni-app x 蒸汽模式案例

打开uni-app x官方案例页,可以看到很多蒸汽模式案例,其中不乏应用商店下载量超百万、千万的大用户量应用。

应用范围分布较广,覆盖政企应用、心理咨询、回收电商、教育学习、金融股讯、娱乐社交、生活工具等不同业务。

> 下载量数据来源于各大应用商店公开可查看的下载数累加。

应用商店下载量千万级

下载体验:Android · iOS · 鸿蒙,微信小程序搜索“抱抱海心理”。

应用商店下载量百万级

下载体验:Android · iOS,微信小程序搜索“京大大回收”。

应用商店下载量十万级

这些应用在各平台的下载入口,可查看完整案例页。

更多案例

完整案例介绍及Android、iOS、鸿蒙等平台下载入口,请查看uni-app x官方案例页。也欢迎开发者提交自己的案例。

开发者访谈

我们访谈了一些开发者,他们有的之前使用老版uni-app、有的使用uni-app x的vdom模式、有的使用flutter,都迁移到了uni-app x 蒸汽模式,各自有一些代表性。

  • 语法分析助手:从老版uni-app迁移到uni-app x蒸汽模式。开发者反馈“速度确实快很多”,同一套代码还发布了小程序和PC版;
  • 金股讯:从uni-app x VDOM模式升级到蒸汽模式。相比uts,蒸汽模式基于ts/js,更符合开发习惯;
  • 鲸鲨记账:从Flutter重构到uni-app x蒸汽模式。团队为了兼容鸿蒙而迁移,并感受到了长列表性能的提升。

这3个访谈案例均是应用商店下载量十万级的应用,都是积累了大量真实用户、正式运营的产品。

他们选择uni-app x蒸汽模式,需要考虑已有业务的延续、真实用户的体验,以及后续版本的持续迭代。
对准备选型的开发者而言,这样的案例实践更有参考价值,大家可以直接到应用商店下载体验。

语法分析助手:1个人1个月,速度和多端复用都有收获

语法分析助手是一款英语学习工具。为了后续增加更多原生功能,开发者决定从老版uni-app升级,此前曾尝试升级到uni-app x VDOM模式,但未成功,最终在蒸汽模式下完成了迁移。

这次迁移由1个人完成,耗时约1个月,主要借助Cursor。 老项目中大量业务代码本来就使用TS编写,迁移到蒸汽模式比较方便。

谈到升级后的体验,他的回答很直接:

> “速度确实快很多。”

另一项收获是多端复用。开发者反馈,这次用同一套代码发布了App、小程序、网站,还提供了桌面版本。其中,桌面版本采用Tauri封装Web内容。

大家可以在App Store、华为应用市场下载体验,也可以在微信小程序中搜索“语法分析助手”。

金股讯:约15人天,让开发方式更符合习惯

金股讯在最初App立项时,就选择了uni-app x,也是较早基于uni-app x上架应用市场的产品之一。本次升级,是从VDOM模式迁移到蒸汽模式。

他的升级动机很有代表性。开发者并没有把提升性能作为主要原因,而是为了开发效率:

> “蒸汽模式更符合开发习惯。”

进一步交流后,这里的“开发习惯”,主要指UTS与普通TS/JS开发方式的差异。在AI开发时代,标准ts比uts有更明显的提效优势。

升级投入了多少工作量?开发者给出的口径是:

> “在AI的加持下,大概要15个人天。”

迁移过程中,开发者使用了uni-agent和Claude Code。

仍在使用uni-app x vdom 模式的开发者,可以参考金股讯的做法,借助AI升级到蒸汽模式,让开发效率更高。

uni-agent已经内置 vdom 升级蒸汽模式的专项skill,升级速度会更快。

金股讯在应用宝、App Store和鸿蒙应用市场均有下载入口。

鲸鲨记账:从Flutter迁入,长列表体验获得提升

鲸鲨记账是一款AI记账工具。和前两个案例不同,它原来使用Flutter开发。为了兼容鸿蒙,团队选择用uni-app x重构,并且直接采用蒸汽模式。

据团队介绍,大约1个月完成重构,大量使用了AI辅助。

具体操作为:让AI先理解原项目的业务逻辑,再用AI重写uni-app x代码。工程师更多时间花在页面样式还原和测试验收上。

这也是已有产品更换技术栈进行重构时,可以借鉴的一种做法:把原项目作为业务参考,让AI辅助转换实现,开发者集中精力检查页面、交互和功能是否符合预期。

从Flutter迁到uni-app x,性能体验有没有变化?开发者的反馈是:

> “长列表方面性能是有提升的,普通页面差不多,基本感受不出来。”

这个反馈很具体。普通页面保持了接近的使用感受,而长列表这样的场景获得了性能收益。对记账类产品而言,账单记录这类长列表,是值得重点关注的体验环节。

再加上支持鸿蒙的技术路线,鲸鲨记账从Flutter迁移到uni-app x蒸汽模式,同时满足了多端覆盖和性能体验两方面的需求。

大家可以通过应用宝、App Store和鸿蒙应用市场下载体验。

当然除了长列表,uni-app x 蒸汽模式相比flutter,还具备更多优势:

  • 支持iOS液态玻璃、鸿蒙沉浸光感
  • 没有原生混合渲染问题
  • 支持发布后的热更新

基础API和开发体验,还需要继续完善

在此次回访中,开发者也提出了一些很具体的建议。

  • 金股讯开发者希望补齐QQ登录、QQ分享等常用能力
  • 语法分析助手开发者则提到了SQLite数据库等基础接口的需求 (官方已提供SQLite插件:https://ext.dcloud.net.cn/plugin?id=29856)
  • 也有开发者反馈全量编译等待较长 (后续已解决,5.31+ 使用lang="ts"或"js")

这些都是实际业务中会用到的功能,支持得越完善,开发者就越能减少自行适配的工作,开发体验就更丝滑。

AI辅助升级,已经有实际项目验证

这3个案例有一个共同点:开发者都借助了AI。

AI非常擅长翻译。

AI可以帮助理解已有业务、转换代码、调整页面,再配合开发者逐项验证。原来需要大量手工处理的迁移工作,现在有了更高效的完成方式。

其中,金股讯已经实际使用uni-agent参与升级。语法分析助手和鲸鲨记账迁移时主要使用Cursor,我们也向他们推荐了uni-agent。

对于准备升级的开发者,建议优先试试uni-agent。

uni-agent是DCloud面向uni-app(x)开发者提供的AI编程助手,内置了针对uni产品的技能和知识库,完善了开发、验证、测试工具链。

  • uni-agent的知识库包含了uni最新文档,它帮助训练数据滞后的AI大模型更好的了解最新的uni-app x。并且比联网搜索更快、更准、更省token。
  • uni-agent连接了DCloud issues和DCloud问答社区的数据。可以发现某个bug是否已经存在、在哪个版本已经修复。
  • uni-agent连接了插件市场精品插件数据,可以判断某个开发者的需求无需从头编写,直接用精品库,更快、更省token、更稳定。
  • uni-agent集成了大量自动化工具,让AI可以自动化工作,自己写代码、自己调试、自己测试、自己发布,数倍提升效率
  • uni-agent支持手机App,让你随时随地指挥你的数字员工。还可以在手机端打包,预览web和apk的效果。
  • uni-agent支持模型分层,会把简单问题交给便宜的模型、复杂问题交给专业的模型,节省token成本。

对于uni-app和uni-app x VDOM应用的升级:

  • 对于uni-app x VDOM模式,uni-agent已有升级蒸汽模式的内置技能,官方的hello uni-app x示例有400多个页面,还有uni-id、uni-pay、uni-ai等开源库,之前都是VDOM选项式页面,均借助uni-agent转升级到了蒸汽模式。VDOM升级蒸汽模式

  • 老版uni-app项目,建议按照官方升级指南,让uni-agent分阶段处理。

uni-agent会大幅提升迁移效率,相信在现在的uni-agent支持下,开发者实际迁移的耗时会比首批案例开发者的耗时短更多。

现在,可以用自己的项目试一试了

这些真实项目,让迁移投入和升级收益有了具体参照:速度、长列表体验、开发习惯和多端代码复用,都有开发者给出了自己的答案。

对于还停留在早期印象的开发者,建议重新体验一下今天的uni-app x蒸汽模式。

欢迎分享你的蒸汽模式开发经验

如果你已经用上uni-app x蒸汽模式,欢迎到知乎、掘金、CSDN等技术社区,或自己的公众号发表文章,分享真实项目中的开发实践。

文章发布后,欢迎把文章链接发给我们,申请uni-agent资源包等奖励。希望这些资源能支持大家继续开发、持续分享,让更多开发者了解uni-app x蒸汽模式,也帮助正在选型或准备升级的团队少走弯路。

我们尤其欢迎老项目升级到蒸汽模式的经验:原来采用什么技术方案,升级的诉求,投入了多少时间,遇到了哪些问题,又是怎样解决的,升级后在性能体验、开发效率和多端复用上有哪些收获。新项目从技术选型到开发上线的经验,也同样欢迎。

也欢迎已经使用uni-app x的开发者,在案例页贡献自己的应用。

收起阅读 »

踩了个坑,uniappX的蒸汽模式下,animation-name不要使用驼峰写法,IOS设备会无效果

animation

animation: areaPeopleEditFadeIn 2s linear;

@keyframes areaPeopleEditFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

这个代码,小程序和安卓正常。但是IOS不正常。。。
多次测试下来,是areaPeopleEditFadeIn这个驼峰命名导致的。
将其改为area-people-edit-fadein,IOS就能看到正常效果了。

继续阅读 »

animation: areaPeopleEditFadeIn 2s linear;

@keyframes areaPeopleEditFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

这个代码,小程序和安卓正常。但是IOS不正常。。。
多次测试下来,是areaPeopleEditFadeIn这个驼峰命名导致的。
将其改为area-people-edit-fadein,IOS就能看到正常效果了。

收起阅读 »

让页面切换,也成为体验的一部分|uni-app x 原生路由插件

uni-app x

让每一次进入与返回,都自然衔接。

kongbai-native-route 原生路由 现已上线。

面向 uni-app x · iOS / Android,把页面转场、手势返回与弹窗交互放进同一套导航体验。打开详情时的层次、滑动返回时的反馈、收起面板后的衔接,都可以做得更细腻。

前往插件市场 · 查看详情与试用

先看真机,再感受细节

动画有没有合适的节奏,拖动与回弹是否自然,实际操作最直观。

观看 iOS 真机演示

观看 Android 真机演示

也可以从插件市场导入示例项目,在自己的设备上体验。

01 / 九种转场,让内容有自己的出场方式

日常页面,轻快进入;重点内容,适当强调层次。根据页面的用途选择转场,让动效服务于内容。

横向滑动 · 景深推进 · 层叠上浮
斜向旋入 · 立体翻转 · 中心展开
交叉淡入 · 顶部落入 · 纵向翻入

进入与返回的动画时长可以按需调整,也支持无动画切换。从列表到详情,从浏览到编辑,为不同操作安排合适的节奏。

02 / 跟手返回,把选择留在手指之间

从页面左侧边缘滑动,页面跟随手势响应。完成滑动,返回上一页;中途取消,回到当前页面。

对于仍保留在导航流程中的上一页,返回后可以继续之前的输入与浏览位置。看完详情接着浏览,临时查看其他内容后继续填写,让操作保持连贯。

03 / 三种弹窗,承接不同分量的内容

半屏 · 先展示重点,再展开更多

支持半屏与展开状态切换,以及下拉关闭。适合筛选条件、选项面板、内容预览和轻量编辑,让用户在当前场景中完成操作。

遮罩 · 聚焦眼前这一步

适合信息提示、操作确认与短内容展示。示例提供点击空白区域关闭的交互,用清晰的前后层次突出当前内容。

卡片 · 展开一段独立的操作

拖动顶部区域,卡片随手势变化;松手后,根据操作关闭或回弹。适合详情预览与临时操作面板,让展开和收起都有自然的反馈。

弹窗支持多层打开、逐层返回。 关闭上层后,可以继续操作仍打开的下层内容,保留其中已输入的文字与当前状态。

04 / 深浅外观,与应用保持协调

支持浅色与深色外观切换,覆盖导航栏和弹窗的相关外观,示例项目可直接体验两种主题。

接入业务时,再配合页面自身的文字、背景与组件配色,让导航和内容保持统一。

从示例开始,把体验带进自己的项目

使用 HBuilderX 5.26 + VDOM 打开示例项目,编译原生插件并运行到 iOS 或 Android 真机,即可体验页面转场、手势返回、弹窗与主题切换。

插件市场已提供完整接入说明,包含项目配置、业务页面注册、启动导航、页面跳转与返回。按文档接入后,应用可在启动页就绪时自动进入业务首页。

当前适用于 uni-app x 的 iOS / Android App,暂不支持普通 uni-app、Web、小程序、HarmonyOS 和 Vapor。 平台环境与系统版本要求请查看插件市场说明;两端的手势与外观细节请结合对应演示体验。页面关闭或应用重启后的数据保存,仍由业务自行处理。

把页面之间的衔接,也认真做好。

如果你正在打磨 uni-app x App 的交互细节,欢迎先看演示,再用示例项目体验。

查看 kongbai-native-route 原生路由 · 获取插件与接入文档 →

欢迎在评论区交流使用场景与体验建议。

继续阅读 »

让每一次进入与返回,都自然衔接。

kongbai-native-route 原生路由 现已上线。

面向 uni-app x · iOS / Android,把页面转场、手势返回与弹窗交互放进同一套导航体验。打开详情时的层次、滑动返回时的反馈、收起面板后的衔接,都可以做得更细腻。

前往插件市场 · 查看详情与试用

先看真机,再感受细节

动画有没有合适的节奏,拖动与回弹是否自然,实际操作最直观。

观看 iOS 真机演示

观看 Android 真机演示

也可以从插件市场导入示例项目,在自己的设备上体验。

01 / 九种转场,让内容有自己的出场方式

日常页面,轻快进入;重点内容,适当强调层次。根据页面的用途选择转场,让动效服务于内容。

横向滑动 · 景深推进 · 层叠上浮
斜向旋入 · 立体翻转 · 中心展开
交叉淡入 · 顶部落入 · 纵向翻入

进入与返回的动画时长可以按需调整,也支持无动画切换。从列表到详情,从浏览到编辑,为不同操作安排合适的节奏。

02 / 跟手返回,把选择留在手指之间

从页面左侧边缘滑动,页面跟随手势响应。完成滑动,返回上一页;中途取消,回到当前页面。

对于仍保留在导航流程中的上一页,返回后可以继续之前的输入与浏览位置。看完详情接着浏览,临时查看其他内容后继续填写,让操作保持连贯。

03 / 三种弹窗,承接不同分量的内容

半屏 · 先展示重点,再展开更多

支持半屏与展开状态切换,以及下拉关闭。适合筛选条件、选项面板、内容预览和轻量编辑,让用户在当前场景中完成操作。

遮罩 · 聚焦眼前这一步

适合信息提示、操作确认与短内容展示。示例提供点击空白区域关闭的交互,用清晰的前后层次突出当前内容。

卡片 · 展开一段独立的操作

拖动顶部区域,卡片随手势变化;松手后,根据操作关闭或回弹。适合详情预览与临时操作面板,让展开和收起都有自然的反馈。

弹窗支持多层打开、逐层返回。 关闭上层后,可以继续操作仍打开的下层内容,保留其中已输入的文字与当前状态。

04 / 深浅外观,与应用保持协调

支持浅色与深色外观切换,覆盖导航栏和弹窗的相关外观,示例项目可直接体验两种主题。

接入业务时,再配合页面自身的文字、背景与组件配色,让导航和内容保持统一。

从示例开始,把体验带进自己的项目

使用 HBuilderX 5.26 + VDOM 打开示例项目,编译原生插件并运行到 iOS 或 Android 真机,即可体验页面转场、手势返回、弹窗与主题切换。

插件市场已提供完整接入说明,包含项目配置、业务页面注册、启动导航、页面跳转与返回。按文档接入后,应用可在启动页就绪时自动进入业务首页。

当前适用于 uni-app x 的 iOS / Android App,暂不支持普通 uni-app、Web、小程序、HarmonyOS 和 Vapor。 平台环境与系统版本要求请查看插件市场说明;两端的手势与外观细节请结合对应演示体验。页面关闭或应用重启后的数据保存,仍由业务自行处理。

把页面之间的衔接,也认真做好。

如果你正在打磨 uni-app x App 的交互细节,欢迎先看演示,再用示例项目体验。

查看 kongbai-native-route 原生路由 · 获取插件与接入文档 →

欢迎在评论区交流使用场景与体验建议。

收起阅读 »

死磕四年,投资数亿,uni-app做出了比原生渲染还快的跨平台框架

性能 uni-app x

> 原文是首发于开源中国公众号的访谈,各级标题为主持人问题,原文详见:​https://mp.weixin.qq.com/s/erfEFl4ZNKMkoa7MViFCpA

背景:

uni-app x 蒸汽模式,发表了比原生渲染还快的benchmark。这让业内大为震惊。

本期请来了DCloud的CEO王安,从设计者的角度聊聊,为什么做这个产品、为什么这么做而不那么做、性能提升的关键在哪里?

1. 跨平台框架赛道,并非当下VC的关注领域,DCloud投入4年,花了几个亿搞 uni-app x,这种执着和冒险的底层逻辑是什么?

我做跨平台已经二十多年了,老骨灰了。我做的产品,虽然也有很大的市场体量,但也一直被开发者们吐槽。

uni-app的第一代产品,在小程序领域是市占率第一的,不管大厂小厂都在用。因为用uni-app开发小程序不会比原生小程序性能差,还能跨平台。

但在App平台,市占率只有百分之十几,主要集中在中小开发者。原因就是App平台的性能不能满足中大开发者的需求。

所以App平台的性能问题,对我们一直是如鲠在喉。

2022年,我们内部讨论 uni-app x 的立项问题,能不能让app像小程序那样,做到与原生开发相比没有性能损失。

其实团队内部,对 uni-app x 的立项是有反对声音的。如果我们守住原有盘子,做好服务和商业化,能让团队成员的日子过的更好点。

但一旦要立项解决跨平台框架的性能问题,这注定不是一个短期能解决的问题,投入肯定不少,机会风险很大。

当时觉得就是不服气,觉得这辈子不能就一直解决不了跨平台的性能问题,活得太憋屈了,想赌一把。

其实当时我也不知道这个项目一做就是4年,我以为2年能搞定。当初如果知道要4年,那承受的压力会非常大,不一定能下得了决心。

2. 这4年中间,有过犹豫和后悔吗?

中间是走了很多弯路啊。有的路走了很久才发现不通,肯定是非常郁闷的。
但并不会因为走错路而后悔。你在登一个没人登过的山,走错路是正常的。
要说后悔的话,从开始就应该建立一个更高效的探路机制,能更快的验证路线的正确性。
这个机制,我们直到2025年才完善,这是我最大的教训吧。

中间有段时间非常焦虑,大约25年初的时候,当时觉得搞了快3年了,不达预期怎么办。
幸好几个月后,25年夏天,一系列技术突破后,终于重拾信心,知道自己搞对了。

3. 你们建的这个高效的探路机制,是什么?

类似实验室。
就是别着急做产品,别着急提代码。
就是先做实验,体系化的、严谨的做实验。
但是然后呢,你又会发现还不能着急做实验,为了能严谨的输出各种实验报告,其实需要做很多基建,你要先投入大量资源去建实验室的基建。

我们以前吃了不少亏,调研报告不准确,误导了技术决策。这都需要一套良好的实验基建、严谨的实验规范流程才能解决。

有很长一段时间,我看不到产品里提交代码,我也不关注这个指标。我每天只看实验报告。

这个过程可能很多公司都没有,大多数公司的技术考核,可能是产品需求的实现工期、完成度、bug数量。
但对于uni-app x蒸汽模式这种产品来讲,实验室机制是它最终能做出来的核心原因之一。

4. 非常延迟满足啊。不做产品先做实验,不做实验先搞批量实验的基建。那这注定不是短期投入。然后我有点好奇,当初是怎么说服团队成员同意这个指标的:比原生渲染还快。

没有。当初如果定一个比原生渲染快2~3倍的目标,别说团队成员了,我自己都不信。

当初的目标,就是参考小程序,uni和原生小程序没有性能差别。然后app也做到和原生app一样,没有性能差别就行。

就这,当时团队成员都不同意,说做不到,最多能做到15%的性能差距,就是比原生慢15%。为了这15%拉扯了半天。

但后来发现,实现到15%的性能差距,仍然困难重重。只能一路往底层追,从系统组件源码、追到系统底层渲染原理,往下又追到编程语言原理,又往下追到硬件层操作原理。

直到你把整个事情都搞明白了,这时候你才发现,传统的原生渲染一层一层套轮子,叠了太多层了,性能优化空间太大了。
不要说达到原生渲染的性能了,比原生渲染快几倍都没问题。

以前如果我diss一个团队成员,说你做的这个东西性能太差了,怎么连原生的同类组件的性能都拼不过。这个人肯定会觉得这个老板有病。
但现在我可以随便diss,在我们团队,做到比原生快并不难,已经是团队共识了。
当然一个新入职的人,还需要一段时间才能适应这种文化。

5. 原生渲染那么多年的优化,真的是流程很长吗?能举个例子吗?

新的挑战者打破旧的,心里是需要这个世界都是草台班子的信念的。虽然这个新的挑战者也会被后来的挑战者视为草台班子。

计算机工业发展了这么多年,长成了庞然大物,轮子一层叠一层,减肥的空间其实挺多的。

我们公司有个面试题,手机左上角的那个返回箭头,怎么绘制是性能最高的。

很多人在项目中使用字体图标,矢量缩放不失真,暗黑能变色,跨平台都能用,不同平台渲染结果一致,听着挺好。

但是一个字体图标,从你写代码加载字体那刻,到屏幕上最终显示出来,经过了非常多环节。

你可能觉得你就写了几行代码,还好吧。但实际上,

  • 首先系统需要从IO里加载你的字体库到内存里,
  • 然后需要构造字体管理对象,解析字体头文件,确定字体的很多参数。
  • 然后要去字体中的众多字里匹配,你想要显示哪个字符。
  • 然后要做文字塑型,确定连字等字间距处理。
  • 然后做换行、双向文本流(有从左到右的文字、也有从右到左的,还有混合的)。
  • 然后做文字测量,把高宽定下来。
  • 然后做光栅化或三角形化。
  • 然后提交给GPU。
  • 最后整体合成上屏。

这里面提到的每个步骤,都有专业的库负责。这些库的源码如果你看过的话,逻辑都非常多。

在Android和鸿蒙上,这些逻辑还涉及原生的kt/arkts和c库的反复交互。

你只看到你写的几行代码,但实际上背后是几十年的文字系统各种库的协作配合。

你知道了背后的流程多长,你肯定想跳过一些流程,比如就一个返回箭头图标,它理应不涉及换行和双向文本流,但这里又有另一个坑。
不同os,上述流程的可控性是不一样的,有的os提供了专门的api可以跳过换行和双向文本流流程,有的os没有提供。

如果你对文字系统有研究兴趣,可以去看看harfbuzz、freetype、icu这些c库的源码,都是几十年的老库了,每个库都是不同的顶级大牛写的。
但作为一个应用开发者,想优化这块,真没有ROI。

5.1. 所以uni蒸汽模式中的返回箭头、文字渲染,都比原生自带的要更快?

是的。首先要投入大量精力研究每个系统的底层,然后优化,然后再给开发者暴露简单易用的跨平台写法。

6. 你们提供了一个benchmark,一个界面中创建了4050个view和text,渲染耗时比原生短2~3倍。这个测试例让很多人不解、甚至不信。不知道这个访谈中,王老师能否透露一些细节原因。比如这个测试例,是因为uni蒸汽的文字系统更快吗?有些人觉得这可能是你们的测试例定向优化,能解释下吗?[^1]

没有定向优化。是反过来的,如何测试一个渲染系统在处理view、text这种基础组件时速度更快?带着这个问题,我们才在一个屏幕内大量创建view和text来做实验。

你想创建多少个、怎么排布,都可以,文字是数字还是汉字都可以,结论都是 uni-app x 蒸汽模式更快。

这种快,来源于很多方面。uni的文字系统真的更快,如果你把4050的例子改一下,在里面放置更多text组件,你会发现uni比原生快了不止2~3倍,会快更多,界面上文字越多,uni的优势越明显。

除了文字系统快,整个template和Style区域,执行的都比原生快。在4050的例子里,是模板里的v-for创建这些view和text,这些代码都是编译成字节码/机器码的,这些高度优化的二进制码性能更高。

uni蒸汽的排版系统也更快,不管是什么组件,view、text还是其他组件,都要排版。uni蒸汽统一是flex排版,这套排版系统的性能也比原生提供的那些排版布局系统快。并且这套排版也不会为4050这种示例做定向优化。

4050这个例子有拍平和非拍平2套测试数据,都是uni蒸汽快。有些人认为uni蒸汽的4050快是拍平的功劳。并不是的,这2套数据之差,已经说明了拍平在这里能提供的价值。

当然拍平也是我们给开发者提供的一个挺好的性能优化方案,让开发者可以方便构造更高性能的UI。一个简单的flatten属性,这种开发方式比原生高效非常多。

7. 有的原生工程师表示不服,他们也可以在原生的基础上优化代码,跑出更快的原生版数据。你怎么看?

我们需要牢记初心,这个测试例比的是view和text的创建速度,不是比4千多个方格子固定排放后如何快速渲染。

uni提供的就是标准的view和text组件,没有定向优化。原生的对比测试例,也不能为了这个4050的测试例而定向优化。

有的原生工程师说,我用draw的方式,只创建1个view,在上面自绘出4050个方格子出来,不测量不排版,所有格子等宽。
那这就是原生为这个测试例定向优化了。这就没法比较原生的view、text,和uni的view、text到底谁快了、排版系统到底谁快了。

如果你基于draw,自己实现文字测量、布局排版,能布任何界面,那其实就是自己做了一套渲染系统。其实这就是 Compose UI 在Android上做的事情。
首先真的做出来这套系统是非常难的,其次做出来性能也很难匹敌uni蒸汽。

Compose UI 的view和text同屏4050测试例,我们是在benchmark里公开过的,比Android原生view还慢,更比不过uni蒸汽。[^1]

如果某个原生工程师有兴趣钻研此道,自己一个人搞是不行的,这是大工程,还是加入DCloud一起干吧,哈哈。

8. 我有个疑惑,为什么操作系统那么多专业的人,不去把原生UI优化的更快呢?给三方这么大的超越空间

我不是操作系统团队的人,我也不知道他们怎么想的。我可以推测下,随便聊了。

可能有的系统厂商,觉得自己已经够快了,没有竞争压力给他们。因为要动这些底层的东西,代价不低,立项时总会有人问花这么多钱能带来什么财务回报?

很多领先的厂商,有一个产业义务,就是推进用户买新设备。这对用户来说听着有点恶心。但产业链的兄弟们持续有钱赚也很重要,大家有钱可以持续创新,推进产业进步。

然后就是组织大了,跨部门协调也难。
比如Android,原生的kotlin是一拨人,各种c库是另一拨人,现有的框子是一层一层叠起来的,想压穿,需要协调非常多人。

还有的技术栈喜欢自举,比如java/kotlin,很多重要的库是java写的、而不是c或汇编写的。当你的产品哲学是自举优先而不是性能优先,就很难把性能做到极致。
你看v8的源码,那不止是优秀的c代码,很多地方要上汇编的,比如json解析,这就是性能优先。

当然了,如果操作系统的人真的愿意好好优化,肯定是可以反超uni蒸汽现在的benchmark数据的。

不过呢,其实uni蒸汽,也还可以再提升性能,只不过我们也一样,觉得快了2~3倍已经足以。现在立项投很多人力财力再让uni蒸汽快4倍5倍,没有性价比。
如果原生优化的更快了,我们也会立项再追。

还是那句话,现代计算机工业这么多年,这个庞然大物减肥的空间挺多的。

9. AI时代,跨平台框架的价值是否要重新看待了。最近shopify放弃react native,转原生。这是不是未来的趋势?你们搞了4年多,这个跨平台框架是不是在AI时代生不逢时了?

AI刚来时,我也有点焦虑,担心AI会颠覆掉跨平台框架。
但后来实践、观察、深入思考,心就定了。

首先看数据,AI Coding后,uni的应用数量进一步增长,并不是下降。
然后看我们内部的实践,我们除了框架,也做应用,比如uni-ai、uni-im,一个是开源的AI聊天客户端,一个是开源的IM系统,我们和我们的开发者沟通,都是在uni-im上。

如果没有 uni-app x 和uniCloud,这些项目在我们内部根本无法立项。是因为实际这2个项目,只有一个前端工程师。

如果要用原生来做,2个应用系统,5个客户端平台,Android、iOS、鸿蒙、web、微信小程序,客户端+服务器+测试,即使每个人都在用ai,最少也是十几个人。

除了开发,还要考虑。不同平台,不但代码不同,测试例、测试工具链也不同。但在uni下,不止代码统一了、测试例也是一套。

uni这边,真的从前到后,从开发到测试,所有平台,只有一个人。他用着AI并行开发,快速完成了2个系统。这生产效率和成本,其他方案真的比不了。

主持人:有的人说,我不要6个人,我一个人就让ai干活,写5个平台的原生代码加服务器和测试例,不行吗?

AI只是工具,它需要人用,需要人背锅。工具不背锅。进度延期、线上出bug,你无法甩锅给AI。只有懂AI写的代码,才能hold住,才能担起责任。

尤其是面向广大用户的、涉及重要权益的系统,AI写的代码有问题,造成线上损失,是需要你担责的。

做一些压力不大的系统,比如自用的、有限人使用的内部管理只读报表,可以不看代码。但真要上线,要面向用户承担责任,那人得把关。

回到上一个问题,我们做的uni-ai,它的性能比原生开发的AI客户端都好,和ChatGPT、豆包、deepseek相比帧率都更稳。

如果使用原生开发,每个平台都达到这个性能水平,那我刚才说的十几个人都还不够,那投资得非常大了。

所以uni系产品带来的价值,生产力,非常大。

AI虽然是个工具,但你让它干活时,它也想用好工具,它不会用好工具它就不是个好AI,对吧。
在AI时代,写代码并不是不需要框架,只是需要好框架。
比如ts、vue,这些都是框架,你也可以让AI去写原始的js,但人们为什么不这么做呢?
因为明显是用ts、vue这些好框架,写代码更轻松、更容易保障性能和质量,也更省时间和token。

在AI时代,并不是跨平台框架没有价值了。而是需要更好的跨平台框架。

一个跨平台框架,只跨2个平台,性能比原生差了不少,那么shopify认为AI降低了我的成本,原生带来了更高性能,这ROI是合适的,就会转原生。
假使,我说的是假使,react native的渲染性能比原生更快,那shopify怎么会切原生去?闲的慌啊。

所以AI时代,那些问题比较多的跨平台框架,不管是性能、质量、跨平台数量,如果做的不好,就会没落。
我个人觉得,AI Coding时代,在App平台,可能未来只有原生和uni-app x。其他跨平台框架估计要走下坡路。
这会和小程序领域类似,拼到最后只剩原生和uni-app了。

我这话说的,听起来有点满,其实我是深入思考过的。
新的编程框架规范,对AI非常不友好,很难冒出来。
老的这几家,比如flutter、react native、CMP,如果能解决做到即跨平台、又不比原生性能差,那自然也不可能被AI淘汰。

但flutter的自渲染,包括CMP在iOS上也是自渲染,这种在混合原生渲染时有硬伤。这个在另一篇文章专门讨论过自渲染和原生渲染的问题。[^2]

而react native,从meta挪到基金会后,这个机制有好有坏,不好的就是很难找到一个强力的人做大改,但如果不大改,它解决不了比原生性能差的问题。
shopify本来是基金会的重要赞助者,出力做了不少关键库,更是给基金会出钱。它的撤出,对react native的打击很大。

当然难归难,也不是绝对的,不知道谁家会天降猛人改变局面。

说回来,我认为,AI会淘汰一些简易、低质量的轮子。但复杂、高质量的轮子,永远都受欢迎。AI想要好轮子。

10. uni-app x 刚出来的时候推的是uts语言,现在蒸汽模式又改成了普通的js/ts。为什么会这样调整?怎么考量的?

uni-app x 立项的时候,是参考我们在小程序上的做法,小程序就是把uni-app的写法,编译成了小程序的原生写法,重编译轻运行时。

那么怎么把uni-app x也编译成各个平台的原生应用?比如在Android上,把uni的写法就编译成一个纯的kotlin应用。

这就需要先做一个基建,跨平台的编译型语言,这就是uts,一个语言可以编译成kotlin、Swift、ArkTS。
一个语言,做出来难,做好更难。

但是语言搞定后,我们发现渲染系统做的不好,拖累了整体表现,又开始研究渲染系统。
语言是一个大关,渲染系统又是一个大关,我们连续磕了2个大难关。

当初立项新版渲染系统时,搭配的语言还是uts。因为当时没有预期蒸汽渲染系统这么快。
新版渲染系统出来后,结果是大幅超过我们预期的。我也没想到这块的优化余地这么大,在这么多测试场景中,都能比原生能快2~3倍。[^1]

然后这个时候AI来了,uts语言作为一个新语言,国外模型不熟悉,也没有模型为uts单独做强化学习,给我们带来了巨大的压力。
其实AI在某种程度上,是不利于创新的。不止uts,其他新的编程语言也一样难。

这逼着我们思考,既然渲染系统能快这么多,那么把语言改回到普通ts/js,合体版本是不是也可以做到比原生快?是不是这样的产品才是AI时代更适合开发者的。
这个时候我们团队内部是转不过来弯的,辛辛苦苦搞了几年uts,投入大量心血,真的要尝试换回js吗?自己的孩子,怎么能不心疼?
我自己也给自己做了很多心理建设。

最后是分期立项,优化跨语言通信。先在鸿蒙和iOS上,确认js+蒸汽渲染仍然可以做到比原生渲染快,然后才全体坚定信心,大力推进Android的js更换。
外部看不到,内部其实经历了一个过渡期才把弯转过来。

优化跨语言通信,这个立项最初目标是js+蒸汽渲染系统,能和原生一样快就行。
最终这个项目的结果,也超过了当初立项的预期。
也得益于这几年对语言底层的持续研究,搞明白很多原理,优化后,跨语言通信只损失了百分之几,但蒸汽渲染系统比原生渲染快200%~300%,
最后整合的效果,基本没变,那百分之几在各种评测实验数据上几乎看不出来,还是比原生快2~3倍。[^1]

跨语言通信有个极限例子,就是canvas,在js中绘制碰撞的小球,绘制逻辑都是在js里。最终这个测试例大家也看到benchmark里的数据了,uni蒸汽下,可以绘制几十万个小球同屏碰撞而不掉帧。

最后js+蒸汽渲染引擎这个方案就定下来了,AI熟悉、开发者门槛低、还能热更新,还比原生渲染快,这不就是最佳方案了吗?夫复何求,对吧。

有最佳方案就行,你过去投入的心血,不要遮蔽住你为用户寻找最佳方案的眼睛。

11. 那uts以后怎么办呢?不管是uts语言,还是你们的HBuilder,都是国产自主创新的代表,最近你们基于vscode搞了HBuilderV,感觉这些国产创新的东西好不容易做出来,要被丢掉吗?

国产化,除非是政府资助,对于一个市场化的产品来讲,还是需要在市场中让用户买单。

我们也会觉得可惜,但以国产创新的理由做技术自嗨,得不到用户的认可,没用。
反正我们自己从来不说:flutter、react native不是国产的,请大家支持国产的uni-app。

uts语言不会消失,我们内部也有大量代码是基于uts的,而且我们内部的uts代码还在增加,它仍然是一个不错的跨平台语言。
会的人可以继续用,不会的人,不学也行,原生插件开发也可以使用原生混编。

其实我们还研究过uts直接编译成机器码,发现可行。只是,做出来容易,但想做好,又得几年、好几亿的投入。
除非是政府主导,一定要搞国产化语言,对于市场主体来讲,这事没有性价比。

编译机器码不难,但编译出的机器码非常高效,这特别难。
我们不能假设只要编译成机器码,性能就一定高。
很多语言都尝试过这事,kotlin也有k/n方案,但同样是机器码,它还得处理gc,它的机器码优化程度,和c语言差太多了。

我们也可以很快拿出一个uts编译机器码的方案,但性能想达到第一梯队,就是达到c、zig的水平,需要很长时间和很多资金投入。

至于HBuilderV,这个产品得感谢AI时代,没有AI迁移,这个产品很难立项。
我们的逻辑是这样,给习惯vscode的开发者提供一种选择,有助于大家更接受uni蒸汽,且在AI的帮助下这个产品的成本可接受,那就做了。

目前HBuilderX和HBuilderV会一起演进。

至于未来,HBuilderX是基于QT的,QT以后不会再用了。
其实我们掌握了优秀的渲染优化技术,在PC上也是一样的,后续我们会出uni蒸汽的pc版,届时也会是最快的跨PC平台渲染引擎,体积也会很小。
在uni蒸汽的pc版出来后,我们会再重构HBuilder。
当然也可能换名字。未来AI持续发展,IDE的传统功能都不一定适用了。可能会是一个全新形态。

11.1 那很期待。至于HBuilderV,你们为什么不做vscode插件,而是和cursor一样整体套壳修改?

大家可能对vscode的插件api的丰富度有过高预期。
当初cursor之所以基于vscode套壳二开,就是因为vscode的插件api不够用,AI所需要的功能,插件无法实现,只能整体二开。

对于HBuilderV也一样,HBuilderX里很简单的一个常用需求,就是同时运行多个平台,在底部开一堆控制台,这功能插件就做不了。
还有各处弹出的界面,比如运行、打包,以及我们也有一个AI产品,叫uni-agent,大量的功能需求,用vscode插件无法实现。只能整体二开。

12. 你刚才提到了c、zig,没有提到rust,现在rust风生水起,bun也从zig迁移到rust了。你们的组件不用rust,还使用c语言吗?

rust在pc上发展的更顺利些,手机上用的要少一些。手机系统的c api、ndk编译链,都是围绕c、c++构建的。

不管是c、zig、rust,核心差别是内存谁来管。
其实放大看,所有语言都有这个终极问题,内存到底怎么管。用这点,可以把所有编程语言放在一个坐标体系里看待。

最开始的c语言,典型的手动管理内存。
但人们觉得,我就是开发业务,你让我研究内存怎么管,还经常泄漏,这门槛太高了,不利于编程普及。
然后就出现了gc,就是垃圾回收,java、js,这些语言凭借一点,你别再操心内存了,专注你的业务就完事,帮助这个世界产生了更多的程序员。
然而gc从来都是有代价的,会影响性能。于是有人又想出点子,能不能让编译器来管内存,运行时不要gc了,然后就设计了一套复杂的借用机制,在编译阶段确保内存安全,这就是rust。
但这套机制,学习门槛很高,为了高性能有时还得用unsafe。
于是又有人想,那能不能还是人手动管理内存,但管理的方便点。zig就出现了。
当然zig不止这一个优势,但通过这个对比,可以简单的明白这些编程语言的定位差异。

就我个人喜好,其实我喜欢zig。当然zig的成熟度、生态确实不如rust。
bun的压力来自于基于它的claude code,用户量巨大,带来了很多边界问题。
但bun的问题,并非zig下不能解。
只不过在zig下,需要投很多人来解。但对于一个AI模型公司来讲,能让AI解更符合他们的飞轮。

当然站在第三方来看,折腾这事干嘛,想求稳,应该直接把bun换成nodejs,而不是把bun从zig改成rust。

13. 你也提到一个成熟的产品,需要处理掉各种边界问题,才能获得开发者的认可。那么uni蒸汽刚出来,是否短期内也面临成熟度问题?

这是肯定的。所有新产品都有成熟过程。对于uni蒸汽来讲,就要想各种办法加速它的成熟。
我可以分享下我们怎么做的。

uni-app x 从2023年起第一个版本就开发了自动化测试系统。
并且 uni-app x 坚持一个原则,修复的每个开发者报的issues,都要配套自动化测试例。每个版本发布,都要过测所有测试例才能发版。
我们专门做了一套issues系统,开发人员在提测issues的时候,表单里就有自动化测试例地址。

这几年来,已经积累几万个自动化测试例,包括功能、性能、UI截图对比、内存泄漏、崩溃等多种维度。
每天晚上,在我们内部的持续集成系统里,都要跑几万个测试例,在各种手机os、各种版本上跑,几十万次,做断言比对、截图对比。。。

uni-app x 蒸汽模式 虽然是2026年推出的,但它每次发版,都得通过 uni-app x 积累的所有自动化测试例。

uni蒸汽的质量管理压力,其实对我们来讲,轻了很多。
以前在vdom模式时,我们还要操心uts语言的质量,现在语言的稳定性交给js引擎处理了。
我们就可以集中精力在渲染系统的质量上。
uni蒸汽还有一个特点:不同平台的组件内部实现代码基本都是一套,都是跨平台的c或uts。所以它的平台差异很小。这也让我们的质量管理压力变小。

精力不分散后,我们也有更多人力投在响应开发者反馈的bug上,现在uni蒸汽的修bug速度还是很快的。

在AI时代,AI其实也在帮助我们把控质量,分析隐患、生成更多自动化测试例。这些都让一个新产品的成熟度比以往更快。

我不能说uni蒸汽已经成熟了,但我认为它的质量是可商用的。

好的,感谢王总的坦诚分享。与王总交流,总能感受到一些新的观点和角度。相信读者们,除了更深度的了解uni相关产品外,还可以从这个访谈中学到一些其他东西,比如一个复杂项目如何立项、如何管理、如何提升性能和质量。更多问题,大家可以留言,我们尽量协调王总回答哈。

没问题。

关联阅读

​1. uni-app x 蒸汽模式 性能测试基准报告 Benchmark【鸿蒙版】
​2. uni-app x 蒸汽模式性能测试基准报告 Benchmark【 iOS版】
​3. uni-app x 蒸汽模式 性能测试基准报告 Benchmark【Android版】

  1. 一文讲透原生渲染和自渲染
继续阅读 »

> 原文是首发于开源中国公众号的访谈,各级标题为主持人问题,原文详见:​https://mp.weixin.qq.com/s/erfEFl4ZNKMkoa7MViFCpA

背景:

uni-app x 蒸汽模式,发表了比原生渲染还快的benchmark。这让业内大为震惊。

本期请来了DCloud的CEO王安,从设计者的角度聊聊,为什么做这个产品、为什么这么做而不那么做、性能提升的关键在哪里?

1. 跨平台框架赛道,并非当下VC的关注领域,DCloud投入4年,花了几个亿搞 uni-app x,这种执着和冒险的底层逻辑是什么?

我做跨平台已经二十多年了,老骨灰了。我做的产品,虽然也有很大的市场体量,但也一直被开发者们吐槽。

uni-app的第一代产品,在小程序领域是市占率第一的,不管大厂小厂都在用。因为用uni-app开发小程序不会比原生小程序性能差,还能跨平台。

但在App平台,市占率只有百分之十几,主要集中在中小开发者。原因就是App平台的性能不能满足中大开发者的需求。

所以App平台的性能问题,对我们一直是如鲠在喉。

2022年,我们内部讨论 uni-app x 的立项问题,能不能让app像小程序那样,做到与原生开发相比没有性能损失。

其实团队内部,对 uni-app x 的立项是有反对声音的。如果我们守住原有盘子,做好服务和商业化,能让团队成员的日子过的更好点。

但一旦要立项解决跨平台框架的性能问题,这注定不是一个短期能解决的问题,投入肯定不少,机会风险很大。

当时觉得就是不服气,觉得这辈子不能就一直解决不了跨平台的性能问题,活得太憋屈了,想赌一把。

其实当时我也不知道这个项目一做就是4年,我以为2年能搞定。当初如果知道要4年,那承受的压力会非常大,不一定能下得了决心。

2. 这4年中间,有过犹豫和后悔吗?

中间是走了很多弯路啊。有的路走了很久才发现不通,肯定是非常郁闷的。
但并不会因为走错路而后悔。你在登一个没人登过的山,走错路是正常的。
要说后悔的话,从开始就应该建立一个更高效的探路机制,能更快的验证路线的正确性。
这个机制,我们直到2025年才完善,这是我最大的教训吧。

中间有段时间非常焦虑,大约25年初的时候,当时觉得搞了快3年了,不达预期怎么办。
幸好几个月后,25年夏天,一系列技术突破后,终于重拾信心,知道自己搞对了。

3. 你们建的这个高效的探路机制,是什么?

类似实验室。
就是别着急做产品,别着急提代码。
就是先做实验,体系化的、严谨的做实验。
但是然后呢,你又会发现还不能着急做实验,为了能严谨的输出各种实验报告,其实需要做很多基建,你要先投入大量资源去建实验室的基建。

我们以前吃了不少亏,调研报告不准确,误导了技术决策。这都需要一套良好的实验基建、严谨的实验规范流程才能解决。

有很长一段时间,我看不到产品里提交代码,我也不关注这个指标。我每天只看实验报告。

这个过程可能很多公司都没有,大多数公司的技术考核,可能是产品需求的实现工期、完成度、bug数量。
但对于uni-app x蒸汽模式这种产品来讲,实验室机制是它最终能做出来的核心原因之一。

4. 非常延迟满足啊。不做产品先做实验,不做实验先搞批量实验的基建。那这注定不是短期投入。然后我有点好奇,当初是怎么说服团队成员同意这个指标的:比原生渲染还快。

没有。当初如果定一个比原生渲染快2~3倍的目标,别说团队成员了,我自己都不信。

当初的目标,就是参考小程序,uni和原生小程序没有性能差别。然后app也做到和原生app一样,没有性能差别就行。

就这,当时团队成员都不同意,说做不到,最多能做到15%的性能差距,就是比原生慢15%。为了这15%拉扯了半天。

但后来发现,实现到15%的性能差距,仍然困难重重。只能一路往底层追,从系统组件源码、追到系统底层渲染原理,往下又追到编程语言原理,又往下追到硬件层操作原理。

直到你把整个事情都搞明白了,这时候你才发现,传统的原生渲染一层一层套轮子,叠了太多层了,性能优化空间太大了。
不要说达到原生渲染的性能了,比原生渲染快几倍都没问题。

以前如果我diss一个团队成员,说你做的这个东西性能太差了,怎么连原生的同类组件的性能都拼不过。这个人肯定会觉得这个老板有病。
但现在我可以随便diss,在我们团队,做到比原生快并不难,已经是团队共识了。
当然一个新入职的人,还需要一段时间才能适应这种文化。

5. 原生渲染那么多年的优化,真的是流程很长吗?能举个例子吗?

新的挑战者打破旧的,心里是需要这个世界都是草台班子的信念的。虽然这个新的挑战者也会被后来的挑战者视为草台班子。

计算机工业发展了这么多年,长成了庞然大物,轮子一层叠一层,减肥的空间其实挺多的。

我们公司有个面试题,手机左上角的那个返回箭头,怎么绘制是性能最高的。

很多人在项目中使用字体图标,矢量缩放不失真,暗黑能变色,跨平台都能用,不同平台渲染结果一致,听着挺好。

但是一个字体图标,从你写代码加载字体那刻,到屏幕上最终显示出来,经过了非常多环节。

你可能觉得你就写了几行代码,还好吧。但实际上,

  • 首先系统需要从IO里加载你的字体库到内存里,
  • 然后需要构造字体管理对象,解析字体头文件,确定字体的很多参数。
  • 然后要去字体中的众多字里匹配,你想要显示哪个字符。
  • 然后要做文字塑型,确定连字等字间距处理。
  • 然后做换行、双向文本流(有从左到右的文字、也有从右到左的,还有混合的)。
  • 然后做文字测量,把高宽定下来。
  • 然后做光栅化或三角形化。
  • 然后提交给GPU。
  • 最后整体合成上屏。

这里面提到的每个步骤,都有专业的库负责。这些库的源码如果你看过的话,逻辑都非常多。

在Android和鸿蒙上,这些逻辑还涉及原生的kt/arkts和c库的反复交互。

你只看到你写的几行代码,但实际上背后是几十年的文字系统各种库的协作配合。

你知道了背后的流程多长,你肯定想跳过一些流程,比如就一个返回箭头图标,它理应不涉及换行和双向文本流,但这里又有另一个坑。
不同os,上述流程的可控性是不一样的,有的os提供了专门的api可以跳过换行和双向文本流流程,有的os没有提供。

如果你对文字系统有研究兴趣,可以去看看harfbuzz、freetype、icu这些c库的源码,都是几十年的老库了,每个库都是不同的顶级大牛写的。
但作为一个应用开发者,想优化这块,真没有ROI。

5.1. 所以uni蒸汽模式中的返回箭头、文字渲染,都比原生自带的要更快?

是的。首先要投入大量精力研究每个系统的底层,然后优化,然后再给开发者暴露简单易用的跨平台写法。

6. 你们提供了一个benchmark,一个界面中创建了4050个view和text,渲染耗时比原生短2~3倍。这个测试例让很多人不解、甚至不信。不知道这个访谈中,王老师能否透露一些细节原因。比如这个测试例,是因为uni蒸汽的文字系统更快吗?有些人觉得这可能是你们的测试例定向优化,能解释下吗?[^1]

没有定向优化。是反过来的,如何测试一个渲染系统在处理view、text这种基础组件时速度更快?带着这个问题,我们才在一个屏幕内大量创建view和text来做实验。

你想创建多少个、怎么排布,都可以,文字是数字还是汉字都可以,结论都是 uni-app x 蒸汽模式更快。

这种快,来源于很多方面。uni的文字系统真的更快,如果你把4050的例子改一下,在里面放置更多text组件,你会发现uni比原生快了不止2~3倍,会快更多,界面上文字越多,uni的优势越明显。

除了文字系统快,整个template和Style区域,执行的都比原生快。在4050的例子里,是模板里的v-for创建这些view和text,这些代码都是编译成字节码/机器码的,这些高度优化的二进制码性能更高。

uni蒸汽的排版系统也更快,不管是什么组件,view、text还是其他组件,都要排版。uni蒸汽统一是flex排版,这套排版系统的性能也比原生提供的那些排版布局系统快。并且这套排版也不会为4050这种示例做定向优化。

4050这个例子有拍平和非拍平2套测试数据,都是uni蒸汽快。有些人认为uni蒸汽的4050快是拍平的功劳。并不是的,这2套数据之差,已经说明了拍平在这里能提供的价值。

当然拍平也是我们给开发者提供的一个挺好的性能优化方案,让开发者可以方便构造更高性能的UI。一个简单的flatten属性,这种开发方式比原生高效非常多。

7. 有的原生工程师表示不服,他们也可以在原生的基础上优化代码,跑出更快的原生版数据。你怎么看?

我们需要牢记初心,这个测试例比的是view和text的创建速度,不是比4千多个方格子固定排放后如何快速渲染。

uni提供的就是标准的view和text组件,没有定向优化。原生的对比测试例,也不能为了这个4050的测试例而定向优化。

有的原生工程师说,我用draw的方式,只创建1个view,在上面自绘出4050个方格子出来,不测量不排版,所有格子等宽。
那这就是原生为这个测试例定向优化了。这就没法比较原生的view、text,和uni的view、text到底谁快了、排版系统到底谁快了。

如果你基于draw,自己实现文字测量、布局排版,能布任何界面,那其实就是自己做了一套渲染系统。其实这就是 Compose UI 在Android上做的事情。
首先真的做出来这套系统是非常难的,其次做出来性能也很难匹敌uni蒸汽。

Compose UI 的view和text同屏4050测试例,我们是在benchmark里公开过的,比Android原生view还慢,更比不过uni蒸汽。[^1]

如果某个原生工程师有兴趣钻研此道,自己一个人搞是不行的,这是大工程,还是加入DCloud一起干吧,哈哈。

8. 我有个疑惑,为什么操作系统那么多专业的人,不去把原生UI优化的更快呢?给三方这么大的超越空间

我不是操作系统团队的人,我也不知道他们怎么想的。我可以推测下,随便聊了。

可能有的系统厂商,觉得自己已经够快了,没有竞争压力给他们。因为要动这些底层的东西,代价不低,立项时总会有人问花这么多钱能带来什么财务回报?

很多领先的厂商,有一个产业义务,就是推进用户买新设备。这对用户来说听着有点恶心。但产业链的兄弟们持续有钱赚也很重要,大家有钱可以持续创新,推进产业进步。

然后就是组织大了,跨部门协调也难。
比如Android,原生的kotlin是一拨人,各种c库是另一拨人,现有的框子是一层一层叠起来的,想压穿,需要协调非常多人。

还有的技术栈喜欢自举,比如java/kotlin,很多重要的库是java写的、而不是c或汇编写的。当你的产品哲学是自举优先而不是性能优先,就很难把性能做到极致。
你看v8的源码,那不止是优秀的c代码,很多地方要上汇编的,比如json解析,这就是性能优先。

当然了,如果操作系统的人真的愿意好好优化,肯定是可以反超uni蒸汽现在的benchmark数据的。

不过呢,其实uni蒸汽,也还可以再提升性能,只不过我们也一样,觉得快了2~3倍已经足以。现在立项投很多人力财力再让uni蒸汽快4倍5倍,没有性价比。
如果原生优化的更快了,我们也会立项再追。

还是那句话,现代计算机工业这么多年,这个庞然大物减肥的空间挺多的。

9. AI时代,跨平台框架的价值是否要重新看待了。最近shopify放弃react native,转原生。这是不是未来的趋势?你们搞了4年多,这个跨平台框架是不是在AI时代生不逢时了?

AI刚来时,我也有点焦虑,担心AI会颠覆掉跨平台框架。
但后来实践、观察、深入思考,心就定了。

首先看数据,AI Coding后,uni的应用数量进一步增长,并不是下降。
然后看我们内部的实践,我们除了框架,也做应用,比如uni-ai、uni-im,一个是开源的AI聊天客户端,一个是开源的IM系统,我们和我们的开发者沟通,都是在uni-im上。

如果没有 uni-app x 和uniCloud,这些项目在我们内部根本无法立项。是因为实际这2个项目,只有一个前端工程师。

如果要用原生来做,2个应用系统,5个客户端平台,Android、iOS、鸿蒙、web、微信小程序,客户端+服务器+测试,即使每个人都在用ai,最少也是十几个人。

除了开发,还要考虑。不同平台,不但代码不同,测试例、测试工具链也不同。但在uni下,不止代码统一了、测试例也是一套。

uni这边,真的从前到后,从开发到测试,所有平台,只有一个人。他用着AI并行开发,快速完成了2个系统。这生产效率和成本,其他方案真的比不了。

主持人:有的人说,我不要6个人,我一个人就让ai干活,写5个平台的原生代码加服务器和测试例,不行吗?

AI只是工具,它需要人用,需要人背锅。工具不背锅。进度延期、线上出bug,你无法甩锅给AI。只有懂AI写的代码,才能hold住,才能担起责任。

尤其是面向广大用户的、涉及重要权益的系统,AI写的代码有问题,造成线上损失,是需要你担责的。

做一些压力不大的系统,比如自用的、有限人使用的内部管理只读报表,可以不看代码。但真要上线,要面向用户承担责任,那人得把关。

回到上一个问题,我们做的uni-ai,它的性能比原生开发的AI客户端都好,和ChatGPT、豆包、deepseek相比帧率都更稳。

如果使用原生开发,每个平台都达到这个性能水平,那我刚才说的十几个人都还不够,那投资得非常大了。

所以uni系产品带来的价值,生产力,非常大。

AI虽然是个工具,但你让它干活时,它也想用好工具,它不会用好工具它就不是个好AI,对吧。
在AI时代,写代码并不是不需要框架,只是需要好框架。
比如ts、vue,这些都是框架,你也可以让AI去写原始的js,但人们为什么不这么做呢?
因为明显是用ts、vue这些好框架,写代码更轻松、更容易保障性能和质量,也更省时间和token。

在AI时代,并不是跨平台框架没有价值了。而是需要更好的跨平台框架。

一个跨平台框架,只跨2个平台,性能比原生差了不少,那么shopify认为AI降低了我的成本,原生带来了更高性能,这ROI是合适的,就会转原生。
假使,我说的是假使,react native的渲染性能比原生更快,那shopify怎么会切原生去?闲的慌啊。

所以AI时代,那些问题比较多的跨平台框架,不管是性能、质量、跨平台数量,如果做的不好,就会没落。
我个人觉得,AI Coding时代,在App平台,可能未来只有原生和uni-app x。其他跨平台框架估计要走下坡路。
这会和小程序领域类似,拼到最后只剩原生和uni-app了。

我这话说的,听起来有点满,其实我是深入思考过的。
新的编程框架规范,对AI非常不友好,很难冒出来。
老的这几家,比如flutter、react native、CMP,如果能解决做到即跨平台、又不比原生性能差,那自然也不可能被AI淘汰。

但flutter的自渲染,包括CMP在iOS上也是自渲染,这种在混合原生渲染时有硬伤。这个在另一篇文章专门讨论过自渲染和原生渲染的问题。[^2]

而react native,从meta挪到基金会后,这个机制有好有坏,不好的就是很难找到一个强力的人做大改,但如果不大改,它解决不了比原生性能差的问题。
shopify本来是基金会的重要赞助者,出力做了不少关键库,更是给基金会出钱。它的撤出,对react native的打击很大。

当然难归难,也不是绝对的,不知道谁家会天降猛人改变局面。

说回来,我认为,AI会淘汰一些简易、低质量的轮子。但复杂、高质量的轮子,永远都受欢迎。AI想要好轮子。

10. uni-app x 刚出来的时候推的是uts语言,现在蒸汽模式又改成了普通的js/ts。为什么会这样调整?怎么考量的?

uni-app x 立项的时候,是参考我们在小程序上的做法,小程序就是把uni-app的写法,编译成了小程序的原生写法,重编译轻运行时。

那么怎么把uni-app x也编译成各个平台的原生应用?比如在Android上,把uni的写法就编译成一个纯的kotlin应用。

这就需要先做一个基建,跨平台的编译型语言,这就是uts,一个语言可以编译成kotlin、Swift、ArkTS。
一个语言,做出来难,做好更难。

但是语言搞定后,我们发现渲染系统做的不好,拖累了整体表现,又开始研究渲染系统。
语言是一个大关,渲染系统又是一个大关,我们连续磕了2个大难关。

当初立项新版渲染系统时,搭配的语言还是uts。因为当时没有预期蒸汽渲染系统这么快。
新版渲染系统出来后,结果是大幅超过我们预期的。我也没想到这块的优化余地这么大,在这么多测试场景中,都能比原生能快2~3倍。[^1]

然后这个时候AI来了,uts语言作为一个新语言,国外模型不熟悉,也没有模型为uts单独做强化学习,给我们带来了巨大的压力。
其实AI在某种程度上,是不利于创新的。不止uts,其他新的编程语言也一样难。

这逼着我们思考,既然渲染系统能快这么多,那么把语言改回到普通ts/js,合体版本是不是也可以做到比原生快?是不是这样的产品才是AI时代更适合开发者的。
这个时候我们团队内部是转不过来弯的,辛辛苦苦搞了几年uts,投入大量心血,真的要尝试换回js吗?自己的孩子,怎么能不心疼?
我自己也给自己做了很多心理建设。

最后是分期立项,优化跨语言通信。先在鸿蒙和iOS上,确认js+蒸汽渲染仍然可以做到比原生渲染快,然后才全体坚定信心,大力推进Android的js更换。
外部看不到,内部其实经历了一个过渡期才把弯转过来。

优化跨语言通信,这个立项最初目标是js+蒸汽渲染系统,能和原生一样快就行。
最终这个项目的结果,也超过了当初立项的预期。
也得益于这几年对语言底层的持续研究,搞明白很多原理,优化后,跨语言通信只损失了百分之几,但蒸汽渲染系统比原生渲染快200%~300%,
最后整合的效果,基本没变,那百分之几在各种评测实验数据上几乎看不出来,还是比原生快2~3倍。[^1]

跨语言通信有个极限例子,就是canvas,在js中绘制碰撞的小球,绘制逻辑都是在js里。最终这个测试例大家也看到benchmark里的数据了,uni蒸汽下,可以绘制几十万个小球同屏碰撞而不掉帧。

最后js+蒸汽渲染引擎这个方案就定下来了,AI熟悉、开发者门槛低、还能热更新,还比原生渲染快,这不就是最佳方案了吗?夫复何求,对吧。

有最佳方案就行,你过去投入的心血,不要遮蔽住你为用户寻找最佳方案的眼睛。

11. 那uts以后怎么办呢?不管是uts语言,还是你们的HBuilder,都是国产自主创新的代表,最近你们基于vscode搞了HBuilderV,感觉这些国产创新的东西好不容易做出来,要被丢掉吗?

国产化,除非是政府资助,对于一个市场化的产品来讲,还是需要在市场中让用户买单。

我们也会觉得可惜,但以国产创新的理由做技术自嗨,得不到用户的认可,没用。
反正我们自己从来不说:flutter、react native不是国产的,请大家支持国产的uni-app。

uts语言不会消失,我们内部也有大量代码是基于uts的,而且我们内部的uts代码还在增加,它仍然是一个不错的跨平台语言。
会的人可以继续用,不会的人,不学也行,原生插件开发也可以使用原生混编。

其实我们还研究过uts直接编译成机器码,发现可行。只是,做出来容易,但想做好,又得几年、好几亿的投入。
除非是政府主导,一定要搞国产化语言,对于市场主体来讲,这事没有性价比。

编译机器码不难,但编译出的机器码非常高效,这特别难。
我们不能假设只要编译成机器码,性能就一定高。
很多语言都尝试过这事,kotlin也有k/n方案,但同样是机器码,它还得处理gc,它的机器码优化程度,和c语言差太多了。

我们也可以很快拿出一个uts编译机器码的方案,但性能想达到第一梯队,就是达到c、zig的水平,需要很长时间和很多资金投入。

至于HBuilderV,这个产品得感谢AI时代,没有AI迁移,这个产品很难立项。
我们的逻辑是这样,给习惯vscode的开发者提供一种选择,有助于大家更接受uni蒸汽,且在AI的帮助下这个产品的成本可接受,那就做了。

目前HBuilderX和HBuilderV会一起演进。

至于未来,HBuilderX是基于QT的,QT以后不会再用了。
其实我们掌握了优秀的渲染优化技术,在PC上也是一样的,后续我们会出uni蒸汽的pc版,届时也会是最快的跨PC平台渲染引擎,体积也会很小。
在uni蒸汽的pc版出来后,我们会再重构HBuilder。
当然也可能换名字。未来AI持续发展,IDE的传统功能都不一定适用了。可能会是一个全新形态。

11.1 那很期待。至于HBuilderV,你们为什么不做vscode插件,而是和cursor一样整体套壳修改?

大家可能对vscode的插件api的丰富度有过高预期。
当初cursor之所以基于vscode套壳二开,就是因为vscode的插件api不够用,AI所需要的功能,插件无法实现,只能整体二开。

对于HBuilderV也一样,HBuilderX里很简单的一个常用需求,就是同时运行多个平台,在底部开一堆控制台,这功能插件就做不了。
还有各处弹出的界面,比如运行、打包,以及我们也有一个AI产品,叫uni-agent,大量的功能需求,用vscode插件无法实现。只能整体二开。

12. 你刚才提到了c、zig,没有提到rust,现在rust风生水起,bun也从zig迁移到rust了。你们的组件不用rust,还使用c语言吗?

rust在pc上发展的更顺利些,手机上用的要少一些。手机系统的c api、ndk编译链,都是围绕c、c++构建的。

不管是c、zig、rust,核心差别是内存谁来管。
其实放大看,所有语言都有这个终极问题,内存到底怎么管。用这点,可以把所有编程语言放在一个坐标体系里看待。

最开始的c语言,典型的手动管理内存。
但人们觉得,我就是开发业务,你让我研究内存怎么管,还经常泄漏,这门槛太高了,不利于编程普及。
然后就出现了gc,就是垃圾回收,java、js,这些语言凭借一点,你别再操心内存了,专注你的业务就完事,帮助这个世界产生了更多的程序员。
然而gc从来都是有代价的,会影响性能。于是有人又想出点子,能不能让编译器来管内存,运行时不要gc了,然后就设计了一套复杂的借用机制,在编译阶段确保内存安全,这就是rust。
但这套机制,学习门槛很高,为了高性能有时还得用unsafe。
于是又有人想,那能不能还是人手动管理内存,但管理的方便点。zig就出现了。
当然zig不止这一个优势,但通过这个对比,可以简单的明白这些编程语言的定位差异。

就我个人喜好,其实我喜欢zig。当然zig的成熟度、生态确实不如rust。
bun的压力来自于基于它的claude code,用户量巨大,带来了很多边界问题。
但bun的问题,并非zig下不能解。
只不过在zig下,需要投很多人来解。但对于一个AI模型公司来讲,能让AI解更符合他们的飞轮。

当然站在第三方来看,折腾这事干嘛,想求稳,应该直接把bun换成nodejs,而不是把bun从zig改成rust。

13. 你也提到一个成熟的产品,需要处理掉各种边界问题,才能获得开发者的认可。那么uni蒸汽刚出来,是否短期内也面临成熟度问题?

这是肯定的。所有新产品都有成熟过程。对于uni蒸汽来讲,就要想各种办法加速它的成熟。
我可以分享下我们怎么做的。

uni-app x 从2023年起第一个版本就开发了自动化测试系统。
并且 uni-app x 坚持一个原则,修复的每个开发者报的issues,都要配套自动化测试例。每个版本发布,都要过测所有测试例才能发版。
我们专门做了一套issues系统,开发人员在提测issues的时候,表单里就有自动化测试例地址。

这几年来,已经积累几万个自动化测试例,包括功能、性能、UI截图对比、内存泄漏、崩溃等多种维度。
每天晚上,在我们内部的持续集成系统里,都要跑几万个测试例,在各种手机os、各种版本上跑,几十万次,做断言比对、截图对比。。。

uni-app x 蒸汽模式 虽然是2026年推出的,但它每次发版,都得通过 uni-app x 积累的所有自动化测试例。

uni蒸汽的质量管理压力,其实对我们来讲,轻了很多。
以前在vdom模式时,我们还要操心uts语言的质量,现在语言的稳定性交给js引擎处理了。
我们就可以集中精力在渲染系统的质量上。
uni蒸汽还有一个特点:不同平台的组件内部实现代码基本都是一套,都是跨平台的c或uts。所以它的平台差异很小。这也让我们的质量管理压力变小。

精力不分散后,我们也有更多人力投在响应开发者反馈的bug上,现在uni蒸汽的修bug速度还是很快的。

在AI时代,AI其实也在帮助我们把控质量,分析隐患、生成更多自动化测试例。这些都让一个新产品的成熟度比以往更快。

我不能说uni蒸汽已经成熟了,但我认为它的质量是可商用的。

好的,感谢王总的坦诚分享。与王总交流,总能感受到一些新的观点和角度。相信读者们,除了更深度的了解uni相关产品外,还可以从这个访谈中学到一些其他东西,比如一个复杂项目如何立项、如何管理、如何提升性能和质量。更多问题,大家可以留言,我们尽量协调王总回答哈。

没问题。

关联阅读

​1. uni-app x 蒸汽模式 性能测试基准报告 Benchmark【鸿蒙版】
​2. uni-app x 蒸汽模式性能测试基准报告 Benchmark【 iOS版】
​3. uni-app x 蒸汽模式 性能测试基准报告 Benchmark【Android版】

  1. 一文讲透原生渲染和自渲染
收起阅读 »

uniappx 基础脚手架,拒绝重复造轮子

uniappx 基础脚手架,包含功能:

  1. 基础简单的登录示例代码
  2. request请求封装
  3. 跳转 + 登录白名单/拦截
  4. 版本更新(多渠道,应用市场, 蒲公英分发或第三方链接等平台)
  5. 文件的上传下载
  6. 图像裁剪上传封装
  7. 主题的切换,包括明暗主题与自定义主题等
继续阅读 »

uniappx 基础脚手架,包含功能:

  1. 基础简单的登录示例代码
  2. request请求封装
  3. 跳转 + 登录白名单/拦截
  4. 版本更新(多渠道,应用市场, 蒲公英分发或第三方链接等平台)
  5. 文件的上传下载
  6. 图像裁剪上传封装
  7. 主题的切换,包括明暗主题与自定义主题等
收起阅读 »

uniappx本地打包微信登录分享提示无提供商

build.gradle.kts新增
buildConfigField("String", "UTSRegisterProviders", "\"[{\\"name\\":\\"weixin\\",\\"service\\":\\"oauth\\",\\"class\\":\\"uts.sdk.modules.DCloudUniOauthWeixin.UniOAuthWeixinProviderImpl\\"},{\\"name\\":\\"weixin\\",\\"service\\":\\"share\\",\\"class\\":\\"uts.sdk.modules.DCloudUniShareWeixin.UniShareWeixinProviderImpl\\"}]\"")

继续阅读 »

build.gradle.kts新增
buildConfigField("String", "UTSRegisterProviders", "\"[{\\"name\\":\\"weixin\\",\\"service\\":\\"oauth\\",\\"class\\":\\"uts.sdk.modules.DCloudUniOauthWeixin.UniOAuthWeixinProviderImpl\\"},{\\"name\\":\\"weixin\\",\\"service\\":\\"share\\",\\"class\\":\\"uts.sdk.modules.DCloudUniShareWeixin.UniShareWeixinProviderImpl\\"}]\"")

收起阅读 »

#插件需求# uniapp x实现APP热更新

插件需求

uniapp x蒸汽模式希望官方出一个 wgt热更新,或是出一个热更新的解决方案,便于APP后期持续开发

uniapp x蒸汽模式希望官方出一个 wgt热更新,或是出一个热更新的解决方案,便于APP后期持续开发

希望官方开放 uniappx 的 页面路由

例如页面跳转 uni.navigateTo

原生的页面转场很多丝滑表现,
例如 安卓里面的转场很丝滑,包含他头部的 标题栏 就像是增量切换一样,而uniappx 和uniapp里面的就像是一块布硬生生的拖过去

iOS 的 页面转场则更多丝滑效果

等等 路由api,就那么几个动画,方式,很基础,很多原生的特性度无法使用,希望官方能把 页面路由的切换开放,提供给我们开发者自定义处理页面的路由跳转

继续阅读 »

例如页面跳转 uni.navigateTo

原生的页面转场很多丝滑表现,
例如 安卓里面的转场很丝滑,包含他头部的 标题栏 就像是增量切换一样,而uniappx 和uniapp里面的就像是一块布硬生生的拖过去

iOS 的 页面转场则更多丝滑效果

等等 路由api,就那么几个动画,方式,很基础,很多原生的特性度无法使用,希望官方能把 页面路由的切换开放,提供给我们开发者自定义处理页面的路由跳转

收起阅读 »

全平台(安卓,iOS,鸿蒙,微信小程序,h5)自定义+动态Tabbar+tabbar页面导航栏样式+tabbr数字角标 +动态主题+3种可选tabbar点击类型

点击查看插件

sunrains-tabbar 自定义 TabBar 插件使用指南

点击体验h5端

点击查看安卓端录屏效果

点击查看安卓端演示安装包

目录

  • [1. 基础配置]
  • [2. pages.json 配置]
  • [3. 动态配置 TabBar]
  • [4. 数字角标功能]
  • [5. 小程序端特殊配置]
  • [6. 主题配置]
  • [7. page页跳转自定义tab页]
  • [8. 拓展底部tabbar类型,(切换、弹出、进入)页面]

1. 基础配置

App.uvue onLaunch 初始化  
 import {initDefaultTabItems} from '@/uni_modules/sunrains-tabbar/tabbar-config.uts'  
        onLaunch(() => {  
        console.log('App Launch')  
        initDefaultTabItems()  
        }

1.1 配置 tabbar-config.uts 文件

在插件根目录 tabbar-config.uts 文件中,配置注册全局tab组件和导航栏设置。

完整示例:

import { setNavbarConfig, NavBarConfig, registerGlobalComponent, setDefaultTabItems, TabItem } from '@/uni_modules/sunrains-tabbar/utssdk/types.uts'  
import { getStoreLoginItems,storeLoginItems } from './utssdk/handle'  
//导入所有自定义tab页  
import Recommend from '@/pages/home/recommend.uvue'  
import Order from '@/pages/order/order.uvue'  
import Mine from '@/pages/mine/mine.uvue'  
import Detail from '@/pages/detail/detail.uvue'  

//注册所有需要切换的tab页或需要弹出的页面  即类型为 tabPage:tab页面  tabPop:弹窗组件  
registerGlobalComponent('home', Recommend)  
registerGlobalComponent('order', Order)  
registerGlobalComponent('mine', Mine)  
registerGlobalComponent('detail', Detail)  

// 2. 配置导航栏(不设置会使用默认配置)  
setNavbarConfig(new NavBarConfig(true, 34, 'bold', '#333333', '#ffffff', "", 88))  

//初始化默认tabbar区域按钮  
export function initDefaultTabItems(){  
    /**  
     *  
     * 3. 设置默认 Tab 列表(至少设置一个固定的,也可以全部设置,其它可通过请求服务端接口动态配置),  
     * 或通过网络请求初始化 游客 显示哪些  有 角色的 按角色 初始化  
     * App.uvue onLaunch 初始化  
      import {initDefaultTabItems} from '@/uni_modules/sunrains-tabbar/tabbar-config.uts'  
        onLaunch(() => {  
        console.log('App Launch')  
        initDefaultTabItems()  
        }  
     */  
    let home = new TabItem('home',"", 12, "red", "yellow", "bold", '首页', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  
    let mine = new TabItem('mine',"", 12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  

    let items = getStoreLoginItems();  
    //判断是否登录  
    if(false || items != null){  
        setDefaultTabItems(items)  
    }else{  
        setDefaultTabItems([  
            home,mine  
        ])  
        storeLoginItems([home,mine])  
    }  

}  

关键步骤说明:

  1. 导入组件:将所有需要作为 Tab 的页面组件导入
  2. 注册组件:使用 registerGlobalComponent 注册每个组件(第一个参数为唯一标识)
  3. 配置导航栏:可选配置,根据项目需求调整
  4. 设置默认 Tab:至少配置一个固定 Tab,其余可动态添加

2. pages.json 配置

2.1 必需配置

必须在 pages.json 中配置自定义 TabBar 入口页面(必须放在第一个位置),tab页无需在pages.json中配置:

{  
  "path": "uni_modules/sunrains-tabbar/index",  
  "style": {  
    "navigationStyle": "custom"  
  }  
}

2.2 完整示例

{  
    "pages": [   
        {  
          "path": "uni_modules/sunrains-tabbar/utssdk/index",  
          "style": {  
            "navigationStyle": "custom"  
          }  
        },  
        {  
          "path": "pages/detail/detail",  
          "style": {  
            "navigationBarTitleText": "详情"  
          }  
        }  
    ],  
    "globalStyle": {  
        "navigationBarTextStyle": "black",  
        "navigationBarTitleText": "uni-app x",  
        "navigationBarBackgroundColor": "#F8F8F8",  
        "backgroundColor": "#F8F8F8"  
    },  
    "uniIdRouter": {}  
}  

注意事项:

  • 自定义 TabBar 入口页面必须配置在 pages 数组的第一位
  • 所有 Tab 页面不需要在 pages 中声明
  • navigationStyle 设置为 "custom" 以隐藏原生导航栏

3. 动态配置 TabBar

3.1 使用场景

当需要根据用户权限、服务端配置等动态调整 TabBar 时,可以在任意页面触发刷新。

3.2 实现示例

以下面文档 mine.uvue 页面为例:

关键点:

  • 使用 refreshTabbar方法触发 TabBar 刷新
  • 确保所有动态 Tab 对应的组件已在 tabbar-config.uts 中注册

4. 数字角标功能

4.1 功能说明

支持为任意 Tab 设置数字角标,用于显示未读消息数、待处理事项等。

特性:

  • ✅ 动态设置/更新角标
  • ✅ 超过 99 自动显示 "99+"
  • ✅ 角标为 0 或 null 时不显示
  • ✅ 响应式更新,立即生效

4.2 使用方法

方法一:通过事件设置(推荐)

在任意页面通过 调用 setTabBadge 方法:

import { setTabBadge} from '@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts'  
// 设置算法 tab 的角标为 5  
setTabBadge("order",5)  
// 清除角标(设置为 0)  
setTabBadge("order",0)  
// 设置超过 99 显示 99+  
setTabBadge("order",150)

方法二:初始化时设置

在创建 TabItem 时传入 badge 参数:

import { TabItem } from '@/uni_modules/sunrains-tabbar/utssdk/types.uts'  

// 带角标的 tab  
new TabItem(  
  'order',   
  '算法',   
  '/static/tab/main2.png',   
  '/static/tab/main2_active.png'  
  10  // badge 参数  
)  

// 不带角标的 tab  
new TabItem(  
  'home',   
  '首页',   
  '/static/tab/main1.png',   
  '/static/tab/main1_active.png'  
)

4.3 完整示例

以 文档后面 mine.uvue 页面为例,实现动态设置角标功能:

4.4 实际应用场景

场景 1:算法未读数

// 从服务端获取未读算法数  
async function loadUnreadOrderCount() {  
  const res = await getOrderCount()  
  setTabBadge("order",res.data.count)  
}  

// 页面加载时调用  
onShow(() => {  
  loadUnreadOrderCount()  
})

场景 2:消息通知

// WebSocket 收到新消息  
websocket.onMessage((msg) => {  
  if (msg.type === 'new_message') {  
    const currentBadge = getCurrentBadge('message')  
    setTabBadge("newMessage",currentBadge + 1)  
  }  
})

场景 3:购物车数量

// 添加商品到购物车后更新角标  
function addToCart(product: Product) {  
  cartItems.push(product)  
  setTabBadge("cart",cartItems.length)  
}

5. 小程序端特殊配置

4.1 配置 tab-content-renderer.uvue

注意: 仅在需要支持微信小程序时需要配置,App 端可忽略此步骤。

在 uni_modules/sunrains-tabbar/tab-content-renderer.uvue 中配置:

<<template>  
  <view style="width: 100%; height: 100%;">  
   <Recommend v-if="currentTab === 'home'" style="width: 100%; height: 100%;" />  
    <Order v-if="currentTab === 'order'" style="width: 100%; height: 100%;" />  
    <Mine v-if="currentTab === 'mine'" style="width: 100%; height: 100%;" />  
      <Detail v-if="currentTab === 'detail'" style="width: 100%; height: 100%;" />  
  </view>  
</template>  

<script setup lang="uts">  
import Recommend from '@/pages/home/recommend.uvue'  
import Order from '@/pages/order/order.uvue'  
import Mine from '@/pages/mine/mine.uvue'  
import Detail from '@/pages/detail/detail.uvue'  
interface Props {  
  currentTab: string  
}  
defineProps({  
    currentTab:{  
        type:String  
    }  
})  
</script>  

配置说明:

  1. 条件编译:使用 #ifndef MP-WEIXIN 和 #ifdef MP-WEIXIN 区分平台
  2. App 端:使用 <component :is="..."> 动态渲染组件
  3. 小程序端:必须静态导入并声明所有可能的 Tab 组件
  4. 导入位置:在 #ifdef MP-WEIXIN 块内导入组件

6. 主题配置

6.1 主题功能说明

插件支持自定义 TabBar 的主题样式,支持网络背景图、本地图片背景图、纯颜色背景(支持rgba透明)、导航栏、tabbar区域背景、图标大小、图标(支持网络图标)、文字颜色、文字大小、等属性 满足不同项目的视觉需求。

主题特性:

  • ✅ 支持全局主题配置
  • ✅ 支持动态切换主题
  • ✅ 支持自定义主题参数

6.2 初始化时配置主题

在 tabbar-config.uts 文件中配置主题:

import {   
  setNavbarConfig,   
  NavBarConfig,   
  registerGlobalComponent,   
  setDefaultTabItems,   
  TabItem,  
  setTabBarTheme,  
  TabBarTheme  
} from '@/uni_modules/sunrains-tabbar/types.uts'  
import { setTabPageTheme} from '@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts'  

let bgType : BgType = "IMG"  
//背景 如果是图片传图片路径 颜色则传 具体颜色  
let bg : string = "/static/2.png"  
//配置导航栏(可选,不设置会使用默认配置)  
let navBarConfig : NavBarConfig = new NavBarConfig(  
    true, // 是否显示导航栏  
    34,  // 字体大小  
    'bold',  // 字体粗细  
    '#FFA500', // 文字颜色  
    '',// 背景颜色  
    "", //背景图片(支持网络图片)  
    90) // 导航栏高度  

let tabbarIcon = new TabbarIcon(  
    "mine",//自定义组件唯一标识  
    //非选中图标  
    "https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4",  
   //选中图标  
    "/static/tab/main3_active.png",  
    24,//图标宽 单位 px  
    24,//图标高 单位 px  
   //文字颜色  
    12,  
    //非选中文字大小  
    "green",  
    //选择文字颜色  
    "white",  
    //选择文字粗细  
    "bold"  
)  

let tabbarIcons : Map<string,TabbarIcon> = new Map();  
tabbarIcons.set("mine",tabbarIcon)  

let tabbarStyle : TabbarStyle = new TabbarStyle(  
    //tabbar区域高度  
    120,  
    //tabbar区域背景  
    "rgba(21, 20, 31, 0.8)",  
    //tabbar图标文字样式  
    tabbarIcons)  

let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
setTabPageTheme(theme)

6.3 动态修改主题

在任意页面通过调用 setTabPageTheme 方法动态修改主题:

import { setTabPageTheme} from '@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts'  
// 动态修改背景色和选中颜色  
let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
setTabPageTheme(theme)

6.4 在页面中动态切换主题

以 mine.uvue 页面为例,实现主题切换功能:

<template>  
    <scroll-view direction="vertical" class="mine-page"  
    show-scrollbar = "false"  
    enable-back-to-top="true"  
    refresher-enabled="true"  
    refresher-default-style="none"  
    refresher-background="rgba(232, 201, 183, 0.5)"  
    :refresher-triggered="data.refreshing2"  

     refresher-max-drag-distance="200px"  
          @refresherpulling="onRefresherpulling2"  
          @refresherrefresh="onRefresherrefresh2"  
          @refresherrestore="onRefresherrestore2"  

     >  
     <uni-refresh-box  
       slot="refresher"  
       :pulling-distance="data.pullingDistance2"  
       :refreshing="data.refreshing2"  
                    loading-class="loading-dark"  
                    text-class="uni-text-class-buildin"  
                    style="flex-direction: column;height: 46px;padding-top: 6px;"  
                    pulling-text="继续下拉可刷新"  
                    loosing-text="释放后会刷新"  
                    loading-text="奋力加载中..."  
                />  

        <view class="page-title">  
            <text style="color:#ff4d40">动态设置中心</text>  
        </view>  
        <view class="tabbar">  
            <text class="title-text">tabbar设置</text>  
            <button class="tabbar-btn" @click="refresh1">动态tabbar(我的)</button>  
            <button class="tabbar-btn" @click="refresh2">动态tabbar(首页、我的)</button>  
            <button class="tabbar-btn" @click="refresh3">动态tabbar(算法、我的)</button>  
            <button class="tabbar-btn" @click="refresh4">动态tabbar(我的、算法、首页、弹出详情、进入详情)</button>  
            <text class="title-text">设置角标</text>  
            <input class="input-item" v-model="tabbar" placeholder="请输入tabbar" />  
            <input class="input-item" v-model="badge" placeholder="请输入角标" />  
            <button class="tabbar-btn" @click="setBadge">确定</button>  
            <button class="tabbar-btn" @click="theme">主题1</button>  
            <button class="tabbar-btn" @click="theme2">主题2</button>  
            <button class="tabbar-btn" @click="theme3">主题3</button>  
            <button class="tabbar-btn" @click="clear">clear</button>  
            <!-- <button class="tabbar-btn" type="default" @click="refresh">刷新</button> -->  
                    <!-- <button   type="default" @click="cPop">返回</button> -->  

        </view>  

    </scroll-view>  
</template>  

<script setup lang="uts">  
    import { ref,onMounted,onUnmounted } from 'vue'  
    import { TabItem, GLOBAL_COMPONENT_REGISTRY, CustomTheme, TabbarIcon, TabbarStyle, NavBarConfig } from '@/uni_modules/sunrains-tabbar/utssdk/types.uts'  
    import { setTabBadge,setTabbarColorHeight,refreshTabbar ,setTabPageTheme,closeTabPop,clearStorage} from '@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts'  

 function cPop(){  
    closeTabPop()  
 }  

   onMounted(() => {  
     console.log('【组件挂载====mine===】')  
   })  

   onUnmounted(() => {  
     console.log('【组件销毁====mine===】')  
   })  

// 用于自动化测试的数据暴露 - 使用 reactive 将所有 ref 状态集中暴露  
const data = reactive({  
  refreshing2: false,  
  pullingDistance2: 0  

})  

function onRefresherpulling2(e: RefresherEvent) {  
  data.pullingDistance2 = e.detail.dy  
}  
    const refreshing = ref(false)  

function onRefresherrefresh2() {  
  data.refreshing2 = true  
  console.log('触发刷新')  
  setTimeout(() => {  
    data.refreshing2 = false  
    console.log('刷新完成')  
  }, 1500)  
}  

function onRefresherrestore2() {  
  data.pullingDistance2 = 0  
}  

function onRefresherabort2() {  
  data.pullingDistance2 = 0  
}  

    // 上拉触底加载  
    const loadMore = () => {  
        console.log("滚动到底部", "info")  
    }  

    function clear() {  
        clearStorage("all")  
    }  

    const bgColor = ref<string>('pink')  
    const tabHeight = ref<string>('120')  
    const badge = ref<string>('0')  
    const ss = ref<any>("");  

    // tab 选项配置  
    const tabbar = ref<string | null>(null)  
    const selectedTab = ref<string>('mine')  
    const selectedTabIndex = ref<number>(0)  

    function setBadge() {  
        //动态设置指定tabbar 角标  
        const badgeNum = parseInt(badge.value)  
        if (isNaN(badgeNum)) {  
            uni.showToast({  
                title: '请输入有效数字',  
                icon: 'none'  
            })  
            return  
        }  
        setTabBadge(tabbar.value,badgeNum)  
        uni.showToast({  
            title: `已设置 ${tabbar.value} 角标为 ${badgeNum}`,  
            icon: 'success'  
        })  
    }  

    function setStyle() {  
        setTabbarColorHeight(bgColor.value,parseInt(tabHeight.value) )  
    }  

    function refresh1() {  
        console.log("动态tabbar(我的)")  
        refreshTabbar([  
            new TabItem('mine', "",12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  
        ]);  
    }  
    function refresh2() {  
        console.log("动态tabbar(首页、我的)")  
        refreshTabbar([  
            new TabItem('home', "",12, "red", "yellow", "bold", '首页', '/static/tab/main1.png', '/static/tab/main1_active.png',24,24,"tabPage"),  
            new TabItem('mine', "",12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  
        ])  
    }  
    function refresh3() {  
        console.log("动态tabbar(算法、我的)")  

        refreshTabbar( [  
            new TabItem('order', "",12, "red", "yellow", "bold", '算法', '/static/tab/main2.png', '/static/tab/main2_active.png',24,24,"tabPage"),  
            new TabItem('mine', "",12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  
        ])  
    }  
    function refresh4() {  
        console.log("动态tabbar(我的、算法、首页、弹出详情、进入详情)")  

        refreshTabbar([  
            new TabItem('mine', "",12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage"),  
            new TabItem('order', "",12, "red", "yellow", "bold", '算法', '/static/tab/main2.png', '/static/tab/main2_active.png',24,24,"tabPage"),  
            new TabItem('home', "",12, "red", "yellow", "bold", '首页', '/static/tab/main1.png',  
             'https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4',  
             24,24,"tabPage"  
             ),  
             new TabItem('detail', "",12, "red", "yellow", "bold", '弹出详情', '/static/tab/main1.png',  
              'https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4',  
              24,24,"tabPop"  
              ),  
              new TabItem('detail', "/pages/detail/detail",12, "red", "yellow", "bold", '进入详情', '/static/tab/main1.png',  
               'https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4',  
               24,24,"page"  
               )  
        ])  
    }  

    function theme() {  
        //主题背景类型  
        let bgType = "IMG"  
        //背景 如果是图片传图片路径 颜色则传 具体颜色  
        let bg : string = "/static/2.png"  
        // let bg:string = "black"  
        let navBarConfig : NavBarConfig = new NavBarConfig(true, 34, 'bold', '#FFA500', "rgba(191, 191, 191, 0.5)", "", 90)  
        let tabbarIcon = new TabbarIcon("mine",  
            "https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4",  
            "/static/tab/main3_active.png",  
            24,24,  
            12,  
            "green",  
            "white",  
            "bold"  
        )  
        let tabbarIcons : Map<string, TabbarIcon> = new Map();  
        tabbarIcons.set("mine", tabbarIcon)  
        let tabbarStyle : TabbarStyle = new TabbarStyle(120, "rgba(21, 20, 31, 0.8)", tabbarIcons)  
        let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
        setTabPageTheme(theme)  
    }  

    function theme2() {  
        //主题背景类型  
        let bgType  = "IMG"  
        //背景 如果是图片传图片路径 颜色则传 具体颜色  
        let bg : string = "/static/sm.jpg"  
        // let bg:string = "black"  
        let navBarConfig : NavBarConfig = new NavBarConfig(false, 34, 'bold', '#0c0c0c', "", "", 90)  
        let tabbarIcon = new TabbarIcon("mine",  
            "/static/aj.png",  
            "/static/aj1.png",  
            24,24,  
            20,  
            "green",  
            "red",  
            "bold"  
        )  
        let tabbarIcon1 = new TabbarIcon("home",  
            "https://static-mp-4c53170b-e5f8-453b-a634-afcb5cf07d59.next.bspapp.com/img/icon/豪横.gif",  
            "https://static-mp-4c53170b-e5f8-453b-a634-afcb5cf07d59.next.bspapp.com/img/icon/豪横.gif",  
            50,24,  
            16,  
            "black",  
            "yellow",  
            "bold"  
        )  
        let tabbarIcons : Map<string, TabbarIcon> = new Map();  
        tabbarIcons.set("mine", tabbarIcon)  
        tabbarIcons.set("home", tabbarIcon1)  
        let tabbarStyle : TabbarStyle = new TabbarStyle(120, "rgba(21, 20, 31, 0.4)", tabbarIcons)  
        let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
        setTabPageTheme(theme)  
    }  
    function theme3() {  
        //主题背景类型  
        let bgType  = "IMG"  
        //背景 如果是图片传图片路径 颜色则传 具体颜色  
        let bg : string = "https://static-mp-4c53170b-e5f8-453b-a634-afcb5cf07d59.next.bspapp.com/img/bizhi/端午1.png"  
        // let bg:string = "black"  
        let navBarConfig : NavBarConfig = new NavBarConfig(true, 34, 'bold', '#0c0c0c', "", "", 90)  
        let tabbarIcon = new TabbarIcon("mine",  
            "/static/aj.png",  
            "/static/aj1.png",  
            24,24,  
            16,  
            "white",  
            "red",  
            "bold"  
        )  
        let tabbarIcon1 = new TabbarIcon("home",  
            "/static/sy1.png",  
            "/static/sy.png",  
            50,24,  
            16,  
            "white",  
            "red",  
            "bold"  
        )  
        let tabbarIcon2 = new TabbarIcon("detail",  
            "/static/tab/main2_active.png",  
            "/static/tab/main2_active.png",  
            24,24,  
            16,  
            "red",  
            "red",  
            "bold"  
        )  
        let tabbarIcons : Map<string, TabbarIcon> = new Map();  
        tabbarIcons.set("mine", tabbarIcon)  
        tabbarIcons.set("home", tabbarIcon1)  
        tabbarIcons.set("detail", tabbarIcon2)  
        let tabbarStyle : TabbarStyle = new TabbarStyle(150, "rgba(10, 13, 31, 0.8)", tabbarIcons)  
        let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
        setTabPageTheme(theme)  
    }  

    function init() {  
        uni.stopPullDownRefresh()  
        console.log('stop onPullDownRefresh=====');  
    }  
    onPullDownRefresh(() => {  
        console.log('onPullDownRefresh=====');  
        init()  

    })  

    function refresh() {  
        uni.startPullDownRefresh({  
            success: () => {  
                console.log("1111")  
            }  
        });  
        init()  
    }  
</script>  

<style scoped lang="scss">  
    .mine-page {  
        height: 100%;  
        // border: 12px solid red;  
        margin: 5px;  
    }  

    .page-title {  
        margin-bottom: 15px;  
        display: flex;  
        align-items: center;  
        justify-content: center;  
        width: 100%;  
    }  

    .title-text {  
        font-size: 18px;  
        font-weight: bold;  
        color: #333;  
    }  

    .tabbar {  
        // border: 2px solid cyan;  
        padding: 15px;  
        display: flex;  
        flex-direction: column;  
    }  

    .input-item {  
        height: 45px;  
        border: 1px solid #ccc;  
        border-radius: 6px;  
        padding-left: 10px;  
        background: #fff;  
        margin: 10px;  
        padding-left: 20px;  
    }  

    .picker-item {  
        height: 45px;  
        border: 1px solid #ccc;  
        border-radius: 6px;  
        background: #fff;  
        margin: 10px;  
        display: flex;  
        align-items: center;  
    }  

    .picker-display {  
        padding-left: 20px;  
        font-size: 16px;  
        color: #333;  
    }  

    .tabbar-btn {  
        margin-bottom: 10px;  
        border-radius: 8px;  
        background: #f5f5f5;  
        border: 1px solid #e5e5e5;  
        padding: 16px 0;  
        font-size: 16px;  
    }  

    .tabbar-btn:last-child {  
        margin-bottom: 0;  
    }  
</style>  

6.5 主题配置注意事项

  1. 颜色格式:颜色值必须为有效的 CSS 颜色格式(如 #ffffff、rgb(255,255,255) 等)
  2. tabbar区域高度单位:高度值为数字类型,单位为 rpx
  3. 动态修改:通过 uni.$emit('setTheme', new CustomTheme(bgType, bg, navBarConfig, tabbarStyle)) 动态修改的主题会立即生效

7 page页跳转自定义tab页

使用 pageToTab 方法 跳转到tab页

以 detail.uvue 页面为例,实现主题切换功能:

<template>  

    <view class="detail-root">  

        <view class="detail-content">  
<!--   
            <view>  
                <input v-model="content" placeholder="请输入。。" />  
                <text>{{sbase64}}</text>  
                <text>{{base64yostr}}</text>  
            </view> -->  
            <view class = "con">  
                <view  class = "com">  
                    <text>网络请求</text>     
                </view>  
                <view>  

                    <button class="btn" type="primary" @click="req">访问testvoid</button>  
                    <button class="btn"  type="primary" @click="reqSm2">访问testSm2</button>  
                    <button class="btn" type="primary" @click="reqSm4">访问testSm4</button>  
                    <button class="btn" type="primary" @click="reqSm2Sm4">访问testreqSm2Sm4</button>  
                </view>  
            </view>  

            <view class = "con">  
                <view class = "com">  
                    <text>跳转tab页</text>  
                </view>  

                <view>  
                    <input style="height: 50px; border:1px solid grey;margin: 5px 0;" v-model="addr" placeholder="输入跳转tabbar key"/>  
                    <button type="warn" @click="tz">跳转tab页</button>   
                </view>  
            </view>  

        </view>  

    </view>  

</template>  

<script setup lang="uts">  
    import { ref, computed } from 'vue'  
    import { stringToBase64, base64ToString, ReqApi } from "@/uni_modules/sunrains-request"  
    import { Response, FrontRequestVo } from "@/uni_modules/sunrains-common-type"  
    import { CustFailError, getComErrorInfo } from "@/uni_modules/sunrains-common-err"  
    import { pageToTab} from "@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts"  

   onMounted(() => {  
     console.log('【组件挂载====detail===】')  
   })  

   onUnmounted(() => {  
     console.log('【组件销毁====detail===】')  
   })  
    const addr = ref("")  
    function tz(){  
        if(addr.value.length<1){  
            uni.showToast({  
                title: '请输入跳转的tabbar key'  
            });  
        }else{  
            pageToTab(addr.value)  
        }  

    }  

    function fh(){  
        uni.navigateBack()  

    }  

    let priKey = "4a4a1d48fc284fe2fc43970641880554aed2f69fdfbea63257ea417af4e65802"  
    let pubKey = "0460e86263ec52b38a3da24c7a605055d14f8aa5c7855e4f4e19819f4f8a5b72181afd7aa4ab19255eb871eaedaba30fa18cd5c71693d3d76b030a04969bd1edfe"  
    const BASEURL = "http://192.168.1.9:8081"  
    type IRootType = {  
        name : string;  
        age : number;  
    }  

    type Builds = {  
        buildingNo : string,  
        sortNum : number,  
        buildAreaId : string,  
        id : string  
    }  

    async function req() {  
        let data : UTSJSONObject = {  
            name: "张三",  
            age: 18  
        }  
        try {  

            let vo : FrontRequestVo = {  
                method: 'GET',  
                url: BASEURL + "/test/get/cs1",  
                data: data,  
                header: null,  
                reqFlag: "void",  
                priKey: priKey,  
                pubKey: pubKey,  
                //method 为POST时 不能为null  
                contentType: "urlencoded",  
                timeout: null,  
                //当为上传文件时 文件路径参数不能为空  
                filePathParamName: null  
            }  
            const res = await ReqApi(vo)  
            console.log("####reqvoid==", res.data)  
            // console.log("####==",res)  
        } catch (err) {  
            uni.showToast({  
                title: err.message ?? "请求异常",  
                icon: "error"  
            })  
            console.log("-----", err)  

            return  
        };  
    }  

    //"sign"|"sm4Encrypt"|"signwithenSM2-Sm4Encrypt"|"void";  
    async function reqSm2() {  
        let data : UTSJSONObject = {  
            name: "张三",  
            age: 18  
        }  

        try {  
            let vo : FrontRequestVo = {  
                method: 'GET',  
                url: BASEURL + "/test/get/cs1",  
                data: data,  
                header: null,  
                reqFlag: "sign",  
                priKey: priKey,  
                pubKey: pubKey,  
                //method 为POST时 不能为null  
                contentType: "urlencoded",  
                timeout: null,  
                //当为上传文件时 文件路径参数不能为空  
                filePathParamName: null  
            }  

            const res = await ReqApi(vo)  
            console.log("####reqSm2==", res.data)  
        } catch (err) {  
            uni.showToast({  
                title: err.message ?? "请求异常",  
                icon: "error"  
            })  

            //#ifndef APP-ANDROID  
            const errMsg = (err as any).errMsg ?? "未知错误";  
            const errCode = (err as any).errCode ?? -1;  
            console.log('标准化错误码:', errMsg);  
            console.log('标准化错误信息:', errCode);  
            //#else   
            if (err instanceof UniError) {  
                let sa = err as UniError  
                console.log('sa==:', sa);  
            }  
            //#endif  
            console.log("-----", err)  
            return  
        };  
    }  

    async function reqSm4() {  
        let data : UTSJSONObject = {  
            name: "张三",  
            age: 18  
        }  
        try {  
            let vo : FrontRequestVo = {  
                method: 'POST',  
                url: BASEURL + "/test/post/cs1",  
                data: data,  
                header: null,  
                reqFlag: "sm4Encrypt",  
                priKey: priKey,  
                pubKey: pubKey,  
                //method 为POST时 不能为null  
                contentType: "urlencoded",  
                timeout: null,  
                //当为上传文件时 文件路径参数不能为空  
                filePathParamName: null  
            }  

            const res = await ReqApi(vo)  
            console.log("####reqSm4==", res.data)  
        } catch (err) {  
            console.log("####reqSm4 err==", err)  
            uni.showToast({  
                title: err.message??"",  
                icon: "error"  
            })  
            return  
        };  
    }  

    async function reqSm2Sm4() {  
        let data : UTSJSONObject = {  
            name: "张三",  
            age: 18  
        }  
        try {  
            let vo : FrontRequestVo = {  
                method: 'POST',  
                url: BASEURL + "/test/post/cs1",  
                data: data,  
                header: null,  
                reqFlag: "signwithenSM2-Sm4Encrypt",  
                priKey: priKey,  
                pubKey: pubKey,  
                //method 为POST时 不能为null  
                contentType: "urlencoded",  
                timeout: null,  
                //当为上传文件时 文件路径参数不能为空  
                filePathParamName: null  
            }  

            const res = await ReqApi(vo)  
            console.log("####reqSm2Sm4==", res.data)  
        } catch (err) {  
            console.log("-----", err)  
            return  
        };  
    }  

    const orderId = ref<string>('')  
    const orderStatus = ref<string>('')  
    const content = ref<string>('')  
    const sbase64 = computed(() => {  
        return stringToBase64(content.value)  
    })  

    const base64yostr = computed(() => {  
        return base64ToString(sbase64.value)  
    })  

    onLoad((options) => {  
        const opts = options as UTSJSONObject  
        const id = options?.["orderId"] as string | null  
        const status = options?.["status"] as string | null  

        if (id != null) {  
            orderId.value = id  
        }  
        if (status != null) {  
            orderStatus.value = status  
        }  
        console.log(`接收算法参数:ID=${orderId.value},状态=${orderStatus.value}`)  
    })  

    const goBack = () => {  
        uni.navigateBack()  
    }  

    function init(){  
              // uni.stopPullDownRefresh()  
              // console.log('stop onPullDownRefresh=====');  
    }  

     //  onPullDownRefresh(() => {  
     //    console.log('onPullDownRefresh=====');  
        // init()  

     //  })  

      function refresh(){  
          uni.removeStorageSync("LOGINTABBARITEMS")  
          // uni.startPullDownRefresh({  
             //  success:()=>{  
                //   console.log("1111")  
             //  }  
          // });  
          // init()  
      }  

</script>  

<style scoped>  
    .detail-root {  
/*      flex: 1;  
        display: flex;  
        flex-direction: column; */  
        background-color: #fff;  
    }  

    .status-bar {  
        height: 44px;  
        background-color: #ffffff;  
    }  

    .detail-header {  
        height: 44px;  
        background-color: #fff;  
        flex-direction: row;  
        align-items: center;  
        padding-left: 15px;  
        padding-right: 15px;  
        border-bottom-width: 1px;  
        border-bottom-style: solid;  
        border-bottom-color: #eee;  
    }  

    .back-icon {  
        font-size: 18px;  
        color: #007aff;  
        margin-right: 10px;  
    }  

    .detail-title {  
        font-size: 16px;  
        font-weight: bold;  
        color: #333;  
    }  

    .detail-content {  
        flex: 1;  
        padding: 20px;  
    }  

    .detail-item {  
        flex-direction: row;  
        margin-bottom: 20px;  
        align-items: center;  
    }  

    .label {  
        font-size: 15px;  
        color: #666;  
        width: 80px;  
    }  

    .value {  
        font-size: 15px;  
        color: #333;  
        font-weight: bold;  
    }  

    .detail-desc {  
        margin-top: 30px;  
    }  

    .com{  
        display:flex;  
        align-items: center;  
        margin:10px 0;   
    }  
    .con{  
        margin:10px 0;   
        padding:10px;  
        background-color: #e3eda1;  
    }  
    .btn{  
        margin: 5px;  
    }  
</style>  

8 拓展底部tabbar类型,(切换、弹出、进入)页面

    let mine = new TabItem(  
           'mine', //组件或页面唯一标识  
            "", //组件或页面路径  当类型为 page 时不可为空,其它传空字符串即可  
            12,  
            "red",  
            "yellow",   
            "bold",   
            '我的',   
            '/static/tab/main3.png',  
            '/static/tab/main3_active.png',  
            24,//图标宽  
            24,//图标高  
            "tabPage" //tabbar 类型 tabPage:tab页面  tabPop:弹窗组件  page:普通页面  
        )  

注:
类型为 tabPop 打开页面 可 右滑手势(当手机开启右滑手势时)关闭、 或点击虚拟返回按键关闭 或 页面中调用方法 closeTabPop()关闭

方法导入 import { pageToTab} from "@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts"


⚠️ 常见问题

Q1: 为什么小程序端需要单独配置?

A: 微信小程序不支持动态组件渲染,必须静态声明所有可能使用的组件。

Q2: 动态 Tab 刷新失败怎么办?

A: 检查以下几点:

  • 组件是否在 tabbar-config.uts 中正确注册
  • 是否正确使用了 GLOBAL_COMPONENT_REGISTRY.get() 获取组件
  • TabItem 的参数是否完整且类型正确

Q3: 导航栏配置不生效?

A: 确保 setNavbarConfig() 在 setDefaultTabItems() 之前调用,或在应用启动时尽早调用。

Q4: 如何设置数字角标?

A: 使用 uni.$emit('setTabBadge', {"key": "tab_key", "badge": number}) 即可,详见第 4 章。

Q5: 角标设置后不显示怎么办?

A: 检查以下几点:

  • badge 值是否大于 0(0 或负数不显示)
  • tab key 是否正确(必须与 TabItem 的 key 一致)
  • 是否重新赋值了整个数组(直接修改对象属性不会触发响应式)

Q6: 退出登录如何清除 tabbar相关缓存

A: 调用 noticetab.uts里的 clearStorage 方法

/**  
 * 清除tabbar相关缓存  
 * all 所有  
 * theme 主题  
 * tabbarItems 所有tabbar  
 * badge 角标  
 */  
export function clearStorage(type:string){  
    switch (type){  
        case "theme":  
            uni.removeStorageSync("sunrains-tabbar-customtheme")      
            break;  
        case "tabbarItems":  
            uni.removeStorageSync("sunrains-tabbar-logintabbaritems")  
            break;    
        case "badge":  
            uni.removeStorageSync("sunrains-tabbar-tabbarbadges")  
            break;    
        default:  
            uni.removeStorageSync("sunrains-tabbar-customtheme")  
            uni.removeStorageSync("sunrains-tabbar-logintabbaritems")  
            uni.removeStorageSync("sunrains-tabbar-tabbarbadges")         
    }  

}

继续阅读 »

点击查看插件

sunrains-tabbar 自定义 TabBar 插件使用指南

点击体验h5端

点击查看安卓端录屏效果

点击查看安卓端演示安装包

目录

  • [1. 基础配置]
  • [2. pages.json 配置]
  • [3. 动态配置 TabBar]
  • [4. 数字角标功能]
  • [5. 小程序端特殊配置]
  • [6. 主题配置]
  • [7. page页跳转自定义tab页]
  • [8. 拓展底部tabbar类型,(切换、弹出、进入)页面]

1. 基础配置

App.uvue onLaunch 初始化  
 import {initDefaultTabItems} from '@/uni_modules/sunrains-tabbar/tabbar-config.uts'  
        onLaunch(() => {  
        console.log('App Launch')  
        initDefaultTabItems()  
        }

1.1 配置 tabbar-config.uts 文件

在插件根目录 tabbar-config.uts 文件中,配置注册全局tab组件和导航栏设置。

完整示例:

import { setNavbarConfig, NavBarConfig, registerGlobalComponent, setDefaultTabItems, TabItem } from '@/uni_modules/sunrains-tabbar/utssdk/types.uts'  
import { getStoreLoginItems,storeLoginItems } from './utssdk/handle'  
//导入所有自定义tab页  
import Recommend from '@/pages/home/recommend.uvue'  
import Order from '@/pages/order/order.uvue'  
import Mine from '@/pages/mine/mine.uvue'  
import Detail from '@/pages/detail/detail.uvue'  

//注册所有需要切换的tab页或需要弹出的页面  即类型为 tabPage:tab页面  tabPop:弹窗组件  
registerGlobalComponent('home', Recommend)  
registerGlobalComponent('order', Order)  
registerGlobalComponent('mine', Mine)  
registerGlobalComponent('detail', Detail)  

// 2. 配置导航栏(不设置会使用默认配置)  
setNavbarConfig(new NavBarConfig(true, 34, 'bold', '#333333', '#ffffff', "", 88))  

//初始化默认tabbar区域按钮  
export function initDefaultTabItems(){  
    /**  
     *  
     * 3. 设置默认 Tab 列表(至少设置一个固定的,也可以全部设置,其它可通过请求服务端接口动态配置),  
     * 或通过网络请求初始化 游客 显示哪些  有 角色的 按角色 初始化  
     * App.uvue onLaunch 初始化  
      import {initDefaultTabItems} from '@/uni_modules/sunrains-tabbar/tabbar-config.uts'  
        onLaunch(() => {  
        console.log('App Launch')  
        initDefaultTabItems()  
        }  
     */  
    let home = new TabItem('home',"", 12, "red", "yellow", "bold", '首页', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  
    let mine = new TabItem('mine',"", 12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  

    let items = getStoreLoginItems();  
    //判断是否登录  
    if(false || items != null){  
        setDefaultTabItems(items)  
    }else{  
        setDefaultTabItems([  
            home,mine  
        ])  
        storeLoginItems([home,mine])  
    }  

}  

关键步骤说明:

  1. 导入组件:将所有需要作为 Tab 的页面组件导入
  2. 注册组件:使用 registerGlobalComponent 注册每个组件(第一个参数为唯一标识)
  3. 配置导航栏:可选配置,根据项目需求调整
  4. 设置默认 Tab:至少配置一个固定 Tab,其余可动态添加

2. pages.json 配置

2.1 必需配置

必须在 pages.json 中配置自定义 TabBar 入口页面(必须放在第一个位置),tab页无需在pages.json中配置:

{  
  "path": "uni_modules/sunrains-tabbar/index",  
  "style": {  
    "navigationStyle": "custom"  
  }  
}

2.2 完整示例

{  
    "pages": [   
        {  
          "path": "uni_modules/sunrains-tabbar/utssdk/index",  
          "style": {  
            "navigationStyle": "custom"  
          }  
        },  
        {  
          "path": "pages/detail/detail",  
          "style": {  
            "navigationBarTitleText": "详情"  
          }  
        }  
    ],  
    "globalStyle": {  
        "navigationBarTextStyle": "black",  
        "navigationBarTitleText": "uni-app x",  
        "navigationBarBackgroundColor": "#F8F8F8",  
        "backgroundColor": "#F8F8F8"  
    },  
    "uniIdRouter": {}  
}  

注意事项:

  • 自定义 TabBar 入口页面必须配置在 pages 数组的第一位
  • 所有 Tab 页面不需要在 pages 中声明
  • navigationStyle 设置为 "custom" 以隐藏原生导航栏

3. 动态配置 TabBar

3.1 使用场景

当需要根据用户权限、服务端配置等动态调整 TabBar 时,可以在任意页面触发刷新。

3.2 实现示例

以下面文档 mine.uvue 页面为例:

关键点:

  • 使用 refreshTabbar方法触发 TabBar 刷新
  • 确保所有动态 Tab 对应的组件已在 tabbar-config.uts 中注册

4. 数字角标功能

4.1 功能说明

支持为任意 Tab 设置数字角标,用于显示未读消息数、待处理事项等。

特性:

  • ✅ 动态设置/更新角标
  • ✅ 超过 99 自动显示 "99+"
  • ✅ 角标为 0 或 null 时不显示
  • ✅ 响应式更新,立即生效

4.2 使用方法

方法一:通过事件设置(推荐)

在任意页面通过 调用 setTabBadge 方法:

import { setTabBadge} from '@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts'  
// 设置算法 tab 的角标为 5  
setTabBadge("order",5)  
// 清除角标(设置为 0)  
setTabBadge("order",0)  
// 设置超过 99 显示 99+  
setTabBadge("order",150)

方法二:初始化时设置

在创建 TabItem 时传入 badge 参数:

import { TabItem } from '@/uni_modules/sunrains-tabbar/utssdk/types.uts'  

// 带角标的 tab  
new TabItem(  
  'order',   
  '算法',   
  '/static/tab/main2.png',   
  '/static/tab/main2_active.png'  
  10  // badge 参数  
)  

// 不带角标的 tab  
new TabItem(  
  'home',   
  '首页',   
  '/static/tab/main1.png',   
  '/static/tab/main1_active.png'  
)

4.3 完整示例

以 文档后面 mine.uvue 页面为例,实现动态设置角标功能:

4.4 实际应用场景

场景 1:算法未读数

// 从服务端获取未读算法数  
async function loadUnreadOrderCount() {  
  const res = await getOrderCount()  
  setTabBadge("order",res.data.count)  
}  

// 页面加载时调用  
onShow(() => {  
  loadUnreadOrderCount()  
})

场景 2:消息通知

// WebSocket 收到新消息  
websocket.onMessage((msg) => {  
  if (msg.type === 'new_message') {  
    const currentBadge = getCurrentBadge('message')  
    setTabBadge("newMessage",currentBadge + 1)  
  }  
})

场景 3:购物车数量

// 添加商品到购物车后更新角标  
function addToCart(product: Product) {  
  cartItems.push(product)  
  setTabBadge("cart",cartItems.length)  
}

5. 小程序端特殊配置

4.1 配置 tab-content-renderer.uvue

注意: 仅在需要支持微信小程序时需要配置,App 端可忽略此步骤。

在 uni_modules/sunrains-tabbar/tab-content-renderer.uvue 中配置:

<<template>  
  <view style="width: 100%; height: 100%;">  
   <Recommend v-if="currentTab === 'home'" style="width: 100%; height: 100%;" />  
    <Order v-if="currentTab === 'order'" style="width: 100%; height: 100%;" />  
    <Mine v-if="currentTab === 'mine'" style="width: 100%; height: 100%;" />  
      <Detail v-if="currentTab === 'detail'" style="width: 100%; height: 100%;" />  
  </view>  
</template>  

<script setup lang="uts">  
import Recommend from '@/pages/home/recommend.uvue'  
import Order from '@/pages/order/order.uvue'  
import Mine from '@/pages/mine/mine.uvue'  
import Detail from '@/pages/detail/detail.uvue'  
interface Props {  
  currentTab: string  
}  
defineProps({  
    currentTab:{  
        type:String  
    }  
})  
</script>  

配置说明:

  1. 条件编译:使用 #ifndef MP-WEIXIN 和 #ifdef MP-WEIXIN 区分平台
  2. App 端:使用 <component :is="..."> 动态渲染组件
  3. 小程序端:必须静态导入并声明所有可能的 Tab 组件
  4. 导入位置:在 #ifdef MP-WEIXIN 块内导入组件

6. 主题配置

6.1 主题功能说明

插件支持自定义 TabBar 的主题样式,支持网络背景图、本地图片背景图、纯颜色背景(支持rgba透明)、导航栏、tabbar区域背景、图标大小、图标(支持网络图标)、文字颜色、文字大小、等属性 满足不同项目的视觉需求。

主题特性:

  • ✅ 支持全局主题配置
  • ✅ 支持动态切换主题
  • ✅ 支持自定义主题参数

6.2 初始化时配置主题

在 tabbar-config.uts 文件中配置主题:

import {   
  setNavbarConfig,   
  NavBarConfig,   
  registerGlobalComponent,   
  setDefaultTabItems,   
  TabItem,  
  setTabBarTheme,  
  TabBarTheme  
} from '@/uni_modules/sunrains-tabbar/types.uts'  
import { setTabPageTheme} from '@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts'  

let bgType : BgType = "IMG"  
//背景 如果是图片传图片路径 颜色则传 具体颜色  
let bg : string = "/static/2.png"  
//配置导航栏(可选,不设置会使用默认配置)  
let navBarConfig : NavBarConfig = new NavBarConfig(  
    true, // 是否显示导航栏  
    34,  // 字体大小  
    'bold',  // 字体粗细  
    '#FFA500', // 文字颜色  
    '',// 背景颜色  
    "", //背景图片(支持网络图片)  
    90) // 导航栏高度  

let tabbarIcon = new TabbarIcon(  
    "mine",//自定义组件唯一标识  
    //非选中图标  
    "https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4",  
   //选中图标  
    "/static/tab/main3_active.png",  
    24,//图标宽 单位 px  
    24,//图标高 单位 px  
   //文字颜色  
    12,  
    //非选中文字大小  
    "green",  
    //选择文字颜色  
    "white",  
    //选择文字粗细  
    "bold"  
)  

let tabbarIcons : Map<string,TabbarIcon> = new Map();  
tabbarIcons.set("mine",tabbarIcon)  

let tabbarStyle : TabbarStyle = new TabbarStyle(  
    //tabbar区域高度  
    120,  
    //tabbar区域背景  
    "rgba(21, 20, 31, 0.8)",  
    //tabbar图标文字样式  
    tabbarIcons)  

let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
setTabPageTheme(theme)

6.3 动态修改主题

在任意页面通过调用 setTabPageTheme 方法动态修改主题:

import { setTabPageTheme} from '@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts'  
// 动态修改背景色和选中颜色  
let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
setTabPageTheme(theme)

6.4 在页面中动态切换主题

以 mine.uvue 页面为例,实现主题切换功能:

<template>  
    <scroll-view direction="vertical" class="mine-page"  
    show-scrollbar = "false"  
    enable-back-to-top="true"  
    refresher-enabled="true"  
    refresher-default-style="none"  
    refresher-background="rgba(232, 201, 183, 0.5)"  
    :refresher-triggered="data.refreshing2"  

     refresher-max-drag-distance="200px"  
          @refresherpulling="onRefresherpulling2"  
          @refresherrefresh="onRefresherrefresh2"  
          @refresherrestore="onRefresherrestore2"  

     >  
     <uni-refresh-box  
       slot="refresher"  
       :pulling-distance="data.pullingDistance2"  
       :refreshing="data.refreshing2"  
                    loading-class="loading-dark"  
                    text-class="uni-text-class-buildin"  
                    style="flex-direction: column;height: 46px;padding-top: 6px;"  
                    pulling-text="继续下拉可刷新"  
                    loosing-text="释放后会刷新"  
                    loading-text="奋力加载中..."  
                />  

        <view class="page-title">  
            <text style="color:#ff4d40">动态设置中心</text>  
        </view>  
        <view class="tabbar">  
            <text class="title-text">tabbar设置</text>  
            <button class="tabbar-btn" @click="refresh1">动态tabbar(我的)</button>  
            <button class="tabbar-btn" @click="refresh2">动态tabbar(首页、我的)</button>  
            <button class="tabbar-btn" @click="refresh3">动态tabbar(算法、我的)</button>  
            <button class="tabbar-btn" @click="refresh4">动态tabbar(我的、算法、首页、弹出详情、进入详情)</button>  
            <text class="title-text">设置角标</text>  
            <input class="input-item" v-model="tabbar" placeholder="请输入tabbar" />  
            <input class="input-item" v-model="badge" placeholder="请输入角标" />  
            <button class="tabbar-btn" @click="setBadge">确定</button>  
            <button class="tabbar-btn" @click="theme">主题1</button>  
            <button class="tabbar-btn" @click="theme2">主题2</button>  
            <button class="tabbar-btn" @click="theme3">主题3</button>  
            <button class="tabbar-btn" @click="clear">clear</button>  
            <!-- <button class="tabbar-btn" type="default" @click="refresh">刷新</button> -->  
                    <!-- <button   type="default" @click="cPop">返回</button> -->  

        </view>  

    </scroll-view>  
</template>  

<script setup lang="uts">  
    import { ref,onMounted,onUnmounted } from 'vue'  
    import { TabItem, GLOBAL_COMPONENT_REGISTRY, CustomTheme, TabbarIcon, TabbarStyle, NavBarConfig } from '@/uni_modules/sunrains-tabbar/utssdk/types.uts'  
    import { setTabBadge,setTabbarColorHeight,refreshTabbar ,setTabPageTheme,closeTabPop,clearStorage} from '@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts'  

 function cPop(){  
    closeTabPop()  
 }  

   onMounted(() => {  
     console.log('【组件挂载====mine===】')  
   })  

   onUnmounted(() => {  
     console.log('【组件销毁====mine===】')  
   })  

// 用于自动化测试的数据暴露 - 使用 reactive 将所有 ref 状态集中暴露  
const data = reactive({  
  refreshing2: false,  
  pullingDistance2: 0  

})  

function onRefresherpulling2(e: RefresherEvent) {  
  data.pullingDistance2 = e.detail.dy  
}  
    const refreshing = ref(false)  

function onRefresherrefresh2() {  
  data.refreshing2 = true  
  console.log('触发刷新')  
  setTimeout(() => {  
    data.refreshing2 = false  
    console.log('刷新完成')  
  }, 1500)  
}  

function onRefresherrestore2() {  
  data.pullingDistance2 = 0  
}  

function onRefresherabort2() {  
  data.pullingDistance2 = 0  
}  

    // 上拉触底加载  
    const loadMore = () => {  
        console.log("滚动到底部", "info")  
    }  

    function clear() {  
        clearStorage("all")  
    }  

    const bgColor = ref<string>('pink')  
    const tabHeight = ref<string>('120')  
    const badge = ref<string>('0')  
    const ss = ref<any>("");  

    // tab 选项配置  
    const tabbar = ref<string | null>(null)  
    const selectedTab = ref<string>('mine')  
    const selectedTabIndex = ref<number>(0)  

    function setBadge() {  
        //动态设置指定tabbar 角标  
        const badgeNum = parseInt(badge.value)  
        if (isNaN(badgeNum)) {  
            uni.showToast({  
                title: '请输入有效数字',  
                icon: 'none'  
            })  
            return  
        }  
        setTabBadge(tabbar.value,badgeNum)  
        uni.showToast({  
            title: `已设置 ${tabbar.value} 角标为 ${badgeNum}`,  
            icon: 'success'  
        })  
    }  

    function setStyle() {  
        setTabbarColorHeight(bgColor.value,parseInt(tabHeight.value) )  
    }  

    function refresh1() {  
        console.log("动态tabbar(我的)")  
        refreshTabbar([  
            new TabItem('mine', "",12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  
        ]);  
    }  
    function refresh2() {  
        console.log("动态tabbar(首页、我的)")  
        refreshTabbar([  
            new TabItem('home', "",12, "red", "yellow", "bold", '首页', '/static/tab/main1.png', '/static/tab/main1_active.png',24,24,"tabPage"),  
            new TabItem('mine', "",12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  
        ])  
    }  
    function refresh3() {  
        console.log("动态tabbar(算法、我的)")  

        refreshTabbar( [  
            new TabItem('order', "",12, "red", "yellow", "bold", '算法', '/static/tab/main2.png', '/static/tab/main2_active.png',24,24,"tabPage"),  
            new TabItem('mine', "",12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage")  
        ])  
    }  
    function refresh4() {  
        console.log("动态tabbar(我的、算法、首页、弹出详情、进入详情)")  

        refreshTabbar([  
            new TabItem('mine', "",12, "red", "yellow", "bold", '我的', '/static/tab/main3.png', '/static/tab/main3_active.png',24,24,"tabPage"),  
            new TabItem('order', "",12, "red", "yellow", "bold", '算法', '/static/tab/main2.png', '/static/tab/main2_active.png',24,24,"tabPage"),  
            new TabItem('home', "",12, "red", "yellow", "bold", '首页', '/static/tab/main1.png',  
             'https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4',  
             24,24,"tabPage"  
             ),  
             new TabItem('detail', "",12, "red", "yellow", "bold", '弹出详情', '/static/tab/main1.png',  
              'https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4',  
              24,24,"tabPop"  
              ),  
              new TabItem('detail', "/pages/detail/detail",12, "red", "yellow", "bold", '进入详情', '/static/tab/main1.png',  
               'https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4',  
               24,24,"page"  
               )  
        ])  
    }  

    function theme() {  
        //主题背景类型  
        let bgType = "IMG"  
        //背景 如果是图片传图片路径 颜色则传 具体颜色  
        let bg : string = "/static/2.png"  
        // let bg:string = "black"  
        let navBarConfig : NavBarConfig = new NavBarConfig(true, 34, 'bold', '#FFA500', "rgba(191, 191, 191, 0.5)", "", 90)  
        let tabbarIcon = new TabbarIcon("mine",  
            "https://gimg3.baidu.com/search/src=https%3A%2F%2Fpic.rmb.bdstatic.com%2Fbjh%2Fuser%2F779083e57f84a4ce5345b17aaf7f8459.jpeg&refer=http%3A%2F%2Fwww.baidu.com&app=2021&size=r1,1&n=0&g=4&er=404&q=100&maxorilen2heic=2000000?sec=1781283600&t=4feca8b5a1149e3ed0f28ee030f767b4",  
            "/static/tab/main3_active.png",  
            24,24,  
            12,  
            "green",  
            "white",  
            "bold"  
        )  
        let tabbarIcons : Map<string, TabbarIcon> = new Map();  
        tabbarIcons.set("mine", tabbarIcon)  
        let tabbarStyle : TabbarStyle = new TabbarStyle(120, "rgba(21, 20, 31, 0.8)", tabbarIcons)  
        let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
        setTabPageTheme(theme)  
    }  

    function theme2() {  
        //主题背景类型  
        let bgType  = "IMG"  
        //背景 如果是图片传图片路径 颜色则传 具体颜色  
        let bg : string = "/static/sm.jpg"  
        // let bg:string = "black"  
        let navBarConfig : NavBarConfig = new NavBarConfig(false, 34, 'bold', '#0c0c0c', "", "", 90)  
        let tabbarIcon = new TabbarIcon("mine",  
            "/static/aj.png",  
            "/static/aj1.png",  
            24,24,  
            20,  
            "green",  
            "red",  
            "bold"  
        )  
        let tabbarIcon1 = new TabbarIcon("home",  
            "https://static-mp-4c53170b-e5f8-453b-a634-afcb5cf07d59.next.bspapp.com/img/icon/豪横.gif",  
            "https://static-mp-4c53170b-e5f8-453b-a634-afcb5cf07d59.next.bspapp.com/img/icon/豪横.gif",  
            50,24,  
            16,  
            "black",  
            "yellow",  
            "bold"  
        )  
        let tabbarIcons : Map<string, TabbarIcon> = new Map();  
        tabbarIcons.set("mine", tabbarIcon)  
        tabbarIcons.set("home", tabbarIcon1)  
        let tabbarStyle : TabbarStyle = new TabbarStyle(120, "rgba(21, 20, 31, 0.4)", tabbarIcons)  
        let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
        setTabPageTheme(theme)  
    }  
    function theme3() {  
        //主题背景类型  
        let bgType  = "IMG"  
        //背景 如果是图片传图片路径 颜色则传 具体颜色  
        let bg : string = "https://static-mp-4c53170b-e5f8-453b-a634-afcb5cf07d59.next.bspapp.com/img/bizhi/端午1.png"  
        // let bg:string = "black"  
        let navBarConfig : NavBarConfig = new NavBarConfig(true, 34, 'bold', '#0c0c0c', "", "", 90)  
        let tabbarIcon = new TabbarIcon("mine",  
            "/static/aj.png",  
            "/static/aj1.png",  
            24,24,  
            16,  
            "white",  
            "red",  
            "bold"  
        )  
        let tabbarIcon1 = new TabbarIcon("home",  
            "/static/sy1.png",  
            "/static/sy.png",  
            50,24,  
            16,  
            "white",  
            "red",  
            "bold"  
        )  
        let tabbarIcon2 = new TabbarIcon("detail",  
            "/static/tab/main2_active.png",  
            "/static/tab/main2_active.png",  
            24,24,  
            16,  
            "red",  
            "red",  
            "bold"  
        )  
        let tabbarIcons : Map<string, TabbarIcon> = new Map();  
        tabbarIcons.set("mine", tabbarIcon)  
        tabbarIcons.set("home", tabbarIcon1)  
        tabbarIcons.set("detail", tabbarIcon2)  
        let tabbarStyle : TabbarStyle = new TabbarStyle(150, "rgba(10, 13, 31, 0.8)", tabbarIcons)  
        let theme = new CustomTheme(bgType, bg, navBarConfig, tabbarStyle);  
        setTabPageTheme(theme)  
    }  

    function init() {  
        uni.stopPullDownRefresh()  
        console.log('stop onPullDownRefresh=====');  
    }  
    onPullDownRefresh(() => {  
        console.log('onPullDownRefresh=====');  
        init()  

    })  

    function refresh() {  
        uni.startPullDownRefresh({  
            success: () => {  
                console.log("1111")  
            }  
        });  
        init()  
    }  
</script>  

<style scoped lang="scss">  
    .mine-page {  
        height: 100%;  
        // border: 12px solid red;  
        margin: 5px;  
    }  

    .page-title {  
        margin-bottom: 15px;  
        display: flex;  
        align-items: center;  
        justify-content: center;  
        width: 100%;  
    }  

    .title-text {  
        font-size: 18px;  
        font-weight: bold;  
        color: #333;  
    }  

    .tabbar {  
        // border: 2px solid cyan;  
        padding: 15px;  
        display: flex;  
        flex-direction: column;  
    }  

    .input-item {  
        height: 45px;  
        border: 1px solid #ccc;  
        border-radius: 6px;  
        padding-left: 10px;  
        background: #fff;  
        margin: 10px;  
        padding-left: 20px;  
    }  

    .picker-item {  
        height: 45px;  
        border: 1px solid #ccc;  
        border-radius: 6px;  
        background: #fff;  
        margin: 10px;  
        display: flex;  
        align-items: center;  
    }  

    .picker-display {  
        padding-left: 20px;  
        font-size: 16px;  
        color: #333;  
    }  

    .tabbar-btn {  
        margin-bottom: 10px;  
        border-radius: 8px;  
        background: #f5f5f5;  
        border: 1px solid #e5e5e5;  
        padding: 16px 0;  
        font-size: 16px;  
    }  

    .tabbar-btn:last-child {  
        margin-bottom: 0;  
    }  
</style>  

6.5 主题配置注意事项

  1. 颜色格式:颜色值必须为有效的 CSS 颜色格式(如 #ffffff、rgb(255,255,255) 等)
  2. tabbar区域高度单位:高度值为数字类型,单位为 rpx
  3. 动态修改:通过 uni.$emit('setTheme', new CustomTheme(bgType, bg, navBarConfig, tabbarStyle)) 动态修改的主题会立即生效

7 page页跳转自定义tab页

使用 pageToTab 方法 跳转到tab页

以 detail.uvue 页面为例,实现主题切换功能:

<template>  

    <view class="detail-root">  

        <view class="detail-content">  
<!--   
            <view>  
                <input v-model="content" placeholder="请输入。。" />  
                <text>{{sbase64}}</text>  
                <text>{{base64yostr}}</text>  
            </view> -->  
            <view class = "con">  
                <view  class = "com">  
                    <text>网络请求</text>     
                </view>  
                <view>  

                    <button class="btn" type="primary" @click="req">访问testvoid</button>  
                    <button class="btn"  type="primary" @click="reqSm2">访问testSm2</button>  
                    <button class="btn" type="primary" @click="reqSm4">访问testSm4</button>  
                    <button class="btn" type="primary" @click="reqSm2Sm4">访问testreqSm2Sm4</button>  
                </view>  
            </view>  

            <view class = "con">  
                <view class = "com">  
                    <text>跳转tab页</text>  
                </view>  

                <view>  
                    <input style="height: 50px; border:1px solid grey;margin: 5px 0;" v-model="addr" placeholder="输入跳转tabbar key"/>  
                    <button type="warn" @click="tz">跳转tab页</button>   
                </view>  
            </view>  

        </view>  

    </view>  

</template>  

<script setup lang="uts">  
    import { ref, computed } from 'vue'  
    import { stringToBase64, base64ToString, ReqApi } from "@/uni_modules/sunrains-request"  
    import { Response, FrontRequestVo } from "@/uni_modules/sunrains-common-type"  
    import { CustFailError, getComErrorInfo } from "@/uni_modules/sunrains-common-err"  
    import { pageToTab} from "@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts"  

   onMounted(() => {  
     console.log('【组件挂载====detail===】')  
   })  

   onUnmounted(() => {  
     console.log('【组件销毁====detail===】')  
   })  
    const addr = ref("")  
    function tz(){  
        if(addr.value.length<1){  
            uni.showToast({  
                title: '请输入跳转的tabbar key'  
            });  
        }else{  
            pageToTab(addr.value)  
        }  

    }  

    function fh(){  
        uni.navigateBack()  

    }  

    let priKey = "4a4a1d48fc284fe2fc43970641880554aed2f69fdfbea63257ea417af4e65802"  
    let pubKey = "0460e86263ec52b38a3da24c7a605055d14f8aa5c7855e4f4e19819f4f8a5b72181afd7aa4ab19255eb871eaedaba30fa18cd5c71693d3d76b030a04969bd1edfe"  
    const BASEURL = "http://192.168.1.9:8081"  
    type IRootType = {  
        name : string;  
        age : number;  
    }  

    type Builds = {  
        buildingNo : string,  
        sortNum : number,  
        buildAreaId : string,  
        id : string  
    }  

    async function req() {  
        let data : UTSJSONObject = {  
            name: "张三",  
            age: 18  
        }  
        try {  

            let vo : FrontRequestVo = {  
                method: 'GET',  
                url: BASEURL + "/test/get/cs1",  
                data: data,  
                header: null,  
                reqFlag: "void",  
                priKey: priKey,  
                pubKey: pubKey,  
                //method 为POST时 不能为null  
                contentType: "urlencoded",  
                timeout: null,  
                //当为上传文件时 文件路径参数不能为空  
                filePathParamName: null  
            }  
            const res = await ReqApi(vo)  
            console.log("####reqvoid==", res.data)  
            // console.log("####==",res)  
        } catch (err) {  
            uni.showToast({  
                title: err.message ?? "请求异常",  
                icon: "error"  
            })  
            console.log("-----", err)  

            return  
        };  
    }  

    //"sign"|"sm4Encrypt"|"signwithenSM2-Sm4Encrypt"|"void";  
    async function reqSm2() {  
        let data : UTSJSONObject = {  
            name: "张三",  
            age: 18  
        }  

        try {  
            let vo : FrontRequestVo = {  
                method: 'GET',  
                url: BASEURL + "/test/get/cs1",  
                data: data,  
                header: null,  
                reqFlag: "sign",  
                priKey: priKey,  
                pubKey: pubKey,  
                //method 为POST时 不能为null  
                contentType: "urlencoded",  
                timeout: null,  
                //当为上传文件时 文件路径参数不能为空  
                filePathParamName: null  
            }  

            const res = await ReqApi(vo)  
            console.log("####reqSm2==", res.data)  
        } catch (err) {  
            uni.showToast({  
                title: err.message ?? "请求异常",  
                icon: "error"  
            })  

            //#ifndef APP-ANDROID  
            const errMsg = (err as any).errMsg ?? "未知错误";  
            const errCode = (err as any).errCode ?? -1;  
            console.log('标准化错误码:', errMsg);  
            console.log('标准化错误信息:', errCode);  
            //#else   
            if (err instanceof UniError) {  
                let sa = err as UniError  
                console.log('sa==:', sa);  
            }  
            //#endif  
            console.log("-----", err)  
            return  
        };  
    }  

    async function reqSm4() {  
        let data : UTSJSONObject = {  
            name: "张三",  
            age: 18  
        }  
        try {  
            let vo : FrontRequestVo = {  
                method: 'POST',  
                url: BASEURL + "/test/post/cs1",  
                data: data,  
                header: null,  
                reqFlag: "sm4Encrypt",  
                priKey: priKey,  
                pubKey: pubKey,  
                //method 为POST时 不能为null  
                contentType: "urlencoded",  
                timeout: null,  
                //当为上传文件时 文件路径参数不能为空  
                filePathParamName: null  
            }  

            const res = await ReqApi(vo)  
            console.log("####reqSm4==", res.data)  
        } catch (err) {  
            console.log("####reqSm4 err==", err)  
            uni.showToast({  
                title: err.message??"",  
                icon: "error"  
            })  
            return  
        };  
    }  

    async function reqSm2Sm4() {  
        let data : UTSJSONObject = {  
            name: "张三",  
            age: 18  
        }  
        try {  
            let vo : FrontRequestVo = {  
                method: 'POST',  
                url: BASEURL + "/test/post/cs1",  
                data: data,  
                header: null,  
                reqFlag: "signwithenSM2-Sm4Encrypt",  
                priKey: priKey,  
                pubKey: pubKey,  
                //method 为POST时 不能为null  
                contentType: "urlencoded",  
                timeout: null,  
                //当为上传文件时 文件路径参数不能为空  
                filePathParamName: null  
            }  

            const res = await ReqApi(vo)  
            console.log("####reqSm2Sm4==", res.data)  
        } catch (err) {  
            console.log("-----", err)  
            return  
        };  
    }  

    const orderId = ref<string>('')  
    const orderStatus = ref<string>('')  
    const content = ref<string>('')  
    const sbase64 = computed(() => {  
        return stringToBase64(content.value)  
    })  

    const base64yostr = computed(() => {  
        return base64ToString(sbase64.value)  
    })  

    onLoad((options) => {  
        const opts = options as UTSJSONObject  
        const id = options?.["orderId"] as string | null  
        const status = options?.["status"] as string | null  

        if (id != null) {  
            orderId.value = id  
        }  
        if (status != null) {  
            orderStatus.value = status  
        }  
        console.log(`接收算法参数:ID=${orderId.value},状态=${orderStatus.value}`)  
    })  

    const goBack = () => {  
        uni.navigateBack()  
    }  

    function init(){  
              // uni.stopPullDownRefresh()  
              // console.log('stop onPullDownRefresh=====');  
    }  

     //  onPullDownRefresh(() => {  
     //    console.log('onPullDownRefresh=====');  
        // init()  

     //  })  

      function refresh(){  
          uni.removeStorageSync("LOGINTABBARITEMS")  
          // uni.startPullDownRefresh({  
             //  success:()=>{  
                //   console.log("1111")  
             //  }  
          // });  
          // init()  
      }  

</script>  

<style scoped>  
    .detail-root {  
/*      flex: 1;  
        display: flex;  
        flex-direction: column; */  
        background-color: #fff;  
    }  

    .status-bar {  
        height: 44px;  
        background-color: #ffffff;  
    }  

    .detail-header {  
        height: 44px;  
        background-color: #fff;  
        flex-direction: row;  
        align-items: center;  
        padding-left: 15px;  
        padding-right: 15px;  
        border-bottom-width: 1px;  
        border-bottom-style: solid;  
        border-bottom-color: #eee;  
    }  

    .back-icon {  
        font-size: 18px;  
        color: #007aff;  
        margin-right: 10px;  
    }  

    .detail-title {  
        font-size: 16px;  
        font-weight: bold;  
        color: #333;  
    }  

    .detail-content {  
        flex: 1;  
        padding: 20px;  
    }  

    .detail-item {  
        flex-direction: row;  
        margin-bottom: 20px;  
        align-items: center;  
    }  

    .label {  
        font-size: 15px;  
        color: #666;  
        width: 80px;  
    }  

    .value {  
        font-size: 15px;  
        color: #333;  
        font-weight: bold;  
    }  

    .detail-desc {  
        margin-top: 30px;  
    }  

    .com{  
        display:flex;  
        align-items: center;  
        margin:10px 0;   
    }  
    .con{  
        margin:10px 0;   
        padding:10px;  
        background-color: #e3eda1;  
    }  
    .btn{  
        margin: 5px;  
    }  
</style>  

8 拓展底部tabbar类型,(切换、弹出、进入)页面

    let mine = new TabItem(  
           'mine', //组件或页面唯一标识  
            "", //组件或页面路径  当类型为 page 时不可为空,其它传空字符串即可  
            12,  
            "red",  
            "yellow",   
            "bold",   
            '我的',   
            '/static/tab/main3.png',  
            '/static/tab/main3_active.png',  
            24,//图标宽  
            24,//图标高  
            "tabPage" //tabbar 类型 tabPage:tab页面  tabPop:弹窗组件  page:普通页面  
        )  

注:
类型为 tabPop 打开页面 可 右滑手势(当手机开启右滑手势时)关闭、 或点击虚拟返回按键关闭 或 页面中调用方法 closeTabPop()关闭

方法导入 import { pageToTab} from "@/uni_modules/sunrains-tabbar/utssdk/noticetab.uts"


⚠️ 常见问题

Q1: 为什么小程序端需要单独配置?

A: 微信小程序不支持动态组件渲染,必须静态声明所有可能使用的组件。

Q2: 动态 Tab 刷新失败怎么办?

A: 检查以下几点:

  • 组件是否在 tabbar-config.uts 中正确注册
  • 是否正确使用了 GLOBAL_COMPONENT_REGISTRY.get() 获取组件
  • TabItem 的参数是否完整且类型正确

Q3: 导航栏配置不生效?

A: 确保 setNavbarConfig() 在 setDefaultTabItems() 之前调用,或在应用启动时尽早调用。

Q4: 如何设置数字角标?

A: 使用 uni.$emit('setTabBadge', {"key": "tab_key", "badge": number}) 即可,详见第 4 章。

Q5: 角标设置后不显示怎么办?

A: 检查以下几点:

  • badge 值是否大于 0(0 或负数不显示)
  • tab key 是否正确(必须与 TabItem 的 key 一致)
  • 是否重新赋值了整个数组(直接修改对象属性不会触发响应式)

Q6: 退出登录如何清除 tabbar相关缓存

A: 调用 noticetab.uts里的 clearStorage 方法

/**  
 * 清除tabbar相关缓存  
 * all 所有  
 * theme 主题  
 * tabbarItems 所有tabbar  
 * badge 角标  
 */  
export function clearStorage(type:string){  
    switch (type){  
        case "theme":  
            uni.removeStorageSync("sunrains-tabbar-customtheme")      
            break;  
        case "tabbarItems":  
            uni.removeStorageSync("sunrains-tabbar-logintabbaritems")  
            break;    
        case "badge":  
            uni.removeStorageSync("sunrains-tabbar-tabbarbadges")  
            break;    
        default:  
            uni.removeStorageSync("sunrains-tabbar-customtheme")  
            uni.removeStorageSync("sunrains-tabbar-logintabbaritems")  
            uni.removeStorageSync("sunrains-tabbar-tabbarbadges")         
    }  

}

收起阅读 »