前段时间,我给自己定了一个看起来很简单的目标:做一款没有登录、没有复杂权限、打开就能用的生活计算工具。

于是,我开发了「换算盒子」。

它现在已经上架华为应用市场,首个版本包含单位换算、BMI 计算、日期计算和房贷计算四项功能。

如果你刚好需要这类工具,可以先下载体验:

华为应用市场:下载「换算盒子」

不过,这篇文章不只是想发一个下载链接。

我更想聊聊,我是怎么从芋道 uni-app 项目开始,删掉 6000 多行业务代码,把它改造成一款独立工具 App,以及我在鸿蒙真机上遇到“编译正常、安装正常,但 App 会异常卡死”之后,是怎么一点点排查的。

如果你也在使用 uni-app、Vue 3 或者正在尝试开发 HarmonyOS 应用,后面的踩坑过程应该会对你有帮助。

为什么我要做「换算盒子」?

我平时经常遇到一些很小、但又不得不查的问题:

  • 一英寸等于多少厘米?

  • 100 斤是多少千克?

  • 两个日期之间相差多少天?

  • 按现在的利率贷款,每个月大概要还多少钱?

  • 输入身高和体重后,BMI 属于哪个区间?

这些问题单独看都不复杂,搜索引擎也能找到答案。但我的真实体验是:每次都要重新搜索,进入不同网页,关掉弹窗,再从一堆内容里找到计算入口。

所以我想做一个简单一点的工具:常用功能集中在一起,不需要注册账号,不索取与计算无关的权限,打开后输入数字就能得到结果。

我给它取名叫「换算盒子」。

目前,它包含四类工具:

  1. 单位换算:长度、重量、面积、体积和温度;

  2. BMI 计算:输入身高、体重后计算 BMI 和对应区间;

  3. 日期计算:计算日期间隔,或者向前、向后推算日期;

  4. 房贷计算:支持等额本息和等额本金,展示月供、总利息和还款总额。

我没有在第一个版本里塞进几十种功能。相比“大而全”,我更希望先把几个真正高频的功能做得简单、稳定、容易使用。

我以为最难的是计算公式,结果是删代码

「换算盒子」并不是从一个空项目开始的。

我使用的是芋道 uni-app 项目框架,技术栈包括 Vue 3、TypeScript、Vite、Wot Design Uni、UnoCSS 和 Pinia。成熟框架帮我搭好了目录结构、构建配置和跨端开发基础,但原项目里还有课程表、GPA、随机点名、倒计时、校园信息、备忘清单等业务。

这些功能与「换算盒子」的定位完全不同。

我做的第一件事不是继续增加页面,而是做减法。

从 Git 记录来看,这次改造涉及 63 个文件:我新增了大约 1151 行代码,同时删除了约 6210 行代码。

删掉 6000 多行代码以后,项目反而变得更清楚了:

  • 首页只保留四个工具入口;

  • 删除与计算无关的页面、状态和资源;

  • 把核心公式集中到独立工具文件;

  • 精简 Android 权限;

  • 去掉启动阶段不必要的业务逻辑;

  • 重新配置应用名称、包名、图标和鸿蒙构建信息。

这次经历让我意识到,使用成熟框架并不等于把模板中的所有功能都带进自己的 App。

模板解决的是工程问题,产品最终要解决的是用户问题。与目标无关的代码保留得越多,后续维护和排查问题的范围就越大。

四类计算功能,我是怎么组织代码的?

我把单位换算、BMI、日期和房贷计算统一放在工具层,页面只负责接收输入和展示结果。

例如普通单位换算,我会先把输入值转换为当前分类的基准单位,再从基准单位转换成目标单位。温度换算不是简单的倍数关系,所以我把它放进独立分支处理。



/** 执行普通单位或温度单位换算,并返回格式化后的文本结果。 */ export function convertUnit( input: number, categoryValue: string, fromValue: string, toValue: string, ): string { const category = getUnitCategory(categoryValue) const fromUnit = getUnitOption(category, fromValue) const toUnit = getUnitOption(category, toValue) if (category.type === 'temperature') { return formatNumber(convertTemperature(input, fromUnit.value, toUnit.value)) } const baseValue = input * fromUnit.factor return formatNumber(baseValue / toUnit.factor) }

这种写法不算高深,但对工具类 App 很合适。

以后增加新的长度或重量单位时,我只需要补充单位配置,不需要重新修改整个页面。计算逻辑与界面分开后,我也更容易验证公式和边界条件。

最让我头疼的问题:鸿蒙真机异常卡死

功能完成以后,我原本以为接下来就是打包和上架。

结果在真机测试阶段,App 出现了异常卡死。

最让人难受的是:项目可以编译,应用也可以安装,页面代码看起来没有明显错误,但运行起来就是不稳定。

我一开始也怀疑过很多方向:

  • 是不是 Vue 响应式数据出现了循环更新?

  • 是不是某个组件在首屏反复渲染?

  • 是不是 Pinia 持久化状态出了问题?

  • 是不是分包页面加载失败?

  • 是不是用户连续点击造成页面栈异常?

如果同时修改所有可能相关的代码,即使问题暂时消失,我也无法判断到底是哪一步发挥了作用。

所以,我采用了一个比较笨、但很有效的方法:先把项目缩减到最小可运行状态,再逐项恢复功能。

第一步:尽量减少启动阶段的工作

我先检查了 App.vue 和 main.ts。

原来的应用启动阶段注册了多个生命周期,并且会在创建应用时注册全局 Store。这些代码单独看都很正常,但排查启动卡死时,变量越少越好。

于是,我暂时移除非必要的启动逻辑,让应用入口只负责创建 Vue 实例和首屏渲染:



/** 创建应用实例,并保持启动阶段只执行首屏渲染所需的最少工作。 */ export function createApp() { const app = createSSRApp(App) return { app } }

这样做并不是因为 Pinia 或生命周期一定有问题,而是为了建立一个明确的最小运行边界。

如果最简单的启动入口仍然会卡死,那我就应该把注意力转向编译结果、页面宿主和构建插件,而不是继续在业务代码里盲目搜索。

第二步:阻止连续点击叠加原生导航任务

「换算盒子」首页是一个工具宫格,点击卡片后通过 uni.navigateTo 进入对应的分包页面。

在页面第一次加载时,如果用户连续点击同一个入口,就可能在上一条导航任务尚未结束时,再次提交新的导航任务。

H5 对这种操作可能不太敏感,但到了原生页面栈中,多次导航任务叠加可能带来更明显的问题。

我给首页导航加了一把简单的锁:



let isNavigating = false /** 在页面切换完成后释放导航锁。 */ function releaseNavigationLock() { isNavigating = false } /** 打开用户选择的工具页面,并阻止连续点击叠加原生导航任务。 */ function openTool(path: string) { if (isNavigating) return isNavigating = true uni.navigateTo({ url: path, complete: releaseNavigationLock, }) }

这段代码很短,却帮我排除了连续点击带来的不确定性。

如果你的 uni-app 项目在 H5 正常,但真机连续点击后容易卡住,可以检查是否在第一次跳转完成前触发了第二次 navigateTo。

第三步:按平台隔离 Vite 构建插件

继续排查后,我把注意力放到了 vite.config.ts。

项目使用了 @uni-ku/bundle-optimizer 处理分包优化和异步跨包调用,也使用了 @uni-ku/root 处理根容器。这些插件在其他目标平台上有实际作用,但到了 app-harmony,构建阶段对分包和根节点的改写可能影响鸿蒙页面宿主的首次初始化。

我的解决思路不是直接删除插件,而是让它们不要进入 HarmonyOS 的构建链:



// HarmonyOS 保留官方分包行为,避免第三方跨包改写影响页面宿主初始化。 UNI_PLATFORM !== 'app-harmony' && Optimization({ enable: { optimization: true, 'async-import': true, 'async-component': true, }, }) // HarmonyOS 使用 uni-app 官方根容器。 UNI_PLATFORM !== 'app-harmony' && UniKuRoot({ excludePages: ['**/components/**/**.*'], })

这个问题给我的教训非常直接:跨端框架可以复用大部分业务代码,但不代表所有构建插件都能在每个平台上无条件复用。

特别是会改变页面结构、模块加载方式、根节点或者分包行为的插件,在接入新平台时最好逐一验证。

第四步:uni-app 相关依赖要整组升级

除了隔离构建插件,我还统一升级了项目中的 @dcloudio 相关依赖。

我没有只修改 @dcloudio/uni-app,而是让 uni-app-harmony、uni-app-plus、各平台适配包、vite-plugin-uni 和 uni-cli-shared 保持在同一版本批次,同时固定 Vue 与 @vue/runtime-core 的版本。

uni-app 的运行时、编译器、Vite 插件和平台包之间存在关联。如果只升级其中一个包,虽然可能继续编译,但也可能产生更隐蔽的兼容问题。

我现在处理这类升级时,会坚持几件事:

  1. 核心包和平台包尽量使用同一发行批次;

  2. 不只修改 package.json,还要重新检查锁文件;

  3. 每次升级前保留可以回退的 Git 提交;

  4. H5 编译成功后,继续在真正要发布的平台上测试;

  5. 构建成功不等于运行稳定,必须以真机结果为准。

从 Git 提交时间来看,精简启动逻辑、增加导航锁、升级依赖和隔离鸿蒙构建插件是在相近阶段完成的。

所以我不能武断地说,某一行代码就是卡死问题的唯一根因。更准确的说法是:我通过减少启动变量、避免重复导航、统一依赖版本和隔离平台插件,最终组成了一套稳定方案。

为什么我坚持不登录、少权限和本地计算?

我做「换算盒子」时,有一个从开始就没有改变的想法:用户只是想算一个结果,不应该先交出一堆个人信息。

目前这四项功能都可以在本地完成,所以我没有增加账号系统,也没有为了简单计算去申请定位、通讯录、相机等权限。

我还把 Android 权限列表尽量精简,并关闭了不必要的统计功能。

这样做对我来说有三个好处:

  • 用户打开 App 就能直接使用;

  • 数据和隐私边界更容易解释;

  • 少一些 SDK 和权限,也能减少跨系统版本的兼容问题。

当然,本地计算不等于随便套公式。

房贷计算要区分等额本金和等额本息,日期要处理不同方向和格式,温度不是简单的倍数换算,数值展示还要处理浮点数精度。这些细节都比页面看上去更容易出错。

从开发到上架,我得到的几个经验

做完第一个版本后,我总结了几条对自己很有用的经验。

第一,使用成熟框架时,我要先判断哪些能力真正属于自己的产品。删掉无关代码并不是浪费,而是在缩小未来的维护成本。

第二,我不能把 H5 正常当成跨端项目已经完成。TypeScript 没有报错、构建命令执行成功,也不代表鸿蒙真机一定稳定。

第三,遇到卡死时,我应该先构造最小可运行版本。每次只恢复一个插件或一项初始化逻辑,比同时修改十几处配置更容易找到方向。

第四,我需要尊重平台差异。跨端复用的是业务能力,不是忽略底层平台的不同。

第五,我更愿意把一个小功能做得直接好用,而不是为了看起来丰富,在首版里堆几十个半成品入口。

最后,认真推荐一下我做的「换算盒子」

写了这么多开发过程,最后还是想认真推荐一下这款 App。

如果你平时需要进行单位换算、计算 BMI、推算日期或者估算房贷,可以试试「换算盒子」。它目前的功能不算多,但我尽量让每一个入口都足够直接:不用注册,少权限,打开就能计算。

下载地址:

点击前往华为应用市场下载「换算盒子」

如果你下载体验后觉得它确实帮你节省了一点时间,希望你能回到华为应用市场,给「换算盒子」留下一个真实评分和使用评价。

对于一款刚上架、几乎没有自然曝光的新 App 来说,每一次下载、评分和反馈,都会帮助它被更多真正需要的人看到。

如果你觉得哪里不好用,也欢迎直接在评论区告诉我。我会继续修复问题,并逐步增加个税、汇率以及更多常用换算工具。

最后也想问问大家:如果只能给「换算盒子」增加一个新功能,你最希望是个税计算、汇率换算,还是其他工具?

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐